package net.wigle.wigleandroid; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.security.KeyPairGeneratorSpec; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.util.Base64; import java.io.IOException; import java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; 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.ProviderException; import java.security.PublicKey; import java.security.UnrecoverableEntryException; import java.security.cert.CertificateException; import java.util.Calendar; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.security.auth.x500.X500Principal; /** * Methods for enc/dec the API Token via the Android KeyStore if supported * Intended to improve secret cred storage in-place * Created by rksh on 3/2/17. */ public class TokenAccess { // identify WiGLE entry in the KeyStore public static final String KEYSTORE_WIGLE_CREDS_KEY_V0 = "WiGLEKeyOld"; public static final String KEYSTORE_WIGLE_CREDS_KEY_V1 = "WiGLEKey"; public static final String ANDROID_KEYSTORE = "AndroidKeyStore"; /** * test presence of a necessary API key, Keystore entry if applicable * @param prefs * @return true if present, otherwise false */ public static boolean hasApiToken(SharedPreferences prefs) { if (!prefs.getString(ListFragment.PREF_TOKEN,"").isEmpty()) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { try { KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); keyStore.load(null); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { if (keyStore.containsAlias(KEYSTORE_WIGLE_CREDS_KEY_V1)) { //TODO: it would be best to test decrypt here, but makes this heavier return true; } } else if (keyStore.containsAlias(KEYSTORE_WIGLE_CREDS_KEY_V0)) { return true; } } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) { MainActivity.error("[TOKEN] Error trying to test token existence: ", e); return false; } } else { return true; } } return false; } /** * remove the token preference * @param prefs * @return */ public static void clearApiToken(SharedPreferences prefs) { final SharedPreferences.Editor editor = prefs.edit(); editor.remove(ListFragment.PREF_TOKEN); editor.apply(); } /** * Set the appropriate API Token, stored with KeyStore crypto if suitable * @param prefs the shared preferences object in which to store the token * @param apiToken the token value to store * @return true if successful, otherwise false */ public static boolean setApiToken(SharedPreferences prefs, String apiToken) { final SharedPreferences.Editor editor = prefs.edit(); if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { //ALIBI: no crypto available here editor.putString(ListFragment.PREF_TOKEN, apiToken); editor.apply(); return true; } else { try { byte[] cypherToken; String keyStr = KEYSTORE_WIGLE_CREDS_KEY_V0; if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { keyStr = KEYSTORE_WIGLE_CREDS_KEY_V1; } KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); keyStore.load(null); KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(keyStr, null); if (null != privateKeyEntry) { PublicKey publicKey = privateKeyEntry.getCertificate().getPublicKey(); Cipher c = null; if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { c = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { c = Cipher.getInstance("RSA/ECB/PKCS1Padding"); } if (null != c) { c.init(Cipher.ENCRYPT_MODE, publicKey); cypherToken = c.doFinal(apiToken.getBytes()); if (null != cypherToken) { //TODO: use same prefskey? make a new one + clear old one? editor.putString(ListFragment.PREF_TOKEN, Base64.encodeToString(cypherToken, Base64.DEFAULT)); editor.apply(); return true; } else { // ALIBI: DEBUG should be unreachable. MainActivity.error("[TOKEN] ERROR: unreachable condition," + "cipherToken NULL. APIv" + android.os.Build.VERSION.SDK_INT); } } else { // ALIBI: DEBUG should be unreachable. MainActivity.error("[TOKEN] ERROR: unreachable condition," + "cipher NULL. APIv" + android.os.Build.VERSION.SDK_INT); } } else { // ALIBI: DEBUG should be unreachable. MainActivity.error("[TOKEN] ERROR: setApiToken for APIv" + android.os.Build.VERSION.SDK_INT + ", privateKey Entry NULL. Key: " + keyStr); editor.putString(ListFragment.PREF_TOKEN, apiToken); editor.apply(); return true; } } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException | UnrecoverableEntryException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException ex) { MainActivity.error("[TOKEN] Failed to set token: ",ex); ex.printStackTrace(); } catch (Exception e) { MainActivity.error("[TOKEN] Other error - failed to set token: ",e); e.printStackTrace(); } return false; } } /** * get the get the API token independent of secure/insecure storage * @param prefs a SharedPreferences instance from which to retrieve the token * @return the String token or null if unavailable */ public static String getApiToken(SharedPreferences prefs) { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { //ALIBI: no crypto available here return prefs.getString(ListFragment.PREF_TOKEN, ""); } else { try { KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); keyStore.load(null); KeyStore.PrivateKeyEntry privateKeyEntry; // prefer v1 key, fall back to v0 key, nada as applicable int versionThreshold = android.os.Build.VERSION_CODES.M; if (keyStore.containsAlias(KEYSTORE_WIGLE_CREDS_KEY_V1)) { privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEYSTORE_WIGLE_CREDS_KEY_V1, null); } else if (keyStore.containsAlias(KEYSTORE_WIGLE_CREDS_KEY_V0)) { privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEYSTORE_WIGLE_CREDS_KEY_V0, null); versionThreshold = Build.VERSION_CODES.JELLY_BEAN_MR2; } else { MainActivity.warn("[TOKEN] Compatible build, but no key set: " + android.os.Build.VERSION.SDK_INT + " - returning plaintext."); return prefs.getString(ListFragment.PREF_TOKEN, ""); } if (null != privateKeyEntry) { String encodedCypherText = prefs.getString(ListFragment.PREF_TOKEN, ""); if (!encodedCypherText.isEmpty()) { byte[] cypherText = Base64.decode(encodedCypherText, Base64.DEFAULT); PrivateKey privateKey = privateKeyEntry.getPrivateKey(); Cipher c; if (versionThreshold >= android.os.Build.VERSION_CODES.M) { c = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); } else { c = Cipher.getInstance("RSA/ECB/PKCS1Padding"); } c.init(Cipher.DECRYPT_MODE, privateKey); String key = new String(c.doFinal(cypherText), "UTF-8"); return key; } else { MainActivity.error("[TOKEN] NULL encoded cyphertext on token decrypt."); return null; } } else { MainActivity.error("[TOKEN] NULL Private Key on token decrypt."); return null; } } catch (CertificateException | NoSuchAlgorithmException | IOException | KeyStoreException | UnrecoverableEntryException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException ex) { MainActivity.error("[TOKEN] Failed to get API Token: ", ex); return null; } } } /** * Initialization method - only intended for run at app onCreate * @param prefs preferences from root context * @param context root context * @return true if successful encryption takes place, else false. */ public static boolean checkMigrateKeystoreVersion(SharedPreferences prefs, Context context) { boolean initOnly = false; if (prefs.getString(ListFragment.PREF_TOKEN, "").isEmpty()) { MainActivity.info("[TOKEN] No auth token stored - no preference migration possible."); initOnly = true; } if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { // no reliable keystore here MainActivity.info("[TOKEN] No KeyStore support - no preference migration possible."); return false; } else { try { MainActivity.info("[TOKEN] Using Android Keystore; check need for new key..."); KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); keyStore.load(null); KeyPairGenerator kpg = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { if (keyStore.containsAlias(KEYSTORE_WIGLE_CREDS_KEY_V1)) { MainActivity.info("[TOKEN] Key present and up-to-date M - no change."); return false; } MainActivity.info("[TOKEN] Initializing SDKv23 Key..."); String token = ""; if (keyStore.containsAlias(KEYSTORE_WIGLE_CREDS_KEY_V0)) { //ALIBI: fetch token with V0 key if it's stored that way token = TokenAccess.getApiToken(prefs); } KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder( KEYSTORE_WIGLE_CREDS_KEY_V1, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) .build(); kpg.initialize(spec); kpg.generateKeyPair(); if (keyStore.containsAlias(KEYSTORE_WIGLE_CREDS_KEY_V0)) { MainActivity.info("[TOKEN] Upgrading from v0->v1 token..."); if ((null == token) || token.isEmpty()) return false; keyStore.deleteEntry(KEYSTORE_WIGLE_CREDS_KEY_V0); } else { token = prefs.getString(ListFragment.PREF_TOKEN, ""); //DEBUG: MainActivity.info("[TOKEN] +"+token+"+"); MainActivity.info("[TOKEN] Encrypting token at v1..."); if (token.isEmpty()) { MainActivity.info("[TOKEN] ...no token, returning after init."); return false; } } if (!initOnly) { if (TokenAccess.setApiToken(prefs, token)) { MainActivity.info("[TOKEN] ...token set at v1."); return true; } else { /** * ALIBI: if you can't migrate it, clear it to force re-authentication. * this isn't optimal, but it beats the alternative. * This is vital here, since Marshmallow and up can backup/restore * SharedPreferences, but NOT keystore entries */ MainActivity.error("[TOKEN] ...Failed token encryption; clearing."); clearApiToken(prefs); } } else { MainActivity.error("[TOKEN] v1 Keystore initialized, but no token present."); } } else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { if (keyStore.containsAlias(KEYSTORE_WIGLE_CREDS_KEY_V0)) { MainActivity.info( "[TOKEN] Key present and up-to-date JB-MR2 - no action required."); return false; } MainActivity.info("[TOKEN] Initializing SDKv18 Key..."); Calendar notBefore = Calendar.getInstance(); Calendar notAfter = Calendar.getInstance(); notAfter.add(Calendar.YEAR, 3); KeyPairGeneratorSpec spec = null; spec = new KeyPairGeneratorSpec.Builder(context) .setAlias(KEYSTORE_WIGLE_CREDS_KEY_V0) // TODO: for some reason, type/size only supported >= SDKv19 //.setKeyType(KeyProperties.KEY_ALGORITHM_RSA) //.setKeySize(4096) .setSubject(new X500Principal("CN=wigle")) .setSerialNumber(BigInteger.ONE) .setStartDate(notBefore.getTime()) //TODO: does endDate for the generation cert => key expiration? .setEndDate(notAfter.getTime()) .build(); kpg.initialize(spec); kpg.generateKeyPair(); String token = prefs.getString(ListFragment.PREF_TOKEN, ""); if (token.isEmpty()) { MainActivity.info("[TOKEN] ...no token, returning after init."); return false; } MainActivity.info("[TOKEN] Encrypting token at v0..."); if (!initOnly) { if (TokenAccess.setApiToken(prefs, token)) { MainActivity.info("[TOKEN] ...token set at v0."); return true; } else { /** * ALIBI: if you can't migrate it, clear it to force re-authentication. * this isn't optimal, but it beats the alternative. * This may not be necessary in the pre-Marshmallow world. */ MainActivity.error("[TOKEN] ...Failed token encryption; clearing."); clearApiToken(prefs); } } else { MainActivity.error("[TOKEN] v0 Keystore initialized, but no token present."); } } } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException | NoSuchProviderException | InvalidAlgorithmParameterException | ProviderException ex) { MainActivity.error("Upgrade/init of token storage failed: ", ex); ex.printStackTrace(); return false; } catch (Exception e) { /** * ALIBI: after production evidence of a ProviderException (runtime), adding belt to * suspenders */ MainActivity.error("Unexpected error in upgrade/init of token storage failed: ", e); e.printStackTrace(); return false; } } return false; } }