/*
* Copyright (c) 2014 Tozny LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Created by Isaac Potoczny-Jones on 11/12/14.
*/
package com.tozny.crypto.android;
import android.os.Build;
import android.os.Process;
import android.util.Base64;
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.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.Security;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import static java.util.Arrays.copyOfRange;
/**
* Simple library for the "right" defaults for AES key generation, encryption,
* and decryption using 128-bit AES, CBC, PKCS5 padding, and a random 16-byte IV
* with SHA1PRNG. Integrity with HmacSHA256.
*/
public class AesCbcWithIntegrity {
private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final String CIPHER = "AES";
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
private static final int AES_KEY_LENGTH_BITS = 128;
private static final int IV_LENGTH_BYTES = 16;
private static final int PBE_ITERATION_COUNT = 10000;
private static final int PBE_SALT_LENGTH_BITS = AES_KEY_LENGTH_BITS; // same size as key output
private static final String PBE_ALGORITHM = "PBKDF2WithHmacSHA1";
private static final int BASE64_FLAGS = Base64.DEFAULT | Base64.NO_WRAP;
private static final AtomicBoolean prngFixed = new AtomicBoolean(false);
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final int HMAC_KEY_LENGTH_BITS = 256;
/**
* Converts the given AES/HMAC keys into a base64 encoded string suitable for
* storage. Sister function of keys.
*
* @param keys The combined aes and hmac keys
* @return a base 64 encoded AES string & hmac key as base64(aesKey) : base64(hmacKey)
*/
public static String keyString(SecretKeys keys) {
return keys.toString();
}
/**
* An aes key derived from a base64 encoded key. This does not generate the
* key. It's not random or a PBE key.
*
* @param keysStr a base64 encoded AES key / hmac key as base64(aesKey) : base64(hmacKey).
* @return an AES & HMAC key set suitable for other functions.
*/
public static SecretKeys keys(String keysStr) throws InvalidKeyException {
String[] keysArr = keysStr.split(":");
if (keysArr.length != 2) {
throw new IllegalArgumentException("Cannot parse aesKey:hmacKey");
} else {
byte[] confidentialityKey = Base64.decode(keysArr[0], BASE64_FLAGS);
if (confidentialityKey.length != AES_KEY_LENGTH_BITS /8) {
throw new InvalidKeyException("Base64 decoded key is not " + AES_KEY_LENGTH_BITS + " bytes");
}
byte[] integrityKey = Base64.decode(keysArr[1], BASE64_FLAGS);
if (integrityKey.length != HMAC_KEY_LENGTH_BITS /8) {
throw new InvalidKeyException("Base64 decoded key is not " + HMAC_KEY_LENGTH_BITS + " bytes");
}
return new SecretKeys(
new SecretKeySpec(confidentialityKey, 0, confidentialityKey.length, CIPHER),
new SecretKeySpec(integrityKey, HMAC_ALGORITHM));
}
}
/**
* A function that generates random AES & HMAC keys and prints out exceptions but
* doesn't throw them since none should be encountered. If they are
* encountered, the return value is null.
*
* @return The AES & HMAC keys.
* @throws GeneralSecurityException if AES is not implemented on this system,
* or a suitable RNG is not available
*/
public static SecretKeys generateKey() throws GeneralSecurityException {
fixPrng();
KeyGenerator keyGen = KeyGenerator.getInstance(CIPHER);
// No need to provide a SecureRandom or set a seed since that will
// happen automatically.
keyGen.init(AES_KEY_LENGTH_BITS);
SecretKey confidentialityKey = keyGen.generateKey();
//Now make the HMAC key
byte[] integrityKeyBytes = randomBytes(HMAC_KEY_LENGTH_BITS / 8);//to get bytes
SecretKey integrityKey = new SecretKeySpec(integrityKeyBytes, HMAC_ALGORITHM);
return new SecretKeys(confidentialityKey, integrityKey);
}
/**
* A function that generates password-based AES & HMAC keys. It prints out exceptions but
* doesn't throw them since none should be encountered. If they are
* encountered, the return value is null.
*
* @param password The password to derive the keys from.
* @return The AES & HMAC keys.
* @throws GeneralSecurityException if AES is not implemented on this system,
* or a suitable RNG is not available
*/
public static SecretKeys generateKeyFromPassword(String password, byte[] salt) throws GeneralSecurityException {
fixPrng();
//Get enough random bytes for both the AES key and the HMAC key:
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt,
PBE_ITERATION_COUNT, AES_KEY_LENGTH_BITS + HMAC_KEY_LENGTH_BITS);
SecretKeyFactory keyFactory = SecretKeyFactory
.getInstance(PBE_ALGORITHM);
byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
// Split the random bytes into two parts:
byte[] confidentialityKeyBytes = copyOfRange(keyBytes, 0, AES_KEY_LENGTH_BITS /8);
byte[] integrityKeyBytes = copyOfRange(keyBytes, AES_KEY_LENGTH_BITS /8, AES_KEY_LENGTH_BITS /8 + HMAC_KEY_LENGTH_BITS /8);
//Generate the AES key
SecretKey confidentialityKey = new SecretKeySpec(confidentialityKeyBytes, CIPHER);
//Generate the HMAC key
SecretKey integrityKey = new SecretKeySpec(integrityKeyBytes, HMAC_ALGORITHM);
return new SecretKeys(confidentialityKey, integrityKey);
}
/**
* A function that generates password-based AES & HMAC keys. See generateKeyFromPassword.
* @param password The password to derive the AES/HMAC keys from
* @param salt A string version of the salt; base64 encoded.
* @return The AES & HMAC keys.
* @throws GeneralSecurityException
*/
public static SecretKeys generateKeyFromPassword(String password, String salt) throws GeneralSecurityException {
return generateKeyFromPassword(password, Base64.decode(salt, BASE64_FLAGS));
}
/**
* Generates a random salt.
* @return The random salt suitable for generateKeyFromPassword.
*/
public static byte[] generateSalt() throws GeneralSecurityException {
return randomBytes(PBE_SALT_LENGTH_BITS);
}
/**
* Converts the given salt into a base64 encoded string suitable for
* storage.
*
* @param salt
* @return a base 64 encoded salt string suitable to pass into generateKeyFromPassword.
*/
public static String saltString(byte[] salt) {
return Base64.encodeToString(salt, BASE64_FLAGS);
}
/**
* Creates a random Initialization Vector (IV) of IV_LENGTH_BYTES.
*
* @return The byte array of this IV
* @throws GeneralSecurityException if a suitable RNG is not available
*/
public static byte[] generateIv() throws GeneralSecurityException {
return randomBytes(IV_LENGTH_BYTES);
}
private static byte[] randomBytes(int length) throws GeneralSecurityException {
fixPrng();
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] b = new byte[length];
random.nextBytes(b);
return b;
}
/*
* -----------------------------------------------------------------
* Encryption
* -----------------------------------------------------------------
*/
/**
* Generates a random IV and encrypts this plain text with the given key. Then attaches
* a hashed MAC, which is contained in the CipherTextIvMac class.
*
* @param plaintext The text that will be encrypted, which
* will be serialized with UTF-8
* @param secretKeys The AES & HMAC keys with which to encrypt
* @return a tuple of the IV, ciphertext, mac
* @throws GeneralSecurityException if AES is not implemented on this system
* @throws UnsupportedEncodingException if UTF-8 is not supported in this system
*/
public static CipherTextIvMac encrypt(String plaintext, SecretKeys secretKeys)
throws UnsupportedEncodingException, GeneralSecurityException {
return encrypt(plaintext, secretKeys, "UTF-8");
}
/**
* Generates a random IV and encrypts this plain text with the given key. Then attaches
* a hashed MAC, which is contained in the CipherTextIvMac class.
*
* @param plaintext The bytes that will be encrypted
* @param secretKeys The AES & HMAC keys with which to encrypt
* @return a tuple of the IV, ciphertext, mac
* @throws GeneralSecurityException if AES is not implemented on this system
* @throws UnsupportedEncodingException if the specified encoding is invalid
*/
public static CipherTextIvMac encrypt(String plaintext, SecretKeys secretKeys, String encoding)
throws UnsupportedEncodingException, GeneralSecurityException {
return encrypt(plaintext.getBytes(encoding), secretKeys);
}
/**
* Generates a random IV and encrypts this plain text with the given key. Then attaches
* a hashed MAC, which is contained in the CipherTextIvMac class.
*
* @param plaintext The text that will be encrypted
* @param secretKeys The combined AES & HMAC keys with which to encrypt
* @return a tuple of the IV, ciphertext, mac
* @throws GeneralSecurityException if AES is not implemented on this system
*/
public static CipherTextIvMac encrypt(byte[] plaintext, SecretKeys secretKeys)
throws GeneralSecurityException {
byte[] iv = generateIv();
Cipher aesCipherForEncryption = Cipher.getInstance(CIPHER_TRANSFORMATION);
aesCipherForEncryption.init(Cipher.ENCRYPT_MODE, secretKeys.getConfidentialityKey(), new IvParameterSpec(iv));
/*
* Now we get back the IV that will actually be used. Some Android
* versions do funny stuff w/ the IV, so this is to work around bugs:
*/
iv = aesCipherForEncryption.getIV();
byte[] byteCipherText = aesCipherForEncryption.doFinal(plaintext);
byte[] ivCipherConcat = CipherTextIvMac.ivCipherConcat(iv, byteCipherText);
byte[] integrityMac = generateMac(ivCipherConcat, secretKeys.getIntegrityKey());
return new CipherTextIvMac(byteCipherText, iv, integrityMac);
}
/**
* Ensures that the PRNG is fixed. Should be used before generating any keys.
* Will only run once, and every subsequent call should return immediately.
*/
private static void fixPrng() {
if (!prngFixed.get()) {
synchronized (PrngFixes.class) {
if (!prngFixed.get()) {
PrngFixes.apply();
prngFixed.set(true);
}
}
}
}
/*
* -----------------------------------------------------------------
* Decryption
* -----------------------------------------------------------------
*/
/**
* AES CBC decrypt.
*
* @param civ The cipher text, IV, and mac
* @param secretKeys The AES & HMAC keys
* @param encoding The string encoding to use to decode the bytes after decryption
* @return A string derived from the decrypted bytes (not base64 encoded)
* @throws GeneralSecurityException if AES is not implemented on this system
* @throws UnsupportedEncodingException if the encoding is unsupported
*/
public static String decryptString(CipherTextIvMac civ, SecretKeys secretKeys, String encoding)
throws UnsupportedEncodingException, GeneralSecurityException {
return new String(decrypt(civ, secretKeys), encoding);
}
/**
* AES CBC decrypt.
*
* @param civ The cipher text, IV, and mac
* @param secretKeys The AES & HMAC keys
* @return A string derived from the decrypted bytes, which are interpreted
* as a UTF-8 String
* @throws GeneralSecurityException if AES is not implemented on this system
* @throws UnsupportedEncodingException if UTF-8 is not supported
*/
public static String decryptString(CipherTextIvMac civ, SecretKeys secretKeys)
throws UnsupportedEncodingException, GeneralSecurityException {
return decryptString(civ, secretKeys, "UTF-8");
}
/**
* AES CBC decrypt.
*
* @param civ the cipher text, iv, and mac
* @param secretKeys the AES & HMAC keys
* @return The raw decrypted bytes
* @throws GeneralSecurityException if MACs don't match or AES is not implemented
*/
public static byte[] decrypt(CipherTextIvMac civ, SecretKeys secretKeys)
throws GeneralSecurityException {
byte[] ivCipherConcat = CipherTextIvMac.ivCipherConcat(civ.getIv(), civ.getCipherText());
byte[] computedMac = generateMac(ivCipherConcat, secretKeys.getIntegrityKey());
if (constantTimeEq(computedMac, civ.getMac())) {
Cipher aesCipherForDecryption = Cipher.getInstance(CIPHER_TRANSFORMATION);
aesCipherForDecryption.init(Cipher.DECRYPT_MODE, secretKeys.getConfidentialityKey(),
new IvParameterSpec(civ.getIv()));
return aesCipherForDecryption.doFinal(civ.getCipherText());
} else {
throw new GeneralSecurityException("MAC stored in civ does not match computed MAC.");
}
}
/*
* -----------------------------------------------------------------
* Helper Code
* -----------------------------------------------------------------
*/
/**
* Generate the mac based on HMAC_ALGORITHM
* @param integrityKey The key used for hmac
* @param byteCipherText the cipher text
* @return A byte array of the HMAC for the given key & ciphertext
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
public static byte[] generateMac(byte[] byteCipherText, SecretKey integrityKey) throws NoSuchAlgorithmException, InvalidKeyException {
//Now compute the mac for later integrity checking
Mac sha256_HMAC = Mac.getInstance(HMAC_ALGORITHM);
sha256_HMAC.init(integrityKey);
return sha256_HMAC.doFinal(byteCipherText);
}
/**
* Holder class that has both the secret AES key for encryption (confidentiality)
* and the secret HMAC key for integrity.
*/
public static class SecretKeys {
private SecretKey confidentialityKey;
private SecretKey integrityKey;
/**
* Construct the secret keys container.
* @param confidentialityKeyIn The AES key
* @param integrityKeyIn the HMAC key
*/
public SecretKeys(SecretKey confidentialityKeyIn, SecretKey integrityKeyIn) {
setConfidentialityKey(confidentialityKeyIn);
setIntegrityKey(integrityKeyIn);
}
public SecretKey getConfidentialityKey() {
return confidentialityKey;
}
public void setConfidentialityKey(SecretKey confidentialityKey) {
this.confidentialityKey = confidentialityKey;
}
public SecretKey getIntegrityKey() {
return integrityKey;
}
public void setIntegrityKey(SecretKey integrityKey) {
this.integrityKey = integrityKey;
}
/**
* Encodes the two keys as a string
* @return base64(confidentialityKey):base64(integrityKey)
*/
@Override
public String toString () {
return Base64.encodeToString(getConfidentialityKey().getEncoded(), BASE64_FLAGS)
+ ":" + Base64.encodeToString(getIntegrityKey().getEncoded(), BASE64_FLAGS);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + confidentialityKey.hashCode();
result = prime * result + integrityKey.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SecretKeys other = (SecretKeys) obj;
if (!integrityKey.equals(other.integrityKey))
return false;
if (!confidentialityKey.equals(other.confidentialityKey))
return false;
return true;
}
}
/**
* Simple constant-time equality of two byte arrays. Used for security to avoid timing attacks.
* @param a
* @param b
* @return true iff the arrays are exactly equal.
*/
public static boolean constantTimeEq(byte[] a, byte[] b) {
if (a.length != b.length) {
return false;
}
int result = 0;
for (int i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result == 0;
}
/**
* Holder class that allows us to bundle ciphertext and IV together.
*/
public static class CipherTextIvMac {
private final byte[] cipherText;
private final byte[] iv;
private final byte[] mac;
public byte[] getCipherText() {
return cipherText;
}
public byte[] getIv() {
return iv;
}
public byte[] getMac() {
return mac;
}
/**
* Construct a new bundle of ciphertext and IV.
* @param c The ciphertext
* @param i The IV
* @param h The mac
*/
public CipherTextIvMac(byte[] c, byte[] i, byte[] h) {
cipherText = Arrays.copyOf(c, c.length);
iv = Arrays.copyOf(i, i.length);
mac = Arrays.copyOf(h, h.length);
}
/**
* Constructs a new bundle of ciphertext and IV from a string of the
* format <code>base64(iv):base64(ciphertext)</code>.
*
* @param base64IvAndCiphertext A string of the format
* <code>iv:ciphertext</code> The IV and ciphertext must each
* be base64-encoded.
*/
public CipherTextIvMac(String base64IvAndCiphertext) {
String[] civArray = base64IvAndCiphertext.split(":");
if (civArray.length != 3) {
throw new IllegalArgumentException("Cannot parse iv:ciphertext:mac");
} else {
iv = Base64.decode(civArray[0], BASE64_FLAGS);
mac = Base64.decode(civArray[1], BASE64_FLAGS);
cipherText = Base64.decode(civArray[2], BASE64_FLAGS);
}
}
/**
* Concatinate the IV to the cipherText using array copy.
* This is used e.g. before computing mac.
* @param iv The IV to prepend
* @param cipherText the cipherText to append
* @return iv:cipherText, a new byte array.
*/
public static byte[] ivCipherConcat(byte[] iv, byte[] cipherText) {
byte[] combined = new byte[iv.length + cipherText.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length);
return combined;
}
/**
* Encodes this ciphertext, IV, mac as a string.
*
* @return base64(iv) : base64(mac) : base64(ciphertext).
* The iv and mac go first because they're fixed length.
*/
@Override
public String toString() {
String ivString = Base64.encodeToString(iv, BASE64_FLAGS);
String cipherTextString = Base64.encodeToString(cipherText, BASE64_FLAGS);
String macString = Base64.encodeToString(mac, BASE64_FLAGS);
return String.format(ivString + ":" + macString + ":" + cipherTextString);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(cipherText);
result = prime * result + Arrays.hashCode(iv);
result = prime * result + Arrays.hashCode(mac);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CipherTextIvMac other = (CipherTextIvMac) obj;
if (!Arrays.equals(cipherText, other.cipherText))
return false;
if (!Arrays.equals(iv, other.iv))
return false;
if (!Arrays.equals(mac, other.mac))
return false;
return true;
}
}
/**
* Fixes for the RNG as per
* http://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html
*
* 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.
*
* Fixes for the output of the default PRNG having low entropy.
*
* The fixes need to be applied via {@link #apply()} before any use of Java
* Cryptography Architecture primitives. A good place to invoke them is in
* the application's {@code onCreate}.
*/
public static 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();
/** Hidden constructor to prevent instantiation. */
private PrngFixes() {
}
/**
* Applies all fixes.
*
* @throws SecurityException if a fix is needed but could not be
* applied.
*/
public static void apply() {
applyOpenSSLFix();
installLinuxPRNGSecureRandom();
}
/**
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if
* the fix is not needed.
*
* @throws SecurityException if the fix is needed but could not be
* applied.
*/
private static void applyOpenSSLFix() throws SecurityException {
if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
// No need to apply the fix
return;
}
try {
// Mix in the device- and invocation-specific seed.
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_seed", byte[].class).invoke(null, generateSeed());
// Mix output of Linux PRNG into OpenSSL's PRNG
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);
}
}
/**
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as
* the default. Does nothing if the implementation is already the
* default or if there is not need to install the implementation.
*
* @throws SecurityException if the fix is needed but could not be
* applied.
*/
private static void installLinuxPRNGSecureRandom() throws SecurityException {
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
// No need to apply the fix
return;
}
// Install a Linux PRNG-based SecureRandom implementation as the
// default, if not yet installed.
Provider[] secureRandomProviders = Security.getProviders("SecureRandom.SHA1PRNG");
if ((secureRandomProviders == null)
|| (secureRandomProviders.length < 1)
|| (!LinuxPRNGSecureRandomProvider.class.equals(secureRandomProviders[0]
.getClass()))) {
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
}
// Assert that new SecureRandom() and
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
// by the Linux PRNG-based SecureRandom implementation.
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());
}
}
/**
* {@code Provider} of {@code SecureRandom} engines which pass through
* all requests to the Linux PRNG.
*/
private static class LinuxPRNGSecureRandomProvider extends Provider {
public LinuxPRNGSecureRandomProvider() {
super("LinuxPRNG", 1.0, "A Linux-specific random number provider that uses"
+ " /dev/urandom");
// Although /dev/urandom is not a SHA-1 PRNG, some apps
// explicitly request a SHA1PRNG SecureRandom and we thus need
// to prevent them from getting the default implementation whose
// output may have low entropy.
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
}
}
/**
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG (
* {@code /dev/urandom}).
*/
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
/*
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a
* seed are passed through to the Linux PRNG (/dev/urandom).
* Instances of this class seed themselves by mixing in the current
* time, PID, UID, build fingerprint, and hardware serial number
* (where available) into Linux PRNG.
*
* Concurrency: Read requests to the underlying Linux PRNG are
* serialized (on sLock) to ensure that multiple threads do not get
* duplicated PRNG output.
*/
private static final File URANDOM_FILE = new File("/dev/urandom");
private static final Object sLock = new Object();
/**
* Input stream for reading from Linux PRNG or {@code null} if not
* yet opened.
*
* @GuardedBy("sLock")
*/
private static DataInputStream sUrandomIn;
/**
* Output stream for writing to Linux PRNG or {@code null} if not
* yet opened.
*
* @GuardedBy("sLock")
*/
private static OutputStream sUrandomOut;
/**
* Whether this engine instance has been seeded. This is needed
* because each instance needs to seed itself if the client does not
* explicitly seed it.
*/
private boolean mSeeded;
@Override
protected void engineSetSeed(byte[] bytes) {
try {
OutputStream out;
synchronized (sLock) {
out = getUrandomOutputStream();
}
out.write(bytes);
out.flush();
} catch (IOException e) {
// On a small fraction of devices /dev/urandom is not
// writable Log and ignore.
Log.w(PrngFixes.class.getSimpleName(), "Failed to mix seed into "
+ URANDOM_FILE);
} finally {
mSeeded = true;
}
}
@Override
protected void engineNextBytes(byte[] bytes) {
if (!mSeeded) {
// Mix in the device- and invocation-specific seed.
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) {
// NOTE: Consider inserting a BufferedInputStream
// between DataInputStream and FileInputStream if you need
// higher PRNG output performance and can live with future PRNG
// output being pulled into this process prematurely.
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;
}
}
}
/**
* Generates a device- and invocation-specific seed to be mixed into the
* 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);
}
}
/**
* Gets the hardware serial number of this device.
*
* @return serial number or {@code null} if not available.
*/
private static String getDeviceSerialNumber() {
// We're using the Reflection API because Build.SERIAL is only
// available since API Level 9 (Gingerbread, Android 2.3).
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");
}
}
}
}