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 | import Foundation
import Testing
@testable import IrdP2PCrypto
@Suite struct RoomCryptoVectorTests {
@Test func derivationsMatchReference() throws {
for d in try vector("room-crypto")["derivations"]!.arr {
let rc = RoomCrypto(secret: try secret(d["secret"]!.str), appSalt: d["appSalt"]!.str, peerId: try peer("00000000000000000000000000000001"))
#expect(rc.roomId.string == d["roomId"]!.str, "roomId \(d["appSalt"]!.str)")
#expect(rc.roomTopic == d["roomTopic"]!.str)
#expect(rc.dbName == d["dbName"]!.str)
#expect(rc.mailboxRoomId == d["mailboxRoomId"]!.str)
#expect(RoomCrypto.roomId(secret: try secret(d["secret"]!.str), appSalt: d["appSalt"]!.str) == rc.roomId)
}
}
@Test func sealerIVsAreEpochThenBigEndianCounter() throws {
let ivs = try vector("room-crypto")["sealerIvs"]!.arr.map(\.str)
var s = Sealer(key: .init(size: .bits256), epoch: Bytes.fromHex(String(ivs[0].prefix(8)))!)
for expected in ivs {
let iv = s.nextIV()
#expect(Bytes.hex(iv) == expected)
}
}
/// Sealing with the vector's epoch reproduces every reference envelope byte for byte,
/// and the recipient opens them all.
@Test func envelopesAreByteExactAndOpen() throws {
let e = try vector("room-crypto")["envelopes"]!
let sec = try secret(e["secret"]!.str), salt = e["appSalt"]!.str
let a = try peer(e["from"]!.str), b = try peer(e["to"]!.str)
var sender = RoomCrypto(secret: sec, appSalt: salt, peerId: a, epoch: Bytes.fromHex(e["epoch"]!.str)!)
var recipient = RoomCrypto(secret: sec, appSalt: salt, peerId: b)
let hello = e["signalHello"]!, offer = e["signalOffer"]!, msg = e["msg"]!, bin = e["binary"]!
let sealedHello = sender.sealSignal(hello["plaintext"]!, to: b)
#expect(sealedHello.json.serialized == hello["envelope"]!.serialized)
let sealedOffer = sender.sealSignal(offer["plaintext"]!, to: b)
#expect(sealedOffer.json.serialized == offer["envelope"]!.serialized)
let sealedMsg = sender.sealMessage(msg["plaintext"]!)
#expect(sealedMsg.json.serialized == msg["envelope"]!.serialized)
let sealedBin = sender.sealMessageBinary(bin["plaintext"]!.bytes)
#expect(Bytes.hex(sealedBin) == bin["sealedHex"]!.str)
let openedHello = recipient.openSignal(Envelope(json: hello["envelope"]!)!, from: a)
#expect(openedHello == hello["plaintext"]!)
let openedOffer = recipient.openSignal(Envelope(json: offer["envelope"]!)!, from: a)
#expect(openedOffer == offer["plaintext"]!)
let openedMsg = recipient.openMessage(Envelope(json: msg["envelope"]!)!, from: a)
#expect(openedMsg == msg["plaintext"]!)
let openedBin = recipient.openMessageBinary(Bytes.fromHex(bin["sealedHex"]!.str)!, from: a)
#expect(openedBin == bin["plaintext"]!.bytes)
// opened plaintext keeps the sender's key order
#expect(openedHello!.serialized == hello["plaintext"]!.serialized)
}
@Test func atRestMatchesReference() throws {
let v = try vector("room-crypto")
let e = v["envelopes"]!, r = v["atRest"]!
let rc = RoomCrypto(secret: try secret(e["secret"]!.str), appSalt: e["appSalt"]!.str, peerId: try peer(e["from"]!.str))
let mbx = r["mbx"]!
let pt = mbx["plaintext"]!.serializedData
#expect(Bytes.hex(rc.mailboxCipher.seal(pt, iv: Bytes.fromHex(mbx["ivHex"]!.str)!)) == mbx["sealedHex"]!.str)
#expect(rc.mailboxCipher.open(Bytes.fromHex(mbx["sealedHex"]!.str)!) == pt)
let entry = r["entry"]!
#expect(Bytes.hex(rc.logEntryCipher.seal(entry["plaintextBytes"]!.bytes, iv: Bytes.fromHex(entry["ivHex"]!.str)!)) == entry["sealedHex"]!.str)
// random-IV path round-trips and never repeats an IV
let s1 = rc.mailboxCipher.seal(pt), s2 = rc.mailboxCipher.seal(pt)
#expect(s1 != s2)
#expect(rc.mailboxCipher.open(s1) == pt)
#expect(rc.mailboxCipher.open(s2) == pt)
}
@Test func tamperTableAllOpenToNil() throws {
let v = try vector("room-crypto")
let e = v["envelopes"]!
let sec = try secret(e["secret"]!.str), salt = e["appSalt"]!.str
let a = try peer(e["from"]!.str), b = try peer(e["to"]!.str)
for (name, c) in v["tamper"]!.obj.pairs {
let opener = try peer(c["by"]?.str ?? b.hex)
let from = try peer(c["from"]?.str ?? a.hex)
var rc = RoomCrypto(secret: try secret(c["secret"]?.str ?? sec.base64URL), appSalt: c["appSalt"]?.str ?? salt, peerId: opener)
switch c["open"]!.str {
case "signal":
var opened: JSONValue? = nil
if let env = Envelope(json: c["env"]!) { opened = rc.openSignal(env, from: from) }
#expect(opened == nil, "\(name)")
case "msg":
let opened = rc.openMessage(Envelope(json: c["env"]!)!, from: from)
#expect(opened == nil, "\(name)")
case "binary":
let opened = rc.openMessageBinary(Bytes.fromHex(c["sealedHex"]!.str)!, from: from)
#expect(opened == nil, "\(name)")
case "atRest":
let cipher = c["aadSuffix"]?.str == "entry" ? rc.logEntryCipher : rc.mailboxCipher
#expect(cipher.open(Bytes.fromHex(c["sealedHex"]!.str)!) == nil, "\(name)")
default: Issue.record("unknown tamper kind \(name)")
}
}
}
@Test func keyCacheIsBounded() {
var cache = KeyCache(limit: 3)
for i in 0..<10 { cache.put("k\(i)", .init(size: .bits256)) }
#expect(cache.count == 3)
#expect(cache.get("k0") == nil)
#expect(cache.get("k9") != nil)
}
@Test func freshRoomCryptoUsesRandomEpochs() throws {
let sec = RoomSecret.random(), p = PeerId.random()
var x = RoomCrypto(secret: sec, appSalt: "t", peerId: p)
var y = RoomCrypto(secret: sec, appSalt: "t", peerId: p)
let (ex, ey) = (x.sealMessage(.null), y.sealMessage(.null))
#expect(ex.iv != ey.iv)
}
}
|