com.doplgangr.secrecy.filesystem.encryption.AES_Crypter.java Source code

Java tutorial

Introduction

Here is the source code for com.doplgangr.secrecy.filesystem.encryption.AES_Crypter.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.doplgangr.secrecy.filesystem.encryption;

import com.doplgangr.secrecy.Config;
import com.doplgangr.secrecy.exceptions.SecrecyCipherStreamException;
import com.doplgangr.secrecy.exceptions.SecrecyFileException;
import com.doplgangr.secrecy.filesystem.files.EncryptedFile;
import com.doplgangr.secrecy.filesystem.files.SecrecyHeaders.FileHeader;
import com.doplgangr.secrecy.filesystem.files.SecrecyHeaders.VaultHeader;
import com.doplgangr.secrecy.filesystem.Storage;
import com.doplgangr.secrecy.Util;
import com.google.protobuf.ByteString;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;

abstract class AES_Crypter implements Crypter {

    private static final String SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA1";
    private static final String HEADER_ENCRYPTION_MODE = "AES/GCM/NoPadding";
    private static final String KEY_ALGORITHM = "AES";
    private static final String VAULT_HEADER_FILENAME = "/.vault";
    private static final String FILE_HEADER_PREFIX = "/.header_";
    private static final int NONCE_LENGTH_BYTE = 16;
    private static final int AES_KEY_SIZE_BIT = 256;
    private static final int SALT_SIZE_BYTE = 16;
    private static final int VAULT_HEADER_VERSION = 1;
    private static final int FILE_HEADER_VERSION = 1;

    private final SecureRandom secureRandom;
    private final String vaultPath;
    private final String encryptionMode;

    private SecretKey vaultFileEncryptionKey;
    private VaultHeader vaultHeader;

    AES_Crypter(String vaultPath, String passphrase, String encryptionMode) throws InvalidKeyException {
        secureRandom = new SecureRandom();
        this.vaultPath = vaultPath;
        this.encryptionMode = encryptionMode;

        File headerFile = new File(this.vaultPath + VAULT_HEADER_FILENAME);
        if (!headerFile.exists()) {
            try {
                KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
                keyGenerator.init(AES_KEY_SIZE_BIT);
                Key encryptionKey = keyGenerator.generateKey();

                byte[] vaultNonce = new byte[NONCE_LENGTH_BYTE];
                byte[] salt = new byte[SALT_SIZE_BYTE];
                secureRandom.nextBytes(vaultNonce);
                secureRandom.nextBytes(salt);

                int pbkdf2Iterations = generatePBKDF2IterationCount(passphrase, salt);

                SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);
                SecretKey keyFromPassphrase = secretKeyFactory.generateSecret(
                        new PBEKeySpec(passphrase.toCharArray(), salt, pbkdf2Iterations, AES_KEY_SIZE_BIT));

                writeVaultHeader(headerFile, vaultNonce, salt, pbkdf2Iterations, encryptionKey, keyFromPassphrase);
            } catch (Exception e) {
                Util.log("Cannot create vault header!");
                e.printStackTrace();
            }
        }

        try {
            FileInputStream headerInputStream = new FileInputStream(headerFile);
            vaultHeader = VaultHeader.parseFrom(headerInputStream);
        } catch (Exception e) {
            Util.log("Cannot read vault header!");
            e.printStackTrace();
        }

        try {
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);
            SecretKey keyFromPassphrase = secretKeyFactory.generateSecret(new PBEKeySpec(passphrase.toCharArray(),
                    vaultHeader.getSalt().toByteArray(), vaultHeader.getPbkdf2Iterations(), AES_KEY_SIZE_BIT));
            Cipher c = Cipher.getInstance(HEADER_ENCRYPTION_MODE);
            c.init(Cipher.UNWRAP_MODE, keyFromPassphrase,
                    new IvParameterSpec(vaultHeader.getVaultIV().toByteArray()));

            vaultFileEncryptionKey = (SecretKey) c.unwrap(vaultHeader.getEncryptedAesKey().toByteArray(),
                    KEY_ALGORITHM, Cipher.SECRET_KEY);
        } catch (InvalidKeyException e) {
            throw new InvalidKeyException("Passphrase is wrong!");
        } catch (Exception e) {
            Util.log("Cannot decrypt AES key");
            e.printStackTrace();
        }
    }

    private static int generatePBKDF2IterationCount(String passphrase, byte[] salt) {
        int calculatedIterations = 0;
        try {
            PBEKeySpec pbeKeySpec = new PBEKeySpec(passphrase.toCharArray(), salt,
                    Config.PBKDF2_ITERATIONS_BENCHMARK, AES_KEY_SIZE_BIT);
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);

            long startTime = System.currentTimeMillis();
            secretKeyFactory.generateSecret(pbeKeySpec);
            long finishTime = System.currentTimeMillis();

            calculatedIterations = (int) ((Config.PBKDF2_ITERATIONS_BENCHMARK / (double) (finishTime - startTime))
                    * Config.PBKDF2_CREATION_TARGET_MS);
        } catch (Exception e) {
            Util.log("Cannot benchmark PBKDF2!");
        }

        if (calculatedIterations > Config.PBKDF2_ITERATIONS_MIN) {
            Util.log("Using " + calculatedIterations + " PBKDF2 iterations");
            return calculatedIterations;
        }
        Util.log("Using " + Config.PBKDF2_ITERATIONS_MIN + " PBKDF2 iterations");
        return Config.PBKDF2_ITERATIONS_MIN;
    }

    private void writeVaultHeader(File headerFile, byte[] vaultNonce, byte[] salt, int pbkdf2Iterations, Key aesKey,
            SecretKey keyFromPassphrase) throws Exception {
        Cipher c = Cipher.getInstance(HEADER_ENCRYPTION_MODE);
        FileOutputStream headerOutputStream = new FileOutputStream(headerFile);

        c.init(Cipher.WRAP_MODE, keyFromPassphrase, new IvParameterSpec(vaultNonce));
        byte[] encryptedAesKey = c.wrap(aesKey);

        VaultHeader.Builder vaultHeaderBuilder = VaultHeader.newBuilder();
        vaultHeaderBuilder.setVersion(VAULT_HEADER_VERSION);
        vaultHeaderBuilder.setSalt(ByteString.copyFrom(salt));
        vaultHeaderBuilder.setVaultIV(ByteString.copyFrom(vaultNonce));
        vaultHeaderBuilder.setPbkdf2Iterations(pbkdf2Iterations);
        vaultHeaderBuilder.setEncryptedAesKey(ByteString.copyFrom(encryptedAesKey));
        vaultHeaderBuilder.build().writeTo(headerOutputStream);
        headerOutputStream.close();
    }

    @Override
    public CipherOutputStream getCipherOutputStream(File file, String outputFileName)
            throws SecrecyCipherStreamException, FileNotFoundException {
        Cipher c;
        try {
            c = Cipher.getInstance(encryptionMode);
        } catch (NoSuchAlgorithmException e) {
            throw new SecrecyCipherStreamException("Encryption algorithm not found!");
        } catch (NoSuchPaddingException e) {
            throw new SecrecyCipherStreamException("Selected padding not found!");
        }

        File headerFile = new File(vaultPath + FILE_HEADER_PREFIX + outputFileName);
        File outputFile = new File(vaultPath + "/" + outputFileName);

        byte[] fileEncryptionNonce = new byte[NONCE_LENGTH_BYTE];
        byte[] fileNameNonce = new byte[NONCE_LENGTH_BYTE];
        secureRandom.nextBytes(fileEncryptionNonce);
        secureRandom.nextBytes(fileNameNonce);

        try {
            c.init(Cipher.ENCRYPT_MODE, vaultFileEncryptionKey, new IvParameterSpec(fileNameNonce));
        } catch (InvalidKeyException e) {
            throw new SecrecyCipherStreamException("Invalid encryption key!");
        } catch (InvalidAlgorithmParameterException e) {
            throw new SecrecyCipherStreamException("Invalid algorithm parameter!");
        }

        byte[] encryptedFileName;
        try {
            encryptedFileName = c.doFinal(file.getName().getBytes());
        } catch (IllegalBlockSizeException e) {
            throw new SecrecyCipherStreamException("Illegal block size!");
        } catch (BadPaddingException e) {
            throw new SecrecyCipherStreamException("Bad padding");
        }

        FileHeader.Builder fileHeaderBuilder = FileHeader.newBuilder();
        fileHeaderBuilder.setVersion(FILE_HEADER_VERSION);
        fileHeaderBuilder.setFileIV(ByteString.copyFrom(fileEncryptionNonce));
        fileHeaderBuilder.setFileNameIV(ByteString.copyFrom(fileNameNonce));
        fileHeaderBuilder.setEncryptedFileName(ByteString.copyFrom(encryptedFileName));

        FileOutputStream headerOutputStream = new FileOutputStream(headerFile);
        try {
            fileHeaderBuilder.build().writeTo(headerOutputStream);
            headerOutputStream.close();
        } catch (IOException e) {
            throw new SecrecyCipherStreamException("IO exception while writing file header");
        }

        try {
            c.init(Cipher.ENCRYPT_MODE, vaultFileEncryptionKey, new IvParameterSpec(fileEncryptionNonce));
        } catch (InvalidKeyException e) {
            throw new SecrecyCipherStreamException("Invalid encryption key!");
        } catch (InvalidAlgorithmParameterException e) {
            throw new SecrecyCipherStreamException("Invalid algorithm parameter!");
        }

        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile),
                Config.BLOCK_SIZE);

        return new CipherOutputStream(bufferedOutputStream, c);
    }

    @Override
    public SecrecyCipherInputStream getCipherInputStream(File encryptedFile)
            throws SecrecyCipherStreamException, FileNotFoundException {
        Cipher c;
        try {
            c = Cipher.getInstance(encryptionMode);
        } catch (NoSuchAlgorithmException e) {
            throw new SecrecyCipherStreamException("Encryption algorithm not found!");
        } catch (NoSuchPaddingException e) {
            throw new SecrecyCipherStreamException("Selected padding not found!");
        }

        File headerFile = new File(encryptedFile.getParent() + FILE_HEADER_PREFIX + encryptedFile.getName());
        if (!headerFile.exists()) {
            throw new FileNotFoundException("Header file not found!");
        }

        FileHeader fileHeader;
        try {
            fileHeader = FileHeader.parseFrom(new FileInputStream(headerFile));
        } catch (IOException e) {
            throw new SecrecyCipherStreamException("Cannot parse file header!");
        }

        try {
            c.init(Cipher.DECRYPT_MODE, vaultFileEncryptionKey,
                    new IvParameterSpec(fileHeader.getFileIV().toByteArray()));
        } catch (InvalidKeyException e) {
            throw new SecrecyCipherStreamException("Invalid encryption key!");
        } catch (InvalidAlgorithmParameterException e) {
            throw new SecrecyCipherStreamException("Invalid algorithm parameter!");
        }

        return new SecrecyCipherInputStream(new FileInputStream(encryptedFile), c);
    }

    public String getDecryptedFileName(File file) throws SecrecyCipherStreamException, FileNotFoundException {
        Cipher c;
        try {
            c = Cipher.getInstance(encryptionMode);
        } catch (NoSuchAlgorithmException e) {
            throw new SecrecyCipherStreamException("Encryption algorithm not found!");
        } catch (NoSuchPaddingException e) {
            throw new SecrecyCipherStreamException("Selected padding not found!");
        }

        File headerFile = new File(file.getParent() + FILE_HEADER_PREFIX + file.getName());
        if (!headerFile.exists()) {
            throw new FileNotFoundException("Header file not found!");
        }

        FileHeader fileHeader;
        try {
            fileHeader = FileHeader.parseFrom(new FileInputStream(headerFile));
        } catch (IOException e) {
            throw new SecrecyCipherStreamException("Cannot parse file header!");
        }

        try {
            c.init(Cipher.DECRYPT_MODE, vaultFileEncryptionKey,
                    new IvParameterSpec(fileHeader.getFileNameIV().toByteArray()));
        } catch (InvalidKeyException e) {
            throw new SecrecyCipherStreamException("Invalid encryption key!");
        } catch (InvalidAlgorithmParameterException e) {
            throw new SecrecyCipherStreamException("Invalid algorithm parameter!");
        }
        byte[] decryptedFileName;
        try {
            decryptedFileName = c.doFinal(fileHeader.getEncryptedFileName().toByteArray());
        } catch (IllegalBlockSizeException e) {
            throw new SecrecyCipherStreamException("Illegal block size!");
        } catch (BadPaddingException e) {
            throw new SecrecyCipherStreamException("Bad padding");
        }
        return new String(decryptedFileName);
    }

    @Override
    public void renameFile(File file, String newName) throws SecrecyCipherStreamException, FileNotFoundException {
        Cipher c;
        try {
            c = Cipher.getInstance(encryptionMode);
        } catch (NoSuchAlgorithmException e) {
            throw new SecrecyCipherStreamException("Encryption algorithm not found!");
        } catch (NoSuchPaddingException e) {
            throw new SecrecyCipherStreamException("Selected padding not found!");
        }

        File headerFile = new File(file.getParent() + FILE_HEADER_PREFIX + file.getName());
        if (!headerFile.exists()) {
            throw new FileNotFoundException("Header file not found!");
        }

        FileHeader fileHeader;
        try {
            fileHeader = FileHeader.parseFrom(new FileInputStream(headerFile));
        } catch (IOException e) {
            throw new SecrecyCipherStreamException("Cannot parse file header!");
        }

        try {
            c.init(Cipher.ENCRYPT_MODE, vaultFileEncryptionKey,
                    new IvParameterSpec(fileHeader.getFileNameIV().toByteArray()));
        } catch (InvalidKeyException e) {
            throw new SecrecyCipherStreamException("Invalid encryption key!");
        } catch (InvalidAlgorithmParameterException e) {
            throw new SecrecyCipherStreamException("Invalid algorithm parameter!");
        }

        byte[] encryptedFileName;
        try {
            encryptedFileName = c.doFinal(newName.getBytes());
        } catch (IllegalBlockSizeException e) {
            throw new SecrecyCipherStreamException("Illegal block size!");
        } catch (BadPaddingException e) {
            throw new SecrecyCipherStreamException("Bad padding");
        }

        FileHeader.Builder fileHeaderBuilder = fileHeader.toBuilder();
        fileHeaderBuilder.setEncryptedFileName(ByteString.copyFrom(encryptedFileName));

        FileOutputStream headerOutputStream = new FileOutputStream(headerFile);
        try {
            fileHeaderBuilder.build().writeTo(headerOutputStream);
            headerOutputStream.close();
        } catch (IOException e) {
            throw new SecrecyCipherStreamException("IO exception while writing file header");
        }
    }

    @Override
    public boolean changePassphrase(String oldPassphrase, String newPassphrase) {
        SecretKeyFactory secretKeyFactory;

        File headerFileOld = new File(this.vaultPath + VAULT_HEADER_FILENAME);
        File headerFileNew = new File(this.vaultPath + VAULT_HEADER_FILENAME + "NEW");
        if (!headerFileNew.exists()) {
            try {
                // Decrypt AES encryption key
                secretKeyFactory = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);
                SecretKey oldKeyFromPassphrase = secretKeyFactory.generateSecret(
                        new PBEKeySpec(oldPassphrase.toCharArray(), vaultHeader.getSalt().toByteArray(),
                                vaultHeader.getPbkdf2Iterations(), AES_KEY_SIZE_BIT));
                Cipher c = Cipher.getInstance(HEADER_ENCRYPTION_MODE);
                c.init(Cipher.UNWRAP_MODE, oldKeyFromPassphrase,
                        new IvParameterSpec(vaultHeader.getVaultIV().toByteArray()));
                Key decryptedKey = c.unwrap(vaultHeader.getEncryptedAesKey().toByteArray(), KEY_ALGORITHM,
                        Cipher.SECRET_KEY);

                // Create new vault nonce and salt
                byte[] vaultNonce = new byte[NONCE_LENGTH_BYTE];
                byte[] salt = new byte[SALT_SIZE_BYTE];
                secureRandom.nextBytes(vaultNonce);
                secureRandom.nextBytes(salt);

                int pbkdf2Iterations = generatePBKDF2IterationCount(newPassphrase, salt);

                // Create new key for AES key encryption
                SecretKey newKeyFromPassphrase = secretKeyFactory.generateSecret(
                        new PBEKeySpec(newPassphrase.toCharArray(), salt, pbkdf2Iterations, AES_KEY_SIZE_BIT));

                writeVaultHeader(headerFileNew, vaultNonce, salt, pbkdf2Iterations, decryptedKey,
                        newKeyFromPassphrase);

            } catch (Exception e) {
                Util.log("Error while reading or creating new vault header!");
                return false;
            }
        } else {
            Util.log("New header file already exists. Cannot change passphrase!");
            return false;
        }

        // Try to parse new header file
        try {
            FileInputStream headerInputStream = new FileInputStream(headerFileNew);
            vaultHeader = VaultHeader.parseFrom(headerInputStream);
        } catch (Exception e) {
            Util.log("Cannot read vault header!");
            headerFileNew.delete();
            return false;
        }

        // Delete old header file and replace with new header file
        if (!headerFileOld.delete()) {
            headerFileNew.delete();
            Util.log("Cannot delete old vault header!");
            return false;
        }
        try {
            org.apache.commons.io.FileUtils.copyFile(headerFileNew, headerFileOld);
        } catch (IOException e) {
            Util.log("Cannot replace old vault header!");
            return false;
        }

        headerFileNew.delete();
        return true;
    }

    @Override
    public void deleteFile(EncryptedFile file) {
        Storage.purgeFile(new File(file.getFile().getParent() + FILE_HEADER_PREFIX + file.getFile().getName()));
        try {
            Storage.purgeFile(new File(file.getEncryptedThumbnail().getFile().getParent() + FILE_HEADER_PREFIX
                    + file.getEncryptedThumbnail().getFile().getName()));
        } catch (SecrecyFileException e) {
            Util.log("Thumbnail header file not found!");
        }
        file.delete();
    }
}