p2p-swift / Sources / IrdP2PCrypto / Envelope.swift
399 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
import Foundation
import CryptoKit

/// The JSON envelope carried inside relay `signal` frames and data-channel text
/// messages: `{"v":1,"iv":"<base64>","ct":"<base64 ciphertext‖tag>"}`, keys in
/// exactly that order.
public struct Envelope: Sendable, Hashable {
    public static let version = 1
    public let iv: Data
    /// AES-GCM ciphertext followed by the 16-byte tag.
    public let ciphertext: Data

    public init(iv: Data, ciphertext: Data) {
        self.iv = iv
        self.ciphertext = ciphertext
    }

    /// The wire form, key order `v`, `iv`, `ct`.
    public var json: JSONValue {
        .object(JSONObject([("v", .int(Envelope.version)), ("iv", .string(Bytes.base64(iv))), ("ct", .string(Bytes.base64(ciphertext)))]))
    }

    /// Strict decoding: `v` must be exactly 1 and `iv`/`ct` strings; `nil` otherwise.
    public init?(json: JSONValue) {
        guard case .object(let o) = json,
              o["v"]?.intValue == Envelope.version,
              let ivS = o["iv"]?.stringValue, let ctS = o["ct"]?.stringValue,
              let iv = Bytes.fromBase64(ivS), let ct = Bytes.fromBase64(ctS)
        else { return nil }
        self.iv = iv
        self.ciphertext = ct
    }
}

/// Byte prefixes of the two binary envelope layouts: `prefix ‖ iv(12) ‖ ct‖tag`.
public enum BinaryEnvelopePrefix {
    /// Session-key binary envelope (data channel).
    public static let session: UInt8 = 0x01
    /// Room-wide at-rest envelope (mailbox).
    public static let atRest: UInt8 = 0x02
    /// Smallest possible envelope: prefix + IV + tag.
    public static let minimumLength = 1 + 12 + 16
}

enum AEAD {
    static func seal(_ plaintext: Data, key: SymmetricKey, iv: Data, aad: Data) -> Data {
        // A 12-byte nonce never fails to construct; a programming error here is unrecoverable.
        let box = try! AES.GCM.seal(plaintext, using: key, nonce: AES.GCM.Nonce(data: iv), authenticating: aad)
        return box.ciphertext + box.tag
    }

    /// `nil` on any failure — the caller drops silently (PROTOCOL.md).
    static func open(_ ciphertextAndTag: Data, key: SymmetricKey, iv: Data, aad: Data) -> Data? {
        guard iv.count == 12, ciphertextAndTag.count >= 16 else { return nil }
        let ct = ciphertextAndTag.prefix(ciphertextAndTag.count - 16)
        let tag = ciphertextAndTag.suffix(16)
        guard let nonce = try? AES.GCM.Nonce(data: iv),
              let box = try? AES.GCM.SealedBox(nonce: nonce, ciphertext: ct, tag: tag)
        else { return nil }
        return try? AES.GCM.open(box, using: key, authenticating: aad)
    }

    static func binary(prefix: UInt8, iv: Data, ciphertext: Data) -> Data {
        var out = Data(capacity: 1 + iv.count + ciphertext.count)
        out.append(prefix)
        out.append(iv)
        out.append(ciphertext)
        return out
    }

    static func splitBinary(_ data: Data, expectedPrefix: UInt8) -> (iv: Data, ciphertext: Data)? {
        guard data.count >= BinaryEnvelopePrefix.minimumLength, data.first == expectedPrefix else { return nil }
        let start = data.startIndex
        return (Data(data[start + 1 ..< start + 13]), Data(data[(start + 13)...]))
    }
}

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