@authon/react-native-wallet 0.0.0-stage → 0.1.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 (159) hide show
  1. package/README.md +221 -2
  2. package/android/build.gradle +40 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/src/main/AndroidManifest.xml +4 -0
  5. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +181 -0
  6. package/android/src/vendor/GENERATED.md +4 -0
  7. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +191 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +82 -0
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +232 -0
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
  21. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
  22. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
  23. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
  24. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +353 -0
  25. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +58 -0
  26. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +449 -0
  27. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +71 -0
  28. package/build/authonContext.d.ts +19 -0
  29. package/build/authonContext.d.ts.map +1 -0
  30. package/build/authonContext.js +20 -0
  31. package/build/authonContext.js.map +1 -0
  32. package/build/client.d.ts +76 -0
  33. package/build/client.d.ts.map +1 -0
  34. package/build/client.js +349 -0
  35. package/build/client.js.map +1 -0
  36. package/build/errors.d.ts +18 -0
  37. package/build/errors.d.ts.map +1 -0
  38. package/build/errors.js +9 -0
  39. package/build/errors.js.map +1 -0
  40. package/build/index.d.ts +7 -0
  41. package/build/index.d.ts.map +1 -0
  42. package/build/index.js +4 -0
  43. package/build/index.js.map +1 -0
  44. package/build/native.d.ts +39 -0
  45. package/build/native.d.ts.map +1 -0
  46. package/build/native.js +12 -0
  47. package/build/native.js.map +1 -0
  48. package/build/types.d.ts +121 -0
  49. package/build/types.d.ts.map +1 -0
  50. package/build/types.js +3 -0
  51. package/build/types.js.map +1 -0
  52. package/build/useAuthonWallet.d.ts +25 -0
  53. package/build/useAuthonWallet.d.ts.map +1 -0
  54. package/build/useAuthonWallet.js +77 -0
  55. package/build/useAuthonWallet.js.map +1 -0
  56. package/expo-module.config.json +9 -0
  57. package/ios/AuthonReactNativeWallet.podspec +37 -0
  58. package/ios/AuthonWalletModule.swift +190 -0
  59. package/ios/vendor/AuthonWallet/AuthonWallet.swift +212 -0
  60. package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
  61. package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
  62. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +104 -0
  63. package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
  64. package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
  65. package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
  66. package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
  67. package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
  68. package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
  69. package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
  70. package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
  71. package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
  72. package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
  73. package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
  74. package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
  75. package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
  76. package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
  77. package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
  78. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +350 -0
  79. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +555 -0
  80. package/ios/vendor/AuthonWallet/WalletError.swift +54 -0
  81. package/ios/vendor/CKeccak/LICENSE +22 -0
  82. package/ios/vendor/CKeccak/README.md +27 -0
  83. package/ios/vendor/CKeccak/byte_order.h +59 -0
  84. package/ios/vendor/CKeccak/include/ckeccak.h +9 -0
  85. package/ios/vendor/CKeccak/memzero.c +75 -0
  86. package/ios/vendor/CKeccak/memzero.h +8 -0
  87. package/ios/vendor/CKeccak/options.h +99 -0
  88. package/ios/vendor/CKeccak/sha3.c +413 -0
  89. package/ios/vendor/CKeccak/sha3.h +89 -0
  90. package/ios/vendor/CSecp256k1/COPYING +19 -0
  91. package/ios/vendor/CSecp256k1/README.md +35 -0
  92. package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
  93. package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
  94. package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
  95. package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
  96. package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
  97. package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
  98. package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
  99. package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
  100. package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
  101. package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
  102. package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
  103. package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
  104. package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
  105. package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
  106. package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
  107. package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
  108. package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
  109. package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
  110. package/ios/vendor/CSecp256k1/src/field.h +350 -0
  111. package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
  112. package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
  113. package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
  114. package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
  115. package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
  116. package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
  117. package/ios/vendor/CSecp256k1/src/group.h +212 -0
  118. package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
  119. package/ios/vendor/CSecp256k1/src/hash.h +44 -0
  120. package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
  121. package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
  122. package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
  123. package/ios/vendor/CSecp256k1/src/int128.h +90 -0
  124. package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
  125. package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
  126. package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
  127. package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
  128. package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
  129. package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
  130. package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
  131. package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
  132. package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
  133. package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
  134. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
  135. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
  136. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
  137. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
  138. package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
  139. package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
  140. package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
  141. package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
  142. package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
  143. package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
  144. package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
  145. package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
  146. package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
  147. package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
  148. package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
  149. package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
  150. package/ios/vendor/CSecp256k1/src/util.h +451 -0
  151. package/ios/vendor/GENERATED.md +4 -0
  152. package/package.json +65 -3
  153. package/src/authonContext.ts +30 -0
  154. package/src/client.ts +375 -0
  155. package/src/errors.ts +52 -0
  156. package/src/index.ts +21 -0
  157. package/src/native.ts +50 -0
  158. package/src/types.ts +120 -0
  159. package/src/useAuthonWallet.ts +103 -0
@@ -0,0 +1,555 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Wallet/WalletOps.swift. Do not edit: change the source and run the script.
2
+ import Foundation
3
+
4
+ public struct WalletStatus: Equatable, Sendable {
5
+ public var exists: Bool
6
+ /// The server has a wallet that was never confirmed (device share not persisted).
7
+ public var pending: Bool
8
+ public var hasLocalShare: Bool
9
+ public var needsRestore: Bool
10
+ public var deviceId: String?
11
+ public var wallet: ServerWallet?
12
+ }
13
+
14
+ /// What apps see (ops.ts toPublicWallet).
15
+ public struct PublicWallet: Equatable, Sendable {
16
+ public struct Device: Equatable, Sendable {
17
+ public var id: String
18
+ public var label: String?
19
+ public var createdAt: String?
20
+ public var lastUsedAt: String?
21
+ public var current: Bool
22
+ }
23
+
24
+ public var address: String
25
+ /// False until a passkey backup exists. An unprotected wallet is lost with this device.
26
+ public var isProtected: Bool
27
+ /// A legacy backup without a passkey public key; call upgradeBackup() from a device that holds the wallet.
28
+ public var backupUpgradeRecommended: Bool
29
+ public var needsRestore: Bool
30
+ public var devices: [Device]
31
+
32
+ init(_ st: WalletStatus) throws {
33
+ guard let w = st.wallet, !st.pending else { throw WalletError(.notFound) }
34
+ address = w.address
35
+ isProtected = w.isProtected
36
+ backupUpgradeRecommended = w.isProtected && w.backupVerifiable == false
37
+ needsRestore = st.needsRestore
38
+ devices = w.devices.filter { $0.revokedAt == nil }.map {
39
+ Device(id: $0.id, label: $0.label, createdAt: $0.createdAt, lastUsedAt: $0.lastUsedAt, current: $0.id == st.deviceId)
40
+ }
41
+ }
42
+ }
43
+
44
+ public struct WalletAccount: Equatable, Sendable {
45
+ public var index: Int
46
+ public var path: String
47
+ public var address: String
48
+ }
49
+
50
+ /// Which account signs. Account 0 is the wallet address the server knows; another account
51
+ /// must name the address the caller expects, and signing is refused when it differs.
52
+ public struct SignAccount: Equatable, Sendable {
53
+ public var index: Int
54
+ public var address: String?
55
+
56
+ public static let primary = SignAccount(index: 0, address: nil)
57
+
58
+ public init(index: Int, address: String?) {
59
+ self.index = index
60
+ self.address = address
61
+ }
62
+
63
+ /// ops.ts parseSignAccount.
64
+ public static func parse(index: Int?, address: String?) throws -> SignAccount {
65
+ guard let index, index != 0 else { return .primary }
66
+ guard WalletShares.isAccountIndex(index) else {
67
+ throw WalletError(.invalidRequest, "account must be an integer 0..\(WalletShares.maxAccountIndex)")
68
+ }
69
+ guard let address, address.count == 42, address.hasPrefix("0x"), Bytes.isHex(address) else {
70
+ throw WalletError(.invalidRequest, "address of the signing account required")
71
+ }
72
+ return SignAccount(index: index, address: address)
73
+ }
74
+ }
75
+
76
+ /// A personal_sign payload.
77
+ public enum WalletMessage: Equatable, Sendable {
78
+ case text(String)
79
+ /// 0x-prefixed hex bytes.
80
+ case raw(String)
81
+ }
82
+
83
+ enum ParsedMessage: Equatable {
84
+ case text(String)
85
+ case raw(String)
86
+ }
87
+
88
+ public enum RestoreMode: String, Sendable {
89
+ /// Enroll this device on the current shares; other devices keep working.
90
+ case add
91
+ /// Re-split the key and revoke every other device (lost device).
92
+ case reset
93
+ }
94
+
95
+ public struct ProtectResult: Equatable, Sendable {
96
+ public var shareVersion: Int
97
+ }
98
+
99
+ public struct RestoreResult: Equatable, Sendable {
100
+ public var shareVersion: Int
101
+ public var backupUpgradeRecommended: Bool
102
+ }
103
+
104
+ struct WalletContext {
105
+ var api: WalletAPIClient
106
+ var store: DeviceStore
107
+ var passkeys: PasskeyProvider
108
+ var projectId: String
109
+ /// From the project's server-side settings only.
110
+ var confirmSigning: Bool
111
+ var backupVersion: BackupVersion
112
+ var approver: WalletApprover?
113
+ var deviceAuthenticator: DeviceAuthenticator?
114
+ }
115
+
116
+ /// The wallet operations, step for step the same as `apps/wallet/src/ops.ts`.
117
+ struct WalletOps {
118
+ let ctx: WalletContext
119
+
120
+ static func decodeUserId(_ session: String) throws -> String {
121
+ let parts = session.split(separator: ".", omittingEmptySubsequences: false)
122
+ guard parts.count > 1, !parts[1].isEmpty else { throw WalletError(.unauthorized, "malformed session token") }
123
+ guard let bytes = Bytes.fromBase64(String(parts[1])),
124
+ let claims = try? JSONValue.parse(String(decoding: bytes, as: UTF8.self)),
125
+ case .string(let sub) = claims["sub"] ?? .null, !sub.isEmpty else {
126
+ throw WalletError(.unauthorized, "malformed session token")
127
+ }
128
+ return sub
129
+ }
130
+
131
+ private func recordKey(_ session: String) throws -> String {
132
+ storageKey(projectId: ctx.projectId, userId: try Self.decodeUserId(session))
133
+ }
134
+
135
+ func status(_ session: String) async throws -> WalletStatus {
136
+ let wallet = try await ctx.api.getMe(session)
137
+ let local = try ctx.store.get(try recordKey(session))
138
+ let pending = wallet?.status == "pending"
139
+ // A local share only counts while the server still trusts this device at the current share version.
140
+ var hasLocal = false
141
+ if !pending, let local, let wallet, local.walletId == wallet.id,
142
+ let device = wallet.devices.first(where: { $0.id == local.deviceId }), device.revokedAt == nil {
143
+ hasLocal = (device.shareVersion ?? local.shareVersion) == wallet.shareVersion && local.shareVersion == wallet.shareVersion
144
+ }
145
+ let exists = wallet != nil && !pending
146
+ return WalletStatus(
147
+ exists: exists,
148
+ pending: pending,
149
+ hasLocalShare: hasLocal,
150
+ needsRestore: exists && !hasLocal,
151
+ deviceId: hasLocal ? local?.deviceId : nil,
152
+ wallet: wallet
153
+ )
154
+ }
155
+
156
+ private func deviceProof(_ session: String, _ record: DeviceRecord) async throws -> DeviceProof {
157
+ let challenge = try await ctx.api.challenge(session, deviceId: record.deviceId)
158
+ let signature = Bytes.base64(try ctx.store.sign(challenge: challenge, record: record))
159
+ return DeviceProof(deviceId: record.deviceId, challenge: challenge, signature: signature)
160
+ }
161
+
162
+ /// Reads the record back and checks it opens to `share`; storage that drops writes fails here.
163
+ @discardableResult
164
+ private func verifyPersisted(_ key: String, deviceId: String, walletId: String, share: [UInt8]?) throws -> DeviceRecord {
165
+ guard let stored = try? ctx.store.get(key), stored.deviceId == deviceId, stored.walletId == walletId else {
166
+ throw WalletError(.storageUnavailable, "device share was not persisted")
167
+ }
168
+ if let share {
169
+ var opened: [UInt8]
170
+ do {
171
+ opened = try ctx.store.open(stored)
172
+ } catch {
173
+ throw WalletError(.storageUnavailable, "device share cannot be read back")
174
+ }
175
+ defer { opened.wipe() }
176
+ guard opened == share else { throw WalletError(.storageUnavailable, "device share read back differs") }
177
+ }
178
+ return stored
179
+ }
180
+
181
+ /// Confirms a pending wallet whose device record is present and readable here.
182
+ private func finishPending(_ session: String, _ wallet: ServerWallet) async throws -> Bool {
183
+ let key = try recordKey(session)
184
+ guard let record = try? ctx.store.get(key), record.walletId == wallet.id,
185
+ wallet.devices.contains(where: { $0.id == record.deviceId }) else { return false }
186
+ do {
187
+ var share = try ctx.store.open(record)
188
+ defer { share.wipe() }
189
+ try verifyPersisted(key, deviceId: record.deviceId, walletId: wallet.id, share: share)
190
+ } catch {
191
+ return false
192
+ }
193
+ try await ctx.api.confirmWallet(session, try await deviceProof(session, record))
194
+ return true
195
+ }
196
+
197
+ /// Two-phase creation: register address and A (pending), store D and read it back, then
198
+ /// confirm with a challenge signed by the stored device key. A pending wallet left by an
199
+ /// interrupted attempt is confirmed (D is here) or discarded and recreated (D is not).
200
+ func create(_ session: String, attempt: Int = 0) async throws -> WalletStatus {
201
+ let existing = try await status(session)
202
+ if existing.exists { return existing }
203
+ if existing.pending, let w = existing.wallet {
204
+ if try await finishPending(session, w) { return try await status(session) }
205
+ if attempt > 0 { throw WalletError(.storageUnavailable) }
206
+ try await ctx.api.discardPending(session)
207
+ try? ctx.store.delete(try recordKey(session))
208
+ return try await create(session, attempt: attempt + 1)
209
+ }
210
+
211
+ let key = try recordKey(session)
212
+ let userId = try Self.decodeUserId(session)
213
+ var secret = try WalletShares.generateSecret()
214
+ var device = [UInt8]()
215
+ var company = [UInt8]()
216
+ var registered = false
217
+ var keys: DeviceKeys?
218
+ var stored = false
219
+ defer {
220
+ secret.wipe()
221
+ device.wipe()
222
+ company.wipe()
223
+ }
224
+ do {
225
+ let address = try WalletShares.address(secret)
226
+ (device, company) = try WalletShares.splitUnprotected(secret)
227
+ let k = try ctx.store.generateKeys()
228
+ keys = k
229
+ let sealed = try ctx.store.seal(device, keys: k, storageKey: key)
230
+ let bound = try await ctx.api.proofChallenge(session, kind: .create)
231
+ let proof = try WalletProof.signCreate(secret, projectId: ctx.projectId, userId: userId, deviceKey: k.publicJwk, bound: bound)
232
+ let created = try await ctx.api.createWallet(session, address: address, companyShare: Bytes.base64(company), devicePublicKey: k.publicJwk, proof: proof, bound: bound)
233
+ registered = true
234
+ let record = DeviceRecord(
235
+ key: key, deviceId: created.deviceId, walletId: created.walletId, address: address,
236
+ shareVersion: created.shareVersion, createdAt: Date().timeIntervalSince1970 * 1000,
237
+ publicJwk: k.publicJwk, keyHandle: k.handle, sealedShare: sealed
238
+ )
239
+ do {
240
+ try ctx.store.put(record)
241
+ } catch {
242
+ throw WalletError(.storageUnavailable, "device share was not persisted")
243
+ }
244
+ stored = true
245
+ let readBack = try verifyPersisted(key, deviceId: created.deviceId, walletId: created.walletId, share: device)
246
+ try await ctx.api.confirmWallet(session, try await deviceProof(session, readBack))
247
+ } catch {
248
+ if !stored, let keys { ctx.store.discardKeys(keys) }
249
+ if let e = error as? WalletError, e.code == .alreadyExists, attempt == 0 {
250
+ return try await create(session, attempt: attempt + 1)
251
+ }
252
+ if registered, let e = error as? WalletError, e.code == .storageUnavailable {
253
+ try? await ctx.api.discardPending(session)
254
+ try? ctx.store.delete(key)
255
+ }
256
+ throw error
257
+ }
258
+ return try await status(session)
259
+ }
260
+
261
+ private func requireLocal(_ session: String) async throws -> (record: DeviceRecord, wallet: ServerWallet) {
262
+ guard let wallet = try await ctx.api.getMe(session), wallet.status != "pending" else { throw WalletError(.notFound) }
263
+ guard let record = try ctx.store.get(try recordKey(session)), record.walletId == wallet.id else {
264
+ throw WalletError(.needsRestore)
265
+ }
266
+ return (record, wallet)
267
+ }
268
+
269
+ private func fetchCompanyShare(_ session: String, _ record: DeviceRecord) async throws -> [UInt8] {
270
+ let b64 = try await ctx.api.releaseShare(session, try await deviceProof(session, record))
271
+ guard let bytes = Bytes.fromBase64(b64) else { throw WalletError(.apiError) }
272
+ return bytes
273
+ }
274
+
275
+ /// The user's approval for a signature, before A is requested. No approver means no signature.
276
+ private func approve(_ summary: SignSummary) async throws {
277
+ guard let approver = ctx.approver else { throw WalletError(.internal, "confirmation unavailable") }
278
+ try await approver.approveSignature(summary)
279
+ }
280
+
281
+ private func withSecret<T>(
282
+ _ session: String,
283
+ account target: SignAccount,
284
+ summary build: (() throws -> SignSummary)?,
285
+ _ fn: (EvmAccount) throws -> T
286
+ ) async throws -> T {
287
+ // Built (and size-checked) only when someone will see it.
288
+ let summary = try build.flatMap { b in ctx.confirmSigning ? Summaries.withAccountRow(try b(), target) : nil }
289
+ let local = try await requireLocal(session)
290
+ if let summary { try await approve(summary) }
291
+ if let auth = ctx.deviceAuthenticator { try await auth.authenticate(reason: summary?.title ?? "Sign with your wallet") }
292
+ var company = try await fetchCompanyShare(session, local.record)
293
+ var device = try ctx.store.open(local.record)
294
+ var secret = [UInt8]()
295
+ defer {
296
+ secret.wipe()
297
+ device.wipe()
298
+ company.wipe()
299
+ }
300
+ secret = try WalletShares.assemble(device, company)
301
+ let primary = try WalletShares.account(secret)
302
+ defer { primary.wipe() }
303
+ guard primary.address.lowercased() == local.wallet.address.lowercased() else { throw WalletError(.addressMismatch) }
304
+ if target.index == 0 { return try fn(primary) }
305
+ guard WalletShares.isHdSecret(secret) else { throw WalletError(.accountUnavailable, "this wallet has only account 0") }
306
+ let account = try WalletShares.account(secret, index: target.index)
307
+ defer { account.wipe() }
308
+ guard account.address.lowercased() == target.address?.lowercased() else { throw WalletError(.addressMismatch) }
309
+ return try fn(account)
310
+ }
311
+
312
+ func accounts(_ session: String, start: Int = 0, count: Int = 1) async throws -> [WalletAccount] {
313
+ guard WalletShares.isAccountIndex(start), count >= 1, WalletShares.isAccountIndex(start + count - 1) else {
314
+ throw WalletError(.invalidRequest, "start..start+count-1 must stay within 0..\(WalletShares.maxAccountIndex)")
315
+ }
316
+ let local = try await requireLocal(session)
317
+ var company = try await fetchCompanyShare(session, local.record)
318
+ var device = try ctx.store.open(local.record)
319
+ var secret = [UInt8]()
320
+ defer {
321
+ secret.wipe()
322
+ device.wipe()
323
+ company.wipe()
324
+ }
325
+ secret = try WalletShares.assemble(device, company)
326
+ guard try WalletShares.address(secret).lowercased() == local.wallet.address.lowercased() else { throw WalletError(.addressMismatch) }
327
+ if !WalletShares.isHdSecret(secret) && (start != 0 || count != 1) {
328
+ throw WalletError(.accountUnavailable, "this wallet has only account 0")
329
+ }
330
+ let list = try WalletShares.addresses(secret, start: start, count: count)
331
+ return list.enumerated().map { WalletAccount(index: start + $0.offset, path: WalletShares.evmPath(start + $0.offset), address: $0.element) }
332
+ }
333
+
334
+ private static func reserved() -> WalletError {
335
+ WalletError(.invalidRequest, "this message is reserved for wallet key proofs and cannot be signed")
336
+ }
337
+
338
+ static func parseMessage(_ m: WalletMessage) throws -> ParsedMessage {
339
+ switch m {
340
+ case .text(let s): return .text(s)
341
+ case .raw(let hex):
342
+ guard hex.hasPrefix("0x"), Bytes.isHex(hex) else { throw WalletError(.invalidRequest, "message must be a string or {raw}") }
343
+ return .raw(hex)
344
+ }
345
+ }
346
+
347
+ func signMessage(_ session: String, _ message: WalletMessage, account: SignAccount = .primary) async throws -> String {
348
+ let parsed = try Self.parseMessage(message)
349
+ let text: String?
350
+ let payload: EvmAccount.Message
351
+ switch parsed {
352
+ case .text(let s):
353
+ text = s
354
+ payload = .text(s)
355
+ case .raw(let hex):
356
+ text = WalletProof.rawMessageText(hex)
357
+ payload = .raw(Bytes.fromHex(hex) ?? [])
358
+ }
359
+ if let text, WalletProof.isReservedMessageText(text) { throw Self.reserved() }
360
+ return try await withSecret(session, account: account, summary: { try Summaries.message(parsed) }) { try $0.signMessage(payload) }
361
+ }
362
+
363
+ func signTypedData(_ session: String, _ typedData: JSONValue, account: SignAccount = .primary) async throws -> String {
364
+ guard case .object = typedData else { throw WalletError(.invalidRequest) }
365
+ guard typedData["types"]?.isTruthy == true, typedData["primaryType"]?.isTruthy == true, typedData["message"]?.isTruthy == true else {
366
+ throw WalletError(.invalidRequest, "typedData incomplete")
367
+ }
368
+ if WalletProof.isReservedTypedData(typedData) { throw Self.reserved() }
369
+ let td = try TypedData(typedData)
370
+ // Malformed typed data is refused before anyone is asked to approve it.
371
+ _ = try td.digest()
372
+ return try await withSecret(session, account: account, summary: { try Summaries.typedData(typedData) }) { try $0.signTypedData(td) }
373
+ }
374
+
375
+ private static let bigintFields = ["value", "gas", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "maxFeePerBlobGas"]
376
+ private static let numberFields = ["nonce", "chainId"]
377
+
378
+ /// ops.ts parseTransaction: numeric fields as BigInt, nonce and chainId as Number.
379
+ static func parseTransaction(_ value: JSONValue) throws -> JSONObject {
380
+ guard var tx = value.objectValue else { throw WalletError(.invalidRequest) }
381
+ for f in bigintFields + numberFields {
382
+ guard let v = tx[f] else { continue }
383
+ guard let n = v.jsBigInt else { throw WalletError(.invalidRequest, "transaction numeric field invalid") }
384
+ tx[f] = .number(n.description)
385
+ }
386
+ guard tx["chainId"] != nil else { throw WalletError(.invalidRequest, "chainId required") }
387
+ return tx
388
+ }
389
+
390
+ func signTransaction(_ session: String, _ transaction: JSONValue, account: SignAccount = .primary) async throws -> String {
391
+ let tx = try Self.parseTransaction(transaction)
392
+ let evmTx = try EvmTransaction(tx)
393
+ return try await withSecret(session, account: account, summary: { Summaries.transaction(tx) }) { try $0.signTransaction(evmTx) }
394
+ }
395
+
396
+ // MARK: - Backup
397
+
398
+ private func writeBackup(_ session: String, upgrade: Bool) async throws -> ProtectResult {
399
+ let (record, wallet) = try await requireLocal(session)
400
+ if !upgrade && wallet.isProtected { return ProtectResult(shareVersion: wallet.shareVersion) }
401
+ if upgrade {
402
+ guard wallet.isProtected else { throw WalletError(.notProtected) }
403
+ guard wallet.backupVerifiable == false else { throw WalletError(.alreadyUpgraded) }
404
+ }
405
+ let key = try recordKey(session)
406
+ let user = try Self.decodeUserId(session)
407
+ let version = ctx.backupVersion
408
+ let salt = WalletBackup.prfSalt(version, projectId: ctx.projectId, walletId: wallet.id)
409
+ let passkeyUser = WalletBackup.passkeyUserId(projectId: ctx.projectId, userId: user)
410
+ if let approver = ctx.approver { try await approver.confirmPasskeyAction(upgrade ? .upgradeBackup : .protect) }
411
+ var pk = try await ctx.passkeys.create(userId: passkeyUser, userName: wallet.address, salt: salt)
412
+ var oldCompany = [UInt8]()
413
+ var oldDevice = [UInt8]()
414
+ var secret = [UInt8]()
415
+ var shares: (device: [UInt8], company: [UInt8], backup: [UInt8]) = ([], [], [])
416
+ defer {
417
+ pk.prfOutput.wipe()
418
+ oldCompany.wipe()
419
+ oldDevice.wipe()
420
+ secret.wipe()
421
+ shares.device.wipe()
422
+ shares.company.wipe()
423
+ shares.backup.wipe()
424
+ }
425
+ oldCompany = try await fetchCompanyShare(session, record)
426
+ oldDevice = try ctx.store.open(record)
427
+ secret = try WalletShares.assemble(oldDevice, oldCompany)
428
+ guard try WalletShares.address(secret).lowercased() == wallet.address.lowercased() else { throw WalletError(.addressMismatch) }
429
+ shares = try WalletShares.splitProtected(secret)
430
+ let backupKey = WalletBackup.deriveKey(prfOutput: pk.prfOutput)
431
+ let backupCiphertext = try WalletBackup.encrypt(key: backupKey, share: shares.backup, walletId: wallet.id, version: version)
432
+ let proof = try await deviceProof(session, record)
433
+ let keyFields: WalletAPIClient.BackupKeyFields
434
+ if let pub = pk.publicKey, let alg = pk.publicKeyAlg {
435
+ keyFields = .init(credentialPublicKey: pub, credentialAlg: alg)
436
+ } else {
437
+ keyFields = .init(attestationObject: pk.attestationObject)
438
+ }
439
+ let shareVersion = try await ctx.api.putBackup(
440
+ session, proof: proof, backupCiphertext: backupCiphertext, credentialId: pk.credentialId,
441
+ newCompanyShare: Bytes.base64(shares.company), key: keyFields, upgrade: upgrade
442
+ )
443
+ let sealed = try ctx.store.seal(shares.device, keys: DeviceKeys(handle: record.keyHandle, publicJwk: record.publicJwk), storageKey: key)
444
+ var updated = record
445
+ updated.sealedShare = sealed
446
+ updated.shareVersion = shareVersion
447
+ try ctx.store.put(updated)
448
+ return ProtectResult(shareVersion: shareVersion)
449
+ }
450
+
451
+ func protect(_ session: String) async throws -> ProtectResult { try await writeBackup(session, upgrade: false) }
452
+
453
+ /// Replaces a legacy backup (no passkey public key) from a device that holds the wallet.
454
+ func upgradeBackup(_ session: String) async throws -> ProtectResult { try await writeBackup(session, upgrade: true) }
455
+
456
+ func restore(_ session: String, mode: RestoreMode = .add) async throws -> RestoreResult {
457
+ guard let wallet = try await ctx.api.getMe(session), wallet.status != "pending" else { throw WalletError(.notFound) }
458
+ let start = try await ctx.api.restoreStart(session)
459
+ guard let ciphertext = start.backupCiphertext, !ciphertext.isEmpty, let credentialId = start.credentialId, !credentialId.isEmpty else {
460
+ throw WalletError(.notProtected)
461
+ }
462
+ let key = try recordKey(session)
463
+ let version = WalletBackup.version(of: ciphertext)
464
+ let salt = WalletBackup.prfSalt(version, projectId: ctx.projectId, walletId: wallet.id)
465
+ let challenge = start.passkeyChallenge.flatMap(Bytes.fromBase64)
466
+ if let approver = ctx.approver { try await approver.confirmPasskeyAction(mode == .reset ? .restoreReset : .restore) }
467
+ var got = try await ctx.passkeys.get(credentialId: credentialId, salt: salt, challenge: challenge)
468
+ var company = [UInt8]()
469
+ var backup = [UInt8]()
470
+ var secret = [UInt8]()
471
+ var shares: (device: [UInt8], company: [UInt8], backup: [UInt8]) = ([], [], [])
472
+ defer {
473
+ got.prfOutput.wipe()
474
+ company.wipe()
475
+ backup.wipe()
476
+ secret.wipe()
477
+ shares.device.wipe()
478
+ shares.company.wipe()
479
+ shares.backup.wipe()
480
+ }
481
+ let backupKey = WalletBackup.deriveKey(prfOutput: got.prfOutput)
482
+ backup = try WalletBackup.decrypt(key: backupKey, payload: ciphertext, walletId: wallet.id)
483
+ let companyB64: String
484
+ if let c = start.companyShare, !c.isEmpty {
485
+ companyB64 = c
486
+ } else {
487
+ companyB64 = try await ctx.api.restoreAssert(session, got.assertion, challenge: start.passkeyChallenge ?? "")
488
+ }
489
+ guard let c = Bytes.fromBase64(companyB64) else { throw WalletError(.apiError) }
490
+ company = c
491
+ secret = try WalletShares.assemble(backup, company)
492
+ guard try WalletShares.address(secret).lowercased() == wallet.address.lowercased() else { throw WalletError(.addressMismatch) }
493
+
494
+ let upgradeRecommended = start.upgradeRecommended
495
+ if mode == .add {
496
+ let shareVersion = try await addThisDevice(session, key: key, wallet: wallet, secret: secret, company: company, backup: backup)
497
+ return RestoreResult(shareVersion: shareVersion, backupUpgradeRecommended: upgradeRecommended)
498
+ }
499
+
500
+ shares = try WalletShares.splitProtected(secret)
501
+ let keys = try ctx.store.generateKeys()
502
+ var stored = false
503
+ defer { if !stored { ctx.store.discardKeys(keys) } }
504
+ let bound = try await ctx.api.proofChallenge(session, kind: .restore)
505
+ let proof = try WalletProof.signRestore(secret, walletId: wallet.id, currentShareVersion: wallet.shareVersion, deviceKey: keys.publicJwk, bound: bound)
506
+ // The PRF output belongs to this backup's salt version, so the new backup keeps it.
507
+ let newBackup = try WalletBackup.encrypt(key: backupKey, share: shares.backup, walletId: wallet.id, version: version)
508
+ let res = try await ctx.api.restore(
509
+ session, newCompanyShare: Bytes.base64(shares.company), newBackupCiphertext: newBackup,
510
+ newDevicePublicKey: keys.publicJwk, credentialId: credentialId, proof: proof, bound: bound
511
+ )
512
+ let sealed = try ctx.store.seal(shares.device, keys: keys, storageKey: key)
513
+ try ctx.store.put(DeviceRecord(
514
+ key: key, deviceId: res.deviceId, walletId: wallet.id, address: wallet.address, shareVersion: res.shareVersion,
515
+ createdAt: Date().timeIntervalSince1970 * 1000, publicJwk: keys.publicJwk, keyHandle: keys.handle, sealedShare: sealed
516
+ ))
517
+ stored = true
518
+ return RestoreResult(shareVersion: res.shareVersion, backupUpgradeRecommended: upgradeRecommended)
519
+ }
520
+
521
+ private func addThisDevice(_ session: String, key: String, wallet: ServerWallet, secret: [UInt8], company: [UInt8], backup: [UInt8]) async throws -> Int {
522
+ var device = try WalletShares.deriveShare(company, backup)
523
+ var check1 = [UInt8]()
524
+ var check2 = [UInt8]()
525
+ defer {
526
+ device.wipe()
527
+ check1.wipe()
528
+ check2.wipe()
529
+ }
530
+ check1 = try WalletShares.assemble(device, company)
531
+ check2 = try WalletShares.assemble(device, backup)
532
+ guard check1 == secret, check2 == secret else { throw WalletError(.internal, "derived share mismatch") }
533
+ let keys = try ctx.store.generateKeys()
534
+ var stored = false
535
+ defer { if !stored { ctx.store.discardKeys(keys) } }
536
+ let bound = try await ctx.api.proofChallenge(session, kind: .device)
537
+ let proof = try WalletProof.signDevice(secret, walletId: wallet.id, shareVersion: wallet.shareVersion, deviceKey: keys.publicJwk, bound: bound)
538
+ let res = try await ctx.api.addDevice(session, devicePublicKey: keys.publicJwk, proof: proof, bound: bound)
539
+ let sealed = try ctx.store.seal(device, keys: keys, storageKey: key)
540
+ try ctx.store.put(DeviceRecord(
541
+ key: key, deviceId: res.deviceId, walletId: wallet.id, address: wallet.address, shareVersion: res.shareVersion,
542
+ createdAt: Date().timeIntervalSince1970 * 1000, publicJwk: keys.publicJwk, keyHandle: keys.handle, sealedShare: sealed
543
+ ))
544
+ stored = true
545
+ return res.shareVersion
546
+ }
547
+
548
+ /// Revokes a device with a request signed by this device's key.
549
+ func revokeDevice(_ session: String, deviceId: String) async throws {
550
+ guard !deviceId.isEmpty else { throw WalletError(.invalidRequest, "deviceId required") }
551
+ let (record, _) = try await requireLocal(session)
552
+ try await ctx.api.revokeDevice(session, deviceId: deviceId, proof: try await deviceProof(session, record))
553
+ if record.deviceId == deviceId { try ctx.store.delete(try recordKey(session)) }
554
+ }
555
+ }
@@ -0,0 +1,54 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/WalletError.swift. Do not edit: change the source and run the script.
2
+ import Foundation
3
+
4
+ /// Error codes shared with the web wallet frame (`apps/wallet/src/errors.ts`).
5
+ public enum WalletErrorCode: String, Sendable, CaseIterable {
6
+ case notFound = "NOT_FOUND"
7
+ case needsRestore = "NEEDS_RESTORE"
8
+ case notProtected = "NOT_PROTECTED"
9
+ /// The OS or authenticator cannot evaluate the passkey PRF extension, so the wallet cannot be protected or restored here.
10
+ case prfUnsupported = "PRF_UNSUPPORTED"
11
+ case originNotAllowed = "ORIGIN_NOT_ALLOWED"
12
+ case userCancelled = "USER_CANCELLED"
13
+ case needsPopup = "NEEDS_POPUP"
14
+ case invalidRequest = "INVALID_REQUEST"
15
+ case unsupportedType = "UNSUPPORTED_TYPE"
16
+ case unauthorized = "UNAUTHORIZED"
17
+ case forbidden = "FORBIDDEN"
18
+ case alreadyExists = "ALREADY_EXISTS"
19
+ case rateLimited = "RATE_LIMITED"
20
+ case addressMismatch = "ADDRESS_MISMATCH"
21
+ case storageUnavailable = "STORAGE_UNAVAILABLE"
22
+ case apiError = "API_ERROR"
23
+ case `internal` = "INTERNAL"
24
+ case lastDevice = "LAST_DEVICE"
25
+ case sessionRequired = "SESSION_REQUIRED"
26
+ case walletPending = "WALLET_PENDING"
27
+ case confirmWindowBlocked = "CONFIRM_WINDOW_BLOCKED"
28
+ case confirmWindowClosed = "CONFIRM_WINDOW_CLOSED"
29
+ case messageTooLarge = "MESSAGE_TOO_LARGE"
30
+ /// The API no longer serves this SDK's protocol version; update the SDK.
31
+ case frameOutdated = "FRAME_OUTDATED"
32
+ case backupKeyRequired = "BACKUP_KEY_REQUIRED"
33
+ case alreadyUpgraded = "ALREADY_UPGRADED"
34
+ case accountUnavailable = "ACCOUNT_UNAVAILABLE"
35
+ }
36
+
37
+ public struct WalletError: Error, Equatable, CustomStringConvertible, LocalizedError {
38
+ public let code: WalletErrorCode
39
+ public let message: String
40
+
41
+ public init(_ code: WalletErrorCode, _ message: String? = nil) {
42
+ self.code = code
43
+ self.message = message ?? code.rawValue
44
+ }
45
+
46
+ public var description: String { "WalletError(\(code.rawValue)): \(message)" }
47
+ public var errorDescription: String? { message }
48
+
49
+ public static func == (a: WalletError, b: WalletError) -> Bool { a.code == b.code }
50
+
51
+ static func wrap(_ error: Error) -> WalletError {
52
+ error as? WalletError ?? WalletError(.internal, "unexpected error")
53
+ }
54
+ }
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Tomas Dzetkulic
4
+ Copyright (c) 2013 Pavol Rusnak
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,27 @@
1
+ # CKeccak
2
+
3
+ Keccak-256 (original Keccak padding, as used by Ethereum) for AuthonWallet.
4
+
5
+ Vendored unchanged from trezor-crypto, MIT (see LICENSE):
6
+
7
+ | File | Source | SHA-256 |
8
+ |---|---|---|
9
+ | sha3.c | https://github.com/trezor/trezor-firmware/blob/core/v2.8.0/crypto/sha3.c | f877c5f62bb2a63653bc363e0995f0bdfe70206c52f5a508d50d04aa373ce767 |
10
+ | sha3.h | https://github.com/trezor/trezor-firmware/blob/core/v2.8.0/crypto/sha3.h | 3b48be872bfeecf352140c4956482e88e60d76776d06cd22d828d3d89e4e8a5b |
11
+ | memzero.c | https://github.com/trezor/trezor-firmware/blob/core/v2.8.0/crypto/memzero.c | 2f014e8d03420b42fb7b9df39f9150a33ac96d1c48a065f5a908a967561bca02 |
12
+ | memzero.h | https://github.com/trezor/trezor-firmware/blob/core/v2.8.0/crypto/memzero.h | ffec031a236dde8df1c0b95cd0c3bd28ca046f6dc62faff36be8401ba82b214a |
13
+ | byte_order.h | https://github.com/trezor/trezor-firmware/blob/core/v2.8.0/crypto/byte_order.h | 496a5a80346a0fb48ef3fbf53c0da2b69399e1de224d67962342657a9dfd77d1 |
14
+ | options.h | https://github.com/trezor/trezor-firmware/blob/core/v2.8.0/crypto/options.h | 02884451075e84c7d9099a2fd295f1fcd87f7df0f3c7812ecba98b398ebe7a4e |
15
+ | LICENSE | https://github.com/trezor/trezor-firmware/blob/core/v2.8.0/crypto/LICENSE | b53d2eb93806f3d618370726f27eafeb6c279c7a90351297f79e073e49e569be |
16
+
17
+ `include/ckeccak.h` is ours: it only declares `keccak_256` for Swift.
18
+
19
+ ## Building outside SwiftPM (CocoaPods)
20
+
21
+ Self-contained; no defines or flags (`options.h` already sets `USE_KECCAK 1`) and no header search paths.
22
+ Compile `sha3.c` and `memzero.c`; the public header is `include/ckeccak.h`.
23
+
24
+ ```ruby
25
+ s.source_files = 'CKeccak/*.{c,h}', 'CKeccak/include/ckeccak.h'
26
+ s.public_header_files = 'CKeccak/include/ckeccak.h'
27
+ ```