昨天在写非对称加密算法的代码时出现了这样一个编译错误:
说我的加密代码有一个潜在的不安全因素,说我这样生成的随机数在Android4.3上已经是旧的了,并给出了一个链接让我查看更多的有关信息,要是按照平常的这些编译错误,绝逼直接@SuppressLint了,想到这关系到加解密的安全性,还是不能就这么忽略掉。作为一个狂热的密码学份子我决定还是看一下链接给出的这篇文章,发现是谷歌对PRNG的一个fix,看完后还是少有收获的,因为网上这方面的资料甚少,我决定花个把小时把该篇文章翻一下,有兴趣的童鞋可以围观下,时间仓促,翻译若有不当见谅并请指出,谢谢!
原文地址:Some SecureRandom Thoughts
作者:Alex Klyubin,Android安全工程师
译者:Aige,Android渣渣
Android安全小组调查了一笔因比特币交易妥协而导致多个比特币应用在8月11号升级(该篇报导我就不翻译了,有空可以自己看看……)的根本原因。
我们现在确定那些使用JCA(Java密码学架构)来生成密钥、签名或随机数的应用可能由于PRNG(伪随机数生成器)没有被正确地初始化而导致在Android设备上没有接收到强加密的值,造成这样的原因是应用没有显式地初始化而直接地调用系统提供的OpenSSL PRNG。应用通过使用HttpClient和java.net包下的类来建立TLS/SSL连接不会影响那些类用/dev/urandom下的值生成OpenSSL PRNG种子。
那些使用JCA来生成密钥、签名或随机数的开发者应该升级自己的应用,使用/dev/urandom或/dev/random获取的熵显式地初始化PRNG。本文下面的代码会附上升级的建议。开发者也应当考虑是否重新生成以前用JCA API像SecureRandom、KeyGenerator、KeyPairGenerator、KeyAgreement和 Signature这样的类所生成的密钥或其他的一些随机值。
除了这些建议外,Android为了确保OpenSSL PRNG能够正确地被初始化还为OHA(开放手机联盟)伙伴提供了开发补丁。
同时,我们也非常感谢那些通知谷歌OpenSSL PRNG不正确初始化BUG的朋友:来自ETRI(韩国电子通信研究院)的Soo Hyeon Kim和Daewan Han、来自Korea University(高丽大学)的Dong Hoon Lee。
下面示例的源代码因为/dev/urandom不可写而导致在一小部分Android设备上不可正确地执行。现在我们更新代码来处理这个问题。
/* * 这段相当于使用声明,具体哥就不鸟了,有兴趣自己看 = = * This software is provided 'as-is', without any express or implied * warranty. In no event will Google be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, as long as the origin is not misrepresented. */ import android.os.Build; import android.os.Process; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.SecureRandom; import java.security.SecureRandomSpi; import java.security.Security; /** * 修复默认PRNG的低熵输出 * 注:熵的概念不懂的可以自己百度谷歌搜狗搜搜Bing一下,这里你可以简单地理解为随机数出现的期望值 * 注:熵的概念因不同学科有不同的解释,但都可以简单地理解为一种描述复杂程度的值、、、不知道我这样概括会不会被喷 = = * * 为了修复代码的正确执行,请在任何使用JCA的地方之前调用apply()方法{@link #apply()} * 当然你做Android最好是在入口声明调用这样就不会每次用到之前都调用一次啦……你要问我入口在哪? * A:如果你有自定义的Application类就在该类中的onCreate方法中声明调用 * B:如果你有自定义的基类Activity也可以在基类Activity的onCreate方法中调用 */ public final class PRNGFixes { private static final int VERSION_CODE_JELLY_BEAN = 16; private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial(); /** 隐藏构造函数防止实例化 */ private PRNGFixes() { } /** * 请求修复 * * @throws SecurityException * 当一个修复应当被完成但不能被完成时将会抛出此异常 */ public static void apply() { applyOpenSSLFix(); installLinuxPRNGSecureRandom(); } /** * 请求修复有低熵的OpenSSL PRNG,如果不需要修复则不会执行 * * @throws SecurityException * 当一个修复应当被完成但不能被完成时将会抛出此异常 */ private static void applyOpenSSLFix() throws SecurityException { if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { // 不需要修复 return; } try { // 混合设备和调用种子 Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto").getMethod("RAND_seed", byte[].class).invoke(null, generateSeed()); int bytesRead = (Integer) Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto").getMethod("RAND_load_file", String.class, long.class).invoke(null, "/dev/urandom", 1024); if (bytesRead != 1024) { throw new IOException("Unexpected number of bytes read from Linux PRNG: " + bytesRead); } } catch (Exception e) { throw new SecurityException("Failed to seed OpenSSL PRNG", e); } } /** * 安装一个支持Linux PRNG的{@code SecureRandom}作为默认实现。 * 如果已经有一个默认实现或者不需要安装实现时则不执行 * * @throws SecurityException * 当一个修复应当被完成但不能被完成时将会抛出此异常 */ private static void installLinuxPRNGSecureRandom() throws SecurityException { if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { // 不需要修复 return; } // 如果还没安装的话则安装一个支持Linux PRNG的SecureRandom作为默认实现 Provider[] secureRandomProviders = Security.getProviders("SecureRandom.SHA1PRNG"); if ((secureRandomProviders == null) || (secureRandomProviders.length < 1) || (!LinuxPRNGSecureRandomProvider.class.equals(secureRandomProviders[0].getClass()))) { Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); } SecureRandom rng1 = new SecureRandom(); if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider().getClass())) { throw new SecurityException("new SecureRandom() backed by wrong Provider: " + rng1.getProvider().getClass()); } SecureRandom rng2; try { rng2 = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException e) { throw new SecurityException("SHA1PRNG not available", e); } if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider().getClass())) { throw new SecurityException("SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + " Provider: " + rng2.getProvider().getClass()); } } /** * 通过所有请求至Linux PRNG{@code SecureRandom}的{@code Provider}引擎 */ private static class LinuxPRNGSecureRandomProvider extends Provider { public LinuxPRNGSecureRandomProvider() { super("LinuxPRNG", 1.0, "A Linux-specific random number provider that uses" + " /dev/urandom"); // 虽然/dev/urandom没有一个SHA-1的PRNG,但是一些APP明确地请求一个SHA1PRNG的SecureRandom, // 因此我们需要防止那些可能输出低熵的APP获取默认的实现 put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); } } /** * 自定义的{@link SecureRandomSpi}来传递所有的请求给Linux PRNG( {@code /dev/urandom}). */ public static class LinuxPRNGSecureRandom extends SecureRandomSpi { /* * 实现时注意:请求生成的bytes和混合的种子被传递至Linux PRNG(/dev/urandom)。 * 本类实例将会把种子在其当前时间、PID、UID混合而生成指纹和硬件序列号(可用的话)写入Linux PRNG。 * 并发性:给底层Linux PRNG读取请求是被序列化的(一个类似安全锁的东西)以此来确保多线程运行时不会获取复制了的PRNG输出 */ private static final File URANDOM_FILE = new File("/dev/urandom"); private static final Object sLock = new Object(); /** * 读取Linux PRNG的输入流 * 如果还未开启则读取值为null * * @GuardedBy("sLock") */ private static DataInputStream sUrandomIn; /** * 写入Linux PRNG的输出流 * 如果还未开启则写入值为null * * @GuardedBy("sLock") */ private static OutputStream sUrandomOut; /** * 该引擎的实例是否已经生成了种子 * 该判断值是必要的,因为每一个实例需要种子如果客户端还未明确生成它 */ private boolean mSeeded; @Override protected void engineSetSeed(byte[] bytes) { try { OutputStream out; synchronized (sLock) { out = getUrandomOutputStream(); } out.write(bytes); out.flush(); } catch (IOException e) { // 在一小部分设备上/dev/urandom是不可写的。我们打印并输出该错误异常 Log.w(PRNGFixes.class.getSimpleName(), "Failed to mix seed into " + URANDOM_FILE); } finally { mSeeded = true; } } @Override protected void engineNextBytes(byte[] bytes) { if (!mSeeded) { // 混合设备和调用种子 engineSetSeed(generateSeed()); } try { DataInputStream in; synchronized (sLock) { in = getUrandomInputStream(); } synchronized (in) { in.readFully(bytes); } } catch (IOException e) { throw new SecurityException("Failed to read from " + URANDOM_FILE, e); } } @Override protected byte[] engineGenerateSeed(int size) { byte[] seed = new byte[size]; engineNextBytes(seed); return seed; } private DataInputStream getUrandomInputStream() { synchronized (sLock) { if (sUrandomIn == null) { try { sUrandomIn = new DataInputStream(new FileInputStream(URANDOM_FILE)); } catch (IOException e) { throw new SecurityException("Failed to open " + URANDOM_FILE + " for reading", e); } } return sUrandomIn; } } private OutputStream getUrandomOutputStream() throws IOException { synchronized (sLock) { if (sUrandomOut == null) { sUrandomOut = new FileOutputStream(URANDOM_FILE); } return sUrandomOut; } } } /** * 生成设备和调用的种子并混合到Linux的PRNG中 */ private static byte[] generateSeed() { try { ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer); seedBufferOut.writeLong(System.currentTimeMillis()); seedBufferOut.writeLong(System.nanoTime()); seedBufferOut.writeInt(Process.myPid()); seedBufferOut.writeInt(Process.myUid()); seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); seedBufferOut.close(); return seedBuffer.toByteArray(); } catch (IOException e) { throw new SecurityException("Failed to generate seed", e); } } /** * 获取当前设备的硬件序列号 * * @return 返回硬件序列号,如果硬件序列号不可获取则返回null */ private static String getDeviceSerialNumber() { /* * 这里我们使用反射的方式去获取序列号 因为在API 9之前Build.SERIAL是不可用的 * 注:Android的一些未完成或者说不确定的API是不会被直接显示在提供的Source中,我们经常会在一些API源码里看到@Hide注解 */ try { return (String) Build.class.getField("SERIAL").get(null); } catch (Exception ignored) { return null; } } private static byte[] getBuildFingerprintAndDeviceSerial() { StringBuilder result = new StringBuilder(); String fingerprint = Build.FINGERPRINT; if (fingerprint != null) { result.append(fingerprint); } String serial = getDeviceSerialNumber(); if (serial != null) { result.append(serial); } try { return result.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException("UTF-8 encoding not supported"); } } }
常言道,知其然知其所以然。凡事我们都爱究其原理这是一个好习惯。但是,你总不能把所有东西都搞懂再去做事吧?就像我常说的,你总不能把所有Android代码都看懂再做开发吧……那不现实。所以,如果你看不懂上面的代码没关系,知道如何用即可。在代码的注释头我已经写明了如何使用,如果你还不会用可以看看我写的:
这是项目结构,就一个安全工具类和上面我给出的那个PRNGFixes类
具体使用只需在生成PRNG前调用PRNGFixes类下的apply()方法即可:
如果你有自定义的Application类:
import com.aigestudio.helpers.PRNGFixes; import android.app.Application; public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); PRNGFixes.apply(); } }
如果你有自定义的Activity基类:
import com.aigestudio.helpers.PRNGFixes; import android.app.Activity; import android.os.Bundle; public class BaseActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PRNGFixes.apply(); } }