package com.cheetah.cryptography;

/**
 * Created by fahadHAlotaibi on 12/31/16.
 */

/*
 * 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.
 */

import android.os.Build;
import android.os.Process;
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.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.Security;

/**
 * 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 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());
    //     }
    // }

    // /**
    //  * 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");
    //     }
    // }

    // /**
    //  * {@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;
            }
        }
    }
}
