Skip to content

Commit 4bb26d8

Browse files
Merge pull request #86 from tombailey/master
Use encrypted shared preferences from androidx.security
2 parents a054f16 + 29ac563 commit 4bb26d8

File tree

4 files changed

+31
-204
lines changed

4 files changed

+31
-204
lines changed

android/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ android {
2626
buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3')
2727

2828
defaultConfig {
29-
minSdkVersion safeExtGet('minSdkVersion', 16)
29+
minSdkVersion safeExtGet('minSdkVersion', 23)
3030
targetSdkVersion safeExtGet('targetSdkVersion', 28)
3131
versionCode 1
3232
versionName "1.0"
@@ -44,4 +44,5 @@ repositories {
4444

4545
dependencies {
4646
implementation 'com.facebook.react:react-native:+'
47+
implementation "androidx.security:security-crypto:1.0.0-rc03"
4748
}

android/src/main/java/com/reactlibrary/securekeystore/RNSecureKeyStoreModule.java

Lines changed: 28 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,23 @@
66

77
package com.reactlibrary.securekeystore;
88

9-
import android.content.Context;
9+
import android.content.SharedPreferences;
1010
import android.os.Build;
11-
import android.security.KeyPairGeneratorSpec;
1211
import android.util.Log;
13-
import java.util.Locale;
12+
13+
import androidx.annotation.Nullable;
14+
import androidx.security.crypto.EncryptedSharedPreferences;
15+
import androidx.security.crypto.MasterKeys;
1416

1517
import com.facebook.react.bridge.Promise;
1618
import com.facebook.react.bridge.ReactApplicationContext;
1719
import com.facebook.react.bridge.ReactContextBaseJavaModule;
1820
import com.facebook.react.bridge.ReactMethod;
21+
import com.facebook.react.bridge.ReadableMap;
1922

20-
import java.io.ByteArrayInputStream;
21-
import java.io.ByteArrayOutputStream;
2223
import java.io.FileNotFoundException;
2324
import java.io.IOException;
24-
import java.math.BigInteger;
2525
import java.security.GeneralSecurityException;
26-
import java.security.KeyPairGenerator;
27-
import java.security.KeyStore;
28-
import java.security.PrivateKey;
29-
import java.security.PublicKey;
30-
import java.util.Calendar;
31-
import androidx.annotation.Nullable;
32-
import com.facebook.react.bridge.ReadableMap;
33-
34-
import javax.crypto.Cipher;
35-
import javax.crypto.CipherInputStream;
36-
import javax.crypto.CipherOutputStream;
37-
import javax.crypto.KeyGenerator;
38-
import javax.crypto.SecretKey;
39-
import javax.crypto.spec.SecretKeySpec;
40-
import javax.security.auth.x500.X500Principal;
4126

4227
public class RNSecureKeyStoreModule extends ReactContextBaseJavaModule {
4328

@@ -56,7 +41,7 @@ public String getName() {
5641
@ReactMethod
5742
public void set(String alias, String input, @Nullable ReadableMap options, Promise promise) {
5843
try {
59-
setCipherText(alias, input);
44+
getSecureSharedPreferences().edit().putString(alias, input).commit();
6045
promise.resolve("stored ciphertext in app storage");
6146
} catch (Exception e) {
6247
e.printStackTrace();
@@ -65,89 +50,26 @@ public void set(String alias, String input, @Nullable ReadableMap options, Promi
6550
}
6651
}
6752

68-
private PublicKey getOrCreatePublicKey(String alias) throws GeneralSecurityException, IOException {
69-
Locale currentLocale = Locale.getDefault();
70-
Locale.setDefault(Locale.ENGLISH);
71-
KeyStore keyStore = KeyStore.getInstance(getKeyStore());
72-
keyStore.load(null);
73-
74-
if (!keyStore.containsAlias(alias) || keyStore.getCertificate(alias) == null) {
75-
Log.i(Constants.TAG, "no existing asymmetric keys for alias");
76-
77-
Calendar start = Calendar.getInstance();
78-
Calendar end = Calendar.getInstance();
79-
end.add(Calendar.YEAR, 50);
80-
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(getContext())
81-
.setAlias(alias)
82-
.setSubject(new X500Principal("CN=" + alias))
83-
.setSerialNumber(BigInteger.ONE)
84-
.setStartDate(start.getTime())
85-
.setEndDate(end.getTime())
86-
.build();
87-
88-
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", getKeyStore());
89-
generator.initialize(spec);
90-
generator.generateKeyPair();
91-
92-
Locale.setDefault(currentLocale);
93-
Log.i(Constants.TAG, "created new asymmetric keys for alias");
94-
}
95-
96-
return keyStore.getCertificate(alias).getPublicKey();
97-
}
98-
99-
private byte[] encryptRsaPlainText(PublicKey publicKey, byte[] plainTextBytes) throws GeneralSecurityException, IOException {
100-
Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
101-
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
102-
return encryptCipherText(cipher, plainTextBytes);
103-
}
104-
105-
private byte[] encryptAesPlainText(SecretKey secretKey, String plainText) throws GeneralSecurityException, IOException {
106-
Cipher cipher = Cipher.getInstance(Constants.AES_ALGORITHM);
107-
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
108-
return encryptCipherText(cipher, plainText);
109-
}
110-
111-
private byte[] encryptCipherText(Cipher cipher, String plainText) throws GeneralSecurityException, IOException {
112-
return encryptCipherText(cipher, plainText.getBytes("UTF-8"));
113-
}
114-
115-
private byte[] encryptCipherText(Cipher cipher, byte[] plainTextBytes) throws GeneralSecurityException, IOException {
116-
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
117-
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
118-
cipherOutputStream.write(plainTextBytes);
119-
cipherOutputStream.close();
120-
return outputStream.toByteArray();
121-
}
122-
123-
private SecretKey getOrCreateSecretKey(String alias) throws GeneralSecurityException, IOException {
124-
try {
125-
return getSymmetricKey(alias);
126-
} catch (FileNotFoundException fnfe) {
127-
Log.i(Constants.TAG, "no existing symmetric key for alias");
128-
129-
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
130-
//32bytes / 256bits AES key
131-
keyGenerator.init(256);
132-
SecretKey secretKey = keyGenerator.generateKey();
133-
PublicKey publicKey = getOrCreatePublicKey(alias);
134-
Storage.writeValues(getContext(), Constants.SKS_KEY_FILENAME + alias,
135-
encryptRsaPlainText(publicKey, secretKey.getEncoded()));
136-
137-
Log.i(Constants.TAG, "created new symmetric keys for alias");
138-
return secretKey;
139-
}
140-
}
141-
142-
private void setCipherText(String alias, String input) throws GeneralSecurityException, IOException {
143-
Storage.writeValues(getContext(), Constants.SKS_DATA_FILENAME + alias,
144-
encryptAesPlainText(getOrCreateSecretKey(alias), input));
53+
private SharedPreferences getSecureSharedPreferences() throws GeneralSecurityException, IOException {
54+
return EncryptedSharedPreferences.create(
55+
"secret_shared_prefs",
56+
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
57+
reactContext,
58+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
59+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
60+
);
14561
}
14662

14763
@ReactMethod
14864
public void get(String alias, Promise promise) {
14965
try {
150-
promise.resolve(getPlainText(alias));
66+
String value = getSecureSharedPreferences().getString(alias, null);
67+
if (value == null) {
68+
//throw FileNotFoundException to keep match old behaviour when a value is missing
69+
throw new FileNotFoundException(alias + " has not been set");
70+
} else {
71+
promise.resolve(value);
72+
}
15173
} catch (FileNotFoundException fnfe) {
15274
fnfe.printStackTrace();
15375
promise.reject("404", "{\"code\":404,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + fnfe.getMessage() + "}", fnfe);
@@ -158,73 +80,15 @@ public void get(String alias, Promise promise) {
15880
}
15981
}
16082

161-
private PrivateKey getPrivateKey(String alias) throws GeneralSecurityException, IOException {
162-
KeyStore keyStore = KeyStore.getInstance(getKeyStore());
163-
keyStore.load(null);
164-
return (PrivateKey) keyStore.getKey(alias, null);
165-
}
166-
167-
private byte[] decryptRsaCipherText(PrivateKey privateKey, byte[] cipherTextBytes) throws GeneralSecurityException, IOException {
168-
Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
169-
cipher.init(Cipher.DECRYPT_MODE, privateKey);
170-
return decryptCipherText(cipher, cipherTextBytes);
171-
}
172-
173-
private byte[] decryptAesCipherText(SecretKey secretKey, byte[] cipherTextBytes) throws GeneralSecurityException, IOException {
174-
Cipher cipher = Cipher.getInstance(Constants.AES_ALGORITHM);
175-
cipher.init(Cipher.DECRYPT_MODE, secretKey);
176-
return decryptCipherText(cipher, cipherTextBytes);
177-
}
178-
179-
private byte[] decryptCipherText(Cipher cipher, byte[] cipherTextBytes) throws IOException {
180-
ByteArrayInputStream bais = new ByteArrayInputStream(cipherTextBytes);
181-
CipherInputStream cipherInputStream = new CipherInputStream(bais, cipher);
182-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
183-
byte[] buffer = new byte[256];
184-
int bytesRead = cipherInputStream.read(buffer);
185-
while (bytesRead != -1) {
186-
baos.write(buffer, 0, bytesRead);
187-
bytesRead = cipherInputStream.read(buffer);
188-
}
189-
return baos.toByteArray();
190-
}
191-
192-
private SecretKey getSymmetricKey(String alias) throws GeneralSecurityException, IOException {
193-
byte[] cipherTextBytes = Storage.readValues(getContext(), Constants.SKS_KEY_FILENAME + alias);
194-
return new SecretKeySpec(decryptRsaCipherText(getPrivateKey(alias), cipherTextBytes), Constants.AES_ALGORITHM);
195-
}
196-
197-
public String getPlainText(String alias) throws GeneralSecurityException, IOException {
198-
SecretKey secretKey = getSymmetricKey(alias);
199-
byte[] cipherTextBytes = Storage.readValues(getContext(), Constants.SKS_DATA_FILENAME + alias);
200-
return new String(decryptAesCipherText(secretKey, cipherTextBytes), "UTF-8");
201-
}
202-
20383
@ReactMethod
20484
public void remove(String alias, Promise promise) {
205-
Storage.resetValues(getContext(), new String[] {
206-
Constants.SKS_DATA_FILENAME + alias,
207-
Constants.SKS_KEY_FILENAME + alias,
208-
});
209-
promise.resolve("cleared alias");
210-
}
211-
212-
private Context getContext() {
213-
return getReactApplicationContext();
214-
}
215-
216-
private String getKeyStore() {
21785
try {
218-
KeyStore.getInstance(Constants.KEYSTORE_PROVIDER_1);
219-
return Constants.KEYSTORE_PROVIDER_1;
220-
} catch (Exception err) {
221-
try {
222-
KeyStore.getInstance(Constants.KEYSTORE_PROVIDER_2);
223-
return Constants.KEYSTORE_PROVIDER_2;
224-
} catch (Exception e) {
225-
return Constants.KEYSTORE_PROVIDER_3;
226-
}
86+
getSecureSharedPreferences().edit().remove(alias).commit();
87+
promise.resolve("cleared alias");
88+
} catch (Exception e) {
89+
e.printStackTrace();
90+
Log.e(Constants.TAG, "Exception: " + e.getMessage());
91+
promise.reject("{\"code\":6,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + e.getMessage() + "}");
22792
}
22893
}
229-
23094
}

android/src/main/java/com/reactlibrary/securekeystore/Storage.java

Lines changed: 0 additions & 38 deletions
This file was deleted.

example/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
buildscript {
44
ext {
55
buildToolsVersion = "28.0.2"
6-
minSdkVersion = 16
6+
minSdkVersion = 23
77
compileSdkVersion = 28
88
targetSdkVersion = 27
99
supportLibVersion = "28.0.0"

0 commit comments

Comments
 (0)