Skip to content

Commit d1deccf

Browse files
Derrick MbaraniDerrick Mbarani
authored andcommitted
feat: react native wrapper for Android Keystore for creating cryptographic key paris and signing
1 parent 61f6d33 commit d1deccf

6 files changed

Lines changed: 10017 additions & 12 deletions

File tree

android/src/main/java/com/rnandroidkeystore/RnAndroidKeystoreModule.kt

Lines changed: 224 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,234 @@
11
package com.rnandroidkeystore
22

3+
import android.os.Build
4+
import android.security.keystore.KeyGenParameterSpec
5+
import android.security.keystore.KeyInfo
6+
import android.security.keystore.KeyProperties
7+
import android.util.Base64
8+
import com.facebook.react.bridge.Arguments
9+
import com.facebook.react.bridge.Promise
310
import com.facebook.react.bridge.ReactApplicationContext
11+
import java.nio.charset.StandardCharsets
12+
import java.security.KeyFactory
13+
import java.security.KeyPairGenerator
14+
import java.security.KeyStore
15+
import java.security.PrivateKey
16+
import java.security.PublicKey
17+
import java.security.Signature
18+
import java.security.spec.ECGenParameterSpec
419

520
class RnAndroidKeystoreModule(reactContext: ReactApplicationContext) :
621
NativeRnAndroidKeystoreSpec(reactContext) {
722

8-
override fun multiply(a: Double, b: Double): Double {
9-
return a * b
23+
override fun getName() = NAME
24+
25+
override fun generateKeyPair(
26+
alias: String,
27+
algorithm: String,
28+
keySize: Double,
29+
hardwareBacked: Boolean,
30+
userAuthenticationRequired: Boolean,
31+
promise: Promise
32+
) {
33+
try {
34+
if (algorithm.uppercase() != "ECDSA") {
35+
promise.resolve(errorResult("Unsupported algorithm: $algorithm"))
36+
return
37+
}
38+
39+
val keyStore = getKeyStore()
40+
if (keyStore.containsAlias(alias)) {
41+
val existingPublicKey = getPublicKeyFromAlias(alias)
42+
if (existingPublicKey == null) {
43+
promise.resolve(errorResult("Failed to load existing public key"))
44+
return
45+
}
46+
47+
promise.resolve(successResult().apply {
48+
putString("publicKey", toPem(existingPublicKey))
49+
putString("privateKey", alias)
50+
putDouble("keySize", keySize)
51+
})
52+
return
53+
}
54+
55+
createEcKeyPair(alias, userAuthenticationRequired, hardwareBacked)
56+
57+
val publicKey = getPublicKeyFromAlias(alias)
58+
if (publicKey == null) {
59+
promise.resolve(errorResult("Failed to retrieve generated public key"))
60+
return
61+
}
62+
63+
promise.resolve(successResult().apply {
64+
putString("publicKey", toPem(publicKey))
65+
putString("privateKey", alias)
66+
putDouble("keySize", keySize)
67+
})
68+
} catch (error: Exception) {
69+
promise.reject("GENERATE_KEY_PAIR_ERROR", error.message, error)
70+
}
71+
}
72+
73+
override fun getPublicKeyPEM(alias: String, promise: Promise) {
74+
try {
75+
val publicKey = getPublicKeyFromAlias(alias)
76+
if (publicKey == null) {
77+
promise.resolve(errorResult("Key not found for alias: $alias"))
78+
return
79+
}
80+
81+
promise.resolve(successResult().apply {
82+
putString("publicKeyPEM", toPem(publicKey))
83+
})
84+
} catch (error: Exception) {
85+
promise.reject("GET_PUBLIC_KEY_ERROR", error.message, error)
86+
}
87+
}
88+
89+
override fun sign(alias: String, payload: String, algorithm: String, promise: Promise) {
90+
try {
91+
val keyStore = getKeyStore()
92+
val key = keyStore.getKey(alias, null) as? PrivateKey
93+
if (key == null) {
94+
promise.resolve(errorResult("Private key not found for alias: $alias"))
95+
return
96+
}
97+
98+
val signature = Signature.getInstance(algorithm)
99+
signature.initSign(key)
100+
signature.update(payload.toByteArray(StandardCharsets.UTF_8))
101+
102+
val signedBytes = signature.sign()
103+
val encodedSignature = Base64.encodeToString(signedBytes, Base64.NO_WRAP)
104+
105+
promise.resolve(successResult().apply {
106+
putString("signature", encodedSignature)
107+
})
108+
} catch (error: Exception) {
109+
promise.reject("SIGN_ERROR", error.message, error)
110+
}
111+
}
112+
113+
override fun deleteKey(alias: String, promise: Promise) {
114+
try {
115+
val keyStore = getKeyStore()
116+
if (keyStore.containsAlias(alias)) {
117+
keyStore.deleteEntry(alias)
118+
}
119+
120+
promise.resolve(successResult())
121+
} catch (error: Exception) {
122+
promise.reject("DELETE_KEY_ERROR", error.message, error)
123+
}
124+
}
125+
126+
override fun keyExists(alias: String, promise: Promise) {
127+
try {
128+
val keyStore = getKeyStore()
129+
val result = Arguments.createMap().apply {
130+
putBoolean("success", true)
131+
putBoolean("exists", keyStore.containsAlias(alias))
132+
}
133+
promise.resolve(result)
134+
} catch (error: Exception) {
135+
val result = Arguments.createMap().apply {
136+
putBoolean("success", false)
137+
putBoolean("exists", false)
138+
}
139+
promise.resolve(result)
140+
}
141+
}
142+
143+
override fun isHardwareBackedKeystoreAvailable(promise: Promise) {
144+
val tempAlias = "android_hw_check_${System.currentTimeMillis()}"
145+
try {
146+
createEcKeyPair(tempAlias, userAuthenticationRequired = false, hardwareBacked = false)
147+
148+
val keyStore = getKeyStore()
149+
val key = keyStore.getKey(tempAlias, null) as? PrivateKey
150+
if (key == null) {
151+
promise.resolve(Arguments.createMap().apply { putBoolean("available", false) })
152+
return
153+
}
154+
155+
val keyFactory = KeyFactory.getInstance(key.algorithm, "AndroidKeyStore")
156+
val keyInfo = keyFactory.getKeySpec(key, KeyInfo::class.java) as KeyInfo
157+
val isHardwareBacked = keyInfo.isInsideSecureHardware
158+
159+
promise.resolve(Arguments.createMap().apply {
160+
putBoolean("available", isHardwareBacked)
161+
})
162+
} catch (_: Exception) {
163+
promise.resolve(Arguments.createMap().apply {
164+
putBoolean("available", false)
165+
})
166+
} finally {
167+
try {
168+
val keyStore = getKeyStore()
169+
if (keyStore.containsAlias(tempAlias)) {
170+
keyStore.deleteEntry(tempAlias)
171+
}
172+
} catch (_: Exception) {
173+
}
174+
}
175+
}
176+
177+
private fun createEcKeyPair(alias: String, userAuthenticationRequired: Boolean, hardwareBacked: Boolean) {
178+
try {
179+
createEcKeyPairInternal(alias, userAuthenticationRequired, hardwareBacked)
180+
} catch (error: Exception) {
181+
if (hardwareBacked) {
182+
createEcKeyPairInternal(alias, userAuthenticationRequired, false)
183+
} else {
184+
throw error
185+
}
186+
}
187+
}
188+
189+
private fun createEcKeyPairInternal(alias: String, userAuthenticationRequired: Boolean, hardwareBacked: Boolean) {
190+
val keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
191+
192+
val builder = KeyGenParameterSpec.Builder(
193+
alias,
194+
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY,
195+
)
196+
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
197+
.setDigests(KeyProperties.DIGEST_SHA256)
198+
.setUserAuthenticationRequired(userAuthenticationRequired)
199+
200+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && hardwareBacked) {
201+
builder.setIsStrongBoxBacked(true)
202+
}
203+
204+
keyPairGenerator.initialize(builder.build())
205+
keyPairGenerator.generateKeyPair()
206+
}
207+
208+
private fun getPublicKeyFromAlias(alias: String): PublicKey? {
209+
val keyStore = getKeyStore()
210+
val certificate = keyStore.getCertificate(alias) ?: return null
211+
return certificate.publicKey
212+
}
213+
214+
private fun toPem(publicKey: PublicKey): String {
215+
val base64 = Base64.encodeToString(publicKey.encoded, Base64.NO_WRAP)
216+
return "-----BEGIN PUBLIC KEY-----\n$base64\n-----END PUBLIC KEY-----"
217+
}
218+
219+
private fun getKeyStore(): KeyStore {
220+
val keyStore = KeyStore.getInstance("AndroidKeyStore")
221+
keyStore.load(null)
222+
return keyStore
223+
}
224+
225+
private fun successResult() = Arguments.createMap().apply {
226+
putBoolean("success", true)
227+
}
228+
229+
private fun errorResult(message: String) = Arguments.createMap().apply {
230+
putBoolean("success", false)
231+
putString("error", message)
10232
}
11233

12234
companion object {

example/src/App.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,39 @@
1+
import { useEffect, useState } from 'react';
12
import { Text, View, StyleSheet } from 'react-native';
2-
import { multiply } from 'rn-android-keystore';
3-
4-
const result = multiply(3, 7);
3+
import * as k from 'rn-android-keystore';
54

65
export default function App() {
6+
const [hardwareBacked, setHardwareBacked] = useState<boolean | null>(null);
7+
const [keypair, setKeypair] = useState<{
8+
publicKey: string;
9+
privateKey: string;
10+
} | null>(null);
11+
12+
useEffect(() => {
13+
(async () => {
14+
const isHardwareBacked = await k.isHardwareBackedKeystoreAvailable();
15+
setHardwareBacked(isHardwareBacked);
16+
17+
const keypairResponse = await k.getOrCreateKeyPair();
18+
setKeypair(keypairResponse);
19+
})();
20+
}, []);
21+
722
return (
823
<View style={styles.container}>
9-
<Text>Result: {result}</Text>
24+
<Text>
25+
Hardware-backed Keystore Available: {hardwareBacked ? 'Yes' : 'No'}
26+
</Text>
27+
<Text>Public Key: {keypair?.publicKey}</Text>
28+
<Text>Private Key: {keypair?.privateKey}</Text>
29+
<View style={styles.methodsContainer}>
30+
<Text>Android Keystore Methods</Text>
31+
<View style={styles.methodsList}>
32+
{Object.keys(k).map((v) => (
33+
<Text key={v}>* {v}</Text>
34+
))}
35+
</View>
36+
</View>
1037
</View>
1138
);
1239
}
@@ -17,4 +44,10 @@ const styles = StyleSheet.create({
1744
alignItems: 'center',
1845
justifyContent: 'center',
1946
},
47+
methodsContainer: {
48+
marginTop: 16,
49+
},
50+
methodsList: {
51+
marginTop: 8,
52+
},
2053
});

src/NativeRnAndroidKeystore.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,45 @@
11
import { TurboModuleRegistry, type TurboModule } from 'react-native';
22

33
export interface Spec extends TurboModule {
4-
multiply(a: number, b: number): number;
4+
generateKeyPair(
5+
alias: string,
6+
algorithm: string,
7+
keySize: number,
8+
hardwareBacked: boolean,
9+
userAuthenticationRequired: boolean
10+
): Promise<{
11+
success: boolean;
12+
error?: string;
13+
publicKey?: string;
14+
privateKey?: string;
15+
keySize?: number;
16+
}>;
17+
getPublicKeyPEM(alias: string): Promise<{
18+
success: boolean;
19+
error?: string;
20+
publicKeyPEM?: string;
21+
}>;
22+
sign(
23+
alias: string,
24+
payload: string,
25+
algorithm: string
26+
): Promise<{
27+
success: boolean;
28+
error?: string;
29+
signature?: string;
30+
}>;
31+
deleteKey(alias: string): Promise<{
32+
success: boolean;
33+
error?: string;
34+
}>;
35+
keyExists(alias: string): Promise<{
36+
success: boolean;
37+
exists: boolean;
38+
}>;
39+
isHardwareBackedKeystoreAvailable(): Promise<{
40+
success: boolean;
41+
available: boolean;
42+
}>;
543
}
644

745
export default TurboModuleRegistry.getEnforcing<Spec>('RnAndroidKeystore');

0 commit comments

Comments
 (0)