package com.couchbase.lite.auth;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import com.couchbase.lite.util.Base64;
import com.couchbase.lite.util.ConversionUtils;
import com.couchbase.lite.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
/**
* Created by hideki on 6/23/16.
*/
public class AESSecureTokenStore implements TokenStore {
////////////////////////////////////////////////////////////
// Constant variables
////////////////////////////////////////////////////////////
public static final String TAG = Log.TAG_SYNC;
// https://developer.android.com/training/articles/keystore.html#SupportedCiphers
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
private static final String serviceName = "CouchbaseLite";
private static final String alias = "CouchbaseLiteTokenStoreAES";
private static final boolean hasKeyStore = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; // API 18
private static final boolean hasKeyGenerator = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; // API 23
////////////////////////////////////////////////////////////
// Member variables
////////////////////////////////////////////////////////////
private Context context = null;
////////////////////////////////////////////////////////////
// Constructors
////////////////////////////////////////////////////////////
public AESSecureTokenStore(Context context) {
this.context = context;
initializePrivateKey(context);
}
////////////////////////////////////////////////////////////
// Implementation of TokenStore
////////////////////////////////////////////////////////////
@TargetApi(Build.VERSION_CODES.M)
@Override
public Map<String, String> loadTokens(URL remoteURL, String localUUID) throws Exception {
if (!hasKeyStore || !hasKeyGenerator)
return null;
SharedPreferences prefs = context.getSharedPreferences(serviceName, Context.MODE_PRIVATE);
String key = getKey(remoteURL, localUUID);
String base64EncryptedStr = prefs.getString(key, null);
if (base64EncryptedStr == null)
return null;
String base64Iv = prefs.getString(key + "_iv", null);
if (base64Iv == null)
return null;
return decrypt(base64EncryptedStr, base64Iv);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public boolean saveTokens(URL remoteURL, String localUUID, Map<String, String> tokens) {
if (!hasKeyStore || !hasKeyGenerator)
return false;
String[] encryptedData = encrypt(tokens);
if (encryptedData == null)
return false;
SharedPreferences prefs = context.getSharedPreferences(serviceName, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
String key = getKey(remoteURL, localUUID);
editor.putString(key, encryptedData[0]);
editor.putString(key + "_iv", encryptedData[1]);
return editor.commit();
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public boolean deleteTokens(URL remoteURL, String localUUID) {
if (!hasKeyStore || !hasKeyGenerator)
return false;
SharedPreferences prefs = context.getSharedPreferences(serviceName, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
String key = getKey(remoteURL, localUUID);
editor.remove(key);
return editor.commit();
}
////////////////////////////////////////////////////////////
// protected/private methods
////////////////////////////////////////////////////////////
private String getKey(URL remoteURL, String localUUID) {
String service = remoteURL.toExternalForm();
String label = String.format(Locale.ENGLISH, "%s OpenID Connect tokens", remoteURL.getHost());
if (localUUID == null)
return String.format(Locale.ENGLISH, "%s%s%s", alias, label, service);
else
return String.format(Locale.ENGLISH, "%s%s%s%s", alias, label, service, localUUID);
}
@TargetApi(Build.VERSION_CODES.M)
private String[] encrypt(Map<String, String> map) {
byte[] bytes = ConversionUtils.toByteArray(map);
if (bytes == null)
return null;
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
SecretKey secretKey = (SecretKey) keyStore.getKey(alias, null);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
byte[] iv = null;
try {
cipherOutputStream.write(bytes);
iv = cipher.getIV();
} finally {
cipherOutputStream.close();
}
byte[] encrypted = outputStream.toByteArray();
String[] encryptedData = new String[2];
encryptedData[0] = Base64.encodeToString(encrypted, Base64.DEFAULT);
encryptedData[1] = Base64.encodeToString(iv, Base64.DEFAULT);
return encryptedData;
} catch (Exception ex) {
Log.e(TAG, "Unable to open KeyStore", ex);
return null;
}
}
@TargetApi(Build.VERSION_CODES.M)
private Map decrypt(String base64EncryptedStr, String base64Iv) {
byte[] decrypted = null;
try {
byte[] encrypted = Base64.decode(base64EncryptedStr, Base64.DEFAULT);
byte[] iv = Base64.decode(base64Iv, Base64.DEFAULT);
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
SecretKey secretKey = (SecretKey) keyStore.getKey(alias, null);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
ByteArrayInputStream inputStream = new ByteArrayInputStream(encrypted);
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
try {
ArrayList<Byte> values = new ArrayList<Byte>();
int nextByte;
while ((nextByte = cipherInputStream.read()) != -1) {
values.add((byte) nextByte);
}
decrypted = new byte[values.size()];
for (int i = 0; i < decrypted.length; i++) {
decrypted[i] = values.get(i).byteValue();
}
} finally {
cipherInputStream.close();
}
} catch (Exception ex) {
Log.e(TAG, "Unable to open KeyStore", ex);
return null;
}
try {
return ConversionUtils.fromByteArray(decrypted);
} catch (IOException e) {
Log.e(TAG, "Unable to decrypt: value=<%s>", e, base64EncryptedStr);
return null;
}
}
@TargetApi(Build.VERSION_CODES.M)
private void initializePrivateKey(Context context) {
if (!hasKeyStore || !hasKeyGenerator)
return;
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
if (keyStore.containsAlias(alias))
return;
} catch (Exception ex) {
Log.e(TAG, "Unable to open KeyStore", ex);
return;
}
// Create the keys if necessary
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(new KeyGenParameterSpec.Builder(alias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
SecretKey secretKey = keyGenerator.generateKey();
} catch (Exception ex) {
Log.e(TAG, "Unable to create new key", ex);
return;
}
}
}