package com.venmo.android.pin.util; import android.content.Context; import android.content.SharedPreferences; import android.util.Base64; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import static android.preference.PreferenceManager.getDefaultSharedPreferences; public class PinHelper { private static final String KEY_PINPUT_PIN_HASH = "com.venmo.pin.pinputview_pin"; private static final String KEY_PR_SALT = "com.venmo.pin.pr_salt"; // default pin encryption settings private static final SecureRandom RANDOM = new SecureRandom(); private static final int ROUNDS = 100; private static final int KEY_LEN = 256; private static final String KEY_ALGORITHM = "PBKDF2WithHmacSHA1"; private static byte[] generateSalt() { byte[] salt = new byte[24]; RANDOM.nextBytes(salt); return salt; } private static byte[] hash(char[] pin, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { PBEKeySpec spec = new PBEKeySpec(pin, salt, ROUNDS, KEY_LEN); Arrays.fill(pin, Character.MIN_VALUE); try { SecretKeyFactory skf = SecretKeyFactory.getInstance(KEY_ALGORITHM); return skf.generateSecret(spec).getEncoded(); } finally { spec.clearPassword(); } } private static boolean validate(char[] actual, byte[] expected, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { byte[] pwdHash = hash(actual, salt); Arrays.fill(actual, Character.MIN_VALUE); if (pwdHash.length != expected.length) return false; for (int i = 0; i < pwdHash.length; i++) { if (pwdHash[i] != expected[i]) return false; } return true; } public static boolean hasDefaultPinSaved(Context c) { return getDefaultSharedPreferences(c).getString(KEY_PINPUT_PIN_HASH, null) != null; } public static void resetDefaultSavedPin(Context c) { getDefaultSharedPreferences(c).edit() .clear() .commit(); } public static boolean doesMatchDefaultPin(Context c, String pin) { try { SharedPreferences def = getDefaultSharedPreferences(c); return validate(pin.toCharArray(), getPinHashFromPreferences(def), getSaltFromPreferences(def)); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("error validating pin", e); } catch (InvalidKeySpecException e) { throw new RuntimeException("error validating pin", e); } } public static void saveDefaultPin(Context context, String pin) { try { final byte[] salt = generateSalt(); final byte[] hash = hash(pin.toCharArray(), salt); // save salt & pin after successful hashing saveToPreferences(getDefaultSharedPreferences(context), salt, hash); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("error saving pin: ", e); } catch (InvalidKeySpecException e) { throw new RuntimeException("error saving pin: ", e); } } private static void saveToPreferences(SharedPreferences prefs, byte[] salt, byte[] hash) { prefs.edit() .putString(KEY_PR_SALT, encode(salt)) .putString(KEY_PINPUT_PIN_HASH, encode(hash)) .commit(); } private static byte[] getSaltFromPreferences(SharedPreferences prefs) { return decode(getStringFromPrefsOrThow(prefs, KEY_PR_SALT)); } private static byte[] getPinHashFromPreferences(SharedPreferences prefs) { return decode(getStringFromPrefsOrThow(prefs, KEY_PINPUT_PIN_HASH)); } private static String getStringFromPrefsOrThow(SharedPreferences prefs, String key) { String val = prefs.getString(key, null); if (val == null) { throw new NullPointerException("Trying to retrieve pin value before it's been set"); } return val; } private static String encode(byte[] src) { return Base64.encodeToString(src, Base64.DEFAULT); } private static byte[] decode(String src) { return Base64.decode(src, Base64.DEFAULT); } }