@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.21 → 1.0.0-canary.23
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/ARCHITECTURE.md +5 -5
- package/CHANGELOG.md +17 -0
- package/README.md +11 -7
- package/android/build.gradle +5 -0
- package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +9 -2
- package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +13 -4
- package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +28 -3
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/Credential.kt +22 -1
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/CredentialRepository.kt +335 -101
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/KeystoreRecords.kt +339 -0
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/PasskeyDerivation.kt +84 -0
- package/app.plugin.js +24 -2
- package/build/PasskeyDerivation.d.ts +128 -0
- package/build/PasskeyDerivation.d.ts.map +1 -0
- package/build/PasskeyDerivation.js +147 -0
- package/build/PasskeyDerivation.js.map +1 -0
- package/build/ReactNativePasskeyAutofill.types.d.ts +14 -2
- package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofill.types.js.map +1 -1
- package/build/ReactNativePasskeyAutofillModule.d.ts +19 -1
- package/build/ReactNativePasskeyAutofillModule.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofillModule.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/AutofillCredentialProvider/CredentialProviderViewController.swift +15 -6
- package/ios/AutofillCredentialProvider/PasskeyCredentialStore.swift +359 -39
- package/ios/AutofillCredentialProvider/PasskeyDerivation.swift +88 -0
- package/ios/ReactNativePasskeyAutofillModule.swift +35 -4
- package/package.json +3 -2
- package/src/PasskeyDerivation.ts +170 -0
- package/src/ReactNativePasskeyAutofill.types.ts +14 -2
- package/src/ReactNativePasskeyAutofillModule.ts +19 -1
- package/src/index.ts +1 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -18,7 +18,7 @@ The module provides an interface to interact with native Passkey AutoFill capabi
|
|
|
18
18
|
### TypeScript Layer (`src/`)
|
|
19
19
|
|
|
20
20
|
- `index.ts`: The entry point for the module, re-exporting the native module.
|
|
21
|
-
- `ReactNativePasskeyAutofillModule.ts`: Defines the `ReactNativePasskeyAutofillModule` class and its methods (`setMasterKey`, `
|
|
21
|
+
- `ReactNativePasskeyAutofillModule.ts`: Defines the `ReactNativePasskeyAutofillModule` class and its methods (`setMasterKey`, `setMainKeyId`, `getMainKeyId`, etc.).
|
|
22
22
|
- `ReactNativePasskeyAutofill.types.ts`: Defines types and interfaces used by the module.
|
|
23
23
|
|
|
24
24
|
### Android Implementation (`android/`)
|
|
@@ -58,9 +58,9 @@ The iOS part is implemented using Swift.
|
|
|
58
58
|
|
|
59
59
|
The following methods are exposed to the JavaScript layer:
|
|
60
60
|
|
|
61
|
-
- `setMasterKey(secret:
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
61
|
+
- `setMasterKey(secret: Uint8Array)`: Sets the master key for credential encryption/decryption. Takes raw bytes (not a hex string) so the secret never becomes a non-zeroable JS string.
|
|
62
|
+
- `setMainKeyId(id: string)`: Sets the ID for the P-256 main key (the parent secret for passkey derivation). The legacy `setHdRootKeyId` remains as a deprecated alias.
|
|
63
|
+
- `getMainKeyId()`: Retrieves the current P-256 main key ID.
|
|
64
64
|
- `configureIntentActions(getPasskeyAction: string, createPasskeyAction: string)`: Configures the intent actions used for Passkey flows.
|
|
65
65
|
- `clearCredentials()`: Clears all stored credentials.
|
|
66
66
|
- `isProviderActive()`: Returns `true` if this app is the user-selected system credential/autofill provider. Uses Android's `Settings.Secure("credential_service"[_primary])` (API 34+) and iOS's `ASCredentialIdentityStore.getState`. Useful both for gating passkey UI at runtime and for E2E tests, which need to confirm that an OS passkey prompt is served by _this_ provider rather than any other installed one.
|
|
@@ -68,7 +68,7 @@ The following methods are exposed to the JavaScript layer:
|
|
|
68
68
|
|
|
69
69
|
## Security Considerations
|
|
70
70
|
|
|
71
|
-
As this module handles sensitive information (Passkeys),
|
|
71
|
+
As this module handles sensitive information (Passkeys), keys and secrets are handled securely in the native layers. The master key is stored in platform secure storage — never in plaintext: on Android it is AES/GCM-encrypted under the AndroidKeyStore, and on iOS it is held in the Keychain (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`) within a shared keychain access group so the app and the AutoFill extension can both read it. It is deliberately not biometric-gated, because the extension must read it to enumerate credentials before the user authenticates; biometric verification is applied at the assertion step instead. The master key crosses the JS↔native bridge as raw bytes (`Uint8Array`), never as a hex string.
|
|
72
72
|
|
|
73
73
|
## End-to-End Tests
|
|
74
74
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
# [1.0.0-canary.23](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.22...v1.0.0-canary.23) (2026-08-13)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- keystore migrations ([00f01b6](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/00f01b6ce17842a58e989a71696fe809d969b002))
|
|
6
|
+
|
|
7
|
+
# [1.0.0-canary.22](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.21...v1.0.0-canary.22) (2026-07-15)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- write keychain group into the extension Info.plist so the extension can resolve it ([a98ea08](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/a98ea0827164b5402a3f530579cd6908e52c0242))
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- accept the master key as raw bytes instead of a hex string ([86f85a8](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/86f85a8667489f938266188f7551598c3e14cc0c))
|
|
16
|
+
- store the iOS master key in the Keychain instead of plaintext UserDefaults ([00dd362](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/00dd362d7214e91296982e012aedd817b09ab83d))
|
|
17
|
+
|
|
1
18
|
# [1.0.0-canary.21](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.20...v1.0.0-canary.21) (2026-06-11)
|
|
2
19
|
|
|
3
20
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -100,14 +100,16 @@ For iOS integration, make sure that:
|
|
|
100
100
|
- The generated extension target can link `AuthenticationServices.framework`, `CryptoKit.framework`, `MMKVCore`, and the deterministic P-256 Swift package.
|
|
101
101
|
- `NSFaceIDUsageDescription` is present when biometric authentication is used.
|
|
102
102
|
|
|
103
|
-
At runtime, the app must provide the native side with the master key, identify the
|
|
103
|
+
At runtime, the app must provide the native side with the master key, identify the P-256 main key (the parent secret for passkey derivation) stored in MMKV, and keep the iOS identity store in sync:
|
|
104
104
|
|
|
105
105
|
```typescript
|
|
106
|
-
await ReactNativePasskeyAutofill.setMasterKey(
|
|
107
|
-
await ReactNativePasskeyAutofill.
|
|
106
|
+
await ReactNativePasskeyAutofill.setMasterKey(masterKeyBytes);
|
|
107
|
+
await ReactNativePasskeyAutofill.setMainKeyId(mainKeyId);
|
|
108
108
|
await ReactNativePasskeyAutofill.refreshCredentialIdentities();
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
The `mainKeyId` points at the keystore record whose sealed material is the 64-byte parent secret. The native side determines the derivation scheme from the record's metadata (`pbkdf2-p256` is preferred; legacy `bip32-ed25519` is also supported).
|
|
112
|
+
|
|
111
113
|
Call `refreshCredentialIdentities()` after creating, importing, deleting, or restoring passkeys so iOS AutoFill sees the current credentials.
|
|
112
114
|
|
|
113
115
|
## Usage
|
|
@@ -115,11 +117,13 @@ Call `refreshCredentialIdentities()` after creating, importing, deleting, or res
|
|
|
115
117
|
```typescript
|
|
116
118
|
import ReactNativePasskeyAutofill from "@algorandfoundation/react-native-passkey-autofill";
|
|
117
119
|
|
|
118
|
-
// 1. Set the master key for encryption (hex string
|
|
119
|
-
|
|
120
|
+
// 1. Set the master key for encryption (raw bytes — never a hex string, so
|
|
121
|
+
// the secret isn't materialized as a non-zeroable JS string)
|
|
122
|
+
await ReactNativePasskeyAutofill.setMasterKey(masterKeyBytes);
|
|
120
123
|
|
|
121
|
-
// 2. Set the
|
|
122
|
-
|
|
124
|
+
// 2. Set the P-256 main key ID (parent secret for passkey derivation).
|
|
125
|
+
// The legacy `setHdRootKeyId(id)` is a deprecated alias for this method.
|
|
126
|
+
await ReactNativePasskeyAutofill.setMainKeyId(mainKeyId);
|
|
123
127
|
|
|
124
128
|
// 3. Configure intent actions for the Passkey flows
|
|
125
129
|
await ReactNativePasskeyAutofill.configureIntentActions(
|
package/android/build.gradle
CHANGED
|
@@ -80,6 +80,11 @@ dependencies {
|
|
|
80
80
|
|
|
81
81
|
// Testing dependencies
|
|
82
82
|
testImplementation "junit:junit:4.13.2"
|
|
83
|
+
// `unitTests.returnDefaultValues` stubs the whole android.jar, and that
|
|
84
|
+
// includes `org.json` — every JSONObject method then returns null, which the
|
|
85
|
+
// record-format tests cannot work with. Put the real implementation on the
|
|
86
|
+
// unit-test classpath instead.
|
|
87
|
+
testImplementation "org.json:json:20240303"
|
|
83
88
|
testImplementation "org.robolectric:robolectric:4.11.1"
|
|
84
89
|
testImplementation "io.mockk:mockk:1.13.8"
|
|
85
90
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0"
|
|
@@ -342,7 +342,12 @@ class CreatePasskeyActivity : AppCompatActivity() {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
Log.d(TAG, "Creating deterministic key pair")
|
|
345
|
-
|
|
345
|
+
// No requested scheme: a new credential takes the preferred parent
|
|
346
|
+
// (the wallet's deterministic-P256 main key), and records which one
|
|
347
|
+
// it got so every later assertion re-derives the same key.
|
|
348
|
+
val derived = credentialRepository.createDomainKeyPair(this@CreatePasskeyActivity, origin, userHandle)
|
|
349
|
+
val keyPair: KeyPair = derived.keyPair
|
|
350
|
+
Log.d(TAG, "Derived from parent ${derived.parentKeyId} (${derived.derivationScheme})")
|
|
346
351
|
Log.d(TAG, "Generating credential ID")
|
|
347
352
|
val credentialId = credentialRepository.generateCredentialId(keyPair)
|
|
348
353
|
val credentialIdBase64 = AndroidBase64.encodeToString(credentialId, AndroidBase64.NO_WRAP)
|
|
@@ -354,7 +359,9 @@ class CreatePasskeyActivity : AppCompatActivity() {
|
|
|
354
359
|
userId = userId,
|
|
355
360
|
publicKey = AndroidBase64.encodeToString(keyPair.public.encoded, AndroidBase64.NO_WRAP),
|
|
356
361
|
privateKey = AndroidBase64.encodeToString(keyPair.private.encoded, AndroidBase64.NO_WRAP),
|
|
357
|
-
count = 0
|
|
362
|
+
count = 0,
|
|
363
|
+
parentKeyId = derived.parentKeyId,
|
|
364
|
+
derivationScheme = derived.derivationScheme
|
|
358
365
|
)
|
|
359
366
|
|
|
360
367
|
Log.d(TAG, "Saving credential to repository")
|
|
@@ -26,6 +26,8 @@ import androidx.credentials.webauthn.PublicKeyCredentialRequestOptions
|
|
|
26
26
|
import co.algorand.passkeyautofill.auth.BiometricRequirement
|
|
27
27
|
import co.algorand.passkeyautofill.credentials.CredentialRepository
|
|
28
28
|
import co.algorand.passkeyautofill.credentials.Credential
|
|
29
|
+
import co.algorand.passkeyautofill.credentials.KeystoreRecords
|
|
30
|
+
import co.algorand.passkeyautofill.credentials.ParentSecretResult
|
|
29
31
|
import co.algorand.passkeyautofill.utils.PasskeyUtils
|
|
30
32
|
import java.security.KeyPair
|
|
31
33
|
import java.security.MessageDigest
|
|
@@ -584,12 +586,19 @@ class GetPasskeyActivity : AppCompatActivity() {
|
|
|
584
586
|
)
|
|
585
587
|
val prfInput = Prf.parseInput(passkeyRequestJsonObj, credentialIdB64Url)
|
|
586
588
|
if (prfInput != null) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
589
|
+
// PRF is recomputed from the parent secret on EVERY assertion,
|
|
590
|
+
// so it must use the very parent this credential was created
|
|
591
|
+
// against — an unstamped credential predates the dp256 main
|
|
592
|
+
// key and is pinned to the BIP32-Ed25519 root.
|
|
593
|
+
val parent = credentialRepository.resolveParentSecret(
|
|
594
|
+
this@GetPasskeyActivity,
|
|
595
|
+
dbCred.derivationScheme ?: KeystoreRecords.SCHEME_BIP32_ED25519,
|
|
596
|
+
)
|
|
597
|
+
if (parent !is ParentSecretResult.Available) {
|
|
598
|
+
Log.w(TAG, "PRF input present but parent secret unavailable (${parent.reason}); skipping PRF output")
|
|
590
599
|
} else {
|
|
591
600
|
val credRandom = Prf.credRandom(
|
|
592
|
-
hdRootSecret =
|
|
601
|
+
hdRootSecret = parent.secret.bytes,
|
|
593
602
|
relyingPartyIdentifier = displayOrigin,
|
|
594
603
|
userHandle = dbCred.userHandle,
|
|
595
604
|
)
|
package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt
CHANGED
|
@@ -45,7 +45,7 @@ class ReactNativePasskeyAutofillModule : Module() {
|
|
|
45
45
|
|
|
46
46
|
Events("onPasskeyAdded", "onPasskeyAuthenticated")
|
|
47
47
|
|
|
48
|
-
AsyncFunction("setMasterKey") { secret:
|
|
48
|
+
AsyncFunction("setMasterKey") { secret: ByteArray ->
|
|
49
49
|
val context = (appContext.reactContext ?: appContext.hostingRuntimeContext) as? Context
|
|
50
50
|
if (context != null) {
|
|
51
51
|
credentialRepository.saveMasterKey(context, secret)
|
|
@@ -54,10 +54,34 @@ class ReactNativePasskeyAutofillModule : Module() {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Points the passkey hierarchy at the wallet's deterministic-P256 main key.
|
|
58
|
+
// The scheme is not a parameter: it is read from the record's own metadata,
|
|
59
|
+
// so a wallet cannot mislabel which hierarchy it handed us.
|
|
60
|
+
AsyncFunction("setMainKeyId") { id: String ->
|
|
61
|
+
val context = (appContext.reactContext ?: appContext.hostingRuntimeContext) as? Context
|
|
62
|
+
if (context != null) {
|
|
63
|
+
credentialRepository.saveMainKeyId(context, id)
|
|
64
|
+
} else {
|
|
65
|
+
Log.e(CredentialRepository.TAG, "Could not get context to save main key ID")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
AsyncFunction("getMainKeyId") {
|
|
70
|
+
val context = (appContext.reactContext ?: appContext.hostingRuntimeContext) as? Context
|
|
71
|
+
if (context != null) {
|
|
72
|
+
credentialRepository.getMainKeyId(context)
|
|
73
|
+
} else {
|
|
74
|
+
Log.e(CredentialRepository.TAG, "Could not get context to get main key ID")
|
|
75
|
+
null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Deprecated aliases of the two above, kept because installed wallets still
|
|
80
|
+
// call them. They address the same slot — see `saveMainKeyId`.
|
|
57
81
|
AsyncFunction("setHdRootKeyId") { id: String ->
|
|
58
82
|
val context = (appContext.reactContext ?: appContext.hostingRuntimeContext) as? Context
|
|
59
83
|
if (context != null) {
|
|
60
|
-
credentialRepository.
|
|
84
|
+
credentialRepository.saveMainKeyId(context, id)
|
|
61
85
|
} else {
|
|
62
86
|
Log.e(CredentialRepository.TAG, "Could not get context to save HD root key ID")
|
|
63
87
|
}
|
|
@@ -66,7 +90,7 @@ class ReactNativePasskeyAutofillModule : Module() {
|
|
|
66
90
|
AsyncFunction("getHdRootKeyId") {
|
|
67
91
|
val context = (appContext.reactContext ?: appContext.hostingRuntimeContext) as? Context
|
|
68
92
|
if (context != null) {
|
|
69
|
-
credentialRepository.
|
|
93
|
+
credentialRepository.getMainKeyId(context)
|
|
70
94
|
} else {
|
|
71
95
|
Log.e(CredentialRepository.TAG, "Could not get context to get HD root key ID")
|
|
72
96
|
null
|
|
@@ -141,6 +165,7 @@ class ReactNativePasskeyAutofillModule : Module() {
|
|
|
141
165
|
"userName" to credential.userHandle,
|
|
142
166
|
"userHandle" to credential.userHandle,
|
|
143
167
|
"publicKey" to credential.publicKey,
|
|
168
|
+
"derivationScheme" to credential.derivationScheme,
|
|
144
169
|
)
|
|
145
170
|
}
|
|
146
171
|
}
|
|
@@ -8,5 +8,26 @@ data class Credential(
|
|
|
8
8
|
val publicKey: String,
|
|
9
9
|
val privateKey: String,
|
|
10
10
|
val count: Int,
|
|
11
|
-
val biometricIv: String? = null
|
|
11
|
+
val biometricIv: String? = null,
|
|
12
|
+
/** The root record this credential's key was derived from, when known. */
|
|
13
|
+
val parentKeyId: String? = null,
|
|
14
|
+
/**
|
|
15
|
+
* The derivation scheme this credential is pinned to for life
|
|
16
|
+
* ([KeystoreRecords.SCHEME_PBKDF2_P256] or
|
|
17
|
+
* [KeystoreRecords.SCHEME_BIP32_ED25519]).
|
|
18
|
+
*
|
|
19
|
+
* `null` on every credential created before the wallet exposed its
|
|
20
|
+
* deterministic-P256 main key, and those all derive from the BIP32-Ed25519
|
|
21
|
+
* root — re-deriving one against a different parent would produce a
|
|
22
|
+
* different key and silently break the passkey the relying party already
|
|
23
|
+
* trusts.
|
|
24
|
+
*/
|
|
25
|
+
val derivationScheme: String? = null,
|
|
26
|
+
/**
|
|
27
|
+
* Which identity string this credential's material is derived from (see
|
|
28
|
+
* [PasskeyDerivation]). Records written before the field existed read back as
|
|
29
|
+
* [PasskeyDerivation.VERSION_LEGACY_LABEL], leaving their derivation
|
|
30
|
+
* unchanged.
|
|
31
|
+
*/
|
|
32
|
+
val derivationVersion: Int = PasskeyDerivation.VERSION_LEGACY_LABEL,
|
|
12
33
|
)
|