summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/bundle_store.py
blob: e7c69819cae4fdbaebfce91ea71f450feba8c995 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""
Bundle store — SQLite-backed storage for GEK bundles and keypair bundles.

GEK bundles: ECIES-wrapped GEK targeted at a specific user's X25519 key.
Keypair bundles: AES-GCM encrypted (Ed25519 + X25519) private keys, encrypted
with the user's password-derived bundle_key. Opaque to the node.

Both are stored and served over the P2P DataChannel during MNP handshake.
"""

import logging
from pathlib import Path

import aiosqlite

log = logging.getLogger(__name__)

_SCHEMA_GEK = """\
CREATE TABLE IF NOT EXISTS gek_bundles (
    group_id    TEXT NOT NULL,
    user_id     TEXT NOT NULL,
    pk_eph_b64  TEXT NOT NULL,
    nonce_b64   TEXT NOT NULL,
    wrapped_b64 TEXT NOT NULL,
    stored_at   TEXT NOT NULL DEFAULT (datetime('now')),
    PRIMARY KEY (group_id, user_id)
);
"""

_SCHEMA_KEYPAIR = """\
CREATE TABLE IF NOT EXISTS keypair_bundles (
    user_id     TEXT PRIMARY KEY,
    bundle_enc  TEXT NOT NULL,
    stored_at   TEXT NOT NULL DEFAULT (datetime('now'))
);
"""


class BundleStore:
    def __init__(self, db_path: Path):
        self._db_path = db_path
        self._db: aiosqlite.Connection | None = None

    async def open(self) -> None:
        self._db_path.parent.mkdir(parents=True, exist_ok=True)
        self._db = await aiosqlite.connect(str(self._db_path))
        await self._db.execute(_SCHEMA_GEK)
        await self._db.execute(_SCHEMA_KEYPAIR)
        await self._db.commit()

    async def store(
        self,
        group_id: str,
        user_id: str,
        pk_eph_b64: str,
        nonce_b64: str,
        wrapped_b64: str,
    ) -> None:
        assert self._db
        await self._db.execute(
            "INSERT OR REPLACE INTO gek_bundles "
            "(group_id, user_id, pk_eph_b64, nonce_b64, wrapped_b64, stored_at) "
            "VALUES (?, ?, ?, ?, ?, datetime('now'))",
            (group_id, user_id, pk_eph_b64, nonce_b64, wrapped_b64),
        )
        await self._db.commit()

    async def fetch(self, group_id: str, user_id: str) -> dict | None:
        assert self._db
        async with self._db.execute(
            "SELECT pk_eph_b64, nonce_b64, wrapped_b64 FROM gek_bundles "
            "WHERE group_id = ? AND user_id = ?",
            (group_id, user_id),
        ) as cursor:
            row = await cursor.fetchone()
            if not row:
                return None
            return {
                "pk_eph_b64": row[0],
                "nonce_b64": row[1],
                "wrapped_b64": row[2],
            }

    async def store_keypair(self, user_id: str, bundle_enc: str) -> None:
        assert self._db
        await self._db.execute(
            "INSERT OR REPLACE INTO keypair_bundles "
            "(user_id, bundle_enc, stored_at) VALUES (?, ?, datetime('now'))",
            (user_id, bundle_enc),
        )
        await self._db.commit()

    async def fetch_keypair(self, user_id: str) -> str | None:
        assert self._db
        async with self._db.execute(
            "SELECT bundle_enc FROM keypair_bundles WHERE user_id = ?",
            (user_id,),
        ) as cursor:
            row = await cursor.fetchone()
            return row[0] if row else None

    async def close(self) -> None:
        if self._db:
            await self._db.close()
            self._db = None