From 8da077c33f67a92b044539e385867f7ac0e3fd38 Mon Sep 17 00:00:00 2001
From: RuslanUC <dev_ruslan_uc@protonmail.com>
Date: Sat, 24 Feb 2024 18:47:57 +0200
Subject: [PATCH 1/2] add remote auth v2

---
 tests/api/test_remote_auth.py                 |  67 ++++++-
 tests/api/utils.py                            |  13 +-
 yepcord/remote_auth/gateway.py                | 183 +++++++++++-------
 yepcord/remote_auth/main.py                   |  17 +-
 yepcord/rest_api/models/users_me.py           |   7 +-
 yepcord/rest_api/routes/users_me.py           |  60 ++++--
 yepcord/yepcord/models/remote_auth_session.py |   3 +
 7 files changed, 240 insertions(+), 110 deletions(-)

diff --git a/tests/api/test_remote_auth.py b/tests/api/test_remote_auth.py
index 47b7748..d89fba7 100644
--- a/tests/api/test_remote_auth.py
+++ b/tests/api/test_remote_auth.py
@@ -61,6 +61,60 @@ async def on_token(token: str) -> None:
     assert not cl.results["cancel"]
 
 
+@pt.mark.asyncio
+async def test_remote_auth_v2_success():
+    client: TestClientType = app.test_client()
+    ra_client: TestClientType = ra_app.test_client()
+    user = (await create_users(client, 1))[0]
+    headers = {"Authorization": user["token"]}
+    state = {"fingerprint": None, "handshake_token": None}
+
+    async def on_fp(fp: str) -> None:
+        resp = await client.post("/api/v9/users/@me/remote-auth/login", headers=headers, json={"fingerprint": fp})
+        assert resp.status_code == 200
+        json = await resp.get_json()
+        assert "handshake_token" in json
+        state.update({"fingerprint": fp, "handshake_token": json["handshake_token"]})
+
+    async def on_userdata(userdata: str) -> None:
+        uid, disc, avatar, username = userdata.split(":")
+
+        assert uid == user["id"]
+        assert disc == user["discriminator"]
+        assert username == user["username"]
+
+        resp = await client.post("/api/v9/users/@me/remote-auth/finish", headers=headers, json={
+            "handshake_token": state["handshake_token"], "temporary_token": False
+        })
+        assert resp.status_code == 204
+
+    async def on_pending_login(ticket: str) -> None:
+        resp = await client.post("/api/v9/users/@me/remote-auth/login",
+                                 json={"ticket": "".join(ticket.split(".")[:-1])})
+        assert resp.status_code == 404
+
+        resp = await client.post("/api/v9/users/@me/remote-auth/login", json={"ticket": ticket})
+        assert resp.status_code == 200
+        json = await resp.get_json()
+        assert "encrypted_token" in json
+        token = cl.decrypt(json["encrypted_token"]).decode("utf-8")
+
+        resp = await client.post("/api/v9/users/@me/remote-auth/login", json={"ticket": ticket})
+        assert resp.status_code == 404
+
+        resp = await client.get("/api/v9/users/@me", headers={"Authorization": token})
+        assert resp.status_code == 200
+        json = await resp.get_json()
+        assert json["id"] == user["id"]
+
+    cl = RemoteAuthClient(on_fp, on_userdata, None, None, on_pending_login)
+
+    async with ra_client.websocket('/?v=2') as ws:
+        await cl.run(ws)
+
+    assert not cl.results["cancel"]
+
+
 @pt.mark.asyncio
 async def test_remote_auth_cancel():
     client: TestClientType = app.test_client()
@@ -130,6 +184,9 @@ async def test_remote_auth_unknown_fp_and_token():
     })
     assert resp.status_code == 404
 
+    resp = await client.post("/api/v9/users/@me/remote-auth/login", json={})
+    assert resp.status_code == 404
+
 
 @pt.mark.asyncio
 async def test_remote_auth_same_keys():
@@ -165,11 +222,11 @@ async def test_remote_auth_without_init():
         with pt.raises(WebsocketDisconnectError):
             await ws.receive_json()
 
-    async with ra_client.websocket('/') as ws:
-        await ws.receive_json()
-        await ws.send_json({"op": "heartbeat"})
-        with pt.raises(WebsocketDisconnectError):
-            await ws.receive_json()
+    #async with ra_client.websocket('/') as ws:
+    #    await ws.receive_json()
+    #    await ws.send_json({"op": "heartbeat"})
+    #    with pt.raises(WebsocketDisconnectError):
+    #        await ws.receive_json()
 
 
 @pt.mark.asyncio
diff --git a/tests/api/utils.py b/tests/api/utils.py
index 2bfd1fd..d9a4e44 100644
--- a/tests/api/utils.py
+++ b/tests/api/utils.py
@@ -251,7 +251,7 @@ def generate_slash_command_payload(application: dict, guild: dict, channel: dict
 
 
 class RemoteAuthClient:
-    def __init__(self, on_fingerprint=None, on_userdata=None, on_token=None, on_cancel=None):
+    def __init__(self, on_fingerprint=None, on_userdata=None, on_token=None, on_cancel=None, on_pending_login=None):
         from cryptography.hazmat.primitives.asymmetric import rsa
 
         self.privKey: Optional[rsa.RSAPrivateKey] = None
@@ -264,11 +264,13 @@ def __init__(self, on_fingerprint=None, on_userdata=None, on_token=None, on_canc
         self.on_userdata = on_userdata
         self.on_token = on_token
         self.on_cancel = on_cancel
+        self.on_pending_login = on_pending_login
 
         self.results: dict[str, Union[Optional[str], bool]] = {
             "fingerprint": None,
             "userdata": None,
             "token": None,
+            "ticket": None,
             "cancel": False,
         }
 
@@ -277,7 +279,9 @@ def __init__(self, on_fingerprint=None, on_userdata=None, on_token=None, on_canc
             "nonce_proof": self.handle_nonce_proof,
             "pending_remote_init": self.handle_pending_remote_init,
             "pending_finish": self.handle_pending_finish,
+            "pending_ticket": self.handle_pending_finish,
             "finish": self.handle_finish,
+            "pending_login": self.handle_pending_login,
             "cancel": self.handle_cancel,
         }
 
@@ -335,6 +339,11 @@ async def handle_finish(self, ws: TestWebsocketConnectionProtocol, msg: dict) ->
         self.results["token"] = token
         if self.on_token is not None: await self.on_token(token)
 
+    async def handle_pending_login(self, ws: TestWebsocketConnectionProtocol, msg: dict) -> None:
+        ticket = msg["ticket"]
+        self.results["ticket"] = ticket
+        if self.on_pending_login is not None: await self.on_pending_login(ticket)
+
     async def handle_cancel(self, ws: TestWebsocketConnectionProtocol, msg: dict) -> None:
         self.results["cancel"] = True
         if self.on_cancel is not None: await self.on_cancel()
@@ -346,7 +355,7 @@ async def run(self, ws: TestWebsocketConnectionProtocol) -> None:
             if msg["op"] not in self.handlers: continue
             handler = self.handlers[msg["op"]]
             await handler(ws, msg)
-            if msg["op"] in {"finish", "cancel"}:
+            if msg["op"] in {"finish", "cancel", "pending_login"}:
                 break
 
         if self.heartbeatTask is not None:
diff --git a/yepcord/remote_auth/gateway.py b/yepcord/remote_auth/gateway.py
index 473d417..c28d209 100644
--- a/yepcord/remote_auth/gateway.py
+++ b/yepcord/remote_auth/gateway.py
@@ -21,12 +21,14 @@
 from hashlib import sha256
 from os import urandom
 from time import time
-from typing import Any
+from typing import Optional
 
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1
+from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
 from cryptography.hazmat.primitives.hashes import SHA256
 from cryptography.hazmat.primitives.serialization import load_pem_public_key
+from quart import Websocket
 
 from ..yepcord.models import RemoteAuthSession
 from ..yepcord.mq_broker import getBroker
@@ -34,40 +36,98 @@
 
 
 class GatewayClient:
-    def __init__(self, ws, pubkey: str, fp: str, nonce: bytes):
+    __slots__ = ["ws", "pubkey", "fingerprint", "nonce", "connect_time", "last_heartbeat", "connected", "version", "gw"]
+
+    def __init__(self, ws: Websocket, version: int, gateway):
         self.ws = ws
-        self.pubkey = load_pem_public_key(
-            f"-----BEGIN PUBLIC KEY-----\n{pubkey}\n-----END PUBLIC KEY-----\n".encode("utf8"),
-            backend=default_backend()
-        )
-        self.fingerprint = fp
-        self.nonce = nonce
-        nonceHash = sha256()
-        nonceHash.update(nonce)
-        self.nonceHash = nonceHash.digest()
-        self.cTime = time()
-        self.lastHeartbeat = time()
+        self.nonce = urandom(32)
+        self.connect_time = time()
+        self.last_heartbeat = time()
+        self.connected = True
+        self.version = version
+        self.gw = gateway
+
+        self.pubkey: Optional[RSAPublicKey] = None
+        self.fingerprint: Optional[str] = None
 
     def encrypt(self, data: bytes):
         return self.pubkey.encrypt(data, OAEP(mgf=MGF1(algorithm=SHA256()), algorithm=SHA256(), label=None))
 
+    async def send(self, op: str, **data) -> None:
+        if not self.connected:  # pragma: no cover
+            return
+        await self.ws.send_json({"op": op, **data})
+
     async def check_timeout(self, _task=False):
         if not _task:
             return get_event_loop().create_task(self.check_timeout(True))
-        while self.ws.connected:  # pragma: no cover
-            if time()-self.cTime > 150:
+        while self.connected:  # pragma: no cover
+            if time()-self.connect_time > 150:
                 await self.ws.close(4003)
                 break
-            if time()-self.lastHeartbeat > 50:
+            if time()-self.last_heartbeat > 50:
                 await self.ws.close(4004)
                 break
             await asleep(5)
 
+    async def handle_init(self, data: dict) -> None:
+        pubkey = data["encoded_public_key"]
+        fingerprint = sha256(_b64decode(pubkey.encode("utf8"))).digest()
+        fingerprint = self.fingerprint = b64encode(fingerprint)
+
+        if not self.gw.init_client(self, fingerprint):
+            return await self.ws.close(1001)
+
+        self.pubkey = load_pem_public_key(
+            f"-----BEGIN PUBLIC KEY-----\n{pubkey}\n-----END PUBLIC KEY-----\n".encode("utf8"),
+            backend=default_backend()
+        )
+
+        await RemoteAuthSession.create(fingerprint=fingerprint, version=self.version)
+        encrypted_nonce = _b64encode(self.encrypt(self.nonce)).decode("utf8")
+        await self.send("nonce_proof", encrypted_nonce=encrypted_nonce)
+        await self.check_timeout()
+
+    async def handle_nonce_proof(self, data: dict) -> None:
+        proof = b64decode(data["proof"])
+        if proof != sha256(self.nonce).digest():
+            return await self.ws.close(1001)
+        await self.send("pending_remote_init", fingerprint=self.fingerprint)
+
+    async def handle_heartbeat(self, data: dict) -> None:
+        self.last_heartbeat = time()
+        await self.send("heartbeat_ack")
+
+    async def send_pending_finish(self, userdata: str) -> None:
+        # userdata = id : discriminator : avatar : username
+        data = _b64encode(self.encrypt(userdata.encode("utf8"))).decode("utf8")
+        await self.send("pending_finish" if self.version == 1 else "pending_ticket", encrypted_user_payload=data)
+
+    async def send_finish_v1(self, token: str) -> None:
+        data = _b64encode(self.encrypt(token.encode("utf8"))).decode("utf8")
+        await self.send("finish", encrypted_token=data)
+        await self.ws.close(1000)
+
+    async def send_finish_v2(self, token: str) -> None:
+        enc_token = _b64encode(self.encrypt(token.encode("utf8"))).decode("utf8")
+        await RemoteAuthSession.filter(fingerprint=self.fingerprint).update(v2_encrypted_token=enc_token)
+        token = ".".join(token.split(".")[:-1])
+        await self.send("pending_login", ticket=f"{token}.{self.fingerprint}")
+        await self.ws.close(1000)
+
+    async def send_finish(self, token: str) -> None:
+        if self.version == 1:
+            return await self.send_finish_v1(token)
+        await self.send_finish_v2(token)
+
+    async def send_cancel(self) -> None:
+        await self.send("cancel")
+        await self.ws.close(1000)
+
 
 class Gateway:
     def __init__(self):
         self.clients_by_fingerprint: dict[str, GatewayClient] = {}
-        self.clients_by_socket: dict[Any, GatewayClient] = {}
         self.broker = getBroker()
         self.broker.handle("yepcord_remote_auth")(self.mq_callback)
 
@@ -78,71 +138,44 @@ async def stop(self):
         await self.broker.close()
 
     async def mq_callback(self, body: dict) -> None:
+        if body["op"] not in {"pending_finish", "finish", "cancel"}:  # pragma: no cover
+            return
+        if not (client := self.clients_by_fingerprint.get(body["fingerprint"])):  # pragma: no cover
+            return
+
         if body["op"] == "pending_finish":
-            await self.sendPendingFinish(body["fingerprint"], body["userdata"])
+            await client.send_pending_finish(body["userdata"])
         elif body["op"] == "finish":
-            await self.sendFinishV1(body["fingerprint"], body["token"])
+            await client.send_finish(body["token"])
         elif body["op"] == "cancel":
-            await self.sendCancel(body["fingerprint"])
+            await client.send_cancel()
 
-    # noinspection PyMethodMayBeStatic
-    async def send(self, ws, op: str, **data) -> None:
-        await ws.send_json({"op": op, **data})
+    def init_client(self, client: GatewayClient, fingerprint: str) -> bool:
+        if fingerprint in self.clients_by_fingerprint:
+            return False
+        self.clients_by_fingerprint[fingerprint] = client
+        return True
+
+    async def process(self, ws: Websocket, data: dict) -> None:
+        if (client := getattr(ws, "_yepcord_client", None)) is None:  # pragma: no cover
+            return await ws.close(1001)
 
-    async def process(self, ws, data: dict) -> None:
         op = data["op"]
-        if op == "init":
-            pubkey = data["encoded_public_key"]
-            s = sha256()
-            s.update(_b64decode(pubkey.encode("utf8")))
-            fingerprint = b64encode(s.digest()).replace("=", "")
-            if self.clients_by_fingerprint.get(fingerprint):
-                return await ws.close(1001)
-            nonce = urandom(32)
-            cl = GatewayClient(ws, pubkey, fingerprint, nonce)
-            self.clients_by_fingerprint[fingerprint] = self.clients_by_socket[ws] = cl
-            await RemoteAuthSession.create(fingerprint=fingerprint)
-            encrypted_nonce = _b64encode(cl.encrypt(nonce)).decode("utf8")
-            await self.send(ws, "nonce_proof", encrypted_nonce=encrypted_nonce)
-            await cl.check_timeout()
-        elif op == "nonce_proof":
-            if not (client := self.clients_by_socket.get(ws)):
-                return await ws.close(1001)
-            proof = b64decode(data["proof"])
-            if proof != client.nonceHash:
-                return await ws.close(1001)
-            await self.send(ws, "pending_remote_init", fingerprint=client.fingerprint)
-        elif op == "heartbeat":
-            if not (client := self.clients_by_socket.get(ws)):
-                return await ws.close(1001)
-            client.lastHeartbeat = time()
-            await self.send(ws, "heartbeat_ack")
-
-    async def sendHello(self, ws) -> None:
-        await self.send(ws, "hello", heartbeat_interval=41500, timeout_ms=150*1000)
-
-    async def sendPendingFinish(self, fingerprint: str, userdata: str) -> None:
-        # userdata = id : discriminator : avatar : username
-        if not (client := self.clients_by_fingerprint.get(fingerprint)):  # pragma: no cover
-            return
-        data = _b64encode(client.encrypt(userdata.encode("utf8"))).decode("utf8")
-        await self.send(client.ws, "pending_finish", encrypted_user_payload=data)
+        if (func := getattr(client, f"handle_{op}", None)) is None:  # pragma: no cover
+            return await ws.close(1001)
 
-    async def sendFinishV1(self, fingerprint: str, token: str) -> None:
-        if not (client := self.clients_by_fingerprint.get(fingerprint)):  # pragma: no cover
-            return
-        data = _b64encode(client.encrypt(token.encode("utf8"))).decode("utf8")
-        await self.send(client.ws, "finish", encrypted_token=data)
-        await client.ws.close(1000)
+        await func(data)
 
-    async def sendCancel(self, fingerprint: str) -> None:
-        if not (client := self.clients_by_fingerprint.get(fingerprint)):  # pragma: no cover
-            return
-        await self.send(client.ws, "cancel")
-        await client.ws.close(1000)
+    async def connect(self, ws: Websocket, version: int) -> None:
+        client = GatewayClient(ws, version, self)
+        setattr(ws, "_yepcord_client", client)
+        await client.send("hello", heartbeat_interval=41500, timeout_ms=150*1000)
 
-    async def disconnect(self, ws) -> None:
-        if not (client := self.clients_by_socket.get(ws)):
+    async def disconnect(self, ws: Websocket):
+        client: GatewayClient
+        if (client := getattr(ws, "_yepcord_client", None)) is None:  # pragma: no cover
             return
-
-        await RemoteAuthSession.filter(fingerprint=client.fingerprint).delete()
+        client.connected = False
+        if client.fingerprint in self.clients_by_fingerprint:
+            del self.clients_by_fingerprint[client.fingerprint]
+            await RemoteAuthSession.filter(fingerprint=client.fingerprint).delete()
diff --git a/yepcord/remote_auth/main.py b/yepcord/remote_auth/main.py
index 2d201e9..31948d2 100644
--- a/yepcord/remote_auth/main.py
+++ b/yepcord/remote_auth/main.py
@@ -15,11 +15,9 @@
     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-
 from asyncio import CancelledError
-from json import loads as jloads
 
-from quart import Quart, websocket
+from quart import Quart, websocket, Websocket
 from tortoise.contrib.quart import register_tortoise
 
 from .gateway import Gateway
@@ -56,16 +54,17 @@ async def set_cors_headers(response):  # pragma: no cover
 
 @app.websocket("/")
 async def ws_gateway():
+    version = websocket.args.get("v", "")
+    version = int(version) if version.isdigit() else 1
+
     # noinspection PyProtectedMember,PyUnresolvedReferences
-    ws = websocket._get_current_object()
-    setattr(ws, "connected", True)
-    await gw.sendHello(ws)
+    ws: Websocket = websocket._get_current_object()
+    await gw.connect(ws, version)
     while True:
         try:
-            data = await ws.receive()
-            await gw.process(ws, jloads(data))
+            data = await ws.receive_json()
+            await gw.process(ws, data)
         except CancelledError:
-            setattr(ws, "connected", False)
             await gw.disconnect(ws)
             raise
 
diff --git a/yepcord/rest_api/models/users_me.py b/yepcord/rest_api/models/users_me.py
index d8322f8..131a42c 100644
--- a/yepcord/rest_api/models/users_me.py
+++ b/yepcord/rest_api/models/users_me.py
@@ -159,10 +159,13 @@ def __init__(self, **data):
 
 # noinspection PyMethodParameters
 class RemoteAuthLogin(BaseModel):
-    fingerprint: str
+    fingerprint: Optional[str] = None
+    ticket: Optional[str] = None
 
     @field_validator("fingerprint")
-    def validate_fingerprint(cls, value: str) -> str:
+    def validate_fingerprint(cls, value: Optional[str]) -> Optional[str]:
+        if value is None:
+            return
         try:
             assert len(b64decode(value)) == 32
         except (ValueError, AssertionError):
diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py
index fe6e8cc..c7642f8 100644
--- a/yepcord/rest_api/routes/users_me.py
+++ b/yepcord/rest_api/routes/users_me.py
@@ -19,9 +19,9 @@
 from base64 import b64encode as _b64encode, b64decode as _b64decode
 from random import choice
 from time import time
-from typing import Union
+from typing import Union, Optional
 
-from ..dependencies import DepUser, DepSession, DepGuildMember, DepGuild
+from ..dependencies import DepUser, DepSession, DepGuildMember, DepGuild, DepUserO
 from ..models.users_me import UserUpdate, UserProfileUpdate, ConsentSettingsUpdate, SettingsUpdate, PutNote, \
     RelationshipRequest, SettingsProtoUpdate, MfaEnable, MfaDisable, MfaCodesVerification, RelationshipPut, \
     DmChannelCreate, DeleteRequest, GetScheduledEventsQuery, RemoteAuthLogin, RemoteAuthFinish, RemoteAuthCancel
@@ -34,6 +34,7 @@
 from ...yepcord.errors import InvalidDataErr, Errors
 from ...yepcord.models import User, UserSettingsProto, FrecencySettings, UserNote, Session, UserData, Guild, \
     GuildMember, RemoteAuthSession, Relationship, Authorization, Bot
+from ...yepcord.models.remote_auth_session import time_plus_150s
 from ...yepcord.proto import FrecencyUserSettings, PreloadedUserSettings
 from ...yepcord.utils import execute_after, validImage, getImage
 
@@ -409,33 +410,58 @@ async def get_scheduled_events(query_args: GetScheduledEventsQuery, user: User =
 
 
 @users_me.post("/remote-auth/login", body_cls=RemoteAuthLogin)
-async def remote_auth_login(data: RemoteAuthLogin, user: User = DepUser):
-    ra_session = await RemoteAuthSession.get_or_none(fingerprint=data.fingerprint, user=None,
-                                                     expires_at__gt=int(time()))
-    if ra_session is None:
-        raise InvalidDataErr(404, Errors.make(10012))
+async def remote_auth_login(data: RemoteAuthLogin, user: Optional[User] = DepUserO):
+    if data.fingerprint is not None and user is not None:
+        ra_session = await RemoteAuthSession.get_or_none(fingerprint=data.fingerprint, user=None,
+                                                         expires_at__gt=int(time()))
+        if ra_session is None:
+            raise InvalidDataErr(404, Errors.make(10012))
 
-    await ra_session.update(user=user)
-    userdata = await user.userdata
-    avatar = userdata.avatar if userdata.avatar else ""
-    await getGw().dispatchRA("pending_finish", {
-        "fingerprint": data.fingerprint,
-        "userdata": f"{user.id}:{userdata.s_discriminator}:{avatar}:{userdata.username}"
-    })
+        await ra_session.update(user=user)
+        userdata = await user.userdata
+        avatar = userdata.avatar if userdata.avatar else ""
+        await getGw().dispatchRA("pending_finish", {
+            "fingerprint": data.fingerprint,
+            "userdata": f"{user.id}:{userdata.s_discriminator}:{avatar}:{userdata.username}"
+        })
+
+        return {"handshake_token": str(ra_session.id)}
+    elif data.ticket is not None:
+        ticket = Session.extract_token(data.ticket)
+        if ticket is None:
+            raise InvalidDataErr(404, Errors.make(10012))
+
+        user_id, session_id, fingerprint = ticket
+
+        ra_session = await RemoteAuthSession.get_or_none(
+            fingerprint=fingerprint, expires_at__gt=int(time()), v2_session__id=session_id, user__id=user_id
+        )
+        if ra_session is None:
+            raise InvalidDataErr(404, Errors.make(10012))
 
-    return {"handshake_token": str(ra_session.id)}
+        await ra_session.delete()
+
+        return {"encrypted_token": ra_session.v2_encrypted_token}
+
+    raise InvalidDataErr(404, Errors.make(10012))
 
 
 @users_me.post("/remote-auth/finish", body_cls=RemoteAuthFinish)
 async def remote_auth_finish(data: RemoteAuthFinish, user: User = DepUser):
     ra_session = await RemoteAuthSession.get_or_none(id=int(data.handshake_token), expires_at__gt=int(time()),
-                                                     user=user)
+                                                     user=user, v2_session=None)
     if ra_session is None:
         raise InvalidDataErr(404, Errors.make(10012))
 
+    session = await getCore().createSession(user)
+    if ra_session.version == 2:
+        await ra_session.update(v2_session=session, expires_at=time_plus_150s())
+    else:
+        await ra_session.delete()
+
     await getGw().dispatchRA("finish", {
         "fingerprint": ra_session.fingerprint,
-        "token": (await getCore().createSession(user)).token
+        "token": session.token
     })
 
     return "", 204
diff --git a/yepcord/yepcord/models/remote_auth_session.py b/yepcord/yepcord/models/remote_auth_session.py
index a44ead3..169ca45 100644
--- a/yepcord/yepcord/models/remote_auth_session.py
+++ b/yepcord/yepcord/models/remote_auth_session.py
@@ -34,3 +34,6 @@ class RemoteAuthSession(Model):
     fingerprint: str = fields.CharField(max_length=64, unique=True)
     user: Optional[models.User] = fields.ForeignKeyField("models.User", null=True, default=None)
     expires_at: int = fields.IntField(default=time_plus_150s)
+    version: int = fields.SmallIntField(default=1)
+    v2_session: Optional[models.Session] = fields.ForeignKeyField("models.Session", null=True, default=None)
+    v2_encrypted_token: Optional[str] = fields.TextField(null=True, default=None)

From 28ff748ef60ea939b610e3e647480da7616a4126 Mon Sep 17 00:00:00 2001
From: RuslanUC <dev_ruslan_uc@protonmail.com>
Date: Sat, 24 Feb 2024 19:12:39 +0200
Subject: [PATCH 2/2] add remote auth v2 to features list

---
 STATUS.md                        | 54 ++++++++++++++++++++++++++++++++
 yepcord/rest_api/routes/other.py |  1 +
 2 files changed, 55 insertions(+)
 create mode 100644 STATUS.md

diff --git a/STATUS.md b/STATUS.md
new file mode 100644
index 0000000..54c8ad2
--- /dev/null
+++ b/STATUS.md
@@ -0,0 +1,54 @@
+# What is working?
+
+  - [x] Auth:
+    - [x] Login
+    - [x] Register
+    - [x] Two-factor authentication
+    - [x] Remote Auth (via qr-code):
+      - [x] V1
+      - [x] V2
+  - [ ] Channels:
+    - [x] DM channels create/delete (hide)
+    - [x] DM Group channels create/edit/delete
+    - [ ] Guild channels:
+      - [x] Text
+      - [ ] Voice
+      - [x] Category
+      - [ ] News
+      - [ ] Stage
+  - [ ] Messages:
+    - [x] Send, edit, delete
+    - [x] Threads
+    - [x] Attachments
+    - [x] Search
+    - [x] Reactions
+    - [x] Pins
+    - [ ] Components
+  - [x] Guilds:
+    - [x] Roles
+    - [x] Permissions
+    - [x] Permission overwrites
+    - [x] Ban/kick
+    - [x] Members edit (custom username, avatar, etc.)
+    - [x] Emojis
+    - [x] Stickers
+    - [x] Invites:
+      - [x] Regular
+      - [x] Vanity url
+    - [x] Scheduled events
+  - [ ] Users:
+    - [x] Edit (avatar, banner, username, email, etc.), deleting
+    - [x] Relationships:
+      - [x] Request
+      - [x] Accept
+      - [x] Remove
+      - [x] Block
+    - [x] Notes
+    - [ ] Connections
+  - [x] OAuth2
+  - [ ] Bots:
+    - [x] Create, edit, delete
+    - [ ] Commands:
+      - [x] Slash commands
+      - [ ] User commands (?)
+      - [ ] Message commands (?)
\ No newline at end of file
diff --git a/yepcord/rest_api/routes/other.py b/yepcord/rest_api/routes/other.py
index 5f728fa..8eebad0 100644
--- a/yepcord/rest_api/routes/other.py
+++ b/yepcord/rest_api/routes/other.py
@@ -242,6 +242,7 @@ async def instance_info():
         "features": [
             "OLD_USERNAMES",
             "REMOTE_AUTH_V1",
+            "REMOTE_AUTH_V2",
             "SETTINGS_PROTO",
         ],
     }