现在的位置: 首页 > 综合 > 正文

关于SecureRandom的一些想法

2016年12月14日 ⁄ 综合 ⁄ 共 8816字 ⁄ 字号 评论关闭

昨天在写非对称加密算法的代码时出现了这样一个编译错误:

说我的加密代码有一个潜在的不安全因素,说我这样生成的随机数在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();
	}
}

抱歉!评论已关闭.