通常,在使用第三方服务时,将需要某种形式的身份验证。 这可能和接受用户名和密码的/login
端点一样简单。
乍看起来,一个简单的解决方案是构建一个UI,要求用户登录,然后捕获并存储其登录凭据。 但是,这不是最佳做法,因为我们的应用程序不需要知道第三方帐户的凭据。 取而代之的是,我们可以使用客户经理,该经理为我们委托处理该敏感信息。
客户经理
帐户管理器是用户帐户凭据的集中帮助者,因此您的应用程序不必直接处理密码。 它通常提供代替实际用户名和密码的令牌,可用于向服务发出经过身份验证的请求。 一个示例是请求OAuth2令牌时 。
有时,所有必需的信息已经存储在设备上,而有时帐户管理器将需要调用服务器以获取刷新的令牌。 您可能已经在设备的“设置”中看到了各种应用的“ 帐户”部分。 我们可以这样获得可用帐户的列表:
AccountManager accountManager = AccountManager.get(this);
Account[] accounts = accountManager.getAccounts();
该代码将需要android.permission.GET_ACCOUNTS
权限。 如果您正在寻找一个特定的帐户,可以这样找到它:
AccountManager accountManager = AccountManager.get(this);
Account[] accounts = accountManager.getAccountsByType("com.google");
拥有帐户后,可以通过调用getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)
方法来检索该帐户的令牌。 然后可以使用令牌对服务发出经过身份验证的API请求。 这可能是RESTful API,您可以在HTTPS请求期间传递令牌参数,而不必知道用户的私人帐户详细信息。
由于每种服务将具有不同的身份验证和存储专用凭据的方式,因此帐户管理器提供了用于第三方服务实现的身份验证器模块。 Android提供了许多流行服务的实现,这意味着您可以编写自己的身份验证器来处理应用程序的帐户身份验证和凭据存储。 这使您可以确保凭据已加密。 请记住,这还意味着其他服务使用的帐户管理器中的凭据可能以明文形式存储,从而使植根设备的任何人都可以看到它们。
有时候,您将需要处理个人或实体的密钥或证书,而不是简单的凭证,例如,当第三方向您发送需要保留的证书文件时。 最常见的情况是应用程序需要向私有组织的服务器进行身份验证时。
在下一个教程中,我们将研究使用证书进行身份验证和安全通信,但是我仍然想解决与此同时如何存储这些项目。 钥匙串API最初是为特定用途而构建的-从PKCS#12文件中安装私钥或证书对。
钥匙扣
在Android 4.0(API级别14)中引入的Keychain API处理密钥管理。 具体来说,它与PrivateKey
和X509Certificate
对象一起使用,并且比使用应用程序的数据存储提供了更安全的容器。 这是因为对私钥的权限仅允许您自己的应用程序访问私钥,并且只有在用户授权后才能访问私钥。 这意味着必须先在设备上设置锁定屏幕,然后才能使用凭据存储。 同样,钥匙串中的对象可以绑定到安全硬件(如果有)。
安装证书的代码如下:
Intent intent = KeyChain.createInstallIntent();
byte[] p12Bytes = //... read from file, such as example.pfx or example.p12...
intent.putExtra(KeyChain.EXTRA_PKCS12, p12Bytes);
startActivity(intent);
系统将提示用户输入密码以访问私钥,并提示用户为证书命名。 要检索密钥,以下代码提供了一个UI,使用户可以从已安装密钥的列表中进行选择。
KeyChain.choosePrivateKeyAlias(this, this, new String[]{"RSA"}, null, null, -1, null);
做出选择后,将在alias(final String alias)
回调中返回alias(final String alias)
,您可以在其中直接访问私钥或证书链。
public class KeychainTest extends Activity implements ..., KeyChainAliasCallback
{
//...
@Override
public void alias(final String alias)
{
Log.e("MyApp", "Alias is " + alias);
try
{
PrivateKey privateKey = KeyChain.getPrivateKey(this, alias);
X509Certificate[] certificateChain = KeyChain.getCertificateChain(this, alias);
}
catch ...
}
//...
}
有了这些知识,现在让我们看看如何使用凭证存储来保存您自己的敏感数据。
密钥库
在上一教程中 ,我们研究了通过用户提供的密码保护数据。 这种设置很好,但是应用程序要求通常避免让用户每次登录并记住其他密码。
这就是可以使用KeyStore API的地方。 从API 1开始,系统已使用KeyStore来存储WiFi和VPN凭据。 从4.3(API 18)开始,它允许您使用自己的应用程序特定的非对称密钥,并且在Android M(API 23)中,它可以存储AES 对称密钥。 因此,尽管API不允许直接存储敏感字符串,但是可以存储这些密钥,然后将其用于加密字符串。
将密钥存储在KeyStore中的好处是它允许在不暴露该密钥的秘密内容的情况下操作密钥。 关键数据不会进入应用程序空间。 请记住,密钥受权限保护,因此只有您的应用程序才能访问它们,并且如果设备有能力,它们可能还具有安全的硬件支持。 这将创建一个容器,使从设备提取密钥更加困难。
生成一个新的随机密钥
对于此示例,我们可以自动生成一个将在KeyStore中受保护的随机密钥,而不是从用户提供的密码生成AES密钥。 为此,我们可以创建一个KeyGenerator
实例,并将其设置为"AndroidKeyStore"
提供程序。
//Generate a key and store it in the KeyStore
final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
//.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled
//.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time
.setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call
.build();
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
这里要注意的重要部分是.setUserAuthenticationRequired(true)
和.setUserAuthenticationValidityDurationSeconds(120)
规范。 这些要求设置锁定屏幕并锁定键,直到用户通过身份验证为止。
查看.setUserAuthenticationValidityDurationSeconds()
的文档,您会发现这意味着密钥仅在密码身份验证后的一定秒数内可用,而传入-1
每次您想要访问密钥时都需要指纹身份验证。 启用身份验证要求还具有在用户删除或更改锁定屏幕时撤销密钥的效果。
由于将未保护的密钥与加密数据一起存储就像将房屋密钥放在门垫下一样,因此这些选项会尝试在设备受损时保护静止的密钥。 一个示例可能是设备的脱机数据转储。 如果不知道设备的密码,该数据将变得无用。
.setRandomizedEncryptionRequired(true)
选项启用要求有足够的随机化(每次都有一个新的随机IV),以便如果第二次对同一数据进行加密,则加密的输出将仍然不同。 这样可以防止攻击者基于馈送相同数据而获得有关密文的线索。
另一个需要注意的选项是setUserAuthenticationValidWhileOnBody(boolean remainsValid)
,一旦设备检测到它不再在人身上,它就会锁定该密钥。
加密数据
现在,密钥已存储在KeyStore中,我们可以创建一个方法,使用给定SecretKey
的Cipher
对象加密数据。 它将返回一个HashMap
其中包含加密的数据和解密数据所需的随机IV。 然后可以将加密的数据与IV一起保存到文件或共享的首选项中。
private HashMap encrypt(final byte[] decryptedBytes)
{
final HashMap map = new HashMap();
try
{
//Get the key
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null);
final SecretKey secretKey = secretKeyEntry.getSecretKey();
//Encrypt data
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
final byte[] ivBytes = cipher.getIV();
final byte[] encryptedBytes = cipher.doFinal(decryptedBytes);
map.put("iv", ivBytes);
map.put("encrypted", encryptedBytes);
}
catch (Throwable e)
{
e.printStackTrace();
}
return map;
}
解密为字节数组
对于解密,应用相反的操作。 使用DECRYPT_MODE
常量初始化Cipher
对象,并返回解密的byte[]
数组。
private byte[] decrypt(final HashMap map)
{
byte[] decryptedBytes = null;
try
{
//Get the key
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null);
final SecretKey secretKey = secretKeyEntry.getSecretKey();
//Extract info from map
final byte[] encryptedBytes = map.get("encrypted");
final byte[] ivBytes = map.get("iv");
//Decrypt data
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
final GCMParameterSpec spec = new GCMParameterSpec(128, ivBytes);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
decryptedBytes = cipher.doFinal(encryptedBytes);
}
catch (Throwable e)
{
e.printStackTrace();
}
return decryptedBytes;
}
测试示例
现在我们可以测试我们的示例!
@TargetApi(Build.VERSION_CODES.M)
private void testEncryption()
{
try
{
//Generate a key and store it in the KeyStore
final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
//.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled
//.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time
.setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call
.build();
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
//Test
final HashMap map = encrypt("My very sensitive string!".getBytes("UTF-8"));
final byte[] decryptedBytes = decrypt(map);
final String decryptedString = new String(decryptedBytes, "UTF-8");
Log.e("MyApp", "The decrypted string is " + decryptedString);
}
catch (Throwable e)
{
e.printStackTrace();
}
}
对较旧的设备使用RSA非对称密钥
这是存储版本M或更高版本的数据的很好的解决方案,但是如果您的应用程序支持早期版本,该怎么办? M下不支持AES对称密钥,而RSA非对称密钥受支持。 这意味着我们可以使用RSA密钥和加密来完成同一件事。
这里的主要区别在于,非对称密钥对包含两个密钥,一个是私有密钥,另一个是公共密钥,其中,公共密钥加密数据,而私有密钥解密数据。 将KeyPairGeneratorSpec
传递到使用KEY_ALGORITHM_RSA
和 "AndroidKeyStore"
提供程序初始化的KeyPairGenerator
中。
private void testPreMEncryption()
{
try
{
//Generate a keypair and store it in the KeyStore
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 10);
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this)
.setAlias("MyKeyAlias")
.setSubject(new X500Principal("CN=MyKeyName, O=Android Authority"))
.setSerialNumber(new BigInteger(1024, new Random()))
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.setEncryptionRequired() //on API level 18, encrypted at rest, requires lock screen to be set up, changing lock screen removes key
.build();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();
//Encryption test
final byte[] encryptedBytes = rsaEncrypt("My secret string!".getBytes("UTF-8"));
final byte[] decryptedBytes = rsaDecrypt(encryptedBytes);
final String decryptedString = new String(decryptedBytes, "UTF-8");
Log.e("MyApp", "Decrypted string is " + decryptedString);
}
catch (Throwable e)
{
e.printStackTrace();
}
}
为了进行加密,我们从密钥对中获取RSAPublicKey
并将其与Cipher
对象一起使用。
public byte[] rsaEncrypt(final byte[] decryptedBytes)
{
byte[] encryptedBytes = null;
try
{
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null);
final RSAPublicKey publicKey = (RSAPublicKey)privateKeyEntry.getCertificate().getPublicKey();
final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
cipherOutputStream.write(decryptedBytes);
cipherOutputStream.close();
encryptedBytes = outputStream.toByteArray();
}
catch (Throwable e)
{
e.printStackTrace();
}
return encryptedBytes;
}
使用RSAPrivateKey
对象完成解密。
public byte[] rsaDecrypt(final byte[] encryptedBytes)
{
byte[] decryptedBytes = null;
try
{
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null);
final RSAPrivateKey privateKey = (RSAPrivateKey)privateKeyEntry.getPrivateKey();
final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
final CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(encryptedBytes), cipher);
final ArrayList arrayList = new ArrayList<>();
int nextByte;
while ( (nextByte = cipherInputStream.read()) != -1 )
{
arrayList.add((byte)nextByte);
}
decryptedBytes = new byte[arrayList.size()];
for(int i = 0; i < decryptedBytes.length; i++)
{
decryptedBytes[i] = arrayList.get(i);
}
}
catch (Throwable e)
{
e.printStackTrace();
}
return decryptedBytes;
}
关于RSA的一件事是加密比AES慢。 通常,这适用于少量信息,例如在保护共享首选项字符串时。 但是,如果发现加密大量数据存在性能问题,则可以改用本示例仅加密和存储AES密钥。 然后,对其他数据使用上一教程中讨论的更快的AES加密。 您可以生成一个新的AES密钥,并将其转换为与此示例兼容的byte[]
数组。
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256); //AES-256
SecretKey secretKey = keyGenerator.generateKey();
byte[] keyBytes = secretKey.getEncoded();
要从字节取回密钥,请执行以下操作:
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
那是很多代码! 为了简化所有示例,我省略了彻底的异常处理。 但是请记住,对于您的生产代码,不建议仅在一个catch语句中捕获所有Throwable
案例。
结论
这样就完成了有关使用凭证和密钥的教程。 关于密钥和存储的大部分困惑都与Android OS的发展有关,但是在您的应用程序支持的API级别下,您可以选择使用哪种解决方案。
既然我们已经介绍了保护静态数据的最佳实践,那么下一个教程将着重于保护传输中的数据。
翻译自: https://code.tutsplus.com/tutorials/keys-credentials-and-storage-on-android--cms-30827