org.cryptomator.crypto.aes256.Aes256Cryptor.java Source code

Java tutorial

Introduction

Here is the source code for org.cryptomator.crypto.aes256.Aes256Cryptor.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Sebastian Stenzel
 * This file is licensed under the terms of the MIT license.
 * See the LICENSE.txt file for more info.
 * 
 * Contributors:
 *     Sebastian Stenzel - initial API and implementation
 ******************************************************************************/
package org.cryptomator.crypto.aes256;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.zip.CRC32;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.generators.SCrypt;
import org.cryptomator.crypto.AbstractCryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions {

    /**
     * PRNG for cryptographically secure random numbers. Defaults to SHA1-based number generator.
     * 
     * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecureRandom
     */
    private static final SecureRandom SECURE_PRNG;

    /**
     * Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction
     * Policy Files isn't installed. Those files can be downloaded here: http://www.oracle.com/technetwork/java/javase/downloads/.
     */
    private static final int AES_KEY_LENGTH_IN_BITS;

    /**
     * Jackson JSON-Mapper.
     */
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or
     * {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with {@link #swipeSensitiveData()}.
     */
    private SecretKey primaryMasterKey;

    /**
     * Decrypted secondary key used for hmac operations.
     */
    private SecretKey hMacMasterKey;

    static {
        try {
            SECURE_PRNG = SecureRandom.getInstance(PRNG_ALGORITHM);
            final int maxKeyLength = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
            AES_KEY_LENGTH_IN_BITS = (maxKeyLength >= PREF_MASTER_KEY_LENGTH_IN_BITS)
                    ? PREF_MASTER_KEY_LENGTH_IN_BITS
                    : maxKeyLength;
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("Algorithm should exist.", e);
        }
    }

    /**
     * Creates a new Cryptor with a newly initialized PRNG.
     */
    public Aes256Cryptor() {
        SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
        byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
        try {
            SECURE_PRNG.nextBytes(bytes);
            this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);

            SECURE_PRNG.nextBytes(bytes);
            this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
        } finally {
            Arrays.fill(bytes, (byte) 0);
        }
    }

    /**
     * Creates a new Cryptor with the given PRNG.<br/>
     * <strong>DO NOT USE IN PRODUCTION</strong>. This constructor must only be used in in unit tests. Do not change method visibility.
     * 
     * @param prng Fast, possibly insecure PRNG.
     */
    Aes256Cryptor(Random prng) {
        byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
        try {
            prng.nextBytes(bytes);
            this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);

            prng.nextBytes(bytes);
            this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
        } finally {
            Arrays.fill(bytes, (byte) 0);
        }
    }

    /**
     * Encrypts the current masterKey with the given password and writes the result to the given output stream.
     */
    @Override
    public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
        try {
            // derive key:
            final byte[] kekSalt = randomData(SCRYPT_SALT_LENGTH);
            final SecretKey kek = scrypt(password, kekSalt, SCRYPT_COST_PARAM, SCRYPT_BLOCK_SIZE,
                    AES_KEY_LENGTH_IN_BITS);

            // encrypt:
            final Cipher encCipher = aesKeyWrapCipher(kek, Cipher.WRAP_MODE);
            byte[] wrappedPrimaryKey = encCipher.wrap(primaryMasterKey);
            byte[] wrappedSecondaryKey = encCipher.wrap(hMacMasterKey);

            // save encrypted masterkey:
            final KeyFile keyfile = new KeyFile();
            keyfile.setScryptSalt(kekSalt);
            keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
            keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
            keyfile.setKeyLength(AES_KEY_LENGTH_IN_BITS);
            keyfile.setPrimaryMasterKey(wrappedPrimaryKey);
            keyfile.setHMacMasterKey(wrappedSecondaryKey);
            objectMapper.writeValue(out, keyfile);
        } catch (InvalidKeyException | IllegalBlockSizeException ex) {
            throw new IllegalStateException("Invalid hard coded configuration.", ex);
        }
    }

    /**
     * Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
     * 
     * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
     * @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
     *             password. In this case a DecryptFailedException will be thrown.
     * @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
     *             this case Java JCE needs to be installed.
     */
    @Override
    public void decryptMasterKey(InputStream in, CharSequence password)
            throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
        try {
            // load encrypted masterkey:
            final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class);

            // check, whether the key length is supported:
            final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
            if (keyfile.getKeyLength() > maxKeyLen) {
                throw new UnsupportedKeyLengthException(keyfile.getKeyLength(), maxKeyLen);
            }

            // derive key:
            final SecretKey kek = scrypt(password, keyfile.getScryptSalt(), keyfile.getScryptCostParam(),
                    keyfile.getScryptBlockSize(), AES_KEY_LENGTH_IN_BITS);

            // decrypt and check password by catching AEAD exception
            final Cipher decCipher = aesKeyWrapCipher(kek, Cipher.UNWRAP_MODE);
            SecretKey primary = (SecretKey) decCipher.unwrap(keyfile.getPrimaryMasterKey(), AES_KEY_ALGORITHM,
                    Cipher.SECRET_KEY);
            SecretKey secondary = (SecretKey) decCipher.unwrap(keyfile.getHMacMasterKey(), HMAC_KEY_ALGORITHM,
                    Cipher.SECRET_KEY);

            // everything ok, assign decrypted keys:
            this.primaryMasterKey = primary;
            this.hMacMasterKey = secondary;
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException("Algorithm should exist.", ex);
        } catch (InvalidKeyException e) {
            throw new WrongPasswordException();
        }
    }

    @Override
    public void swipeSensitiveDataInternal() {
        destroyQuietly(primaryMasterKey);
        destroyQuietly(hMacMasterKey);
    }

    private void destroyQuietly(Destroyable d) {
        try {
            d.destroy();
        } catch (DestroyFailedException e) {
            // ignore
        }
    }

    private Cipher aesKeyWrapCipher(SecretKey key, int cipherMode) {
        try {
            final Cipher cipher = Cipher.getInstance(AES_KEYWRAP_CIPHER);
            cipher.init(cipherMode, key);
            return cipher;
        } catch (InvalidKeyException ex) {
            throw new IllegalArgumentException("Invalid key.", ex);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
            throw new IllegalStateException("Algorithm/Padding should exist and accept GCM specs.", ex);
        }
    }

    private Cipher aesCtrCipher(SecretKey key, byte[] iv, int cipherMode) {
        try {
            final Cipher cipher = Cipher.getInstance(AES_CTR_CIPHER);
            cipher.init(cipherMode, key, new IvParameterSpec(iv));
            return cipher;
        } catch (InvalidKeyException ex) {
            throw new IllegalArgumentException("Invalid key.", ex);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
            throw new IllegalStateException("Algorithm/Padding should exist and accept an IV.", ex);
        }
    }

    private Cipher aesEcbCipher(SecretKey key, int cipherMode) {
        try {
            final Cipher cipher = Cipher.getInstance(AES_ECB_CIPHER);
            cipher.init(cipherMode, key);
            return cipher;
        } catch (InvalidKeyException ex) {
            throw new IllegalArgumentException("Invalid key.", ex);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
            throw new AssertionError(
                    "Every implementation of the Java platform is required to support AES/ECB/PKCS5Padding.", ex);
        }

    }

    private Mac hmacSha256(SecretKey key) {
        try {
            final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
            mac.init(key);
            return mac;
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.",
                    e);
        } catch (InvalidKeyException e) {
            throw new IllegalArgumentException("Invalid key", e);
        }
    }

    private byte[] randomData(int length) {
        final byte[] result = new byte[length];
        SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
        SECURE_PRNG.nextBytes(result);
        return result;
    }

    private SecretKey scrypt(CharSequence password, byte[] salt, int costParam, int blockSize,
            int keyLengthInBits) {
        // use sb, as password.toString's implementation is unknown
        final StringBuilder sb = new StringBuilder(password);
        final byte[] pw = sb.toString().getBytes();
        try {
            final byte[] key = SCrypt.generate(pw, salt, costParam, blockSize, 1, keyLengthInBits / Byte.SIZE);
            return new SecretKeySpec(key, AES_KEY_ALGORITHM);
        } finally {
            // destroy copied bytes of the plaintext password:
            Arrays.fill(pw, (byte) 0);
            for (int i = 0; i < password.length(); i++) {
                sb.setCharAt(i, (char) 0);
            }
        }
    }

    private long crc32Sum(byte[] source) {
        final CRC32 crc32 = new CRC32();
        crc32.update(source);
        return crc32.getValue();
    }

    @Override
    public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep,
            CryptorIOSupport ioSupport) {
        try {
            final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
            final List<String> encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
            for (final String cleartext : cleartextPathComps) {
                final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, ioSupport);
                encryptedPathComps.add(encrypted);
            }
            return StringUtils.join(encryptedPathComps, encryptedPathSep);
        } catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
            throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e);
        }
    }

    /**
     * Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.<br/>
     * Encryption will blow up the filename length due to aes block sizes and base32 encoding. The result may be too long for some old file
     * systems.<br/>
     * This means that we need a workaround for filenames longer than the limit defined in
     * {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
     * <br/>
     * In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No
     * cryptographically secure hash is needed here. We just want an uniform distribution for better load balancing. All encrypted filenames
     * with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique
     * alternative names are stored.<br/>
     * <br/>
     * These alternative names consist of the checksum, a unique id and a special file extension defined in
     * {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
     */
    private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport)
            throws IllegalBlockSizeException, BadPaddingException, IOException {
        final byte[] mac = hmacSha256(hMacMasterKey).doFinal(cleartext.getBytes());
        final byte[] partialIv = ArrayUtils.subarray(mac, 0, 10);
        final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
        iv.put(partialIv);
        final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.ENCRYPT_MODE);
        // add NULL padding to the cleartext to get a multiple of the block size:
        final byte[] cleartextBytes = cleartext.getBytes(StandardCharsets.UTF_8);
        final byte[] nullBytePadding = new byte[AES_BLOCK_LENGTH - cleartextBytes.length % AES_BLOCK_LENGTH];
        final byte[] paddedCleartextBytes = ArrayUtils.addAll(cleartextBytes, nullBytePadding);
        final byte[] encryptedBytes = cipher.doFinal(paddedCleartextBytes);
        final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(partialIv) + IV_PREFIX_SEPARATOR
                + ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);

        if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
            final String crc32 = Long.toHexString(crc32Sum(ivAndCiphertext.getBytes()));
            final String metadataFilename = crc32 + METADATA_FILE_EXT;
            final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
            final String alternativeFileName = crc32 + LONG_NAME_PREFIX_SEPARATOR
                    + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT;
            this.storeMetadata(ioSupport, metadataFilename, metadata);
            return alternativeFileName;
        } else {
            return ivAndCiphertext + BASIC_FILE_EXT;
        }
    }

    @Override
    public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep,
            CryptorIOSupport ioSupport) {
        try {
            final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
            final List<String> cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
            for (final String encrypted : encryptedPathComps) {
                final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, ioSupport);
                cleartextPathComps.add(new String(cleartext));
            }
            return StringUtils.join(cleartextPathComps, cleartextPathSep);
        } catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
            throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e);
        }
    }

    /**
     * @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
     */
    private String decryptPathComponent(final String encrypted, final SecretKey key, CryptorIOSupport ioSupport)
            throws IllegalBlockSizeException, BadPaddingException, IOException {
        final String ivAndCiphertext;
        if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
            final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
            final String crc32 = StringUtils.substringBefore(basename, LONG_NAME_PREFIX_SEPARATOR);
            final String uuid = StringUtils.substringAfter(basename, LONG_NAME_PREFIX_SEPARATOR);
            final String metadataFilename = crc32 + METADATA_FILE_EXT;
            final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
            ivAndCiphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
        } else if (encrypted.endsWith(BASIC_FILE_EXT)) {
            ivAndCiphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
        } else {
            throw new IllegalArgumentException("Unsupported path component: " + encrypted);
        }

        final String partialIvStr = StringUtils.substringBefore(ivAndCiphertext, IV_PREFIX_SEPARATOR);
        final String ciphertext = StringUtils.substringAfter(ivAndCiphertext, IV_PREFIX_SEPARATOR);
        final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
        iv.put(ENCRYPTED_FILENAME_CODEC.decode(partialIvStr));

        final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.DECRYPT_MODE);
        final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
        final byte[] paddedCleartextBytes = cipher.doFinal(encryptedBytes);

        // remove NULL padding (not valid in file names anyway)
        final int beginOfPadding = ArrayUtils.indexOf(paddedCleartextBytes, (byte) 0x00);
        if (beginOfPadding == -1) {
            return new String(paddedCleartextBytes, StandardCharsets.UTF_8);
        } else {
            final byte[] cleartextBytes = Arrays.copyOf(paddedCleartextBytes, beginOfPadding);
            return new String(cleartextBytes, StandardCharsets.UTF_8);
        }
    }

    private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException {
        final byte[] fileContent = ioSupport.readPathSpecificMetadata(metadataFile);
        if (fileContent == null) {
            return new LongFilenameMetadata();
        } else {
            return objectMapper.readValue(fileContent, LongFilenameMetadata.class);
        }
    }

    private void storeMetadata(CryptorIOSupport ioSupport, String metadataFile, LongFilenameMetadata metadata)
            throws JsonProcessingException, IOException {
        ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
    }

    @Override
    public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
        // init mac:
        final Mac calculatedMac = this.hmacSha256(hMacMasterKey);

        // read stored mac:
        encryptedFile.position(16);
        final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
        final int numMacBytesRead = encryptedFile.read(storedMac);

        // check validity of header:
        if (numMacBytesRead != calculatedMac.getMacLength()) {
            throw new IOException("Failed to read file header.");
        }

        // read all encrypted data and calculate mac:
        encryptedFile.position(64);
        final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
        final InputStream macIn = new MacInputStream(in, calculatedMac);
        IOUtils.copyLarge(macIn, new NullOutputStream());

        // compare (in constant time):
        return MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
    }

    @Override
    public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
        // skip 128bit IV + 256 bit MAC:
        encryptedFile.position(48);

        // read encrypted value:
        final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
        final int numFileSizeBytesRead = encryptedFile.read(encryptedFileSizeBuffer);

        // return "unknown" value, if EOF
        if (numFileSizeBytesRead != encryptedFileSizeBuffer.capacity()) {
            return null;
        }

        // decrypt size:
        try {
            final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE);
            final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedFileSizeBuffer.array());
            final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
            return fileSizeBuffer.getLong();
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
        // read iv:
        encryptedFile.position(0);
        final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
        final int numIvBytesRead = encryptedFile.read(countingIv);

        // read file size:
        final Long fileSize = decryptedContentLength(encryptedFile);

        // check validity of header:
        if (numIvBytesRead != AES_BLOCK_LENGTH || fileSize == null) {
            throw new IOException("Failed to read file header.");
        }

        // go to begin of content:
        encryptedFile.position(64);

        // generate cipher:
        final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);

        // read content
        final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
        final InputStream cipheredIn = new CipherInputStream(in, cipher);
        return IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
    }

    @Override
    public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length)
            throws IOException {
        // read iv:
        encryptedFile.position(0);
        final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
        final int numIvBytesRead = encryptedFile.read(countingIv);

        // check validity of header:
        if (numIvBytesRead != AES_BLOCK_LENGTH) {
            throw new IOException("Failed to read file header.");
        }

        // seek relevant position and update iv:
        long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
        long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
        long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
        countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, firstRelevantBlock);

        // fast forward stream:
        encryptedFile.position(64 + beginOfFirstRelevantBlock);

        // generate cipher:
        final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);

        // read content
        final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
        final InputStream cipheredIn = new CipherInputStream(in, cipher);
        return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
    }

    @Override
    public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
        // truncate file
        encryptedFile.truncate(0);

        // use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
        final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
        countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l);
        countingIv.position(0);
        encryptedFile.write(countingIv);

        // init crypto stuff:
        final Mac mac = this.hmacSha256(hMacMasterKey);
        final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE);

        // init mac buffer and skip 32 bytes
        final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
        encryptedFile.write(macBuffer);

        // init filesize buffer and skip 16 bytes
        final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
        encryptedFile.write(encryptedFileSizeBuffer);

        // write content:
        final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
        final OutputStream macOut = new MacOutputStream(out, mac);
        final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
        final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
        final Long plaintextSize = IOUtils.copyLarge(plaintextFile, blockSizeBufferedOut);

        // ensure total byte count is a multiple of the block size, in CTR mode:
        final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH);
        blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]);

        // append a few blocks of fake data:
        final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
        final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
        final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
        for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
            blockSizeBufferedOut.write(emptyBytes);
        }
        blockSizeBufferedOut.flush();

        // write MAC of total ciphertext:
        macBuffer.position(0);
        macBuffer.put(mac.doFinal());
        macBuffer.position(0);
        encryptedFile.position(16); // right behind the IV
        encryptedFile.write(macBuffer); // 256 bit MAC

        // encrypt and write plaintextSize
        try {
            final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
            fileSizeBuffer.putLong(plaintextSize);
            final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
            final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
            encryptedFileSizeBuffer.position(0);
            encryptedFileSizeBuffer.put(encryptedFileSize);
            encryptedFileSizeBuffer.position(0);
            encryptedFile.position(48); // right behind the IV and MAC
            encryptedFile.write(encryptedFileSizeBuffer);
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            throw new IllegalStateException(
                    "Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.",
                    e);
        }

        return plaintextSize;
    }

    @Override
    public Filter<Path> getPayloadFilesFilter() {
        return new Filter<Path>() {
            @Override
            public boolean accept(Path entry) throws IOException {
                return ENCRYPTED_FILE_GLOB_MATCHER.matches(entry);
            }
        };
    }

}