@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,341 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Support/JSONValue.swift. Do not edit: change the source and run the script.
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// JSON with object key order and number literals preserved. Typed data and transactions
|
|
5
|
+
/// arrive as JSON text or Foundation values; JavaScript type checks (`typeof x === 'number'`)
|
|
6
|
+
/// in the frame map to the cases here.
|
|
7
|
+
public indirect enum JSONValue: Equatable, Sendable {
|
|
8
|
+
case null
|
|
9
|
+
case bool(Bool)
|
|
10
|
+
/// The number literal as written (no precision loss for large integers).
|
|
11
|
+
case number(String)
|
|
12
|
+
case string(String)
|
|
13
|
+
case array([JSONValue])
|
|
14
|
+
case object(JSONObject)
|
|
15
|
+
|
|
16
|
+
public subscript(key: String) -> JSONValue? {
|
|
17
|
+
if case .object(let o) = self { return o[key] }
|
|
18
|
+
return nil
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var stringValue: String? {
|
|
22
|
+
if case .string(let s) = self { return s }
|
|
23
|
+
return nil
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var objectValue: JSONObject? {
|
|
27
|
+
if case .object(let o) = self { return o }
|
|
28
|
+
return nil
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var arrayValue: [JSONValue]? {
|
|
32
|
+
if case .array(let a) = self { return a }
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// JavaScript truthiness.
|
|
37
|
+
var isTruthy: Bool {
|
|
38
|
+
switch self {
|
|
39
|
+
case .null: return false
|
|
40
|
+
case .bool(let b): return b
|
|
41
|
+
case .number(let n): return (Double(n) ?? 0) != 0
|
|
42
|
+
case .string(let s): return !s.isEmpty
|
|
43
|
+
case .array, .object: return true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// `BigInt(value)` for numbers (integral only), strings and booleans.
|
|
48
|
+
var jsBigInt: BigInteger? {
|
|
49
|
+
switch self {
|
|
50
|
+
case .number(let literal): return JSONValue.integer(literal)
|
|
51
|
+
case .string(let s): return BigInteger(jsString: s)
|
|
52
|
+
case .bool(let b): return BigInteger(BigUInt(b ? 1 : 0))
|
|
53
|
+
default: return nil
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static func integer(_ literal: String) -> BigInteger? {
|
|
58
|
+
if let exact = BigInteger(jsString: literal), !literal.isEmpty, !literal.lowercased().hasPrefix("0x") { return exact }
|
|
59
|
+
guard let d = Double(literal), d.isFinite, d.rounded() == d else { return nil }
|
|
60
|
+
let text = String(format: "%.0f", d.magnitude)
|
|
61
|
+
guard let mag = BigUInt(decimal: text) else { return nil }
|
|
62
|
+
return BigInteger(mag, negative: d < 0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MARK: - Conversion
|
|
66
|
+
|
|
67
|
+
public static func parse(_ text: String) throws -> JSONValue {
|
|
68
|
+
var parser = JSONParser(Array(text.utf8))
|
|
69
|
+
let v = try parser.parseDocument()
|
|
70
|
+
return v
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Converts Foundation values ([String: Any], [Any], NSNumber, String, NSNull). Dictionary keys are sorted.
|
|
74
|
+
public static func from(_ value: Any?) throws -> JSONValue {
|
|
75
|
+
guard let value else { return .null }
|
|
76
|
+
if let v = value as? JSONValue { return v }
|
|
77
|
+
if value is NSNull { return .null }
|
|
78
|
+
if let s = value as? String { return .string(s) }
|
|
79
|
+
// NSNumber first: a bridged 1 would also cast to Bool.
|
|
80
|
+
if let n = value as? NSNumber {
|
|
81
|
+
if CFGetTypeID(n) == CFBooleanGetTypeID() { return .bool(n.boolValue) }
|
|
82
|
+
let type = String(cString: n.objCType)
|
|
83
|
+
if type == "q" || type == "l" || type == "i" || type == "s" || type == "c" { return .number(String(n.int64Value)) }
|
|
84
|
+
if type == "Q" || type == "L" || type == "I" || type == "S" || type == "C" { return .number(String(n.uint64Value)) }
|
|
85
|
+
let d = n.doubleValue
|
|
86
|
+
guard d.isFinite else { throw WalletError(.invalidRequest, "non-finite number") }
|
|
87
|
+
if d.rounded() == d, d.magnitude < 1e18 { return .number(String(Int64(d))) }
|
|
88
|
+
return .number(String(d))
|
|
89
|
+
}
|
|
90
|
+
if let a = value as? [Any?] { return .array(try a.map { try from($0) }) }
|
|
91
|
+
if let a = value as? [Any] { return .array(try a.map { try from($0) }) }
|
|
92
|
+
if let d = value as? [String: Any?] {
|
|
93
|
+
return .object(JSONObject(try d.keys.sorted().map { ($0, try from(d[$0] ?? nil)) }))
|
|
94
|
+
}
|
|
95
|
+
if let d = value as? [String: Any] {
|
|
96
|
+
return .object(JSONObject(try d.keys.sorted().map { ($0, try from(d[$0])) }))
|
|
97
|
+
}
|
|
98
|
+
throw WalletError(.invalidRequest, "unsupported value \(type(of: value))")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// `JSON.stringify(value, null, 2)`.
|
|
102
|
+
func pretty(indent: Int = 0) -> String {
|
|
103
|
+
let pad = String(repeating: " ", count: indent + 1)
|
|
104
|
+
let close = String(repeating: " ", count: indent)
|
|
105
|
+
switch self {
|
|
106
|
+
case .null: return "null"
|
|
107
|
+
case .bool(let b): return b ? "true" : "false"
|
|
108
|
+
case .number(let n): return n
|
|
109
|
+
case .string(let s): return JSONValue.quote(s)
|
|
110
|
+
case .array(let a):
|
|
111
|
+
if a.isEmpty { return "[]" }
|
|
112
|
+
return "[\n" + a.map { pad + $0.pretty(indent: indent + 1) }.joined(separator: ",\n") + "\n" + close + "]"
|
|
113
|
+
case .object(let o):
|
|
114
|
+
if o.entries.isEmpty { return "{}" }
|
|
115
|
+
return "{\n" + o.entries.map { pad + JSONValue.quote($0.key) + ": " + $0.value.pretty(indent: indent + 1) }
|
|
116
|
+
.joined(separator: ",\n") + "\n" + close + "}"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// `JSON.stringify(value)`.
|
|
121
|
+
func compact() -> String {
|
|
122
|
+
switch self {
|
|
123
|
+
case .null, .bool, .number, .string: return pretty()
|
|
124
|
+
case .array(let a): return "[" + a.map { $0.compact() }.joined(separator: ",") + "]"
|
|
125
|
+
case .object(let o): return "{" + o.entries.map { JSONValue.quote($0.key) + ":" + $0.value.compact() }.joined(separator: ",") + "}"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static func quote(_ s: String) -> String {
|
|
130
|
+
var out = "\""
|
|
131
|
+
for u in s.unicodeScalars {
|
|
132
|
+
switch u {
|
|
133
|
+
case "\"": out += "\\\""
|
|
134
|
+
case "\\": out += "\\\\"
|
|
135
|
+
case "\n": out += "\\n"
|
|
136
|
+
case "\r": out += "\\r"
|
|
137
|
+
case "\t": out += "\\t"
|
|
138
|
+
case "\u{08}": out += "\\b"
|
|
139
|
+
case "\u{0C}": out += "\\f"
|
|
140
|
+
default:
|
|
141
|
+
if u.value < 0x20 { out += String(format: "\\u%04x", u.value) } else { out.unicodeScalars.append(u) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out + "\""
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public struct JSONObject: Equatable, Sendable {
|
|
149
|
+
public var entries: [(key: String, value: JSONValue)]
|
|
150
|
+
|
|
151
|
+
public init(_ entries: [(String, JSONValue)] = []) {
|
|
152
|
+
self.entries = entries.map { (key: $0.0, value: $0.1) }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public subscript(key: String) -> JSONValue? {
|
|
156
|
+
get { entries.last(where: { $0.key == key })?.value }
|
|
157
|
+
set {
|
|
158
|
+
if let i = entries.firstIndex(where: { $0.key == key }) {
|
|
159
|
+
if let newValue { entries[i].value = newValue } else { entries.remove(at: i) }
|
|
160
|
+
} else if let newValue {
|
|
161
|
+
entries.append((key: key, value: newValue))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public var keys: [String] { entries.map(\.key) }
|
|
167
|
+
|
|
168
|
+
public static func == (a: JSONObject, b: JSONObject) -> Bool {
|
|
169
|
+
a.entries.count == b.entries.count && zip(a.entries, b.entries).allSatisfy { $0.key == $1.key && $0.value == $1.value }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private struct JSONParser {
|
|
174
|
+
let s: [UInt8]
|
|
175
|
+
var i = 0
|
|
176
|
+
var depth = 0
|
|
177
|
+
|
|
178
|
+
init(_ s: [UInt8]) { self.s = s }
|
|
179
|
+
|
|
180
|
+
private func fail() -> WalletError { WalletError(.invalidRequest, "invalid JSON") }
|
|
181
|
+
|
|
182
|
+
mutating func parseDocument() throws -> JSONValue {
|
|
183
|
+
let v = try value()
|
|
184
|
+
skip()
|
|
185
|
+
guard i == s.count else { throw fail() }
|
|
186
|
+
return v
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
mutating func skip() {
|
|
190
|
+
while i < s.count, s[i] == 0x20 || s[i] == 0x0a || s[i] == 0x0d || s[i] == 0x09 { i += 1 }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
mutating func value() throws -> JSONValue {
|
|
194
|
+
skip()
|
|
195
|
+
guard i < s.count else { throw fail() }
|
|
196
|
+
switch s[i] {
|
|
197
|
+
case UInt8(ascii: "{"): return try object()
|
|
198
|
+
case UInt8(ascii: "["): return try array()
|
|
199
|
+
case UInt8(ascii: "\""): return .string(try string())
|
|
200
|
+
case UInt8(ascii: "t"): try literal("true"); return .bool(true)
|
|
201
|
+
case UInt8(ascii: "f"): try literal("false"); return .bool(false)
|
|
202
|
+
case UInt8(ascii: "n"): try literal("null"); return .null
|
|
203
|
+
default: return try number()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
mutating func literal(_ word: String) throws {
|
|
208
|
+
let w = Array(word.utf8)
|
|
209
|
+
guard i + w.count <= s.count, Array(s[i..<i + w.count]) == w else { throw fail() }
|
|
210
|
+
i += w.count
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mutating func object() throws -> JSONValue {
|
|
214
|
+
depth += 1
|
|
215
|
+
guard depth < 64 else { throw fail() }
|
|
216
|
+
defer { depth -= 1 }
|
|
217
|
+
i += 1
|
|
218
|
+
var obj = JSONObject()
|
|
219
|
+
skip()
|
|
220
|
+
if i < s.count, s[i] == UInt8(ascii: "}") { i += 1; return .object(obj) }
|
|
221
|
+
while true {
|
|
222
|
+
skip()
|
|
223
|
+
guard i < s.count, s[i] == UInt8(ascii: "\"") else { throw fail() }
|
|
224
|
+
let k = try string()
|
|
225
|
+
skip()
|
|
226
|
+
guard i < s.count, s[i] == UInt8(ascii: ":") else { throw fail() }
|
|
227
|
+
i += 1
|
|
228
|
+
let v = try value()
|
|
229
|
+
// JSON.parse keeps the first position and the last value of a duplicate key.
|
|
230
|
+
obj[k] = v
|
|
231
|
+
skip()
|
|
232
|
+
guard i < s.count else { throw fail() }
|
|
233
|
+
if s[i] == UInt8(ascii: ",") { i += 1; continue }
|
|
234
|
+
if s[i] == UInt8(ascii: "}") { i += 1; return .object(obj) }
|
|
235
|
+
throw fail()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
mutating func array() throws -> JSONValue {
|
|
240
|
+
depth += 1
|
|
241
|
+
guard depth < 64 else { throw fail() }
|
|
242
|
+
defer { depth -= 1 }
|
|
243
|
+
i += 1
|
|
244
|
+
var out = [JSONValue]()
|
|
245
|
+
skip()
|
|
246
|
+
if i < s.count, s[i] == UInt8(ascii: "]") { i += 1; return .array(out) }
|
|
247
|
+
while true {
|
|
248
|
+
out.append(try value())
|
|
249
|
+
skip()
|
|
250
|
+
guard i < s.count else { throw fail() }
|
|
251
|
+
if s[i] == UInt8(ascii: ",") { i += 1; continue }
|
|
252
|
+
if s[i] == UInt8(ascii: "]") { i += 1; return .array(out) }
|
|
253
|
+
throw fail()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
mutating func string() throws -> String {
|
|
258
|
+
i += 1
|
|
259
|
+
var scalars = String.UnicodeScalarView()
|
|
260
|
+
var raw = [UInt8]()
|
|
261
|
+
func flush() throws {
|
|
262
|
+
if raw.isEmpty { return }
|
|
263
|
+
guard let str = String(bytes: raw, encoding: .utf8) else { throw fail() }
|
|
264
|
+
scalars.append(contentsOf: str.unicodeScalars)
|
|
265
|
+
raw.removeAll()
|
|
266
|
+
}
|
|
267
|
+
while i < s.count {
|
|
268
|
+
let c = s[i]
|
|
269
|
+
if c == UInt8(ascii: "\"") {
|
|
270
|
+
i += 1
|
|
271
|
+
try flush()
|
|
272
|
+
return String(scalars)
|
|
273
|
+
}
|
|
274
|
+
if c < 0x20 { throw fail() }
|
|
275
|
+
if c == UInt8(ascii: "\\") {
|
|
276
|
+
try flush()
|
|
277
|
+
i += 1
|
|
278
|
+
guard i < s.count else { throw fail() }
|
|
279
|
+
let e = s[i]
|
|
280
|
+
i += 1
|
|
281
|
+
switch e {
|
|
282
|
+
case UInt8(ascii: "\""): scalars.append("\"")
|
|
283
|
+
case UInt8(ascii: "\\"): scalars.append("\\")
|
|
284
|
+
case UInt8(ascii: "/"): scalars.append("/")
|
|
285
|
+
case UInt8(ascii: "b"): scalars.append("\u{08}")
|
|
286
|
+
case UInt8(ascii: "f"): scalars.append("\u{0C}")
|
|
287
|
+
case UInt8(ascii: "n"): scalars.append("\n")
|
|
288
|
+
case UInt8(ascii: "r"): scalars.append("\r")
|
|
289
|
+
case UInt8(ascii: "t"): scalars.append("\t")
|
|
290
|
+
case UInt8(ascii: "u"):
|
|
291
|
+
var unit = try hex4()
|
|
292
|
+
if (0xD800..<0xDC00).contains(unit), i + 1 < s.count, s[i] == UInt8(ascii: "\\"), s[i + 1] == UInt8(ascii: "u") {
|
|
293
|
+
let save = i
|
|
294
|
+
i += 2
|
|
295
|
+
let low = try hex4()
|
|
296
|
+
if (0xDC00..<0xE000).contains(low) {
|
|
297
|
+
unit = 0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00)
|
|
298
|
+
} else {
|
|
299
|
+
i = save
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Lone surrogates become U+FFFD (JavaScript keeps them; Swift strings cannot).
|
|
303
|
+
scalars.append(Unicode.Scalar(unit) ?? "\u{FFFD}")
|
|
304
|
+
default: throw fail()
|
|
305
|
+
}
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
raw.append(c)
|
|
309
|
+
i += 1
|
|
310
|
+
}
|
|
311
|
+
throw fail()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
mutating func hex4() throws -> UInt32 {
|
|
315
|
+
guard i + 4 <= s.count, let v = UInt32(String(decoding: s[i..<i + 4], as: UTF8.self), radix: 16) else { throw fail() }
|
|
316
|
+
i += 4
|
|
317
|
+
return v
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
mutating func number() throws -> JSONValue {
|
|
321
|
+
let start = i
|
|
322
|
+
if i < s.count, s[i] == UInt8(ascii: "-") { i += 1 }
|
|
323
|
+
let intStart = i
|
|
324
|
+
while i < s.count, (0x30...0x39).contains(s[i]) { i += 1 }
|
|
325
|
+
guard i > intStart, !(s[intStart] == 0x30 && i - intStart > 1) else { throw fail() }
|
|
326
|
+
if i < s.count, s[i] == UInt8(ascii: ".") {
|
|
327
|
+
i += 1
|
|
328
|
+
let f = i
|
|
329
|
+
while i < s.count, (0x30...0x39).contains(s[i]) { i += 1 }
|
|
330
|
+
guard i > f else { throw fail() }
|
|
331
|
+
}
|
|
332
|
+
if i < s.count, s[i] == UInt8(ascii: "e") || s[i] == UInt8(ascii: "E") {
|
|
333
|
+
i += 1
|
|
334
|
+
if i < s.count, s[i] == UInt8(ascii: "+") || s[i] == UInt8(ascii: "-") { i += 1 }
|
|
335
|
+
let e = i
|
|
336
|
+
while i < s.count, (0x30...0x39).contains(s[i]) { i += 1 }
|
|
337
|
+
guard i > e else { throw fail() }
|
|
338
|
+
}
|
|
339
|
+
return .number(String(decoding: s[start..<i], as: UTF8.self))
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/UI/WalletApprover.swift. Do not edit: change the source and run the script.
|
|
2
|
+
import Foundation
|
|
3
|
+
import LocalAuthentication
|
|
4
|
+
import SwiftUI
|
|
5
|
+
#if canImport(UIKit)
|
|
6
|
+
import UIKit
|
|
7
|
+
#elseif canImport(AppKit)
|
|
8
|
+
import AppKit
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
/// Passkey operations an approver may confirm before the system passkey sheet appears.
|
|
12
|
+
public enum PasskeyAction: String, Sendable {
|
|
13
|
+
case protect
|
|
14
|
+
case upgradeBackup
|
|
15
|
+
case restore
|
|
16
|
+
/// Restore that revokes every other device.
|
|
17
|
+
case restoreReset
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Approves signatures. It runs before the company share is requested, so a refused request
|
|
21
|
+
/// never assembles the key. Apps can replace the default sheet; the app is inside the trust
|
|
22
|
+
/// boundary anyway (docs/wallet/NATIVE.md section 2).
|
|
23
|
+
public protocol WalletApprover: Sendable {
|
|
24
|
+
/// Return to approve; throw (USER_CANCELLED) to refuse.
|
|
25
|
+
func approveSignature(_ summary: SignSummary) async throws
|
|
26
|
+
/// Return to continue; throw (USER_CANCELLED) to stop.
|
|
27
|
+
func confirmPasskeyAction(_ action: PasskeyAction) async throws
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
extension WalletApprover {
|
|
31
|
+
public func confirmPasskeyAction(_ action: PasskeyAction) async throws {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Extra device-owner check (Face ID, Touch ID or passcode) before each signature.
|
|
35
|
+
public protocol DeviceAuthenticator: Sendable {
|
|
36
|
+
func authenticate(reason: String) async throws
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public struct LocalDeviceAuthenticator: DeviceAuthenticator {
|
|
40
|
+
public init() {}
|
|
41
|
+
|
|
42
|
+
public func authenticate(reason: String) async throws {
|
|
43
|
+
let context = LAContext()
|
|
44
|
+
var error: NSError?
|
|
45
|
+
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
|
|
46
|
+
throw WalletError(.forbidden, "device authentication is not available")
|
|
47
|
+
}
|
|
48
|
+
let ok: Bool
|
|
49
|
+
do {
|
|
50
|
+
ok = try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason)
|
|
51
|
+
} catch {
|
|
52
|
+
throw WalletError(.userCancelled)
|
|
53
|
+
}
|
|
54
|
+
if !ok { throw WalletError(.userCancelled) }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The default approver: a SwiftUI sheet over the key window. It never waits for a sheet
|
|
59
|
+
/// that is not on screen: without a window to present from, or when the sheet has not
|
|
60
|
+
/// appeared within `appearTimeout`, the request fails with CONFIRM_WINDOW_BLOCKED (nothing
|
|
61
|
+
/// is signed). Rejecting in the sheet is USER_CANCELLED.
|
|
62
|
+
public struct SheetWalletApprover: WalletApprover {
|
|
63
|
+
#if canImport(UIKit)
|
|
64
|
+
typealias Presenter = @MainActor @Sendable () -> UIViewController?
|
|
65
|
+
#else
|
|
66
|
+
typealias Presenter = @MainActor @Sendable () -> NSWindow?
|
|
67
|
+
#endif
|
|
68
|
+
|
|
69
|
+
let appearTimeout: TimeInterval
|
|
70
|
+
let presenter: Presenter
|
|
71
|
+
|
|
72
|
+
public init(appearTimeout: TimeInterval = 2) {
|
|
73
|
+
self.init(appearTimeout: appearTimeout, presenter: { Self.defaultPresenter() })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
init(appearTimeout: TimeInterval, presenter: @escaping Presenter) {
|
|
77
|
+
self.appearTimeout = appearTimeout
|
|
78
|
+
self.presenter = presenter
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
enum Outcome { case approved, rejected, blocked }
|
|
82
|
+
|
|
83
|
+
private static func check(_ outcome: Outcome) throws {
|
|
84
|
+
switch outcome {
|
|
85
|
+
case .approved: return
|
|
86
|
+
case .rejected: throw WalletError(.userCancelled)
|
|
87
|
+
case .blocked: throw WalletError(.confirmWindowBlocked, "the approval sheet could not be shown")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public func approveSignature(_ summary: SignSummary) async throws {
|
|
92
|
+
try Self.check(await present { finish in ApprovalView(summary: summary, onFinish: finish) })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public func confirmPasskeyAction(_ action: PasskeyAction) async throws {
|
|
96
|
+
// The system passkey sheet is the confirmation for the other actions.
|
|
97
|
+
guard action == .restoreReset else { return }
|
|
98
|
+
try Self.check(await present { finish in ResetConfirmView(onFinish: finish) })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@MainActor
|
|
102
|
+
func present<V: View>(_ build: @escaping (@escaping (Bool) -> Void) -> V) async -> Outcome {
|
|
103
|
+
await withCheckedContinuation { (cont: CheckedContinuation<Outcome, Never>) in
|
|
104
|
+
var finished = false
|
|
105
|
+
var appeared = false
|
|
106
|
+
#if canImport(UIKit)
|
|
107
|
+
var host: UIViewController?
|
|
108
|
+
let complete: (Outcome) -> Void = { outcome in
|
|
109
|
+
guard !finished else { return }
|
|
110
|
+
finished = true
|
|
111
|
+
// Resume once the sheet is gone, so a following request can present its own
|
|
112
|
+
// sheet (presenting on a controller that is being dismissed silently fails).
|
|
113
|
+
guard let h = host, h.presentingViewController != nil else {
|
|
114
|
+
cont.resume(returning: outcome)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
h.dismiss(animated: true) { cont.resume(returning: outcome) }
|
|
118
|
+
}
|
|
119
|
+
let controller = UIHostingController(rootView: build { complete($0 ? .approved : .rejected) })
|
|
120
|
+
controller.isModalInPresentation = true
|
|
121
|
+
host = controller
|
|
122
|
+
guard let top = presenter() else {
|
|
123
|
+
complete(.blocked)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
top.present(controller, animated: true) { appeared = true }
|
|
127
|
+
let isOnScreen = { appeared || controller.viewIfLoaded?.window != nil }
|
|
128
|
+
#else
|
|
129
|
+
var sheet: NSWindow?
|
|
130
|
+
var parent: NSWindow?
|
|
131
|
+
let complete: (Outcome) -> Void = { outcome in
|
|
132
|
+
guard !finished else { return }
|
|
133
|
+
finished = true
|
|
134
|
+
if let sheet, sheet.sheetParent != nil { parent?.endSheet(sheet) }
|
|
135
|
+
cont.resume(returning: outcome)
|
|
136
|
+
}
|
|
137
|
+
guard let window = presenter() else {
|
|
138
|
+
complete(.blocked)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
let w = NSWindow(contentViewController: NSHostingController(rootView: build { complete($0 ? .approved : .rejected) }.frame(minWidth: 420, minHeight: 360)))
|
|
142
|
+
sheet = w
|
|
143
|
+
parent = window
|
|
144
|
+
window.beginSheet(w)
|
|
145
|
+
let isOnScreen = { appeared || w.sheetParent != nil || w.isVisible }
|
|
146
|
+
#endif
|
|
147
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + appearTimeout) {
|
|
148
|
+
if !finished && !isOnScreen() { complete(.blocked) }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#if canImport(UIKit)
|
|
154
|
+
@MainActor
|
|
155
|
+
static func defaultPresenter() -> UIViewController? {
|
|
156
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
157
|
+
let window = scenes.flatMap(\.windows).first(where: \.isKeyWindow) ?? scenes.first?.windows.first
|
|
158
|
+
var top = window?.rootViewController
|
|
159
|
+
while let presented = top?.presentedViewController, !presented.isBeingDismissed { top = presented }
|
|
160
|
+
return top
|
|
161
|
+
}
|
|
162
|
+
#else
|
|
163
|
+
@MainActor
|
|
164
|
+
static func defaultPresenter() -> NSWindow? {
|
|
165
|
+
NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first(where: \.isVisible)
|
|
166
|
+
}
|
|
167
|
+
#endif
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
struct ApprovalView: View {
|
|
171
|
+
let summary: SignSummary
|
|
172
|
+
let onFinish: (Bool) -> Void
|
|
173
|
+
@State private var readToEnd = false
|
|
174
|
+
|
|
175
|
+
var body: some View {
|
|
176
|
+
VStack(alignment: .leading, spacing: 16) {
|
|
177
|
+
Text(summary.title).font(.title2.bold())
|
|
178
|
+
ForEach(Array(summary.warnings.enumerated()), id: \.offset) { _, w in
|
|
179
|
+
Label(w, systemImage: "exclamationmark.triangle.fill")
|
|
180
|
+
.foregroundColor(.red)
|
|
181
|
+
.font(.callout)
|
|
182
|
+
}
|
|
183
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
184
|
+
ForEach(Array(summary.rows.enumerated()), id: \.offset) { _, row in
|
|
185
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
186
|
+
Text(row.label).font(.caption).foregroundColor(.secondary)
|
|
187
|
+
Text(row.value)
|
|
188
|
+
.font(row.mono ? .system(.footnote, design: .monospaced) : .body)
|
|
189
|
+
.textSelection(.enabled)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if let text = summary.body {
|
|
194
|
+
ScrollView {
|
|
195
|
+
LazyVStack(alignment: .leading, spacing: 0) {
|
|
196
|
+
Text(text)
|
|
197
|
+
.font(.system(.footnote, design: .monospaced))
|
|
198
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
199
|
+
.textSelection(.enabled)
|
|
200
|
+
Color.clear.frame(height: 1).onAppear { readToEnd = true }
|
|
201
|
+
}
|
|
202
|
+
.padding(8)
|
|
203
|
+
}
|
|
204
|
+
.frame(maxHeight: 280)
|
|
205
|
+
.background(Color.secondary.opacity(0.1))
|
|
206
|
+
.cornerRadius(8)
|
|
207
|
+
if !readToEnd {
|
|
208
|
+
Text("Scroll to the end to sign.").font(.caption).foregroundColor(.secondary)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
Spacer(minLength: 0)
|
|
212
|
+
HStack(spacing: 12) {
|
|
213
|
+
Button(role: .cancel) { onFinish(false) } label: {
|
|
214
|
+
Text("Reject").frame(maxWidth: .infinity)
|
|
215
|
+
}
|
|
216
|
+
.buttonStyle(.bordered)
|
|
217
|
+
Button { onFinish(true) } label: {
|
|
218
|
+
Text("Sign").frame(maxWidth: .infinity)
|
|
219
|
+
}
|
|
220
|
+
.buttonStyle(.borderedProminent)
|
|
221
|
+
.tint(summary.warnings.isEmpty ? .accentColor : .red)
|
|
222
|
+
.disabled(summary.body != nil && !readToEnd)
|
|
223
|
+
}
|
|
224
|
+
.controlSize(.large)
|
|
225
|
+
}
|
|
226
|
+
.padding(20)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
struct ResetConfirmView: View {
|
|
231
|
+
let onFinish: (Bool) -> Void
|
|
232
|
+
|
|
233
|
+
var body: some View {
|
|
234
|
+
VStack(alignment: .leading, spacing: 16) {
|
|
235
|
+
Text("Restore and sign out other devices").font(.title2.bold())
|
|
236
|
+
Text("Your wallet will be restored on this device with your passkey. Every other device and browser that holds this wallet will stop working and must restore again.")
|
|
237
|
+
.font(.body)
|
|
238
|
+
Spacer(minLength: 0)
|
|
239
|
+
HStack(spacing: 12) {
|
|
240
|
+
Button(role: .cancel) { onFinish(false) } label: { Text("Cancel").frame(maxWidth: .infinity) }
|
|
241
|
+
.buttonStyle(.bordered)
|
|
242
|
+
Button { onFinish(true) } label: { Text("Restore").frame(maxWidth: .infinity) }
|
|
243
|
+
.buttonStyle(.borderedProminent)
|
|
244
|
+
.tint(.red)
|
|
245
|
+
}
|
|
246
|
+
.controlSize(.large)
|
|
247
|
+
}
|
|
248
|
+
.padding(20)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonWallet/Wallet/Backup.swift. Do not edit: change the source and run the script.
|
|
2
|
+
import CryptoKit
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/// Passkey backup encryption, byte-identical to `apps/wallet/src/prf.ts`.
|
|
6
|
+
public enum BackupVersion: Int, Sendable {
|
|
7
|
+
/// Salt SHA-256("authon-wallet-backup-v1"); ciphertext without prefix.
|
|
8
|
+
case v1 = 1
|
|
9
|
+
/// Salt bound to project and wallet; ciphertext stored as `v2.<base64>`.
|
|
10
|
+
case v2 = 2
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
enum WalletBackup {
|
|
14
|
+
static let saltLabelV1 = "authon-wallet-backup-v1"
|
|
15
|
+
static let saltLabelV2 = "authon-wallet-backup-v2"
|
|
16
|
+
static let hkdfInfo = "authon-wallet-backup-v1/aes-gcm-256"
|
|
17
|
+
static let v2Prefix = "v2."
|
|
18
|
+
|
|
19
|
+
static func passkeyUserId(projectId: String, userId: String) -> [UInt8] {
|
|
20
|
+
Bytes.sha256(Array("\(projectId):\(userId)".utf8))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func prfSalt(_ version: BackupVersion, projectId: String, walletId: String) -> [UInt8] {
|
|
24
|
+
switch version {
|
|
25
|
+
case .v1: return Bytes.sha256(Array(saltLabelV1.utf8))
|
|
26
|
+
case .v2: return Bytes.sha256(Array("\(saltLabelV2)\u{0}\(projectId)\u{0}\(walletId)".utf8))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func version(of payload: String) -> BackupVersion { payload.hasPrefix(v2Prefix) ? .v2 : .v1 }
|
|
31
|
+
|
|
32
|
+
/// HKDF-SHA256(ikm = PRF output, salt = empty, info) -> AES-256 key.
|
|
33
|
+
static func deriveKey(prfOutput: [UInt8]) -> SymmetricKey {
|
|
34
|
+
HKDF<SHA256>.deriveKey(
|
|
35
|
+
inputKeyMaterial: SymmetricKey(data: prfOutput),
|
|
36
|
+
salt: Data(),
|
|
37
|
+
info: Data(hkdfInfo.utf8),
|
|
38
|
+
outputByteCount: 32
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private static func aad(_ walletId: String, _ version: BackupVersion) -> Data {
|
|
43
|
+
Data((version == .v1 ? "backup:\(walletId)" : "backup:v2:\(walletId)").utf8)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static func encrypt(key: SymmetricKey, share: [UInt8], walletId: String, version: BackupVersion, iv: [UInt8]? = nil) throws -> String {
|
|
47
|
+
let nonce = try AES.GCM.Nonce(data: iv ?? (try Bytes.random(12)))
|
|
48
|
+
let box = try AES.GCM.seal(share, using: key, nonce: nonce, authenticating: aad(walletId, version))
|
|
49
|
+
let body = Bytes.base64(Array(nonce) + Array(box.ciphertext) + Array(box.tag))
|
|
50
|
+
return version == .v1 ? body : v2Prefix + body
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static func decrypt(key: SymmetricKey, payload: String, walletId: String) throws -> [UInt8] {
|
|
54
|
+
let version = version(of: payload)
|
|
55
|
+
guard let raw = Bytes.fromBase64(version == .v1 ? payload : String(payload.dropFirst(v2Prefix.count))), raw.count >= 28 else {
|
|
56
|
+
throw WalletError(.invalidRequest, "backup ciphertext malformed")
|
|
57
|
+
}
|
|
58
|
+
do {
|
|
59
|
+
let box = try AES.GCM.SealedBox(
|
|
60
|
+
nonce: AES.GCM.Nonce(data: raw[0..<12]),
|
|
61
|
+
ciphertext: raw[12..<(raw.count - 16)],
|
|
62
|
+
tag: raw[(raw.count - 16)...]
|
|
63
|
+
)
|
|
64
|
+
return Array(try AES.GCM.open(box, using: key, authenticating: aad(walletId, version)))
|
|
65
|
+
} catch {
|
|
66
|
+
throw WalletError(.invalidRequest, "backup cannot be decrypted with this passkey")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|