Android中SecureRandom类的安全性浅析

</pre><h2>背景</h2><p>2013年比特币开发商在一篇博客中透露,由于Android系统存在一处关键漏洞,该平台上的比特币电子钱包很容易失窃。比特币开发商称,该漏洞影响到Android平台上的每一个比特币电子钱包应用程序,包括流行的比特币钱包(Bitcoin Wallet)、blockchain.info钱包(blockchain.info wallet)、BitcoinSpinner钱包(BitcoinSpinner Wallet)和Mycelium钱包(Mycelium Wallet)等。</p><p>该漏洞存在于Android系统随机生成数字串安全密钥的环节中。该漏洞的生成原因是对SecureRandom类的不正确使用方式导致的。翻看Android的官方文档会发现。对于SecureRandom类的构造函数SecureRandom(byte[] seed)和SecureRandom#setSeed方法有一段安全性提醒:</p><p><span style="color:rgb(34,34,34); font-family:Roboto,sans-serif; font-size:14px; line-height:19px; background-color:rgb(249,249,249)">A seed is an array of bytes used to bootstrap random number generation. To produce cryptographically secure random numbers, both the seed and the algorithm must be secure.</span></p><p style="margin-top:0px; margin-bottom:15px; color:rgb(34,34,34); font-family:Roboto,sans-serif; font-size:14px; line-height:19px; background-color:rgb(249,249,249)">By default, instances of this class will generate an initial seed using an internal entropy source, such as <code style="font-size:13px; color:rgb(0,102,0); line-height:14px">/dev/urandom</code>. This seed is unpredictable and appropriate for secure use.</p><p style="margin-top:0px; margin-bottom:15px; color:rgb(34,34,34); font-family:Roboto,sans-serif; font-size:14px; line-height:19px; background-color:rgb(249,249,249)">Using the <code style="font-size:13px; color:rgb(0,102,0); line-height:14px"><a target=_blank target="_blank" href="http://developer.android.com/reference/java/security/SecureRandom.html#SecureRandom(byte[])" style="color:rgb(37,138,175); text-decoration:none">seeded constructor</a></code> or calling <code style="font-size:13px; color:rgb(0,102,0); line-height:14px"><a target=_blank target="_blank" href="http://developer.android.com/reference/java/security/SecureRandom.html#setSeed(byte[])" style="color:rgb(37,138,175); text-decoration:none">setSeed(byte[])</a></code> may completely replace the cryptographically strong default seed causing the instance to return a predictable sequence of numbers unfit for secure use. Due to variations between implementations it is not recommended to use <code style="font-size:13px; color:rgb(0,102,0); line-height:14px">setSeed</code> at all.</p>遗憾的是Android官网并未对此做过多的解释。setSeed方法为什么会引起安全风险?应该怎样使用SecureRandom类?这些问题都要从SecureRandom的原理说起。<h2>SecureRandom如何生成随机数?</h2><div>SecureRandom随机性是通过它的seed来保证的。如果输入相同的seed会导致生成重复的随机数。SecureRandom内部维护一个internal random state,它生成随机数的方式具有确定性。(如果输入相同的seed那么生成的随机数也相同)具体过程如下图:</div><div></div><div><img src="https://img-blog.csdn.net/20150111161934156?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZmlzaG1lbjI2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" width="450" height="350" alt="" /></div><div></div><div>在生成一个随机数时internal random state会从seed源中取出一个seed。通过内部运算生成随机数。所以SecureRandom依靠输入随机的选取seed来保证自己能够生成出不相同的随机数。</div><div></div><h2>SecureRandom的安全隐患</h2><div>在SecureRandom生成随机数时,如果我们不调用setSeed方法,SecureRandom会从系统的中找到一个默认随机源。每次生成随机数时都会从这个随机源中取seed。在linux和Android中这个随机源位于/dev/urandom文件。 如果我们在终端可以运行cat /dev/urandom命令,会观察到随机值会不断的打印到屏幕上。</div><div></div><h2>setSeed方法为何有安全风险?</h2><div>在Android 4.2以下,SecureRandom是基于老版的Bouncy Castle实现的。如果生成SecureRandom对象后马上调用setSeed方法。SecureRandom会用用户设置的seed代替默认的随机源。使得每次生成随机数时都是会使用相同的seed作为输入。从而导致生成的随机数是相同的。下面是一段存在安全风险的使用方法:</div><div></div><div><pre name="code" class="java">SecureRandom secureRandom = new SecureRandom();
  byte[] b = new byte[] { (byte) 1 };
  secureRandom.setSeed(b);
  // Prior to Android 4.2, the next line would always return the same number!
  System.out.println(secureRandom.nextInt());

4.2以上的SecureRadom类为什么没有这个问题呢?因为经过比特币钱包漏洞之后Google修改了SecureRandom的内部实现,用基于OpenSSL的算法替代了老版的Bouncy Castle。用户调用setSeed时会将用户设置的seed添加到随机源(/dev/urandom)中而不是简单的替换。

如何在Android 4.2以前安全的调用SecureRadom类的setSeed方法呢?

我们可以通过SecureRandom#nextBytes(byte[] bytes)避免这个问题。具体做法是调用setSeed方法前先调用一次SecureRandom#nextBytes(byte[] bytes)方法。为什么这样就可以避免默认随机源被替代呢? 我们可以从源码中找到答案。(本文所引用代码全部基于Android API 16)

在SecureRandom初始化时, 会生成一个SecureRandomSpi对象。SecureRandom的核心方法都由SecureRandomSpi对象代理。 下面是SecureRandomSpi类的子类SHA1PRNG_SecureRandomImpl的初始化

  public SHA1PRNG_SecureRandomImpl() {

        seed = new int[HASH_OFFSET + EXTRAFRAME_OFFSET];
        seed[HASH_OFFSET] = H0;
        seed[HASH_OFFSET + 1] = H1;
        seed[HASH_OFFSET + 2] = H2;
        seed[HASH_OFFSET + 3] = H3;
        seed[HASH_OFFSET + 4] = H4;

        seedLength = 0;
        copies = new int[2 * FRAME_LENGTH + EXTRAFRAME_OFFSET];
        nextBytes = new byte[DIGEST_LENGTH];
        nextBIndex = HASHBYTES_TO_USE;
        counter = COUNTER_BASE;
        state = UNDEFINED;
    }

SecureRandom#nextBytes方法会调用SecureRandomSpi#engineNextBytes方法

protected synchronized void engineNextBytes(byte[] bytes) {

        int i, n;

        long bits; // number of bits required by Secure Hash Standard
        int nextByteToReturn; // index of ready bytes in "bytes" array
        int lastWord; // index of last word in frame containing bytes
        final int extrabytes = 7;// # of bytes to add in order to computer # of 8 byte words

        if (bytes == null) {
            throw new NullPointerException("bytes == null");
        }

        lastWord = seed[BYTES_OFFSET] == 0 ? 0
                : (seed[BYTES_OFFSET] + extrabytes) >> 3 - 1;

        if (state == UNDEFINED) {

            // no seed supplied by user, hence it is generated thus randomizing internal state
            updateSeed(RandomBitsSupplier.getRandomBits(DIGEST_LENGTH));
            nextBIndex = HASHBYTES_TO_USE;

        } else if (state == SET_SEED) {
        ...


 如果state的状态为UNDEFINED,那么nextBytes会使用默认的随机源。并将state设置为NEXT_BYTES之后如果调用setSeed方法,最终会调用到SecureRandomSpi#engineSetSeed方法.源码如下: 

protected synchronized void engineSetSeed(byte[] seed) {

        if (seed == null) {
            throw new NullPointerException("seed == null");
        }

        if (state == NEXT_BYTES) { // first setSeed after NextBytes; restoring hash
            System.arraycopy(copies, HASHCOPY_OFFSET, this.seed, HASH_OFFSET,
                    EXTRAFRAME_OFFSET);
        }
        state = SET_SEED;

        if (seed.length != 0) {
            updateSeed(seed);
        }
    }


当state == NEXT_BYTES时会恢复原有的hash再更新seed,因为nextBytes时已经设置了系统的seed所以setSeed中传入的seed将添加到系统seed的尾部。

所以在调用setSeed方式之前一定要先调用nextBytes方法。保证seed随机源不会被替代。


SecureRandom的一种误用模式

最近网上流传一种利用SecureRandom输出固定随机值并用这个随机值当作加密秘钥的用法。这种模式利用前一节中提到的用特定seed代替系统随机源的方法,故意让SecureRandom每次都输出固定的随机值。通过这个固定值作为秘钥加密本地文本。其使用方式和流程如下:



代码例子:
public static byte[] encrypt(byte[] data, String seed) throws Exception {

    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
    secrand.setSeed(seed.getBytes());
    keygen.init(128, secrand);

    SecretKey seckey = keygen.generateKey();
    byte[] rawKey = seckey.getEncoded();

    SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    return cipher.doFinal(data);
}

public static byte[] decrypt(byte[] data, String seed) throws Exception {

    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    SecureRandom secrand = SecureRandom.getInstance("SHA1PRNG");
    secrand.setSeed(seed.getBytes());
    keygen.init(128, secrand);

    SecretKey seckey = keygen.generateKey();
    byte[] rawKey = seckey.getEncoded();

    SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, skeySpec);
    return cipher.doFinal(data);
}

这种方式确实可以对原始秘钥做一定的隐藏。起到混淆的作用。但google官方博客否定了该方式的使用。 其原因大致如下:
  1. 对资深的攻击者而言这种方式的加密太过简单。他们可以轻易的看懂这里在干什么,并构造有效的攻击代码。
  2. 整个加密的过程依赖SecureRandom的实现细节,这种依赖使得程序非常的健壮性和可扩展性都非常脆弱。例如在Android API 17以后SecureRandom的默认实现方式从Cipher.RSA 换到了 OpenSSL。SecureRandom新的实现方式不能将自己的seed替换掉系统的seed。造成这段代码在API 17以上不能工作。APP必须强制升级才能继续运作。对某个类内部细节的依赖是软件设计中的大忌。
  3. 从seed到生成key的过程非常的廉价,时间成本和资源要求的很低。如果攻击者采用暴力破解这种加密方式将显的很脆弱。

如何正确的从password生成一个秘钥?

标准的秘钥生成方式应该使用PKCS#5算法。该算法主要有两个优点。
1. 利用随机盐加强秘钥的强度。随机盐可以有效的防止暴力破解。同一个passwrod可以生成多个秘钥。攻击者不得不针对每个salt构造不同的秘钥字典。
2. 通过迭代方式增加秘钥生成的时间成本。使得攻击者破解秘钥的时间大大增加。

Android的JCE provider 现在能支持PBKDF2WithHmacSHA1。下面的代码将展示如何通过PBK算法将passowrd生成为一个秘钥。

String password  = "password";
int iterationCount = 1000;
int keyLength = 256;
int saltLength = keyLength / 8; // same size as key output

SecureRandom random = new SecureRandom();
byte[] salt = new byte[saltLength];
randomb.nextBytes(salt);
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");

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = new byte[cipher.getBlockSize());
random.nextBytes(iv);
IvParameterSpec ivParams = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivParams);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));

更多内容请参看参考文档:

http://android-developers.blogspot.co.uk/2013/02/using-cryptography-to-store-credentials.html
http://crypto.stackexchange.com/questions/11260/why-is-sharing-the-seed-and-using-securerandom-deterministically-so-bad
http://stackoverflow.com/questions/13433529/android-4-2-broke-my-encrypt-decrypt-code-and-the-provided-solutions-dont-work
http://nelenkov.blogspot.com/2012/04/using-password-based-encryption-on.html
http://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html
http://emboss.github.io/blog/2013/08/21/openssl-prng-is-not-really-fork-safe/
https://bitcoin.org/en/alert/2013-08-11-android





  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值