Security "Crypto" provider deprecated in Android N

和丰羽
2023-12-01
原文地址:
http://android-developers.blogspot.com/2016/06/security-crypto-provider-deprecated-in.html

Posted by Sergio Giro, software engineer

如果你的应用使用Crypto供应商提供的SHA1PRNG算法推导秘钥,你必须开始使用一个真正的秘钥推导函数并且很有可能你需要重新加密你的数据。


Java Cryptography Architecture允许开发者创建一个类(如cipher)的实例,或一个伪随机数生成器,如

SomeClass.getInstance("SomeAlgorithm", "SomeProvider");

或者更简单的

SomeClass.getInstance("SomeAlgorithm");

例如:

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

在Android上,我们不推荐你指定供应商。一般来说,任何对Java Cryptography Extension(JCE) APIs的调用,如果指定了供应商,那么它只有满足以下情况才会起作用:

应用包含了该供应商

or

程序可以处理可能会出现的ProviderNotFoundException


很不幸的是,很多应用依赖于现在已经过时的“Crypto”供应商,这都是秘钥推导的反面例子。


该供应商只提供了“SHA1PRNG”算法的一个实现来作为SecureRandom的一个实例。问题是SHA1PRNG算法并不是一个强壮的加密算法。读者对该细节感兴趣可以看这里:On statistical distance based testing of pseudo random sequences and experiments with PHP and Debian OpenSSL,Section 8.1, by Yongge Want and Tony Nicol, states that the “random” sequence, considered in binary form, is biased towards returning 0s, and that the bias worsens depending on the seed.


基于以上原因,在Android N中,我们彻底地反对“SHA1PRNG”算法的实现和“Crypto”供应商。几年前,我们使用SecureRandom来做秘钥推导仓促地覆盖该问题( Using Cryptography to Store Credentials Safely)。但是,鉴于其仍在使用,我们将继续观察。


关于该供应商,一个常见但错误的做法是使用密码作为种子来推导用于加密的秘钥。“SHA1PRNG”算法的实现有一个bug,如果在获得输出前调用setSeed(),会使得秘钥是确定的,不可改变的。该bug使用提供给它的密码作为种子来推导秘钥,然后使用“随机”输出字节作为秘钥(“随机”在这里的意思是可预知的,弱加密性的)。这样的秘钥接下来会被用来加密和解密数据。


下面,我们将会解释如何正确的推导秘钥,以及如何解密使用不安全秘钥加密的数据。这里也有一个完整的例子(full example),该例子包含了一个助手类,该类使用过时的SHA1PRNG函数,这个例子的唯一意图是做为例子展示解密数据,在其他方面没有价值。


可以根据以下方法推导秘钥:

  • 如果你从磁盘读取一个AES秘钥,你只需要储存真实的秘钥,而不需要经过下面的方法。你可以通过下面的方法从字节数组中取得用于AES的安全秘钥:

SecretKey key = new SecretKeySpec(keyBytes, "AES");

   /* User types in their password: */  
   String password = "password";  

   /* Store these things on disk used to derive key later: */  
   int iterationCount = 1000;  
   int saltLength = 32; // bytes; should be the same size
              //as the output (256 / 8 = 32)  
   int keyLength = 256; // 256-bits for AES-256, 128-bits for AES-128, etc  
   byte[] salt; // Should be of saltLength  

   /* When first creating the key, obtain a salt with this: */  
   SecureRandom random = new SecureRandom();  
   byte[] salt = new byte[saltLength];  
   random.nextBytes(salt);  

   /* Use this to derive the key from the password: */  
   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");  

就是这样,你不需要做其它多余的。


为了使数据转换更为容易,我们也考虑了这种情况:开发者总是使用密码来推导不安全的秘钥,并用该秘钥加密数据。你可以使用上面提到的full example中的助手类InsecureSHA1PRNGDerivator去推导(不安全的)秘钥。

 private static SecretKey deriveKeyInsecurely(String password, int
 keySizeInBytes) {  
    byte[] passwordBytes = password.getBytes(StandardCharsets.US_ASCII);  
    return new SecretKeySpec(  
            InsecureSHA1PRNGKeyDerivator.deriveInsecureKey(  
                     passwordBytes, keySizeInBytes),  
            "AES");  
 }  

之后你可以使用安全地推导的秘钥重新加密你的数据。


Note 1:

作为使程序继续工作的临时措施,我们决定还是为Android SDK 23以及Android M对应的SDK版本或更低版本提供实例。请不要依赖Android SDK里的Crypto供应商,我们的计划是在未来完全删除它。


Note 2:

因为系统的很多部分都假定SHA1PRNG算法的存在,当请求一个SHA1PRNG的实例而不指定供应商时,我们会返回一个OpenSSLRandom的实例,OpenSSLRandom是由OpenSSL推导出的强大的随机数源。


 类似资料: