package com.yakivmospan.scytale; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.security.KeyPairGeneratorSpec; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.support.annotation.NonNull; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.UnrecoverableEntryException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.util.Calendar; import java.util.Date; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.security.auth.x500.X500Principal; import static java.security.KeyStore.getDefaultType; import static java.security.KeyStore.getInstance; /** * API to create, save and get keys */ public class Store extends ErrorHandler { private static final String PROVIDER_BC = "BC"; private static final String PROVIDER_ANDROID_KEY_STORE = "AndroidKeyStore"; private static final String DEFAULT_KEYSTORE_NAME = "keystore"; private static final char[] DEFAULT_KEYSTORE_PASSWORD = BuildConfig.APPLICATION_ID.toCharArray(); private String mKeystoreName = DEFAULT_KEYSTORE_NAME; private char[] mKeystorePassword = DEFAULT_KEYSTORE_PASSWORD; private final File mKeystoreFile; private final Context mContext; /** * Creates a store with default name and password. Name is "keystore" and password is application id * * @param context used to get local files dir of application */ public Store(@NonNull Context context) { mContext = context; mKeystoreFile = new File(mContext.getFilesDir(), mKeystoreName); } /** * Creates a store with provided name and password. * * @param context used to get local files dir of application */ public Store(@NonNull Context context, @NonNull String name, char[] password) { mContext = context; mKeystoreName = name; mKeystorePassword = password; mKeystoreFile = new File(mContext.getFilesDir(), mKeystoreName); } /** * Create and saves RSA 1024 Private key with given alias and password. Use generateAsymmetricKey(@NonNull * KeyProps keyProps) to customize key properties * <p/> * Saves key to KeyStore. Uses keystore with default type located in application cache on device if API < 18. * Uses AndroidKeyStore if API is >= 18. * * @return KeyPair or null if any error occurs */ public KeyPair generateAsymmetricKey(@NonNull String alias, char[] password) { final Calendar start = Calendar.getInstance(); final Calendar end = Calendar.getInstance(); end.add(Calendar.YEAR, 20); KeyProps keyProps = new KeyProps.Builder() .setAlias(alias) .setPassword(password) .setKeySize(1024) .setKeyType(Options.ALGORITHM_RSA) .setSerialNumber(BigInteger.ONE) .setSubject(new X500Principal("CN=" + alias + " CA Certificate")) .setStartDate(start.getTime()) .setEndDate(end.getTime()) .setBlockModes(Options.BLOCK_MODE_ECB) .setEncryptionPaddings(Options.PADDING_PKCS_1) .setSignatureAlgorithm(Options.ALGORITHM_SHA256_WITH_RSA_ENCRYPTION) .build(); return generateAsymmetricKey(keyProps); } /** * Create and saves Private key specified in KeyProps with self signed x509 Certificate. * <p/> * Saves key to KeyStore. Uses keystore with default type located in application cache on device if API < 18. * Uses AndroidKeyStore if API is >= 18. * * @return KeyPair or null if any error occurs */ public KeyPair generateAsymmetricKey(@NonNull KeyProps keyProps) { KeyPair result = null; if (Utils.lowerThenJellyBean()) { result = generateDefaultAsymmetricKey(keyProps); } else if (Utils.lowerThenMarshmallow()) { result = generateAndroidJellyAsymmetricKey(keyProps); } else { result = generateAndroidMAsymmetricKey(keyProps); } return result; } /** * Create and saves 256 AES SecretKey key using provided alias and password. * <p/> * Saves key to KeyStore. Uses keystore with default type located in application cache on device if API < 23. * Uses AndroidKeyStore if API is >= 23. * * @return KeyPair or null if any error occurs */ public SecretKey generateSymmetricKey(@NonNull String alias, char[] password) { KeyProps keyProps = new KeyProps.Builder() .setAlias(alias) .setPassword(password) .setKeySize(256) .setKeyType(Options.ALGORITHM_AES) .setBlockModes(Options.BLOCK_MODE_CBC) .setEncryptionPaddings(Options.PADDING_PKCS_7) .build(); return generateSymmetricKey(keyProps); } /** * Create and saves SecretKey key specified in KeyProps. * <p/> * Saves key to KeyStore. Uses keystore with default type located in application cache on device if API < 23. * Uses AndroidKeyStore if API is >= 23. * * @return KeyPair or null if any error occurs */ public SecretKey generateSymmetricKey(@NonNull KeyProps keyProps) { SecretKey result = null; if (Utils.lowerThenMarshmallow()) { result = generateDefaultSymmetricKey(keyProps); } else { result = generateAndroidSymmetricKey(keyProps); } return result; } /** * @return KeyPair or null if any error occurs */ public KeyPair getAsymmetricKey(@NonNull String alias, char[] password) { KeyPair result = null; if (Utils.lowerThenJellyBean()) { result = getAsymmetricKeyFromDefaultKeyStore(alias, password); } else { result = getAsymmetricKeyFromAndroidKeyStore(alias); } return result; } /** * @return SecretKey or null if any error occurs */ public SecretKey getSymmetricKey(@NonNull String alias, char[] password) { SecretKey result = null; if (Utils.lowerThenMarshmallow()) { result = getSymmetricKeyFromDefaultKeyStore(alias, password); } else { result = getSymmetricKeyFromAndroidtKeyStore(alias); } return result; } /** * @return true if key with given alias is in keystore */ public boolean hasKey(@NonNull String alias) { boolean result = false; try { KeyStore keyStore; if (Utils.lowerThenJellyBean()) { keyStore = createDefaultKeyStore(); result = isKeyEntry(alias, keyStore); } else if (Utils.lowerThenMarshmallow()) { keyStore = createAndroidKeystore(); result = isKeyEntry(alias, keyStore); if (!result) { // SecretKey's are stored in default keystore up to 23 API keyStore = createDefaultKeyStore(); result = isKeyEntry(alias, keyStore); } } else { keyStore = createAndroidKeystore(); result = isKeyEntry(alias, keyStore); } } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { onException(e); } return result; } /** * Deletes key with given alias */ public void deleteKey(@NonNull String alias) { try { KeyStore keyStore; if (Utils.lowerThenJellyBean()) { keyStore = createDefaultKeyStore(); deleteEntryFromDefaultKeystore(alias, keyStore); } else if (Utils.lowerThenMarshmallow()) { keyStore = createAndroidKeystore(); if (isKeyEntry(alias, keyStore)) { deleteEntryFromAndroidKeystore(alias, keyStore); } else { keyStore = createDefaultKeyStore(); if (isKeyEntry(alias, keyStore)) { deleteEntryFromDefaultKeystore(alias, keyStore); } } } else { keyStore = createAndroidKeystore(); deleteEntryFromAndroidKeystore(alias, keyStore); } } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { onException(e); } } private boolean isKeyEntry(@NonNull String alias, KeyStore keyStore) throws KeyStoreException { return keyStore != null && keyStore.isKeyEntry(alias); } private void deleteEntryFromDefaultKeystore(@NonNull String alias, KeyStore keyStore) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { if (keyStore != null) { keyStore.deleteEntry(alias); keyStore.store(new FileOutputStream(mKeystoreFile), mKeystorePassword); } } private void deleteEntryFromAndroidKeystore(@NonNull String alias, KeyStore keyStore) throws KeyStoreException { if (keyStore != null) { keyStore.deleteEntry(alias); } } private KeyPair generateDefaultAsymmetricKey(KeyProps keyProps) { try { KeyPair keyPair = createAsymmetricKey(keyProps); PrivateKey key = keyPair.getPrivate(); X509Certificate certificate = keyToCertificateReflection(keyPair, keyProps); KeyStore keyStore = createDefaultKeyStore(); keyStore.setKeyEntry(keyProps.mAlias, key, keyProps.mPassword, new Certificate[]{certificate}); keyStore.store(new FileOutputStream(mKeystoreFile), mKeystorePassword); return keyPair; } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | UnsupportedOperationException e) { onException(e); } catch (NoSuchMethodException e) { onException(e); } catch (InvocationTargetException e) { onException(e); } catch (InstantiationException e) { onException(e); } catch (IllegalAccessException e) { onException(e); } return null; } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private KeyPair generateAndroidJellyAsymmetricKey(KeyProps keyProps) { try { KeyPairGeneratorSpec keySpec = keyPropsToKeyPairGeneratorSpec(keyProps); return generateAndroidAsymmetricKey(keyProps, keySpec); } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { onException(e); } return null; } @TargetApi(Build.VERSION_CODES.M) private KeyPair generateAndroidMAsymmetricKey(KeyProps keyProps) { try { KeyGenParameterSpec keySpec = keyPropsToKeyGenParameterASpec(keyProps); return generateAndroidAsymmetricKey(keyProps, keySpec); } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { onException(e); } return null; } private KeyPair generateAndroidAsymmetricKey(KeyProps keyProps, AlgorithmParameterSpec keySpec) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { String provider = PROVIDER_ANDROID_KEY_STORE; KeyPairGenerator generator = KeyPairGenerator.getInstance(keyProps.mKeyType, provider); generator.initialize(keySpec); return generator.generateKeyPair(); } private KeyPair createAsymmetricKey(KeyProps keyProps) throws NoSuchAlgorithmException { KeyPairGenerator generator = KeyPairGenerator.getInstance(keyProps.mKeyType); generator.initialize(keyProps.mKeySize); return generator.generateKeyPair(); } private SecretKey generateDefaultSymmetricKey(KeyProps keyProps) { try { SecretKey key = createSymmetricKey(keyProps); KeyStore.SecretKeyEntry keyEntry = new KeyStore.SecretKeyEntry(key); KeyStore keyStore = createDefaultKeyStore(); keyStore.setEntry(keyProps.mAlias, keyEntry, new KeyStore.PasswordProtection(keyProps.mPassword)); keyStore.store(new FileOutputStream(mKeystoreFile), mKeystorePassword); return key; } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException e) { onException(e); } return null; } @TargetApi(Build.VERSION_CODES.M) private SecretKey generateAndroidSymmetricKey(KeyProps keyProps) { try { String provider = PROVIDER_ANDROID_KEY_STORE; KeyGenerator keyGenerator = KeyGenerator.getInstance(keyProps.mKeyType, provider); KeyGenParameterSpec keySpec = keyPropsToKeyGenParameterSSpec(keyProps); keyGenerator.init(keySpec); return keyGenerator.generateKey(); } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { onException(e); } return null; } /** * Generating X509Certificate using private com.android.org.bouncycastle.x509.X509V3CertificateGenerator class. * If it is not found, tries to use Google did copied http://www.bouncycastle.org/ but made it private. To not * include additional library Im using reflection here. Tested on API level 16, 17 */ private X509Certificate keyToCertificateReflection(KeyPair keyPair, KeyProps keyProps) throws UnsupportedOperationException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { Class generatorClass = null; try { generatorClass = Class.forName("com.android.org.bouncycastle.x509.X509V3CertificateGenerator"); } catch (ClassNotFoundException e) { // if there is no android default implementation of X509V3CertificateGenerator try to find it from library try { generatorClass = Class.forName("org.bouncycastle.x509.X509V3CertificateGenerator"); } catch (ClassNotFoundException e1) { throw new UnsupportedOperationException( "You need to include http://www.bouncycastle.org/ library to generate KeyPair on " + Utils.VERSION + " API version. You can do this via gradle using command 'compile 'org.bouncycastle:bcprov-jdk15on:1.54'"); } } return keyToCertificateReflection(generatorClass, keyPair, keyProps); } /** * Generating X509Certificate using private com.android.org.bouncycastle.x509.X509V3CertificateGenerator class. * Google did copied http://www.bouncycastle.org/ but made it private. To not include additional library Im * using reflection here. Tested on API level 16, 17 */ private X509Certificate keyToCertificateReflection(Class generatorClass, KeyPair keyPair, KeyProps keyProps) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { Object generator = generatorClass.newInstance(); Method method = generator.getClass().getMethod("setPublicKey", PublicKey.class); method.invoke(generator, keyPair.getPublic()); method = generator.getClass().getMethod("setSerialNumber", BigInteger.class); method.invoke(generator, keyProps.mSerialNumber); method = generator.getClass().getMethod("setSubjectDN", X500Principal.class); method.invoke(generator, keyProps.mSubject); method = generator.getClass().getMethod("setIssuerDN", X500Principal.class); method.invoke(generator, keyProps.mSubject); method = generator.getClass().getMethod("setNotBefore", Date.class); method.invoke(generator, keyProps.mStartDate); method = generator.getClass().getMethod("setNotAfter", Date.class); method.invoke(generator, keyProps.mEndDate); method = generator.getClass().getMethod("setSignatureAlgorithm", String.class); method.invoke(generator, keyProps.mSignatureAlgorithm); method = generator.getClass().getMethod("generate", PrivateKey.class, String.class); return (X509Certificate) method.invoke(generator, keyPair.getPrivate(), PROVIDER_BC); } @TargetApi(Build.VERSION_CODES.KITKAT) private KeyPairGeneratorSpec keyPropsToKeyPairGeneratorSpec(KeyProps keyProps) throws NoSuchAlgorithmException { KeyPairGeneratorSpec.Builder builder = new KeyPairGeneratorSpec.Builder(mContext) .setAlias(keyProps.mAlias) .setSerialNumber(keyProps.mSerialNumber) .setSubject(keyProps.mSubject) .setStartDate(keyProps.mStartDate) .setEndDate(keyProps.mEndDate); if (Utils.biggerThenJellyBean()) { builder.setKeySize(keyProps.mKeySize); } return builder.build(); } @TargetApi(Build.VERSION_CODES.M) private KeyGenParameterSpec keyPropsToKeyGenParameterASpec(KeyProps keyProps) throws NoSuchAlgorithmException { int purposes = KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT; return new KeyGenParameterSpec.Builder(keyProps.mAlias, purposes) .setKeySize(keyProps.mKeySize) .setCertificateSerialNumber(keyProps.mSerialNumber) .setCertificateSubject(keyProps.mSubject) .setCertificateNotBefore(keyProps.mStartDate) .setCertificateNotAfter(keyProps.mEndDate) .setBlockModes(keyProps.mBlockModes) .setEncryptionPaddings(keyProps.mEncryptionPaddings) .build(); } @TargetApi(Build.VERSION_CODES.M) private KeyGenParameterSpec keyPropsToKeyGenParameterSSpec(KeyProps keyProps) throws NoSuchAlgorithmException { int purposes = KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT; return new KeyGenParameterSpec.Builder(keyProps.mAlias, purposes) .setKeySize(keyProps.mKeySize) .setBlockModes(keyProps.mBlockModes) .setEncryptionPaddings(keyProps.mEncryptionPaddings) .build(); } private SecretKey createSymmetricKey(KeyProps keyProps) throws NoSuchAlgorithmException { KeyGenerator keyGenerator = KeyGenerator.getInstance(keyProps.mKeyType); keyGenerator.init(keyProps.mKeySize); SecretKey key = keyGenerator.generateKey(); return key; } private KeyPair getAsymmetricKeyFromDefaultKeyStore(@NonNull String alias, char[] password) { KeyPair result = null; try { // get asymmetric key KeyStore keyStore = createDefaultKeyStore(); KeyStore.PasswordProtection protection = new KeyStore.PasswordProtection(password); KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, protection); if (entry != null) { result = new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey()); } } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | UnrecoverableEntryException e) { onException(e); } return result; } private KeyPair getAsymmetricKeyFromAndroidKeyStore(@NonNull String alias) { KeyPair result = null; try { KeyStore keyStore = createAndroidKeystore(); PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null); if (privateKey != null) { PublicKey publicKey = keyStore.getCertificate(alias).getPublicKey(); result = new KeyPair(publicKey, privateKey); } } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | UnrecoverableEntryException e) { onException(e); } return result; } private SecretKey getSymmetricKeyFromDefaultKeyStore(@NonNull String alias, char[] password) { SecretKey result = null; try { KeyStore keyStore = createDefaultKeyStore(); result = (SecretKey) keyStore.getKey(alias, password); } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | UnrecoverableEntryException e) { onException(e); } return result; } private SecretKey getSymmetricKeyFromAndroidtKeyStore(@NonNull String alias) { SecretKey result = null; try { KeyStore keyStore = createAndroidKeystore(); result = (SecretKey) keyStore.getKey(alias, null); } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | UnrecoverableEntryException e) { onException(e); } return result; } /** * Cache for default keystore */ private KeyStore mDefaultKeyStore; private KeyStore createDefaultKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { if (mDefaultKeyStore == null) { String defaultType = getDefaultType(); mDefaultKeyStore = getInstance(defaultType); if (!mKeystoreFile.exists()) { mDefaultKeyStore.load(null); } else { mDefaultKeyStore.load(new FileInputStream(mKeystoreFile), mKeystorePassword); } } return mDefaultKeyStore; } /** * Cache for android keystore */ private KeyStore mAndroidKeyStore; private KeyStore createAndroidKeystore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { if (mAndroidKeyStore == null) { mAndroidKeyStore = KeyStore.getInstance(PROVIDER_ANDROID_KEY_STORE); } mAndroidKeyStore.load(null); return mAndroidKeyStore; } }