Android中SecureRandom类的安全性浅析

背景

2013年比特币开发商在一篇博客中透露,由于Android系统存在一处关键漏洞,该平台上的比特币电子钱包很容易失窃。比特币开发商称,该漏洞影响到Android平台上的每一个比特币电子钱包应用程序,包括流行的比特币钱包(Bitcoin Wallet)、blockchain.info钱包(blockchain.info wallet)、BitcoinSpinner钱包(BitcoinSpinner Wallet)和Mycelium钱包(Mycelium Wallet)等。

该漏洞存在于Android系统随机生成数字串安全密钥的环节中。该漏洞的生成原因是对SecureRandom类的不正确使用方式导致的。翻看Android的官方文档会发现。对于SecureRandom类的构造函数SecureRandom(byte[] seed)和SecureRandom#setSeed方法有一段安全性提醒:

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.

By default, instances of this class will generate an initial seed using an internal entropy source, such as /dev/urandom. This seed is unpredictable and appropriate for secure use.

Using the seeded constructor or calling setSeed(byte[]) 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 setSeed at all.

遗憾的是Android官网并未对此做过多的解释。setSeed方法为什么会引起安全风险?应该怎样使用SecureRandom类?这些问题都要从SecureRandom的原理说起。

SecureRandom如何生成随机数?

SecureRandom随机性是通过它的seed来保证的。如果输入相同的seed会导致生成重复的随机数。SecureRandom内部维护一个internal random state,它生成随机数的方式具有确定性。(如果输入相同的seed那么生成的随机数也相同)具体过程如下图:
在生成一个随机数时internal random state会从seed源中取出一个seed。通过内部运算生成随机数。所以SecureRandom依靠输入随机的选取seed来保证自己能够生成出不相同的随机数。

SecureRandom的安全隐患

在SecureRandom生成随机数时,如果我们不调用setSeed方法,SecureRandom会从系统的中找到一个默认随机源。每次生成随机数时都会从这个随机源中取seed。在linux和Android中这个随机源位于/dev/urandom文件。 如果我们在终端可以运行cat /dev/urandom命令,会观察到随机值会不断的打印到屏幕上。

setSeed方法为何有安全风险?

在Android 4.2以下,SecureRandom是基于老版的Bouncy Castle实现的。如果生成SecureRandom对象后马上调用setSeed方法。SecureRandom会用用户设置的seed代替默认的随机源。使得每次生成随机数时都是会使用相同的seed作为输入。从而导致生成的随机数是相同的。下面是一段存在安全风险的使用方法:
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每次都输出固定的随机值。通过这个固定值作为秘钥加密本地文本。其使用方式和流程如下:

Android中SecureRandom类的安全性浅析_第1张图片

代码例子:
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





你可能感兴趣的:(Android中SecureRandom类的安全性浅析)