0
votes

As the title already states, I want to use a fingerprint to encrypt some data in my app. Specifically, I want the user to be able to set up a password and then allow them to use a fingerprint (maybe by enabling it in the settings, etc.) to decrypt it again.

I've seen this in Alexander Malikov's Private notepad app. I've looked into working with the fingerprint on Android already but I don't know how to go about encrypting strings, etc. with it. Having downloaded the app mentioned already, my question is: How can you decrypt data with just the fingerprint? (Because there must be some kind of way of accessing the actual password / pin code / pattern set up to decrypt the notes, but you never have to set this up manually or enter the password, you just have to scan your fingerprint).

My phone is rooted so I decided to look into the database that this app uses. It stores the hash of the password or other locking method and the encrypted notes as well, but there is no sign of a hash of the fingerprint. I know how to verify the user's identity using the fingerprint sensor (without manually hashing anything, so that explains the hash not being stored anywhere). So while I could say (with a boolean) if the user is authenticated, how would I get access to a key to decrypt my data using just the fingerprint and having a password set up? (e.g. if I've got the hash stored somewhere to check)

Also: Does the OS / fingerprint API use a hash to authenticate the user?

(By the way: I don't know a lot of things about encryption and security so please excuse my use of wrong terms, etc.)

I hope someone can help me out on this :)

2

2 Answers

2
votes

So, if I understand correctly, you want to encrypt the data (a password) without scanning the fingerprint but require the fingerprint when decrypting the data.

First of all you need to generate a key pair for encryption/decryption. This can be done as follows:

private static final String KEY_STORE_ID = "AndroidKeyStore";
private static final String KEY_PAIR_ALIAS = "MyKeyPair"

private PrivateKey getPrivateKey() {
    KeyStore keyStore = getKeyStore();
    try {
        return (PrivateKey) keyStore.getKey(KEY_PAIR_ALIAS, null);
    } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
        throw new RuntimeException(e);
    }
}

private PublicKey getPublicKey() {
    KeyStore keyStore = getKeyStore();
    Certificate certificate;
    try {
        certificate = keyStore.getCertificate(KEY_PAIR_ALIAS);
    } catch (KeyStoreException e) {
        throw new RuntimeException(e);
    }
    if (certificate == null) {
        throw new RuntimeException("Key pair not found");
    }
    PublicKey publicKey = certificate.getPublicKey();

    // This conversion is currently needed on API Level 23 (Android M) due to a platform bug which prevents the
    // use of Android Keystore public keys when their private keys require user authentication. This conversion
    // creates a new public key which is not backed by Android Keystore and thus is not affected by the bug.
    // See https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html
    try {
        KeyFactory keyFactory = KeyFactory.getInstance(publicKey.getAlgorithm());
        return keyFactory.generatePublic(new X509EncodedKeySpec(publicKey.getEncoded()));
    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
        throw new RuntimeException("Failed to copy public key.", e);
    }
}

private KeyStore getKeyStore() {
    try {
        KeyStore keyStore = KeyStore.getInstance(KEY_STORE_ID);
        keyStore.load(null);
        if (!keyStore.containsAlias(KEY_PAIR_ALIAS)) {
            resetKeyPair();
        }
        return keyStore;
    } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
        throw new RuntimeException(e);
    }
}

private void resetKeyPair() {
    try {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEY_STORE_ID);
        keyPairGenerator.initialize(new KeyGenParameterSpec.Builder(KEY_PAIR_ALIAS, KeyProperties.PURPOSE_DECRYPT)
                .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                .setUserAuthenticationRequired(true).build());
        keyPairGenerator.generateKeyPair();
    } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
        throw new RuntimeException("Could not generate key pair", e);
    }
}

Now you can use getPrivateKey() to get the decryption key (which requires authentication) and getPublicKey() to get the encryption key (which does not require authentication). You can use them like this:

public byte[] encrypt(byte[] input) throws KeyPermanentlyInvalidatedException, BadPaddingException, IllegalBlockSizeException {
    Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, getPublicKey());
    return cipher.doFinal(input);
}

public interface Callback {
    void done(byte[] result);
}

public CancellationSignal decrypt(Context context, final byte[] input, final Callback callback) throws KeyPermanentlyInvalidatedException {
    CancellationSignal cancellationSignal = new CancellationSignal();
    FingerprintManager fingerprintManager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
    FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(getCipher(Cipher.DECRYPT_MODE, getPrivateKey()));
    fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, new FingerprintManager.AuthenticationCallback() {
        @Override
        public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
            Cipher cipher = result.getCryptoObject().getCipher();
            byte[] output;
            try {
                output = cipher.doFinal(input);
            } catch (IllegalBlockSizeException | BadPaddingException e) {
                throw new RuntimeException(e);
            }
            callback.done(output);
        }
        // Override other methods as well
    }, null);
    return cancellationSignal;
}

private Cipher getCipher(int mode, Key key) throws KeyPermanentlyInvalidatedException {
    Cipher cipher;
    try {
        cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_RSA + "/" + KeyProperties.BLOCK_MODE_ECB + "/" + "OAEPWithSHA-256AndMGF1Padding");
    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
        throw new RuntimeException(e);
    }
    OAEPParameterSpec algorithmParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
    try {
        cipher.init(mode, key, algorithmParameterSpec);
    } catch (KeyPermanentlyInvalidatedException e) {
        // The key pair has been invalidated!
        throw e;
    } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
        throw new RuntimeException(e);
    }
    return cipher;
}

Note that this is just an example. Some suggested improvements:

  • Improve error handling.
  • Don't throw RuntimeException.
  • Override more methods of FingerprintManager.AuthenticationCallback.
  • If you don't want the key pair to be invalidated when the user enrolls a new fingerprint or deletes an old one you can call setInvalidatedByBiometricEnrollment(false) (added in API level 24) on the KeyGenParameterSpec.Builder in resetKeyPair(). This will reduce security but improve usability.
0
votes

I've got to say my wording is a bit confusing... In the end it was just a problem of not having done enough research.

I was assuming I could use symmetric encryption like AES does it. But when I did a bit more research it turns out I completely misunderstood what I wanted to do.

Basically what I was thinking of was that the password and the fingerprint somehow would end up with the same hash, meaning they would need to actually be the same thing. I thought that wasn't possible, and it isn't obviously, hence my question about that topic.

Also that is why I wondered how it was possible in the app I referenced, because I never saw any hash in the files, as I said. But I just didn't understand the password and fingerprint are just for confirming the user's identity, and I would need to encrypt / decrypt the data with something else.

As I said, I haven't got a very good understanding of encryption and security (and maybe my process of thought was just a bit too complicated because I missed the basic purpose of a password). :)