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.
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());
我们可以通过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;
}
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) {
...
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);
}
}
所以在调用setSeed方式之前一定要先调用nextBytes方法。保证seed随机源不会被替代。
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);
}
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"));