问题
I'm creating an app where the user has two options to unlock their app, one is using a pin and the other is using a fingerprint. In order to use the fingerprint they must first set up a pin because this pin is the decryption key to get their encrypted details out of SharedPreferences
.
So i've followed this tutorial here: http://www.techotopia.com/index.php/An_Android_Fingerprint_Authentication_Tutorial#Accessing_the_Android_Keystore_and_KeyGenerator
I've managed to get the app to read a fingerprint and say whether it is valid or not. But when the fingerprint is authorised I have no idea how to get that pin out of the Android keystore.
Here is some code to demonstrate:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
fingerprintManager = (FingerprintManager) getSystemService(FINGERPRINT_SERVICE);
if (!keyguardManager.isKeyguardSecure()) {
Toast.makeText(this, "Lock screen security not enabled in Settings", Toast.LENGTH_LONG).show();
return;
}
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.USE_FINGERPRINT) !=
PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Fingerprint authentication permission not enabled", Toast.LENGTH_LONG).show();
return;
}
if (!fingerprintManager.hasEnrolledFingerprints()) {
// This happens when no fingerprints are registered.
Toast.makeText(this, "Register at least one fingerprint in Settings", Toast.LENGTH_LONG).show();
return;
}
generateKey();
if (cipherInit()) {
cryptoObject = new FingerprintManager.CryptoObject(cipher);
FingerprintHandler helper = new FingerprintHandler(this);
helper.startAuth(fingerprintManager, cryptoObject);
}
}
protected void generateKey() {
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
} catch (Exception e) {
e.printStackTrace();
}
try {
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
} catch (NoSuchAlgorithmException |
NoSuchProviderException e) {
throw new RuntimeException("Failed to get KeyGenerator instance", e);
}
try {
keyStore.load(null);
keyGenerator.init(new
KeyGenParameterSpec.Builder(KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT |
KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(
KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
keyGenerator.generateKey();
} catch (NoSuchAlgorithmException |
InvalidAlgorithmParameterException
| CertificateException | IOException e) {
throw new RuntimeException(e);
}
}
public boolean cipherInit() {
try {
cipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException |
NoSuchPaddingException e) {
throw new RuntimeException("Failed to get Cipher", e);
}
try {
keyStore.load(null);
SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME,
null);
cipher.init(Cipher.ENCRYPT_MODE, key);
return true;
} catch (KeyPermanentlyInvalidatedException e) {
return false;
} catch (KeyStoreException | CertificateException
| UnrecoverableKeyException | IOException
| NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Failed to init Cipher", e);
}
}
KEY_NAME is the key(pin) i'm trying to store (I think).
Then in the FingerprintHandler
class there is this method:
public void onAuthenticationSucceeded(
FingerprintManager.AuthenticationResult result) {
Toast.makeText(appContext,
"Authentication succeeded.",
Toast.LENGTH_LONG).show();
}
But how do i get the key i want out of the result
if at all?
回答1:
So to do this I ended up encrypting the users pin in to shared preferences and then decrypting when the fingerprint auth was successful:
So to save the pin:
private static final String CHARSET_NAME = "UTF-8";
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String TRANSFORMATION = KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7;
private static final int AUTHENTICATION_DURATION_SECONDS = 30;
private KeyguardManager keyguardManager;
private static final int SAVE_CREDENTIALS_REQUEST_CODE = 1;
public void saveUserPin(String pin) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
// encrypt the password
try {
SecretKey secretKey = createKey();
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptionIv = cipher.getIV();
byte[] passwordBytes = pin.getBytes(CHARSET_NAME);
byte[] encryptedPasswordBytes = cipher.doFinal(passwordBytes);
String encryptedPassword = Base64.encodeToString(encryptedPasswordBytes, Base64.DEFAULT);
// store the login data in the shared preferences
// only the password is encrypted, IV used for the encryption is stored
SharedPreferences.Editor editor = BaseActivity.prefs.edit();
editor.putString("password", encryptedPassword);
editor.putString("encryptionIv", Base64.encodeToString(encryptionIv, Base64.DEFAULT));
editor.apply();
} catch (UserNotAuthenticatedException e) {
e.printStackTrace();
showAuthenticationScreen(SAVE_CREDENTIALS_REQUEST_CODE);
}
}
private SecretKey createKey() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
keyGenerator.init(new KeyGenParameterSpec.Builder(Constants.KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
return keyGenerator.generateKey();
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("Failed to create a symmetric key", e);
}
}
Then to decrypt:
public String getUserPin() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, NoSuchPaddingException, UnrecoverableKeyException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
// load login data from shared preferences (
// only the password is encrypted, IV used for the encryption is loaded from shared preferences
SharedPreferences sharedPreferences = BaseActivity.prefs;
String base64EncryptedPassword = sharedPreferences.getString("password", null);
String base64EncryptionIv = sharedPreferences.getString("encryptionIv", null);
byte[] encryptionIv = Base64.decode(base64EncryptionIv, Base64.DEFAULT);
byte[] encryptedPassword = Base64.decode(base64EncryptedPassword, Base64.DEFAULT);
// decrypt the password
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
SecretKey secretKey = (SecretKey) keyStore.getKey(Constants.KEY_NAME, null);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(encryptionIv));
byte[] passwordBytes = cipher.doFinal(encryptedPassword);
String string = new String(passwordBytes, CHARSET_NAME);
return string;
}
The showAuthenticationScreen method that is called looks like this:
private void showAuthenticationScreen(int requestCode) {
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null);
if (intent != null) {
startActivityForResult(intent, requestCode);
}
}
And then to get the result back from showAuthenticationScreen
just override onActivityResult
and call saveUserPin
or getUserPin
again whichever is required.
回答2:
The way in which the tutorial you mentioned, as well as the Fingerprint Dialog Sample provided by Google, handles authentication is by assuming that the user is authentic when onAuthenticationSucceeded()
is called. The Google sample takes this a step further by checking if the Cipher
provided by the can encrypt arbitrary data:
/**
* Proceed the purchase operation
*
* @param withFingerprint {@code true} if the purchase was made by using a fingerprint
* @param cryptoObject the Crypto object
*/
public void onPurchased(boolean withFingerprint,
@Nullable FingerprintManager.CryptoObject cryptoObject) {
if (withFingerprint) {
// If the user has authenticated with fingerprint, verify that using cryptography and
// then show the confirmation message.
assert cryptoObject != null;
tryEncrypt(cryptoObject.getCipher());
} else {
// Authentication happened with backup password. Just show the confirmation message.
showConfirmation(null);
}
}
/**
* Tries to encrypt some data with the generated key in {@link #createKey} which is
* only works if the user has just authenticated via fingerprint.
*/
private void tryEncrypt(Cipher cipher) {
try {
byte[] encrypted = cipher.doFinal(SECRET_MESSAGE.getBytes());
showConfirmation(encrypted);
} catch (BadPaddingException | IllegalBlockSizeException e) {
Toast.makeText(this, "Failed to encrypt the data with the generated key. "
+ "Retry the purchase", Toast.LENGTH_LONG).show();
Log.e(TAG, "Failed to encrypt the data with the generated key." + e.getMessage());
}
}
This is a valid form of authentication, but if you need to actually store and retrieve a secret (in your case a pin), it is not sufficient. Instead, you can use asymmetric cryptography to encrypt your secret, then decrypt it upon onAuthenticationSucceeded()
. This is similar to how authentication is handled in the Asymmetric Fingerprint Dialog Sample, although without a back end server.
来源:https://stackoverflow.com/questions/40724749/how-to-get-key-from-keystore-on-successful-fingerprint-auth