/*
 * 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.orhanobut.hawk;

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;

/**
 * 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.
 */
final 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.NO_WRAP;
  private static final AtomicBoolean PRNG_FIXED = new AtomicBoolean(false);
  private static final String HMAC_ALGORITHM = "HmacSHA256";
  private static final int HMAC_KEY_LENGTH_BITS = 256;

  private AesCbcWithIntegrity() {
    //no instance
  }

  /**
   * 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 java.security.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 java.security.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 java.security.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 java.security.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, "Crypto");
    byte[] b = new byte[length];
    random.nextBytes(b);
    return b;
  }

  /**
   * Copy the elements from the start to the end
   *
   * @param from  the source
   * @param start the start index to copy
   * @param end   the end index to finish
   * @return the new buffer
   */
  private static byte[] copyOfRange(byte[] from, int start, int end) {
    int length = end - start;
    byte[] result = new byte[length];
    System.arraycopy(from, start, result, 0, length);
    return result;
  }

    /*
     * -----------------------------------------------------------------
     * 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 java.security.GeneralSecurityException if AES is not implemented on this system
   * @throws java.io.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 java.security.GeneralSecurityException if AES is not implemented on this system
   * @throws java.io.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 java.security.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 (!PRNG_FIXED.get()) {
      synchronized (PrngFixes.class) {
        if (!PRNG_FIXED.get()) {
          PrngFixes.apply();
          PRNG_FIXED.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 java.security.GeneralSecurityException if AES is not implemented on this system
   * @throws java.io.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 java.security.GeneralSecurityException if AES is not implemented on this system
   * @throws java.io.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 java.security.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 java.security.NoSuchAlgorithmException
   * @throws java.security.InvalidKeyException
   */
  public static byte[] generateMac(byte[] byteCipherText, SecretKey integrityKey)
      throws NoSuchAlgorithmException, InvalidKeyException {
    //Now compute the mac for later integrity checking
    Mac sha256HMAC = Mac.getInstance(HMAC_ALGORITHM);
    sha256HMAC.init(integrityKey);
    return sha256HMAC.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 = copyOfRange(c, 0, c.length);
      iv = copyOfRange(i, 0, i.length);
      mac = copyOfRange(h, 0, 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
   * <p/>
   * 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.
   * <p/>
   * 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.
   * <p/>
   * Fixes for the output of the default PRNG having low entropy.
   * <p/>
   * 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 java.security.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 LOCK = 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 (LOCK) {
            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 (LOCK) {
            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 (LOCK) {
          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 (LOCK) {
          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");
      }
    }
  }
}
