Posted by Sergio Giro, Software Engineer

random_droid

If your Android app derives keys using the SHA1PRNG algorithm from the encryption provider, it must start using the actual key derivation function and possibly re-encrypt the data.

The Java Cryptography Architecture allows developers to create instances of classes, such as passwords or pseudo-random number generators, using calls like:

SomeClass.getInstance(“SomeAlgorithm”,“SomeProvider”);

or simply:

SomeClass.getInstance( “SomeAlgorithm”);

E.g,

Cipher.getInstance(“AES / CBC / PKCS5PADDING”); 
SecureRandom.getInstance(“SHA1PRNG”);

On Android, we do not recommend specifying a provider. In general, any calls to the Java Cryptography Extension (JCE) API for a given provider should only be made if the provider is included in the application or if the application is capable of handling a possible ProviderNotFoundException.

Unfortunately, many applications rely on the now-removed "crypto" provider for an anti-pattern for key derivation.

此提供程序仅为SecureRandom实例提供了算法“SHA1PRNG”的实现。问题是SHA1PRNG算法在加密方面不强。对于对细节感兴趣的读者,基于统计距离的伪随机序列测试以及PHP和Debian OpenSSL的实验,第8.1节,作者:Yongge Want和Tony Nicol,指出以二进制形式考虑的“随机”序列偏向于返回0,并且偏差根据种子而恶化。

因此,在Android N中,我们完全弃用了SHA1PRNG算法和Crypto提供程序的实现。我们之前曾介绍过几年前使用SecureRandom进行密钥派生在使用加密技术安全存储凭证方面的问题但是,鉴于其继续使用,我们将在此重新审视。

此提供程序的常见但不正确的用法是使用密码作为种子来派生加密密钥。SHA1PRNG的实现有一个错误,如果在获得输出之前调用了setSeed(),那么它就具有确定性。此错误用于通过提供密码作为种子来获取密钥,然后使用密钥的“随机”输出字节(此句中的“随机”表示“可预测且加密弱”)。然后可以使用这样的密钥来加密和解密数据。

在下文中,我们将解释如何正确导出密钥,以及如何解密使用不安全密钥加密的数据。还有一个 完整的示例,包括一个使用不推荐使用的SHA1PRNG功能的帮助程序类,其唯一目的是解密原本不可用的数据。

可以通过以下方式派生密钥:

  • 如果您正在从磁盘读取AES密钥,只需存储实际密钥,不要经历这种奇怪的舞蹈。您可以通过执行以下操作从字节中获取AES使用的SecretKey:

    SecretKey key = new SecretKeySpec(keyBytes,“AES”);

  • 如果您使用密码来获取密钥,请遵循Nikolay Elenkov的优秀教程,但需要注意的是,经验法则是盐的大小应与密钥输出的大小相同。它看起来像这样:
   / *用户输入密码:* /  
   String password =“password”;  

   / *将这些东西存储在以后用于导出密钥的磁盘上:* /  
   int iterationCount = 1000;  
   int saltLength = 32; //字节; 应该是相同的大小
              作为输出(256/8 = 32)  
   int keyLength = 256; // AES-256为256位,AES-128为128位等  
   byte [] salt; //应该是saltLength  

   / *首次创建密钥时,使用以下方法获取盐:* /  
   SecureRandom random = new SecureRandom();  
   byte [] salt = new byte [saltLength];  
   random.nextBytes(盐);  

   / *使用此命令从密码中导出密钥:* /  
   KeySpec keySpec = new PBEKeySpec(password.toCharArray(),salt,  
              iterationCount,keyLength);  
   SecretKeyFactory keyFactory = SecretKeyFactory  
              .getInstance( “PBKDF2WithHmacSHA1”);  
   byte [] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();  
   SecretKey key = new SecretKeySpec(keyBytes,“AES”);  

而已。你不需要任何其他东西。

为了简化转换数据,我们介绍了使用不安全密钥加密数据的开发人员的情况,该密钥每次都来自密码。您可以在 示例应用程序中使用帮助程序类InsecureSHA1PRNGKeyDerivator 来派生密钥。

 private static SecretKey deriveKeyInsecurely(String password,int
 keySizeInBytes){  
    byte [] passwordBytes = password.getBytes(StandardCharsets.US_ASCII);  
    返回新的SecretKeySpec(  
            InsecureSHA1PRNGKeyDerivator.deriveInsecureKey(  
                     passwordBytes,keySizeInBytes),  
            “AES”);  
 }  

然后,您可以使用如上所述的安全派生密钥重新加密您的数据,并过上幸福的生活。

注意1:作为保持应用正常运行的临时措施,我们仍决定为针对SDK版本23的应用创建实例,即适用于Marshmallow的SDK版本或更低版本。请不要依赖Android SDK中Crypto提供程序的存在,我们的计划是将来完全删除它。

注2:由于系统的许多部分假设存在SHA1PRNG算法,当请求SHA1PRNG的实例并且未指定提供者时,我们返回OpenSSLRandom的实例,这是从OpenSSL派生的强随机数源。