1 package io.keybase.ossifrage; 2 3 import android.annotation.SuppressLint; 4 import android.content.Context; 5 import android.content.SharedPreferences; 6 import android.os.Build; 7 import android.security.keystore.KeyPermanentlyInvalidatedException; 8 import android.util.Base64; 9 10 import org.msgpack.MessagePack; 11 12 import java.io.IOException; 13 import java.security.InvalidKeyException; 14 import java.security.KeyPair; 15 import java.security.KeyStore.Entry; 16 import java.security.KeyStore.PrivateKeyEntry; 17 import java.security.KeyStoreException; 18 import java.security.NoSuchAlgorithmException; 19 import java.security.NoSuchProviderException; 20 import java.security.cert.CertificateException; 21 import java.util.ArrayList; 22 import java.util.Iterator; 23 24 import javax.crypto.Cipher; 25 import javax.crypto.IllegalBlockSizeException; 26 import javax.crypto.NoSuchPaddingException; 27 import javax.crypto.SecretKey; 28 import javax.crypto.spec.SecretKeySpec; 29 30 import keybase.UnsafeExternalKeyStore; 31 import io.keybase.ossifrage.keystore.KeyStoreHelper; 32 import io.keybase.ossifrage.modules.NativeLogger; 33 34 public class KeyStore implements UnsafeExternalKeyStore { 35 private final Context context; 36 private final SharedPreferences prefs; 37 private java.security.KeyStore ks; 38 39 // Prefix for the key we use when we place the data in shared preferences 40 private static final String PREFS_KEY = "_wrappedKey_"; 41 // The name of the key we use to store our created RSA keypair in android's keystore 42 private static final String KEY_ALIAS = "_keybase-rsa-wrapper_"; 43 44 private static final String ALGORITHM = "RSA_SECRETBOX"; 45 KeyStore(final Context context, final SharedPreferences prefs)46 public KeyStore(final Context context, final SharedPreferences prefs) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { 47 this.context = context; 48 this.prefs = prefs; 49 50 ks = java.security.KeyStore.getInstance("AndroidKeyStore"); 51 ks.load(null); 52 NativeLogger.info("KeyStore: initialized"); 53 } 54 sharedPrefKeyPrefix(final String serviceName)55 private String sharedPrefKeyPrefix(final String serviceName) { 56 return serviceName + PREFS_KEY; 57 } 58 keyStoreAlias(final String serviceName)59 private String keyStoreAlias(final String serviceName) { 60 return serviceName + KEY_ALIAS; 61 } 62 63 @SuppressLint("CommitPrefEdits") 64 @Override clearSecret(final String serviceName, final String key)65 public void clearSecret(final String serviceName, final String key) throws Exception { 66 String id = serviceName + ":" + key; 67 NativeLogger.info("KeyStore: clearing secret for " + id); 68 69 try { 70 prefs.edit().remove(sharedPrefKeyPrefix(serviceName) + key).commit(); 71 } catch (Exception e) { 72 NativeLogger.error("KeyStore: error clearing secret for " + id, e); 73 throw e; 74 } 75 76 NativeLogger.info("KeyStore: cleared secret for " + id); 77 } 78 79 @Override getUsersWithStoredSecretsMsgPack(final String serviceName)80 public synchronized byte[] getUsersWithStoredSecretsMsgPack(final String serviceName) throws Exception { 81 NativeLogger.info("KeyStore: getting users with stored secrets for " + serviceName); 82 83 try { 84 final Iterator<String> keyIterator = prefs.getAll().keySet().iterator(); 85 final ArrayList<String> userNames = new ArrayList<>(); 86 87 while (keyIterator.hasNext()) { 88 final String key = keyIterator.next(); 89 if (key.indexOf(sharedPrefKeyPrefix(serviceName)) == 0) { 90 userNames.add(key.substring(sharedPrefKeyPrefix(serviceName).length())); 91 } 92 } 93 94 NativeLogger.info("KeyStore: got " + userNames.size() + " users with stored secrets for " + serviceName); 95 96 MessagePack msgpack = new MessagePack(); 97 return msgpack.write(userNames); 98 } catch (Exception e) { 99 NativeLogger.error("KeyStore: error getting users with stored secrets for " + serviceName, e); 100 throw e; 101 } 102 } 103 104 @Override retrieveSecret(final String serviceName, final String key)105 public synchronized byte[] retrieveSecret(final String serviceName, final String key) throws Exception { 106 String id = serviceName + ":" + key; 107 NativeLogger.info("KeyStore: retrieving secret for " + id); 108 109 try { 110 final byte[] wrappedSecret = readWrappedSecret(prefs, sharedPrefKeyPrefix(serviceName) + key); 111 Entry entry = ks.getEntry(keyStoreAlias(serviceName), null); 112 113 if (entry == null) { 114 throw new KeyStoreException("No RSA keys in the keystore"); 115 } 116 117 if (!(entry instanceof PrivateKeyEntry)) { 118 throw new KeyStoreException("Entry is not a PrivateKeyEntry. It is: " + entry.getClass()); 119 } 120 121 try { 122 byte[] secret = unwrapSecret((PrivateKeyEntry) entry, wrappedSecret).getEncoded(); 123 NativeLogger.info("KeyStore: retrieved " + secret.length + "-byte secret for " + id); 124 return secret; 125 } catch (InvalidKeyException e) { 126 // Invalid key, this can happen when a user changes their lock screen from something to nothing 127 // or enrolls a new finger. See https://developer.android.com/reference/android/security/keystore/KeyPermanentlyInvalidatedException.html 128 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && e instanceof KeyPermanentlyInvalidatedException) { 129 NativeLogger.info("KeyStore: key no longer valid; deleting entry", e); 130 ks.deleteEntry((keyStoreAlias(serviceName))); 131 } 132 throw e; 133 } 134 } catch (Exception e) { 135 NativeLogger.error("KeyStore: error retrieving secret for " + id, e); 136 throw e; 137 } 138 } 139 140 @Override setupKeyStore(final String serviceName, final String key)141 public synchronized void setupKeyStore(final String serviceName, final String key) throws Exception { 142 String id = serviceName + ":" + key; 143 NativeLogger.info("KeyStore: setting up key store for " + id); 144 145 try { 146 if (!ks.containsAlias(keyStoreAlias(serviceName))) { 147 KeyStoreHelper.generateRSAKeyPair(context, keyStoreAlias(serviceName)); 148 } 149 150 // Try to read the entry from the keystore. 151 // The entry may exist, but it may not be readable by us. 152 // (this happens when the app is uninstalled and reinstalled) 153 // In that case, lets' delete the entry and recreate it. 154 // Note we are purposely not recursing to avoid a state where we 155 // constantly try to generate new RSA keys (which is slow) 156 try { 157 final Entry entry = ks.getEntry(keyStoreAlias(serviceName), null); 158 if (entry == null) { 159 ks.deleteEntry(keyStoreAlias(serviceName)); 160 KeyStoreHelper.generateRSAKeyPair(context, keyStoreAlias(serviceName)); 161 } 162 } finally { 163 // Reload the keystore 164 ks = java.security.KeyStore.getInstance("AndroidKeyStore"); 165 ks.load(null); 166 } 167 } catch (Exception e) { 168 NativeLogger.error("KeyStore: error setting up key store for " + id, e); 169 throw e; 170 } 171 172 NativeLogger.info("KeyStore: finished setting up key store for " + id); 173 } 174 175 @Override storeSecret(final String serviceName, final String key, final byte[] bytes)176 public synchronized void storeSecret(final String serviceName, final String key, final byte[] bytes) throws Exception { 177 String id = serviceName + ":" + key; 178 NativeLogger.info("KeyStore: storing " + bytes.length + "-byte secret for " + id); 179 180 try { 181 Entry entry = ks.getEntry(keyStoreAlias(serviceName), null); 182 183 if (entry == null) { 184 throw new KeyStoreException("No RSA keys in the keystore"); 185 } 186 187 final byte[] wrappedSecret = wrapSecret((PrivateKeyEntry) entry, new SecretKeySpec(bytes, ALGORITHM)); 188 189 if (wrappedSecret == null) { 190 throw new IOException("Null return when wrapping secret"); 191 } 192 193 saveWrappedSecret(prefs, sharedPrefKeyPrefix(serviceName) + key, wrappedSecret); 194 } catch (Exception e) { 195 NativeLogger.error("KeyStore: error storing secret for " + id, e); 196 throw e; 197 } 198 199 NativeLogger.info("KeyStore: stored " + bytes.length + "-byte secret for " + id); 200 } 201 saveWrappedSecret(SharedPreferences prefs, String prefsKey, byte[] wrappedSecret)202 private static void saveWrappedSecret(SharedPreferences prefs, String prefsKey, byte[] wrappedSecret) { 203 prefs.edit().putString(prefsKey, Base64.encodeToString(wrappedSecret, Base64.NO_WRAP)).apply(); 204 } 205 206 readWrappedSecret(SharedPreferences prefs, String prefsKey)207 private static byte[] readWrappedSecret(SharedPreferences prefs, String prefsKey) throws Exception { 208 final String wrappedKey = prefs.getString(prefsKey, ""); 209 if (wrappedKey.isEmpty()) { 210 throw new KeyStoreException("No secret for " + prefsKey); 211 } 212 return Base64.decode(wrappedKey, Base64.NO_WRAP); 213 } 214 215 /** 216 * Similar to Android's example Vault https://github.com/android/platform_development/tree/master/samples/Vault 217 */ wrapSecret(PrivateKeyEntry entry, SecretKey key)218 private static byte[] wrapSecret(PrivateKeyEntry entry, SecretKey key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, NoSuchProviderException { 219 KeyPair mPair = new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey()); 220 // This is the only cipher that's supported by AndroidKeystore (api version +18) 221 // The padding makes sure this encryption isn't deterministic 222 Cipher mCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); 223 mCipher.init(Cipher.WRAP_MODE, mPair.getPublic()); 224 return mCipher.wrap(key); 225 } 226 unwrapSecret(PrivateKeyEntry entry, byte[] wrappedSecretKey)227 private static SecretKey unwrapSecret(PrivateKeyEntry entry, byte[] wrappedSecretKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, NoSuchProviderException { 228 KeyPair mPair = new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey()); 229 Cipher mCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); 230 mCipher.init(Cipher.UNWRAP_MODE, mPair.getPrivate()); 231 return (SecretKey) mCipher.unwrap(wrappedSecretKey, ALGORITHM, Cipher.SECRET_KEY); 232 } 233 234 235 } 236