Skip to content

Commit 660873f

Browse files
Merge pull request #9 from tombailey/master
Use hybrid RSA and AES keys to resolve RSA 256bytes input limit
2 parents 2f45117 + 8ae3a67 commit 660873f

File tree

5 files changed

+191
-137
lines changed

5 files changed

+191
-137
lines changed

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ android {
1616
buildToolsVersion "23.0.1"
1717

1818
defaultConfig {
19-
minSdkVersion 16
19+
minSdkVersion 18
2020
targetSdkVersion 22
2121
versionCode 1
2222
versionName "1.0"

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ public class Constants {
88
public static final String KEYSTORE_PROVIDER_1 = "AndroidKeyStore";
99
public static final String KEYSTORE_PROVIDER_2 = "AndroidKeyStoreBCWorkaround";
1010
public static final String KEYSTORE_PROVIDER_3 = "AndroidOpenSSL";
11+
1112
public static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
13+
public static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
14+
1215
public static final String TAG = "SecureKeyStore";
1316

1417
// Internal storage file
15-
public static final String SKS_FILENAME = "SKS_KEY_FILE";
18+
public static final String SKS_KEY_FILENAME = "SKS_KEY_FILE";
19+
public static final String SKS_DATA_FILENAME = "SKS_DATA_FILE";
20+
21+
public static final int MAX_CIPHERTEXT_CHUNK_LENGTH = 128;
1622
}

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

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

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

Lines changed: 145 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,34 @@
66

77
package com.reactlibrary.securekeystore;
88

9-
import com.facebook.react.bridge.NativeModule;
9+
import android.content.Context;
10+
import android.os.Build;
11+
import android.security.KeyPairGeneratorSpec;
12+
import android.util.Log;
13+
14+
import com.facebook.react.bridge.Promise;
1015
import com.facebook.react.bridge.ReactApplicationContext;
11-
import com.facebook.react.bridge.ReactContext;
1216
import com.facebook.react.bridge.ReactContextBaseJavaModule;
1317
import com.facebook.react.bridge.ReactMethod;
14-
import com.facebook.react.bridge.Promise;
1518

16-
import android.content.Context;
17-
import android.util.Log;
18-
import android.util.Base64;
19-
import android.security.KeyPairGeneratorSpec;
20-
import android.os.Build;
21-
22-
import java.security.*;
23-
import java.math.BigInteger;
24-
import java.util.ArrayList;
2519
import java.io.ByteArrayInputStream;
2620
import java.io.ByteArrayOutputStream;
27-
import java.io.FileInputStream;
28-
import java.io.FileOutputStream;
29-
import java.lang.StringBuffer;
21+
import java.io.FileNotFoundException;
22+
import java.io.IOException;
23+
import java.math.BigInteger;
24+
import java.security.GeneralSecurityException;
25+
import java.security.KeyPairGenerator;
26+
import java.security.KeyStore;
27+
import java.security.PrivateKey;
28+
import java.security.PublicKey;
3029
import java.util.Calendar;
3130

3231
import javax.crypto.Cipher;
3332
import javax.crypto.CipherInputStream;
3433
import javax.crypto.CipherOutputStream;
34+
import javax.crypto.KeyGenerator;
35+
import javax.crypto.SecretKey;
36+
import javax.crypto.spec.SecretKeySpec;
3537
import javax.security.auth.x500.X500Principal;
3638

3739
public class RNSecureKeyStoreModule extends ReactContextBaseJavaModule {
@@ -50,104 +52,163 @@ public String getName() {
5052

5153
@ReactMethod
5254
public void set(String alias, String input, Promise promise) {
53-
5455
try {
56+
setCipherText(alias, input);
57+
promise.resolve("stored ciphertext in app storage");
58+
} catch (Exception e) {
59+
e.printStackTrace();
60+
Log.e(Constants.TAG, "Exception: " + e.getMessage());
61+
promise.reject("{\"code\":9,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + e.getMessage() + "}");
62+
}
63+
}
5564

56-
KeyStore keyStore = KeyStore.getInstance(getKeyStore());
57-
keyStore.load(null);
58-
59-
if (!keyStore.containsAlias(alias)) {
60-
Calendar start = Calendar.getInstance();
61-
Calendar end = Calendar.getInstance();
62-
end.add(Calendar.YEAR, 1);
63-
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(getContext())
64-
.setAlias(alias)
65-
.setSubject(new X500Principal("CN=" + alias))
66-
.setSerialNumber(BigInteger.ONE)
67-
.setStartDate(start.getTime())
68-
.setEndDate(end.getTime()).build();
69-
70-
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", getKeyStore());
71-
generator.initialize(spec);
65+
private PublicKey getOrCreatePublicKey(String alias) throws GeneralSecurityException, IOException {
66+
KeyStore keyStore = KeyStore.getInstance(getKeyStore());
67+
keyStore.load(null);
7268

73-
KeyPair keyPair = generator.generateKeyPair();
69+
if (!keyStore.containsAlias(alias)) {
70+
Log.i(Constants.TAG, "no existing asymmetric keys for alias");
7471

75-
Log.i(Constants.TAG, "created new key pairs");
76-
}
72+
Calendar start = Calendar.getInstance();
73+
Calendar end = Calendar.getInstance();
74+
end.add(Calendar.YEAR, 50);
75+
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(getContext())
76+
.setAlias(alias)
77+
.setSubject(new X500Principal("CN=" + alias))
78+
.setSerialNumber(BigInteger.ONE)
79+
.setStartDate(start.getTime())
80+
.setEndDate(end.getTime()).build();
7781

78-
PublicKey publicKey = keyStore.getCertificate(alias).getPublicKey();
82+
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", getKeyStore());
83+
generator.initialize(spec);
84+
generator.generateKeyPair();
7985

80-
if (input.isEmpty()) {
81-
Log.d(Constants.TAG, "Exception: input text is empty");
82-
return;
83-
}
86+
Log.i(Constants.TAG, "created new asymmetric keys for alias");
87+
}
8488

85-
Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
86-
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
87-
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
88-
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
89-
cipherOutputStream.write(input.getBytes("UTF-8"));
90-
cipherOutputStream.close();
91-
byte[] vals = outputStream.toByteArray();
89+
return keyStore.getCertificate(alias).getPublicKey();
90+
}
9291

93-
// writing key to storage
94-
KeyStorage.writeValues(getContext(), alias, vals);
95-
Log.i(Constants.TAG, "key created and stored successfully");
96-
promise.resolve("key stored successfully");
92+
private byte[] encryptRsaPlainText(PublicKey publicKey, byte[] plainTextBytes) throws GeneralSecurityException, IOException {
93+
Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
94+
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
95+
return encryptCipherText(cipher, plainTextBytes);
96+
}
9797

98-
} catch (Exception e) {
99-
Log.e(Constants.TAG, "Exception: " + e.getMessage());
100-
promise.reject("{\"code\":9,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + e.getMessage() + "}");
101-
}
98+
private byte[] encryptAesPlainText(SecretKey secretKey, String plainText) throws GeneralSecurityException, IOException {
99+
Cipher cipher = Cipher.getInstance(Constants.AES_ALGORITHM);
100+
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
101+
return encryptCipherText(cipher, plainText);
102+
}
102103

104+
private byte[] encryptCipherText(Cipher cipher, String plainText) throws GeneralSecurityException, IOException {
105+
return encryptCipherText(cipher, plainText.getBytes("UTF-8"));
103106
}
104107

105-
@ReactMethod
106-
public void get(String alias, Promise promise) {
108+
private byte[] encryptCipherText(Cipher cipher, byte[] plainTextBytes) throws GeneralSecurityException, IOException {
109+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
110+
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
111+
cipherOutputStream.write(plainTextBytes);
112+
cipherOutputStream.close();
113+
return outputStream.toByteArray();
114+
}
107115

116+
private SecretKey getOrCreateSecretKey(String alias) throws GeneralSecurityException, IOException {
108117
try {
118+
return getSymmetricKey(alias);
119+
} catch (FileNotFoundException fnfe) {
120+
Log.i(Constants.TAG, "no existing symmetric key for alias");
109121

110-
KeyStore keyStore = KeyStore.getInstance(getKeyStore());
111-
keyStore.load(null);
112-
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null);
122+
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
123+
//32bytes / 256bits AES key
124+
keyGenerator.init(256);
125+
SecretKey secretKey = keyGenerator.generateKey();
126+
PublicKey publicKey = getOrCreatePublicKey(alias);
113127

114-
Cipher output = Cipher.getInstance(Constants.RSA_ALGORITHM);
115-
output.init(Cipher.DECRYPT_MODE, privateKey);
116-
CipherInputStream cipherInputStream = new CipherInputStream(
117-
new ByteArrayInputStream(KeyStorage.readValues(getContext(), alias)), output);
118128

119-
ArrayList<Byte> values = new ArrayList<Byte>();
120-
int nextByte;
121-
while ((nextByte = cipherInputStream.read()) != -1) {
122-
values.add((byte) nextByte);
123-
}
124-
byte[] bytes = new byte[values.size()];
125-
for (int i = 0; i < bytes.length; i++) {
126-
bytes[i] = values.get(i).byteValue();
127-
}
129+
//TODO: develop only
130+
Log.d(Constants.TAG, "saving secret key: " + new String(secretKey.getEncoded(), "UTF-8"));
128131

129-
String finalText = new String(bytes, 0, bytes.length, "UTF-8");
130-
promise.resolve(finalText);
131132

132-
} catch (Exception e) {
133-
Log.e(Constants.TAG, "Exception: " + e.getMessage());
134-
promise.reject("{\"code\":1,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + e.getMessage() + "}");
133+
Storage.writeValues(getContext(), Constants.SKS_KEY_FILENAME + alias,
134+
encryptRsaPlainText(publicKey, secretKey.getEncoded()));
135+
136+
Log.i(Constants.TAG, "created new symmetric keys for alias");
137+
return secretKey;
135138
}
136139
}
137140

141+
private void setCipherText(String alias, String input) throws GeneralSecurityException, IOException {
142+
Storage.writeValues(getContext(), Constants.SKS_DATA_FILENAME + alias,
143+
encryptAesPlainText(getOrCreateSecretKey(alias), input));
144+
}
145+
138146
@ReactMethod
139-
public void remove(String alias, Promise promise) {
147+
public void get(String alias, Promise promise) {
140148
try {
141-
KeyStorage.resetValues(getContext(), alias);
142-
Log.i(Constants.TAG, "key removed successfully");
143-
promise.resolve("key removed successfully");
144-
149+
promise.resolve(getPlainText(alias));
145150
} catch (Exception e) {
151+
e.printStackTrace();
146152
Log.e(Constants.TAG, "Exception: " + e.getMessage());
147-
promise.reject("{\"code\":6,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + e.getMessage() + "}");
153+
promise.reject("{\"code\":1,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + e.getMessage() + "}");
148154
}
149155
}
150156

157+
private PrivateKey getPrivateKey(String alias) throws GeneralSecurityException, IOException {
158+
KeyStore keyStore = KeyStore.getInstance(getKeyStore());
159+
keyStore.load(null);
160+
return (PrivateKey) keyStore.getKey(alias, null);
161+
}
162+
163+
private byte[] decryptRsaCipherText(PrivateKey privateKey, byte[] cipherTextBytes) throws GeneralSecurityException, IOException {
164+
Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM);
165+
cipher.init(Cipher.DECRYPT_MODE, privateKey);
166+
return decryptCipherText(cipher, cipherTextBytes);
167+
}
168+
169+
private byte[] decryptAesCipherText(SecretKey secretKey, byte[] cipherTextBytes) throws GeneralSecurityException, IOException {
170+
Cipher cipher = Cipher.getInstance(Constants.AES_ALGORITHM);
171+
cipher.init(Cipher.DECRYPT_MODE, secretKey);
172+
return decryptCipherText(cipher, cipherTextBytes);
173+
}
174+
175+
private byte[] decryptCipherText(Cipher cipher, byte[] cipherTextBytes) throws IOException {
176+
ByteArrayInputStream bais = new ByteArrayInputStream(cipherTextBytes);
177+
CipherInputStream cipherInputStream = new CipherInputStream(bais, cipher);
178+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
179+
byte[] buffer = new byte[256];
180+
int bytesRead = cipherInputStream.read(buffer);
181+
while (bytesRead != -1) {
182+
baos.write(buffer, 0, bytesRead);
183+
bytesRead = cipherInputStream.read(buffer);
184+
}
185+
return baos.toByteArray();
186+
}
187+
188+
private SecretKey getSymmetricKey(String alias) throws GeneralSecurityException, IOException {
189+
byte[] cipherTextBytes = Storage.readValues(getContext(), Constants.SKS_KEY_FILENAME + alias);
190+
191+
//TODO: develop only
192+
Log.d(Constants.TAG, "reading secret key: " + new String(decryptRsaCipherText(getPrivateKey(alias), cipherTextBytes), "UTF-8"));
193+
194+
return new SecretKeySpec(decryptRsaCipherText(getPrivateKey(alias), cipherTextBytes), Constants.AES_ALGORITHM);
195+
}
196+
197+
private 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+
203+
@ReactMethod
204+
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+
151212
private Context getContext() {
152213
return getReactApplicationContext();
153214
}

0 commit comments

Comments
 (0)