/*
 * [TestCipherRSA.java]
 *
 * Summary: Demonstrate use of CipherOutputStream and CipherInputStream to encipher and decipher a message.
 *
 * Copyright: (c) 2008-2017 Roedy Green, Canadian Mind Products, http://mindprod.com
 *
 * Licence: This software may be copied and used freely for any purpose but military.
 *          http://mindprod.com/contact/nonmil.html
 *
 * Requires: JDK 1.8+
 *
 * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/
 *
 * Version History:
 *  1.0 2008-07-07
 */
package com.mindprod.example;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;

import static java.lang.System.*;

/**
 * Demonstrate use of CipherOutputStream and CipherInputStream to encipher and decipher a message.
 * <p/>
 * This particular version uses RSA/ECB/PKCS1Padding
 * but it fairly easy to convert it to use other algorithms.
 * RSA requires a digital certificate in your .keystore.
 * Does not require a shared secret key.
 * Works with a public/private key pair stored in certificates.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 1.0 2008-07-07
 * @since 2008-07-07
 */
public class TestCipherRSA
    {
    /**
     * configure with encryption algorithm to use. Changes to algorithm may require additional ivParms.
     */
    private static final String ALGORITHM = "RSA";

    /**
     * configure with block mode to use. We have to use insecure ECB since Sun support nothing else.
     */
    private static final String BLOCK_MODE = "ECB";

    /**
     * where to find .keystore.  There should be a way to automatically find the default .keystore.
     * perhaps via a System property.
     */
    private static final String KEYSTORE_FILENAME = "C:/users/roedy/.keystore";

    /**
     * configure with padding method to use
     */
    private static final String PADDING = "PKCS1Padding";

    /**
     * alias of the RSA certificate in .keystore in JKS format.  Contains private key of recipient.
     */
    private static final String RECEIVERS_PRIVATE_CERTIFICATE_ALIAS = "mindprodcert2010rsa";

    /**
     * receiver's public key in standalone certificate, in x.509 format
     */
    private static final String RECEIVERS_PUBLIC_CERTIFICATE = "E:/mindprod/contact/mindprodcert2010rsa.cer";

    /**
     * the encoding to use when converting bytes <--> String
     */
    private static final Charset CHARSET = Charset.forName( "UTF-8" );

    /**
     * configure .keystore password
     */
    private static final char[] PASSWORD = "sesame".toCharArray();

    /**
     * get the receiver's private cert from .keystore.
     *
     * @return containing private key
     * @throws java.security.KeyStoreException         if .keystore corrupt.
     * @throws java.io.IOException                     if trouble loading cert.
     * @throws java.security.NoSuchAlgorithmException  if no JCE support.
     * @throws java.security.cert.CertificateException if certificate corrupt.
     * @throws java.security.UnrecoverableKeyException if wrong password.
     */
    private static PrivateKey getPrivateKey()
            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
            UnrecoverableKeyException
        {
        final KeyStore keystore = KeyStore.getInstance( "JKS" );
        keystore.load( new FileInputStream( KEYSTORE_FILENAME ), null );
        return ( PrivateKey ) keystore.getKey( RECEIVERS_PRIVATE_CERTIFICATE_ALIAS, PASSWORD );
        }

    /**
     * get the receiver's public key from standalone cert file.
     *
     * @return public key
     * @throws java.io.FileNotFoundException           if missing certificate file.
     * @throws java.security.cert.CertificateException if certificate corrupt.
     */
    private static PublicKey getPublicKey()
            throws FileNotFoundException, CertificateException
        {
        // instead of generating a secret key, we use a certificate, containing the receiver's public key
        final FileInputStream fis = new FileInputStream( RECEIVERS_PUBLIC_CERTIFICATE );
        final CertificateFactory cf = CertificateFactory.getInstance( "X.509" );
        // will get an X509Certificate, but no need to cast
        return cf.generateCertificate( fis ).getPublicKey();
        }

    /**
     * read an enciphered file and retrieve its plaintext message.
     *
     * @param cipher     method used to encrypt the file
     * @param privateKey private key of recipient.
     * @param file       file where the message was written.
     *
     * @return the reconstituted decrypted message.
     * @throws java.security.InvalidKeyException if something wrong with the key.
     * @throws java.io.IOException               if problems reading the file.
     */
    @SuppressWarnings( { "JavaDoc" } )
    private static String readCiphered( Cipher cipher, PrivateKey privateKey, File file )
            throws InvalidKeyException, IOException
        {
        cipher.init( Cipher.DECRYPT_MODE, privateKey );
        final CipherInputStream cin = new CipherInputStream( new FileInputStream( file ), cipher );
        // read big endian short length, msb then lsb
        final int messageLengthInBytes = ( cin.read() << 8 ) | cin.read();
        out.println( file.length() + " enciphered bytes in file" );
        out.println( messageLengthInBytes + " reconstituted bytes" );
        final byte[] reconstitutedBytes = new byte[ messageLengthInBytes ];
        // we can't trust CipherInputStream to give us all the data in one shot
        int bytesReadSoFar = 0;
        int bytesRemaining = messageLengthInBytes;
        while ( bytesRemaining > 0 )
            {
            final int bytesThisChunk = cin.read( reconstitutedBytes, bytesReadSoFar, bytesRemaining );
            if ( bytesThisChunk == 0 )
                {
                throw new IOException( file.toString() + " corrupted." );
                }
            bytesReadSoFar += bytesThisChunk;
            bytesRemaining -= bytesThisChunk;
            }
        cin.close();
        return new String( reconstitutedBytes, CHARSET );
        }

    /**
     * write a plaintext message to a file enciphered.
     *
     * @param cipher    the method to use to encrypt the file.
     * @param publicKey public key of recipient.
     * @param file      the file to write the encrypted message to.
     * @param plainText the plaintext of the message to write.
     *
     * @throws java.security.InvalidKeyException if something is wrong with the key
     * @throws java.io.IOException               if there are problems writing the file.
     *                                           .
     */
    private static void writeCiphered( Cipher cipher, PublicKey publicKey, File file, String plainText )
            throws InvalidKeyException, IOException
        {
        cipher.init( Cipher.ENCRYPT_MODE, publicKey );
        final CipherOutputStream cout = new CipherOutputStream( new FileOutputStream( file ), cipher );
        final byte[] plainTextBytes = plainText.getBytes( CHARSET );
        out.println( plainTextBytes.length + " plaintext bytes written" );
        // prepend with big-endian short message length, will be encrypted too.
        cout.write( plainTextBytes.length >>> 8 );// msb
        cout.write( plainTextBytes.length & 0xff );// lsb
        cout.write( plainTextBytes );
        cout.close();
        }

    /**
     * Demonstrate use of CipherOutputStream and CipherInputStream to encipher and decipher a message.
     *
     * @param args not used
     *
     * @throws java.security.NoSuchAlgorithmException           if RSA is not supported
     * @throws javax.crypto.NoSuchPaddingException              if PKCS5 padding is not supported.
     * @throws java.security.InvalidKeyException                if there is something wrong with the key.
     * @throws java.io.IOException                              if there are problems reading or writing the file.
     * @throws java.security.InvalidAlgorithmParameterException if programming error
     * @throws java.security.NoSuchProviderException            if no JCE support
     * @throws java.security.KeyStoreException                  if .keystore corrupt.
     * @throws java.security.cert.CertificateException          if either certificate corrupt.
     * @throws java.security.UnrecoverableKeyException          if wrong password
     */
    public static void main( String[] args ) throws
            CertificateException,
            InvalidAlgorithmParameterException,
            InvalidKeyException,
            IOException,
            KeyStoreException,
            NoSuchAlgorithmException,
            NoSuchPaddingException,
            NoSuchProviderException,
            UnrecoverableKeyException
        {
        // The secret message we want to send to our secret agent in London.
        final String plainText = "W. to visit Abu Ghraib for a hands on, wink wink, tomorrow at 19:05.";
        // use default Sun provider
        final Cipher cipher = Cipher.getInstance( ALGORITHM + "/" + BLOCK_MODE + "/" + PADDING );
        // write out the ciphered message
        writeCiphered( cipher, getPublicKey(), new File( "transport.bin" ), plainText );
        // now try reading message back in deciphering it.
        final String reconstitutedText = readCiphered( cipher, getPrivateKey(), new File( "transport.bin" ) );
        out.println( "original: " + plainText );
        out.println( "reconstituted: " + reconstitutedText );
        // output is:
        // 68 plaintext bytes written
        // 128 enciphered bytes in file
        // 68 reconstituted bytes
        // original: W. to visit Abu Ghraib for a hands on, wink wink, tomorrow at 19:05.
        // reconstituted: W. to visit Abu Ghraib for a hands on, wink wink, tomorrow at 19:05.
        }
    }