@ethora/chat-component-rn 26.10.6 → 26.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +6 -3
  3. package/lib/commonjs/helpers/secureStoreRuntime.js +10 -1
  4. package/lib/commonjs/helpers/secureStoreRuntime.js.map +1 -1
  5. package/lib/commonjs/main.js +81 -0
  6. package/lib/commonjs/main.js.map +1 -1
  7. package/lib/commonjs/networking/api-requests/push.api.js +22 -7
  8. package/lib/commonjs/networking/api-requests/push.api.js.map +1 -1
  9. package/lib/commonjs/push/pushKeyStore.js +169 -0
  10. package/lib/commonjs/push/pushKeyStore.js.map +1 -0
  11. package/lib/commonjs/push/sealedBox.js +106 -0
  12. package/lib/commonjs/push/sealedBox.js.map +1 -0
  13. package/lib/commonjs/push/sealedPush.js +124 -0
  14. package/lib/commonjs/push/sealedPush.js.map +1 -0
  15. package/lib/commonjs/push/sealedPushTask.js +195 -0
  16. package/lib/commonjs/push/sealedPushTask.js.map +1 -0
  17. package/lib/commonjs/services/pushRegistration.js +12 -7
  18. package/lib/commonjs/services/pushRegistration.js.map +1 -1
  19. package/lib/commonjs/version.js +1 -1
  20. package/lib/module/helpers/secureStoreRuntime.js +10 -1
  21. package/lib/module/helpers/secureStoreRuntime.js.map +1 -1
  22. package/lib/module/main.js +6 -0
  23. package/lib/module/main.js.map +1 -1
  24. package/lib/module/networking/api-requests/push.api.js +21 -6
  25. package/lib/module/networking/api-requests/push.api.js.map +1 -1
  26. package/lib/module/push/pushKeyStore.js +159 -0
  27. package/lib/module/push/pushKeyStore.js.map +1 -0
  28. package/lib/module/push/sealedBox.js +97 -0
  29. package/lib/module/push/sealedBox.js.map +1 -0
  30. package/lib/module/push/sealedPush.js +117 -0
  31. package/lib/module/push/sealedPush.js.map +1 -0
  32. package/lib/module/push/sealedPushTask.js +187 -0
  33. package/lib/module/push/sealedPushTask.js.map +1 -0
  34. package/lib/module/services/pushRegistration.js +12 -7
  35. package/lib/module/services/pushRegistration.js.map +1 -1
  36. package/lib/module/version.js +1 -1
  37. package/lib/typescript/helpers/secureStoreRuntime.d.ts +17 -3
  38. package/lib/typescript/helpers/secureStoreRuntime.d.ts.map +1 -1
  39. package/lib/typescript/main.d.ts +6 -0
  40. package/lib/typescript/main.d.ts.map +1 -1
  41. package/lib/typescript/networking/api-requests/push.api.d.ts +14 -2
  42. package/lib/typescript/networking/api-requests/push.api.d.ts.map +1 -1
  43. package/lib/typescript/push/pushKeyStore.d.ts +35 -0
  44. package/lib/typescript/push/pushKeyStore.d.ts.map +1 -0
  45. package/lib/typescript/push/sealedBox.d.ts +24 -0
  46. package/lib/typescript/push/sealedBox.d.ts.map +1 -0
  47. package/lib/typescript/push/sealedPush.d.ts +61 -0
  48. package/lib/typescript/push/sealedPush.d.ts.map +1 -0
  49. package/lib/typescript/push/sealedPushTask.d.ts +33 -0
  50. package/lib/typescript/push/sealedPushTask.d.ts.map +1 -0
  51. package/lib/typescript/services/pushRegistration.d.ts +8 -0
  52. package/lib/typescript/services/pushRegistration.d.ts.map +1 -1
  53. package/lib/typescript/types/types.d.ts +6 -0
  54. package/lib/typescript/types/types.d.ts.map +1 -1
  55. package/lib/typescript/version.d.ts +1 -1
  56. package/package.json +11 -1
  57. package/plugins/notification-service/NotificationService.swift +113 -0
  58. package/plugins/notification-service/SealedBox.swift +355 -0
  59. package/plugins/notification-service/check/gen-vectors.mjs +24 -0
  60. package/plugins/notification-service/check/main.swift +37 -0
  61. package/plugins/notification-service/check/run.sh +12 -0
  62. package/plugins/notification-service/check/vectors.json +1 -0
  63. package/plugins/withNotificationServiceExtension.js +70 -18
  64. package/src/helpers/secureStoreRuntime.ts +21 -3
  65. package/src/main.ts +30 -0
  66. package/src/networking/api-requests/push.api.ts +29 -4
  67. package/src/push/pushKeyStore.ts +163 -0
  68. package/src/push/sealedBox.ts +104 -0
  69. package/src/push/sealedPush.ts +127 -0
  70. package/src/push/sealedPushTask.ts +175 -0
  71. package/src/services/pushRegistration.ts +25 -5
  72. package/src/types/types.ts +6 -0
  73. package/src/version.ts +1 -1
@@ -1,6 +1,14 @@
1
1
  import UniformTypeIdentifiers
2
2
  import UserNotifications
3
3
 
4
+ /// Ethora chat pushes, two jobs:
5
+ /// - encrypted (sealed) pushes: the push carries a placeholder ("New message")
6
+ /// and a libsodium sealed box (`enc`) for the key this device registered
7
+ /// with its token. The box is opened with the secret key the app keeps in
8
+ /// the keychain (see `SealedPush`), and the notification is rewritten with
9
+ /// the real title and text. Whatever goes wrong, the placeholder stays.
10
+ /// - attachment previews: a plain photo / video push carries `previewUrl`
11
+ /// with `mutable-content: 1`; the thumbnail is downloaded and attached.
4
12
  class NotificationService: UNNotificationServiceExtension {
5
13
  private let lock = NSLock()
6
14
  private var contentHandler: ((UNNotificationContent) -> Void)?
@@ -18,6 +26,12 @@ class NotificationService: UNNotificationServiceExtension {
18
26
  self.contentHandler = contentHandler
19
27
  self.content = content
20
28
 
29
+ if SealedPush.isSealed(content.userInfo) {
30
+ SealedPush.apply(to: content)
31
+ finish()
32
+ return
33
+ }
34
+
21
35
  guard let url = Self.previewURL(in: content.userInfo) else {
22
36
  finish()
23
37
  return
@@ -80,3 +94,102 @@ class NotificationService: UNNotificationServiceExtension {
80
94
  }
81
95
  }
82
96
  }
97
+
98
+ /// Encrypted pushes (ethora-backend, services/push/README.md, "Encrypted
99
+ /// pushes"). The envelope sits next to `aps`: `v`, `alg`, `kid`, `jid`,
100
+ /// `msgID`, `userJid` in the clear, `enc` = base64 of the sealed box. Inside:
101
+ /// UTF-8 JSON with `title`, `body`, `sender`, `senderJid`, `chat`, `chatJid`,
102
+ /// `msgID`, `image`.
103
+ ///
104
+ /// The secret key is the item the SDK writes through expo-secure-store
105
+ /// (`src/push/pushKeyStore.ts` in @ethora/chat-component-rn): a generic
106
+ /// password with service `ethora.push:no-auth` (expo-secure-store adds the
107
+ /// `:no-auth` suffix), account `ethora_push_secret_key`, value = base64 of
108
+ /// the 32-byte X25519 secret key, in the keychain access group the config
109
+ /// plugin shares between the app and this extension.
110
+ typealias Bytes = [UInt8]
111
+
112
+ enum SealedPush {
113
+ static let algorithm = "sealed-box-x25519"
114
+ static let keychainServices = ["ethora.push:no-auth", "ethora.push"]
115
+ static let keychainAccount = "ethora_push_secret_key"
116
+
117
+ static func isSealed(_ userInfo: [AnyHashable: Any]) -> Bool {
118
+ (userInfo["alg"] as? String) == algorithm
119
+ }
120
+
121
+ /// Replace the placeholder with the decrypted notification. Leaves the
122
+ /// content as it came (the placeholder) when anything fails.
123
+ static func apply(to content: UNMutableNotificationContent) {
124
+ let userInfo = content.userInfo
125
+ let kid = (userInfo["kid"] as? String) ?? ""
126
+ guard let enc = userInfo["enc"] as? String,
127
+ let sealed = Data(base64Encoded: enc, options: .ignoreUnknownCharacters)
128
+ else {
129
+ NSLog("[EthoraNotificationService] sealed push kid=%@: no ciphertext, keeping the placeholder", kid)
130
+ return
131
+ }
132
+ guard let secretKey = loadSecretKey() else {
133
+ NSLog("[EthoraNotificationService] sealed push kid=%@: no device key in the keychain, keeping the placeholder", kid)
134
+ return
135
+ }
136
+ guard let plaintext = open(sealed: sealed, secretKey: secretKey) else {
137
+ NSLog("[EthoraNotificationService] sealed push kid=%@: the box did not open (another key?), keeping the placeholder", kid)
138
+ return
139
+ }
140
+ guard let json = try? JSONSerialization.jsonObject(with: plaintext) as? [String: Any] else {
141
+ NSLog("[EthoraNotificationService] sealed push kid=%@: opened, but not JSON", kid)
142
+ return
143
+ }
144
+ NSLog("[EthoraNotificationService] sealed push kid=%@: opened", kid)
145
+
146
+ let title = text(json["title"])
147
+ let body = text(json["body"])
148
+ let sender = text(json["sender"])
149
+ let chat = text(json["chat"])
150
+ content.title = title.isEmpty ? (sender.isEmpty ? chat : sender) : title
151
+ content.body = body
152
+ if content.threadIdentifier.isEmpty, let jid = userInfo["jid"] as? String, !jid.isEmpty {
153
+ content.threadIdentifier = jid
154
+ }
155
+ // What the tap handler reads (the host hands `userInfo` to
156
+ // `handlePushPayload`); the ciphertext is of no use to it.
157
+ var info = userInfo
158
+ info["enc"] = nil
159
+ info["chatJid"] = text(json["chatJid"])
160
+ info["senderJid"] = text(json["senderJid"])
161
+ info["sealed"] = true
162
+ content.userInfo = info
163
+ }
164
+
165
+ private static func text(_ value: Any?) -> String {
166
+ (value as? String) ?? ""
167
+ }
168
+
169
+ private static func open(sealed: Data, secretKey: Bytes) -> Data? {
170
+ guard let opened = SealedBox.open([UInt8](sealed), secretKey: secretKey) else { return nil }
171
+ return Data(opened)
172
+ }
173
+
174
+ private static func loadSecretKey() -> Bytes? {
175
+ for service in keychainServices {
176
+ for account in [Data(keychainAccount.utf8) as Any, keychainAccount as Any] {
177
+ let query: [String: Any] = [
178
+ kSecClass as String: kSecClassGenericPassword,
179
+ kSecAttrService as String: service,
180
+ kSecAttrAccount as String: account,
181
+ kSecReturnData as String: true,
182
+ kSecMatchLimit as String: kSecMatchLimitOne,
183
+ ]
184
+ var item: CFTypeRef?
185
+ guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
186
+ let data = item as? Data,
187
+ let text = String(data: data, encoding: .utf8),
188
+ let key = Data(base64Encoded: text.trimmingCharacters(in: .whitespacesAndNewlines))
189
+ else { continue }
190
+ return Bytes(key)
191
+ }
192
+ }
193
+ return nil
194
+ }
195
+ }
@@ -0,0 +1,355 @@
1
+ import CryptoKit
2
+ import Foundation
3
+
4
+ /// libsodium's sealed box (`crypto_box_seal_open`) without libsodium: the
5
+ /// notification service extension cannot load the app's JavaScript, and a
6
+ /// pod for the extension is one more thing every host app would have to
7
+ /// carry (the Sodium pod also excludes arm64 simulators). X25519 comes from
8
+ /// CryptoKit; HSalsa20, XSalsa20, Poly1305 and BLAKE2b are ported from
9
+ /// tweetnacl and poly1305-donna / the BLAKE2 reference, public domain.
10
+ ///
11
+ /// Layout: ephemeral public key (32) || crypto_box(message, nonce,
12
+ /// recipient pk, ephemeral sk), with crypto_box = XSalsa20-Poly1305 under
13
+ /// HSalsa20(X25519 shared secret) and nonce = BLAKE2b-192(epk || rpk).
14
+ /// The SDK's `src/push/sealedBox.ts` is the same construction in JS; both
15
+ /// are pinned against libsodium (JS: `__tests__/sealedBox.test.ts`, Swift:
16
+ /// `plugins/notification-service/check/`).
17
+ enum SealedBox {
18
+ static let publicKeyBytes = 32
19
+ static let secretKeyBytes = 32
20
+ static let macBytes = 16
21
+ static let nonceBytes = 24
22
+ /// Ephemeral public key + Poly1305 tag.
23
+ static let overheadBytes = 48
24
+
25
+ /// The X25519 public key of a secret key (`crypto_scalarmult_base`).
26
+ static func publicKey(of secretKey: [UInt8]) -> [UInt8]? {
27
+ guard let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: secretKey) else { return nil }
28
+ return [UInt8](key.publicKey.rawRepresentation)
29
+ }
30
+
31
+ /// `crypto_box_seal_open`. Nil for anything that does not open.
32
+ static func open(_ sealed: [UInt8], secretKey: [UInt8]) -> [UInt8]? {
33
+ guard sealed.count >= overheadBytes, secretKey.count == secretKeyBytes,
34
+ let recipientPublicKey = publicKey(of: secretKey)
35
+ else { return nil }
36
+ let ephemeralPublicKey = Array(sealed[0..<publicKeyBytes])
37
+ let box = Array(sealed[publicKeyBytes...])
38
+ guard let shared = x25519(secretKey: secretKey, publicKey: ephemeralPublicKey) else { return nil }
39
+ let key = Salsa.hsalsa20(key: shared, input: [UInt8](repeating: 0, count: 16))
40
+ let nonce = Blake2b.hash(ephemeralPublicKey + recipientPublicKey, outputLength: nonceBytes)
41
+ return secretboxOpen(box, nonce: nonce, key: key)
42
+ }
43
+
44
+ private static func x25519(secretKey: [UInt8], publicKey: [UInt8]) -> [UInt8]? {
45
+ guard let priv = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: secretKey),
46
+ let pub = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: publicKey),
47
+ let shared = try? priv.sharedSecretFromKeyAgreement(with: pub)
48
+ else { return nil }
49
+ return shared.withUnsafeBytes { [UInt8]($0) }
50
+ }
51
+
52
+ /// `crypto_secretbox_open_easy`: tag (16) || ciphertext under XSalsa20-Poly1305.
53
+ static func secretboxOpen(_ box: [UInt8], nonce: [UInt8], key: [UInt8]) -> [UInt8]? {
54
+ guard box.count >= macBytes, nonce.count == nonceBytes, key.count == 32 else { return nil }
55
+ let tag = Array(box[0..<macBytes])
56
+ let ciphertext = Array(box[macBytes...])
57
+ // Keystream block 0: the first 32 bytes key Poly1305, the message starts at byte 32.
58
+ let stream = Salsa.xsalsa20(key: key, nonce: nonce, length: 32 + ciphertext.count)
59
+ let expected = Poly1305.mac(ciphertext, key: Array(stream[0..<32]))
60
+ guard constantTimeEqual(expected, tag) else { return nil }
61
+ var message = [UInt8](repeating: 0, count: ciphertext.count)
62
+ for i in 0..<ciphertext.count { message[i] = ciphertext[i] ^ stream[32 + i] }
63
+ return message
64
+ }
65
+
66
+ private static func constantTimeEqual(_ a: [UInt8], _ b: [UInt8]) -> Bool {
67
+ guard a.count == b.count else { return false }
68
+ var diff: UInt8 = 0
69
+ for i in 0..<a.count { diff |= a[i] ^ b[i] }
70
+ return diff == 0
71
+ }
72
+ }
73
+
74
+ // MARK: - Salsa20 / HSalsa20 / XSalsa20 (tweetnacl)
75
+
76
+ enum Salsa {
77
+ private static let sigma: [UInt32] = load32([UInt8]("expand 32-byte k".utf8))
78
+
79
+ private static func load32(_ bytes: [UInt8]) -> [UInt32] {
80
+ var words = [UInt32](repeating: 0, count: bytes.count / 4)
81
+ for i in 0..<words.count {
82
+ var w: UInt32 = 0
83
+ for j in 0..<4 { w |= UInt32(bytes[i * 4 + j]) << (8 * UInt32(j)) }
84
+ words[i] = w
85
+ }
86
+ return words
87
+ }
88
+
89
+ private static func store32(_ words: [UInt32]) -> [UInt8] {
90
+ var out = [UInt8](repeating: 0, count: words.count * 4)
91
+ for (i, w) in words.enumerated() {
92
+ out[i * 4] = UInt8(truncatingIfNeeded: w)
93
+ out[i * 4 + 1] = UInt8(truncatingIfNeeded: w >> 8)
94
+ out[i * 4 + 2] = UInt8(truncatingIfNeeded: w >> 16)
95
+ out[i * 4 + 3] = UInt8(truncatingIfNeeded: w >> 24)
96
+ }
97
+ return out
98
+ }
99
+
100
+ @inline(__always) private static func rotl(_ v: UInt32, _ n: UInt32) -> UInt32 {
101
+ (v << n) | (v >> (32 - n))
102
+ }
103
+
104
+ /// The Salsa20 state: sigma on the diagonal, key in two halves, 16-byte input in the middle row.
105
+ private static func state(key: [UInt8], input: [UInt8]) -> [UInt32] {
106
+ let k = load32(key)
107
+ let n = load32(input)
108
+ return [
109
+ sigma[0], k[0], k[1], k[2],
110
+ k[3], sigma[1], n[0], n[1],
111
+ n[2], n[3], sigma[2], k[4],
112
+ k[5], k[6], k[7], sigma[3],
113
+ ]
114
+ }
115
+
116
+ /// 20 rounds (10 double rounds) in place.
117
+ private static func rounds(_ x: inout [UInt32]) {
118
+ @inline(__always) func quarter(_ a: Int, _ b: Int, _ c: Int, _ d: Int) {
119
+ x[b] ^= rotl(x[a] &+ x[d], 7)
120
+ x[c] ^= rotl(x[b] &+ x[a], 9)
121
+ x[d] ^= rotl(x[c] &+ x[b], 13)
122
+ x[a] ^= rotl(x[d] &+ x[c], 18)
123
+ }
124
+ for _ in 0..<10 {
125
+ // column round
126
+ quarter(0, 4, 8, 12)
127
+ quarter(5, 9, 13, 1)
128
+ quarter(10, 14, 2, 6)
129
+ quarter(15, 3, 7, 11)
130
+ // row round
131
+ quarter(0, 1, 2, 3)
132
+ quarter(5, 6, 7, 4)
133
+ quarter(10, 11, 8, 9)
134
+ quarter(15, 12, 13, 14)
135
+ }
136
+ }
137
+
138
+ /// HSalsa20: 32-byte key, 16-byte input → 32-byte subkey (no feed-forward).
139
+ static func hsalsa20(key: [UInt8], input: [UInt8]) -> [UInt8] {
140
+ var x = state(key: key, input: input)
141
+ rounds(&x)
142
+ return store32([x[0], x[5], x[10], x[15], x[6], x[7], x[8], x[9]])
143
+ }
144
+
145
+ /// One 64-byte Salsa20 block for the 8-byte nonce and the block counter.
146
+ private static func block(key: [UInt8], nonce: [UInt8], counter: UInt64) -> [UInt8] {
147
+ var input = nonce
148
+ for i in 0..<8 { input.append(UInt8(truncatingIfNeeded: counter >> (8 * UInt64(i)))) }
149
+ let initial = state(key: key, input: input)
150
+ var x = initial
151
+ rounds(&x)
152
+ for i in 0..<16 { x[i] = x[i] &+ initial[i] }
153
+ return store32(x)
154
+ }
155
+
156
+ /// XSalsa20 keystream: HSalsa20 over the first 16 nonce bytes, Salsa20 over the last 8.
157
+ static func xsalsa20(key: [UInt8], nonce: [UInt8], length: Int) -> [UInt8] {
158
+ let subkey = hsalsa20(key: key, input: Array(nonce[0..<16]))
159
+ let nonce8 = Array(nonce[16..<24])
160
+ var out = [UInt8]()
161
+ out.reserveCapacity(length + 64)
162
+ var counter: UInt64 = 0
163
+ while out.count < length {
164
+ out += block(key: subkey, nonce: nonce8, counter: counter)
165
+ counter += 1
166
+ }
167
+ return Array(out[0..<length])
168
+ }
169
+ }
170
+
171
+ // MARK: - Poly1305 (poly1305-donna, 26-bit limbs)
172
+
173
+ enum Poly1305 {
174
+ private static func load32(_ m: [UInt8], _ i: Int) -> UInt32 {
175
+ var w: UInt32 = 0
176
+ for j in 0..<4 { w |= UInt32(m[i + j]) << (8 * UInt32(j)) }
177
+ return w
178
+ }
179
+
180
+ static func mac(_ message: [UInt8], key: [UInt8]) -> [UInt8] {
181
+ precondition(key.count == 32)
182
+ // r, clamped
183
+ let r0 = load32(key, 0) & 0x3ffffff
184
+ let r1 = (load32(key, 3) >> 2) & 0x3ffff03
185
+ let r2 = (load32(key, 6) >> 4) & 0x3ffc0ff
186
+ let r3 = (load32(key, 9) >> 6) & 0x3f03fff
187
+ let r4 = (load32(key, 12) >> 8) & 0x00fffff
188
+ let s1 = r1 &* 5, s2 = r2 &* 5, s3 = r3 &* 5, s4 = r4 &* 5
189
+ var h0: UInt32 = 0, h1: UInt32 = 0, h2: UInt32 = 0, h3: UInt32 = 0, h4: UInt32 = 0
190
+
191
+ func absorb(_ m: [UInt8], _ offset: Int, hibit: UInt32) {
192
+ h0 &+= load32(m, offset) & 0x3ffffff
193
+ h1 &+= (load32(m, offset + 3) >> 2) & 0x3ffffff
194
+ h2 &+= (load32(m, offset + 6) >> 4) & 0x3ffffff
195
+ h3 &+= (load32(m, offset + 9) >> 6) & 0x3ffffff
196
+ h4 &+= (load32(m, offset + 12) >> 8) | hibit
197
+
198
+ let d0 = UInt64(h0) * UInt64(r0) + UInt64(h1) * UInt64(s4) + UInt64(h2) * UInt64(s3) + UInt64(h3) * UInt64(s2) + UInt64(h4) * UInt64(s1)
199
+ var d1 = UInt64(h0) * UInt64(r1) + UInt64(h1) * UInt64(r0) + UInt64(h2) * UInt64(s4) + UInt64(h3) * UInt64(s3) + UInt64(h4) * UInt64(s2)
200
+ var d2 = UInt64(h0) * UInt64(r2) + UInt64(h1) * UInt64(r1) + UInt64(h2) * UInt64(r0) + UInt64(h3) * UInt64(s4) + UInt64(h4) * UInt64(s3)
201
+ var d3 = UInt64(h0) * UInt64(r3) + UInt64(h1) * UInt64(r2) + UInt64(h2) * UInt64(r1) + UInt64(h3) * UInt64(r0) + UInt64(h4) * UInt64(s4)
202
+ var d4 = UInt64(h0) * UInt64(r4) + UInt64(h1) * UInt64(r3) + UInt64(h2) * UInt64(r2) + UInt64(h3) * UInt64(r1) + UInt64(h4) * UInt64(r0)
203
+
204
+ var c = UInt32(truncatingIfNeeded: d0 >> 26); h0 = UInt32(truncatingIfNeeded: d0) & 0x3ffffff
205
+ d1 += UInt64(c); c = UInt32(truncatingIfNeeded: d1 >> 26); h1 = UInt32(truncatingIfNeeded: d1) & 0x3ffffff
206
+ d2 += UInt64(c); c = UInt32(truncatingIfNeeded: d2 >> 26); h2 = UInt32(truncatingIfNeeded: d2) & 0x3ffffff
207
+ d3 += UInt64(c); c = UInt32(truncatingIfNeeded: d3 >> 26); h3 = UInt32(truncatingIfNeeded: d3) & 0x3ffffff
208
+ d4 += UInt64(c); c = UInt32(truncatingIfNeeded: d4 >> 26); h4 = UInt32(truncatingIfNeeded: d4) & 0x3ffffff
209
+ h0 &+= c &* 5; c = h0 >> 26; h0 &= 0x3ffffff
210
+ h1 &+= c
211
+ }
212
+
213
+ var offset = 0
214
+ while message.count - offset >= 16 {
215
+ absorb(message, offset, hibit: 1 << 24)
216
+ offset += 16
217
+ }
218
+ if offset < message.count {
219
+ var last = Array(message[offset...])
220
+ last.append(1)
221
+ while last.count < 16 { last.append(0) }
222
+ absorb(last, 0, hibit: 0)
223
+ }
224
+
225
+ // full carry
226
+ var c = h1 >> 26; h1 &= 0x3ffffff
227
+ h2 &+= c; c = h2 >> 26; h2 &= 0x3ffffff
228
+ h3 &+= c; c = h3 >> 26; h3 &= 0x3ffffff
229
+ h4 &+= c; c = h4 >> 26; h4 &= 0x3ffffff
230
+ h0 &+= c &* 5; c = h0 >> 26; h0 &= 0x3ffffff
231
+ h1 &+= c
232
+
233
+ // h + -p
234
+ var g0 = h0 &+ 5; c = g0 >> 26; g0 &= 0x3ffffff
235
+ var g1 = h1 &+ c; c = g1 >> 26; g1 &= 0x3ffffff
236
+ var g2 = h2 &+ c; c = g2 >> 26; g2 &= 0x3ffffff
237
+ var g3 = h3 &+ c; c = g3 >> 26; g3 &= 0x3ffffff
238
+ var g4 = h4 &+ c &- (1 << 26)
239
+
240
+ // select h if h < p, else h - p
241
+ var mask = (g4 >> 31) &- 1
242
+ g0 &= mask; g1 &= mask; g2 &= mask; g3 &= mask; g4 &= mask
243
+ mask = ~mask
244
+ h0 = (h0 & mask) | g0
245
+ h1 = (h1 & mask) | g1
246
+ h2 = (h2 & mask) | g2
247
+ h3 = (h3 & mask) | g3
248
+ h4 = (h4 & mask) | g4
249
+
250
+ // h = h % 2^128
251
+ h0 = (h0 | (h1 << 26))
252
+ h1 = ((h1 >> 6) | (h2 << 20))
253
+ h2 = ((h2 >> 12) | (h3 << 14))
254
+ h3 = ((h3 >> 18) | (h4 << 8))
255
+
256
+ // mac = (h + pad) % 2^128
257
+ var f = UInt64(h0) + UInt64(load32(key, 16)); h0 = UInt32(truncatingIfNeeded: f)
258
+ f = UInt64(h1) + UInt64(load32(key, 20)) + (f >> 32); h1 = UInt32(truncatingIfNeeded: f)
259
+ f = UInt64(h2) + UInt64(load32(key, 24)) + (f >> 32); h2 = UInt32(truncatingIfNeeded: f)
260
+ f = UInt64(h3) + UInt64(load32(key, 28)) + (f >> 32); h3 = UInt32(truncatingIfNeeded: f)
261
+
262
+ var out = [UInt8]()
263
+ for w in [h0, h1, h2, h3] {
264
+ for i in 0..<4 { out.append(UInt8(truncatingIfNeeded: w >> (8 * UInt32(i)))) }
265
+ }
266
+ return out
267
+ }
268
+ }
269
+
270
+ // MARK: - BLAKE2b (RFC 7693), unkeyed
271
+
272
+ enum Blake2b {
273
+ private static let iv: [UInt64] = [
274
+ 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1,
275
+ 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179,
276
+ ]
277
+
278
+ private static let sigma: [[Int]] = [
279
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
280
+ [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
281
+ [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
282
+ [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
283
+ [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
284
+ [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
285
+ [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
286
+ [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
287
+ [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
288
+ [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
289
+ ]
290
+
291
+ @inline(__always) private static func rotr(_ v: UInt64, _ n: UInt64) -> UInt64 {
292
+ (v >> n) | (v << (64 - n))
293
+ }
294
+
295
+ private static func compress(_ h: inout [UInt64], block: [UInt8], counter: UInt64, last: Bool) {
296
+ var m = [UInt64](repeating: 0, count: 16)
297
+ for i in 0..<16 {
298
+ var w: UInt64 = 0
299
+ for j in 0..<8 { w |= UInt64(block[i * 8 + j]) << (8 * UInt64(j)) }
300
+ m[i] = w
301
+ }
302
+ var v = h + iv
303
+ v[12] ^= counter
304
+ // counter high word stays 0: inputs here are far below 2^64 bytes
305
+ if last { v[14] = ~v[14] }
306
+
307
+ @inline(__always) func g(_ a: Int, _ b: Int, _ c: Int, _ d: Int, _ x: UInt64, _ y: UInt64) {
308
+ v[a] = v[a] &+ v[b] &+ x
309
+ v[d] = rotr(v[d] ^ v[a], 32)
310
+ v[c] = v[c] &+ v[d]
311
+ v[b] = rotr(v[b] ^ v[c], 24)
312
+ v[a] = v[a] &+ v[b] &+ y
313
+ v[d] = rotr(v[d] ^ v[a], 16)
314
+ v[c] = v[c] &+ v[d]
315
+ v[b] = rotr(v[b] ^ v[c], 63)
316
+ }
317
+
318
+ for round in 0..<12 {
319
+ let s = sigma[round % 10]
320
+ g(0, 4, 8, 12, m[s[0]], m[s[1]])
321
+ g(1, 5, 9, 13, m[s[2]], m[s[3]])
322
+ g(2, 6, 10, 14, m[s[4]], m[s[5]])
323
+ g(3, 7, 11, 15, m[s[6]], m[s[7]])
324
+ g(0, 5, 10, 15, m[s[8]], m[s[9]])
325
+ g(1, 6, 11, 12, m[s[10]], m[s[11]])
326
+ g(2, 7, 8, 13, m[s[12]], m[s[13]])
327
+ g(3, 4, 9, 14, m[s[14]], m[s[15]])
328
+ }
329
+ for i in 0..<8 { h[i] ^= v[i] ^ v[i + 8] }
330
+ }
331
+
332
+ /// BLAKE2b with no key, `outputLength` 1...64 bytes (`crypto_generichash`).
333
+ static func hash(_ message: [UInt8], outputLength: Int) -> [UInt8] {
334
+ precondition((1...64).contains(outputLength))
335
+ var h = iv
336
+ h[0] ^= 0x01010000 ^ UInt64(outputLength)
337
+
338
+ var offset = 0
339
+ // Every full block except the last one; the last block (full or not) is final.
340
+ while message.count - offset > 128 {
341
+ compress(&h, block: Array(message[offset..<offset + 128]), counter: UInt64(offset + 128), last: false)
342
+ offset += 128
343
+ }
344
+ var last = Array(message[offset...])
345
+ let consumed = UInt64(message.count)
346
+ while last.count < 128 { last.append(0) }
347
+ compress(&h, block: last, counter: consumed, last: true)
348
+
349
+ var out = [UInt8]()
350
+ for w in h {
351
+ for i in 0..<8 { out.append(UInt8(truncatingIfNeeded: w >> (8 * UInt64(i)))) }
352
+ }
353
+ return Array(out[0..<outputLength])
354
+ }
355
+ }
@@ -0,0 +1,24 @@
1
+ // Sealed boxes made by libsodium for known keys, for the Swift check.
2
+ import sodium from 'libsodium-wrappers-sumo';
3
+ import { createHash } from 'node:crypto';
4
+ await sodium.ready;
5
+ const b64 = (u) => Buffer.from(u).toString('base64');
6
+ const vectors = [];
7
+ const messages = ['', 'a', 'hello', 'x'.repeat(15), 'y'.repeat(16), 'z'.repeat(17), 'q'.repeat(1000),
8
+ JSON.stringify({ title: 'Alice', body: 'héllo 👋 — ok', sender: 'Alice', senderJid: 'a@x', chat: 'Team', chatJid: 't@c.x', msgID: '1', image: '' })];
9
+ for (const message of messages) {
10
+ const pair = sodium.crypto_box_keypair();
11
+ const sealed = sodium.crypto_box_seal(sodium.from_string(message), pair.publicKey);
12
+ vectors.push({ sk: b64(pair.privateKey), pk: b64(pair.publicKey), sealed: b64(sealed), plaintext: b64(sodium.from_string(message)),
13
+ kid: createHash('sha256').update(Buffer.from(pair.publicKey)).digest('hex').slice(0, 16) });
14
+ }
15
+ // primitives
16
+ const key = sodium.randombytes_buf(32), nonce = sodium.randombytes_buf(24), msg = sodium.randombytes_buf(100);
17
+ const box = sodium.crypto_secretbox_easy(msg, nonce, key);
18
+ const hs = null;
19
+ const bl = sodium.crypto_generichash(24, msg);
20
+ const pkey = sodium.randombytes_buf(32);
21
+ const auth = sodium.crypto_onetimeauth(msg, pkey);
22
+ const out = { vectors, secretbox: { key: b64(key), nonce: b64(nonce), msg: b64(msg), box: b64(box) },
23
+ blake2b24: { msg: b64(msg), out: b64(bl) }, poly1305: { key: b64(pkey), msg: b64(msg), out: b64(auth) } };
24
+ process.stdout.write(JSON.stringify(out));
@@ -0,0 +1,37 @@
1
+ import Foundation
2
+
3
+ struct Vector: Decodable { let sk: String; let pk: String; let sealed: String; let plaintext: String; let kid: String }
4
+ struct Secretbox: Decodable { let key: String; let nonce: String; let msg: String; let box: String }
5
+ struct Blake: Decodable { let msg: String; let out: String }
6
+ struct Poly: Decodable { let key: String; let msg: String; let out: String }
7
+ struct File: Decodable { let vectors: [Vector]; let secretbox: Secretbox; let blake2b24: Blake; let poly1305: Poly }
8
+
9
+ func b(_ s: String) -> [UInt8] { [UInt8](Data(base64Encoded: s)!) }
10
+
11
+ let path = CommandLine.arguments[1]
12
+ let file = try! JSONDecoder().decode(File.self, from: Data(contentsOf: URL(fileURLWithPath: path)))
13
+ var failures = 0
14
+ func check(_ name: String, _ ok: Bool) {
15
+ print((ok ? "ok " : "FAIL ") + name)
16
+ if !ok { failures += 1 }
17
+ }
18
+
19
+ check("blake2b-24", Blake2b.hash(b(file.blake2b24.msg), outputLength: 24) == b(file.blake2b24.out))
20
+ check("poly1305", Poly1305.mac(b(file.poly1305.msg), key: b(file.poly1305.key)) == b(file.poly1305.out))
21
+ check("secretbox open", SealedBox.secretboxOpen(b(file.secretbox.box), nonce: b(file.secretbox.nonce), key: b(file.secretbox.key)) == b(file.secretbox.msg))
22
+ var damaged = b(file.secretbox.box); damaged[damaged.count - 1] ^= 1
23
+ check("secretbox rejects a damaged box", SealedBox.secretboxOpen(damaged, nonce: b(file.secretbox.nonce), key: b(file.secretbox.key)) == nil)
24
+
25
+ for (i, v) in file.vectors.enumerated() {
26
+ let sk = b(v.sk)
27
+ check("vector \(i) public key", SealedBox.publicKey(of: sk) == b(v.pk))
28
+ let opened = SealedBox.open(b(v.sealed), secretKey: sk)
29
+ check("vector \(i) opens (\(b(v.plaintext).count) bytes)", opened == b(v.plaintext))
30
+ var bad = b(v.sealed); bad[40] ^= 0x80
31
+ check("vector \(i) rejects a damaged box", SealedBox.open(bad, secretKey: sk) == nil)
32
+ let other = b(file.vectors[(i + 1) % file.vectors.count].sk)
33
+ check("vector \(i) rejects another key", SealedBox.open(b(v.sealed), secretKey: other) == nil)
34
+ }
35
+ check("short box", SealedBox.open([UInt8](repeating: 1, count: 47), secretKey: b(file.vectors[0].sk)) == nil)
36
+ print(failures == 0 ? "ALL OK" : "\(failures) FAILED")
37
+ exit(failures == 0 ? 0 : 1)
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ # Pins SealedBox.swift against libsodium: vectors.json holds sealed boxes,
3
+ # a secretbox, a BLAKE2b-192 and a Poly1305 tag made by libsodium
4
+ # (gen-vectors.mjs, needs `npm i libsodium-wrappers-sumo` next to it), and
5
+ # main.swift opens them with the extension's code. Needs Xcode's swiftc.
6
+ #
7
+ # plugins/notification-service/check/run.sh
8
+ set -euo pipefail
9
+ cd "$(dirname "$0")"
10
+ out="$(mktemp -d)"
11
+ swiftc -O ../SealedBox.swift main.swift -o "$out/check"
12
+ "$out/check" vectors.json
@@ -0,0 +1 @@
1
+ {"vectors":[{"sk":"psM/YEAHlVzqtN7KovphUjE5m7l17ff2MtiE3XU3Rwc=","pk":"P0AoVn+A+5HOI2AWQR5LXYfM9PHq3kJp1FCs2HRCkXY=","sealed":"G3wRmRzod33/f03nW+VhrVO2l4nnAvjGQdTvmzclOxE4cjpoZLBRb+HnFofI4ypH","plaintext":"","kid":"fdc829f52a022956"},{"sk":"Ogi04QlXCW1IiS2xC3KnnFkpbcfABAGUkGb4GxcLEO8=","pk":"FQQKe0KEz2TtlctxVMQcpuu+XNTygtNsiu+D6PtUAFE=","sealed":"bKRCdjHWR0LV9UWcpjKTYXdYGApsnfDm2q5MCWQCFzPQ4CEOt0+cnBZbWjVBoP5VVw==","plaintext":"YQ==","kid":"cb31ce9c7a59fb37"},{"sk":"4Eyj9vf7de+t6iaYSg84h1ewjMdotI4ModztdG3ATg8=","pk":"Otr0gAyXgdMbayn6xJPrblxVl69qQ59XdWBjRpX7JQc=","sealed":"9ouwN9/psosK75W1BruvhtjoJwp6JtazX1uhIrq+5QZuRFKFrW5LIkFpY5DN4irDdsugdZ0=","plaintext":"aGVsbG8=","kid":"730fca4a57de1bc7"},{"sk":"7ny883FomgIgDoED095pPBX0wwDH4bakBIBeMwkylrY=","pk":"mlNQn4wPpUoHdW4sn/OzwTuHtloRvWrbHXDuuR1qYhw=","sealed":"46McjbyP1UQ0ZlHmeQJatsWhiKxdiREiDZ8VEIRVcH/GSjfUNIEXiu0sBMTJDzCS2DsN/IsltMFuGq0/gpNi","plaintext":"eHh4eHh4eHh4eHh4eHh4","kid":"9306f2956c55f34a"},{"sk":"SiT54TCZ8JikZYE/8d5HO9Py4h2frgC4UeyFOt1nWGc=","pk":"hlinYRVWthcOWNcpj1+8GLiPglaMduupEJZbPH59wkg=","sealed":"7PFOl2gyBwvfoxfiqOJujHB5tPTTXFlwrH4UoLnGYEw/+Em1x466vmTFEMqLqiFLF+/5vYJAfprmr0VxmnGJEg==","plaintext":"eXl5eXl5eXl5eXl5eXl5eQ==","kid":"e2a6c8eb7bf954f2"},{"sk":"YK2tqAoGCoeOR04drKeXQ0p154uTagHKz1fFFh5QWvI=","pk":"jWEcVvha+Q+pYCae2Q078liIUW8nxqdVU8lFMWu3lBY=","sealed":"My+/ng0WLFg0rdK2ndFbojUDKBWzetQlfSEju7a5Fy5Ea+gAwGuwHNem4bhOc7JYPtWPDwSHhjVVvB6iNzP2yew=","plaintext":"enp6enp6enp6enp6enp6eno=","kid":"7d297889ca460ceb"},{"sk":"4lpxBLHJVWN1LILEZ9ewSH0FYO4yF8IPQGs41y5E7cI=","pk":"tXrQ+qOYKuTvEU66adVcFPI2aBV4mH+zan5F0iExmwE=","sealed":"/SM2pXuwyPneAQhJutEN00rUTK4Ngnfgto3jX+3hGFoRWy/JJAXEp+i7huR8bbhcs2DUH/V4soWiE1/8mq4qrIbJJUlZWdePskVnU88de5vN/mfPFo+TzaHQVaAudAM+Qf3SkT5t+qxECXdY/9rpEUkI44FQkWz5zxP67RwtYe5nJQxTyM25mr31pKqr4j7Od3vQXLOxhROPnn4ftJC7UAnJuAmhV81TXnWUKakUJFD87/Ge2EVt/MJfplndu7tC0KDTRRpwd+KLsAE0FaSRqvOD9eg+muND7GhJEhMLiTB+ZjHB8dbXw/JQPvTl7Bdynh86UGDjG2C/QchiK26OkYHRLNFgHjeMm8Vs0yDM+FIsiNYtG5qKym2qlLP+E9+tgJW/9Mo3CWA+0B3iOoRebmfab9maJBHf1CoZv8oFeXliPGBdDaj+cIxAfY+W28cQAHuwrZ/tmfdIoW+qxkFN2fAquYc+NY31s0RVuPZje3XnNAgou8bI8dokiXx+eELAXFfPWo7fdI6ZUFTQA69Y+2ZOtqPJ+Am63rZg40TLsJJhvwjJkhfRaNbvNxlh3YQ2LZ3OkN+SI4yCG0LcDYxWwD2D18Npjajnn+X40RfAVKU77MFl4sFUkK7VhCZrxfQ2LxRNT9OQM4dOp/uf74258c7bV9//qckfO7PsZZCu35aGXF6JlhfDLwU8V36/3TE0eKgJIhV/PsbsHk9Wq6r4Io2r3sGpk9xfqjrg/n1IMFrl3C6L8szSJWjd/P/mnh+L95DnJfX6hMh13Z5R19e//2pTQCjUk9o4fJJef4SUgc15BjejcPPyPsyK/eaSpi/pbkVrprvCkUFmZmXHOtua/YAhtJ3sKlIb6w5jDg0URXc0XeSBJvbCY/V7EuLLBtPnpld6emExNYXTI//Qi7E76zOYQ5PZLU8rJgKEI9DZ1FqGKMTHvgydKscMJSAPzZ2fDbEMjKBvWN+CUAiLpEYma+ciF8mLMB0VZeeJyAV+oEZSMG4pDyOOh1smR5tJ1/McPkXht5gTuHfMRfaVQZa0cr/hzPbWhFMBvgJOWdUbYKExhrXjnrHC28xPsGfqT0c/atP3ySyuuDiGLcIpzLo4TAM2QJVE2f/2ia++ptoKhFp/Q3kQ4bqPenHxf3AbWCveHzmfQ9BRlwZ8aLPPYWtzG4s0bz48RpLRO2Cnxa9/MbNaG8WumuG6sK9wU5SK4diau82G1GVxKSE/32lJKBx4OnI2nH6SkB9h7H1UBwaYZPgkR75rocaQeAn/ley/4IWb/5NKbTx3AIYUIpPSNvfzFbzunb+hDLOkJiKDodQE6E+JYbOctbogmWQULCyBW1x1du38VeMUJjlSfse9nRN3vYkvf9DzkZIVc3Fd8WcIzO8MDEjy/VBedg==","plaintext":"cXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcQ==","kid":"3f3c6f78c9cc4b77"},{"sk":"85ijOhZXrKrVfynAK+oLZ9y8Gl4fxXmkbvy/seh9/Qc=","pk":"ExZYsb44gdGGq48vel9El5jzMnaD60ND81UCJx8RKy0=","sealed":"0qM9JBNfalw7KVnCCfumgNBwsIt3qSn0Gnuo2N9fQybkAvSAV0PBug210jJPb4nAKE32EhNq7977njuOMvrAZzMA2AuI9GKNxlTYpdeV6+0G4ZPcZpH0eR5Joblk7myRF622E+23XmuBI4q4dpYNJaWr/SGzUQ951UFBiqMwoDvbrNPfNCpQ8+0q3Pg4flEHEjzF7hVuSJX+elQ2E4gMTJRJmm7Fx6c9dJbPYpO3P7P8EN9R0oxv","plaintext":"eyJ0aXRsZSI6IkFsaWNlIiwiYm9keSI6ImjDqWxsbyDwn5GLIOKAlCBvayIsInNlbmRlciI6IkFsaWNlIiwic2VuZGVySmlkIjoiYUB4IiwiY2hhdCI6IlRlYW0iLCJjaGF0SmlkIjoidEBjLngiLCJtc2dJRCI6IjEiLCJpbWFnZSI6IiJ9","kid":"c396a01c6df912f0"}],"secretbox":{"key":"PNi8//d+s1h1376uDwtQb02HwDJ867wS69nn2Nh+YGk=","nonce":"MJALUxy6rktv0IeXa5Eel2TPg3PRx3Ra","msg":"7gJAxFbQvjY9WUcggw+Eip8eld00817fLWH3YPBiQlFWAF20JCKXf/KfxxpQ4yk6nkJjUJdowDYhoHPKiPKcPrH6nUWJ+0WHHD7BLziabdCNxWr0WLcHqYlfevynoDBOZINpNA==","box":"ABn+r2ayhlV/tBVzYZUS1PoxVKdLrv+IZMTgOk3Czqlz3bVm+VHOolF1XW4XCodO7sL7ikl5dzbGuUye/XRuIgQxGiWXIyewmK9NKv/7CJG4pIujo0Jqydp0LIvWL4fvcZhDCzmR+dIaUfHIQizDs2ofGFc="},"blake2b24":{"msg":"7gJAxFbQvjY9WUcggw+Eip8eld00817fLWH3YPBiQlFWAF20JCKXf/KfxxpQ4yk6nkJjUJdowDYhoHPKiPKcPrH6nUWJ+0WHHD7BLziabdCNxWr0WLcHqYlfevynoDBOZINpNA==","out":"aOiFzIaoPYSHzp1/PrvwUfTbLOdT/mwY"},"poly1305":{"key":"kRbuceM6lJgm3xkFq9APi50xfeG1DQc2HaG7bunl3rA=","msg":"7gJAxFbQvjY9WUcggw+Eip8eld00817fLWH3YPBiQlFWAF20JCKXf/KfxxpQ4yk6nkJjUJdowDYhoHPKiPKcPrH6nUWJ+0WHHD7BLziabdCNxWr0WLcHqYlfevynoDBOZINpNA==","out":"6iotU8uBgFrEu9FpQuxrkQ=="}}