@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.2 → 1.0.0-canary.21
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/.nvmrc +1 -0
- package/.oxfmtrc.json +4 -0
- package/.oxlintrc.json +14 -0
- package/.releaserc +30 -0
- package/ARCHITECTURE.md +8 -0
- package/CHANGELOG.md +115 -0
- package/CONTRIBUTING.md +15 -5
- package/README.md +150 -11
- package/android/build.gradle +33 -10
- package/android/libs/repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.pom +9 -0
- package/android/src/main/AndroidManifest.xml +9 -3
- package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +396 -44
- package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +583 -44
- package/android/src/main/java/co/algorand/passkeyautofill/Prf.kt +184 -0
- package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +190 -2
- package/android/src/main/java/co/algorand/passkeyautofill/WebAuthn.kt +135 -0
- package/android/src/main/java/co/algorand/passkeyautofill/auth/BiometricRequirement.kt +86 -0
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/Credential.kt +2 -1
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/CredentialRepository.kt +314 -53
- package/android/src/main/java/co/algorand/passkeyautofill/service/PasskeyAutofillCredentialProviderService.kt +152 -38
- package/android/src/main/java/co/algorand/passkeyautofill/utils/PasskeyUtils.kt +119 -0
- package/android/src/main/res/values/styles.xml +10 -0
- package/app.plugin.js +593 -60
- package/assets/banner.png +0 -0
- package/build/ReactNativePasskeyAutofill.types.d.ts +41 -1
- package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofill.types.js +3 -1
- package/build/ReactNativePasskeyAutofill.types.js.map +1 -1
- package/build/ReactNativePasskeyAutofillModule.d.ts +41 -4
- package/build/ReactNativePasskeyAutofillModule.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofillModule.js +21 -2
- package/build/ReactNativePasskeyAutofillModule.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/e2e/README.md +155 -0
- package/e2e/driver/adb.ts +73 -0
- package/e2e/driver/capabilities.ts +106 -0
- package/e2e/driver/credentialProvider.ts +104 -0
- package/e2e/driver/fingerprint.ts +885 -0
- package/e2e/driver/selectors.ts +103 -0
- package/e2e/driver/session.ts +25 -0
- package/e2e/jest.config.ts +20 -0
- package/e2e/package.json +11 -0
- package/e2e/tests/passkey.test.ts +190 -0
- package/e2e/tsconfig.json +16 -0
- package/ios/AutofillCredentialProvider/BiometricRequirement.swift +34 -0
- package/ios/AutofillCredentialProvider/CredentialProviderViewController.swift +570 -0
- package/ios/AutofillCredentialProvider/PasskeyAutofillCredentialProvider-Bridging-Header.h +1 -0
- package/ios/AutofillCredentialProvider/PasskeyCredentialStore.swift +612 -0
- package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.h +25 -0
- package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.mm +86 -0
- package/ios/AutofillCredentialProvider/Prf.swift +89 -0
- package/ios/AutofillCredentialProvider/WebAuthn.swift +186 -0
- package/ios/ReactNativePasskeyAutofill.podspec +11 -2
- package/ios/ReactNativePasskeyAutofillModule.swift +170 -15
- package/lefthook.yml +14 -0
- package/package.json +115 -25
- package/pnpm-workspace.yaml +36 -0
- package/src/ReactNativePasskeyAutofill.types.ts +38 -0
- package/src/ReactNativePasskeyAutofillModule.ts +66 -3
- package/src/index.ts +2 -2
- package/tsconfig.json +2 -2
- package/.eslintrc.js +0 -5
- /package/android/libs/{dP256Android-release.aar → repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.aar} +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#import "PasskeyKeystoreMMKV.h"
|
|
2
|
+
|
|
3
|
+
#import <MMKVCore/MMKV.h>
|
|
4
|
+
|
|
5
|
+
@implementation PasskeyKeystoreMMKV
|
|
6
|
+
|
|
7
|
+
+ (nullable mmkv::MMKV *)keystoreForAppGroup:(NSString *)appGroup
|
|
8
|
+
error:(NSError * _Nullable * _Nullable)error {
|
|
9
|
+
NSURL *containerURL = [[NSFileManager defaultManager]
|
|
10
|
+
containerURLForSecurityApplicationGroupIdentifier:appGroup];
|
|
11
|
+
if (containerURL == nil) {
|
|
12
|
+
if (error != nil) {
|
|
13
|
+
*error = [NSError errorWithDomain:@"ReactNativePasskeyAutofill"
|
|
14
|
+
code:2
|
|
15
|
+
userInfo:@{
|
|
16
|
+
NSLocalizedDescriptionKey:
|
|
17
|
+
[NSString stringWithFormat:@"App Group is not accessible: %@", appGroup]
|
|
18
|
+
}];
|
|
19
|
+
}
|
|
20
|
+
return nil;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
std::string rootPath([containerURL.path UTF8String]);
|
|
24
|
+
mmkv::MMKV::initializeMMKV(rootPath);
|
|
25
|
+
return mmkv::MMKV::mmkvWithID("keystore", mmkv::MMKV_MULTI_PROCESS, nullptr, &rootPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
+ (BOOL)setString:(NSString *)value
|
|
29
|
+
forKey:(NSString *)key
|
|
30
|
+
appGroup:(NSString *)appGroup
|
|
31
|
+
error:(NSError * _Nullable * _Nullable)error {
|
|
32
|
+
mmkv::MMKV *keystore = [self keystoreForAppGroup:appGroup error:error];
|
|
33
|
+
if (keystore == nullptr) {
|
|
34
|
+
return NO;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
std::string cppValue([value UTF8String]);
|
|
38
|
+
std::string cppKey([key UTF8String]);
|
|
39
|
+
return keystore->set(cppValue, cppKey);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
+ (nullable NSString *)stringForKey:(NSString *)key
|
|
43
|
+
appGroup:(NSString *)appGroup
|
|
44
|
+
error:(NSError * _Nullable * _Nullable)error {
|
|
45
|
+
mmkv::MMKV *keystore = [self keystoreForAppGroup:appGroup error:error];
|
|
46
|
+
if (keystore == nullptr) {
|
|
47
|
+
return nil;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
std::string result;
|
|
51
|
+
std::string cppKey([key UTF8String]);
|
|
52
|
+
if (!keystore->getString(cppKey, result)) {
|
|
53
|
+
return nil;
|
|
54
|
+
}
|
|
55
|
+
return [NSString stringWithUTF8String:result.c_str()];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
+ (NSArray<NSString *> *)allKeysForAppGroup:(NSString *)appGroup
|
|
59
|
+
error:(NSError * _Nullable * _Nullable)error {
|
|
60
|
+
mmkv::MMKV *keystore = [self keystoreForAppGroup:appGroup error:error];
|
|
61
|
+
if (keystore == nullptr) {
|
|
62
|
+
return @[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
std::vector<std::string> keys = keystore->allKeys();
|
|
66
|
+
NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:keys.size()];
|
|
67
|
+
for (const auto &key : keys) {
|
|
68
|
+
[result addObject:[NSString stringWithUTF8String:key.c_str()]];
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
+ (BOOL)removeValueForKey:(NSString *)key
|
|
74
|
+
appGroup:(NSString *)appGroup
|
|
75
|
+
error:(NSError * _Nullable * _Nullable)error {
|
|
76
|
+
mmkv::MMKV *keystore = [self keystoreForAppGroup:appGroup error:error];
|
|
77
|
+
if (keystore == nullptr) {
|
|
78
|
+
return NO;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
std::string cppKey([key UTF8String]);
|
|
82
|
+
keystore->removeValueForKey(cppKey);
|
|
83
|
+
return YES;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// WebAuthn PRF (`prf`) extension helpers, backed by the authenticator
|
|
5
|
+
/// `hmac-secret` mechanism (CTAP2.1).
|
|
6
|
+
///
|
|
7
|
+
/// Per the WebAuthn spec, for each input salt the authenticator computes:
|
|
8
|
+
///
|
|
9
|
+
/// output = HMAC-SHA256(credRandom, SHA256("WebAuthn PRF" || 0x00 || salt))
|
|
10
|
+
///
|
|
11
|
+
/// where `credRandom` is a 32-byte per-credential secret. To avoid a storage
|
|
12
|
+
/// migration we derive `credRandom` deterministically from the wallet's HD
|
|
13
|
+
/// root secret and the credential's (rpId, userHandle) — the same inputs that
|
|
14
|
+
/// already produce the deterministic P-256 signing key. As a result, restoring
|
|
15
|
+
/// the wallet seed on another device produces the same PRF outputs.
|
|
16
|
+
enum Prf {
|
|
17
|
+
/// Domain separator used to derive `credRandom` from the HD root secret.
|
|
18
|
+
/// Kept under a distinct label from the P-256 key derivation to ensure
|
|
19
|
+
/// the credRandom value can never collide with private-key material.
|
|
20
|
+
static let credRandomInfo = "WebAuthn-PRF-credRandom"
|
|
21
|
+
|
|
22
|
+
/// WebAuthn-defined prefix for PRF salt hashing.
|
|
23
|
+
/// See: https://www.w3.org/TR/webauthn-3/#prf-extension
|
|
24
|
+
static let saltPrefix: [UInt8] = Array("WebAuthn PRF".utf8) + [0x00]
|
|
25
|
+
|
|
26
|
+
/// Derive a 32-byte per-credential PRF secret (`credRandom`) from the wallet
|
|
27
|
+
/// HD root secret. Deterministic over `(hdRootSecret, rpId, userHandle)`.
|
|
28
|
+
static func credRandom(
|
|
29
|
+
hdRootSecret: Data,
|
|
30
|
+
relyingPartyIdentifier: String,
|
|
31
|
+
userHandle: String
|
|
32
|
+
) -> Data {
|
|
33
|
+
var salt = Data()
|
|
34
|
+
salt.append(contentsOf: relyingPartyIdentifier.lowercased().utf8)
|
|
35
|
+
salt.append(0x00)
|
|
36
|
+
salt.append(contentsOf: userHandle.lowercased().utf8)
|
|
37
|
+
|
|
38
|
+
let info = Data(credRandomInfo.utf8)
|
|
39
|
+
let key = SymmetricKey(data: hdRootSecret)
|
|
40
|
+
let derived = HKDF<SHA256>.deriveKey(
|
|
41
|
+
inputKeyMaterial: key,
|
|
42
|
+
salt: salt,
|
|
43
|
+
info: info,
|
|
44
|
+
outputByteCount: 32
|
|
45
|
+
)
|
|
46
|
+
return derived.withUnsafeBytes { Data($0) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Evaluate the PRF for a single salt. Returns 32 bytes of output material.
|
|
50
|
+
///
|
|
51
|
+
/// Per the WebAuthn spec the PRF input the authenticator sees is the
|
|
52
|
+
/// hash `SHA-256("WebAuthn PRF" || 0x00 || rpSalt)`. The platform layer
|
|
53
|
+
/// (Apple AuthenticationServices on iOS, the system credential manager on
|
|
54
|
+
/// Android with `prfAlreadyHashed`) performs this hashing before passing
|
|
55
|
+
/// the salt down to credential providers. Pass `alreadyHashed = true` in
|
|
56
|
+
/// that case so we don't hash twice.
|
|
57
|
+
static func evaluate(credRandom: Data, salt: Data, alreadyHashed: Bool = true) -> Data {
|
|
58
|
+
let macInput: Data
|
|
59
|
+
if alreadyHashed {
|
|
60
|
+
macInput = salt
|
|
61
|
+
} else {
|
|
62
|
+
var hashInput = Data(saltPrefix)
|
|
63
|
+
hashInput.append(salt)
|
|
64
|
+
macInput = Data(SHA256.hash(data: hashInput))
|
|
65
|
+
}
|
|
66
|
+
let mac = HMAC<SHA256>.authenticationCode(
|
|
67
|
+
for: macInput,
|
|
68
|
+
using: SymmetricKey(data: credRandom)
|
|
69
|
+
)
|
|
70
|
+
return Data(mac)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Parsed PRF extension input as supplied by the relying party.
|
|
75
|
+
///
|
|
76
|
+
/// At the WebAuthn layer the RP can either supply a single global pair of
|
|
77
|
+
/// salts (`eval`) or a per-credential map (`evalByCredential`). We always
|
|
78
|
+
/// reduce that down to at most two salts for the credential we are about to
|
|
79
|
+
/// assert with, matching the platform behaviour on iOS 18 and Android.
|
|
80
|
+
struct PrfInput {
|
|
81
|
+
let first: Data
|
|
82
|
+
let second: Data?
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Computed PRF outputs ready to attach to a WebAuthn response.
|
|
86
|
+
struct PrfOutput {
|
|
87
|
+
let first: Data
|
|
88
|
+
let second: Data?
|
|
89
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
enum WebAuthnError: LocalizedError {
|
|
5
|
+
case invalidP256PublicKeyLength(Int)
|
|
6
|
+
|
|
7
|
+
var errorDescription: String? {
|
|
8
|
+
switch self {
|
|
9
|
+
case .invalidP256PublicKeyLength(let length):
|
|
10
|
+
return "Invalid P-256 public key length: \(length)."
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
enum WebAuthn {
|
|
16
|
+
private static let aaguidInfoDictionaryKey = "ReactNativePasskeyAutofillAAGUID"
|
|
17
|
+
private static let defaultAaguid = UUID(uuidString: "1F59713A-C021-4E63-9158-2CC5FDC14E52")!
|
|
18
|
+
|
|
19
|
+
// Injected by the Expo config plugin (`aaguid` prop) via the extension Info.plist so all
|
|
20
|
+
// platforms can present the same authenticator identity; falls back to the module default.
|
|
21
|
+
static let aaguid: UUID = {
|
|
22
|
+
guard
|
|
23
|
+
let raw = Bundle.main.object(forInfoDictionaryKey: aaguidInfoDictionaryKey) as? String,
|
|
24
|
+
let configured = UUID(uuidString: raw)
|
|
25
|
+
else {
|
|
26
|
+
return defaultAaguid
|
|
27
|
+
}
|
|
28
|
+
return configured
|
|
29
|
+
}()
|
|
30
|
+
|
|
31
|
+
static func authenticatorDataForAssertion(relyingPartyIdentifier: String) -> Data {
|
|
32
|
+
var data = Data(SHA256.hash(data: Data(relyingPartyIdentifier.utf8)))
|
|
33
|
+
data.append(flags(attestedCredentialDataIncluded: false))
|
|
34
|
+
data.append(signCount())
|
|
35
|
+
return data
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static func authenticatorDataForAttestation(
|
|
39
|
+
relyingPartyIdentifier: String,
|
|
40
|
+
credentialId: Data,
|
|
41
|
+
publicKey: Data
|
|
42
|
+
) throws -> Data {
|
|
43
|
+
var data = Data(SHA256.hash(data: Data(relyingPartyIdentifier.utf8)))
|
|
44
|
+
data.append(flags(attestedCredentialDataIncluded: true))
|
|
45
|
+
data.append(signCount())
|
|
46
|
+
data.append(try attestedCredentialData(credentialId: credentialId, publicKey: publicKey))
|
|
47
|
+
return data
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static func attestationObject(authenticatorData: Data) -> Data {
|
|
51
|
+
Cbor.encodeMap([
|
|
52
|
+
(Cbor.encodeText("fmt"), Cbor.encodeText("none")),
|
|
53
|
+
(Cbor.encodeText("attStmt"), Cbor.encodeMap([])),
|
|
54
|
+
(Cbor.encodeText("authData"), Cbor.encodeBytes(authenticatorData)),
|
|
55
|
+
])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static func credentialId(publicKey: Data) -> Data {
|
|
59
|
+
Data(SHA256.hash(data: publicKey))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static func flags(attestedCredentialDataIncluded: Bool) -> Data {
|
|
63
|
+
var flags: UInt8 = 0x01
|
|
64
|
+
flags |= 0x04
|
|
65
|
+
flags |= 0x08
|
|
66
|
+
flags |= 0x10
|
|
67
|
+
if attestedCredentialDataIncluded {
|
|
68
|
+
flags |= 0x40
|
|
69
|
+
}
|
|
70
|
+
return Data([flags])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static func signCount() -> Data {
|
|
74
|
+
Data([0x00, 0x00, 0x00, 0x00])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static func attestedCredentialData(credentialId: Data, publicKey: Data) throws -> Data {
|
|
78
|
+
var data = aaguid.bytes
|
|
79
|
+
data.append(UInt8((credentialId.count >> 8) & 0xff))
|
|
80
|
+
data.append(UInt8(credentialId.count & 0xff))
|
|
81
|
+
data.append(credentialId)
|
|
82
|
+
data.append(try coseKey(publicKey: publicKey))
|
|
83
|
+
return data
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static func coseKey(publicKey: Data) throws -> Data {
|
|
87
|
+
let coordinates = try p256Coordinates(publicKey: publicKey)
|
|
88
|
+
|
|
89
|
+
return Cbor.encodeMap([
|
|
90
|
+
(Cbor.encodeInt(1), Cbor.encodeInt(2)),
|
|
91
|
+
(Cbor.encodeInt(3), Cbor.encodeInt(-7)),
|
|
92
|
+
(Cbor.encodeInt(-1), Cbor.encodeInt(1)),
|
|
93
|
+
(Cbor.encodeInt(-2), Cbor.encodeBytes(coordinates.x)),
|
|
94
|
+
(Cbor.encodeInt(-3), Cbor.encodeBytes(coordinates.y)),
|
|
95
|
+
])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private static func p256Coordinates(publicKey: Data) throws -> (x: Data, y: Data) {
|
|
99
|
+
if let derPublicKey = try? P256.Signing.PublicKey(derRepresentation: publicKey) {
|
|
100
|
+
return try p256Coordinates(publicKey: derPublicKey.x963Representation)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if publicKey.count == 65, publicKey.first == 0x04 {
|
|
104
|
+
let coordinates = publicKey.dropFirst()
|
|
105
|
+
return (
|
|
106
|
+
Data(coordinates.prefix(32)),
|
|
107
|
+
Data(coordinates.dropFirst(32).prefix(32))
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if publicKey.count == 64 {
|
|
112
|
+
return (
|
|
113
|
+
Data(publicKey.prefix(32)),
|
|
114
|
+
Data(publicKey.dropFirst(32).prefix(32))
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw WebAuthnError.invalidP256PublicKeyLength(publicKey.count)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private enum Cbor {
|
|
123
|
+
static func encodeInt(_ value: Int) -> Data {
|
|
124
|
+
if value >= 0 {
|
|
125
|
+
return encodeUnsigned(UInt64(value), majorType: 0)
|
|
126
|
+
}
|
|
127
|
+
return encodeUnsigned(UInt64(-1 - value), majorType: 1)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static func encodeBytes(_ bytes: Data) -> Data {
|
|
131
|
+
encodeUnsigned(UInt64(bytes.count), majorType: 2) + bytes
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static func encodeText(_ string: String) -> Data {
|
|
135
|
+
let bytes = Data(string.utf8)
|
|
136
|
+
return encodeUnsigned(UInt64(bytes.count), majorType: 3) + bytes
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static func encodeMap(_ pairs: [(Data, Data)]) -> Data {
|
|
140
|
+
var data = encodeUnsigned(UInt64(pairs.count), majorType: 5)
|
|
141
|
+
for (key, value) in pairs {
|
|
142
|
+
data.append(key)
|
|
143
|
+
data.append(value)
|
|
144
|
+
}
|
|
145
|
+
return data
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private static func encodeUnsigned(_ value: UInt64, majorType: UInt8) -> Data {
|
|
149
|
+
let prefix = majorType << 5
|
|
150
|
+
switch value {
|
|
151
|
+
case 0..<24:
|
|
152
|
+
return Data([prefix | UInt8(value)])
|
|
153
|
+
case 24..<256:
|
|
154
|
+
return Data([prefix | 24, UInt8(value)])
|
|
155
|
+
case 256..<65_536:
|
|
156
|
+
return Data([prefix | 25, UInt8((value >> 8) & 0xff), UInt8(value & 0xff)])
|
|
157
|
+
case 65_536..<4_294_967_296:
|
|
158
|
+
return Data([
|
|
159
|
+
prefix | 26,
|
|
160
|
+
UInt8((value >> 24) & 0xff),
|
|
161
|
+
UInt8((value >> 16) & 0xff),
|
|
162
|
+
UInt8((value >> 8) & 0xff),
|
|
163
|
+
UInt8(value & 0xff),
|
|
164
|
+
])
|
|
165
|
+
default:
|
|
166
|
+
return Data([
|
|
167
|
+
prefix | 27,
|
|
168
|
+
UInt8((value >> 56) & 0xff),
|
|
169
|
+
UInt8((value >> 48) & 0xff),
|
|
170
|
+
UInt8((value >> 40) & 0xff),
|
|
171
|
+
UInt8((value >> 32) & 0xff),
|
|
172
|
+
UInt8((value >> 24) & 0xff),
|
|
173
|
+
UInt8((value >> 16) & 0xff),
|
|
174
|
+
UInt8((value >> 8) & 0xff),
|
|
175
|
+
UInt8(value & 0xff),
|
|
176
|
+
])
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private extension UUID {
|
|
182
|
+
var bytes: Data {
|
|
183
|
+
var uuid = uuid
|
|
184
|
+
return withUnsafeBytes(of: &uuid) { Data($0) }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -19,11 +19,20 @@ Pod::Spec.new do |s|
|
|
|
19
19
|
s.static_framework = true
|
|
20
20
|
|
|
21
21
|
s.dependency 'ExpoModulesCore'
|
|
22
|
+
s.dependency 'MMKVCore'
|
|
22
23
|
|
|
23
|
-
# Swift/Objective-C compatibility
|
|
24
24
|
s.pod_target_xcconfig = {
|
|
25
25
|
'DEFINES_MODULE' => 'YES',
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
s.source_files =
|
|
28
|
+
s.source_files = [
|
|
29
|
+
"*.{h,m,mm,swift,hpp,cpp}",
|
|
30
|
+
"AutofillCredentialProvider/PasskeyAutofillCredentialProvider-Bridging-Header.h",
|
|
31
|
+
"AutofillCredentialProvider/PasskeyCredentialStore.swift",
|
|
32
|
+
"AutofillCredentialProvider/PasskeyKeystoreMMKV.h",
|
|
33
|
+
"AutofillCredentialProvider/PasskeyKeystoreMMKV.mm"
|
|
34
|
+
]
|
|
35
|
+
s.public_header_files = [
|
|
36
|
+
"AutofillCredentialProvider/PasskeyKeystoreMMKV.h"
|
|
37
|
+
]
|
|
29
38
|
end
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
|
+
import AuthenticationServices
|
|
3
|
+
#if canImport(UIKit)
|
|
4
|
+
import UIKit
|
|
5
|
+
#endif
|
|
2
6
|
|
|
3
7
|
public class ReactNativePasskeyAutofillModule: Module {
|
|
4
8
|
// Each module class must implement the definition function. The definition consists of components
|
|
@@ -10,26 +14,177 @@ public class ReactNativePasskeyAutofillModule: Module {
|
|
|
10
14
|
// The module will be accessible from `requireNativeModule('ReactNativePasskeyAutofill')` in JavaScript.
|
|
11
15
|
Name("ReactNativePasskeyAutofill")
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
Events("onPasskeyAdded", "onPasskeyAuthenticated")
|
|
18
|
+
|
|
19
|
+
AsyncFunction("setMasterKey") { (secret: String) in
|
|
20
|
+
guard let store = PasskeyCredentialStore() else {
|
|
21
|
+
throw NSError(
|
|
22
|
+
domain: "ReactNativePasskeyAutofill",
|
|
23
|
+
code: 1,
|
|
24
|
+
userInfo: [NSLocalizedDescriptionKey: "App Group is not configured for passkey autofill."]
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
store.saveMasterKey(secret)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
AsyncFunction("setHdRootKeyId") { (id: String) in
|
|
31
|
+
guard let store = PasskeyCredentialStore() else {
|
|
32
|
+
throw NSError(
|
|
33
|
+
domain: "ReactNativePasskeyAutofill",
|
|
34
|
+
code: 1,
|
|
35
|
+
userInfo: [NSLocalizedDescriptionKey: "App Group is not configured for passkey autofill."]
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
store.saveHdRootKeyId(id)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
AsyncFunction("getHdRootKeyId") { () -> String? in
|
|
42
|
+
guard let store = PasskeyCredentialStore() else {
|
|
43
|
+
return nil
|
|
44
|
+
}
|
|
45
|
+
return store.hdRootKeyId()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
AsyncFunction("clearCredentials") {
|
|
49
|
+
guard let store = PasskeyCredentialStore() else {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
store.clear()
|
|
53
|
+
try await store.removeAllIdentities()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
AsyncFunction("deleteCredential") { (credentialId: String) in
|
|
57
|
+
guard let store = PasskeyCredentialStore() else {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
try store.removeCredential(id: credentialId)
|
|
61
|
+
try await store.replaceIdentityStore()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
AsyncFunction("configureIntentActions") { (getPasskeyAction: String, createPasskeyAction: String) in
|
|
65
|
+
guard let store = PasskeyCredentialStore() else {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
store.configureIntentActions(
|
|
69
|
+
getPasskeyAction: getPasskeyAction,
|
|
70
|
+
createPasskeyAction: createPasskeyAction
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
AsyncFunction("replaceCredentialIdentities") { (credentials: [[String: Any]]) in
|
|
75
|
+
guard let store = PasskeyCredentialStore() else {
|
|
76
|
+
throw NSError(
|
|
77
|
+
domain: "ReactNativePasskeyAutofill",
|
|
78
|
+
code: 1,
|
|
79
|
+
userInfo: [NSLocalizedDescriptionKey: "App Group is not configured for passkey autofill."]
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let storedCredentials = credentials.compactMap { credential -> StoredPasskeyCredential? in
|
|
84
|
+
guard let credentialId = credential["credentialId"] as? String ?? credential["id"] as? String,
|
|
85
|
+
let relyingPartyIdentifier =
|
|
86
|
+
credential["relyingPartyIdentifier"] as? String ??
|
|
87
|
+
credential["rpId"] as? String ??
|
|
88
|
+
credential["origin"] as? String,
|
|
89
|
+
let userName =
|
|
90
|
+
credential["userName"] as? String ??
|
|
91
|
+
credential["name"] as? String ??
|
|
92
|
+
credential["userHandle"] as? String,
|
|
93
|
+
let userHandle =
|
|
94
|
+
credential["userHandle"] as? String ??
|
|
95
|
+
credential["userId"] as? String,
|
|
96
|
+
let privateKey =
|
|
97
|
+
credential["privateKey"] as? String ??
|
|
98
|
+
credential["privateKeyBase64"] as? String
|
|
99
|
+
else {
|
|
100
|
+
return nil
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let metadata = credential["metadata"] as? [String: Any]
|
|
104
|
+
|
|
105
|
+
return StoredPasskeyCredential(
|
|
106
|
+
credentialId: credentialId,
|
|
107
|
+
relyingPartyIdentifier: relyingPartyIdentifier.relyingPartyIdentifier,
|
|
108
|
+
userName: userName,
|
|
109
|
+
userHandle: userHandle,
|
|
110
|
+
privateKey: privateKey,
|
|
111
|
+
publicKey: credential["publicKey"] as? String ?? credential["publicKeyBase64"] as? String,
|
|
112
|
+
createdAt: credential["createdAt"] as? Double ?? Date().timeIntervalSince1970,
|
|
113
|
+
lastUsedAt: credential["lastUsedAt"] as? Double ?? metadata?["lastUsedAt"] as? Double,
|
|
114
|
+
parentKeyId: credential["parentKeyId"] as? String ?? metadata?["parentKeyId"] as? String
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try store.replace(storedCredentials)
|
|
119
|
+
try await store.replaceIdentityStore()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
AsyncFunction("refreshCredentialIdentities") {
|
|
123
|
+
guard let store = PasskeyCredentialStore() else {
|
|
124
|
+
throw NSError(
|
|
125
|
+
domain: "ReactNativePasskeyAutofill",
|
|
126
|
+
code: 1,
|
|
127
|
+
userInfo: [NSLocalizedDescriptionKey: "App Group is not configured for passkey autofill."]
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try await store.replaceIdentityStore()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
AsyncFunction("getStoredCredentials") { () -> [[String: Any]] in
|
|
135
|
+
guard let store = PasskeyCredentialStore() else {
|
|
136
|
+
return []
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return store.allCredentials().map { credential in
|
|
140
|
+
var result: [String: Any] = [
|
|
141
|
+
"credentialId": credential.credentialId,
|
|
142
|
+
"relyingPartyIdentifier": credential.relyingPartyIdentifier,
|
|
143
|
+
"userName": credential.userName,
|
|
144
|
+
"userHandle": credential.userHandle,
|
|
145
|
+
"createdAt": credential.createdAt,
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if let publicKey = credential.publicKey {
|
|
149
|
+
result["publicKey"] = publicKey
|
|
150
|
+
}
|
|
151
|
+
if let lastUsedAt = credential.lastUsedAt {
|
|
152
|
+
result["lastUsedAt"] = lastUsedAt
|
|
153
|
+
}
|
|
154
|
+
if let parentKeyId = credential.parentKeyId {
|
|
155
|
+
result["parentKeyId"] = parentKeyId
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
}
|
|
16
160
|
}
|
|
17
161
|
|
|
18
|
-
|
|
19
|
-
|
|
162
|
+
AsyncFunction("getDiagnostics") { () -> [String] in
|
|
163
|
+
guard let store = PasskeyCredentialStore() else {
|
|
164
|
+
return []
|
|
165
|
+
}
|
|
166
|
+
return store.diagnostics()
|
|
167
|
+
}
|
|
20
168
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
169
|
+
// Returns true when this app is the user-selected AutoFill credential
|
|
170
|
+
// provider. On iOS the Credential Provider extension reports its
|
|
171
|
+
// enablement via `ASCredentialIdentityStore.getState`.
|
|
172
|
+
AsyncFunction("isProviderActive") { (promise: Promise) in
|
|
173
|
+
ASCredentialIdentityStore.shared.getState { state in
|
|
174
|
+
promise.resolve(state.isEnabled)
|
|
175
|
+
}
|
|
24
176
|
}
|
|
25
177
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
178
|
+
// Best-effort deep link to the iOS AutoFill settings screen.
|
|
179
|
+
AsyncFunction("openProviderSettings") { () -> Bool in
|
|
180
|
+
#if canImport(UIKit)
|
|
181
|
+
if let url = URL(string: UIApplication.openSettingsURLString),
|
|
182
|
+
UIApplication.shared.canOpenURL(url) {
|
|
183
|
+
UIApplication.shared.open(url)
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
#endif
|
|
187
|
+
return false
|
|
33
188
|
}
|
|
34
189
|
|
|
35
190
|
// Enables the module to be used as a native view. Definition components that are accepted as part of the
|
package/lefthook.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Lefthook configuration
|
|
2
|
+
# Refer to https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
|
|
3
|
+
|
|
4
|
+
pre-commit:
|
|
5
|
+
commands:
|
|
6
|
+
lint:
|
|
7
|
+
run: pnpm run lint
|
|
8
|
+
fmt:
|
|
9
|
+
run: pnpm run fmt
|
|
10
|
+
pre-push:
|
|
11
|
+
parallel: true
|
|
12
|
+
commands:
|
|
13
|
+
test:
|
|
14
|
+
run: pnpm run test:all
|