ch.lamacrypt.internal.crypto.GCMCipher.java Source code

Java tutorial

Introduction

Here is the source code for ch.lamacrypt.internal.crypto.GCMCipher.java

Source

/* 
 * Copyright (c) 2016, LamaCrypt
 * All rights reserved.
 *
 * The LamaCrypt client software and its source code are available
 * under the LamaCrypt Software License: 
 * https://github.com/LamaCrypt/desktop-client/blob/master/LICENSE.md
 */
package ch.lamacrypt.internal.crypto;

import ch.lamacrypt.internal.Settings;
import ch.lamacrypt.internal.network.NTP;
import ch.lamacrypt.visual.DefaultFrame;
import ch.lamacrypt.visual.DownloadFrame;
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.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.bouncycastle.crypto.generators.SCrypt;
import org.bouncycastle.util.encoders.Hex;

/**
 * General purpose class used for encryption and decryption of files, using
 * AES-256 in the GCM mode of operation
 * <p>
 * Contains methods for the following encryption schemes:
 * <ul>
 * <li>v00</li>
 * </ul>
 *
 * @author LamaGuy
 */
public final class GCMCipher {

    private static final String CIPHER = "AES/GCM/NoPadding", CRYPTO_PROVIDER = "BC";
    private static final int CIPHER_KEY_BITS = 256, GCM_NONCE_BYTES = 12, GCM_TAG_BITS = 128, Sk_BYTES = 64,
            Sn_BYTES = 6, R_BYTES = 64, BUFFER_SIZE = 8192, KDF_r = 8, KDF_p = 1, VS1 = Sk_BYTES,
            S1N1 = VS1 + GCM_NONCE_BYTES, N1K1N = S1N1 + 1, K1NR = N1K1N + R_BYTES + GCM_TAG_BITS / 8,
            RS2 = K1NR + Sk_BYTES, S2N2 = RS2 + GCM_NONCE_BYTES, N2K2N = S2N2 + 1, AEAD_EXCEPTION = 4;

    private static int K1_KDF_N = 21, K2_KDF_N = 19, intFileCnt = 0;

    private final byte[] buf = new byte[BUFFER_SIZE];
    private final DataOutputStream dos;
    private final DataInputStream dis;
    private final Cipher cipher;

    private int r;

    /**
     * Sets the new CPU/RAM cost parameter for scrypt when deriving K1, read as
     * a power of two
     * <p>
     * Default value: 21
     *
     * @param N new CPU/RAM cost for scrypt, as a power of two
     */
    protected static void setK1N(int N) {
        GCMCipher.K1_KDF_N = N;
    }

    /**
     * Returns the current scrypt CPU/RAM work factor (N)
     *
     * @return
     */
    protected static int getK1N() {
        return GCMCipher.K1_KDF_N;
    }

    /**
     * Sets the new CPU/RAM cost parameter for scrypt when deriving K2, read as
     * a power of two
     * <p>
     * Default value: 19
     *
     * @param N new CPU/RAM cost for scrypt, as a power of two
     */
    protected static void setK2N(int N) {
        GCMCipher.K2_KDF_N = N;
    }

    /**
     * Instantiates a Cipher object using AES-256 in GCM mode of operation,
     * allowing subsequent use for file encryption and decryption
     *
     * @param dis
     * @param dos
     * @throws java.lang.ClassNotFoundException if the class
     * javax.crypto.JceSecurity does not exist
     * @throws java.lang.NoSuchFieldException if the field isRestricted does not
     * exist in the class javax.crypto.JceSecurity
     * @throws java.lang.IllegalAccessException if the isRestricted field is
     * enforcing Java language access control and it is either inaccessible or
     * final
     * @throws java.security.NoSuchAlgorithmException
     * @throws java.security.NoSuchProviderException
     * @throws javax.crypto.NoSuchPaddingException
     *
     */
    protected GCMCipher(DataOutputStream dos, DataInputStream dis)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchAlgorithmException,
            NoSuchProviderException, NoSuchPaddingException {
        // instantiating AES-256 w/ GCM from Bouncy Castle
        this.cipher = Cipher.getInstance(CIPHER, CRYPTO_PROVIDER);

        // settings the I/O streams
        this.dos = dos;
        this.dis = dis;
    }

    /**
     * Encrypts a given file with AES-256 in GCM mode of operation
     * <p>
     * Reads data from the InputStream and writes the encrypted data to the
     * OutputStream
     *
     * @param inputFile
     * @return
     * @throws java.io.IOException
     * @throws java.security.InvalidKeyException
     * @throws java.security.InvalidAlgorithmParameterException
     * @throws javax.crypto.BadPaddingException
     * @throws javax.crypto.IllegalBlockSizeException
     */
    protected int encrypt_V00(File inputFile) throws IOException, InvalidKeyException,
            InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException {
        DefaultFrame.setFileQueueItemStatus("Generating header");

        // getting the encryption password
        char[] pass = DefaultCipher.getEncryptionPassword();
        // generating Sx, Nx, R, Kx
        final byte[] Sk1 = GPCrypto.randomGen(Sk_BYTES), Sk2 = GPCrypto.randomGen(Sk_BYTES),
                Sn1 = GPCrypto.randomGen(Sn_BYTES), Sn2 = GPCrypto.randomGen(Sn_BYTES),
                byteFileCnt = GPCrypto.intToByteArray(intFileCnt),
                epoch = DatatypeConverter.parseHexBinary(Long.toHexString(NTP.getTime() / 1000)),
                N1 = new byte[] { Sn1[0], Sn1[1], Sn1[2], Sn1[3], Sn1[4], Sn1[5], byteFileCnt[0], byteFileCnt[1],
                        epoch[0], epoch[1], epoch[2], epoch[3] },
                N2 = new byte[] { Sn2[0], Sn2[1], Sn2[2], Sn2[3], Sn1[4], Sn1[5], byteFileCnt[0],
                        (byte) (byteFileCnt[1] + 0x01), epoch[0], epoch[1], epoch[2], (byte) (epoch[3] + 0x01) },
                R = GPCrypto.randomGen(R_BYTES);
        final SecretKey K1 = new SecretKeySpec(SCrypt.generate(GPCrypto.charToByte(pass), Sk1,
                (int) Math.pow(2, K1_KDF_N), KDF_r, KDF_p, CIPHER_KEY_BITS / 8), 0, CIPHER_KEY_BITS / 8, "AES"),
                K2 = new SecretKeySpec(
                        SCrypt.generate(R, Sk2, (int) Math.pow(2, K2_KDF_N), KDF_r, KDF_p, CIPHER_KEY_BITS / 8), 0,
                        CIPHER_KEY_BITS / 8, "AES");

        // writing header
        this.cipher.init(Cipher.ENCRYPT_MODE, K1, new GCMParameterSpec(GCM_TAG_BITS, N1, 0, GCM_NONCE_BYTES));
        dos.write((byte) 0x00);
        dos.write(Sk1);
        dos.write(N1);
        dos.write(DatatypeConverter.parseHexBinary(Integer.toHexString(K1_KDF_N)));
        dos.write(cipher.doFinal(R));
        dos.write(Sk2);
        dos.write(N2);
        dos.write(DatatypeConverter.parseHexBinary(Integer.toHexString(K2_KDF_N)));

        // encrypting file
        DefaultFrame.setFileQueueItemStatus("Uploading (0%)");
        this.cipher.init(Cipher.ENCRYPT_MODE, K2, new GCMParameterSpec(GCM_TAG_BITS, N2, 0, GCM_NONCE_BYTES));
        long fileSize = inputFile.length(), percentage = fileSize / 100L, bytesRead = 0L, iterCnt;

        // defining input stream and
        InputStream input = new FileInputStream(inputFile);

        if (fileSize < BUFFER_SIZE) {
            r = input.read(buf, 0, (int) fileSize);
            dos.write(this.cipher.doFinal(buf, 0, r));
        } else if (fileSize % BUFFER_SIZE == 0) {
            iterCnt = (fileSize / BUFFER_SIZE) - 1;

            for (long i = 0; i < iterCnt; i++) {
                input.read(buf);
                dos.write(this.cipher.update(buf));

                bytesRead += BUFFER_SIZE;
                if (bytesRead % percentage > 1) {
                    updateStatus("Uploading (" + bytesRead / percentage + "%)");
                }
            }

            input.read(buf);
            dos.write(this.cipher.doFinal(buf));
        } else {
            iterCnt = fileSize / BUFFER_SIZE;

            for (long i = 0; i < iterCnt; i++) {
                input.read(buf);
                dos.write(this.cipher.update(buf));

                bytesRead += BUFFER_SIZE;
                if (bytesRead % percentage > 1) {
                    updateStatus("Uploading (" + bytesRead / percentage + "%)");
                }
            }

            r = input.read(buf, 0, (int) (fileSize % BUFFER_SIZE));
            dos.write(this.cipher.doFinal(buf, 0, r));
        }

        updateStatus("Finalizing");

        // cleaning up
        GPCrypto.eraseByteArrays(Sk1, Sk2, epoch, N1, N2, R);
        GPCrypto.eraseKeys(K1, K2);
        GPCrypto.sanitize(pass);
        input.close();
        updateFileCnt();

        return dis.readInt();
    }

    /**
     * Decrypts a given file with AES-256 in GCM mode of operation
     *
     * @param outputFile
     * @return
     * @throws java.io.IOException
     * @throws java.security.InvalidKeyException
     * @throws java.security.InvalidAlgorithmParameterException
     * @throws javax.crypto.BadPaddingException
     * @throws javax.crypto.IllegalBlockSizeException
     */
    protected int decrypt_V00(File outputFile) throws IOException, InvalidKeyException,
            InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException {
        updateStatus("Reading header");

        // getting file size and calculating iterCnt
        long fileSize = dis.readLong(), dlSize = fileSize + GCM_TAG_BITS / 8, iterCnt = dlSize / BUFFER_SIZE,
                percentage = dlSize / 100L, bytesRead = 0L;

        // defining output stream
        OutputStream output = new FileOutputStream(outputFile);

        // getting the encryption password
        char[] pass = DefaultCipher.getEncryptionPassword();

        // reading header
        byte[] header = new byte[234];
        dis.read(header, 0, 234);

        // reading Sx, Nx, scrypt factors and generating K1
        final byte[] S1 = Arrays.copyOfRange(header, 0, VS1), N1 = Arrays.copyOfRange(header, VS1, S1N1),
                K1_N = Arrays.copyOfRange(header, S1N1, N1K1N), S2 = Arrays.copyOfRange(header, K1NR, RS2),
                N2 = Arrays.copyOfRange(header, RS2, S2N2), K2_N = Arrays.copyOfRange(header, S2N2, N2K2N);
        final int K1_N_bak = (int) Math.pow(2, Integer.valueOf(Hex.toHexString(K1_N), 16)),
                K2_N_bak = (int) Math.pow(2, Integer.valueOf(Hex.toHexString(K2_N), 16));
        final SecretKey K1 = new SecretKeySpec(
                SCrypt.generate(GPCrypto.charToByte(pass), S1, K1_N_bak, KDF_r, KDF_p, CIPHER_KEY_BITS / 8), 0,
                CIPHER_KEY_BITS / 8, "AES");

        // reading E(K1, N1, R) 
        this.cipher.init(Cipher.DECRYPT_MODE, K1, new GCMParameterSpec(GCM_TAG_BITS, N1, 0, GCM_NONCE_BYTES));
        boolean failFree = true, AEADBadTag = false;
        byte[] R = new byte[64];

        try {
            R = cipher.doFinal(Arrays.copyOfRange(header, N1K1N, K1NR));
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            failFree = false;
            updateStatus("Error");
            if (e instanceof javax.crypto.AEADBadTagException) {
                AEADBadTag = true;
            }
        }

        dos.writeBoolean(failFree);
        // decrypting file if no exception has been caught
        if (failFree) {
            // generating K2
            final SecretKey K2 = new SecretKeySpec(
                    SCrypt.generate(R, S2, K2_N_bak, KDF_r, KDF_p, CIPHER_KEY_BITS / 8), 0, CIPHER_KEY_BITS / 8,
                    "AES");

            updateStatus("Downloading (0%)");
            this.cipher.init(Cipher.DECRYPT_MODE, K2, new GCMParameterSpec(GCM_TAG_BITS, N2, 0, GCM_NONCE_BYTES));

            if (dlSize > BUFFER_SIZE) {
                if (dlSize % BUFFER_SIZE == 0) {
                    iterCnt--;
                }

                // reading full blocks
                for (long i = 0; i < iterCnt; i++) {
                    dis.readFully(buf);
                    output.write(this.cipher.update(buf));

                    bytesRead += BUFFER_SIZE;
                    if (bytesRead % percentage > 1) {
                        updateStatus("Downloading (" + bytesRead / percentage + "%)");
                    }
                }

                // reading last chunk
                if (dlSize % BUFFER_SIZE != 0) {
                    r = dis.read(buf, 0, (int) dlSize % BUFFER_SIZE);
                    output.write(this.cipher.doFinal(buf, 0, r));
                } else {
                    dis.readFully(buf);
                    output.write(this.cipher.doFinal(buf));
                }
            } else {
                r = dis.read(buf, 0, (int) dlSize);
                output.write(this.cipher.doFinal(buf, 0, r));
            }

            // erasing cryptographic parameters and closing streams
            updateStatus("Finalizing");
            GPCrypto.eraseByteArrays(header, S1, S2, N1, N2, R);
            GPCrypto.eraseKeys(K1, K2);
            GPCrypto.sanitize(pass);
            output.close();

            return dis.readInt();
        } else {
            // erasing cryptographic parameters and closing streams
            GPCrypto.eraseByteArrays(header, S1, S2, N1, N2, R);
            GPCrypto.eraseKeys(K1);
            GPCrypto.sanitize(pass);
            output.close();

            if (AEADBadTag) {
                return AEAD_EXCEPTION;
            } else {
                return 5;
            }
        }
    }

    protected int decryptshare_V00(String key, File outputFile) throws IOException, InvalidKeyException,
            InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
        // defining output stream
        FileOutputStream output = new FileOutputStream(outputFile);

        // getting file size and calculating iterCnt
        long fileSize = dis.readLong(), dlSize = fileSize + GCM_TAG_BITS / 8,
                iterCnt = (dlSize - BUFFER_SIZE) / BUFFER_SIZE, percentage = (dlSize - BUFFER_SIZE) / 100L,
                bytesRead = 0L;

        // recovering K2 and N2
        updateStatus("Reading header");
        final byte[] N2 = new byte[12];
        dis.read(N2, 0, 12);
        final SecretKey K2 = new SecretKeySpec(DatatypeConverter.parseHexBinary(key), 0, CIPHER_KEY_BITS / 8,
                "AES");

        boolean AEADBadTag = false, failFree = true, done = true;

        // reading first buffer
        this.cipher.init(Cipher.DECRYPT_MODE, K2, new GCMParameterSpec(GCM_TAG_BITS, N2, 0, GCM_NONCE_BYTES));
        updateStatus("Downloading (0%)");

        try {
            if (dlSize > BUFFER_SIZE) {
                dis.readFully(buf);
                output.write(this.cipher.update(buf));
                done = false;
            } else {
                r = dis.read(buf, 0, (int) dlSize);
                output.write(this.cipher.doFinal(buf, 0, r));
            }
        } catch (IOException | IllegalBlockSizeException | BadPaddingException e) {
            failFree = false;
            updateStatus("Error");
            if (e instanceof javax.crypto.AEADBadTagException) {
                AEADBadTag = true;
            }
        }

        dos.writeBoolean(failFree);

        // decrypting file if no exception has been caught
        if (failFree) {
            if (!done) {
                if (dlSize % BUFFER_SIZE == 0) {
                    iterCnt--;
                }

                // reading full blocks
                for (long i = 0; i < iterCnt; i++) {
                    dis.readFully(buf);
                    output.write(this.cipher.update(buf));

                    bytesRead += BUFFER_SIZE;
                    if (bytesRead % percentage > 1) {
                        updateStatus("Downloading (" + bytesRead / percentage + "%)");
                    }
                }

                // reading last chunk
                if (dlSize % BUFFER_SIZE != 0) {
                    r = dis.read(buf, 0, (int) dlSize % BUFFER_SIZE);
                    output.write(this.cipher.doFinal(buf, 0, r));
                } else {
                    dis.readFully(buf);
                    output.write(this.cipher.doFinal(buf));
                }

                // erasing cryptographic parameters and closing streams
                updateStatus("Finalizing");
                finish(N2, K2, output);

                return dis.readInt();
            } else {
                // erasing cryptographic parameters and closing streams
                updateStatus("Finalizing");
                finish(N2, K2, output);

                return 0;
            }
        } else {
            // erasing cryptographic parameters and closing streams
            finish(N2, K2, output);

            if (AEADBadTag) {
                return AEAD_EXCEPTION;
            } else {
                return 7;
            }
        }
    }

    protected static String getKey_V00(byte[] header)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
            InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
        // getting the encryption password
        char[] pass = DefaultCipher.getEncryptionPassword();

        // reading Sx, Nx, scrypt factors and generating K1
        final byte[] S1 = Arrays.copyOfRange(header, 0, VS1), N1 = Arrays.copyOfRange(header, VS1, S1N1),
                K1_N = Arrays.copyOfRange(header, S1N1, N1K1N), S2 = Arrays.copyOfRange(header, K1NR, RS2),
                N2 = Arrays.copyOfRange(header, RS2, S2N2), K2_N = Arrays.copyOfRange(header, S2N2, N2K2N);
        final int K1_N_bak = (int) Math.pow(2, Integer.valueOf(Hex.toHexString(K1_N), 16)),
                K2_N_bak = (int) Math.pow(2, Integer.valueOf(Hex.toHexString(K2_N), 16));
        final SecretKey K1 = new SecretKeySpec(
                SCrypt.generate(GPCrypto.charToByte(pass), S1, K1_N_bak, KDF_r, KDF_p, CIPHER_KEY_BITS / 8), 0,
                CIPHER_KEY_BITS / 8, "AES");

        // reading E(K1, N1, R)
        Cipher cipher = Cipher.getInstance(CIPHER);
        cipher.init(Cipher.DECRYPT_MODE, K1, new GCMParameterSpec(GCM_TAG_BITS, N1, 0, GCM_NONCE_BYTES));

        byte[] R = new byte[64];
        boolean failFree = true;

        try {
            R = cipher.doFinal(Arrays.copyOfRange(header, N1K1N, K1NR));
        } catch (IllegalBlockSizeException | BadPaddingException ex) {
            failFree = false;
        }

        if (failFree) {
            // generating K2
            final SecretKey K2 = new SecretKeySpec(
                    SCrypt.generate(R, S2, K2_N_bak, KDF_r, KDF_p, CIPHER_KEY_BITS / 8), 0, CIPHER_KEY_BITS / 8,
                    "AES");

            GPCrypto.eraseByteArrays(header, S1, S2, N1, N2, R);
            GPCrypto.eraseKeys(K1);
            GPCrypto.sanitize(pass);

            return Hex.toHexString(K2.getEncoded());
        } else {
            GPCrypto.eraseByteArrays(header, S1, S2, N1, N2, R);
            GPCrypto.eraseKeys(K1);
            GPCrypto.sanitize(pass);

            return "error";
        }
    }

    /**
     * Updates the status of the download/upload job on the DefaultFrame (if
     * logged) or the DownloadFrame (if unlogged)
     *
     * @param txt new status text
     */
    private void updateStatus(String txt) {
        if (Settings.getLogged()) {
            DefaultFrame.setFileQueueItemStatus(txt);
        } else {
            DownloadFrame.updateStatus(txt);
        }
    }

    /**
     * Cleans up in-memory cryptographic values and closes the FileOutputStream
     *
     * @param N2 nonce
     * @param K2 AES key
     * @param fos FileOutputStream
     * @throws IOException
     */
    private void finish(byte[] N2, SecretKey K2, FileOutputStream fos) throws IOException {
        GPCrypto.sanitize(N2, GPCrypto.SANITIZATION_COUNT);
        GPCrypto.eraseKeys(K2);
        fos.close();
    }

    /**
     * Increments the file counter by two if the new value is under 65535, else
     * sets it back to 0.
     */
    private void updateFileCnt() {
        if (intFileCnt + 2 < 65535) {
            intFileCnt += 2;
        } else {
            intFileCnt = 0;
        }
    }
}