@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.
- package/CHANGELOG.md +3 -1
- package/README.md +6 -3
- package/lib/commonjs/helpers/secureStoreRuntime.js +10 -1
- package/lib/commonjs/helpers/secureStoreRuntime.js.map +1 -1
- package/lib/commonjs/main.js +81 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/api-requests/push.api.js +22 -7
- package/lib/commonjs/networking/api-requests/push.api.js.map +1 -1
- package/lib/commonjs/push/pushKeyStore.js +169 -0
- package/lib/commonjs/push/pushKeyStore.js.map +1 -0
- package/lib/commonjs/push/sealedBox.js +106 -0
- package/lib/commonjs/push/sealedBox.js.map +1 -0
- package/lib/commonjs/push/sealedPush.js +124 -0
- package/lib/commonjs/push/sealedPush.js.map +1 -0
- package/lib/commonjs/push/sealedPushTask.js +195 -0
- package/lib/commonjs/push/sealedPushTask.js.map +1 -0
- package/lib/commonjs/services/pushRegistration.js +12 -7
- package/lib/commonjs/services/pushRegistration.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/helpers/secureStoreRuntime.js +10 -1
- package/lib/module/helpers/secureStoreRuntime.js.map +1 -1
- package/lib/module/main.js +6 -0
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/api-requests/push.api.js +21 -6
- package/lib/module/networking/api-requests/push.api.js.map +1 -1
- package/lib/module/push/pushKeyStore.js +159 -0
- package/lib/module/push/pushKeyStore.js.map +1 -0
- package/lib/module/push/sealedBox.js +97 -0
- package/lib/module/push/sealedBox.js.map +1 -0
- package/lib/module/push/sealedPush.js +117 -0
- package/lib/module/push/sealedPush.js.map +1 -0
- package/lib/module/push/sealedPushTask.js +187 -0
- package/lib/module/push/sealedPushTask.js.map +1 -0
- package/lib/module/services/pushRegistration.js +12 -7
- package/lib/module/services/pushRegistration.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/helpers/secureStoreRuntime.d.ts +17 -3
- package/lib/typescript/helpers/secureStoreRuntime.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +6 -0
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/push.api.d.ts +14 -2
- package/lib/typescript/networking/api-requests/push.api.d.ts.map +1 -1
- package/lib/typescript/push/pushKeyStore.d.ts +35 -0
- package/lib/typescript/push/pushKeyStore.d.ts.map +1 -0
- package/lib/typescript/push/sealedBox.d.ts +24 -0
- package/lib/typescript/push/sealedBox.d.ts.map +1 -0
- package/lib/typescript/push/sealedPush.d.ts +61 -0
- package/lib/typescript/push/sealedPush.d.ts.map +1 -0
- package/lib/typescript/push/sealedPushTask.d.ts +33 -0
- package/lib/typescript/push/sealedPushTask.d.ts.map +1 -0
- package/lib/typescript/services/pushRegistration.d.ts +8 -0
- package/lib/typescript/services/pushRegistration.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +6 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/package.json +11 -1
- package/plugins/notification-service/NotificationService.swift +113 -0
- package/plugins/notification-service/SealedBox.swift +355 -0
- package/plugins/notification-service/check/gen-vectors.mjs +24 -0
- package/plugins/notification-service/check/main.swift +37 -0
- package/plugins/notification-service/check/run.sh +12 -0
- package/plugins/notification-service/check/vectors.json +1 -0
- package/plugins/withNotificationServiceExtension.js +70 -18
- package/src/helpers/secureStoreRuntime.ts +21 -3
- package/src/main.ts +30 -0
- package/src/networking/api-requests/push.api.ts +29 -4
- package/src/push/pushKeyStore.ts +163 -0
- package/src/push/sealedBox.ts +104 -0
- package/src/push/sealedPush.ts +127 -0
- package/src/push/sealedPushTask.ts +175 -0
- package/src/services/pushRegistration.ts +25 -5
- package/src/types/types.ts +6 -0
- 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=="}}
|