@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.
Files changed (29) hide show
  1. package/index.cjs +1 -66
  2. package/index.js +1 -66
  3. package/package.json +7 -8
  4. package/android/AndroidManifest.xml +0 -1
  5. package/android/KeyStoreKeyManager.kt +0 -148
  6. package/android/KeychainModule.kt +0 -71
  7. package/android/build.gradle +0 -32
  8. package/android/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
  9. package/android/dynamic/keychain/KeychainModule.kt +0 -71
  10. package/android/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
  11. package/android/java/xyz/dynamic/keychain/KeychainModule.kt +0 -71
  12. package/android/keychain/KeyStoreKeyManager.kt +0 -148
  13. package/android/keychain/KeychainModule.kt +0 -71
  14. package/android/main/AndroidManifest.xml +0 -1
  15. package/android/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
  16. package/android/main/java/xyz/dynamic/keychain/KeychainModule.kt +0 -71
  17. package/android/src/main/AndroidManifest.xml +0 -1
  18. package/android/src/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
  19. package/android/src/main/java/xyz/dynamic/keychain/KeychainModule.kt +0 -71
  20. package/android/xyz/dynamic/keychain/KeyStoreKeyManager.kt +0 -148
  21. package/android/xyz/dynamic/keychain/KeychainModule.kt +0 -71
  22. package/expo-module.config.json +0 -9
  23. package/ios/Keychain.podspec +0 -15
  24. package/ios/KeychainModule.swift +0 -39
  25. package/ios/SecureEnclaveKeyManager.swift +0 -170
  26. package/src/ReactNativeExtension/setupKeychainHandler/index.d.ts +0 -1
  27. package/src/ReactNativeExtension/setupKeychainHandler/setupKeychainHandler.d.ts +0 -2
  28. package/src/nativeModules/Keychain.d.ts +0 -16
  29. 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.67.3-device-registration.0";
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.67.3-device-registration.0";
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.67.3-device-registration.0",
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.67.3-device-registration.0",
22
- "@dynamic-labs/client": "4.67.3-device-registration.0",
23
- "@dynamic-labs/logger": "4.67.3-device-registration.0",
24
- "@dynamic-labs/message-transport": "4.67.3-device-registration.0",
25
- "@dynamic-labs/webview-messages": "4.67.3-device-registration.0"
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
- }
@@ -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
- }