p2p-swift / Sources / IrdP2PCrypto / RoomCrypto.swift
769 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
 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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import Foundation
import CryptoKit

/// Every derivation of one room (PROTOCOL.md): ids, per-sender session keys,
/// the mailbox room id and the room-wide at-rest key. All from
/// `HKDF-SHA256(ikm: secret, salt: UTF8(appSalt), info: label)`.
///
/// A value type: the owning actor holds it as `var` because sealing advances
/// counters and opening fills a bounded key cache.
public struct RoomCrypto: Sendable {
    public let roomId: RoomId
    public let peerId: PeerId
    public let appSalt: String
    private let ikm: Data
    private let salt: Data
    private var sigSealer: Sealer
    private var msgSealer: Sealer
    private var keyCache = KeyCache(limit: 512)

    public init(secret: RoomSecret, appSalt: String, peerId: PeerId) {
        self.init(secret: secret, appSalt: appSalt, peerId: peerId, epoch: nil)
    }

    /// Deterministic epochs — tests and vector reproduction only.
    init(secret: RoomSecret, appSalt: String, peerId: PeerId, epoch: Data?) {
        ikm = secret.bytes
        salt = Data(appSalt.utf8)
        self.appSalt = appSalt
        self.peerId = peerId
        roomId = RoomId(unchecked: Bytes.base64URL(KDF.deriveBytes(ikm: ikm, salt: salt, info: "roomid")))
        let kSig = KDF.deriveKey(ikm: ikm, salt: salt, info: "signal|\(peerId.hex)")
        let kMsg = KDF.deriveKey(ikm: ikm, salt: salt, info: "msg|\(peerId.hex)")
        if let epoch {
            sigSealer = Sealer(key: kSig, epoch: epoch)
            msgSealer = Sealer(key: kMsg, epoch: epoch)
        } else {
            sigSealer = Sealer(key: kSig)
            msgSealer = Sealer(key: kMsg)
        }
    }

    /// Derives the room id alone (no session state), e.g. to look up a room.
    public static func roomId(secret: RoomSecret, appSalt: String) -> RoomId {
        RoomId(unchecked: Bytes.base64URL(KDF.deriveBytes(ikm: secret.bytes, salt: Data(appSalt.utf8), info: "roomid")))
    }

    // MARK: derivations shared with the reference implementation

    /// Node-visible mailbox room id — its own derivation so the mailbox cannot be correlated with the relay room.
    public var mailboxRoomId: String { Bytes.base64URL(KDF.deriveBytes(ikm: ikm, salt: salt, info: "mailbox|room")) }
    /// Gossip topic of the reference implementation (unused by this SDK; kept for vector parity).
    public var roomTopic: String { "sueta/2/" + Bytes.base64URL(KDF.deriveBytes(ikm: ikm, salt: salt, info: "topic|room")) }
    /// Database name of the reference implementation (unused by this SDK; kept for vector parity).
    public var dbName: String { Bytes.base64URL(KDF.deriveBytes(ikm: ikm, salt: salt, info: "dbname")) }

    // MARK: AADs

    func sigAAD(from: PeerId, to: PeerId) -> Data { Data("v1|\(roomId.string)|\(from.hex)|\(to.hex)|sig".utf8) }
    func msgAAD(from: PeerId) -> Data { Data("v1|\(roomId.string)|\(from.hex)|msg".utf8) }
    func atRestAAD(_ suffix: String) -> Data { Data("v2|\(roomId.string)|\(suffix)".utf8) }

    // MARK: sealing (as the local peer)

    /// Seals a directed payload for `to` under this peer's signal key.
    public mutating func sealSignal(_ payload: JSONValue, to: PeerId) -> Envelope {
        sigSealer.seal(payload, aad: sigAAD(from: peerId, to: to))
    }

    /// Seals a broadcast payload under this peer's message key (same ciphertext for every recipient).
    public mutating func sealMessage(_ payload: JSONValue) -> Envelope {
        msgSealer.seal(payload, aad: msgAAD(from: peerId))
    }

    /// Seals raw bytes under this peer's message key: `0x01 ‖ iv ‖ ct‖tag`.
    public mutating func sealMessageBinary(_ data: Data) -> Data {
        msgSealer.sealBinary(data, aad: msgAAD(from: peerId))
    }

    // MARK: opening (from a remote peer) — nil means drop silently

    public mutating func openSignal(_ envelope: Envelope, from: PeerId) -> JSONValue? {
        open(envelope, key: key("signal|\(from.hex)"), aad: sigAAD(from: from, to: peerId))
    }

    public mutating func openMessage(_ envelope: Envelope, from: PeerId) -> JSONValue? {
        open(envelope, key: key("msg|\(from.hex)"), aad: msgAAD(from: from))
    }

    public mutating func openMessageBinary(_ data: Data, from: PeerId) -> Data? {
        guard let (iv, ct) = AEAD.splitBinary(data, expectedPrefix: BinaryEnvelopePrefix.session) else { return nil }
        return AEAD.open(ct, key: key("msg|\(from.hex)"), iv: iv, aad: msgAAD(from: from))
    }

    private func open(_ envelope: Envelope, key: SymmetricKey, aad: Data) -> JSONValue? {
        guard let pt = AEAD.open(envelope.ciphertext, key: key, iv: envelope.iv, aad: aad) else { return nil }
        return try? JSONValue.parse(pt)
    }

    // MARK: room-wide at-rest ciphers

    /// Mailbox payload cipher (`mailbox-blob`, AAD `v2|room|mbx`).
    public var mailboxCipher: AtRestCipher { atRest(info: "mailbox-blob", suffix: "mbx") }
    /// Reference-implementation ciphers, kept for vector parity only.
    var logEntryCipher: AtRestCipher { atRest(info: "log-entry", suffix: "entry") }
    var logPayloadCipher: AtRestCipher { atRest(info: "log-payload", suffix: "payload") }
    var imageCipher: AtRestCipher { atRest(info: "img", suffix: "img") }

    private func atRest(info: String, suffix: String) -> AtRestCipher {
        AtRestCipher(key: KDF.deriveKey(ikm: ikm, salt: salt, info: info), aad: atRestAAD(suffix))
    }

    // MARK: key cache

    private mutating func key(_ info: String) -> SymmetricKey {
        if let k = keyCache.get(info) { return k }
        let k = KDF.deriveKey(ikm: ikm, salt: salt, info: info)
        keyCache.put(info, k)
        return k
    }
}

/// FIFO-bounded cache: remote sender ids are attacker-mintable, so the cache
/// must not grow without bound.
struct KeyCache: Sendable {
    let limit: Int
    private var order: [String] = []
    private var keys: [String: SymmetricKey] = [:]

    init(limit: Int) { self.limit = limit }

    func get(_ k: String) -> SymmetricKey? { keys[k] }

    mutating func put(_ k: String, _ v: SymmetricKey) {
        if keys[k] == nil {
            order.append(k)
            if order.count > limit { keys.removeValue(forKey: order.removeFirst()) }
        }
        keys[k] = v
    }

    var count: Int { keys.count }
}

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