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