@authon/react-native-wallet 0.0.0-stage → 0.4.0

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 (183) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +251 -2
  3. package/android/build.gradle +40 -0
  4. package/android/consumer-rules.pro +10 -0
  5. package/android/src/main/AndroidManifest.xml +8 -0
  6. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +235 -0
  7. package/android/src/vendor/GENERATED.md +4 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +218 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
  21. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
  22. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
  23. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +86 -0
  24. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
  25. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +236 -0
  26. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
  27. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
  28. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
  29. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
  30. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
  31. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
  32. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +361 -0
  33. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
  34. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +74 -0
  35. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +551 -0
  36. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +78 -0
  37. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
  38. package/build/authonContext.d.ts +19 -0
  39. package/build/authonContext.d.ts.map +1 -0
  40. package/build/authonContext.js +20 -0
  41. package/build/authonContext.js.map +1 -0
  42. package/build/client.d.ts +92 -0
  43. package/build/client.d.ts.map +1 -0
  44. package/build/client.js +431 -0
  45. package/build/client.js.map +1 -0
  46. package/build/errors.d.ts +25 -0
  47. package/build/errors.d.ts.map +1 -0
  48. package/build/errors.js +15 -0
  49. package/build/errors.js.map +1 -0
  50. package/build/index.d.ts +7 -0
  51. package/build/index.d.ts.map +1 -0
  52. package/build/index.js +4 -0
  53. package/build/index.js.map +1 -0
  54. package/build/native.d.ts +43 -0
  55. package/build/native.d.ts.map +1 -0
  56. package/build/native.js +12 -0
  57. package/build/native.js.map +1 -0
  58. package/build/types.d.ts +167 -0
  59. package/build/types.d.ts.map +1 -0
  60. package/build/types.js +3 -0
  61. package/build/types.js.map +1 -0
  62. package/build/useAuthonWallet.d.ts +25 -0
  63. package/build/useAuthonWallet.d.ts.map +1 -0
  64. package/build/useAuthonWallet.js +77 -0
  65. package/build/useAuthonWallet.js.map +1 -0
  66. package/expo-module.config.json +9 -0
  67. package/ios/AuthonReactNativeWallet.podspec +41 -0
  68. package/ios/AuthonWalletModule.swift +233 -0
  69. package/ios/vendor/AuthonWallet/AuthonWallet.swift +232 -0
  70. package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
  71. package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
  72. package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
  73. package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
  74. package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
  75. package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
  76. package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
  77. package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
  78. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +128 -0
  79. package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
  80. package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
  81. package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
  82. package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
  83. package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
  84. package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
  85. package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
  86. package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
  87. package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
  88. package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
  89. package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
  90. package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
  91. package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
  92. package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
  93. package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
  94. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +361 -0
  95. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +654 -0
  96. package/ios/vendor/AuthonWallet/WalletError.swift +84 -0
  97. package/ios/vendor/CKeccak/LICENSE +22 -0
  98. package/ios/vendor/CKeccak/README.md +30 -0
  99. package/ios/vendor/CKeccak/byte_order.h +59 -0
  100. package/ios/vendor/CKeccak/include/ckeccak.h +13 -0
  101. package/ios/vendor/CKeccak/memzero.c +75 -0
  102. package/ios/vendor/CKeccak/memzero.h +8 -0
  103. package/ios/vendor/CKeccak/options.h +99 -0
  104. package/ios/vendor/CKeccak/ripemd160.c +343 -0
  105. package/ios/vendor/CKeccak/ripemd160.h +22 -0
  106. package/ios/vendor/CKeccak/sha3.c +413 -0
  107. package/ios/vendor/CKeccak/sha3.h +89 -0
  108. package/ios/vendor/CMonocypher/LICENCE.md +173 -0
  109. package/ios/vendor/CMonocypher/README.md +27 -0
  110. package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
  111. package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
  112. package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
  113. package/ios/vendor/CMonocypher/monocypher.c +2956 -0
  114. package/ios/vendor/CSecp256k1/COPYING +19 -0
  115. package/ios/vendor/CSecp256k1/README.md +35 -0
  116. package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
  117. package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
  118. package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
  119. package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
  120. package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
  121. package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
  122. package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
  123. package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
  124. package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
  125. package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
  126. package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
  127. package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
  128. package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
  129. package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
  130. package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
  131. package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
  132. package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
  133. package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
  134. package/ios/vendor/CSecp256k1/src/field.h +350 -0
  135. package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
  136. package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
  137. package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
  138. package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
  139. package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
  140. package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
  141. package/ios/vendor/CSecp256k1/src/group.h +212 -0
  142. package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
  143. package/ios/vendor/CSecp256k1/src/hash.h +44 -0
  144. package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
  145. package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
  146. package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
  147. package/ios/vendor/CSecp256k1/src/int128.h +90 -0
  148. package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
  149. package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
  150. package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
  151. package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
  152. package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
  153. package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
  154. package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
  155. package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
  156. package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
  157. package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
  158. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
  159. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
  160. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
  161. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
  162. package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
  163. package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
  164. package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
  165. package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
  166. package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
  167. package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
  168. package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
  169. package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
  170. package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
  171. package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
  172. package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
  173. package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
  174. package/ios/vendor/CSecp256k1/src/util.h +451 -0
  175. package/ios/vendor/GENERATED.md +4 -0
  176. package/package.json +66 -3
  177. package/src/authonContext.ts +30 -0
  178. package/src/client.ts +459 -0
  179. package/src/errors.ts +58 -0
  180. package/src/index.ts +26 -0
  181. package/src/native.ts +52 -0
  182. package/src/types.ts +171 -0
  183. package/src/useAuthonWallet.ts +103 -0
@@ -0,0 +1,351 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Wallet/DeviceStore.swift. Do not edit: change the source and run the script.
2
+ import CryptoKit
3
+ import Foundation
4
+ import Security
5
+
6
+ /// What this device keeps for one project and user: the sealed device share D and a
7
+ /// reference to the non-exportable device keys. Never the wallet key.
8
+ public struct DeviceRecord: Codable, Equatable, Sendable {
9
+ /// `projectId \0 userId`
10
+ public var key: String
11
+ public var deviceId: String
12
+ public var walletId: String
13
+ public var address: String
14
+ public var shareVersion: Int
15
+ /// Unix milliseconds.
16
+ public var createdAt: Double
17
+ public var publicJwk: DevicePublicJwk
18
+ /// Store-specific reference to the device keys.
19
+ public var keyHandle: Data
20
+ /// D sealed by the store.
21
+ public var sealedShare: Data
22
+ }
23
+
24
+ /// A freshly generated device key pair (signing key and share-sealing key).
25
+ public struct DeviceKeys: Equatable, Sendable {
26
+ public var handle: Data
27
+ public var publicJwk: DevicePublicJwk
28
+ }
29
+
30
+ /// Device storage, the native counterpart of the frame's IndexedDB store.
31
+ /// `SecureEnclaveDeviceStore` is the production implementation; tests and the simulator
32
+ /// can use `SoftwareDeviceStore`.
33
+ public protocol DeviceStore: AnyObject, Sendable {
34
+ func generateKeys() throws -> DeviceKeys
35
+ /// Removes keys that never made it into a stored record.
36
+ func discardKeys(_ keys: DeviceKeys)
37
+ func seal(_ share: [UInt8], keys: DeviceKeys, storageKey: String) throws -> Data
38
+ /// Throws NEEDS_RESTORE when the share cannot be opened.
39
+ func open(_ record: DeviceRecord) throws -> [UInt8]
40
+ /// ECDSA P-256 / SHA-256 over the UTF-8 challenge, raw r || s (64 bytes) like WebCrypto.
41
+ func sign(challenge: String, record: DeviceRecord) throws -> [UInt8]
42
+ func get(_ key: String) throws -> DeviceRecord?
43
+ /// Replaces the record for `record.key`. Keys of a replaced record are removed.
44
+ func put(_ record: DeviceRecord) throws
45
+ func delete(_ key: String) throws
46
+ }
47
+
48
+ func storageKey(projectId: String, userId: String) -> String { "\(projectId)\u{0}\(userId)" }
49
+
50
+ /// Generic-password items holding DeviceRecord JSON. This-device-only, never synchronized.
51
+ struct KeychainRecords {
52
+ let service: String
53
+
54
+ private func base(_ key: String) -> [String: Any] {
55
+ var q: [String: Any] = [
56
+ kSecClass as String: kSecClassGenericPassword,
57
+ kSecAttrService as String: service,
58
+ // The storage key contains a NUL; the account is its hash.
59
+ kSecAttrAccount as String: Bytes.hex(Bytes.sha256(Array(key.utf8))),
60
+ kSecAttrSynchronizable as String: kCFBooleanFalse!,
61
+ ]
62
+ #if os(macOS)
63
+ q[kSecUseDataProtectionKeychain as String] = true
64
+ #endif
65
+ return q
66
+ }
67
+
68
+ func get(_ key: String) throws -> DeviceRecord? {
69
+ var q = base(key)
70
+ q[kSecReturnData as String] = true
71
+ q[kSecMatchLimit as String] = kSecMatchLimitOne
72
+ var out: AnyObject?
73
+ let status = SecItemCopyMatching(q as CFDictionary, &out)
74
+ if status == errSecItemNotFound { return nil }
75
+ guard status == errSecSuccess, let data = out as? Data else { throw WalletError(.storageUnavailable, "keychain read failed (\(status))") }
76
+ guard let record = try? JSONDecoder().decode(DeviceRecord.self, from: data), record.key == key else { return nil }
77
+ return record
78
+ }
79
+
80
+ func put(_ record: DeviceRecord) throws {
81
+ let data = try JSONEncoder().encode(record)
82
+ let q = base(record.key)
83
+ let update: [String: Any] = [
84
+ kSecValueData as String: data,
85
+ kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
86
+ ]
87
+ var status = SecItemUpdate(q as CFDictionary, update as CFDictionary)
88
+ if status == errSecItemNotFound {
89
+ var add = q
90
+ add.merge(update) { $1 }
91
+ status = SecItemAdd(add as CFDictionary, nil)
92
+ }
93
+ guard status == errSecSuccess else { throw WalletError(.storageUnavailable, "keychain write failed (\(status))") }
94
+ }
95
+
96
+ func delete(_ key: String) throws {
97
+ let status = SecItemDelete(base(key) as CFDictionary)
98
+ guard status == errSecSuccess || status == errSecItemNotFound else {
99
+ throw WalletError(.storageUnavailable, "keychain delete failed (\(status))")
100
+ }
101
+ }
102
+ }
103
+
104
+ /// P-256 keys in the Secure Enclave. The signing key's public half is the device key the API
105
+ /// knows; a second enclave key seals D with ECIES (X9.63 SHA-256 KDF, AES-GCM). D's record is a
106
+ /// this-device-only Keychain item, so it is not in backups or device transfers.
107
+ public final class SecureEnclaveDeviceStore: DeviceStore, @unchecked Sendable {
108
+ private let records: KeychainRecords
109
+ private let tagPrefix: String
110
+ private static let algorithm = SecKeyAlgorithm.eciesEncryptionCofactorVariableIVX963SHA256AESGCM
111
+
112
+ /// Throws STORAGE_UNAVAILABLE on devices without a Secure Enclave (including the simulator).
113
+ public init(service: String = "dev.authon.wallet") throws {
114
+ guard SecureEnclave.isAvailable else {
115
+ throw WalletError(.storageUnavailable, "this device has no Secure Enclave")
116
+ }
117
+ records = KeychainRecords(service: service)
118
+ tagPrefix = service + ".key."
119
+ }
120
+
121
+ private struct Handle: Codable {
122
+ var sign: String
123
+ var seal: String
124
+ }
125
+
126
+ private func handle(_ data: Data) throws -> Handle {
127
+ guard let h = try? JSONDecoder().decode(Handle.self, from: data) else { throw WalletError(.needsRestore, "device key reference unreadable") }
128
+ return h
129
+ }
130
+
131
+ private func createKey(tag: String) throws -> SecKey {
132
+ var error: Unmanaged<CFError>?
133
+ guard let access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, &error) else {
134
+ throw WalletError(.storageUnavailable, "access control unavailable")
135
+ }
136
+ var attrs: [String: Any] = [
137
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
138
+ kSecAttrKeySizeInBits as String: 256,
139
+ kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
140
+ kSecPrivateKeyAttrs as String: [
141
+ kSecAttrIsPermanent as String: true,
142
+ kSecAttrApplicationTag as String: Data(tag.utf8),
143
+ kSecAttrAccessControl as String: access,
144
+ ] as [String: Any],
145
+ ]
146
+ #if os(macOS)
147
+ attrs[kSecUseDataProtectionKeychain as String] = true
148
+ #endif
149
+ guard let key = SecKeyCreateRandomKey(attrs as CFDictionary, &error) else {
150
+ throw WalletError(.storageUnavailable, "secure enclave key creation failed")
151
+ }
152
+ return key
153
+ }
154
+
155
+ private func loadKey(tag: String) throws -> SecKey {
156
+ var q: [String: Any] = [
157
+ kSecClass as String: kSecClassKey,
158
+ kSecAttrApplicationTag as String: Data(tag.utf8),
159
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
160
+ kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
161
+ kSecReturnRef as String: true,
162
+ ]
163
+ #if os(macOS)
164
+ q[kSecUseDataProtectionKeychain as String] = true
165
+ #endif
166
+ var out: AnyObject?
167
+ guard SecItemCopyMatching(q as CFDictionary, &out) == errSecSuccess, let key = out else {
168
+ throw WalletError(.needsRestore, "device key not found")
169
+ }
170
+ return key as! SecKey
171
+ }
172
+
173
+ private func deleteKey(tag: String) {
174
+ var q: [String: Any] = [kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: Data(tag.utf8)]
175
+ #if os(macOS)
176
+ q[kSecUseDataProtectionKeychain as String] = true
177
+ #endif
178
+ SecItemDelete(q as CFDictionary)
179
+ }
180
+
181
+ private func publicPoint(_ key: SecKey) throws -> [UInt8] {
182
+ guard let pub = SecKeyCopyPublicKey(key), let raw = SecKeyCopyExternalRepresentation(pub, nil) as Data? else {
183
+ throw WalletError(.storageUnavailable, "device public key unavailable")
184
+ }
185
+ return Array(raw)
186
+ }
187
+
188
+ public func generateKeys() throws -> DeviceKeys {
189
+ let id = UUID().uuidString
190
+ let h = Handle(sign: tagPrefix + id + ".sign", seal: tagPrefix + id + ".seal")
191
+ do {
192
+ let signKey = try createKey(tag: h.sign)
193
+ _ = try createKey(tag: h.seal)
194
+ return DeviceKeys(handle: try JSONEncoder().encode(h), publicJwk: try DevicePublicJwk(uncompressed: try publicPoint(signKey)))
195
+ } catch {
196
+ deleteKey(tag: h.sign)
197
+ deleteKey(tag: h.seal)
198
+ throw error
199
+ }
200
+ }
201
+
202
+ public func discardKeys(_ keys: DeviceKeys) {
203
+ guard let h = try? handle(keys.handle) else { return }
204
+ deleteKey(tag: h.sign)
205
+ deleteKey(tag: h.seal)
206
+ }
207
+
208
+ public func seal(_ share: [UInt8], keys: DeviceKeys, storageKey: String) throws -> Data {
209
+ let h = try handle(keys.handle)
210
+ guard let pub = SecKeyCopyPublicKey(try loadKey(tag: h.seal)) else { throw WalletError(.storageUnavailable) }
211
+ var plain = Bytes.sha256(Array(storageKey.utf8)) + share
212
+ defer { plain.wipe() }
213
+ var error: Unmanaged<CFError>?
214
+ guard let ct = SecKeyCreateEncryptedData(pub, Self.algorithm, Data(plain) as CFData, &error) as Data? else {
215
+ throw WalletError(.storageUnavailable, "device share cannot be sealed")
216
+ }
217
+ return ct
218
+ }
219
+
220
+ public func open(_ record: DeviceRecord) throws -> [UInt8] {
221
+ let h = try handle(record.keyHandle)
222
+ let key = try loadKey(tag: h.seal)
223
+ var error: Unmanaged<CFError>?
224
+ guard let pt = SecKeyCreateDecryptedData(key, Self.algorithm, record.sealedShare as CFData, &error) as Data? else {
225
+ throw WalletError(.needsRestore, "local share unreadable")
226
+ }
227
+ var plain = Array(pt)
228
+ defer { plain.wipe() }
229
+ guard plain.count > 32, Array(plain[0..<32]) == Bytes.sha256(Array(record.key.utf8)) else {
230
+ throw WalletError(.needsRestore, "local share unreadable")
231
+ }
232
+ return Array(plain[32...])
233
+ }
234
+
235
+ public func sign(challenge: String, record: DeviceRecord) throws -> [UInt8] {
236
+ let key = try loadKey(tag: try handle(record.keyHandle).sign)
237
+ var error: Unmanaged<CFError>?
238
+ guard let der = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, Data(challenge.utf8) as CFData, &error) as Data? else {
239
+ throw WalletError(.needsRestore, "device key unusable")
240
+ }
241
+ return Array(try P256.Signing.ECDSASignature(derRepresentation: der).rawRepresentation)
242
+ }
243
+
244
+ public func get(_ key: String) throws -> DeviceRecord? { try records.get(key) }
245
+
246
+ public func put(_ record: DeviceRecord) throws {
247
+ let previous = try? records.get(record.key)
248
+ try records.put(record)
249
+ if let previous, previous.keyHandle != record.keyHandle, let h = try? handle(previous.keyHandle) {
250
+ deleteKey(tag: h.sign)
251
+ deleteKey(tag: h.seal)
252
+ }
253
+ }
254
+
255
+ public func delete(_ key: String) throws {
256
+ let previous = try? records.get(key)
257
+ try records.delete(key)
258
+ if let previous, let h = try? handle(previous.keyHandle) {
259
+ deleteKey(tag: h.sign)
260
+ deleteKey(tag: h.seal)
261
+ }
262
+ }
263
+ }
264
+
265
+ /// Software P-256 keys and AES-GCM sealing (AAD = storage key, like the web frame). For tests
266
+ /// and the simulator only: the keys are as exportable as any app data. Records are kept in
267
+ /// memory, or in the Keychain with `persistent: true`.
268
+ public final class SoftwareDeviceStore: DeviceStore, @unchecked Sendable {
269
+ private var memory: [String: DeviceRecord] = [:]
270
+ private let keychain: KeychainRecords?
271
+ private let lock = NSLock()
272
+ /// Test hook: when set, `put` stores this instead (simulates storage that drops or corrupts writes).
273
+ public var interceptPut: ((DeviceRecord) -> DeviceRecord?)?
274
+
275
+ /// Refused in release builds on real devices.
276
+ public init(persistent: Bool = false, service: String = "dev.authon.wallet.software") throws {
277
+ #if !DEBUG && !targetEnvironment(simulator)
278
+ throw WalletError(.storageUnavailable, "software device keys are only for tests and the simulator")
279
+ #else
280
+ keychain = persistent ? KeychainRecords(service: service) : nil
281
+ #endif
282
+ }
283
+
284
+ private struct Handle: Codable {
285
+ var sign: Data
286
+ var seal: Data
287
+ }
288
+
289
+ private func handle(_ data: Data) throws -> Handle {
290
+ guard let h = try? JSONDecoder().decode(Handle.self, from: data) else { throw WalletError(.needsRestore, "device key reference unreadable") }
291
+ return h
292
+ }
293
+
294
+ public func generateKeys() throws -> DeviceKeys {
295
+ let sign = P256.Signing.PrivateKey()
296
+ let seal = SymmetricKey(size: .bits256).withUnsafeBytes { Data($0) }
297
+ let h = Handle(sign: sign.rawRepresentation, seal: seal)
298
+ return DeviceKeys(handle: try JSONEncoder().encode(h), publicJwk: try DevicePublicJwk(uncompressed: Array(sign.publicKey.x963Representation)))
299
+ }
300
+
301
+ public func discardKeys(_ keys: DeviceKeys) {}
302
+
303
+ public func seal(_ share: [UInt8], keys: DeviceKeys, storageKey: String) throws -> Data {
304
+ let h = try handle(keys.handle)
305
+ let box = try AES.GCM.seal(share, using: SymmetricKey(data: h.seal), authenticating: Data(storageKey.utf8))
306
+ return box.combined!
307
+ }
308
+
309
+ public func open(_ record: DeviceRecord) throws -> [UInt8] {
310
+ do {
311
+ let h = try handle(record.keyHandle)
312
+ let box = try AES.GCM.SealedBox(combined: record.sealedShare)
313
+ return Array(try AES.GCM.open(box, using: SymmetricKey(data: h.seal), authenticating: Data(record.key.utf8)))
314
+ } catch {
315
+ throw WalletError(.needsRestore, "local share unreadable")
316
+ }
317
+ }
318
+
319
+ public func sign(challenge: String, record: DeviceRecord) throws -> [UInt8] {
320
+ let key = try P256.Signing.PrivateKey(rawRepresentation: try handle(record.keyHandle).sign)
321
+ return Array(try key.signature(for: Data(challenge.utf8)).rawRepresentation)
322
+ }
323
+
324
+ public func get(_ key: String) throws -> DeviceRecord? {
325
+ if let keychain { return try keychain.get(key) }
326
+ lock.lock()
327
+ defer { lock.unlock() }
328
+ return memory[key]
329
+ }
330
+
331
+ public func put(_ record: DeviceRecord) throws {
332
+ let stored: DeviceRecord
333
+ if let interceptPut {
334
+ guard let r = interceptPut(record) else { return }
335
+ stored = r
336
+ } else {
337
+ stored = record
338
+ }
339
+ if let keychain { return try keychain.put(stored) }
340
+ lock.lock()
341
+ defer { lock.unlock() }
342
+ memory[stored.key] = stored
343
+ }
344
+
345
+ public func delete(_ key: String) throws {
346
+ if let keychain { return try keychain.delete(key) }
347
+ lock.lock()
348
+ defer { lock.unlock() }
349
+ memory[key] = nil
350
+ }
351
+ }
@@ -0,0 +1,190 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Wallet/Passkeys.swift. Do not edit: change the source and run the script.
2
+ import AuthenticationServices
3
+ import CryptoKit
4
+ import Foundation
5
+ #if canImport(UIKit)
6
+ import UIKit
7
+ #elseif canImport(AppKit)
8
+ import AppKit
9
+ #endif
10
+
11
+ /// A WebAuthn assertion with base64url fields, as the API verifies it.
12
+ public struct PasskeyAssertion: Equatable, Sendable {
13
+ public var credentialId: String
14
+ public var clientDataJSON: String
15
+ public var authenticatorData: String
16
+ public var signature: String
17
+
18
+ public init(credentialId: String, clientDataJSON: String, authenticatorData: String, signature: String) {
19
+ self.credentialId = credentialId
20
+ self.clientDataJSON = clientDataJSON
21
+ self.authenticatorData = authenticatorData
22
+ self.signature = signature
23
+ }
24
+ }
25
+
26
+ public struct PasskeyCreateResult: Sendable {
27
+ /// base64url
28
+ public var credentialId: String
29
+ /// 32-byte PRF output for the salt.
30
+ public var prfOutput: [UInt8]
31
+ /// SPKI public key (base64url) and COSE algorithm, when the provider can supply them.
32
+ public var publicKey: String?
33
+ public var publicKeyAlg: Int?
34
+ /// The raw attestationObject (base64url); the API reads the COSE key from it.
35
+ public var attestationObject: String?
36
+
37
+ public init(credentialId: String, prfOutput: [UInt8], publicKey: String? = nil, publicKeyAlg: Int? = nil, attestationObject: String? = nil) {
38
+ self.credentialId = credentialId
39
+ self.prfOutput = prfOutput
40
+ self.publicKey = publicKey
41
+ self.publicKeyAlg = publicKeyAlg
42
+ self.attestationObject = attestationObject
43
+ }
44
+ }
45
+
46
+ public struct PasskeyGetResult: Sendable {
47
+ public var prfOutput: [UInt8]
48
+ public var assertion: PasskeyAssertion
49
+
50
+ public init(prfOutput: [UInt8], assertion: PasskeyAssertion) {
51
+ self.prfOutput = prfOutput
52
+ self.assertion = assertion
53
+ }
54
+ }
55
+
56
+ /// Backup passkeys with the PRF extension (`PasskeyProvider` in prf.ts).
57
+ public protocol PasskeyProvider: Sendable {
58
+ func create(userId: [UInt8], userName: String, salt: [UInt8]) async throws -> PasskeyCreateResult
59
+ /// `challenge` is the server's restore challenge when the API verifies the assertion.
60
+ func get(credentialId: String, salt: [UInt8], challenge: [UInt8]?) async throws -> PasskeyGetResult
61
+ }
62
+
63
+ /// Platform passkeys for the `wallet.authon.dev` relying party. The app needs the Associated
64
+ /// Domain `webcredentials:wallet.authon.dev` and an approved app registration. PRF needs
65
+ /// iOS 18 / macOS 15; older systems report PRF_UNSUPPORTED (the wallet cannot be protected).
66
+ public final class PlatformPasskeyProvider: PasskeyProvider, @unchecked Sendable {
67
+ public let relyingPartyIdentifier: String
68
+ private let anchor: (@MainActor () -> ASPresentationAnchor?)?
69
+
70
+ public init(relyingPartyIdentifier: String = "wallet.authon.dev", anchor: (@MainActor () -> ASPresentationAnchor?)? = nil) {
71
+ self.relyingPartyIdentifier = relyingPartyIdentifier
72
+ self.anchor = anchor
73
+ }
74
+
75
+ public func create(userId: [UInt8], userName: String, salt: [UInt8]) async throws -> PasskeyCreateResult {
76
+ guard #available(iOS 18.0, macOS 15.0, *) else { throw WalletError(.prfUnsupported) }
77
+ let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: relyingPartyIdentifier)
78
+ let request = provider.createCredentialRegistrationRequest(challenge: Data(try Bytes.random(32)), name: userName, userID: Data(userId))
79
+ request.displayName = userName
80
+ request.userVerificationPreference = .required
81
+ request.attestationPreference = .none
82
+ request.prf = .inputValues(.saltInput1(Data(salt)))
83
+ let auth = try await perform(request)
84
+ guard let reg = auth.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration else {
85
+ throw WalletError(.internal, "passkey failure")
86
+ }
87
+ guard let prf = reg.prf, prf.isSupported else { throw WalletError(.prfUnsupported) }
88
+ let credentialId = Bytes.base64url(Array(reg.credentialID))
89
+ let attestation = reg.rawAttestationObject.map { Bytes.base64url(Array($0)) }
90
+ if let first = prf.first {
91
+ return PasskeyCreateResult(credentialId: credentialId, prfOutput: try prfBytes(first), attestationObject: attestation)
92
+ }
93
+ // Some authenticators only evaluate PRF on assertion.
94
+ let second = try await get(credentialId: credentialId, salt: salt, challenge: nil)
95
+ return PasskeyCreateResult(credentialId: credentialId, prfOutput: second.prfOutput, attestationObject: attestation)
96
+ }
97
+
98
+ public func get(credentialId: String, salt: [UInt8], challenge: [UInt8]?) async throws -> PasskeyGetResult {
99
+ guard #available(iOS 18.0, macOS 15.0, *) else { throw WalletError(.prfUnsupported) }
100
+ guard let id = Bytes.fromBase64(credentialId) else { throw WalletError(.invalidRequest, "invalid credential id") }
101
+ let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: relyingPartyIdentifier)
102
+ let request = provider.createCredentialAssertionRequest(challenge: Data(try challenge ?? Bytes.random(32)))
103
+ request.allowedCredentials = [ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: Data(id))]
104
+ request.userVerificationPreference = .required
105
+ request.prf = .inputValues(.saltInput1(Data(salt)))
106
+ let auth = try await perform(request)
107
+ guard let cred = auth.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else {
108
+ throw WalletError(.internal, "passkey failure")
109
+ }
110
+ guard let prf = cred.prf else { throw WalletError(.prfUnsupported) }
111
+ return PasskeyGetResult(
112
+ prfOutput: try prfBytes(prf.first),
113
+ assertion: PasskeyAssertion(
114
+ credentialId: Bytes.base64url(Array(cred.credentialID)),
115
+ clientDataJSON: Bytes.base64url(Array(cred.rawClientDataJSON)),
116
+ authenticatorData: Bytes.base64url(Array(cred.rawAuthenticatorData)),
117
+ signature: Bytes.base64url(Array(cred.signature))
118
+ )
119
+ )
120
+ }
121
+
122
+ private func prfBytes(_ key: SymmetricKey) throws -> [UInt8] {
123
+ let out = key.withUnsafeBytes { Array($0) }
124
+ guard out.count == 32 else { throw WalletError(.prfUnsupported) }
125
+ return out
126
+ }
127
+
128
+ @MainActor
129
+ private func perform(_ request: ASAuthorizationRequest) async throws -> ASAuthorization {
130
+ let delegate = AuthorizationDelegate(anchor: anchor?() ?? Self.defaultAnchor())
131
+ return try await delegate.run(request)
132
+ }
133
+
134
+ @MainActor
135
+ static func defaultAnchor() -> ASPresentationAnchor {
136
+ #if canImport(UIKit)
137
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
138
+ return scenes.flatMap(\.windows).first(where: \.isKeyWindow) ?? scenes.first?.windows.first ?? ASPresentationAnchor()
139
+ #else
140
+ return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor()
141
+ #endif
142
+ }
143
+ }
144
+
145
+ @MainActor
146
+ private final class AuthorizationDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
147
+ private let anchor: ASPresentationAnchor
148
+ private var continuation: CheckedContinuation<ASAuthorization, Error>?
149
+ private var retain: AuthorizationDelegate?
150
+
151
+ init(anchor: ASPresentationAnchor) {
152
+ self.anchor = anchor
153
+ }
154
+
155
+ func run(_ request: ASAuthorizationRequest) async throws -> ASAuthorization {
156
+ try await withCheckedThrowingContinuation { cont in
157
+ continuation = cont
158
+ retain = self
159
+ let controller = ASAuthorizationController(authorizationRequests: [request])
160
+ controller.delegate = self
161
+ controller.presentationContextProvider = self
162
+ controller.performRequests()
163
+ }
164
+ }
165
+
166
+ nonisolated func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
167
+ MainActor.assumeIsolated { anchor }
168
+ }
169
+
170
+ nonisolated func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
171
+ MainActor.assumeIsolated { finish(.success(authorization)) }
172
+ }
173
+
174
+ nonisolated func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
175
+ let mapped: WalletError
176
+ if let e = error as? ASAuthorizationError, e.code == .canceled {
177
+ mapped = WalletError(.userCancelled)
178
+ } else {
179
+ // Missing Associated Domains or an unapproved app registration also end here (.failed).
180
+ mapped = WalletError(.internal, "passkey failure (\((error as NSError).code))")
181
+ }
182
+ MainActor.assumeIsolated { finish(.failure(mapped)) }
183
+ }
184
+
185
+ private func finish(_ result: Result<ASAuthorization, Error>) {
186
+ continuation?.resume(with: result)
187
+ continuation = nil
188
+ retain = nil
189
+ }
190
+ }
@@ -0,0 +1,143 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Wallet/Proof.swift. Do not edit: change the source and run the script.
2
+ import Foundation
3
+
4
+ /// Device public key as the API stores it.
5
+ public struct DevicePublicJwk: Codable, Equatable, Sendable {
6
+ public var kty: String
7
+ public var crv: String
8
+ public var x: String
9
+ public var y: String
10
+
11
+ public init(x: String, y: String) {
12
+ kty = "EC"
13
+ crv = "P-256"
14
+ self.x = x
15
+ self.y = y
16
+ }
17
+
18
+ /// From a 65-byte uncompressed P-256 point.
19
+ init(uncompressed point: [UInt8]) throws {
20
+ guard point.count == 65, point[0] == 4 else { throw WalletError(.internal, "invalid device public key") }
21
+ self.init(x: Bytes.base64url(Array(point[1..<33])), y: Bytes.base64url(Array(point[33..<65])))
22
+ }
23
+
24
+ var json: JSONValue {
25
+ .object(JSONObject([("kty", .string(kty)), ("crv", .string(crv)), ("x", .string(x)), ("y", .string(y))]))
26
+ }
27
+ }
28
+
29
+ /// Server-issued nonce bound into a wallet key proof (`expiresAt` in Unix seconds).
30
+ public struct ProofNonce: Equatable, Sendable {
31
+ public var nonce: String
32
+ public var expiresAt: Int64
33
+ }
34
+
35
+ /// Wallet key proofs (`apps/wallet/src/proof.ts`): EIP-712 under the wallet's own domain.
36
+ enum WalletProof {
37
+ static let domainName = "Authon Wallet Proof"
38
+ static let reservedPrefixes = ["create:", "device:", "restore:"]
39
+
40
+ static func canonicalJwk(_ jwk: DevicePublicJwk) -> String {
41
+ "{\"crv\":\(JSONValue.quote(jwk.crv)),\"kty\":\(JSONValue.quote(jwk.kty)),\"x\":\(JSONValue.quote(jwk.x)),\"y\":\(JSONValue.quote(jwk.y))}"
42
+ }
43
+
44
+ static func devicePublicKeyHash(_ jwk: DevicePublicJwk) -> String {
45
+ Bytes.hex(Bytes.sha256(Array(canonicalJwk(jwk).utf8)))
46
+ }
47
+
48
+ enum Kind: String {
49
+ case create = "CreateWallet"
50
+ case device = "AddDevice"
51
+ case restore = "RestoreWallet"
52
+ }
53
+
54
+ static func typedData(_ kind: Kind, _ fields: [(String, JSONValue)], bound: ProofNonce) -> JSONValue {
55
+ func field(_ n: String, _ t: String) -> JSONValue { .object(JSONObject([("name", .string(n)), ("type", .string(t))])) }
56
+ let head: [JSONValue]
57
+ switch kind {
58
+ case .create: head = [field("projectId", "string"), field("userId", "string"), field("deviceKeyHash", "string")]
59
+ case .device, .restore: head = [field("walletId", "string"), field("shareVersion", "uint256"), field("deviceKeyHash", "string")]
60
+ }
61
+ let typeList = head + [field("nonce", "string"), field("expiresAt", "uint256")]
62
+ return .object(JSONObject([
63
+ ("domain", .object(JSONObject([("name", .string(domainName)), ("version", .string("1"))]))),
64
+ ("types", .object(JSONObject([(kind.rawValue, .array(typeList))]))),
65
+ ("primaryType", .string(kind.rawValue)),
66
+ ("message", .object(JSONObject(fields + [("nonce", .string(bound.nonce)), ("expiresAt", .number(String(bound.expiresAt)))]))),
67
+ ]))
68
+ }
69
+
70
+ private static func sign(_ secret: [UInt8], _ td: JSONValue) throws -> String {
71
+ let account = try WalletShares.account(secret)
72
+ defer { account.wipe() }
73
+ return try account.signTypedData(try TypedData(td))
74
+ }
75
+
76
+ static func createTypedData(projectId: String, userId: String, deviceKey: DevicePublicJwk, bound: ProofNonce) -> JSONValue {
77
+ typedData(.create, [("projectId", .string(projectId)), ("userId", .string(userId)), ("deviceKeyHash", .string(devicePublicKeyHash(deviceKey)))], bound: bound)
78
+ }
79
+
80
+ static func deviceTypedData(walletId: String, shareVersion: Int, deviceKey: DevicePublicJwk, bound: ProofNonce) -> JSONValue {
81
+ typedData(.device, [("walletId", .string(walletId)), ("shareVersion", .number(String(shareVersion))), ("deviceKeyHash", .string(devicePublicKeyHash(deviceKey)))], bound: bound)
82
+ }
83
+
84
+ /// The restore proof names the next share version (current + 1).
85
+ static func restoreTypedData(walletId: String, currentShareVersion: Int, deviceKey: DevicePublicJwk, bound: ProofNonce) -> JSONValue {
86
+ typedData(.restore, [("walletId", .string(walletId)), ("shareVersion", .number(String(currentShareVersion + 1))), ("deviceKeyHash", .string(devicePublicKeyHash(deviceKey)))], bound: bound)
87
+ }
88
+
89
+ static func signCreate(_ secret: [UInt8], projectId: String, userId: String, deviceKey: DevicePublicJwk, bound: ProofNonce) throws -> String {
90
+ try sign(secret, createTypedData(projectId: projectId, userId: userId, deviceKey: deviceKey, bound: bound))
91
+ }
92
+
93
+ static func signDevice(_ secret: [UInt8], walletId: String, shareVersion: Int, deviceKey: DevicePublicJwk, bound: ProofNonce) throws -> String {
94
+ try sign(secret, deviceTypedData(walletId: walletId, shareVersion: shareVersion, deviceKey: deviceKey, bound: bound))
95
+ }
96
+
97
+ static func signRestore(_ secret: [UInt8], walletId: String, currentShareVersion: Int, deviceKey: DevicePublicJwk, bound: ProofNonce) throws -> String {
98
+ try sign(secret, restoreTypedData(walletId: walletId, currentShareVersion: currentShareVersion, deviceKey: deviceKey, bound: bound))
99
+ }
100
+
101
+ /// App text that looks like a (legacy) wallet proof is never signed for an app.
102
+ static func isReservedMessageText(_ text: String) -> Bool {
103
+ let scalars = text.unicodeScalars.drop { $0.properties.isWhitespace || $0.value < 0x20 || $0.value == 0xfeff }
104
+ let t = String(String.UnicodeScalarView(scalars)).lowercased()
105
+ return reservedPrefixes.contains { t.hasPrefix($0) }
106
+ }
107
+
108
+ /// `{raw}` hex decoded as strict UTF-8; nil when it is not valid UTF-8.
109
+ static func rawMessageText(_ hex: String) -> String? {
110
+ let body = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex
111
+ guard body.count % 2 == 0, let bytes = Bytes.fromHex(body) else { return nil }
112
+ return String(strictUTF8: bytes)
113
+ }
114
+
115
+ static func isReservedTypedData(_ td: JSONValue) -> Bool {
116
+ guard case .string(let name) = td["domain"]?["name"] ?? .null else { return false }
117
+ return name.trimmingCharacters(in: jsWhitespace).lowercased() == domainName.lowercased()
118
+ }
119
+
120
+ /// JavaScript String.prototype.trim whitespace.
121
+ static let jsWhitespace: CharacterSet = {
122
+ var set = CharacterSet.whitespacesAndNewlines
123
+ set.insert(charactersIn: "\u{feff}")
124
+ return set
125
+ }()
126
+ }
127
+
128
+ extension String {
129
+ /// Strict UTF-8 decoding (TextDecoder with fatal: true).
130
+ init?(strictUTF8 bytes: [UInt8]) {
131
+ var it = bytes.makeIterator()
132
+ var decoder = UTF8()
133
+ var scalars = String.UnicodeScalarView()
134
+ loop: while true {
135
+ switch decoder.decode(&it) {
136
+ case .scalarValue(let s): scalars.append(s)
137
+ case .emptyInput: break loop
138
+ case .error: return nil
139
+ }
140
+ }
141
+ self = String(scalars)
142
+ }
143
+ }