我正在使用Android M指纹API来允许用户登录该应用程序。为此,我需要在设备上存储用户名和密码。目前我有登录工作,以及指纹API,但用户名和密码都存储为纯文本。我希望在存储密码之前加密密码,并且在用户使用指纹进行身份验证后能够检索密码。
我很难让这个工作起来。我一直试图从Android Security samples应用我能做的,但每个例子似乎只处理加密或签名,而不是解密。
到目前为止,我必须获取AndroidKeyStore
,KeyPairGenerator
和Cipher
的实例,使用非对称加密技术来允许使用Android {{ 3}}。非对称加密的原因是,如果用户未经过身份验证,setUserAuthenticationRequired
方法将阻止对密钥的任何使用,但是:
此授权仅适用于密钥和私钥操作。公钥操作不受限制。
这应该允许我在用户使用指纹进行身份验证之前使用公钥加密密码,然后仅在用户通过身份验证后才使用私钥解密。
public KeyStore getKeyStore() {
try {
return KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException exception) {
throw new RuntimeException("Failed to get an instance of KeyStore", exception);
}
}
public KeyPairGenerator getKeyPairGenerator() {
try {
return KeyPairGenerator.getInstance("EC", "AndroidKeyStore");
} catch(NoSuchAlgorithmException | NoSuchProviderException exception) {
throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception);
}
}
public Cipher getCipher() {
try {
return Cipher.getInstance("EC");
} catch(NoSuchAlgorithmException | NoSuchPaddingException exception) {
throw new RuntimeException("Failed to get an instance of Cipher", exception);
}
}
private void createKey() {
try {
mKeyPairGenerator.initialize(
new KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")
.setUserAuthenticationRequired(true)
.build());
mKeyPairGenerator.generateKeyPair();
} catch(InvalidAlgorithmParameterException exception) {
throw new RuntimeException(exception);
}
}
private boolean initCipher(int opmode) {
try {
mKeyStore.load(null);
if(opmode == Cipher.ENCRYPT_MODE) {
PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey();
mCipher.init(opmode, key);
} else {
PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null);
mCipher.init(opmode, key);
}
return true;
} catch (KeyPermanentlyInvalidatedException exception) {
return false;
} catch(KeyStoreException | CertificateException | UnrecoverableKeyException
| IOException | NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException exception) {
throw new RuntimeException("Failed to initialize Cipher", exception);
}
}
private void encrypt(String password) {
try {
initCipher(Cipher.ENCRYPT_MODE);
byte[] bytes = mCipher.doFinal(password.getBytes());
String encryptedPassword = Base64.encodeToString(bytes, Base64.NO_WRAP);
mPreferences.getString("password").set(encryptedPassword);
} catch(IllegalBlockSizeException | BadPaddingException exception) {
throw new RuntimeException("Failed to encrypt password", exception);
}
}
private String decryptPassword(Cipher cipher) {
try {
String encryptedPassword = mPreferences.getString("password").get();
byte[] bytes = Base64.decode(encryptedPassword, Base64.NO_WRAP);
return new String(cipher.doFinal(bytes));
} catch (IllegalBlockSizeException | BadPaddingException exception) {
throw new RuntimeException("Failed to decrypt password", exception);
}
}
说实话,我不确定这是否正确,这是我能找到的关于这个主题的点点滴滴。我更改的所有内容都会抛出一个不同的异常,并且此特定构建不会运行,因为我无法实例化Cipher
,它会抛出NoSuchAlgorithmException: No provider found for EC
。我也试过切换到RSA
,但我也遇到了类似的错误。
所以我的问题基本上是这样的;如何在Android上加密明文,并在用户通过指纹API进行身份验证后使其可用于解密?
我取得了一些进展,主要是因为在KeyGenParameterSpec.Builder().setUserAuthenticationRequired(true)
文档页面上发现了这些信息。
我保持getKeyStore
,encryptePassword
,decryptPassword
,getKeyPairGenerator
和getCipher
大致相同,但我更改了KeyPairGenerator.getInstance
和{ {1}}分别为Cipher.getInstance
和"RSA"
。
我还将其余代码更改为RSA而不是Elliptic Curve,因为根据我的理解,Java 1.7(以及Android)不支持使用EC进行加密和解密。我在文档页面上基于“使用RSA OAEP加密/解密的RSA密钥对”示例更改了我的"RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
方法:
createKeyPair
我还根据private void createKeyPair() {
try {
mKeyPairGenerator.initialize(
new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setUserAuthenticationRequired(true)
.build());
mKeyPairGenerator.generateKeyPair();
} catch(InvalidAlgorithmParameterException exception) {
throw new RuntimeException(exception);
}
}
文档中的已知问题更改了我的initCipher
方法:
Android 6.0(API级别23)中的已知错误导致即使对于公钥也强制执行与用户身份验证相关的授权。要解决此问题,请提取要在Android Keystore外部使用的公钥材料。
KeyGenParameterSpec
现在我可以加密密码,并保存加密密码。但是当我获得加密密码并尝试解密时,我收到private boolean initCipher(int opmode) {
try {
mKeyStore.load(null);
if(opmode == Cipher.ENCRYPT_MODE) {
PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey();
PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm())
.generatePublic(new X509EncodedKeySpec(key.getEncoded()));
mCipher.init(opmode, unrestricted);
} else {
PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null);
mCipher.init(opmode, key);
}
return true;
} catch (KeyPermanentlyInvalidatedException exception) {
return false;
} catch(KeyStoreException | CertificateException | UnrecoverableKeyException
| IOException | NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException exception) {
throw new RuntimeException("Failed to initialize Cipher", exception);
}
}
未知错误 ...
KeyStoreException
答案 0 :(得分:36)
我在Android Issue Tracker找到了拼图的最后一部分,另一个已知错误导致无限制PublicKey
在使用OAEP时与Cipher
不兼容。解决方法是在初始化时向OAEPParameterSpec
添加新的Cipher
:
OAEPParameterSpec spec = new OAEPParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
mCipher.init(opmode, unrestricted, spec);
以下是最终代码:
public KeyStore getKeyStore() {
try {
return KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException exception) {
throw new RuntimeException("Failed to get an instance of KeyStore", exception);
}
}
public KeyPairGenerator getKeyPairGenerator() {
try {
return KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
} catch(NoSuchAlgorithmException | NoSuchProviderException exception) {
throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception);
}
}
public Cipher getCipher() {
try {
return Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
} catch(NoSuchAlgorithmException | NoSuchPaddingException exception) {
throw new RuntimeException("Failed to get an instance of Cipher", exception);
}
}
private void createKeyPair() {
try {
mKeyPairGenerator.initialize(
new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setUserAuthenticationRequired(true)
.build());
mKeyPairGenerator.generateKeyPair();
} catch(InvalidAlgorithmParameterException exception) {
throw new RuntimeException("Failed to generate key pair", exception);
}
}
private boolean initCipher(int opmode) {
try {
mKeyStore.load(null);
if(opmode == Cipher.ENCRYPT_MODE) {
PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey();
PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm())
.generatePublic(new X509EncodedKeySpec(key.getEncoded()));
OAEPParameterSpec spec = new OAEPParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
mCipher.init(opmode, unrestricted, spec);
} else {
PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null);
mCipher.init(opmode, key);
}
return true;
} catch (KeyPermanentlyInvalidatedException exception) {
return false;
} catch(KeyStoreException | CertificateException | UnrecoverableKeyException
| IOException | NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException exception) {
throw new RuntimeException("Failed to initialize Cipher", exception);
}
}
private void encrypt(String password) {
try {
initCipher(Cipher.ENCRYPT_MODE);
byte[] bytes = mCipher.doFinal(password.getBytes());
String encrypted = Base64.encodeToString(bytes, Base64.NO_WRAP);
mPreferences.getString("password").set(encrypted);
} catch(IllegalBlockSizeException | BadPaddingException exception) {
throw new RuntimeException("Failed to encrypt password", exception);
}
}
private String decrypt(Cipher cipher) {
try {
String encoded = mPreferences.getString("password").get();
byte[] bytes = Base64.decode(encoded, Base64.NO_WRAP);
return new String(cipher.doFinal(bytes));
} catch (IllegalBlockSizeException | BadPaddingException exception) {
throw new RuntimeException("Failed to decrypt password", exception);
}
}