p2p-swift / Sources / IrdP2P / SealedMailbox.swift
339 words
 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
import Foundation
import IrdP2PCrypto
import IrdP2PSignaling

/// A replayed, decrypted mailbox message.
public struct SealedItem: Sendable, Equatable {
    public var seq: MailboxSeq
    public var id: String
    public var date: Date
    public var message: Message
}

/// The offline mailbox of one room: deposits are sealed under the room-wide
/// mailbox key (random IVs) and filed under a mailbox room id the node cannot
/// correlate with the relay room. Undecryptable entries are dropped silently.
public actor SealedMailbox {
    public nonisolated let mailboxRoomId: String
    public nonisolated let roomId: RoomId
    private let cipher: AtRestCipher
    private let client: MailboxClient
    public let outbox: MailboxOutbox

    init(secret: RoomSecret, appSalt: String, client: MailboxClient, clock: any P2PClock) {
        let crypto = RoomCrypto(secret: secret, appSalt: appSalt, peerId: .random())
        roomId = crypto.roomId
        mailboxRoomId = crypto.mailboxRoomId
        cipher = crypto.mailboxCipher
        self.client = client
        outbox = MailboxOutbox(client: client, clock: clock)
    }

    /// Seals and deposits now.
    public func deposit(_ message: Message) async throws -> DepositResult {
        try await client.deposit(room: mailboxRoomId, seal(message))
    }

    /// Seals and queues for `outbox.flush()` (retries with backoff).
    public func enqueue(_ message: Message) async {
        await outbox.enqueue(room: mailboxRoomId, payload: seal(message))
    }

    public func replay(after: MailboxSeq? = nil, before: MailboxSeq? = nil, limit: Int = 100) async throws -> (items: [SealedItem], page: ReplayPage) {
        let page = try await client.replay(room: mailboxRoomId, after: after, before: before, limit: limit)
        return (page.messages.compactMap(open), page)
    }

    public func replayAll(after: MailboxSeq?) async throws -> (items: [SealedItem], cursor: MailboxSeq?, gap: Bool) {
        let r = try await client.replayAll(room: mailboxRoomId, after: after)
        return (r.messages.compactMap(open), r.cursor, r.gap)
    }

    private func seal(_ message: Message) -> Data {
        let inner: InnerMessage
        switch message {
        case .json(let v): inner = .msg(v)
        case .data(let d): inner = .bin(d)
        }
        return cipher.seal(inner.json.serializedData)
    }

    private func open(_ m: MailboxMessage) -> SealedItem? {
        guard let plain = cipher.open(m.payload), let v = try? JSONValue.parse(plain), let inner = InnerMessage(json: v) else { return nil }
        switch inner {
        case .msg(let payload): return SealedItem(seq: m.seq, id: m.id, date: m.date, message: .json(payload))
        case .bin(let data): return SealedItem(seq: m.seq, id: m.id, date: m.date, message: .data(data))
        default: return nil
        }
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-swift.git