• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

Android上的密钥、凭据和存储如何保护数据

通常,在使用第三方服务时,需要某种形式的身份验证。这可能就像/login接受用户名和密码的端点一样简单。 

Android上的密钥、凭据和存储如何保护数据  第1张

乍一看,一个简单的解决方案似乎是构建一个要求用户登录的 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");

拥有帐户后,可以通过调用该方法来检索该帐户的令牌。然后可以使用令牌向服务发出经过身份验证的api请求。这可能是一个安静的 API,您可以在https请求期间传入令牌参数,而无需知道用户的私人帐户详细信息。getAuthToken(Account, String, Bundle, Activity, AccountManagercallback, Handler)

由于每项服务都有不同的身份验证和存储私有凭证的方式,Account Manager 为第三方服务提供了身份验证模块来实现。虽然 Android 实现了许多流行的服务,但这意味着您可以编写自己的身份验证器来处理应用的帐户身份验证和凭据存储。这使您可以确保凭据已加密。请记住,这也意味着其他服务使用的帐户管理器中的凭据可能以明文形式存储,使任何已对其设备进行根植的人都可以看到它们。

有时您需要处理个人或实体的密钥或证书,而不是简单的凭据,例如,当第三方向您发送您需要保留的证书文件时。最常见的情况是当应用程序需要向私有组织的服务器进行身份验证时。 

在下一个教程中,我们将研究使用证书进行身份验证和安全通信,但我仍然想同时解决如何存储这些项目。Keychain 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) ,您可以在其中直接访问私钥或证书链。

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 中的好处是它允许对密钥进行操作而不会暴露该密钥的秘密内容;关键数据不进入应用空间。请记住,密钥受权限保护,因此只有您的应用程序可以访问它们,并且如果设备有能力,它们还可能是安全的硬件支持。这会创建一个容器,使从设备中提取密钥变得更加困难。 

生成一个新的随机密钥

对于这个例子,我们可以自动生成一个随机密钥,而不是从用户提供的密码生成一个 AES 密钥,该密钥将在 KeyStore 中受到保护。我们可以通过创建一个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 中,我们可以创建一个使用Cipher对象加密数据的方法,给定SecretKey. 它将返回一个HashMap包含加密数据和解密数据所需的随机 IV。然后可以将加密数据与 IV 一起保存到文件或共享首选项中。

private HashMap<String, byte[]> encrypt(final byte[] decryptedBytes)
{
    final HashMap<String, byte[]> map = new HashMap<String, byte[]>();
    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;
}

解密为字节数组

对于解密,应用相反。使用constCipher ant初始化对象,并返回一个解密的数组。DECRYPT_MODE byte[]

private byte[] decrypt(final HashMap<String, byte[]> 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<String, byte[]> 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 密钥和加密来完成同样的事情。 

这里的主要区别在于,非对称密钥对包含两个密钥,一个私钥和一个公钥,其中公钥加密数据,私钥解密数据。AKeyPairGeneratorSpec被传递到KeyPairGenerator初始化的那个和提供者中。KEY_ALGORITHM_RSA "AndroidKeyStore"

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<Byte> 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");

那是很多代码!为了使所有示例保持简单,我省略了彻底的异常处理。但请记住,对于您的生产代码,不建议Throwable在一个 catch 语句中简单地捕获所有情况。

结论

这样就完成了有关使用凭据和密钥的教程。围绕密钥和存储的许多困惑与 Android 操作系统的发展有关,但您可以根据应用支持的 API 级别选择使用哪种解决方案。 

现在我们已经介绍了保护静态数据的最佳实践,下一个教程将重点介绍保护传输中的数据。


文章目录
  • 客户经理
  • 钥匙扣
  • 密钥库
    • 生成一个新的随机密钥
    • 加密数据
    • 解密为字节数组
    • 测试示例
    • 为旧设备使用 RSA 非对称密钥
  • 结论