public class DecryptingReadableByteChannel implements ReadableByteChannel {

	private final ReadableByteChannel delegate;
	private final Cryptor cryptor;
	private final boolean authenticate;
	private ByteBuffer cleartextChunk;
	private FileHeader header;
	private boolean reachedEof;
	private long chunk;

	/**
	 * Creates a DecryptingReadableByteChannel that decrypts a whole ciphertext file beginning at its first byte.
	 *
	 * @param src          A ciphertext channel positioned at the begin of the file header
	 * @param cryptor      The cryptor to use
	 * @param authenticate Set to <code>false</code> to skip ciphertext authentication (may not be supported)
	 */
	public DecryptingReadableByteChannel(ReadableByteChannel src, Cryptor cryptor, boolean authenticate) {
		this(src, cryptor, authenticate, null, 0);
	}

	/**
	 * Creates a DecryptingReadableByteChannel with a previously read header, allowing to start decryption at any chunk.
	 *
	 * @param src          A ciphertext channel positioned at the beginning of the given <code>firstChunk</code>
	 * @param cryptor      The cryptor to use
	 * @param authenticate Set to <code>false</code> to skip ciphertext authentication (may not be supported)
	 * @param header       The file's header
	 * @param firstChunk   The index of the chunk at which the <code>src</code> channel is positioned
	 */
	public DecryptingReadableByteChannel(ReadableByteChannel src, Cryptor cryptor, boolean authenticate, FileHeader header, long firstChunk) {
		this.delegate = src;
		this.cryptor = cryptor;
		this.authenticate = authenticate;
		this.cleartextChunk = ByteBuffer.allocate(0); // empty buffer will trigger loadNextCleartextChunk() on first access.
		this.header = header;
		this.reachedEof = false;
		this.chunk = firstChunk;
	}

	@Override
	public boolean isOpen() {
		return delegate.isOpen();
	}

	@Override
	public void close() throws IOException {
		delegate.close();
	}

	@Override
	public synchronized int read(ByteBuffer dst) throws IOException {
		try {
			loadHeaderIfNecessary();
			if (reachedEof) {
				return -1;
			} else {
				return readInternal(dst);
			}
		} catch (AuthenticationFailedException e) {
			throw new IOException("Unauthentic ciphertext", e);
		}
	}

	private int readInternal(ByteBuffer dst) throws IOException, AuthenticationFailedException {
		assert header != null : "header must be initialized";

		int result = 0;
		while (dst.hasRemaining() && !reachedEof) {
			if (cleartextChunk.hasRemaining() || loadNextCleartextChunk()) {
				result += ByteBuffers.copy(cleartextChunk, dst);
			} else {
				assert reachedEof : "no further cleartext available";
			}
		}
		return result;
	}

	private void loadHeaderIfNecessary() throws IOException, AuthenticationFailedException {
		if (header == null) {
			ByteBuffer headerBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize());
			int read = ByteBuffers.fill(delegate, headerBuf);
			if (read != headerBuf.capacity()) {
				throw new EOFException("Unable to read header from channel.");
			}
			headerBuf.flip();
			header = cryptor.fileHeaderCryptor().decryptHeader(headerBuf);
		}
	}

	private boolean loadNextCleartextChunk() throws IOException, AuthenticationFailedException {
		ByteBuffer ciphertextChunk = ByteBuffer.allocate(cryptor.fileContentCryptor().ciphertextChunkSize());
		int read = ByteBuffers.fill(delegate, ciphertextChunk);
		if (read == 0) {
			reachedEof = true;
			return false;
		} else {
			ciphertextChunk.flip();
			cleartextChunk = cryptor.fileContentCryptor().decryptChunk(ciphertextChunk, chunk++, header, authenticate);
			return true;
		}
	}

} public class EncryptingWritableByteChannel implements WritableByteChannel {

	private final WritableByteChannel delegate;
	private final Cryptor cryptor;
	private final FileHeader header;
	private final ByteBuffer cleartextBuffer;

	private boolean firstWrite = true;
	private long chunkNumber = 0;

	public EncryptingWritableByteChannel(WritableByteChannel destination, Cryptor cryptor) {
		this.delegate = destination;
		this.cryptor = cryptor;
		this.header = cryptor.fileHeaderCryptor().create();
		this.cleartextBuffer = ByteBuffer.allocate(cryptor.fileContentCryptor().cleartextChunkSize());
	}

	@Override
	public boolean isOpen() {
		return delegate.isOpen();
	}

	@Override
	public synchronized void close() throws IOException {
		writeHeaderOnFirstWrite();
		encryptAndFlushBuffer();
		delegate.close();
	}

	@Override
	public synchronized int write(ByteBuffer src) throws IOException {
		writeHeaderOnFirstWrite();
		int result = 0;
		while (src.hasRemaining()) {
			result += ByteBuffers.copy(src, cleartextBuffer);
			if (!cleartextBuffer.hasRemaining()) {
				encryptAndFlushBuffer();
			}
		}
		return result;
	}

	private void writeHeaderOnFirstWrite() throws IOException {
		if (firstWrite) {
			delegate.write(cryptor.fileHeaderCryptor().encryptHeader(header));
		}
		firstWrite = false;
	}

	private void encryptAndFlushBuffer() throws IOException {
		cleartextBuffer.flip();
		if (cleartextBuffer.hasRemaining()) {
			ByteBuffer ciphertextBuffer = cryptor.fileContentCryptor().encryptChunk(cleartextBuffer, chunkNumber++, header);
			delegate.write(ciphertextBuffer);
		}
		cleartextBuffer.clear();
	}

} public class AuthenticationFailedException extends CryptoException {

	public AuthenticationFailedException(String message) {
		super(message);
	}

	public AuthenticationFailedException(String message, Throwable cause) {
		super(message, cause);
	}

} public interface Cryptor extends Destroyable, AutoCloseable {

	FileContentCryptor fileContentCryptor();

	FileHeaderCryptor fileHeaderCryptor();

	FileNameCryptor fileNameCryptor();

	@Override
	void destroy();

	/**
	 * Calls {@link #destroy()}.
	 */
	@Override
	default void close() {
		destroy();
	}

} public interface FileHeader {

	/**
	 * Returns the value of a currently unused 64 bit field in the file header.
	 * <p>
	 * Formerly used for storing the plaintext file size.
	 *
	 * @return 64 bit integer for future use.
	 * @deprecated Don't rely on this method. It may be redefined any time.
	 */
	@Deprecated
	long getReserved();

	/**
	 * Sets the 64 bit field in the file header.
	 * <p>
	 * Formerly used for storing the plaintext file size.
	 *
	 * @param reserved 64 bit integer for future use
	 * @deprecated Don't rely on this method. It may be redefined any time.
	 */
	@Deprecated
	void setReserved(long reserved);

} public class Masterkey extends DestroyableSecretKey {

	private static final String KEY_ALGORITHM = "MASTERKEY";
	public static final String ENC_ALG = "AES";
	public static final String MAC_ALG = "HmacSHA256";
	public static final int SUBKEY_LEN_BYTES = 32;

	public Masterkey(byte[] key) {
		super(checkKeyLength(key), KEY_ALGORITHM);
	}

	private static byte[] checkKeyLength(byte[] key) {
		Preconditions.checkArgument(key.length == SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES, "Invalid raw key length %s", key.length);
		return key;
	}

	public static Masterkey generate(SecureRandom csprng) {
		byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES];
		try {
			csprng.nextBytes(key);
			return new Masterkey(key);
		} finally {
			Arrays.fill(key, (byte) 0x00);
		}
	}

	public static Masterkey from(DestroyableSecretKey encKey, DestroyableSecretKey macKey) {
		Preconditions.checkArgument(encKey.getEncoded().length == SUBKEY_LEN_BYTES, "Invalid key length of encKey");
		Preconditions.checkArgument(macKey.getEncoded().length == SUBKEY_LEN_BYTES, "Invalid key length of macKey");
		byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES];
		try {
			System.arraycopy(encKey.getEncoded(), 0, key, 0, SUBKEY_LEN_BYTES);
			System.arraycopy(macKey.getEncoded(), 0, key, SUBKEY_LEN_BYTES, SUBKEY_LEN_BYTES);
			return new Masterkey(key);
		} finally {
			Arrays.fill(key, (byte) 0x00);
		}
	}

	@Override
	public Masterkey copy() {
		return new Masterkey(getEncoded());
	}

	/**
	 * Get the encryption subkey.
	 *
	 * @return A new copy of the subkey used for encryption
	 */
	public DestroyableSecretKey getEncKey() {
		return new DestroyableSecretKey(getEncoded(), 0, SUBKEY_LEN_BYTES, ENC_ALG);
	}

	/**
	 * Get the MAC subkey.
	 *
	 * @return A new copy of the subkey used for message authentication
	 */
	public DestroyableSecretKey getMacKey() {
		return new DestroyableSecretKey(getEncoded(), SUBKEY_LEN_BYTES, SUBKEY_LEN_BYTES, MAC_ALG);
	}

} public class DestroyableSecretKey implements SecretKey, AutoCloseable {

	private static final String KEY_DESTROYED_ERROR = "Key has been destroyed";

	private final transient byte[] key;
	private final String algorithm;
	private boolean destroyed;

	/**
	 * Convenience constructor for {@link #DestroyableSecretKey(byte[], int, int, String)}
	 *
	 * @param key       The raw key data (will get copied)
	 * @param algorithm The {@link #getAlgorithm() algorithm name}
	 */
	public DestroyableSecretKey(byte[] key, String algorithm) {
		this(key, 0, key.length, algorithm);
	}

	/**
	 * Creates a new destroyable secret key, copying of the provided raw key bytes.
	 *
	 * @param key       A byte[] holding the key material (relevant part will get copied)
	 * @param offset    The offset within <code>key</code> where the key starts
	 * @param len       The number of bytes beginning at <code>offset</code> to read from <code>key</code>
	 * @param algorithm The {@link #getAlgorithm() algorithm name}
	 */
	public DestroyableSecretKey(byte[] key, int offset, int len, String algorithm) {
		Preconditions.checkArgument(offset >= 0, "Invalid offset");
		Preconditions.checkArgument(len >= 0, "Invalid length");
		Preconditions.checkArgument(key.length >= offset + len, "Invalid offset/len");
		this.key = new byte[len];
		this.algorithm = Preconditions.checkNotNull(algorithm, "Algorithm must not be null");
		this.destroyed = false;
		System.arraycopy(key, offset, this.key, 0, len);
	}

	/**
	 * Casts or converts a given {@link SecretKey} to a DestroyableSecretKey
	 *
	 * @param secretKey The secret key
	 * @return Either the provided or a new key, depending on whether the provided key is already a DestroyableSecretKey
	 */
	public static DestroyableSecretKey from(Key secretKey) {
		if (secretKey instanceof DestroyableSecretKey) {
			return (DestroyableSecretKey) secretKey;
		} else {
			return new DestroyableSecretKey(secretKey.getEncoded(), secretKey.getAlgorithm());
		}
	}

	/**
	 * Creates a new key of given length and for use with given algorithm using entropy from the given csprng.
	 *
	 * @param csprng      A cryptographically secure random number source
	 * @param algorithm   The {@link #getAlgorithm() key algorithm}
	 * @param keyLenBytes The length of the key (in bytes)
	 * @return A new secret key
	 */
	public static DestroyableSecretKey generate(SecureRandom csprng, String algorithm, int keyLenBytes) {
		byte[] key = new byte[keyLenBytes];
		try {
			csprng.nextBytes(key);
			return new DestroyableSecretKey(key, algorithm);
		} finally {
			Arrays.fill(key, (byte) 0x00);
		}
	}

	@Override
	public String getAlgorithm() {
		Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR);
		return algorithm;
	}

	@Override
	public String getFormat() {
		Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR);
		return "RAW";
	}

	/**
	 * Returns the raw key bytes this instance wraps.
	 * <p>
	 * <b>Important:</b> Any change to the returned array will reflect in this key. Make sure to
	 * {@link #copy() make a local copy} if you can't rule out mutations.
	 *
	 * @return A byte array holding the secret key
	 */
	@Override
	public byte[] getEncoded() {
		Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR);
		return key;
	}

	/**
	 * Returns an independent copy of this key
	 * @return New copy of <code>this</code>
	 */
	public DestroyableSecretKey copy() {
		Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR);
		return new DestroyableSecretKey(key, algorithm); // key will get copied by the constructor as per contract
	}

	@Override
	public void destroy() {
		Arrays.fill(key, (byte) 0x00);
		destroyed = true;
	}

	@Override
	public boolean isDestroyed() {
		return destroyed;
	}

	/**
	 * Same as {@link #destroy()}
	 */
	@Override
	public void close() {
		destroy();
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		DestroyableSecretKey that = (DestroyableSecretKey) o;
		return algorithm.equals(that.algorithm) && MessageDigest.isEqual(this.key, that.key);
	}

	@Override
	public int hashCode() {
		int result = Objects.hash(algorithm);
		result = 31 * result + Arrays.hashCode(key);
		return result;
	}
} public class SecureRandomMock extends SecureRandom {

	private static final ByteFiller NULL_FILLER = bytes -> Arrays.fill(bytes, (byte) 0x00);
	public static final SecureRandomMock NULL_RANDOM = new SecureRandomMock(NULL_FILLER);
	private static final ByteFiller PRNG_FILLER = new ByteFiller() {

		private final Random random = new Random();

		@Override
		public void fill(byte[] bytes) {
			random.nextBytes(bytes);
		}

	};
	public static final SecureRandomMock PRNG_RANDOM = new SecureRandomMock(PRNG_FILLER);

	private final ByteFiller byteFiller;

	public SecureRandomMock(ByteFiller byteFiller) {
		this.byteFiller = byteFiller;
	}

	@Override
	public void nextBytes(byte[] bytes) {
		byteFiller.fill(bytes);
	}

	public static SecureRandomMock cycle(byte... bytes) {
		return new SecureRandomMock(new CyclicByteFiller(bytes));
	}

	public interface ByteFiller {
		void fill(byte[] bytes);
	}

	private static class CyclicByteFiller implements ByteFiller {

		private final Iterator<Byte> source;

		CyclicByteFiller(byte... bytes) {
			source = Iterators.cycle(Bytes.asList(bytes));
		}

		@Override
		public void fill(byte[] bytes) {
			Arrays.fill(bytes, source.next());
		}

	}

} public class SeekableByteChannelMock implements SeekableByteChannel {

	boolean open = true;
	private final ByteBuffer buf;

	public SeekableByteChannelMock(ByteBuffer buf) {
		this.buf = buf;
	}

	@Override
	public boolean isOpen() {
		return open;
	}

	@Override
	public void close() {
		open = false;
	}

	@Override
	public int read(ByteBuffer dst) throws IOException {
		if (!buf.hasRemaining()) {
			return -1;
		} else {
			int num = Math.min(buf.remaining(), dst.remaining());
			ByteBuffer limitedSrc = buf.asReadOnlyBuffer();
			limitedSrc.limit(limitedSrc.position() + num);
			dst.put(limitedSrc);
			buf.position(limitedSrc.position());
			return num;
		}
	}

	@Override
	public int write(ByteBuffer src) {
		int num = Math.min(buf.remaining(), src.remaining());
		ByteBuffer limitedSrc = src.asReadOnlyBuffer();
		limitedSrc.limit(limitedSrc.position() + num);
		buf.put(limitedSrc);
		return num;
	}

	@Override
	public long position() {
		return buf.position();
	}

	@Override
	public SeekableByteChannel position(long newPosition) {
		assert newPosition < Integer.MAX_VALUE;
		buf.position((int) newPosition);
		return this;
	}

	@Override
	public long size() throws IOException {
		return buf.limit();
	}

	@Override
	public SeekableByteChannel truncate(long size) {
		assert size < Integer.MAX_VALUE;
		if (size < buf.position()) {
			buf.position((int) size);
		}
		buf.limit((int) size);
		return this;
	}

} static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM static final int GCM_TAG_SIZE = 16;
import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.cryptomator.cryptolib.common.SeekableByteChannelMock;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.cryptomator.cryptolib.v2.Constants.GCM_NONCE_SIZE;
import static org.cryptomator.cryptolib.v2.Constants.GCM_TAG_SIZE;
/*******************************************************************************
 * Copyright (c) 2016 Sebastian Stenzel and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the accompanying LICENSE.txt.
 *
 * Contributors:
 *     Sebastian Stenzel - initial API and implementation
 *******************************************************************************/
package org.cryptomator.cryptolib.v2;




public class FileContentCryptorImplTest {

	// AES-GCM implementation requires non-repeating nonces, still we need deterministic nonces for testing
	private static final SecureRandom CSPRNG = Mockito.spy(SecureRandomMock.cycle((byte) 0xF0, (byte) 0x0F));

	private FileHeaderImpl header;
	private FileHeaderCryptorImpl headerCryptor;
	private FileContentCryptorImpl fileContentCryptor;
	private Cryptor cryptor;

	@BeforeEach
	public void setup() {
		Masterkey masterkey = new Masterkey(new byte[64]);
		header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]));
		headerCryptor = new FileHeaderCryptorImpl(masterkey, CSPRNG);
		fileContentCryptor = new FileContentCryptorImpl(CSPRNG);
		cryptor = Mockito.mock(Cryptor.class);
		Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor);
		Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor);
	}

	@Test
	public void testDecryptedEncryptedEqualsPlaintext() throws AuthenticationFailedException {
		DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES");