@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.
- package/CHANGELOG.md +20 -0
- package/README.md +251 -2
- package/android/build.gradle +40 -0
- package/android/consumer-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +235 -0
- package/android/src/vendor/GENERATED.md +4 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +218 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +236 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +361 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +74 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +551 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +78 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
- package/build/authonContext.d.ts +19 -0
- package/build/authonContext.d.ts.map +1 -0
- package/build/authonContext.js +20 -0
- package/build/authonContext.js.map +1 -0
- package/build/client.d.ts +92 -0
- package/build/client.d.ts.map +1 -0
- package/build/client.js +431 -0
- package/build/client.js.map +1 -0
- package/build/errors.d.ts +25 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +15 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/native.d.ts +43 -0
- package/build/native.d.ts.map +1 -0
- package/build/native.js +12 -0
- package/build/native.js.map +1 -0
- package/build/types.d.ts +167 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +3 -0
- package/build/types.js.map +1 -0
- package/build/useAuthonWallet.d.ts +25 -0
- package/build/useAuthonWallet.d.ts.map +1 -0
- package/build/useAuthonWallet.js +77 -0
- package/build/useAuthonWallet.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AuthonReactNativeWallet.podspec +41 -0
- package/ios/AuthonWalletModule.swift +233 -0
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +232 -0
- package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
- package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
- package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
- package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
- package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
- package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
- package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
- package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +128 -0
- package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
- package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
- package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
- package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
- package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
- package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
- package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
- package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
- package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
- package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
- package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
- package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
- package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
- package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
- package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +361 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +654 -0
- package/ios/vendor/AuthonWallet/WalletError.swift +84 -0
- package/ios/vendor/CKeccak/LICENSE +22 -0
- package/ios/vendor/CKeccak/README.md +30 -0
- package/ios/vendor/CKeccak/byte_order.h +59 -0
- package/ios/vendor/CKeccak/include/ckeccak.h +13 -0
- package/ios/vendor/CKeccak/memzero.c +75 -0
- package/ios/vendor/CKeccak/memzero.h +8 -0
- package/ios/vendor/CKeccak/options.h +99 -0
- package/ios/vendor/CKeccak/ripemd160.c +343 -0
- package/ios/vendor/CKeccak/ripemd160.h +22 -0
- package/ios/vendor/CKeccak/sha3.c +413 -0
- package/ios/vendor/CKeccak/sha3.h +89 -0
- package/ios/vendor/CMonocypher/LICENCE.md +173 -0
- package/ios/vendor/CMonocypher/README.md +27 -0
- package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
- package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
- package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
- package/ios/vendor/CMonocypher/monocypher.c +2956 -0
- package/ios/vendor/CSecp256k1/COPYING +19 -0
- package/ios/vendor/CSecp256k1/README.md +35 -0
- package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
- package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
- package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
- package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
- package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
- package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
- package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
- package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
- package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
- package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
- package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
- package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
- package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
- package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
- package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
- package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
- package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
- package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
- package/ios/vendor/CSecp256k1/src/field.h +350 -0
- package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
- package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
- package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
- package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
- package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
- package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
- package/ios/vendor/CSecp256k1/src/group.h +212 -0
- package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
- package/ios/vendor/CSecp256k1/src/hash.h +44 -0
- package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
- package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
- package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
- package/ios/vendor/CSecp256k1/src/int128.h +90 -0
- package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
- package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
- package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
- package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
- package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
- package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
- package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
- package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
- package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
- package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
- package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
- package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
- package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
- package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
- package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
- package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
- package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
- package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
- package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
- package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
- package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
- package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
- package/ios/vendor/CSecp256k1/src/util.h +451 -0
- package/ios/vendor/GENERATED.md +4 -0
- package/package.json +66 -3
- package/src/authonContext.ts +30 -0
- package/src/client.ts +459 -0
- package/src/errors.ts +58 -0
- package/src/index.ts +26 -0
- package/src/native.ts +52 -0
- package/src/types.ts +171 -0
- 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
|
+
}
|