// // Copyright (c) 2016 Couchbase, Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file // except in compliance with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software distributed under the // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing permissions // and limitations under the License. // package com.couchbase.lite.auth; import android.annotation.TargetApi; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.security.KeyPairGeneratorSpec; import com.couchbase.lite.support.security.SymmetricKey; import com.couchbase.lite.support.security.SymmetricKeyException; 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.math.BigInteger; import java.net.URL; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.Calendar; import java.util.Locale; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.security.auth.x500.X500Principal; /** * Created by hideki on 6/22/16. */ public class RSASecureTokenStore implements TokenStore { //////////////////////////////////////////////////////////// // Constant variables //////////////////////////////////////////////////////////// private static final String TAG = Log.TAG_SYNC; // https://developer.android.com/training/articles/keystore.html#SupportedCiphers private static final String CIPHER_ALGORITHM_RSA = "RSA/ECB/PKCS1Padding"; // https://developer.android.com/reference/java/security/KeyPairGenerator.html private static final String KEYPAIRGEN_ALGORITHM = "RSA"; private static final String serviceName = "CouchbaseLite"; private static final String alias = "CouchbaseLiteTokenStoreRSA"; private static final boolean hasKeyStore = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; // API 18 //////////////////////////////////////////////////////////// // Member variables //////////////////////////////////////////////////////////// private Context context = null; //////////////////////////////////////////////////////////// // Constructors //////////////////////////////////////////////////////////// public RSASecureTokenStore(Context context) { this.context = context; initializePrivateKey(context); } //////////////////////////////////////////////////////////// // Implementation of TokenStore //////////////////////////////////////////////////////////// @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) @Override public Map<String, String> loadTokens(URL remoteURL, String localUUID) throws Exception { if (!hasKeyStore) return null; SharedPreferences prefs = context.getSharedPreferences(serviceName, Context.MODE_PRIVATE); String key = getKey(remoteURL, localUUID); if (!prefs.contains(key + "_key")) return null; if (!prefs.contains(key + "_data")) return null; byte[] secretKey = Base64.decode(prefs.getString(key + "_key", null), Base64.DEFAULT); byte[] data = Base64.decode(prefs.getString(key + "_data", null), Base64.DEFAULT); return decrypt(secretKey, data); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) @Override public boolean saveTokens(URL remoteURL, String localUUID, Map<String, String> tokens) { if (!hasKeyStore) return false; byte[][] encrypted = encrypt(tokens); if (encrypted == null || encrypted.length != 2) return false; SharedPreferences prefs = context.getSharedPreferences(serviceName, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); String key = getKey(remoteURL, localUUID); editor.putString(key + "_key", Base64.encodeToString(encrypted[0], Base64.DEFAULT)); editor.putString(key + "_data", Base64.encodeToString(encrypted[1], Base64.DEFAULT)); return editor.commit(); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) @Override public boolean deleteTokens(URL remoteURL, String localUUID) { if (!hasKeyStore) return false; SharedPreferences prefs = context.getSharedPreferences(serviceName, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); String key = getKey(remoteURL, localUUID); editor.remove(key + "_key"); editor.remove(key + "_data"); return editor.commit(); } //////////////////////////////////////////////////////////// // protected/private methods //////////////////////////////////////////////////////////// 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); } byte[][] encrypt(Map<String, String> map) { // convert from Map to byte[] byte[] bytes = ConversionUtils.toByteArray(map); if (bytes == null) return null; try { // initialize Symmetric Key SymmetricKey symmetricKey = new SymmetricKey(); // encrypt symmetricKey byte[] encryptedKey = encryptDataByRSA(getRSAPublicKeyFromKeyStore(), symmetricKey.getKey()); if (encryptedKey == null) return null; // return encrypted symmetric key, encrypted datav byte[][] data = new byte[2][]; data[0] = encryptedKey; data[1] = symmetricKey.encryptData(bytes); return data; } catch (SymmetricKeyException ex) { Log.e(TAG, "Error in encryption", ex); return null; } } Map decrypt(byte[] encryptedKey, byte[] encryptedData) { try { // decrypt symmetric Key by RSA, and initialize symmetric key SymmetricKey symmetricKey = new SymmetricKey(decryptDataByRSA(getRSAPrivateKeyFromKeyStore(), encryptedKey)); if (symmetricKey == null) return null; // decrypt data byte[] bytes = symmetricKey.decryptData(encryptedData); if (bytes == null) return null; return ConversionUtils.fromByteArray(bytes); } catch (Exception ex) { Log.e(TAG, "Error in decryption", ex); return null; } } // get RSA public key from KeyStore static RSAPublicKey getRSAPublicKeyFromKeyStore() { try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null); RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey(); return publicKey; } catch (Exception ex) { Log.e(TAG, "Unable to open KeyStore or to get RSA key", ex); return null; } } // get RSA private key from KeyStore static RSAPrivateKey getRSAPrivateKeyFromKeyStore() { try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null); RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey(); return privateKey; } catch (Exception ex) { Log.e(TAG, "Unable to open KeyStore or to get RSA key", ex); return null; } } // encrypt data by RSA static byte[] encryptDataByRSA(RSAPublicKey publicKey, byte[] data) { try { Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_RSA); cipher.init(Cipher.ENCRYPT_MODE, publicKey); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { CipherOutputStream cos = new CipherOutputStream(bos, cipher); try { cos.write(data); } finally { cos.close(); } return bos.toByteArray(); } finally { bos.close(); } } catch (Exception ex) { Log.e(TAG, "Unable to open KeyStore", ex); return null; } } // decrypt data by RSA static byte[] decryptDataByRSA(RSAPrivateKey privateKey, byte[] encryptedData) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(2048); try { Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_RSA); cipher.init(Cipher.DECRYPT_MODE, privateKey); ByteArrayInputStream bis = new ByteArrayInputStream(encryptedData); try { CipherInputStream cis = new CipherInputStream(bis, cipher); try { byte[] read = new byte[512]; // Your buffer size. for (int i; (i = cis.read(read)) != -1; ) bos.write(read, 0, i); } finally { cis.close(); } } finally { bis.close(); } return bos.toByteArray(); } finally { bos.close(); } } catch (Exception ex) { Log.e(TAG, "Unable to decrypt data", ex); return null; } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private void initializePrivateKey(Context context) { if (!hasKeyStore) 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 { // https://developer.android.com/reference/android/security/KeyPairGeneratorSpec.Builder.html Calendar start = Calendar.getInstance(); Calendar end = Calendar.getInstance(); end.add(Calendar.YEAR, 1); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context) .setAlias(alias) .setSubject(new X500Principal("CN=" + alias)) .setSerialNumber(BigInteger.valueOf(1337)) .setStartDate(start.getTime()) .setEndDate(end.getTime()) .build(); KeyPairGenerator generator = KeyPairGenerator.getInstance(KEYPAIRGEN_ALGORITHM, "AndroidKeyStore"); generator.initialize(spec); KeyPair keyPair = generator.generateKeyPair(); } catch (Exception ex) { Log.e(TAG, "Unable to create new key", ex); return; } } }