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