/**
* Copyright 2012, Board of Regents of the University of
* Wisconsin System. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Board of Regents of the University of Wisconsin
* System 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 edu.wisc.doit.tcrypt;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.security.KeyPair;
import java.util.regex.Pattern;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CloseShieldInputStream;
import org.apache.commons.io.output.TeeOutputStream;
import org.bouncycastle.crypto.AsymmetricBlockCipher;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.GeneralDigest;
import org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.io.CipherOutputStream;
import org.bouncycastle.crypto.io.DigestInputStream;
import org.bouncycastle.crypto.io.DigestOutputStream;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.openssl.PEMKeyPair;
/**
* Decrypts whole files contained in specially formatted TAR files.
* <br/>
* The first entry in the tar file must be named {@link #KEYFILE_ENC_NAME} and contain two lines.
* The file line is the secretKey and the second line is the initVector used in the block cipher
* that encrypted the second entry in the tar file.
* <br/>
* The block cipher must be AES with CBC. The AES strenth is determined by the initVector.
*
*
* @author Eric Dalquist
* @version $Revision: 187 $
*/
public class BouncyCastleFileDecrypter extends AbstractPublicKeyDecrypter implements FileDecrypter {
public static final Pattern KEYFILE_SEPERATOR_PATTERN = Pattern.compile(Character.toString(FileEncrypter.KEYFILE_LINE_SEPERATOR));
public static final int MAX_ENCRYPTED_KEY_FILE_SIZE = 500;
public BouncyCastleFileDecrypter(AsymmetricKeyParameter publicKeyParam, AsymmetricKeyParameter privateKeyParam) {
super(publicKeyParam, privateKeyParam);
}
public BouncyCastleFileDecrypter(KeyPair keyPair) throws IOException {
super(keyPair);
}
public BouncyCastleFileDecrypter(PEMKeyPair keyPair) throws IOException {
super(keyPair);
}
public BouncyCastleFileDecrypter(Reader privateKeyReader) throws IOException {
super(privateKeyReader);
}
@Override
public void decrypt(InputStream inputStream, OutputStream outputStream) throws InvalidCipherTextException, IOException, DecoderException {
final TarArchiveInputStream tarInputStream = new TarArchiveInputStream(inputStream, FileEncrypter.ENCODING);
final BufferedBlockCipher cipher = createCipher(tarInputStream);
//Advance to the next entry in the tar file
tarInputStream.getNextTarEntry();
//Create digest output stream used to generate digest while decrypting
final DigestOutputStream digestOutputStream = new DigestOutputStream(this.createDigester());
//Do a streaming decryption of the file output
final CipherOutputStream cipherOutputStream = new CipherOutputStream(new TeeOutputStream(outputStream, digestOutputStream), cipher);
IOUtils.copy(tarInputStream, cipherOutputStream);
cipherOutputStream.close();
//Capture the hash of the decrypted output
final byte[] hashBytes = digestOutputStream.getDigest();
verifyOutputHash(tarInputStream, hashBytes);
}
@Override
public InputStream decrypt(InputStream inputStream) throws InvalidCipherTextException, IOException, DecoderException {
final TarArchiveInputStream tarInputStream = new TarArchiveInputStream(inputStream, FileEncrypter.ENCODING);
final BufferedBlockCipher cipher = createCipher(tarInputStream);
//Advance to the next entry in the tar file
tarInputStream.getNextTarEntry();
//Protect the underlying TAR stream from being closed by the cipher stream
final CloseShieldInputStream is = new CloseShieldInputStream(tarInputStream);
//Setup the decrypting cipher stream
final CipherInputStream stream = new CipherInputStream(is, cipher);
//Generate a digest of the decrypted data
final GeneralDigest digest = this.createDigester();
final DigestInputStream digestInputStream = new DigestInputStream(stream, digest);
return new DecryptingInputStream(digestInputStream, tarInputStream, digest);
}
protected BufferedBlockCipher createCipher(final TarArchiveInputStream tarInputStream) throws IOException, InvalidCipherTextException, DecoderException {
//Read the cipher parameters from the tar file
final CipherParameters key = this.getCipherParameters(tarInputStream);
//Get the block cipher used for decrypting
return getDecryptBlockCipher(key);
}
protected void verifyOutputHash(final TarArchiveInputStream tarInputStream, final byte[] hashBytes) throws InvalidCipherTextException, IOException {
final String actualHash = new String(Base64.encodeBase64(hashBytes), FileEncrypter.CHARSET);
//Get the expected hash and verify
final String expectedHash = this.getExpectedHash(tarInputStream);
if (!expectedHash.equals(actualHash)) {
throw new IllegalArgumentException("Hash " + actualHash + " doesn't match expected hash " + expectedHash + " for decrypted file. The data written to the OutputStream should be discarded.");
}
}
protected String getExpectedHash(TarArchiveInputStream inputStream) throws InvalidCipherTextException, IOException {
return readAndDecrypt(inputStream, FileEncrypter.HASHFILE_ENC_NAME).trim();
}
protected CipherParameters getCipherParameters(TarArchiveInputStream inputStream) throws IOException, InvalidCipherTextException, DecoderException {
final String keyFileStr = readAndDecrypt(inputStream, FileEncrypter.KEYFILE_ENC_NAME);
//Split the keyfile
final String[] keyFileParts = KEYFILE_SEPERATOR_PATTERN.split(keyFileStr);
if (keyFileParts.length != 2) {
throw new IllegalArgumentException(FileEncrypter.KEYFILE_ENC_NAME + " must have exactly two lines, this one has: " + keyFileParts.length);
}
//line 0 is the secretKey, 1 is the initVector, 2 is the file md5
final byte[] secretKey = Hex.decodeHex(keyFileParts[0].toCharArray());
final byte[] initVector = Hex.decodeHex(keyFileParts[1].toCharArray());
//Create the key parameters
final KeyParameter keyParam = new KeyParameter(secretKey);
return new ParametersWithIV(keyParam, initVector);
}
protected String readAndDecrypt(TarArchiveInputStream inputStream, final String fileName) throws IOException, InvalidCipherTextException {
//Read keyfile.enc from the TAR
final TarArchiveEntry keyFileEntry = inputStream.getNextTarEntry();
//Verify file name
final String keyFileName = keyFileEntry.getName();
if (!fileName.equals(keyFileName)) {
throw new IllegalArgumentException("The first entry in the TAR must be name: " + fileName);
}
//Verify file size
if (keyFileEntry.getSize() > MAX_ENCRYPTED_KEY_FILE_SIZE) {
throw new IllegalArgumentException("The encrypted archive's key file cannot be longer than " + MAX_ENCRYPTED_KEY_FILE_SIZE + " bytes");
}
//Decode the base64 keyfile
final byte[] encKeyFileBase64Bytes = IOUtils.toByteArray(inputStream);
final byte[] encKeyFileBytes = Base64.decodeBase64(encKeyFileBase64Bytes);
//Decrypt the keyfile into UTF-8 String
final AsymmetricBlockCipher decryptCipher = this.getDecryptCipher();
final byte[] keyFileBytes = decryptCipher.processBlock(encKeyFileBytes, 0, encKeyFileBytes.length);
return new String(keyFileBytes, FileEncrypter.CHARSET);
}
private final class DecryptingInputStream extends FilterInputStream {
private final TarArchiveInputStream tarInputStream;
private final Digest digest;
private DecryptingInputStream(InputStream is, TarArchiveInputStream tarInputStream, Digest digest) {
super(is);
this.tarInputStream = tarInputStream;
this.digest = digest;
}
@Override
public void close() throws IOException {
//Complete the decryption
super.close();
//Verify the decrypted output
byte[] hashBytes = new byte[digest.getDigestSize()];
digest.doFinal(hashBytes, 0);
try {
verifyOutputHash(tarInputStream, hashBytes);
}
catch (InvalidCipherTextException e) {
throw new IOException(e);
}
}
}
}