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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +251 -2
  3. package/android/build.gradle +40 -0
  4. package/android/consumer-rules.pro +10 -0
  5. package/android/src/main/AndroidManifest.xml +8 -0
  6. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +235 -0
  7. package/android/src/vendor/GENERATED.md +4 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +218 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
  21. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
  22. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
  23. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +86 -0
  24. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
  25. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +236 -0
  26. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
  27. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
  28. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
  29. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
  30. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
  31. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
  32. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +361 -0
  33. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
  34. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +74 -0
  35. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +551 -0
  36. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +78 -0
  37. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
  38. package/build/authonContext.d.ts +19 -0
  39. package/build/authonContext.d.ts.map +1 -0
  40. package/build/authonContext.js +20 -0
  41. package/build/authonContext.js.map +1 -0
  42. package/build/client.d.ts +92 -0
  43. package/build/client.d.ts.map +1 -0
  44. package/build/client.js +431 -0
  45. package/build/client.js.map +1 -0
  46. package/build/errors.d.ts +25 -0
  47. package/build/errors.d.ts.map +1 -0
  48. package/build/errors.js +15 -0
  49. package/build/errors.js.map +1 -0
  50. package/build/index.d.ts +7 -0
  51. package/build/index.d.ts.map +1 -0
  52. package/build/index.js +4 -0
  53. package/build/index.js.map +1 -0
  54. package/build/native.d.ts +43 -0
  55. package/build/native.d.ts.map +1 -0
  56. package/build/native.js +12 -0
  57. package/build/native.js.map +1 -0
  58. package/build/types.d.ts +167 -0
  59. package/build/types.d.ts.map +1 -0
  60. package/build/types.js +3 -0
  61. package/build/types.js.map +1 -0
  62. package/build/useAuthonWallet.d.ts +25 -0
  63. package/build/useAuthonWallet.d.ts.map +1 -0
  64. package/build/useAuthonWallet.js +77 -0
  65. package/build/useAuthonWallet.js.map +1 -0
  66. package/expo-module.config.json +9 -0
  67. package/ios/AuthonReactNativeWallet.podspec +41 -0
  68. package/ios/AuthonWalletModule.swift +233 -0
  69. package/ios/vendor/AuthonWallet/AuthonWallet.swift +232 -0
  70. package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
  71. package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
  72. package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
  73. package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
  74. package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
  75. package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
  76. package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
  77. package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
  78. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +128 -0
  79. package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
  80. package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
  81. package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
  82. package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
  83. package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
  84. package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
  85. package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
  86. package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
  87. package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
  88. package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
  89. package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
  90. package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
  91. package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
  92. package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
  93. package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
  94. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +361 -0
  95. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +654 -0
  96. package/ios/vendor/AuthonWallet/WalletError.swift +84 -0
  97. package/ios/vendor/CKeccak/LICENSE +22 -0
  98. package/ios/vendor/CKeccak/README.md +30 -0
  99. package/ios/vendor/CKeccak/byte_order.h +59 -0
  100. package/ios/vendor/CKeccak/include/ckeccak.h +13 -0
  101. package/ios/vendor/CKeccak/memzero.c +75 -0
  102. package/ios/vendor/CKeccak/memzero.h +8 -0
  103. package/ios/vendor/CKeccak/options.h +99 -0
  104. package/ios/vendor/CKeccak/ripemd160.c +343 -0
  105. package/ios/vendor/CKeccak/ripemd160.h +22 -0
  106. package/ios/vendor/CKeccak/sha3.c +413 -0
  107. package/ios/vendor/CKeccak/sha3.h +89 -0
  108. package/ios/vendor/CMonocypher/LICENCE.md +173 -0
  109. package/ios/vendor/CMonocypher/README.md +27 -0
  110. package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
  111. package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
  112. package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
  113. package/ios/vendor/CMonocypher/monocypher.c +2956 -0
  114. package/ios/vendor/CSecp256k1/COPYING +19 -0
  115. package/ios/vendor/CSecp256k1/README.md +35 -0
  116. package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
  117. package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
  118. package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
  119. package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
  120. package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
  121. package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
  122. package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
  123. package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
  124. package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
  125. package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
  126. package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
  127. package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
  128. package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
  129. package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
  130. package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
  131. package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
  132. package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
  133. package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
  134. package/ios/vendor/CSecp256k1/src/field.h +350 -0
  135. package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
  136. package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
  137. package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
  138. package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
  139. package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
  140. package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
  141. package/ios/vendor/CSecp256k1/src/group.h +212 -0
  142. package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
  143. package/ios/vendor/CSecp256k1/src/hash.h +44 -0
  144. package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
  145. package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
  146. package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
  147. package/ios/vendor/CSecp256k1/src/int128.h +90 -0
  148. package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
  149. package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
  150. package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
  151. package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
  152. package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
  153. package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
  154. package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
  155. package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
  156. package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
  157. package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
  158. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
  159. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
  160. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
  161. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
  162. package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
  163. package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
  164. package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
  165. package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
  166. package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
  167. package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
  168. package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
  169. package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
  170. package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
  171. package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
  172. package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
  173. package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
  174. package/ios/vendor/CSecp256k1/src/util.h +451 -0
  175. package/ios/vendor/GENERATED.md +4 -0
  176. package/package.json +66 -3
  177. package/src/authonContext.ts +30 -0
  178. package/src/client.ts +459 -0
  179. package/src/errors.ts +58 -0
  180. package/src/index.ts +26 -0
  181. package/src/native.ts +52 -0
  182. package/src/types.ts +171 -0
  183. package/src/useAuthonWallet.ts +103 -0
@@ -0,0 +1,130 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/HdWallet.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import org.web3j.crypto.Bip32ECKeyPair
5
+ import org.web3j.crypto.MnemonicUtils
6
+ import org.web3j.utils.Numeric
7
+
8
+ /**
9
+ * The shared secret is BIP39 entropy (16 bytes = 12 words); keys are m/44'/60'/0'/0/{index}.
10
+ * A 32-byte secret is a raw secp256k1 key from before HD wallets and has only account 0.
11
+ * Same rules as `apps/wallet/src/shares.ts`.
12
+ */
13
+ internal object HdWallet {
14
+ const val HD_ENTROPY_BYTES = 16
15
+ const val MAX_ACCOUNT_INDEX: Long = 0x7fffffff
16
+
17
+ private const val H = Bip32ECKeyPair.HARDENED_BIT
18
+ private val ACCOUNT_PARENT = intArrayOf(44 or H, 60 or H, 0 or H, 0)
19
+
20
+ fun evmPath(index: Long): String = "m/44'/60'/0'/0/$index"
21
+
22
+ fun generateSecret(): ByteArray = Bytes.randomBytes(HD_ENTROPY_BYTES)
23
+
24
+ fun isHdSecret(secret: ByteArray): Boolean = secret.size == HD_ENTROPY_BYTES
25
+
26
+ fun isAccountIndex(index: Long): Boolean = index in 0..MAX_ACCOUNT_INDEX
27
+
28
+ fun mnemonicOf(entropy: ByteArray): String = MnemonicUtils.generateMnemonic(entropy)
29
+
30
+ /** BIP39 seed (PBKDF2-HMAC-SHA512, 2048 rounds, salt "mnemonic"). Callers wipe it. */
31
+ fun seedOf(entropy: ByteArray): ByteArray = MnemonicUtils.generateSeed(mnemonicOf(entropy), "")
32
+
33
+ private fun accountParent(secret: ByteArray): Bip32ECKeyPair {
34
+ val seed = seedOf(secret)
35
+ try {
36
+ return Bip32ECKeyPair.deriveKeyPair(Bip32ECKeyPair.generateKeyPair(seed), ACCOUNT_PARENT)
37
+ } finally {
38
+ seed.fill(0)
39
+ }
40
+ }
41
+
42
+ /** The EVM private key at account [index]. Callers wipe the result. */
43
+ fun privateKey(secret: ByteArray, index: Long = 0): ByteArray {
44
+ if (!isAccountIndex(index)) throw IllegalArgumentException("invalid account index")
45
+ if (secret.size == 32) {
46
+ if (index != 0L) throw walletError(WalletErrorCode.ACCOUNT_UNAVAILABLE, "this wallet has only account 0")
47
+ return secret.copyOf()
48
+ }
49
+ if (!isHdSecret(secret)) throw walletError(WalletErrorCode.INTERNAL, "unsupported wallet secret")
50
+ val node = Bip32ECKeyPair.deriveKeyPair(accountParent(secret), intArrayOf(index.toInt()))
51
+ return Numeric.toBytesPadded(node.privateKey, 32)
52
+ }
53
+
54
+ fun account(secret: ByteArray, index: Long = 0): EvmAccount {
55
+ val key = privateKey(secret, index)
56
+ try {
57
+ return EvmAccount(key)
58
+ } finally {
59
+ key.fill(0)
60
+ }
61
+ }
62
+
63
+ fun address(secret: ByteArray, index: Long = 0): String = account(secret, index).address
64
+
65
+ /** Addresses of accounts start..start+count-1; the seed is derived once for the range. */
66
+ fun addresses(secret: ByteArray, start: Long, count: Int): List<String> {
67
+ if (!isAccountIndex(start) || count < 1 || !isAccountIndex(start + count - 1)) {
68
+ throw IllegalArgumentException("invalid account range")
69
+ }
70
+ if (secret.size == 32) {
71
+ if (start != 0L || count != 1) throw walletError(WalletErrorCode.ACCOUNT_UNAVAILABLE, "this wallet has only account 0")
72
+ return listOf(address(secret, 0))
73
+ }
74
+ if (!isHdSecret(secret)) throw walletError(WalletErrorCode.INTERNAL, "unsupported wallet secret")
75
+ val parent = accountParent(secret)
76
+ return (0 until count).map { i ->
77
+ val node = Bip32ECKeyPair.deriveKeyPair(parent, intArrayOf((start + i).toInt()))
78
+ val key = Numeric.toBytesPadded(node.privateKey, 32)
79
+ try {
80
+ EvmAccount(key).address
81
+ } finally {
82
+ key.fill(0)
83
+ }
84
+ }
85
+ }
86
+
87
+ class SplitShares(val device: ByteArray, val company: ByteArray, val backup: ByteArray?) {
88
+ fun wipe() = Bytes.wipe(device, company, backup)
89
+ }
90
+
91
+ fun splitUnprotected(secret: ByteArray): SplitShares {
92
+ val s = Shamir.split(secret, 2, 2)
93
+ return SplitShares(s[0], s[1], null)
94
+ }
95
+
96
+ fun splitProtected(secret: ByteArray): SplitShares {
97
+ val s = Shamir.split(secret, 3, 2)
98
+ return SplitShares(s[0], s[1], s[2])
99
+ }
100
+
101
+ fun assemble(a: ByteArray, b: ByteArray): ByteArray = Shamir.combine(listOf(a, b))
102
+
103
+ /**
104
+ * One more share on the same 2-of-n polynomial as [a] and [b] (see shares.ts deriveShare):
105
+ * shifting both x coordinates by x3 and interpolating at zero yields f(x3).
106
+ */
107
+ fun deriveShare(a: ByteArray, b: ByteArray, pickX: () -> Int = ::randomX): ByteArray {
108
+ if (a.size != b.size || a.size < 2) throw IllegalArgumentException("share length mismatch")
109
+ val xa = a[a.size - 1].toInt() and 0xff
110
+ val xb = b[b.size - 1].toInt() and 0xff
111
+ var x3 = 0
112
+ while (x3 == 0 || x3 == xa || x3 == xb) x3 = pickX() and 0xff
113
+ val shiftedA = a.copyOf()
114
+ val shiftedB = b.copyOf()
115
+ shiftedA[a.size - 1] = (xa xor x3).toByte()
116
+ shiftedB[b.size - 1] = (xb xor x3).toByte()
117
+ try {
118
+ val y = Shamir.combine(listOf(shiftedA, shiftedB))
119
+ val share = ByteArray(a.size)
120
+ y.copyInto(share)
121
+ share[a.size - 1] = x3.toByte()
122
+ y.fill(0)
123
+ return share
124
+ } finally {
125
+ Bytes.wipe(shiftedA, shiftedB)
126
+ }
127
+ }
128
+
129
+ private fun randomX(): Int = Bytes.randomBytes(1)[0].toInt() and 0xff
130
+ }
@@ -0,0 +1,86 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Http.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import kotlinx.coroutines.suspendCancellableCoroutine
5
+ import kotlinx.serialization.json.Json
6
+ import kotlinx.serialization.json.JsonElement
7
+ import kotlinx.serialization.json.JsonObject
8
+ import okhttp3.Call
9
+ import okhttp3.Callback
10
+ import okhttp3.MediaType.Companion.toMediaType
11
+ import okhttp3.OkHttpClient
12
+ import okhttp3.Request
13
+ import okhttp3.RequestBody.Companion.toRequestBody
14
+ import okhttp3.Response
15
+ import java.io.IOException
16
+ import java.util.concurrent.TimeUnit
17
+ import kotlin.coroutines.resume
18
+ import kotlin.coroutines.resumeWithException
19
+
20
+ internal class HttpResult(val status: Int, val text: String, private val requestIdHeader: String? = null) {
21
+ val ok: Boolean get() = status in 200..299
22
+
23
+ /** The body as a JSON object; `{}` when it is empty or not an object (like the frame's `asRecord`). */
24
+ val json: JsonObject by lazy {
25
+ if (text.isBlank()) {
26
+ JsonObject(emptyMap())
27
+ } else {
28
+ runCatching { Json.parseToJsonElement(text) as? JsonObject }.getOrNull() ?: JsonObject(emptyMap())
29
+ }
30
+ }
31
+
32
+ /** Diagnostic id: the error body's `requestId`, else the `X-Request-Id` header (NATIVE.md section 10). */
33
+ val requestId: String?
34
+ get() = (json["requestId"] as? kotlinx.serialization.json.JsonPrimitive)?.takeIf { it.isString }?.content ?: requestIdHeader
35
+ }
36
+
37
+ /** Keep in sync with `version` in authon/build.gradle.kts. */
38
+ internal const val SDK_VERSION = "0.1.0"
39
+
40
+ /** HTTP helper shared by the wallet and the auth core; lives here so the wallet package stands alone. */
41
+ internal object Http {
42
+ private val JSON_MEDIA = "application/json".toMediaType()
43
+
44
+ val defaultClient: OkHttpClient by lazy {
45
+ OkHttpClient.Builder()
46
+ .connectTimeout(15, TimeUnit.SECONDS)
47
+ .readTimeout(30, TimeUnit.SECONDS)
48
+ .writeTimeout(30, TimeUnit.SECONDS)
49
+ .followRedirects(false)
50
+ .build()
51
+ }
52
+
53
+ fun userAgent(): String = "authon-android/$SDK_VERSION"
54
+
55
+ fun request(
56
+ url: String,
57
+ method: String,
58
+ headers: Map<String, String>,
59
+ body: JsonElement?,
60
+ ): Request {
61
+ val builder = Request.Builder().url(url)
62
+ for ((k, v) in headers) builder.header(k, v)
63
+ val payload = body?.toString()?.toRequestBody(JSON_MEDIA)
64
+ builder.method(method, payload ?: if (method == "POST" || method == "PUT" || method == "PATCH") ByteArray(0).toRequestBody(null) else null)
65
+ return builder.build()
66
+ }
67
+
68
+ /** Executes the call; throws IOException on network failure. Cancelling the coroutine cancels the call. */
69
+ suspend fun execute(client: OkHttpClient, request: Request): HttpResult =
70
+ suspendCancellableCoroutine { cont ->
71
+ val call = client.newCall(request)
72
+ cont.invokeOnCancellation { call.cancel() }
73
+ call.enqueue(
74
+ object : Callback {
75
+ override fun onFailure(call: Call, e: IOException) {
76
+ if (cont.isActive) cont.resumeWithException(e)
77
+ }
78
+
79
+ override fun onResponse(call: Call, response: Response) {
80
+ val result = response.use { HttpResult(it.code, it.body?.string() ?: "", it.header("X-Request-Id")) }
81
+ if (cont.isActive) cont.resume(result)
82
+ }
83
+ },
84
+ )
85
+ }
86
+ }
@@ -0,0 +1,83 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/JsValues.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import kotlinx.serialization.json.JsonArray
5
+ import kotlinx.serialization.json.JsonElement
6
+ import kotlinx.serialization.json.JsonNull
7
+ import kotlinx.serialization.json.JsonObject
8
+ import kotlinx.serialization.json.JsonPrimitive
9
+ import kotlinx.serialization.json.booleanOrNull
10
+ import java.math.BigDecimal
11
+ import java.math.BigInteger
12
+
13
+ /** Conversions that follow JavaScript semantics where the frame relies on them. */
14
+ internal object JsValues {
15
+ /** `BigInt(value)`: integers, decimal strings, 0x/0o/0b strings; "" is 0. Null when JS would throw. */
16
+ fun toBigIntOrNull(value: Any?): BigInteger? = when (value) {
17
+ null -> null
18
+ is BigInteger -> value
19
+ is Int -> BigInteger.valueOf(value.toLong())
20
+ is Long -> BigInteger.valueOf(value)
21
+ is Short -> BigInteger.valueOf(value.toLong())
22
+ is Byte -> BigInteger.valueOf(value.toLong())
23
+ is Double, is Float -> numberToBigInt(BigDecimal(value.toString()))
24
+ is BigDecimal -> numberToBigInt(value)
25
+ is Boolean -> if (value) BigInteger.ONE else BigInteger.ZERO
26
+ is String -> stringToBigInt(value)
27
+ is JsonNull -> null
28
+ is JsonPrimitive ->
29
+ if (value.isString) stringToBigInt(value.content)
30
+ else value.booleanOrNull?.let { if (it) BigInteger.ONE else BigInteger.ZERO }
31
+ ?: runCatching { numberToBigInt(BigDecimal(value.content)) }.getOrNull()
32
+ else -> null
33
+ }
34
+
35
+ private fun numberToBigInt(d: BigDecimal): BigInteger? = try {
36
+ d.toBigIntegerExact()
37
+ } catch (_: ArithmeticException) {
38
+ null
39
+ }
40
+
41
+ private fun stringToBigInt(raw: String): BigInteger? {
42
+ val s = raw.trim()
43
+ if (s.isEmpty()) return BigInteger.ZERO
44
+ val radix = when {
45
+ s.startsWith("0x") || s.startsWith("0X") -> 16
46
+ s.startsWith("0o") || s.startsWith("0O") -> 8
47
+ s.startsWith("0b") || s.startsWith("0B") -> 2
48
+ else -> 10
49
+ }
50
+ return try {
51
+ if (radix == 10) {
52
+ if (!Regex("^[+-]?[0-9]+$").matches(s)) null else BigInteger(s)
53
+ } else {
54
+ val body = s.substring(2)
55
+ if (body.isEmpty() || body.any { Character.digit(it, radix) < 0 }) null else BigInteger(body, radix)
56
+ }
57
+ } catch (_: NumberFormatException) {
58
+ null
59
+ }
60
+ }
61
+
62
+ /** Converts Kotlin maps/lists/primitives (e.g. from a React Native bridge) to JSON. */
63
+ fun toJson(value: Any?): JsonElement = when (value) {
64
+ null -> JsonNull
65
+ is JsonElement -> value
66
+ is String -> JsonPrimitive(value)
67
+ is Boolean -> JsonPrimitive(value)
68
+ is BigInteger -> JsonPrimitive(value.toString())
69
+ is Number -> {
70
+ val d = value.toDouble()
71
+ if ((value is Double || value is Float) && d == Math.floor(d) && !d.isInfinite() && Math.abs(d) < 1e21) {
72
+ JsonPrimitive(BigDecimal(d).toBigInteger())
73
+ } else {
74
+ JsonPrimitive(value)
75
+ }
76
+ }
77
+ is Map<*, *> -> JsonObject(value.entries.associate { (k, v) -> k.toString() to toJson(v) })
78
+ is Iterable<*> -> JsonArray(value.map { toJson(it) })
79
+ is Array<*> -> JsonArray(value.map { toJson(it) })
80
+ is ByteArray -> JsonPrimitive("0x" + Bytes.toHex(value))
81
+ else -> throw walletError(WalletErrorCode.INVALID_REQUEST, "unsupported value type ${value::class.java.simpleName}")
82
+ }
83
+ }
@@ -0,0 +1,236 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+ import android.security.keystore.KeyGenParameterSpec
8
+ import android.security.keystore.KeyInfo
9
+ import android.security.keystore.KeyProperties
10
+ import android.security.keystore.StrongBoxUnavailableException
11
+ import android.util.AtomicFile
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.withContext
14
+ import kotlinx.serialization.json.Json
15
+ import kotlinx.serialization.json.JsonObject
16
+ import kotlinx.serialization.json.JsonPrimitive
17
+ import kotlinx.serialization.json.buildJsonObject
18
+ import kotlinx.serialization.json.contentOrNull
19
+ import kotlinx.serialization.json.intOrNull
20
+ import kotlinx.serialization.json.longOrNull
21
+ import kotlinx.serialization.json.put
22
+ import java.io.File
23
+ import java.io.IOException
24
+ import java.security.Key
25
+ import java.security.KeyFactory
26
+ import java.security.KeyPairGenerator
27
+ import java.security.KeyStore
28
+ import java.security.PrivateKey
29
+ import java.security.Signature
30
+ import java.security.interfaces.ECPublicKey
31
+ import java.security.spec.ECGenParameterSpec
32
+ import java.util.UUID
33
+ import javax.crypto.KeyGenerator
34
+ import javax.crypto.SecretKey
35
+ import javax.crypto.SecretKeyFactory
36
+
37
+ /** Where a Keystore key lives, as reported by KeyInfo. */
38
+ enum class KeySecurityLevel { STRONGBOX, TRUSTED_ENVIRONMENT, UNKNOWN_SECURE, SOFTWARE, UNKNOWN }
39
+
40
+ /**
41
+ * Device share storage in the Android Keystore (NATIVE.md section 3).
42
+ * - Signing key: P-256, non-exportable, StrongBox when available, hardware TEE accepted.
43
+ * Software-only keys are refused unless [allowSoftwareKeys] (tests and emulators only).
44
+ * - D is sealed with a Keystore AES-256-GCM key, AAD = storage key `projectId\0userId`, and written
45
+ * with AtomicFile to `noBackupFilesDir`, so it is not part of cloud backups or device transfers.
46
+ * Every write is read back and compared.
47
+ */
48
+ class KeystoreDeviceStore @JvmOverloads constructor(
49
+ context: Context,
50
+ private val allowSoftwareKeys: Boolean = false,
51
+ ) : DeviceStore {
52
+ private val appContext = context.applicationContext
53
+ private val dir = File(appContext.noBackupFilesDir, "authon-wallet")
54
+ private val hasStrongBox = Build.VERSION.SDK_INT >= 28 &&
55
+ appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
56
+
57
+ private val keyStore: KeyStore by lazy { KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } }
58
+
59
+ private inner class KeystoreKeys(override val handle: String, override val publicJwk: DevicePublicJwk) : DeviceKeys {
60
+ private fun signKey(): PrivateKey = keyStore.getKey("$handle.sign", null) as? PrivateKey
61
+ ?: throw walletError(WalletErrorCode.NEEDS_RESTORE, "device key missing")
62
+
63
+ private fun aesKey(): SecretKey = keyStore.getKey("$handle.share", null) as? SecretKey
64
+ ?: throw walletError(WalletErrorCode.NEEDS_RESTORE, "device share key missing")
65
+
66
+ override fun sign(data: ByteArray): ByteArray {
67
+ val s = Signature.getInstance("SHA256withECDSA")
68
+ s.initSign(signKey())
69
+ s.update(data)
70
+ return DeviceStorage.derToRaw(s.sign())
71
+ }
72
+
73
+ override fun seal(plain: ByteArray, aad: ByteArray): SealedShare = DeviceStorage.gcmSeal(aesKey(), plain, aad)
74
+
75
+ override fun open(sealed: SealedShare, aad: ByteArray): ByteArray = DeviceStorage.gcmOpen(aesKey(), sealed, aad)
76
+ }
77
+
78
+ /** Security level of the signing key behind [keys] (diagnostics and tests). */
79
+ fun securityLevelOf(keys: DeviceKeys): KeySecurityLevel = securityLevel(keyStore.getKey("${keys.handle}.sign", null))
80
+
81
+ override suspend fun generateKeys(): DeviceKeys = withContext(Dispatchers.IO) {
82
+ val handle = "dev.authon.wallet.${UUID.randomUUID()}"
83
+ try {
84
+ val pair = withStrongBoxFallback { strongBox ->
85
+ val spec = KeyGenParameterSpec.Builder("$handle.sign", KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY)
86
+ .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
87
+ .setDigests(KeyProperties.DIGEST_SHA256)
88
+ .apply { if (strongBox) setIsStrongBoxBacked(true) }
89
+ .build()
90
+ KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE).apply { initialize(spec) }.generateKeyPair()
91
+ }
92
+ val aes = withStrongBoxFallback { strongBox ->
93
+ val spec = KeyGenParameterSpec.Builder("$handle.share", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
94
+ .setKeySize(256)
95
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
96
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
97
+ .apply { if (strongBox) setIsStrongBoxBacked(true) }
98
+ .build()
99
+ KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE).apply { init(spec) }.generateKey()
100
+ }
101
+ if (!allowSoftwareKeys) {
102
+ val levels = listOf(securityLevel(pair.private), securityLevel(aes))
103
+ if (levels.any { it == KeySecurityLevel.SOFTWARE || it == KeySecurityLevel.UNKNOWN }) {
104
+ deleteAliases(handle)
105
+ throw walletError(WalletErrorCode.STORAGE_UNAVAILABLE, "this device has no hardware-backed keystore")
106
+ }
107
+ }
108
+ KeystoreKeys(handle, DeviceStorage.jwkOf(pair.public as ECPublicKey))
109
+ } catch (e: WalletException) {
110
+ throw e
111
+ } catch (e: Exception) {
112
+ deleteAliases(handle)
113
+ throw WalletException(WalletErrorCode.STORAGE_UNAVAILABLE, "device keys could not be created", e)
114
+ }
115
+ }
116
+
117
+ private fun <T> withStrongBoxFallback(block: (Boolean) -> T): T {
118
+ if (hasStrongBox) {
119
+ try {
120
+ return block(true)
121
+ } catch (_: StrongBoxUnavailableException) {
122
+ // fall through to the TEE
123
+ }
124
+ }
125
+ return block(false)
126
+ }
127
+
128
+ private fun securityLevel(key: Key?): KeySecurityLevel = KeystoreSecurity.levelOf(key)
129
+
130
+ private fun deleteAliases(handle: String) {
131
+ for (alias in listOf("$handle.sign", "$handle.share")) runCatching { keyStore.deleteEntry(alias) }
132
+ }
133
+
134
+ private fun fileFor(key: String) = AtomicFile(File(dir, Bytes.toHex(Bytes.sha256(Bytes.utf8(key))) + ".json"))
135
+
136
+ override suspend fun discardKeys(keys: DeviceKeys) = withContext(Dispatchers.IO) { deleteAliases(keys.handle) }
137
+
138
+ override suspend fun get(key: String): DeviceRecord? = withContext(Dispatchers.IO) { read(key) }
139
+
140
+ private fun read(key: String): DeviceRecord? {
141
+ val file = fileFor(key)
142
+ if (!file.baseFile.exists()) return null
143
+ val o = try {
144
+ Json.parseToJsonElement(String(file.readFully(), Charsets.UTF_8)) as? JsonObject
145
+ } catch (e: Exception) {
146
+ throw WalletException(WalletErrorCode.STORAGE_UNAVAILABLE, "device record unreadable", e)
147
+ } ?: return null
148
+ fun s(k: String) = (o[k] as? JsonPrimitive)?.contentOrNull
149
+ if (s("key") != key) return null
150
+ val handle = s("handle") ?: return null
151
+ val jwk = o["publicJwk"] as? JsonObject ?: return null
152
+ return DeviceRecord(
153
+ key = key,
154
+ keys = KeystoreKeys(
155
+ handle,
156
+ DevicePublicJwk((jwk["x"] as JsonPrimitive).content, (jwk["y"] as JsonPrimitive).content),
157
+ ),
158
+ iv = Bytes.fromBase64(s("iv") ?: return null),
159
+ ciphertext = Bytes.fromBase64(s("ciphertext") ?: return null),
160
+ deviceId = s("deviceId") ?: return null,
161
+ walletId = s("walletId") ?: return null,
162
+ address = s("address") ?: return null,
163
+ shareVersion = (o["shareVersion"] as? JsonPrimitive)?.intOrNull ?: return null,
164
+ createdAt = (o["createdAt"] as? JsonPrimitive)?.longOrNull ?: 0L,
165
+ )
166
+ }
167
+
168
+ override suspend fun put(record: DeviceRecord) = withContext(Dispatchers.IO) {
169
+ val previous = runCatching { read(record.key) }.getOrNull()
170
+ val bytes = buildJsonObject {
171
+ put("v", 1)
172
+ put("key", record.key)
173
+ put("handle", record.keys.handle)
174
+ put("publicJwk", record.keys.publicJwk.toJson())
175
+ put("iv", Bytes.toBase64(record.iv))
176
+ put("ciphertext", Bytes.toBase64(record.ciphertext))
177
+ put("deviceId", record.deviceId)
178
+ put("walletId", record.walletId)
179
+ put("address", record.address)
180
+ put("shareVersion", record.shareVersion)
181
+ put("createdAt", record.createdAt)
182
+ }.toString().toByteArray(Charsets.UTF_8)
183
+ val file = fileFor(record.key)
184
+ try {
185
+ if (!dir.exists() && !dir.mkdirs()) throw IOException("cannot create ${dir.name}")
186
+ val out = file.startWrite()
187
+ try {
188
+ out.write(bytes)
189
+ file.finishWrite(out)
190
+ } catch (e: IOException) {
191
+ file.failWrite(out)
192
+ throw e
193
+ }
194
+ if (!file.readFully().contentEquals(bytes)) throw IOException("read-back differs")
195
+ } catch (e: IOException) {
196
+ throw WalletException(WalletErrorCode.STORAGE_UNAVAILABLE, "device share could not be stored", e)
197
+ }
198
+ if (previous != null && previous.keys.handle != record.keys.handle) deleteAliases(previous.keys.handle)
199
+ }
200
+
201
+ override suspend fun delete(key: String) = withContext(Dispatchers.IO) {
202
+ val previous = runCatching { read(key) }.getOrNull()
203
+ fileFor(key).delete()
204
+ if (previous != null) deleteAliases(previous.keys.handle)
205
+ }
206
+
207
+ private companion object {
208
+ const val ANDROID_KEYSTORE = "AndroidKeyStore"
209
+ }
210
+ }
211
+
212
+ internal object KeystoreSecurity {
213
+ fun levelOf(key: Key?): KeySecurityLevel {
214
+ if (key == null) return KeySecurityLevel.UNKNOWN
215
+ val info: KeyInfo = try {
216
+ when (key) {
217
+ is PrivateKey -> KeyFactory.getInstance(key.algorithm, "AndroidKeyStore").getKeySpec(key, KeyInfo::class.java)
218
+ is SecretKey -> SecretKeyFactory.getInstance(key.algorithm, "AndroidKeyStore").getKeySpec(key, KeyInfo::class.java) as KeyInfo
219
+ else -> return KeySecurityLevel.UNKNOWN
220
+ }
221
+ } catch (_: Exception) {
222
+ return KeySecurityLevel.UNKNOWN
223
+ }
224
+ if (Build.VERSION.SDK_INT >= 31) {
225
+ return when (info.securityLevel) {
226
+ KeyProperties.SECURITY_LEVEL_STRONGBOX -> KeySecurityLevel.STRONGBOX
227
+ KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> KeySecurityLevel.TRUSTED_ENVIRONMENT
228
+ KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE -> KeySecurityLevel.UNKNOWN_SECURE
229
+ KeyProperties.SECURITY_LEVEL_SOFTWARE -> KeySecurityLevel.SOFTWARE
230
+ else -> KeySecurityLevel.UNKNOWN
231
+ }
232
+ }
233
+ @Suppress("DEPRECATION")
234
+ return if (info.isInsideSecureHardware) KeySecurityLevel.UNKNOWN_SECURE else KeySecurityLevel.SOFTWARE
235
+ }
236
+ }
@@ -0,0 +1,111 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Prf.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import javax.crypto.Cipher
5
+ import javax.crypto.Mac
6
+ import javax.crypto.spec.GCMParameterSpec
7
+ import javax.crypto.spec.SecretKeySpec
8
+
9
+ /** A WebAuthn assertion, base64url fields, as the API verifies it. */
10
+ data class PasskeyAssertion(
11
+ val credentialId: String,
12
+ val clientDataJSON: String,
13
+ val authenticatorData: String,
14
+ val signature: String,
15
+ )
16
+
17
+ class PasskeyCreateResult(
18
+ val credentialId: String,
19
+ /** 32-byte PRF output. The SDK wipes it after use. */
20
+ val prfOutput: ByteArray,
21
+ /** SPKI public key (base64url) when the provider reports it. */
22
+ val publicKey: String?,
23
+ /** COSE algorithm of [publicKey] (-7 ES256, -257 RS256, -8 EdDSA). */
24
+ val publicKeyAlg: Int?,
25
+ /** The raw attestationObject (base64url); the API reads the COSE key from it when [publicKey] is null. */
26
+ val attestationObject: String?,
27
+ )
28
+
29
+ class PasskeyGetResult(val prfOutput: ByteArray, val assertion: PasskeyAssertion)
30
+
31
+ /** Passkey operations with the PRF extension for RP `wallet.authon.dev`. */
32
+ interface PasskeyProvider {
33
+ suspend fun create(userId: ByteArray, userName: String, salt: ByteArray): PasskeyCreateResult
34
+
35
+ /** [challenge] is the server's restore challenge when the API verifies the assertion. */
36
+ suspend fun get(credentialId: String, salt: ByteArray, challenge: ByteArray?): PasskeyGetResult
37
+ }
38
+
39
+ /** Backup encryption of `apps/wallet/src/prf.ts`: PRF salt v1/v2, HKDF-SHA256, AES-256-GCM, AAD, `v2.` prefix. */
40
+ internal object Prf {
41
+ const val PRF_SALT_LABEL = "authon-wallet-backup-v1"
42
+ const val PRF_SALT_LABEL_V2 = "authon-wallet-backup-v2"
43
+ const val DEFAULT_WRITE_BACKUP_VERSION = 1
44
+ private const val HKDF_INFO = "authon-wallet-backup-v1/aes-gcm-256"
45
+ private const val V2_PREFIX = "v2."
46
+
47
+ fun passkeyUserId(userHandle: String): ByteArray = Bytes.sha256(Bytes.utf8(userHandle))
48
+
49
+ fun salt(version: Int, projectId: String?, walletId: String?): ByteArray =
50
+ if (version == 1) {
51
+ Bytes.sha256(Bytes.utf8(PRF_SALT_LABEL))
52
+ } else {
53
+ if (projectId == null || walletId == null) throw walletError(WalletErrorCode.INTERNAL, "backup context required")
54
+ Bytes.sha256(Bytes.utf8("$PRF_SALT_LABEL_V2\u0000$projectId\u0000$walletId"))
55
+ }
56
+
57
+ fun backupVersionOf(payload: String): Int = if (payload.startsWith(V2_PREFIX)) 2 else 1
58
+
59
+ /** RFC 5869 HKDF-SHA256 with an empty salt, one output block (the AES-256 key). */
60
+ fun deriveBackupKey(prfOutput: ByteArray): ByteArray = hkdfSha256(prfOutput, ByteArray(0), Bytes.utf8(HKDF_INFO), 32)
61
+
62
+ fun hkdfSha256(ikm: ByteArray, salt: ByteArray, info: ByteArray, length: Int): ByteArray {
63
+ require(length in 1..255 * 32)
64
+ val extract = Mac.getInstance("HmacSHA256")
65
+ // An empty salt means HashLen zero bytes (RFC 5869 2.2); JCA rejects empty HMAC keys.
66
+ extract.init(SecretKeySpec(if (salt.isEmpty()) ByteArray(32) else salt, "HmacSHA256"))
67
+ val prk = extract.doFinal(ikm)
68
+ val expand = Mac.getInstance("HmacSHA256")
69
+ expand.init(SecretKeySpec(prk, "HmacSHA256"))
70
+ val out = ByteArray(length)
71
+ var t = ByteArray(0)
72
+ var offset = 0
73
+ var counter = 1
74
+ while (offset < length) {
75
+ expand.update(t)
76
+ expand.update(info)
77
+ expand.update(counter.toByte())
78
+ t = expand.doFinal()
79
+ val n = minOf(t.size, length - offset)
80
+ t.copyInto(out, offset, 0, n)
81
+ offset += n
82
+ counter++
83
+ }
84
+ Bytes.wipe(prk, t)
85
+ return out
86
+ }
87
+
88
+ private fun aad(walletId: String, version: Int) = Bytes.utf8(if (version == 1) "backup:$walletId" else "backup:v2:$walletId")
89
+
90
+ fun encryptBackup(key: ByteArray, share: ByteArray, walletId: String, version: Int, iv: ByteArray = Bytes.randomBytes(12)): String {
91
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
92
+ cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, iv))
93
+ cipher.updateAAD(aad(walletId, version))
94
+ val body = Bytes.toBase64(Bytes.concat(iv, cipher.doFinal(share)))
95
+ return if (version == 1) body else V2_PREFIX + body
96
+ }
97
+
98
+ fun decryptBackup(key: ByteArray, payload: String, walletId: String): ByteArray {
99
+ val version = backupVersionOf(payload)
100
+ val raw = Bytes.fromBase64(if (version == 1) payload else payload.substring(V2_PREFIX.length))
101
+ if (raw.size < 12 + 16) throw walletError(WalletErrorCode.INVALID_REQUEST, "backup ciphertext malformed")
102
+ return try {
103
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
104
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, raw, 0, 12))
105
+ cipher.updateAAD(aad(walletId, version))
106
+ cipher.doFinal(raw, 12, raw.size - 12)
107
+ } catch (e: Exception) {
108
+ throw WalletException(WalletErrorCode.INVALID_REQUEST, "backup cannot be decrypted with this passkey", e)
109
+ }
110
+ }
111
+ }