@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,157 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Chains.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import org.bouncycastle.crypto.digests.RIPEMD160Digest
|
|
5
|
+
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
|
|
6
|
+
import org.web3j.crypto.Bip32ECKeyPair
|
|
7
|
+
import org.web3j.crypto.Hash
|
|
8
|
+
import org.web3j.crypto.Keys
|
|
9
|
+
import org.web3j.crypto.Sign
|
|
10
|
+
import javax.crypto.Mac
|
|
11
|
+
import javax.crypto.spec.SecretKeySpec
|
|
12
|
+
|
|
13
|
+
/** Chains a wallet derives addresses for (NATIVE.md section 11). [wire] is the contract string. */
|
|
14
|
+
enum class WalletChain(val wire: String) {
|
|
15
|
+
EVM("evm"),
|
|
16
|
+
TRON("tron"),
|
|
17
|
+
XRP("xrp"),
|
|
18
|
+
BITCOIN("bitcoin"),
|
|
19
|
+
BITCOIN_TESTNET("bitcoin-testnet"),
|
|
20
|
+
SOLANA("solana");
|
|
21
|
+
|
|
22
|
+
companion object {
|
|
23
|
+
fun fromWire(value: String): WalletChain = entries.firstOrNull { it.wire == value }
|
|
24
|
+
?: throw walletError(WalletErrorCode.INVALID_REQUEST, "chain must be one of ${entries.joinToString(", ") { it.wire }}")
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Per-chain account addresses from the BIP39 seed, with the paths of standard wallets
|
|
30
|
+
* (`apps/wallet/src/chains.ts`). Curve math comes from web3j / Bouncy Castle; only the address
|
|
31
|
+
* encodings and SLIP-0010 chaining are assembled here. Address derivation only, no signing.
|
|
32
|
+
*/
|
|
33
|
+
internal object Chains {
|
|
34
|
+
private const val H = Bip32ECKeyPair.HARDENED_BIT
|
|
35
|
+
|
|
36
|
+
fun path(chain: WalletChain, index: Long): String = when (chain) {
|
|
37
|
+
WalletChain.EVM -> "m/44'/60'/0'/0/$index"
|
|
38
|
+
WalletChain.TRON -> "m/44'/195'/$index'/0/0"
|
|
39
|
+
WalletChain.XRP -> "m/44'/144'/$index'/0/0"
|
|
40
|
+
WalletChain.BITCOIN -> "m/84'/0'/$index'/0/0"
|
|
41
|
+
WalletChain.BITCOIN_TESTNET -> "m/84'/1'/$index'/0/0"
|
|
42
|
+
WalletChain.SOLANA -> "m/44'/501'/$index'/0'"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private fun secpPath(chain: WalletChain, index: Int): IntArray = when (chain) {
|
|
46
|
+
WalletChain.EVM -> intArrayOf(44 or H, 60 or H, 0 or H, 0, index)
|
|
47
|
+
WalletChain.TRON -> intArrayOf(44 or H, 195 or H, index or H, 0, 0)
|
|
48
|
+
WalletChain.XRP -> intArrayOf(44 or H, 144 or H, index or H, 0, 0)
|
|
49
|
+
WalletChain.BITCOIN -> intArrayOf(84 or H, 0 or H, index or H, 0, 0)
|
|
50
|
+
WalletChain.BITCOIN_TESTNET -> intArrayOf(84 or H, 1 or H, index or H, 0, 0)
|
|
51
|
+
WalletChain.SOLANA -> throw IllegalArgumentException("solana is not secp256k1")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun hash160(data: ByteArray): ByteArray {
|
|
55
|
+
val sha = Bytes.sha256(data)
|
|
56
|
+
val d = RIPEMD160Digest()
|
|
57
|
+
d.update(sha, 0, sha.size)
|
|
58
|
+
return ByteArray(20).also { d.doFinal(it, 0) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun secpAddress(chain: WalletChain, master: Bip32ECKeyPair, index: Int): String =
|
|
62
|
+
addressOfPrivateKey(chain, Bip32ECKeyPair.deriveKeyPair(master, secpPath(chain, index)).privateKey)
|
|
63
|
+
|
|
64
|
+
/** The secp256k1 private key of account [index] on [chain]. Callers wipe it. */
|
|
65
|
+
fun secpPrivateKey(secret: ByteArray, chain: WalletChain, index: Long): ByteArray {
|
|
66
|
+
if (!HdWallet.isAccountIndex(index)) throw walletError(WalletErrorCode.INVALID_REQUEST, "invalid account index")
|
|
67
|
+
if (chain == WalletChain.EVM) return HdWallet.privateKey(secret, index)
|
|
68
|
+
if (!HdWallet.isHdSecret(secret)) throw walletError(WalletErrorCode.ACCOUNT_UNAVAILABLE, "this wallet has only EVM account 0")
|
|
69
|
+
val seed = HdWallet.seedOf(secret)
|
|
70
|
+
try {
|
|
71
|
+
val node = Bip32ECKeyPair.deriveKeyPair(Bip32ECKeyPair.generateKeyPair(seed), secpPath(chain, index.toInt()))
|
|
72
|
+
return org.web3j.utils.Numeric.toBytesPadded(node.privateKey, 32)
|
|
73
|
+
} finally {
|
|
74
|
+
Bytes.wipe(seed)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Compressed secp256k1 public key of account [index] on [chain]. */
|
|
79
|
+
fun secpPublicKey(secret: ByteArray, chain: WalletChain, index: Long): ByteArray {
|
|
80
|
+
val key = secpPrivateKey(secret, chain, index)
|
|
81
|
+
try {
|
|
82
|
+
return compressedPublicKey(key)
|
|
83
|
+
} finally {
|
|
84
|
+
Bytes.wipe(key)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fun compressedPublicKey(privateKey: ByteArray): ByteArray =
|
|
89
|
+
Sign.publicPointFromPrivate(java.math.BigInteger(1, privateKey)).getEncoded(true)
|
|
90
|
+
|
|
91
|
+
fun hash160Of(data: ByteArray): ByteArray = hash160(data)
|
|
92
|
+
|
|
93
|
+
fun addressOfKey(chain: WalletChain, privateKey: ByteArray): String =
|
|
94
|
+
addressOfPrivateKey(chain, java.math.BigInteger(1, privateKey))
|
|
95
|
+
|
|
96
|
+
private fun addressOfPrivateKey(chain: WalletChain, privateKey: java.math.BigInteger): String {
|
|
97
|
+
val point = Sign.publicPointFromPrivate(privateKey)
|
|
98
|
+
val compressed = point.getEncoded(true)
|
|
99
|
+
val uncompressed = point.getEncoded(false)
|
|
100
|
+
return when (chain) {
|
|
101
|
+
WalletChain.EVM -> Keys.toChecksumAddress(Bytes.toHex(Hash.sha3(uncompressed.copyOfRange(1, 65)).copyOfRange(12, 32)))
|
|
102
|
+
WalletChain.TRON -> Base58.encodeCheck(byteArrayOf(0x41) + Hash.sha3(uncompressed.copyOfRange(1, 65)).copyOfRange(12, 32))
|
|
103
|
+
WalletChain.XRP -> Base58.encodeCheck(byteArrayOf(0x00) + hash160(compressed), Base58.XRP)
|
|
104
|
+
WalletChain.BITCOIN -> Bech32.segwitV0("bc", hash160(compressed))
|
|
105
|
+
WalletChain.BITCOIN_TESTNET -> Bech32.segwitV0("tb", hash160(compressed))
|
|
106
|
+
WalletChain.SOLANA -> throw IllegalArgumentException("solana is not secp256k1")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun hmacSha512(key: ByteArray, data: ByteArray): ByteArray =
|
|
111
|
+
Mac.getInstance("HmacSHA512").run { init(SecretKeySpec(key, "HmacSHA512")); doFinal(data) }
|
|
112
|
+
|
|
113
|
+
/** SLIP-0010 ed25519 derivation; every level is hardened. Callers wipe the 32-byte key. */
|
|
114
|
+
fun ed25519Key(seed: ByteArray, indices: IntArray): ByteArray {
|
|
115
|
+
var i = hmacSha512(Bytes.utf8("ed25519 seed"), seed)
|
|
116
|
+
var key = i.copyOfRange(0, 32)
|
|
117
|
+
var code = i.copyOfRange(32, 64)
|
|
118
|
+
for (index in indices) {
|
|
119
|
+
val hardened = index or H
|
|
120
|
+
val ser = byteArrayOf((hardened ushr 24).toByte(), (hardened ushr 16).toByte(), (hardened ushr 8).toByte(), hardened.toByte())
|
|
121
|
+
val data = byteArrayOf(0) + key + ser
|
|
122
|
+
Bytes.wipe(i)
|
|
123
|
+
i = hmacSha512(code, data)
|
|
124
|
+
Bytes.wipe(data, key, code)
|
|
125
|
+
key = i.copyOfRange(0, 32)
|
|
126
|
+
code = i.copyOfRange(32, 64)
|
|
127
|
+
}
|
|
128
|
+
Bytes.wipe(i, code)
|
|
129
|
+
return key
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private fun solanaAddress(seed: ByteArray, index: Int): String {
|
|
133
|
+
val key = ed25519Key(seed, intArrayOf(44, 501, index, 0))
|
|
134
|
+
try {
|
|
135
|
+
return Base58.encode(Ed25519PrivateKeyParameters(key, 0).generatePublicKey().encoded)
|
|
136
|
+
} finally {
|
|
137
|
+
Bytes.wipe(key)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Addresses of accounts start..start+count-1 on [chain]. Only HD wallets have chains other than EVM account 0. */
|
|
142
|
+
fun addresses(secret: ByteArray, chain: WalletChain, start: Long, count: Int): List<String> {
|
|
143
|
+
if (!HdWallet.isAccountIndex(start) || count < 1 || !HdWallet.isAccountIndex(start + count - 1)) {
|
|
144
|
+
throw walletError(WalletErrorCode.INVALID_REQUEST, "invalid account range")
|
|
145
|
+
}
|
|
146
|
+
if (chain == WalletChain.EVM) return HdWallet.addresses(secret, start, count)
|
|
147
|
+
if (!HdWallet.isHdSecret(secret)) throw walletError(WalletErrorCode.ACCOUNT_UNAVAILABLE, "this wallet has only EVM account 0")
|
|
148
|
+
val seed = HdWallet.seedOf(secret)
|
|
149
|
+
try {
|
|
150
|
+
if (chain == WalletChain.SOLANA) return (0 until count).map { solanaAddress(seed, (start + it).toInt()) }
|
|
151
|
+
val master = Bip32ECKeyPair.generateKeyPair(seed)
|
|
152
|
+
return (0 until count).map { secpAddress(chain, master, (start + it).toInt()) }
|
|
153
|
+
} finally {
|
|
154
|
+
Bytes.wipe(seed)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import androidx.credentials.CreatePublicKeyCredentialRequest
|
|
6
|
+
import androidx.credentials.CreatePublicKeyCredentialResponse
|
|
7
|
+
import androidx.credentials.CredentialManager
|
|
8
|
+
import androidx.credentials.GetCredentialRequest
|
|
9
|
+
import androidx.credentials.GetPublicKeyCredentialOption
|
|
10
|
+
import androidx.credentials.PublicKeyCredential
|
|
11
|
+
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
|
12
|
+
import androidx.credentials.exceptions.CreateCredentialException
|
|
13
|
+
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
|
|
14
|
+
import androidx.credentials.exceptions.CreateCredentialUnsupportedException
|
|
15
|
+
import androidx.credentials.exceptions.GetCredentialCancellationException
|
|
16
|
+
import androidx.credentials.exceptions.GetCredentialException
|
|
17
|
+
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
|
|
18
|
+
import androidx.credentials.exceptions.GetCredentialUnsupportedException
|
|
19
|
+
import androidx.credentials.exceptions.NoCredentialException
|
|
20
|
+
import androidx.credentials.exceptions.domerrors.DomError
|
|
21
|
+
import androidx.credentials.exceptions.domerrors.NotAllowedError
|
|
22
|
+
import androidx.credentials.exceptions.domerrors.NotSupportedError
|
|
23
|
+
import androidx.credentials.exceptions.domerrors.SecurityError
|
|
24
|
+
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
|
|
25
|
+
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
|
|
26
|
+
import kotlinx.coroutines.Dispatchers
|
|
27
|
+
import kotlinx.coroutines.withContext
|
|
28
|
+
import kotlinx.serialization.json.Json
|
|
29
|
+
import kotlinx.serialization.json.JsonObject
|
|
30
|
+
import kotlinx.serialization.json.JsonPrimitive
|
|
31
|
+
import kotlinx.serialization.json.addJsonObject
|
|
32
|
+
import kotlinx.serialization.json.booleanOrNull
|
|
33
|
+
import kotlinx.serialization.json.buildJsonObject
|
|
34
|
+
import kotlinx.serialization.json.contentOrNull
|
|
35
|
+
import kotlinx.serialization.json.intOrNull
|
|
36
|
+
import kotlinx.serialization.json.put
|
|
37
|
+
import kotlinx.serialization.json.putJsonArray
|
|
38
|
+
import kotlinx.serialization.json.putJsonObject
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Backup passkeys through Credential Manager with WebAuthn JSON for RP `wallet.authon.dev`
|
|
42
|
+
* (the same RP as the web frame, so synced passkeys restore on either side). PRF is requested with
|
|
43
|
+
* `extensions.prf.eval.first` and read from `clientExtensionResults.prf.results.first`.
|
|
44
|
+
*
|
|
45
|
+
* The app must be listed in the RP's Digital Asset Links (`get_login_creds`), which the API
|
|
46
|
+
* publishes only for approved app registrations.
|
|
47
|
+
*/
|
|
48
|
+
class CredentialManagerPasskeys(
|
|
49
|
+
private val activityProvider: () -> Activity?,
|
|
50
|
+
private val rpId: String = WalletApi.DEFAULT_RP_ID,
|
|
51
|
+
) : PasskeyProvider {
|
|
52
|
+
|
|
53
|
+
private fun activity(): Activity = activityProvider()?.takeUnless { it.isFinishing || it.isDestroyed }
|
|
54
|
+
?: throw walletError(WalletErrorCode.INTERNAL, "no foreground activity for the passkey prompt")
|
|
55
|
+
|
|
56
|
+
override suspend fun create(userId: ByteArray, userName: String, salt: ByteArray): PasskeyCreateResult {
|
|
57
|
+
val request = buildJsonObject {
|
|
58
|
+
put("challenge", Bytes.toBase64Url(Bytes.randomBytes(32)))
|
|
59
|
+
putJsonObject("rp") {
|
|
60
|
+
put("id", rpId)
|
|
61
|
+
put("name", "Authon Wallet")
|
|
62
|
+
}
|
|
63
|
+
putJsonObject("user") {
|
|
64
|
+
put("id", Bytes.toBase64Url(userId))
|
|
65
|
+
put("name", userName)
|
|
66
|
+
put("displayName", userName)
|
|
67
|
+
}
|
|
68
|
+
putJsonArray("pubKeyCredParams") {
|
|
69
|
+
for (alg in listOf(-7, -257, -8)) addJsonObject {
|
|
70
|
+
put("type", "public-key")
|
|
71
|
+
put("alg", alg)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
put("attestation", "none")
|
|
75
|
+
putJsonObject("authenticatorSelection") {
|
|
76
|
+
put("residentKey", "preferred")
|
|
77
|
+
put("userVerification", "required")
|
|
78
|
+
}
|
|
79
|
+
prfExtension(salt)
|
|
80
|
+
}
|
|
81
|
+
val activity = activity()
|
|
82
|
+
val response = try {
|
|
83
|
+
withContext(Dispatchers.Main) {
|
|
84
|
+
CredentialManager.create(activity).createCredential(activity, CreatePublicKeyCredentialRequest(request.toString()))
|
|
85
|
+
}
|
|
86
|
+
} catch (e: CreateCredentialException) {
|
|
87
|
+
throw mapCreate(e)
|
|
88
|
+
}
|
|
89
|
+
val json = (response as? CreatePublicKeyCredentialResponse)?.registrationResponseJson
|
|
90
|
+
?: throw walletError(WalletErrorCode.INTERNAL, "unexpected credential response")
|
|
91
|
+
val cred = Json.parseToJsonElement(json) as JsonObject
|
|
92
|
+
val ext = cred["clientExtensionResults"] as? JsonObject
|
|
93
|
+
val prf = ext?.get("prf") as? JsonObject
|
|
94
|
+
if (prf == null || (prf["enabled"] as? JsonPrimitive)?.booleanOrNull == false) throw walletError(WalletErrorCode.PRF_UNSUPPORTED)
|
|
95
|
+
val credentialId = normalize(cred.str("rawId") ?: cred.str("id") ?: throw walletError(WalletErrorCode.INTERNAL, "credential id missing"))
|
|
96
|
+
val resp = cred["response"] as? JsonObject ?: JsonObject(emptyMap())
|
|
97
|
+
val alg = (resp["publicKeyAlgorithm"] as? JsonPrimitive)?.intOrNull
|
|
98
|
+
val spki = resp.str("publicKey")?.takeIf { alg == -7 || alg == -257 || alg == -8 }?.let { normalize(it) }
|
|
99
|
+
val attestation = resp.str("attestationObject")?.let { normalize(it) }
|
|
100
|
+
val direct = prfFirst(prf)
|
|
101
|
+
val prfOutput = direct ?: get(credentialId, salt, null).prfOutput
|
|
102
|
+
return PasskeyCreateResult(credentialId, prfOutput, spki, if (spki != null) alg else null, attestation)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override suspend fun get(credentialId: String, salt: ByteArray, challenge: ByteArray?): PasskeyGetResult {
|
|
106
|
+
val request = buildJsonObject {
|
|
107
|
+
put("challenge", Bytes.toBase64Url(challenge ?: Bytes.randomBytes(32)))
|
|
108
|
+
put("rpId", rpId)
|
|
109
|
+
putJsonArray("allowCredentials") {
|
|
110
|
+
addJsonObject {
|
|
111
|
+
put("type", "public-key")
|
|
112
|
+
put("id", normalize(credentialId))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
put("userVerification", "required")
|
|
116
|
+
prfExtension(salt)
|
|
117
|
+
}
|
|
118
|
+
val activity = activity()
|
|
119
|
+
val result = try {
|
|
120
|
+
withContext(Dispatchers.Main) {
|
|
121
|
+
CredentialManager.create(activity).getCredential(
|
|
122
|
+
activity,
|
|
123
|
+
GetCredentialRequest(listOf(GetPublicKeyCredentialOption(request.toString()))),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
} catch (e: GetCredentialException) {
|
|
127
|
+
throw mapGet(e)
|
|
128
|
+
}
|
|
129
|
+
val json = (result.credential as? PublicKeyCredential)?.authenticationResponseJson
|
|
130
|
+
?: throw walletError(WalletErrorCode.INTERNAL, "unexpected credential response")
|
|
131
|
+
val cred = Json.parseToJsonElement(json) as JsonObject
|
|
132
|
+
val prf = (cred["clientExtensionResults"] as? JsonObject)?.get("prf") as? JsonObject
|
|
133
|
+
val out = prf?.let { prfFirst(it) } ?: throw walletError(WalletErrorCode.PRF_UNSUPPORTED)
|
|
134
|
+
val resp = cred["response"] as? JsonObject ?: JsonObject(emptyMap())
|
|
135
|
+
fun field(name: String) = resp.str(name)?.let { normalize(it) } ?: ""
|
|
136
|
+
return PasskeyGetResult(
|
|
137
|
+
out,
|
|
138
|
+
PasskeyAssertion(
|
|
139
|
+
credentialId = normalize(cred.str("rawId") ?: cred.str("id") ?: credentialId),
|
|
140
|
+
clientDataJSON = field("clientDataJSON"),
|
|
141
|
+
authenticatorData = field("authenticatorData"),
|
|
142
|
+
signature = field("signature"),
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private fun kotlinx.serialization.json.JsonObjectBuilder.prfExtension(salt: ByteArray) {
|
|
148
|
+
putJsonObject("extensions") {
|
|
149
|
+
putJsonObject("prf") {
|
|
150
|
+
putJsonObject("eval") { put("first", Bytes.toBase64Url(salt)) }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private fun prfFirst(prf: JsonObject): ByteArray? {
|
|
156
|
+
val first = ((prf["results"] as? JsonObject)?.get("first") as? JsonPrimitive)?.contentOrNull ?: return null
|
|
157
|
+
val bytes = runCatching { Bytes.fromBase64(first) }.getOrNull() ?: return null
|
|
158
|
+
return bytes.takeIf { it.size == 32 }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun normalize(b64: String): String = Bytes.toBase64Url(Bytes.fromBase64(b64))
|
|
162
|
+
|
|
163
|
+
private fun JsonObject.str(k: String) = (this[k] as? JsonPrimitive)?.takeIf { it.isString }?.content
|
|
164
|
+
|
|
165
|
+
private fun mapDom(error: DomError, cause: Exception): WalletException = when (error) {
|
|
166
|
+
is NotAllowedError -> WalletException(WalletErrorCode.USER_CANCELLED, cause.message, cause)
|
|
167
|
+
is SecurityError -> WalletException(
|
|
168
|
+
WalletErrorCode.ORIGIN_NOT_ALLOWED,
|
|
169
|
+
"this app is not linked to $rpId (Digital Asset Links): ${cause.message}",
|
|
170
|
+
cause,
|
|
171
|
+
)
|
|
172
|
+
is NotSupportedError -> WalletException(WalletErrorCode.PRF_UNSUPPORTED, cause.message, cause)
|
|
173
|
+
else -> WalletException(WalletErrorCode.INTERNAL, "passkey failure", cause)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private fun mapCreate(e: CreateCredentialException): WalletException = when (e) {
|
|
177
|
+
is CreateCredentialCancellationException -> WalletException(WalletErrorCode.USER_CANCELLED, e.message, e)
|
|
178
|
+
is CreateCredentialUnsupportedException, is CreateCredentialProviderConfigurationException ->
|
|
179
|
+
WalletException(WalletErrorCode.PRF_UNSUPPORTED, e.message, e)
|
|
180
|
+
is CreatePublicKeyCredentialDomException -> mapDom(e.domError, e)
|
|
181
|
+
else -> WalletException(WalletErrorCode.INTERNAL, "passkey failure", e)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun mapGet(e: GetCredentialException): WalletException = when (e) {
|
|
185
|
+
is GetCredentialCancellationException -> WalletException(WalletErrorCode.USER_CANCELLED, e.message, e)
|
|
186
|
+
is NoCredentialException -> WalletException(WalletErrorCode.USER_CANCELLED, "no matching backup passkey", e)
|
|
187
|
+
is GetCredentialUnsupportedException, is GetCredentialProviderConfigurationException ->
|
|
188
|
+
WalletException(WalletErrorCode.PRF_UNSUPPORTED, e.message, e)
|
|
189
|
+
is GetPublicKeyCredentialDomException -> mapDom(e.domError, e)
|
|
190
|
+
else -> WalletException(WalletErrorCode.INTERNAL, "passkey failure", e)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Der.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import java.math.BigInteger
|
|
5
|
+
|
|
6
|
+
/** DER encoding of an ECDSA (r, s) signature. */
|
|
7
|
+
internal object Der {
|
|
8
|
+
private fun int(v: ByteArray): ByteArray {
|
|
9
|
+
val n = BigInteger(1, v).toByteArray() // minimal, with a leading 0 when the high bit is set
|
|
10
|
+
return byteArrayOf(0x02, n.size.toByte()) + n
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fun encode(r: ByteArray, s: ByteArray): ByteArray {
|
|
14
|
+
val body = int(r) + int(s)
|
|
15
|
+
return byteArrayOf(0x30, body.size.toByte()) + body
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/DeviceStore.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import java.math.BigInteger
|
|
5
|
+
import java.security.KeyPairGenerator
|
|
6
|
+
import java.security.PrivateKey
|
|
7
|
+
import java.security.Signature
|
|
8
|
+
import java.security.interfaces.ECPublicKey
|
|
9
|
+
import java.security.spec.ECGenParameterSpec
|
|
10
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
11
|
+
import javax.crypto.Cipher
|
|
12
|
+
import javax.crypto.KeyGenerator
|
|
13
|
+
import javax.crypto.SecretKey
|
|
14
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
15
|
+
|
|
16
|
+
class SealedShare(val iv: ByteArray, val ciphertext: ByteArray)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The non-exportable keys of one device record: a P-256 signing key (its public JWK is registered
|
|
20
|
+
* with the API) and an AES-256-GCM key that seals the device share D.
|
|
21
|
+
*/
|
|
22
|
+
interface DeviceKeys {
|
|
23
|
+
/** Identifies the keys inside their store (e.g. the Keystore alias prefix). */
|
|
24
|
+
val handle: String
|
|
25
|
+
val publicJwk: DevicePublicJwk
|
|
26
|
+
|
|
27
|
+
/** ECDSA P-256 / SHA-256 over [data]; raw r||s (64 bytes) like WebCrypto. */
|
|
28
|
+
fun sign(data: ByteArray): ByteArray
|
|
29
|
+
|
|
30
|
+
fun seal(plain: ByteArray, aad: ByteArray): SealedShare
|
|
31
|
+
|
|
32
|
+
fun open(sealed: SealedShare, aad: ByteArray): ByteArray
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data class DeviceRecord(
|
|
36
|
+
/** `projectId \0 userId`. */
|
|
37
|
+
val key: String,
|
|
38
|
+
val keys: DeviceKeys,
|
|
39
|
+
val iv: ByteArray,
|
|
40
|
+
val ciphertext: ByteArray,
|
|
41
|
+
val deviceId: String,
|
|
42
|
+
val walletId: String,
|
|
43
|
+
val address: String,
|
|
44
|
+
val shareVersion: Int,
|
|
45
|
+
val createdAt: Long,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
/** Where the device share lives. Only D (sealed) and key references are stored; never k, A or B. */
|
|
49
|
+
interface DeviceStore {
|
|
50
|
+
suspend fun generateKeys(): DeviceKeys
|
|
51
|
+
|
|
52
|
+
/** Removes keys that never made it into a stored record. */
|
|
53
|
+
suspend fun discardKeys(keys: DeviceKeys)
|
|
54
|
+
|
|
55
|
+
suspend fun get(key: String): DeviceRecord?
|
|
56
|
+
|
|
57
|
+
/** Replaces the record for [DeviceRecord.key]; keys of a replaced record are removed. */
|
|
58
|
+
suspend fun put(record: DeviceRecord)
|
|
59
|
+
|
|
60
|
+
suspend fun delete(key: String)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
internal object DeviceStorage {
|
|
64
|
+
fun storageKey(projectId: String, userId: String): String = "$projectId\u0000$userId"
|
|
65
|
+
|
|
66
|
+
/** DER ECDSA signature -> raw r||s, each left-padded to [size] bytes. */
|
|
67
|
+
fun derToRaw(der: ByteArray, size: Int = 32): ByteArray {
|
|
68
|
+
var i = 0
|
|
69
|
+
fun readLen(): Int {
|
|
70
|
+
var len = der[i++].toInt() and 0xff
|
|
71
|
+
if (len and 0x80 != 0) {
|
|
72
|
+
val n = len and 0x7f
|
|
73
|
+
len = 0
|
|
74
|
+
repeat(n) { len = (len shl 8) or (der[i++].toInt() and 0xff) }
|
|
75
|
+
}
|
|
76
|
+
return len
|
|
77
|
+
}
|
|
78
|
+
require(der[i++].toInt() == 0x30) { "not a DER sequence" }
|
|
79
|
+
readLen()
|
|
80
|
+
fun readInt(): ByteArray {
|
|
81
|
+
require(der[i++].toInt() == 0x02) { "not a DER integer" }
|
|
82
|
+
val len = readLen()
|
|
83
|
+
val v = der.copyOfRange(i, i + len)
|
|
84
|
+
i += len
|
|
85
|
+
return v
|
|
86
|
+
}
|
|
87
|
+
val r = readInt()
|
|
88
|
+
val s = readInt()
|
|
89
|
+
val out = ByteArray(size * 2)
|
|
90
|
+
fun place(v: ByteArray, offset: Int) {
|
|
91
|
+
val trimmed = v.dropWhile { it.toInt() == 0 }.toByteArray()
|
|
92
|
+
require(trimmed.size <= size) { "integer too long" }
|
|
93
|
+
trimmed.copyInto(out, offset + size - trimmed.size)
|
|
94
|
+
}
|
|
95
|
+
place(r, 0)
|
|
96
|
+
place(s, size)
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun unsigned32(v: BigInteger): ByteArray {
|
|
101
|
+
val raw = v.toByteArray()
|
|
102
|
+
val trimmed = if (raw.size > 32) raw.copyOfRange(raw.size - 32, raw.size) else raw
|
|
103
|
+
return ByteArray(32 - trimmed.size) + trimmed
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun jwkOf(key: ECPublicKey): DevicePublicJwk =
|
|
107
|
+
DevicePublicJwk(Bytes.toBase64Url(unsigned32(key.w.affineX)), Bytes.toBase64Url(unsigned32(key.w.affineY)))
|
|
108
|
+
|
|
109
|
+
fun gcmSeal(key: SecretKey, plain: ByteArray, aad: ByteArray, iv: ByteArray? = null): SealedShare {
|
|
110
|
+
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
111
|
+
if (iv == null) cipher.init(Cipher.ENCRYPT_MODE, key) else cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(128, iv))
|
|
112
|
+
cipher.updateAAD(aad)
|
|
113
|
+
val ct = cipher.doFinal(plain)
|
|
114
|
+
return SealedShare(cipher.iv, ct)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fun gcmOpen(key: SecretKey, sealed: SealedShare, aad: ByteArray): ByteArray {
|
|
118
|
+
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
119
|
+
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, sealed.iv))
|
|
120
|
+
cipher.updateAAD(aad)
|
|
121
|
+
return cipher.doFinal(sealed.ciphertext)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* In-memory store with software JCA keys. For JVM tests and emulators only: nothing survives the
|
|
127
|
+
* process and the keys are not hardware-backed.
|
|
128
|
+
*/
|
|
129
|
+
class InMemoryDeviceStore : DeviceStore {
|
|
130
|
+
private val records = ConcurrentHashMap<String, DeviceRecord>()
|
|
131
|
+
private var counter = 0
|
|
132
|
+
|
|
133
|
+
private class SoftwareKeys(override val handle: String) : DeviceKeys {
|
|
134
|
+
private val signKey: PrivateKey
|
|
135
|
+
private val aesKey: SecretKey = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
|
|
136
|
+
override val publicJwk: DevicePublicJwk
|
|
137
|
+
|
|
138
|
+
init {
|
|
139
|
+
val pair = KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair()
|
|
140
|
+
signKey = pair.private
|
|
141
|
+
publicJwk = DeviceStorage.jwkOf(pair.public as ECPublicKey)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun sign(data: ByteArray): ByteArray {
|
|
145
|
+
val s = Signature.getInstance("SHA256withECDSA")
|
|
146
|
+
s.initSign(signKey)
|
|
147
|
+
s.update(data)
|
|
148
|
+
return DeviceStorage.derToRaw(s.sign())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override fun seal(plain: ByteArray, aad: ByteArray): SealedShare =
|
|
152
|
+
DeviceStorage.gcmSeal(aesKey, plain, aad, Bytes.randomBytes(12))
|
|
153
|
+
|
|
154
|
+
override fun open(sealed: SealedShare, aad: ByteArray): ByteArray = DeviceStorage.gcmOpen(aesKey, sealed, aad)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override suspend fun generateKeys(): DeviceKeys = synchronized(this) { SoftwareKeys("mem-${++counter}") }
|
|
158
|
+
|
|
159
|
+
override suspend fun discardKeys(keys: DeviceKeys) = Unit
|
|
160
|
+
|
|
161
|
+
override suspend fun get(key: String): DeviceRecord? = records[key]
|
|
162
|
+
|
|
163
|
+
override suspend fun put(record: DeviceRecord) {
|
|
164
|
+
records[record.key] = record
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override suspend fun delete(key: String) {
|
|
168
|
+
records.remove(key)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.app.AlertDialog
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.graphics.Typeface
|
|
9
|
+
import android.hardware.biometrics.BiometricManager
|
|
10
|
+
import android.hardware.biometrics.BiometricPrompt
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.os.CancellationSignal
|
|
13
|
+
import android.util.TypedValue
|
|
14
|
+
import android.view.ViewGroup
|
|
15
|
+
import android.widget.LinearLayout
|
|
16
|
+
import android.widget.ScrollView
|
|
17
|
+
import android.widget.TextView
|
|
18
|
+
import kotlinx.coroutines.Dispatchers
|
|
19
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
20
|
+
import kotlinx.coroutines.withContext
|
|
21
|
+
import kotlin.coroutines.resume
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default approval UI: a platform AlertDialog with the summary (rows, warnings, the full text).
|
|
25
|
+
* "Sign" is enabled only after the text was scrolled to the end, like the web approval card.
|
|
26
|
+
*/
|
|
27
|
+
class DialogWalletApprover(private val activityProvider: () -> Activity?) : WalletApprover {
|
|
28
|
+
|
|
29
|
+
override suspend fun approve(summary: SignSummary): Boolean = withContext(Dispatchers.Main) {
|
|
30
|
+
val activity = activityProvider()?.takeUnless { it.isFinishing || it.isDestroyed }
|
|
31
|
+
?: throw walletError(WalletErrorCode.CONFIRM_WINDOW_BLOCKED, "no foreground activity for the approval")
|
|
32
|
+
suspendCancellableCoroutine { cont ->
|
|
33
|
+
val scroll = ScrollView(activity)
|
|
34
|
+
scroll.addView(content(activity, summary))
|
|
35
|
+
var answered = false
|
|
36
|
+
val dialog = AlertDialog.Builder(activity)
|
|
37
|
+
.setTitle(summary.title)
|
|
38
|
+
.setView(scroll)
|
|
39
|
+
.setPositiveButton("Sign") { _, _ ->
|
|
40
|
+
answered = true
|
|
41
|
+
if (cont.isActive) cont.resume(true)
|
|
42
|
+
}
|
|
43
|
+
.setNegativeButton("Cancel") { _, _ ->
|
|
44
|
+
answered = true
|
|
45
|
+
if (cont.isActive) cont.resume(false)
|
|
46
|
+
}
|
|
47
|
+
.setOnDismissListener {
|
|
48
|
+
if (!answered && cont.isActive) cont.resume(false)
|
|
49
|
+
}
|
|
50
|
+
.create()
|
|
51
|
+
dialog.setCanceledOnTouchOutside(false)
|
|
52
|
+
dialog.setOnShowListener {
|
|
53
|
+
val sign = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
|
54
|
+
fun update() {
|
|
55
|
+
sign.isEnabled = !scroll.canScrollVertically(1)
|
|
56
|
+
}
|
|
57
|
+
update()
|
|
58
|
+
scroll.setOnScrollChangeListener { _, _, _, _, _ -> if (!sign.isEnabled) update() }
|
|
59
|
+
scroll.post { update() }
|
|
60
|
+
}
|
|
61
|
+
cont.invokeOnCancellation { dialog.dismiss() }
|
|
62
|
+
dialog.show()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun content(context: Context, summary: SignSummary): LinearLayout {
|
|
67
|
+
val pad = dp(context, 20)
|
|
68
|
+
val layout = LinearLayout(context).apply {
|
|
69
|
+
orientation = LinearLayout.VERTICAL
|
|
70
|
+
setPadding(pad, dp(context, 8), pad, dp(context, 8))
|
|
71
|
+
}
|
|
72
|
+
for (w in summary.warnings) {
|
|
73
|
+
layout.addView(text(context, w).apply { setTextColor(Color.rgb(0xb4, 0x23, 0x18)); setPadding(0, 0, 0, dp(context, 8)) })
|
|
74
|
+
}
|
|
75
|
+
for (row in summary.rows) {
|
|
76
|
+
layout.addView(text(context, row.label).apply { alpha = 0.7f; setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) })
|
|
77
|
+
layout.addView(
|
|
78
|
+
text(context, row.value).apply {
|
|
79
|
+
if (row.mono) typeface = Typeface.MONOSPACE
|
|
80
|
+
setTextIsSelectable(true)
|
|
81
|
+
setPadding(0, 0, 0, dp(context, 8))
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
summary.body?.let { body ->
|
|
86
|
+
layout.addView(
|
|
87
|
+
text(context, body).apply {
|
|
88
|
+
typeface = Typeface.MONOSPACE
|
|
89
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
|
90
|
+
setTextIsSelectable(true)
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
return layout
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun text(context: Context, value: String) = TextView(context).apply {
|
|
98
|
+
text = value
|
|
99
|
+
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun dp(context: Context, v: Int) = (v * context.resources.displayMetrics.density).toInt()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Device lock check with the platform BiometricPrompt: biometrics or the device credential on
|
|
107
|
+
* Android 10+, biometrics only on Android 9.
|
|
108
|
+
*/
|
|
109
|
+
class BiometricDeviceAuthenticator(private val activityProvider: () -> Activity?) : DeviceAuthenticator {
|
|
110
|
+
|
|
111
|
+
override suspend fun authenticate(reason: String): Boolean = withContext(Dispatchers.Main) {
|
|
112
|
+
val activity = activityProvider()?.takeUnless { it.isFinishing || it.isDestroyed }
|
|
113
|
+
?: throw walletError(WalletErrorCode.CONFIRM_WINDOW_BLOCKED, "no foreground activity for device authentication")
|
|
114
|
+
suspendCancellableCoroutine { cont ->
|
|
115
|
+
val executor = activity.mainExecutor
|
|
116
|
+
val builder = BiometricPrompt.Builder(activity).setTitle(reason).setSubtitle("Confirm it is you")
|
|
117
|
+
when {
|
|
118
|
+
Build.VERSION.SDK_INT >= 30 -> builder.setAllowedAuthenticators(
|
|
119
|
+
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL,
|
|
120
|
+
)
|
|
121
|
+
Build.VERSION.SDK_INT == 29 -> @Suppress("DEPRECATION") builder.setDeviceCredentialAllowed(true)
|
|
122
|
+
else -> builder.setNegativeButton("Cancel", executor) { _, _ -> if (cont.isActive) cont.resume(false) }
|
|
123
|
+
}
|
|
124
|
+
val signal = CancellationSignal()
|
|
125
|
+
cont.invokeOnCancellation { signal.cancel() }
|
|
126
|
+
builder.build().authenticate(
|
|
127
|
+
signal,
|
|
128
|
+
executor,
|
|
129
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
130
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
131
|
+
if (cont.isActive) cont.resume(true)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
135
|
+
if (cont.isActive) cont.resume(false)
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|