/* * Copyright (c) 2014, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ package com.facebook.android.crypto.keychain; import android.content.Context; import android.content.SharedPreferences; import android.util.Base64; import com.facebook.crypto.CryptoConfig; import com.facebook.crypto.exception.KeyChainException; import com.facebook.crypto.keychain.KeyChain; import com.facebook.crypto.mac.NativeMac; import java.security.SecureRandom; import java.util.Arrays; /** * An implementation of a keychain that is backed by shared preferences. * </p> * The implementation tries to cache results as much as possible to avoid * having to do expensive lookups to shared preferences. The keys are generated * lazily on first use. * </p> * If your code is sensitive to running shared preference I/O operations on the * UI thread, consider calling {@link #getCipherKey()} off the main thread, or * providing your own implementation similar to this class using a different * backing store. */ public class SharedPrefsBackedKeyChain implements KeyChain { // Visible for testing. /* package */ static final String SHARED_PREF_NAME = "crypto"; /* package */ static final String CIPHER_KEY_PREF = "cipher_key"; /* package */ static final String MAC_KEY_PREF = "mac_key"; private final CryptoConfig mCryptoConfig; private final SharedPreferences mSharedPreferences; private final SecureRandom mSecureRandom; protected byte[] mCipherKey; protected boolean mSetCipherKey; protected byte[] mMacKey; protected boolean mSetMacKey; /** * @deprecated This default constructor uses 128-bit keys for backward compatibility * but current standard is 256-bits. Use explicit constructor instead. */ @Deprecated public SharedPrefsBackedKeyChain(Context context) { this(context, CryptoConfig.KEY_128); } public SharedPrefsBackedKeyChain(Context context, CryptoConfig config) { String prefName = prefNameForConfig(config); mSharedPreferences = context.getSharedPreferences(prefName, Context.MODE_PRIVATE); mSecureRandom = SecureRandomFix.createLocalSecureRandom(); mCryptoConfig = config; } /** * We should store different configuration keys separately, specially to support the * case of migration: one KeyChain has the 128-bit to read old stored data, another KeyChain * has the 256-bit value to rewrite all data. * <p> * So the preference name will depend on the config. * For backward compatibility the name for 128-bits is kept as SHARED_PREF_NAME. */ private static String prefNameForConfig(CryptoConfig config) { return config == CryptoConfig.KEY_128 ? SHARED_PREF_NAME : SHARED_PREF_NAME + "." + String.valueOf(config); } @Override public synchronized byte[] getCipherKey() throws KeyChainException { if (!mSetCipherKey) { mCipherKey = maybeGenerateKey(CIPHER_KEY_PREF, mCryptoConfig.keyLength); } mSetCipherKey = true; return mCipherKey; } @Override public byte[] getMacKey() throws KeyChainException { if (!mSetMacKey) { mMacKey = maybeGenerateKey(MAC_KEY_PREF, NativeMac.KEY_LENGTH); } mSetMacKey = true; return mMacKey; } @Override public byte[] getNewIV() throws KeyChainException { byte[] iv = new byte[mCryptoConfig.ivLength]; mSecureRandom.nextBytes(iv); return iv; } @Override public synchronized void destroyKeys() { mSetCipherKey = false; mSetMacKey = false; if (mCipherKey != null) { Arrays.fill(mCipherKey, (byte) 0); } if (mMacKey != null) { Arrays.fill(mMacKey, (byte) 0); } mCipherKey = null; mMacKey = null; SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.remove(CIPHER_KEY_PREF); editor.remove(MAC_KEY_PREF); editor.commit(); } /** * Generates a key associated with a preference. */ private byte[] maybeGenerateKey(String pref, int length) throws KeyChainException { String base64Key = mSharedPreferences.getString(pref, null); if (base64Key == null) { // Generate key if it doesn't exist. return generateAndSaveKey(pref, length); } else { return decodeFromPrefs(base64Key); } } private byte[] generateAndSaveKey(String pref, int length) throws KeyChainException { byte[] key = new byte[length]; mSecureRandom.nextBytes(key); // Store the session key. SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.putString( pref, encodeForPrefs(key)); editor.commit(); return key; } /** * Visible for testing. */ /* package */ byte[] decodeFromPrefs(String keyString) { if (keyString == null) { return null; } return Base64.decode(keyString, Base64.DEFAULT); } /** * Visible for testing. */ /* package */ String encodeForPrefs(byte[] key) { if (key == null ) { return null; } return Base64.encodeToString(key, Base64.DEFAULT); } }