@dynamic-labs/react-native-extension 4.67.3-device-registration.0 → 4.68.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/index.cjs +1 -66
- package/index.js +1 -66
- package/package.json +7 -8
- package/android/AndroidManifest.xml +0 -1
- package/android/KeyStoreKeyManager.kt +0 -148
- package/android/KeychainModule.kt +0 -71
- package/android/build.gradle +0 -32
- package/android/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
- package/android/dynamic/keychain/KeychainModule.kt +0 -71
- package/android/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
- package/android/java/xyz/dynamic/keychain/KeychainModule.kt +0 -71
- package/android/keychain/KeyStoreKeyManager.kt +0 -148
- package/android/keychain/KeychainModule.kt +0 -71
- package/android/main/AndroidManifest.xml +0 -1
- package/android/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
- package/android/main/java/xyz/dynamic/keychain/KeychainModule.kt +0 -71
- package/android/src/main/AndroidManifest.xml +0 -1
- package/android/src/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
- package/android/src/main/java/xyz/dynamic/keychain/KeychainModule.kt +0 -71
- package/android/xyz/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
- package/android/xyz/dynamic/keychain/KeychainModule.kt +0 -71
- package/expo-module.config.json +0 -9
- package/ios/Keychain.podspec +0 -15
- package/ios/KeychainModule.swift +0 -39
- package/ios/SecureEnclaveKeyManager.swift +0 -170
- package/src/ReactNativeExtension/setupKeychainHandler/index.d.ts +0 -1
- package/src/ReactNativeExtension/setupKeychainHandler/setupKeychainHandler.d.ts +0 -2
- package/src/nativeModules/Keychain.d.ts +0 -16
- package/src/nativeModules/index.d.ts +0 -1
package/index.cjs
CHANGED
|
@@ -14,7 +14,6 @@ var expoLinking = require('expo-linking');
|
|
|
14
14
|
var expoWebBrowser = require('expo-web-browser');
|
|
15
15
|
var expoSecureStore = require('expo-secure-store');
|
|
16
16
|
var reactNativePasskeyStamper = require('@turnkey/react-native-passkey-stamper');
|
|
17
|
-
var expoModulesCore = require('expo-modules-core');
|
|
18
17
|
|
|
19
18
|
function _interopNamespace(e) {
|
|
20
19
|
if (e && e.__esModule) return e;
|
|
@@ -34,7 +33,7 @@ function _interopNamespace(e) {
|
|
|
34
33
|
return Object.freeze(n);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
var version = "4.
|
|
36
|
+
var version = "4.68.0";
|
|
38
37
|
|
|
39
38
|
function _extends() {
|
|
40
39
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -647,69 +646,6 @@ const setupTurnkeyPasskeyHandler = core => {
|
|
|
647
646
|
}));
|
|
648
647
|
};
|
|
649
648
|
|
|
650
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
651
|
-
const getKeychain = () => {
|
|
652
|
-
try {
|
|
653
|
-
const keychain = expoModulesCore.requireNativeModule('Keychain');
|
|
654
|
-
return keychain;
|
|
655
|
-
} catch (error) {
|
|
656
|
-
logger.warn('Could not get dynamic keychain, using unavailable keychain');
|
|
657
|
-
const unavailableKeychain = {
|
|
658
|
-
deleteKey: () => {
|
|
659
|
-
throw new Error('Keychain is not available');
|
|
660
|
-
},
|
|
661
|
-
generateKeyPair: () => {
|
|
662
|
-
throw new Error('Keychain is not available');
|
|
663
|
-
},
|
|
664
|
-
getPublicKey: () => {
|
|
665
|
-
throw new Error('Keychain is not available');
|
|
666
|
-
},
|
|
667
|
-
hasKey: () => {
|
|
668
|
-
throw new Error('Keychain is not available');
|
|
669
|
-
},
|
|
670
|
-
isAvailable: () => Promise.resolve(false),
|
|
671
|
-
sign: () => {
|
|
672
|
-
throw new Error('Keychain is not available');
|
|
673
|
-
}
|
|
674
|
-
};
|
|
675
|
-
return unavailableKeychain;
|
|
676
|
-
}
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
const setupKeychainHandler = core => {
|
|
680
|
-
const keychainRequestChannel = messageTransport.createRequestChannel(core.messageTransport);
|
|
681
|
-
const keychain = getKeychain();
|
|
682
|
-
keychainRequestChannel.handle('keychain_isAvailable', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
683
|
-
return keychain.isAvailable();
|
|
684
|
-
}));
|
|
685
|
-
keychainRequestChannel.handle('keychain_hasKey', _a => __awaiter(void 0, [_a], void 0, function* ({
|
|
686
|
-
key
|
|
687
|
-
}) {
|
|
688
|
-
return keychain.hasKey(key);
|
|
689
|
-
}));
|
|
690
|
-
keychainRequestChannel.handle('keychain_generateKey', _b => __awaiter(void 0, [_b], void 0, function* ({
|
|
691
|
-
key
|
|
692
|
-
}) {
|
|
693
|
-
return keychain.generateKeyPair(key);
|
|
694
|
-
}));
|
|
695
|
-
keychainRequestChannel.handle('keychain_getPublicKey', _c => __awaiter(void 0, [_c], void 0, function* ({
|
|
696
|
-
key
|
|
697
|
-
}) {
|
|
698
|
-
return keychain.getPublicKey(key);
|
|
699
|
-
}));
|
|
700
|
-
keychainRequestChannel.handle('keychain_sign', _d => __awaiter(void 0, [_d], void 0, function* ({
|
|
701
|
-
key,
|
|
702
|
-
payload
|
|
703
|
-
}) {
|
|
704
|
-
return keychain.sign(key, payload);
|
|
705
|
-
}));
|
|
706
|
-
keychainRequestChannel.handle('keychain_removeKey', _e => __awaiter(void 0, [_e], void 0, function* ({
|
|
707
|
-
key
|
|
708
|
-
}) {
|
|
709
|
-
return keychain.deleteKey(key);
|
|
710
|
-
}));
|
|
711
|
-
};
|
|
712
|
-
|
|
713
649
|
const defaultWebviewUrl = `https://webview.dynamicauth.com/${version}`;
|
|
714
650
|
const ReactNativeExtension = ({
|
|
715
651
|
webviewUrl: _webviewUrl = defaultWebviewUrl,
|
|
@@ -733,7 +669,6 @@ const ReactNativeExtension = ({
|
|
|
733
669
|
setupTurnkeyPasskeyHandler(core);
|
|
734
670
|
setupPlatformHandler(core);
|
|
735
671
|
setupStorageHandler(core);
|
|
736
|
-
setupKeychainHandler(core);
|
|
737
672
|
return {
|
|
738
673
|
reactNative: {
|
|
739
674
|
WebView: createWebView({
|
package/index.js
CHANGED
|
@@ -10,9 +10,8 @@ import { createURL, getInitialURL, addEventListener, openURL } from 'expo-linkin
|
|
|
10
10
|
import { openAuthSessionAsync } from 'expo-web-browser';
|
|
11
11
|
import { getItemAsync, deleteItemAsync, setItemAsync } from 'expo-secure-store';
|
|
12
12
|
import { createPasskey, PasskeyStamper } from '@turnkey/react-native-passkey-stamper';
|
|
13
|
-
import { requireNativeModule } from 'expo-modules-core';
|
|
14
13
|
|
|
15
|
-
var version = "4.
|
|
14
|
+
var version = "4.68.0";
|
|
16
15
|
|
|
17
16
|
function _extends() {
|
|
18
17
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -625,69 +624,6 @@ const setupTurnkeyPasskeyHandler = core => {
|
|
|
625
624
|
}));
|
|
626
625
|
};
|
|
627
626
|
|
|
628
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
629
|
-
const getKeychain = () => {
|
|
630
|
-
try {
|
|
631
|
-
const keychain = requireNativeModule('Keychain');
|
|
632
|
-
return keychain;
|
|
633
|
-
} catch (error) {
|
|
634
|
-
logger.warn('Could not get dynamic keychain, using unavailable keychain');
|
|
635
|
-
const unavailableKeychain = {
|
|
636
|
-
deleteKey: () => {
|
|
637
|
-
throw new Error('Keychain is not available');
|
|
638
|
-
},
|
|
639
|
-
generateKeyPair: () => {
|
|
640
|
-
throw new Error('Keychain is not available');
|
|
641
|
-
},
|
|
642
|
-
getPublicKey: () => {
|
|
643
|
-
throw new Error('Keychain is not available');
|
|
644
|
-
},
|
|
645
|
-
hasKey: () => {
|
|
646
|
-
throw new Error('Keychain is not available');
|
|
647
|
-
},
|
|
648
|
-
isAvailable: () => Promise.resolve(false),
|
|
649
|
-
sign: () => {
|
|
650
|
-
throw new Error('Keychain is not available');
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
return unavailableKeychain;
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
const setupKeychainHandler = core => {
|
|
658
|
-
const keychainRequestChannel = createRequestChannel(core.messageTransport);
|
|
659
|
-
const keychain = getKeychain();
|
|
660
|
-
keychainRequestChannel.handle('keychain_isAvailable', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
661
|
-
return keychain.isAvailable();
|
|
662
|
-
}));
|
|
663
|
-
keychainRequestChannel.handle('keychain_hasKey', _a => __awaiter(void 0, [_a], void 0, function* ({
|
|
664
|
-
key
|
|
665
|
-
}) {
|
|
666
|
-
return keychain.hasKey(key);
|
|
667
|
-
}));
|
|
668
|
-
keychainRequestChannel.handle('keychain_generateKey', _b => __awaiter(void 0, [_b], void 0, function* ({
|
|
669
|
-
key
|
|
670
|
-
}) {
|
|
671
|
-
return keychain.generateKeyPair(key);
|
|
672
|
-
}));
|
|
673
|
-
keychainRequestChannel.handle('keychain_getPublicKey', _c => __awaiter(void 0, [_c], void 0, function* ({
|
|
674
|
-
key
|
|
675
|
-
}) {
|
|
676
|
-
return keychain.getPublicKey(key);
|
|
677
|
-
}));
|
|
678
|
-
keychainRequestChannel.handle('keychain_sign', _d => __awaiter(void 0, [_d], void 0, function* ({
|
|
679
|
-
key,
|
|
680
|
-
payload
|
|
681
|
-
}) {
|
|
682
|
-
return keychain.sign(key, payload);
|
|
683
|
-
}));
|
|
684
|
-
keychainRequestChannel.handle('keychain_removeKey', _e => __awaiter(void 0, [_e], void 0, function* ({
|
|
685
|
-
key
|
|
686
|
-
}) {
|
|
687
|
-
return keychain.deleteKey(key);
|
|
688
|
-
}));
|
|
689
|
-
};
|
|
690
|
-
|
|
691
627
|
const defaultWebviewUrl = `https://webview.dynamicauth.com/${version}`;
|
|
692
628
|
const ReactNativeExtension = ({
|
|
693
629
|
webviewUrl: _webviewUrl = defaultWebviewUrl,
|
|
@@ -711,7 +647,6 @@ const ReactNativeExtension = ({
|
|
|
711
647
|
setupTurnkeyPasskeyHandler(core);
|
|
712
648
|
setupPlatformHandler(core);
|
|
713
649
|
setupStorageHandler(core);
|
|
714
|
-
setupKeychainHandler(core);
|
|
715
650
|
return {
|
|
716
651
|
reactNative: {
|
|
717
652
|
WebView: createWebView({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-labs/react-native-extension",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.68.0",
|
|
4
4
|
"main": "./index.cjs",
|
|
5
5
|
"module": "./index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"@turnkey/react-native-passkey-stamper": "1.2.7",
|
|
19
19
|
"@react-native-documents/picker": "^11.0.0",
|
|
20
20
|
"react-native-fs": ">=2.20.0",
|
|
21
|
-
"@dynamic-labs/assert-package-version": "4.
|
|
22
|
-
"@dynamic-labs/client": "4.
|
|
23
|
-
"@dynamic-labs/logger": "4.
|
|
24
|
-
"@dynamic-labs/message-transport": "4.
|
|
25
|
-
"@dynamic-labs/webview-messages": "4.
|
|
21
|
+
"@dynamic-labs/assert-package-version": "4.68.0",
|
|
22
|
+
"@dynamic-labs/client": "4.68.0",
|
|
23
|
+
"@dynamic-labs/logger": "4.68.0",
|
|
24
|
+
"@dynamic-labs/message-transport": "4.68.0",
|
|
25
|
+
"@dynamic-labs/webview-messages": "4.68.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=18.0.0 <20.0.0",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"react-native-webview": "^13.6.4",
|
|
31
31
|
"expo-linking": ">=6.2.2",
|
|
32
32
|
"expo-web-browser": ">=12.0.0",
|
|
33
|
-
"expo-secure-store": ">=12.0.0"
|
|
34
|
-
"expo-modules-core": ">=2.0.0"
|
|
33
|
+
"expo-secure-store": ">=12.0.0"
|
|
35
34
|
}
|
|
36
35
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
package xyz.dynamic.keychain
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.content.pm.PackageManager
|
|
5
|
-
import android.os.Build
|
|
6
|
-
import android.security.keystore.KeyGenParameterSpec
|
|
7
|
-
import android.security.keystore.KeyProperties
|
|
8
|
-
import android.security.keystore.StrongBoxUnavailableException
|
|
9
|
-
import java.security.KeyPairGenerator
|
|
10
|
-
import java.security.KeyStore
|
|
11
|
-
import java.security.Signature
|
|
12
|
-
import java.security.spec.ECGenParameterSpec
|
|
13
|
-
|
|
14
|
-
/// Platform-agnostic Android KeyStore key manager.
|
|
15
|
-
/// Provides P-256 key generation, signing, and management backed by hardware TEE/StrongBox.
|
|
16
|
-
/// All public keys are returned in uncompressed SEC1 format (65 bytes: 04 || x || y).
|
|
17
|
-
/// All binary data uses base64url encoding (RFC 4648 §5, no padding).
|
|
18
|
-
class KeyStoreKeyManager {
|
|
19
|
-
|
|
20
|
-
fun isAvailable(context: Context?): Boolean {
|
|
21
|
-
return try {
|
|
22
|
-
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
23
|
-
keyStore.load(null)
|
|
24
|
-
val hasStrongBox = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && context != null) {
|
|
25
|
-
context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
|
|
26
|
-
} else {
|
|
27
|
-
false
|
|
28
|
-
}
|
|
29
|
-
// Even without StrongBox, Android KeyStore provides TEE-backed keys on most devices
|
|
30
|
-
hasStrongBox || Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
31
|
-
} catch (e: Exception) {
|
|
32
|
-
false
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
fun hasKey(alias: String): Boolean {
|
|
37
|
-
val keyStore = loadKeyStore()
|
|
38
|
-
return keyStore.containsAlias(alias)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
fun generateKeyPair(alias: String): String {
|
|
42
|
-
val specBuilder = KeyGenParameterSpec.Builder(
|
|
43
|
-
alias,
|
|
44
|
-
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
|
45
|
-
)
|
|
46
|
-
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
|
47
|
-
.setDigests(KeyProperties.DIGEST_SHA256)
|
|
48
|
-
|
|
49
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
50
|
-
specBuilder.setIsStrongBoxBacked(true)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
val keyPair = try {
|
|
54
|
-
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE)
|
|
55
|
-
kpg.initialize(specBuilder.build())
|
|
56
|
-
kpg.generateKeyPair()
|
|
57
|
-
} catch (e: Exception) {
|
|
58
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && e is StrongBoxUnavailableException) {
|
|
59
|
-
// Retry without StrongBox
|
|
60
|
-
specBuilder.setIsStrongBoxBacked(false)
|
|
61
|
-
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE)
|
|
62
|
-
kpg.initialize(specBuilder.build())
|
|
63
|
-
kpg.generateKeyPair()
|
|
64
|
-
} else {
|
|
65
|
-
throw e
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
val publicKeyBytes = extractUncompressedPublicKey(keyPair.public.encoded)
|
|
70
|
-
return base64urlEncode(publicKeyBytes)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
fun getPublicKey(alias: String): String? {
|
|
74
|
-
val keyStore = loadKeyStore()
|
|
75
|
-
val entry = keyStore.getEntry(alias, null)
|
|
76
|
-
|
|
77
|
-
if (entry == null || entry !is KeyStore.PrivateKeyEntry) {
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
val publicKeyBytes = extractUncompressedPublicKey(entry.certificate.publicKey.encoded)
|
|
82
|
-
return base64urlEncode(publicKeyBytes)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
fun sign(alias: String, payload: ByteArray): String {
|
|
86
|
-
val keyStore = loadKeyStore()
|
|
87
|
-
val entry = keyStore.getEntry(alias, null)
|
|
88
|
-
|
|
89
|
-
if (entry == null || entry !is KeyStore.PrivateKeyEntry) {
|
|
90
|
-
throw IllegalArgumentException("Key not found: $alias")
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
val signature = Signature.getInstance("SHA256withECDSA")
|
|
94
|
-
signature.initSign(entry.privateKey)
|
|
95
|
-
signature.update(payload)
|
|
96
|
-
val signatureBytes = signature.sign()
|
|
97
|
-
|
|
98
|
-
return base64urlEncode(signatureBytes)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
fun deleteKey(alias: String) {
|
|
102
|
-
val keyStore = loadKeyStore()
|
|
103
|
-
keyStore.deleteEntry(alias)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// region Private helpers
|
|
107
|
-
|
|
108
|
-
private fun loadKeyStore(): KeyStore {
|
|
109
|
-
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
110
|
-
keyStore.load(null)
|
|
111
|
-
return keyStore
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Extract uncompressed SEC1 public key (65 bytes: 04 || x || y)
|
|
116
|
-
* from X.509 SubjectPublicKeyInfo DER encoding.
|
|
117
|
-
*
|
|
118
|
-
* For a P-256 key, the SubjectPublicKeyInfo contains the uncompressed
|
|
119
|
-
* point at the end of the DER structure. The point is always 65 bytes.
|
|
120
|
-
*/
|
|
121
|
-
private fun extractUncompressedPublicKey(x509Encoded: ByteArray): ByteArray {
|
|
122
|
-
val uncompressedPointLength = 65
|
|
123
|
-
return x509Encoded.copyOfRange(
|
|
124
|
-
x509Encoded.size - uncompressedPointLength,
|
|
125
|
-
x509Encoded.size
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private fun base64urlEncode(data: ByteArray): String {
|
|
130
|
-
return android.util.Base64.encodeToString(
|
|
131
|
-
data,
|
|
132
|
-
android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP or android.util.Base64.NO_PADDING
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
companion object {
|
|
137
|
-
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
|
138
|
-
|
|
139
|
-
fun base64urlDecode(input: String): ByteArray {
|
|
140
|
-
return android.util.Base64.decode(
|
|
141
|
-
input,
|
|
142
|
-
android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP or android.util.Base64.NO_PADDING
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// endregion
|
|
148
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
package xyz.dynamic.keychain
|
|
2
|
-
|
|
3
|
-
import expo.modules.kotlin.modules.Module
|
|
4
|
-
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
-
import expo.modules.kotlin.Promise
|
|
6
|
-
|
|
7
|
-
class KeychainModule : Module() {
|
|
8
|
-
private val keyManager = KeyStoreKeyManager()
|
|
9
|
-
|
|
10
|
-
override fun definition() = ModuleDefinition {
|
|
11
|
-
Name("Keychain")
|
|
12
|
-
|
|
13
|
-
AsyncFunction("isAvailable") { promise: Promise ->
|
|
14
|
-
try {
|
|
15
|
-
val context = appContext.reactContext
|
|
16
|
-
promise.resolve(keyManager.isAvailable(context))
|
|
17
|
-
} catch (e: Exception) {
|
|
18
|
-
promise.resolve(false)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
AsyncFunction("hasKey") { key: String, promise: Promise ->
|
|
23
|
-
try {
|
|
24
|
-
promise.resolve(keyManager.hasKey(key))
|
|
25
|
-
} catch (e: Exception) {
|
|
26
|
-
promise.reject("ERR_KEYCHAIN", "Failed to check key: ${e.message}", e)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
AsyncFunction("generateKeyPair") { key: String, promise: Promise ->
|
|
31
|
-
try {
|
|
32
|
-
val publicKey = keyManager.generateKeyPair(key)
|
|
33
|
-
promise.resolve(mapOf("publicKey" to publicKey))
|
|
34
|
-
} catch (e: Exception) {
|
|
35
|
-
promise.reject("ERR_KEYCHAIN", "Failed to generate key pair: ${e.message}", e)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
AsyncFunction("getPublicKey") { key: String, promise: Promise ->
|
|
40
|
-
try {
|
|
41
|
-
val publicKey = keyManager.getPublicKey(key)
|
|
42
|
-
if (publicKey == null) {
|
|
43
|
-
promise.resolve(null)
|
|
44
|
-
} else {
|
|
45
|
-
promise.resolve(mapOf("publicKey" to publicKey))
|
|
46
|
-
}
|
|
47
|
-
} catch (e: Exception) {
|
|
48
|
-
promise.reject("ERR_KEYCHAIN", "Failed to get public key: ${e.message}", e)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
AsyncFunction("sign") { key: String, payload: String, promise: Promise ->
|
|
53
|
-
try {
|
|
54
|
-
val payloadData = KeyStoreKeyManager.base64urlDecode(payload)
|
|
55
|
-
val signature = keyManager.sign(key, payloadData)
|
|
56
|
-
promise.resolve(mapOf("signature" to signature))
|
|
57
|
-
} catch (e: Exception) {
|
|
58
|
-
promise.reject("ERR_KEYCHAIN", "Failed to sign: ${e.message}", e)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
AsyncFunction("deleteKey") { key: String, promise: Promise ->
|
|
63
|
-
try {
|
|
64
|
-
keyManager.deleteKey(key)
|
|
65
|
-
promise.resolve(null)
|
|
66
|
-
} catch (e: Exception) {
|
|
67
|
-
promise.reject("ERR_KEYCHAIN", "Failed to delete key: ${e.message}", e)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
package/android/build.gradle
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
apply plugin: 'com.android.library'
|
|
2
|
-
apply plugin: 'kotlin-android'
|
|
3
|
-
apply plugin: 'expo-module'
|
|
4
|
-
|
|
5
|
-
group = 'xyz.dynamic.keychain'
|
|
6
|
-
|
|
7
|
-
android {
|
|
8
|
-
namespace 'xyz.dynamic.keychain'
|
|
9
|
-
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
10
|
-
|
|
11
|
-
defaultConfig {
|
|
12
|
-
minSdkVersion safeExtGet("minSdkVersion", 23)
|
|
13
|
-
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
compileOptions {
|
|
17
|
-
sourceCompatibility JavaVersion.VERSION_17
|
|
18
|
-
targetCompatibility JavaVersion.VERSION_17
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
kotlinOptions {
|
|
22
|
-
jvmTarget = '17'
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
dependencies {
|
|
27
|
-
implementation project(':expo-modules-core')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
def safeExtGet(prop, fallback) {
|
|
31
|
-
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
32
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
package xyz.dynamic.keychain
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.content.pm.PackageManager
|
|
5
|
-
import android.os.Build
|
|
6
|
-
import android.security.keystore.KeyGenParameterSpec
|
|
7
|
-
import android.security.keystore.KeyProperties
|
|
8
|
-
import android.security.keystore.StrongBoxUnavailableException
|
|
9
|
-
import java.security.KeyPairGenerator
|
|
10
|
-
import java.security.KeyStore
|
|
11
|
-
import java.security.Signature
|
|
12
|
-
import java.security.spec.ECGenParameterSpec
|
|
13
|
-
|
|
14
|
-
/// Platform-agnostic Android KeyStore key manager.
|
|
15
|
-
/// Provides P-256 key generation, signing, and management backed by hardware TEE/StrongBox.
|
|
16
|
-
/// All public keys are returned in uncompressed SEC1 format (65 bytes: 04 || x || y).
|
|
17
|
-
/// All binary data uses base64url encoding (RFC 4648 §5, no padding).
|
|
18
|
-
class KeyStoreKeyManager {
|
|
19
|
-
|
|
20
|
-
fun isAvailable(context: Context?): Boolean {
|
|
21
|
-
return try {
|
|
22
|
-
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
23
|
-
keyStore.load(null)
|
|
24
|
-
val hasStrongBox = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && context != null) {
|
|
25
|
-
context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
|
|
26
|
-
} else {
|
|
27
|
-
false
|
|
28
|
-
}
|
|
29
|
-
// Even without StrongBox, Android KeyStore provides TEE-backed keys on most devices
|
|
30
|
-
hasStrongBox || Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
31
|
-
} catch (e: Exception) {
|
|
32
|
-
false
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
fun hasKey(alias: String): Boolean {
|
|
37
|
-
val keyStore = loadKeyStore()
|
|
38
|
-
return keyStore.containsAlias(alias)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
fun generateKeyPair(alias: String): String {
|
|
42
|
-
val specBuilder = KeyGenParameterSpec.Builder(
|
|
43
|
-
alias,
|
|
44
|
-
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
|
45
|
-
)
|
|
46
|
-
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
|
47
|
-
.setDigests(KeyProperties.DIGEST_SHA256)
|
|
48
|
-
|
|
49
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
50
|
-
specBuilder.setIsStrongBoxBacked(true)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
val keyPair = try {
|
|
54
|
-
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE)
|
|
55
|
-
kpg.initialize(specBuilder.build())
|
|
56
|
-
kpg.generateKeyPair()
|
|
57
|
-
} catch (e: Exception) {
|
|
58
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && e is StrongBoxUnavailableException) {
|
|
59
|
-
// Retry without StrongBox
|
|
60
|
-
specBuilder.setIsStrongBoxBacked(false)
|
|
61
|
-
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE)
|
|
62
|
-
kpg.initialize(specBuilder.build())
|
|
63
|
-
kpg.generateKeyPair()
|
|
64
|
-
} else {
|
|
65
|
-
throw e
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
val publicKeyBytes = extractUncompressedPublicKey(keyPair.public.encoded)
|
|
70
|
-
return base64urlEncode(publicKeyBytes)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
fun getPublicKey(alias: String): String? {
|
|
74
|
-
val keyStore = loadKeyStore()
|
|
75
|
-
val entry = keyStore.getEntry(alias, null)
|
|
76
|
-
|
|
77
|
-
if (entry == null || entry !is KeyStore.PrivateKeyEntry) {
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
val publicKeyBytes = extractUncompressedPublicKey(entry.certificate.publicKey.encoded)
|
|
82
|
-
return base64urlEncode(publicKeyBytes)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
fun sign(alias: String, payload: ByteArray): String {
|
|
86
|
-
val keyStore = loadKeyStore()
|
|
87
|
-
val entry = keyStore.getEntry(alias, null)
|
|
88
|
-
|
|
89
|
-
if (entry == null || entry !is KeyStore.PrivateKeyEntry) {
|
|
90
|
-
throw IllegalArgumentException("Key not found: $alias")
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
val signature = Signature.getInstance("SHA256withECDSA")
|
|
94
|
-
signature.initSign(entry.privateKey)
|
|
95
|
-
signature.update(payload)
|
|
96
|
-
val signatureBytes = signature.sign()
|
|
97
|
-
|
|
98
|
-
return base64urlEncode(signatureBytes)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
fun deleteKey(alias: String) {
|
|
102
|
-
val keyStore = loadKeyStore()
|
|
103
|
-
keyStore.deleteEntry(alias)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// region Private helpers
|
|
107
|
-
|
|
108
|
-
private fun loadKeyStore(): KeyStore {
|
|
109
|
-
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
|
110
|
-
keyStore.load(null)
|
|
111
|
-
return keyStore
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Extract uncompressed SEC1 public key (65 bytes: 04 || x || y)
|
|
116
|
-
* from X.509 SubjectPublicKeyInfo DER encoding.
|
|
117
|
-
*
|
|
118
|
-
* For a P-256 key, the SubjectPublicKeyInfo contains the uncompressed
|
|
119
|
-
* point at the end of the DER structure. The point is always 65 bytes.
|
|
120
|
-
*/
|
|
121
|
-
private fun extractUncompressedPublicKey(x509Encoded: ByteArray): ByteArray {
|
|
122
|
-
val uncompressedPointLength = 65
|
|
123
|
-
return x509Encoded.copyOfRange(
|
|
124
|
-
x509Encoded.size - uncompressedPointLength,
|
|
125
|
-
x509Encoded.size
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private fun base64urlEncode(data: ByteArray): String {
|
|
130
|
-
return android.util.Base64.encodeToString(
|
|
131
|
-
data,
|
|
132
|
-
android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP or android.util.Base64.NO_PADDING
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
companion object {
|
|
137
|
-
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
|
138
|
-
|
|
139
|
-
fun base64urlDecode(input: String): ByteArray {
|
|
140
|
-
return android.util.Base64.decode(
|
|
141
|
-
input,
|
|
142
|
-
android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP or android.util.Base64.NO_PADDING
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// endregion
|
|
148
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
package xyz.dynamic.keychain
|
|
2
|
-
|
|
3
|
-
import expo.modules.kotlin.modules.Module
|
|
4
|
-
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
-
import expo.modules.kotlin.Promise
|
|
6
|
-
|
|
7
|
-
class KeychainModule : Module() {
|
|
8
|
-
private val keyManager = KeyStoreKeyManager()
|
|
9
|
-
|
|
10
|
-
override fun definition() = ModuleDefinition {
|
|
11
|
-
Name("Keychain")
|
|
12
|
-
|
|
13
|
-
AsyncFunction("isAvailable") { promise: Promise ->
|
|
14
|
-
try {
|
|
15
|
-
val context = appContext.reactContext
|
|
16
|
-
promise.resolve(keyManager.isAvailable(context))
|
|
17
|
-
} catch (e: Exception) {
|
|
18
|
-
promise.resolve(false)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
AsyncFunction("hasKey") { key: String, promise: Promise ->
|
|
23
|
-
try {
|
|
24
|
-
promise.resolve(keyManager.hasKey(key))
|
|
25
|
-
} catch (e: Exception) {
|
|
26
|
-
promise.reject("ERR_KEYCHAIN", "Failed to check key: ${e.message}", e)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
AsyncFunction("generateKeyPair") { key: String, promise: Promise ->
|
|
31
|
-
try {
|
|
32
|
-
val publicKey = keyManager.generateKeyPair(key)
|
|
33
|
-
promise.resolve(mapOf("publicKey" to publicKey))
|
|
34
|
-
} catch (e: Exception) {
|
|
35
|
-
promise.reject("ERR_KEYCHAIN", "Failed to generate key pair: ${e.message}", e)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
AsyncFunction("getPublicKey") { key: String, promise: Promise ->
|
|
40
|
-
try {
|
|
41
|
-
val publicKey = keyManager.getPublicKey(key)
|
|
42
|
-
if (publicKey == null) {
|
|
43
|
-
promise.resolve(null)
|
|
44
|
-
} else {
|
|
45
|
-
promise.resolve(mapOf("publicKey" to publicKey))
|
|
46
|
-
}
|
|
47
|
-
} catch (e: Exception) {
|
|
48
|
-
promise.reject("ERR_KEYCHAIN", "Failed to get public key: ${e.message}", e)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
AsyncFunction("sign") { key: String, payload: String, promise: Promise ->
|
|
53
|
-
try {
|
|
54
|
-
val payloadData = KeyStoreKeyManager.base64urlDecode(payload)
|
|
55
|
-
val signature = keyManager.sign(key, payloadData)
|
|
56
|
-
promise.resolve(mapOf("signature" to signature))
|
|
57
|
-
} catch (e: Exception) {
|
|
58
|
-
promise.reject("ERR_KEYCHAIN", "Failed to sign: ${e.message}", e)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
AsyncFunction("deleteKey") { key: String, promise: Promise ->
|
|
63
|
-
try {
|
|
64
|
-
keyManager.deleteKey(key)
|
|
65
|
-
promise.resolve(null)
|
|
66
|
-
} catch (e: Exception) {
|
|
67
|
-
promise.reject("ERR_KEYCHAIN", "Failed to delete key: ${e.message}", e)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|