@authon/react-native-wallet 0.1.0 → 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 +34 -4
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +57 -3
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +29 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +6 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +27 -23
- 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 +16 -8
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +18 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +103 -1
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +8 -1
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
- package/build/client.d.ts +18 -2
- package/build/client.d.ts.map +1 -1
- package/build/client.js +88 -6
- package/build/client.js.map +1 -1
- package/build/errors.d.ts +8 -1
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +7 -1
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/native.d.ts +5 -1
- package/build/native.d.ts.map +1 -1
- package/build/native.js.map +1 -1
- package/build/types.d.ts +47 -1
- package/build/types.d.ts.map +1 -1
- package/ios/AuthonReactNativeWallet.podspec +6 -2
- package/ios/AuthonWalletModule.swift +46 -3
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +24 -4
- 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/Chains.swift +278 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +24 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +19 -8
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +101 -2
- package/ios/vendor/AuthonWallet/WalletError.swift +32 -2
- package/ios/vendor/CKeccak/README.md +6 -3
- package/ios/vendor/CKeccak/include/ckeccak.h +4 -0
- package/ios/vendor/CKeccak/ripemd160.c +343 -0
- package/ios/vendor/CKeccak/ripemd160.h +22 -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/GENERATED.md +1 -1
- package/package.json +3 -2
- package/src/client.ts +90 -6
- package/src/errors.ts +7 -1
- package/src/index.ts +5 -0
- package/src/native.ts +4 -2
- package/src/types.ts +52 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @authon/react-native-wallet
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- `signChainTransaction({ chain, account, address, payload })` for app-built transactions on other chains (NATIVE.md section 12): Solana (`{ message }` → `{ signature }`), TRON (`{ rawDataHex }` → `{ txID, signature }`), Bitcoin and Bitcoin testnet (`{ psbt }` → `{ psbt, txHex? }`) and XRP (`{ txBlobHex }` → `{ signature }`). EVM stays on `signTransaction` (`INVALID_PARAMS` for `chain: 'evm'`). The native approval UI summarizes the transaction before the key is assembled.
|
|
6
|
+
- `getAccounts` passes through `publicKey` when the native wallet returns one (XRP).
|
|
7
|
+
|
|
8
|
+
## 0.3.0
|
|
9
|
+
|
|
10
|
+
- `getAccounts({ start, count, chain })`: account addresses on `evm` (default), `tron`, `xrp`, `bitcoin`, `bitcoin-testnet` and `solana`, derived from the same seed with the standard paths. Each account carries its `chain`; a native wallet that answers without it is refused (`UNSUPPORTED_TYPE`). Signing stays EVM only.
|
|
11
|
+
|
|
12
|
+
## 0.2.0
|
|
13
|
+
|
|
14
|
+
- `capabilities()`: checks before sign-in whether this device and app can use the wallet (app registration and approval, wallet enabled, hardware key storage, device lock, passkey backup support), with reason codes and the app-config `diagnosticId`. No session needed.
|
|
15
|
+
- `AuthonWalletError` has `diagnosticId` (server request ID) and `retryable`, passed through from the Swift and Kotlin wallets.
|
|
16
|
+
- iOS: a second signature right after the first no longer hangs; the approval sheet fails with `CONFIRM_WINDOW_BLOCKED` when it cannot be shown.
|
|
17
|
+
|
|
18
|
+
## 0.1.0
|
|
19
|
+
|
|
20
|
+
- First release: Expo module over the Swift and Kotlin embedded wallets (status, create/ensure, accounts, personal_sign, EIP-712, transactions, passkey protect/upgrade/restore, device revoke), `useAuthonWallet()` with `@authon/react-native`.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> (`AuthonWallet`) and Kotlin (`dev.authon.sdk.wallet`) wallet implementations.
|
|
5
5
|
|
|
6
6
|
[](../../LICENSE)
|
|
7
|
-
[]()
|
|
8
8
|
|
|
9
9
|
It is the same wallet as the web SDK (`@authon/js`): same wallet row, same `/v1/wallet/me/*` API,
|
|
10
10
|
same shares. A user gets the same address in your web app and your mobile app, and the app install
|
|
@@ -27,8 +27,12 @@ appears as one more device in the wallet's device list.
|
|
|
27
27
|
- iOS 16+ (iOS 18+ for passkey backup and restore, which need the passkey PRF extension).
|
|
28
28
|
- Android minSdk 28, compileSdk 35+. Passkey backup and restore need Google Play services with a
|
|
29
29
|
passkey provider that supports PRF (Google Password Manager on Android 9+ with current Play services).
|
|
30
|
-
- EVM
|
|
30
|
+
- Signing: EVM (personal_sign, EIP-712 v4, legacy / EIP-2930 / EIP-1559 transactions), HD accounts
|
|
31
31
|
`m/44'/60'/0'/0/n`.
|
|
32
|
+
- Addresses (receive / display) from the same seed: TRON `m/44'/195'/n'/0/0`, XRP `m/44'/144'/n'/0/0`,
|
|
33
|
+
Bitcoin P2WPKH `m/84'/0'/n'/0/0` (testnet `m/84'/1'/n'/0/0`), Solana `m/44'/501'/n'/0'`. These
|
|
34
|
+
chains sign through `signChainTransaction` (the app builds the transaction; fees, balances and broadcast are up to the app). A wallet created before HD wallets has EVM account 0 only
|
|
35
|
+
(`ACCOUNT_UNAVAILABLE` for other chains).
|
|
32
36
|
|
|
33
37
|
## Install
|
|
34
38
|
|
|
@@ -128,14 +132,16 @@ Method names and result shapes follow `@authon/js` (`WalletClient`).
|
|
|
128
132
|
|
|
129
133
|
| Method | Description |
|
|
130
134
|
|---|---|
|
|
135
|
+
| `capabilities()` | Pre-sign-in check of this device and app (see below). No session, creates nothing, opens no UI. |
|
|
131
136
|
| `appConfig()` | App registration and wallet settings: `{ projectId, allowed, approved, enabled, chainIds, confirmSigning, rpId, minFrameProtocol }`. No session needed. |
|
|
132
137
|
| `get()` / `status()` | The signed-in user's wallet `{ address, protected, needsRestore, backupUpgradeRecommended, devices }`, or `null` when there is none. |
|
|
133
138
|
| `create()` | Creates the wallet (or returns the existing one). |
|
|
134
139
|
| `ensure()` | `get()`, then `create()` when there is no wallet. Concurrent calls share one request. |
|
|
135
140
|
| `isProtected()` | True when the wallet has a passkey backup. |
|
|
136
|
-
| `getAccounts({ start, count })` | HD account addresses `{ index, path, address }` (public, no approval). |
|
|
141
|
+
| `getAccounts({ start, count, chain })` | HD account addresses `{ index, path, address, chain }` (public, no approval). `chain`: `evm` (default), `tron`, `xrp`, `bitcoin`, `bitcoin-testnet`, `solana`. |
|
|
137
142
|
| `signMessage(message, { account })` | EIP-191 personal_sign; `message` is text or `{ raw: '0x...' }`. |
|
|
138
143
|
| `signTypedData(typedData, { account })` | EIP-712 v4. |
|
|
144
|
+
| `signChainTransaction({ chain, account, address, payload })` | Signs an app-built transaction: Solana `{ message }` (base64 Message) → `{ signature }` (base58); TRON `{ rawDataHex }` → `{ txID, signature }`; Bitcoin / testnet `{ psbt }` (base64, every input with witness_utxo; only our P2WPKH inputs are signed) → `{ psbt, txHex? }` (txHex when every input is ours); XRP `{ txBlobHex }` (encodeForSigning output with SigningPubKey = `getAccounts({ chain: 'xrp' })` `publicKey`) → `{ signature }` (DER). The approval UI summarizes it; unknown programs are shown as high risk. `address` must be the account's address on that chain (`ADDRESS_MISMATCH` otherwise). |
|
|
139
145
|
| `signTransaction(tx, { account })` | Legacy, EIP-2930 or EIP-1559; returns the serialized signed transaction. `bigint` values are allowed. |
|
|
140
146
|
| `protect()` | Creates the passkey backup. Do not let users receive funds before the wallet is protected. |
|
|
141
147
|
| `upgradeBackup()` | Replaces a legacy backup (`backupUpgradeRecommended`) with a verifiable one. |
|
|
@@ -159,14 +165,38 @@ Options of `createAuthonWallet` / `useAuthonWallet`:
|
|
|
159
165
|
| `requireDeviceAuthentication` | Also ask for Face ID / Touch ID / passcode (iOS) or BiometricPrompt (Android) before each signature. |
|
|
160
166
|
| `testing` | **Test only**, refused outside debug builds: `{ allowSoftwareKeys, autoApprove }` for simulators and emulators (software device keys instead of Secure Enclave / StrongBox, and no approval UI). Never ship it. |
|
|
161
167
|
|
|
168
|
+
### Before sign-in: `capabilities()`
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const caps = await wallet.capabilities();
|
|
172
|
+
if (caps.ready === 'unavailable') showWalletUnavailable(caps); // tell the user before sign-in
|
|
173
|
+
// 'unknown': continue, but be ready for that feature to fail
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`{ ready, app, wallet, secureStorage, deviceLock, passkeyBackup, diagnosticId }`; each check is
|
|
177
|
+
`{ status: 'available' | 'unavailable' | 'unknown', reason }`. `ready` is `unavailable` if any check is,
|
|
178
|
+
else `unknown` if any is, else `available`. It answers for a release build (the `testing` options are
|
|
179
|
+
ignored).
|
|
180
|
+
|
|
181
|
+
| Check | unavailable reason | unknown reason |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `app` | `APP_NOT_REGISTERED`, `APP_SIGNATURE_MISMATCH` (Android: the running app's signing certificate differs from the registered one) | `APP_NOT_APPROVED` (signing works, passkey backup waits for approval), `NETWORK_ERROR`, `API_ERROR` |
|
|
184
|
+
| `wallet` | `WALLET_DISABLED` | `NETWORK_ERROR` |
|
|
185
|
+
| `secureStorage` | `HARDWARE_KEYSTORE_UNAVAILABLE` | `SIMULATOR` (simulators and emulators never count as hardware protected), `CHECK_FAILED` |
|
|
186
|
+
| `deviceLock` | `DEVICE_LOCK_NOT_SET` | `CHECK_FAILED` |
|
|
187
|
+
| `passkeyBackup` | `OS_UNSUPPORTED` (iOS < 18, Android < 9), `PASSKEY_API_UNAVAILABLE` | `PRF_PROVIDER_UNVERIFIED` (the provider's PRF support is confirmed on the first `protect()`) |
|
|
188
|
+
|
|
162
189
|
### Errors
|
|
163
190
|
|
|
164
|
-
Every failure is an `AuthonWalletError` with
|
|
191
|
+
Every failure is an `AuthonWalletError` with `code`, `message`, `diagnosticId` (the server's request ID
|
|
192
|
+
when an API response caused it; log it, never secrets) and `retryable`. Codes are the web wallet's
|
|
165
193
|
(`NOT_FOUND`, `NEEDS_RESTORE`, `NOT_PROTECTED`, `PRF_UNSUPPORTED`, `ORIGIN_NOT_ALLOWED`,
|
|
166
194
|
`USER_CANCELLED`, `UNAUTHORIZED`, `ADDRESS_MISMATCH`, `STORAGE_UNAVAILABLE`, `LAST_DEVICE`,
|
|
167
195
|
`MESSAGE_TOO_LARGE`, `FRAME_OUTDATED`, `BACKUP_KEY_REQUIRED`, `ALREADY_UPGRADED`,
|
|
168
196
|
`ACCOUNT_UNAVAILABLE`, ...), reported unchanged by the Swift and Kotlin wallets. The JS layer adds
|
|
169
197
|
`NOT_SIGNED_IN` (no token), `INVALID_PARAMS`, `DISPOSED` and `NOT_CONFIGURED` (native module missing).
|
|
198
|
+
`USER_CANCELLED` means the user rejected the approval or closed the passkey sheet: go back without an
|
|
199
|
+
error message. `CONFIRM_WINDOW_BLOCKED` means the approval UI could not be shown; nothing was signed.
|
|
170
200
|
|
|
171
201
|
```ts
|
|
172
202
|
try {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
2
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
3
3
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
|
4
|
+
<!-- capabilities(): checks whether Google Play services (passkey provider) is installed. -->
|
|
5
|
+
<queries>
|
|
6
|
+
<package android:name="com.google.android.gms" />
|
|
7
|
+
</queries>
|
|
4
8
|
</manifest>
|
|
@@ -2,6 +2,9 @@ package dev.authon.reactnative.wallet
|
|
|
2
2
|
|
|
3
3
|
import android.content.pm.ApplicationInfo
|
|
4
4
|
import dev.authon.sdk.wallet.AuthonWallet
|
|
5
|
+
import dev.authon.sdk.wallet.CapabilityCheck
|
|
6
|
+
import dev.authon.sdk.wallet.WalletCapabilities
|
|
7
|
+
import dev.authon.sdk.wallet.WalletChain
|
|
5
8
|
import dev.authon.sdk.wallet.PublicWallet
|
|
6
9
|
import dev.authon.sdk.wallet.RestoreMode
|
|
7
10
|
import dev.authon.sdk.wallet.SignAccount
|
|
@@ -15,6 +18,15 @@ import expo.modules.kotlin.functions.Coroutine
|
|
|
15
18
|
import expo.modules.kotlin.modules.Module
|
|
16
19
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
17
20
|
import java.util.concurrent.ConcurrentHashMap
|
|
21
|
+
import kotlinx.serialization.json.Json
|
|
22
|
+
import kotlinx.serialization.json.JsonArray
|
|
23
|
+
import kotlinx.serialization.json.JsonElement
|
|
24
|
+
import kotlinx.serialization.json.JsonNull
|
|
25
|
+
import kotlinx.serialization.json.JsonObject
|
|
26
|
+
import kotlinx.serialization.json.JsonPrimitive
|
|
27
|
+
import kotlinx.serialization.json.booleanOrNull
|
|
28
|
+
import kotlinx.serialization.json.doubleOrNull
|
|
29
|
+
import kotlinx.serialization.json.longOrNull
|
|
18
30
|
import kotlin.coroutines.AbstractCoroutineContextElement
|
|
19
31
|
import kotlin.coroutines.CoroutineContext
|
|
20
32
|
import kotlinx.coroutines.CancellationException
|
|
@@ -47,6 +59,10 @@ class AuthonWalletModule : Module() {
|
|
|
47
59
|
call(id, null) { appConfig(it.appConfig()) }
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
AsyncFunction("capabilities") Coroutine { id: String ->
|
|
63
|
+
call(id, null) { capabilities(it.capabilities()) }
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
AsyncFunction("status") Coroutine { id: String, token: String ->
|
|
51
67
|
call(id, token) { wallet(it.status()) }
|
|
52
68
|
}
|
|
@@ -55,9 +71,11 @@ class AuthonWalletModule : Module() {
|
|
|
55
71
|
call(id, token) { wallet(it.create()) }
|
|
56
72
|
}
|
|
57
73
|
|
|
58
|
-
AsyncFunction("accounts") Coroutine { id: String, token: String, start: Int, count: Int ->
|
|
74
|
+
AsyncFunction("accounts") Coroutine { id: String, token: String, start: Int, count: Int, chain: String ->
|
|
59
75
|
call(id, token) { w ->
|
|
60
|
-
w.accounts(start.toLong(), count
|
|
76
|
+
w.accounts(start.toLong(), count, WalletChain.fromWire(chain)).map {
|
|
77
|
+
mapOf("index" to it.index.toInt(), "path" to it.path, "address" to it.address, "chain" to it.chain.wire, "publicKey" to it.publicKey)
|
|
78
|
+
}
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
81
|
|
|
@@ -76,6 +94,10 @@ class AuthonWalletModule : Module() {
|
|
|
76
94
|
call(id, token) { it.signTransaction(transactionJson, SignAccount.parse(account.toLong(), address)) }
|
|
77
95
|
}
|
|
78
96
|
|
|
97
|
+
AsyncFunction("signChainTransaction") Coroutine { id: String, token: String, chain: String, account: Int, address: String, payloadJson: String ->
|
|
98
|
+
call(id, token) { it.signChainTransaction(WalletChain.fromWire(chain), account.toLong(), address, parsePayload(payloadJson)) }
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
AsyncFunction("protect") Coroutine { id: String, token: String ->
|
|
80
102
|
call(id, token) { mapOf("shareVersion" to it.protect().shareVersion) }
|
|
81
103
|
}
|
|
@@ -149,7 +171,26 @@ class AuthonWalletModule : Module() {
|
|
|
149
171
|
|
|
150
172
|
private fun failure(e: Throwable): Map<String, Any?> {
|
|
151
173
|
val w = e as? WalletException ?: WalletException(WalletErrorCode.INTERNAL, "unexpected error")
|
|
152
|
-
return mapOf(
|
|
174
|
+
return mapOf(
|
|
175
|
+
"ok" to false,
|
|
176
|
+
"code" to w.code.name,
|
|
177
|
+
"message" to (w.message ?: w.code.name),
|
|
178
|
+
"diagnosticId" to w.diagnosticId,
|
|
179
|
+
"retryable" to w.retryable,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private fun parsePayload(json: String): Map<String, Any?> {
|
|
184
|
+
val obj = runCatching { Json.parseToJsonElement(json) }.getOrNull() as? JsonObject
|
|
185
|
+
?: throw WalletException(WalletErrorCode.INVALID_REQUEST, "payload must be a JSON object")
|
|
186
|
+
return obj.mapValues { (_, v) -> plain(v) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun plain(e: JsonElement): Any? = when (e) {
|
|
190
|
+
is JsonNull -> null
|
|
191
|
+
is JsonObject -> e.mapValues { (_, v) -> plain(v) }
|
|
192
|
+
is JsonArray -> e.map { plain(it) }
|
|
193
|
+
is JsonPrimitive -> if (e.isString) e.content else e.booleanOrNull ?: e.longOrNull ?: e.doubleOrNull
|
|
153
194
|
}
|
|
154
195
|
|
|
155
196
|
// Public data only
|
|
@@ -164,6 +205,19 @@ class AuthonWalletModule : Module() {
|
|
|
164
205
|
},
|
|
165
206
|
)
|
|
166
207
|
|
|
208
|
+
private fun capabilities(c: WalletCapabilities): Map<String, Any?> {
|
|
209
|
+
fun check(k: CapabilityCheck) = mapOf("status" to k.status.wire, "reason" to k.reason)
|
|
210
|
+
return mapOf(
|
|
211
|
+
"ready" to c.ready.wire,
|
|
212
|
+
"app" to check(c.app),
|
|
213
|
+
"wallet" to check(c.wallet),
|
|
214
|
+
"secureStorage" to check(c.secureStorage),
|
|
215
|
+
"deviceLock" to check(c.deviceLock),
|
|
216
|
+
"passkeyBackup" to check(c.passkeyBackup),
|
|
217
|
+
"diagnosticId" to c.diagnosticId,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
167
221
|
private fun appConfig(c: WalletAppConfig): Map<String, Any?> = mapOf(
|
|
168
222
|
"projectId" to c.projectId,
|
|
169
223
|
"allowed" to c.allowed,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import android.app.KeyguardManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.security.keystore.KeyGenParameterSpec
|
|
9
|
+
import android.security.keystore.KeyProperties
|
|
10
|
+
import kotlinx.coroutines.Dispatchers
|
|
11
|
+
import kotlinx.coroutines.withContext
|
|
12
|
+
import java.security.KeyPairGenerator
|
|
13
|
+
import java.security.KeyStore
|
|
14
|
+
import java.security.MessageDigest
|
|
15
|
+
import java.security.spec.ECGenParameterSpec
|
|
16
|
+
import java.util.UUID
|
|
17
|
+
|
|
18
|
+
/** Device checks for capabilities() on Android. Test-only software-key settings are ignored. */
|
|
19
|
+
class AndroidCapabilityProbe(context: Context) : CapabilityProbe {
|
|
20
|
+
private val appContext = context.applicationContext
|
|
21
|
+
|
|
22
|
+
override fun signingCertSha256(): String? {
|
|
23
|
+
if (Build.VERSION.SDK_INT < 28) return null
|
|
24
|
+
val info = appContext.packageManager.getPackageInfo(appContext.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
|
|
25
|
+
val cert = info.signingInfo?.apkContentsSigners?.firstOrNull() ?: return null
|
|
26
|
+
return fingerprint(cert.toByteArray())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override suspend fun secureStorage(): CapabilityCheck = withContext(Dispatchers.IO) {
|
|
30
|
+
if (isEmulator()) return@withContext CapabilityCheck.unknown(Capabilities.SIMULATOR)
|
|
31
|
+
val alias = "dev.authon.wallet.probe.${UUID.randomUUID()}"
|
|
32
|
+
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
|
33
|
+
try {
|
|
34
|
+
val spec = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_SIGN)
|
|
35
|
+
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
|
36
|
+
.setDigests(KeyProperties.DIGEST_SHA256)
|
|
37
|
+
.build()
|
|
38
|
+
val pair = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore").apply { initialize(spec) }.generateKeyPair()
|
|
39
|
+
when (KeystoreSecurity.levelOf(pair.private)) {
|
|
40
|
+
KeySecurityLevel.STRONGBOX, KeySecurityLevel.TRUSTED_ENVIRONMENT, KeySecurityLevel.UNKNOWN_SECURE -> CapabilityCheck.AVAILABLE
|
|
41
|
+
KeySecurityLevel.SOFTWARE -> CapabilityCheck.unavailable(Capabilities.HARDWARE_KEYSTORE_UNAVAILABLE)
|
|
42
|
+
KeySecurityLevel.UNKNOWN -> CapabilityCheck.unknown(Capabilities.CHECK_FAILED)
|
|
43
|
+
}
|
|
44
|
+
} catch (_: Exception) {
|
|
45
|
+
CapabilityCheck.unknown(Capabilities.CHECK_FAILED)
|
|
46
|
+
} finally {
|
|
47
|
+
runCatching { ks.deleteEntry(alias) }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override fun deviceLock(): CapabilityCheck {
|
|
52
|
+
val km = appContext.getSystemService(KeyguardManager::class.java) ?: return CapabilityCheck.unknown(Capabilities.CHECK_FAILED)
|
|
53
|
+
return if (km.isDeviceSecure) CapabilityCheck.AVAILABLE else CapabilityCheck.unavailable(Capabilities.DEVICE_LOCK_NOT_SET)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun passkeyBackup(): CapabilityCheck {
|
|
57
|
+
if (Build.VERSION.SDK_INT < 28) return CapabilityCheck.unavailable(Capabilities.OS_UNSUPPORTED)
|
|
58
|
+
// Android 14+ ships the Credential Manager framework service; older versions need Play services.
|
|
59
|
+
val framework = Build.VERSION.SDK_INT >= 34 && appContext.getSystemService("credential") != null
|
|
60
|
+
val playServices = try {
|
|
61
|
+
appContext.packageManager.getApplicationInfo("com.google.android.gms", 0).enabled
|
|
62
|
+
} catch (_: PackageManager.NameNotFoundException) {
|
|
63
|
+
false
|
|
64
|
+
}
|
|
65
|
+
return if (framework || playServices) {
|
|
66
|
+
CapabilityCheck.unknown(Capabilities.PRF_PROVIDER_UNVERIFIED)
|
|
67
|
+
} else {
|
|
68
|
+
CapabilityCheck.unavailable(Capabilities.PASSKEY_API_UNAVAILABLE)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
companion object {
|
|
73
|
+
fun fingerprint(der: ByteArray): String =
|
|
74
|
+
MessageDigest.getInstance("SHA-256").digest(der).joinToString(":") { "%02X".format(it.toInt() and 0xff) }
|
|
75
|
+
|
|
76
|
+
/** Conservative emulator detection (goldfish/ranchu hardware, generic/emulator builds). */
|
|
77
|
+
fun isEmulator(): Boolean {
|
|
78
|
+
val hw = Build.HARDWARE.lowercase()
|
|
79
|
+
val fp = Build.FINGERPRINT.lowercase()
|
|
80
|
+
val product = Build.PRODUCT.lowercase()
|
|
81
|
+
return hw.contains("goldfish") || hw.contains("ranchu") || fp.startsWith("generic") ||
|
|
82
|
+
fp.contains("emulator") || fp.contains("/sdk_gphone") || product.startsWith("sdk_gphone") ||
|
|
83
|
+
product.contains("emulator") || product.contains("simulator")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -44,6 +44,7 @@ class AuthonWallet internal constructor(
|
|
|
44
44
|
private val passkeysFactory: (rpId: String) -> PasskeyProvider,
|
|
45
45
|
private val approver: WalletApprover?,
|
|
46
46
|
private val deviceAuthenticator: DeviceAuthenticator?,
|
|
47
|
+
private val probe: CapabilityProbe?,
|
|
47
48
|
private val dispatcher: CoroutineContext = Dispatchers.Default,
|
|
48
49
|
) {
|
|
49
50
|
/**
|
|
@@ -69,6 +70,7 @@ class AuthonWallet internal constructor(
|
|
|
69
70
|
passkeysFactory = { rpId -> options.passkeys ?: CredentialManagerPasskeys(activityProvider, rpId) },
|
|
70
71
|
approver = options.approver ?: DialogWalletApprover(activityProvider),
|
|
71
72
|
deviceAuthenticator = if (options.requireDeviceAuthentication) BiometricDeviceAuthenticator(activityProvider) else null,
|
|
73
|
+
probe = AndroidCapabilityProbe(context),
|
|
72
74
|
)
|
|
73
75
|
|
|
74
76
|
private val mutex = Mutex()
|
|
@@ -77,6 +79,17 @@ class AuthonWallet internal constructor(
|
|
|
77
79
|
/** This app's wallet configuration from the API (cached after the first success). */
|
|
78
80
|
suspend fun appConfig(): WalletAppConfig = run { config ?: api.appConfig(packageName).also { config = it } }
|
|
79
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Whether this device and app can use the wallet (NATIVE.md section 9). Needs no session,
|
|
84
|
+
* opens no login or passkey UI, creates nothing, and never throws.
|
|
85
|
+
*/
|
|
86
|
+
suspend fun capabilities(): WalletCapabilities = withContext(dispatcher) {
|
|
87
|
+
val p = probe ?: throw IllegalStateException("no capability probe")
|
|
88
|
+
val (caps, cfg) = Capabilities.evaluate(api, packageName, p)
|
|
89
|
+
if (cfg != null) config = cfg
|
|
90
|
+
caps
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
private class Ready(val ops: WalletOps, val session: String, val config: WalletAppConfig)
|
|
81
94
|
|
|
82
95
|
private suspend fun ready(): Ready {
|
|
@@ -123,7 +136,9 @@ class AuthonWallet internal constructor(
|
|
|
123
136
|
/** Creates the wallet (two-phase; resumes or discards an interrupted attempt), or returns the existing one. */
|
|
124
137
|
suspend fun create(): PublicWallet = op { it.ops.toPublicWallet(it.ops.createWallet(it.session)) }
|
|
125
138
|
|
|
126
|
-
|
|
139
|
+
/** Addresses of accounts start..start+count-1 on [chain] (NATIVE.md section 11). Signing is EVM only. */
|
|
140
|
+
suspend fun accounts(start: Long = 0, count: Int = 1, chain: WalletChain = WalletChain.EVM): List<WalletAccount> =
|
|
141
|
+
op { it.ops.accounts(it.session, start, count, chain) }
|
|
127
142
|
|
|
128
143
|
suspend fun signMessage(message: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
129
144
|
signMessage(WalletMessage.Text(message), account)
|
|
@@ -150,6 +165,17 @@ class AuthonWallet internal constructor(
|
|
|
150
165
|
suspend fun signTransaction(transactionJson: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
151
166
|
op { it.ops.signTransaction(it.session, parseObject(transactionJson), account) }
|
|
152
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Signs an app-built transaction on another chain (NATIVE.md section 12). Solana: payload
|
|
170
|
+
* `message` (serialized legacy/v0 Message, base64, at most 1232 bytes) and optional `cluster`
|
|
171
|
+
* (shown as "Solana <cluster>"); returns `signature` (base58). The signer must be the fee payer,
|
|
172
|
+
* and [address] must be the chain address of [account] (from accounts()). TRON: payload
|
|
173
|
+
* `rawDataHex` (raw_data protobuf, at most 4096 bytes); returns `txID` and `signature`
|
|
174
|
+
* (r||s||v hex); the owner must be the signer.
|
|
175
|
+
*/
|
|
176
|
+
suspend fun signChainTransaction(chain: WalletChain, account: Long, address: String, payload: Map<String, Any?>): Map<String, String> =
|
|
177
|
+
op { it.ops.signChainTransaction(it.session, chain, account, address, payload) }
|
|
178
|
+
|
|
153
179
|
/** Creates the passkey backup (B) of an unprotected wallet. */
|
|
154
180
|
suspend fun protect(): ProtectResult = op {
|
|
155
181
|
requirePasskeyApproval(it.config)
|
|
@@ -186,6 +212,7 @@ class AuthonWallet internal constructor(
|
|
|
186
212
|
passkeys: PasskeyProvider,
|
|
187
213
|
approver: WalletApprover?,
|
|
188
214
|
deviceAuthenticator: DeviceAuthenticator? = null,
|
|
189
|
-
|
|
215
|
+
probe: CapabilityProbe? = null,
|
|
216
|
+
) = AuthonWallet(api, packageName, sessionProvider, store, { passkeys }, approver, deviceAuthenticator, probe)
|
|
190
217
|
}
|
|
191
218
|
}
|