Skip to content

Commit 136bc81

Browse files
committed
Update encryption method to support api level 18+
1 parent 696d09e commit 136bc81

File tree

4 files changed

+161
-85
lines changed

4 files changed

+161
-85
lines changed

skygear/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ android {
4242
useLibrary 'org.apache.http.legacy'
4343

4444
defaultConfig {
45-
minSdkVersion 16
45+
minSdkVersion 18
4646
targetSdkVersion 26
4747
versionCode 1
4848
versionName skygearVersion

skygear/src/main/java/io/skygear/skygear/PersistentStore.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class PersistentStore {
4242
static final String DEVICE_ID_KEY = "device_id";
4343
static final String DEVICE_TOKEN_KEY = "device_token";
4444

45-
private final Context context;
45+
final Context context;
4646
/**
4747
* The Current user.
4848
*/

skygear/src/main/java/io/skygear/skygear/SecurePersistentStore.java

+158-82
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,39 @@
33
import android.content.Context;
44
import android.content.SharedPreferences;
55
import android.os.Build;
6-
import android.security.keystore.KeyGenParameterSpec;
7-
import android.security.keystore.KeyProperties;
6+
import android.security.KeyPairGeneratorSpec;
87
import android.util.Base64;
98
import android.util.Log;
109

1110
import org.json.JSONObject;
1211

12+
import java.io.ByteArrayInputStream;
13+
import java.io.ByteArrayOutputStream;
1314
import java.io.IOException;
14-
import java.io.UnsupportedEncodingException;
15+
import java.math.BigInteger;
1516
import java.security.InvalidAlgorithmParameterException;
1617
import java.security.InvalidKeyException;
18+
import java.security.KeyPair;
19+
import java.security.KeyPairGenerator;
1720
import java.security.KeyStore;
1821
import java.security.KeyStoreException;
1922
import java.security.NoSuchAlgorithmException;
2023
import java.security.NoSuchProviderException;
24+
import java.security.PublicKey;
25+
import java.security.SecureRandom;
2126
import java.security.UnrecoverableEntryException;
2227
import java.security.cert.CertificateException;
28+
import java.util.ArrayList;
29+
import java.util.Calendar;
2330

2431
import javax.crypto.BadPaddingException;
2532
import javax.crypto.Cipher;
33+
import javax.crypto.CipherInputStream;
34+
import javax.crypto.CipherOutputStream;
2635
import javax.crypto.IllegalBlockSizeException;
27-
import javax.crypto.KeyGenerator;
2836
import javax.crypto.NoSuchPaddingException;
29-
import javax.crypto.SecretKey;
30-
import javax.crypto.spec.GCMParameterSpec;
37+
import javax.crypto.spec.SecretKeySpec;
38+
import javax.security.auth.x500.X500Principal;
3139

3240
/**
3341
* The Skygear secure persistent store.
@@ -38,15 +46,15 @@
3846
class SecurePersistentStore extends PersistentStore {
3947
private static final String TAG = "Skygear SDK";
4048

41-
static final String ACCESS_TOKEN_KEY_ALIAS = "SkygearAccessTokenKey";
42-
static final String CURRENT_USER_KEY_ALIAS = "SkygearCurrentUserKey";
49+
static final String KEY_ALIAS = "SkygearKey";
4350

4451
static final String CURRENT_USER_ENCRYPTED_KEY = "current_user_encrypted";
4552
static final String ACCESS_TOKEN_ENCRYPTED_KEY = "access_token_encrypted";
46-
static final String CURRENT_USER_IV_KEY = "current_user_iv";
47-
static final String ACCESS_TOKEN_IV_KEY = "access_token_iv";
53+
static final String CURRENT_USER_ENCRYPTED_KEY_KEY = "current_user_encrypted_key";
54+
static final String ACCESS_TOKEN_ENCRYPTED_KEY_KEY = "access_token_encrypted_key";
4855

49-
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
56+
private static final String RSA_MODE = "RSA/ECB/PKCS1Padding";
57+
private static final String AES_MODE = "AES/ECB/PKCS7Padding";
5058
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
5159

5260
Encryptor encryptor;
@@ -59,17 +67,15 @@ public SecurePersistentStore(Context context) {
5967
@Override
6068
void restoreAuthUser(SharedPreferences pref) {
6169
String encryptedCurrentUser = pref.getString(CURRENT_USER_ENCRYPTED_KEY, null);
62-
String encodedIV = pref.getString(CURRENT_USER_IV_KEY, null);
70+
String encryptedKey = pref.getString(CURRENT_USER_ENCRYPTED_KEY_KEY, null);
6371

64-
if (encryptedCurrentUser == null || encodedIV == null) {
72+
if (encryptedCurrentUser == null || encryptedKey == null) {
6573
this.currentUser = null;
6674
return;
6775
}
6876

6977
try {
70-
byte[] encryptedData = base64DecodeToByte(encryptedCurrentUser);
71-
byte[] iv = base64DecodeToByte(encodedIV);
72-
String currentUserString = getDecryptor().decryptData(CURRENT_USER_KEY_ALIAS, encryptedData, iv);
78+
String currentUserString = getDecryptor().decryptData(encryptedKey, encryptedCurrentUser);
7379
this.currentUser = RecordSerializer.deserialize(
7480
new JSONObject(currentUserString)
7581
);
@@ -84,34 +90,30 @@ void saveAuthUser(SharedPreferences.Editor prefEditor) {
8490
if (this.currentUser != null) {
8591
String currentUserString = RecordSerializer.serialize(this.currentUser).toString();
8692
try {
87-
EncryptedResult r = getEncryptor().encryptText(CURRENT_USER_KEY_ALIAS, currentUserString);
88-
String encryptedCurrentUser = base64EncodeToString(r.encryptedData);
89-
String encodedIV = base64EncodeToString(r.initializationVector);
90-
prefEditor.putString(CURRENT_USER_ENCRYPTED_KEY, encryptedCurrentUser);
91-
prefEditor.putString(CURRENT_USER_IV_KEY, encodedIV);
93+
EncryptedResult r = getEncryptor().encryptText(currentUserString);
94+
prefEditor.putString(CURRENT_USER_ENCRYPTED_KEY, r.encryptedData);
95+
prefEditor.putString(CURRENT_USER_ENCRYPTED_KEY_KEY, r.encryptedKey);
9296
} catch (Exception e) {
9397
Log.w(TAG, "Fail to encrypt and save current user object", e);
9498
}
9599
} else {
96100
prefEditor.remove(CURRENT_USER_ENCRYPTED_KEY);
97-
prefEditor.remove(CURRENT_USER_IV_KEY);
101+
prefEditor.remove(CURRENT_USER_ENCRYPTED_KEY_KEY);
98102
}
99103
}
100104

101105
@Override
102106
void restoreAccessToken(SharedPreferences pref) {
103107
String encryptedAccessToken = pref.getString(ACCESS_TOKEN_ENCRYPTED_KEY, null);
104-
String encodedIV = pref.getString(ACCESS_TOKEN_IV_KEY, null);
108+
String encryptedKey = pref.getString(ACCESS_TOKEN_ENCRYPTED_KEY_KEY, null);
105109

106-
if (encryptedAccessToken == null || encodedIV == null) {
110+
if (encryptedAccessToken == null || encryptedKey == null) {
107111
this.accessToken = null;
108112
return;
109113
}
110114

111115
try {
112-
byte[] encryptedData = base64DecodeToByte(encryptedAccessToken);
113-
byte[] iv = base64DecodeToByte(encodedIV);
114-
this.accessToken = getDecryptor().decryptData(ACCESS_TOKEN_KEY_ALIAS, encryptedData, iv);
116+
this.accessToken = getDecryptor().decryptData(encryptedKey, encryptedAccessToken);
115117
} catch (Exception e) {
116118
Log.w(TAG, "Fail to decrypt saved access token", e);
117119
this.accessToken = null;
@@ -122,105 +124,179 @@ void restoreAccessToken(SharedPreferences pref) {
122124
void saveAccessToken(SharedPreferences.Editor prefEditor) {
123125
if (this.accessToken != null) {
124126
try {
125-
EncryptedResult r = getEncryptor().encryptText(ACCESS_TOKEN_KEY_ALIAS, this.accessToken);
126-
String ecryptedAccessToken = base64EncodeToString(r.encryptedData);
127-
String encodedIV = base64EncodeToString(r.initializationVector);
128-
prefEditor.putString(ACCESS_TOKEN_ENCRYPTED_KEY, ecryptedAccessToken);
129-
prefEditor.putString(ACCESS_TOKEN_IV_KEY, encodedIV);
127+
EncryptedResult r = getEncryptor().encryptText(this.accessToken);
128+
prefEditor.putString(ACCESS_TOKEN_ENCRYPTED_KEY, r.encryptedData);
129+
prefEditor.putString(ACCESS_TOKEN_ENCRYPTED_KEY_KEY, r.encryptedKey);
130130
} catch (Exception e) {
131131
Log.w(TAG, "Fail to encrypt and save access token", e);
132132
}
133133
} else {
134134
prefEditor.remove(ACCESS_TOKEN_ENCRYPTED_KEY);
135-
prefEditor.remove(ACCESS_TOKEN_IV_KEY);
135+
prefEditor.remove(ACCESS_TOKEN_ENCRYPTED_KEY_KEY);
136136
}
137137
}
138138

139-
protected String base64EncodeToString(byte[] b) {
140-
return Base64.encodeToString(b, Base64.DEFAULT);
141-
}
142-
143-
protected byte[] base64DecodeToByte(String s) {
144-
return Base64.decode(s, Base64.DEFAULT);
145-
}
146-
147-
protected Encryptor getEncryptor() {
139+
private Encryptor getEncryptor() {
148140
if (encryptor == null) {
149-
encryptor = new Encryptor();
141+
encryptor = new Encryptor(this.context);
150142
}
151143
return encryptor;
152144
}
153145

154-
protected Decryptor getDecryptor() {
146+
private Decryptor getDecryptor() {
155147
if (decryptor == null) {
156148
decryptor = new Decryptor();
157149
}
158150
return decryptor;
159151
}
160152

153+
private String getRSACipherProvider() {
154+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
155+
return "AndroidOpenSSL";
156+
} else {
157+
return "AndroidKeyStoreBCWorkaround";
158+
}
159+
}
160+
161161
class Decryptor {
162162

163163
private KeyStore keyStore;
164164

165-
String decryptData(final String alias, final byte[] encrypted, final byte[] iv) throws NoSuchPaddingException,
166-
NoSuchAlgorithmException, UnrecoverableEntryException, KeyStoreException, InvalidAlgorithmParameterException,
167-
InvalidKeyException, BadPaddingException, IllegalBlockSizeException, IOException, CertificateException {
168-
final Cipher cipher = Cipher.getInstance(TRANSFORMATION);
169-
final GCMParameterSpec spec = new GCMParameterSpec(128, iv);
170-
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(alias), spec);
171-
return new String(cipher.doFinal(encrypted), "UTF-8");
172-
}
165+
private byte[] rsaDecrypt(byte[] encrypted) throws KeyStoreException,
166+
CertificateException, NoSuchAlgorithmException, IOException, NoSuchProviderException,
167+
NoSuchPaddingException, InvalidKeyException, UnrecoverableEntryException {
173168

174-
private SecretKey getSecretKey(final String alias) throws UnrecoverableEntryException,
175-
NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException {
176169
if (keyStore == null) {
177170
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
178171
keyStore.load(null);
179172
}
180-
return ((KeyStore.SecretKeyEntry) keyStore.getEntry(alias, null)).getSecretKey();
173+
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
174+
KEY_ALIAS, null);
175+
Cipher output = Cipher.getInstance(RSA_MODE, getRSACipherProvider());
176+
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey());
177+
CipherInputStream cipherInputStream = new CipherInputStream(
178+
new ByteArrayInputStream(encrypted), output);
179+
ArrayList<Byte> values = new ArrayList<>();
180+
int nextByte;
181+
while ((nextByte = cipherInputStream.read()) != -1) {
182+
values.add((byte)nextByte);
183+
}
184+
185+
byte[] bytes = new byte[values.size()];
186+
for(int i = 0; i < bytes.length; i++) {
187+
bytes[i] = values.get(i).byteValue();
188+
}
189+
return bytes;
190+
}
191+
192+
String decryptData(final String encryptedKey, final String encryptedData) throws NoSuchPaddingException,
193+
NoSuchAlgorithmException, UnrecoverableEntryException, KeyStoreException,
194+
InvalidKeyException, IOException, CertificateException, NoSuchProviderException,
195+
BadPaddingException, IllegalBlockSizeException {
196+
byte[] encryptedKeyBytes = Base64.decode(encryptedKey, Base64.DEFAULT);
197+
byte[] encryptedDataBytes = Base64.decode(encryptedData, Base64.DEFAULT);
198+
byte[] secretKey = rsaDecrypt(encryptedKeyBytes);
199+
200+
Cipher c = Cipher.getInstance(AES_MODE, "BC");
201+
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secretKey, "AES"));
202+
byte[] decodedBytes = c.doFinal(encryptedDataBytes);
203+
return new String(decodedBytes, "UTF-8");
181204
}
182205
}
183206

184207
class Encryptor {
185208

186-
EncryptedResult encryptText(final String alias, final String textToEncrypt) throws NoSuchPaddingException,
187-
NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException,
188-
InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {
209+
private final Context context;
210+
private KeyStore keyStore;
189211

190-
final Cipher cipher = Cipher.getInstance(TRANSFORMATION);
191-
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(alias));
192-
byte[] iv = cipher.getIV();
212+
Encryptor(Context context) {
213+
super();
214+
this.context = context;
215+
}
193216

194-
return new EncryptedResult(cipher.doFinal(textToEncrypt.getBytes("UTF-8")), iv);
217+
EncryptedResult encryptText(final String textToEncrypt) throws NoSuchPaddingException,
218+
NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, IOException,
219+
BadPaddingException, IllegalBlockSizeException, CertificateException, KeyStoreException,
220+
UnrecoverableEntryException, InvalidAlgorithmParameterException {
221+
byte[] secretKey = generateAESSecret();
222+
byte[] encryptedKeyBytes = rsaEncrypt(secretKey);
223+
String encryptedKey = Base64.encodeToString(encryptedKeyBytes, Base64.DEFAULT);
224+
225+
Cipher c = Cipher.getInstance(AES_MODE, "BC");
226+
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secretKey, "AES"));
227+
byte[] encodedBytes = c.doFinal(textToEncrypt.getBytes("UTF-8"));
228+
String encryptedBase64Encoded = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
229+
return new EncryptedResult(encryptedKey, encryptedBase64Encoded);
195230
}
196231

197-
private SecretKey getSecretKey(final String alias) throws NoSuchAlgorithmException,
198-
NoSuchProviderException, InvalidAlgorithmParameterException {
199-
KeyGenerator keyGenerator;
200-
201-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
202-
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
203-
keyGenerator.init(new KeyGenParameterSpec.Builder(
204-
alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
205-
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
206-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
207-
.build());
208-
} else {
209-
keyGenerator = KeyGenerator.getInstance(ANDROID_KEY_STORE);
210-
// fixup init keyGenerator
232+
private byte[] generateAESSecret() {
233+
byte[] key = new byte[16];
234+
SecureRandom secureRandom = new SecureRandom();
235+
secureRandom.nextBytes(key);
236+
return key;
237+
}
238+
239+
private byte[] rsaEncrypt(byte[] secret) throws NoSuchAlgorithmException,
240+
IOException, NoSuchProviderException, NoSuchPaddingException,
241+
InvalidKeyException, InvalidAlgorithmParameterException, CertificateException,
242+
KeyStoreException, UnrecoverableEntryException {
243+
PublicKey publicKey = getRSAPublicKey();
244+
Cipher inputCipher = Cipher.getInstance(RSA_MODE, getRSACipherProvider());
245+
inputCipher.init(Cipher.ENCRYPT_MODE, publicKey);
246+
247+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
248+
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, inputCipher);
249+
cipherOutputStream.write(secret);
250+
cipherOutputStream.close();
251+
252+
byte[] vals = outputStream.toByteArray();
253+
return vals;
254+
}
255+
256+
private PublicKey getRSAPublicKey() throws NoSuchAlgorithmException,
257+
NoSuchProviderException, InvalidAlgorithmParameterException, KeyStoreException,
258+
IOException, CertificateException, UnrecoverableEntryException {
259+
if (keyStore == null) {
260+
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
261+
keyStore.load(null);
262+
}
263+
264+
if (!keyStore.containsAlias(KEY_ALIAS)) {
265+
return generateRSAPublicKey(KEY_ALIAS);
211266
}
212-
return keyGenerator.generateKey();
267+
268+
return ((KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null))
269+
.getCertificate().getPublicKey();
213270
}
214271

272+
private PublicKey generateRSAPublicKey(final String alias) throws NoSuchAlgorithmException,
273+
NoSuchProviderException, InvalidAlgorithmParameterException {
274+
Calendar start = Calendar.getInstance();
275+
Calendar end = Calendar.getInstance();
276+
end.add(Calendar.YEAR, 1);
277+
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this.context)
278+
.setAlias(alias)
279+
.setSerialNumber(BigInteger.ONE)
280+
.setSubject(new X500Principal("CN=" + alias))
281+
.setStartDate(start.getTime())
282+
.setEndDate(end.getTime())
283+
.build();
284+
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", ANDROID_KEY_STORE);
285+
generator.initialize(spec);
286+
287+
KeyPair keyPair = generator.generateKeyPair();
288+
return keyPair.getPublic();
289+
}
215290
}
216291

217292
class EncryptedResult {
218-
byte[] encryptedData;
219-
byte[] initializationVector;
293+
String encryptedKey;
294+
String encryptedData;
220295

221-
EncryptedResult(byte[] data, byte[] iv) {
222-
encryptedData = data;
223-
initializationVector = iv;
296+
EncryptedResult(String encryptedKey, String encryptedData) {
297+
super();
298+
this.encryptedKey = encryptedKey;
299+
this.encryptedData = encryptedData;
224300
}
225301
}
226302
}

skygear_example/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ android {
3636

3737
defaultConfig {
3838
applicationId "io.skygear.skygear_example"
39-
minSdkVersion 16
39+
minSdkVersion 18
4040
targetSdkVersion 26
4141
versionCode 1
4242
versionName "1.0"

0 commit comments

Comments
 (0)