@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,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
|
+
}
|