/*
* Copyright 2016 The Simple File Server Authors
*
* Licensed 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 org.sfs.encryption;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.StreamCipher;
import org.bouncycastle.crypto.engines.AESFastEngine;
import org.bouncycastle.crypto.io.InvalidCipherTextIOException;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
public class CipherWriteStreamValidation {
public static final int KEY_SIZE_BYTES = 32;
public static final int NONCE_SIZE_BYTES = 12;
private static final int MAC_SIZE_BITS = 96;
private byte[] salt;
private final GCMBlockCipher encryptor;
private final GCMBlockCipher decryptor;
public CipherWriteStreamValidation(byte[] secretBytes, byte[] salt) {
this.salt = salt.clone();
secretBytes = secretBytes.clone();
if (secretBytes.length != KEY_SIZE_BYTES) {
secretBytes = Hashing.sha256().hashBytes(secretBytes).asBytes();
}
try {
KeyParameter key = new KeyParameter(secretBytes);
AEADParameters params = new AEADParameters(key, MAC_SIZE_BITS, this.salt);
this.encryptor = new GCMBlockCipher(new AESFastEngine());
this.encryptor.init(true, params);
this.decryptor = new GCMBlockCipher(new AESFastEngine());
this.decryptor.init(false, params);
} catch (Exception e) {
throw new RuntimeException("could not create cipher for AES256", e);
} finally {
Arrays.fill(secretBytes, (byte) 0);
}
}
public byte[] getSalt() {
return salt.clone();
}
public byte[] decrypt(byte[] data) {
if (data == null) {
return null;
}
try (InputStream inputStream = decrypt(new ByteArrayInputStream(data))) {
return ByteStreams.toByteArray(inputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public byte[] encrypt(byte[] data) {
if (data == null) {
return null;
}
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
try (OutputStream outputStream = encrypt(byteArrayOutputStream)) {
outputStream.write(data);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public InputStream decrypt(InputStream inputStream) {
return new CipherInputStream(inputStream, decryptor);
}
public OutputStream encrypt(OutputStream outputStream) {
return new CipherOutputStream(outputStream, encryptor);
}
/**
* A CipherInputStream is composed of an InputStream and a cipher so that read() methods return data
* that are read in from the underlying InputStream but have been additionally processed by the
* Cipher. The cipher must be fully initialized before being used by a CipherInputStream.
* <p/>
* For example, if the Cipher is initialized for decryption, the
* CipherInputStream will attempt to read in data and decrypt them,
* before returning the decrypted data.
*/
private static class CipherInputStream
extends FilterInputStream {
private BufferedBlockCipher bufferedBlockCipher;
private StreamCipher streamCipher;
private AEADBlockCipher aeadBlockCipher;
private final byte[] buf;
private final byte[] inBuf;
private int bufOff;
private int maxBuf;
private boolean finalized;
private static final int INPUT_BUF_SIZE = 2048;
/**
* Constructs a CipherInputStream from an InputStream and a
* BufferedBlockCipher.
*/
public CipherInputStream(
InputStream is,
BufferedBlockCipher cipher) {
super(is);
this.bufferedBlockCipher = cipher;
int outSize = cipher.getOutputSize(INPUT_BUF_SIZE);
buf = new byte[(outSize > INPUT_BUF_SIZE) ? outSize : INPUT_BUF_SIZE];
inBuf = new byte[INPUT_BUF_SIZE];
}
public CipherInputStream(
InputStream is,
StreamCipher cipher) {
super(is);
this.streamCipher = cipher;
buf = new byte[INPUT_BUF_SIZE];
inBuf = new byte[INPUT_BUF_SIZE];
}
/**
* Constructs a CipherInputStream from an InputStream and an AEADBlockCipher.
*/
public CipherInputStream(InputStream is, AEADBlockCipher cipher) {
super(is);
this.aeadBlockCipher = cipher;
int outSize = cipher.getOutputSize(INPUT_BUF_SIZE);
buf = new byte[(outSize > INPUT_BUF_SIZE) ? outSize : INPUT_BUF_SIZE];
inBuf = new byte[INPUT_BUF_SIZE];
}
/**
* Read data from underlying stream and process with cipher until end of stream or some data is
* available after cipher processing.
*
* @return -1 to indicate end of stream, or the number of bytes (> 0) available.
*/
private int nextChunk()
throws IOException {
if (finalized) {
return -1;
}
bufOff = 0;
maxBuf = 0;
// Keep reading until EOF or cipher processing produces data
while (maxBuf == 0) {
int read = in.read(inBuf);
if (read == -1) {
finaliseCipher();
if (maxBuf == 0) {
return -1;
}
return maxBuf;
}
try {
if (bufferedBlockCipher != null) {
maxBuf = bufferedBlockCipher.processBytes(inBuf, 0, read, buf, 0);
} else if (aeadBlockCipher != null) {
maxBuf = aeadBlockCipher.processBytes(inBuf, 0, read, buf, 0);
} else {
streamCipher.processBytes(inBuf, 0, read, buf, 0);
maxBuf = read;
}
} catch (Exception e) {
throw new IOException("Error processing stream " + e);
}
}
return maxBuf;
}
private void finaliseCipher()
throws IOException {
try {
finalized = true;
if (bufferedBlockCipher != null) {
maxBuf = bufferedBlockCipher.doFinal(buf, 0);
} else if (aeadBlockCipher != null) {
maxBuf = aeadBlockCipher.doFinal(buf, 0);
} else {
maxBuf = 0; // a stream cipher
}
} catch (final InvalidCipherTextException e) {
throw new InvalidCipherTextIOException("Error finalising cipher", e);
} catch (Exception e) {
throw new IOException("Error finalising cipher " + e);
}
}
/**
* Reads data from the underlying stream and processes it with the cipher until the cipher
* outputs data, and returns the next available byte.
* <p/>
* If the underlying stream is exhausted by this call, the cipher will be finalised.
*
* @throws java.io.IOException if there was an error closing the input stream.
* @throws org.bouncycastle.crypto.io.InvalidCipherTextIOException if the data read from the stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public int read()
throws IOException {
if (bufOff >= maxBuf) {
if (nextChunk() < 0) {
return -1;
}
}
return buf[bufOff++] & 0xff;
}
/**
* Reads data from the underlying stream and processes it with the cipher until the cipher
* outputs data, and then returns up to <code>b.length</code> bytes in the provided array.
* <p/>
* If the underlying stream is exhausted by this call, the cipher will be finalised.
*
* @param b the buffer into which the data is read.
* @return the total number of bytes read into the buffer, or <code>-1</code> if there is no
* more data because the end of the stream has been reached.
* @throws java.io.IOException if there was an error closing the input stream.
* @throws org.bouncycastle.crypto.io.InvalidCipherTextIOException if the data read from the stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public int read(
byte[] b)
throws IOException {
return read(b, 0, b.length);
}
/**
* Reads data from the underlying stream and processes it with the cipher until the cipher
* outputs data, and then returns up to <code>len</code> bytes in the provided array.
* <p/>
* If the underlying stream is exhausted by this call, the cipher will be finalised.
*
* @param b the buffer into which the data is read.
* @param off the set offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or <code>-1</code> if there is no
* more data because the end of the stream has been reached.
* @throws java.io.IOException if there was an error closing the input stream.
* @throws org.bouncycastle.crypto.io.InvalidCipherTextIOException if the data read from the stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public int read(
byte[] b,
int off,
int len)
throws IOException {
if (bufOff >= maxBuf) {
if (nextChunk() < 0) {
return -1;
}
}
int toSupply = Math.min(len, available());
System.arraycopy(buf, bufOff, b, off, toSupply);
bufOff += toSupply;
return toSupply;
}
public long skip(
long n)
throws IOException {
if (n <= 0) {
return 0;
}
int skip = (int) Math.min(n, available());
bufOff += skip;
return skip;
}
public int available()
throws IOException {
return maxBuf - bufOff;
}
/**
* Closes the underlying input stream and finalises the processing of the data by the cipher.
*
* @throws java.io.IOException if there was an error closing the input stream.
* @throws org.bouncycastle.crypto.io.InvalidCipherTextIOException if the data read from the stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public void close()
throws IOException {
try {
in.close();
} finally {
if (!finalized) {
// Reset the cipher, discarding any data buffered in it
// Errors in cipher finalisation trump I/O error closing input
finaliseCipher();
}
}
maxBuf = bufOff = 0;
}
public void mark(int readlimit) {
}
public void reset()
throws IOException {
}
public boolean markSupported() {
return false;
}
}
private static class CipherOutputStream
extends FilterOutputStream {
private BufferedBlockCipher bufferedBlockCipher;
private StreamCipher streamCipher;
private AEADBlockCipher aeadBlockCipher;
private final byte[] oneByte = new byte[1];
private byte[] buf;
/**
* Constructs a CipherOutputStream from an OutputStream and a
* BufferedBlockCipher.
*/
public CipherOutputStream(
OutputStream os,
BufferedBlockCipher cipher) {
super(os);
this.bufferedBlockCipher = cipher;
}
/**
* Constructs a CipherOutputStream from an OutputStream and a
* BufferedBlockCipher.
*/
public CipherOutputStream(
OutputStream os,
StreamCipher cipher) {
super(os);
this.streamCipher = cipher;
}
/**
* Constructs a CipherOutputStream from an OutputStream and a AEADBlockCipher.
*/
public CipherOutputStream(OutputStream os, AEADBlockCipher cipher) {
super(os);
this.aeadBlockCipher = cipher;
}
/**
* Writes the specified byte to this output stream.
*
* @param b the <code>byte</code>.
* @throws java.io.IOException if an I/O error occurs.
*/
public void write(
int b)
throws IOException {
oneByte[0] = (byte) b;
if (streamCipher != null) {
out.write(streamCipher.returnByte((byte) b));
} else {
write(oneByte, 0, 1);
}
}
/**
* Writes <code>b.length</code> bytes from the specified byte array
* to this output stream.
* <p/>
* The <code>write</code> method of
* <code>CipherOutputStream</code> calls the <code>write</code>
* method of three arguments with the three arguments
* <code>b</code>, <code>0</code>, and <code>b.length</code>.
*
* @param b the data.
* @throws java.io.IOException if an I/O error occurs.
* @see #write(byte[], int, int)
*/
public void write(
byte[] b)
throws IOException {
write(b, 0, b.length);
}
/**
* Writes <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> to this output stream.
*
* @param b the data.
* @param off the set offset in the data.
* @param len the number of bytes to write.
* @throws java.io.IOException if an I/O error occurs.
*/
public void write(
byte[] b,
int off,
int len)
throws IOException {
ensureCapacity(len);
if (bufferedBlockCipher != null) {
int outLen = bufferedBlockCipher.processBytes(b, off, len, buf, 0);
if (outLen != 0) {
out.write(buf, 0, outLen);
}
} else if (aeadBlockCipher != null) {
int outLen = aeadBlockCipher.processBytes(b, off, len, buf, 0);
if (outLen != 0) {
out.write(buf, 0, outLen);
}
} else {
streamCipher.processBytes(b, off, len, buf, 0);
out.write(buf, 0, len);
}
}
/**
* Ensure the ciphertext buffer has space sufficient to accept an upcoming output.
*
* @param outputSize the size of the pending update.
*/
private void ensureCapacity(int outputSize) {
// This overestimates buffer on updates for AEAD/padded, but keeps it simple.
int bufLen;
if (bufferedBlockCipher != null) {
bufLen = bufferedBlockCipher.getOutputSize(outputSize);
} else if (aeadBlockCipher != null) {
bufLen = aeadBlockCipher.getOutputSize(outputSize);
} else {
bufLen = outputSize;
}
if ((buf == null) || (buf.length < bufLen)) {
buf = new byte[bufLen];
}
}
/**
* Flushes this output stream by forcing any buffered output bytes
* that have already been processed by the encapsulated cipher object
* to be written out.
* <p/>
* <p/>
* Any bytes buffered by the encapsulated cipher
* and waiting to be processed by it will not be written out. For example,
* if the encapsulated cipher is a block cipher, and the total number of
* bytes written using one of the <code>write</code> methods is less than
* the cipher's block size, no bytes will be written out.
*
* @throws java.io.IOException if an I/O error occurs.
*/
public void flush()
throws IOException {
out.flush();
}
/**
* Closes this output stream and releases any system resources
* associated with this stream.
* <p/>
* This method invokes the <code>doFinal</code> method of the encapsulated
* cipher object, which causes any bytes buffered by the encapsulated
* cipher to be processed. The result is written out by calling the
* <code>flush</code> method of this output stream.
* <p/>
* This method resets the encapsulated cipher object to its initial state
* and calls the <code>close</code> method of the underlying output
* stream.
*
* @throws java.io.IOException if an I/O error occurs.
* @throws org.bouncycastle.crypto.io.InvalidCipherTextIOException if the data written to this stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public void close()
throws IOException {
ensureCapacity(0);
IOException error = null;
try {
if (bufferedBlockCipher != null) {
int outLen = bufferedBlockCipher.doFinal(buf, 0);
if (outLen != 0) {
out.write(buf, 0, outLen);
}
} else if (aeadBlockCipher != null) {
int outLen = aeadBlockCipher.doFinal(buf, 0);
if (outLen != 0) {
out.write(buf, 0, outLen);
}
}
} catch (final InvalidCipherTextException e) {
error = new InvalidCipherTextIOException("Error finalising cipher data", e);
} catch (Exception e) {
error = new IOException("Error closing stream: " + e);
}
try {
flush();
out.close();
} catch (IOException e) {
// Invalid ciphertext takes precedence over close error
if (error == null) {
error = e;
}
}
if (error != null) {
throw error;
}
}
}
}