/* Copyright 2014 Duncan Jones * * 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.cryptonode.jncryptor; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.security.GeneralSecurityException; import javax.crypto.Cipher; import javax.crypto.CipherOutputStream; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; /** * Writes RNCryptor-format (version 3) data in a stream fashion. The stream must * be closed to finish writing the data and output the HMAC value. * * @since 1.1.0 */ public class AES256JNCryptorOutputStream extends OutputStream { private CipherOutputStream cipherStream; private MacOutputStream macOutputStream; private boolean writtenHeader; private final boolean passwordBased; private byte[] encryptionSalt; private byte[] iv; private byte[] hmacSalt; /** * Creates an output stream for key-encrypted data. * * @param out * the {@code OutputStream} to write the JNCryptor data to * @param encryptionKey * the key to encrypt with * @param hmacKey * the key to calculate the HMAC with */ public AES256JNCryptorOutputStream(OutputStream out, SecretKey encryptionKey, SecretKey hmacKey) throws CryptorException { Validate.notNull(out, "Output stream cannot be null."); Validate.notNull(encryptionKey, "Encryption key cannot be null."); Validate.notNull(hmacKey, "HMAC key cannot be null."); byte[] iv = AES256JNCryptor .getSecureRandomData(AES256Ciphertext.AES_BLOCK_SIZE); passwordBased = false; createStreams(encryptionKey, hmacKey, iv, out); } /** * Creates an output stream for password-encrypted data, using a specific * number of PBKDF iterations. * * @param out * the {@code OutputStream} to write the JNCryptor data to * @param password * the password * @param iterations * the number of PBKDF iterations to perform */ public AES256JNCryptorOutputStream(OutputStream out, char[] password, int iterations) throws CryptorException { Validate.notNull(out, "Output stream cannot be null."); Validate.notNull(password, "Password cannot be null."); Validate.isTrue(password.length > 0, "Password cannot be empty."); Validate.isTrue(iterations > 0, "Iterations must be greater than zero."); AES256JNCryptor cryptor = new AES256JNCryptor(iterations); encryptionSalt = AES256JNCryptor .getSecureRandomData(AES256JNCryptor.SALT_LENGTH); SecretKey encryptionKey = cryptor.keyForPassword(password, encryptionSalt); hmacSalt = AES256JNCryptor.getSecureRandomData(AES256JNCryptor.SALT_LENGTH); SecretKey hmacKey = cryptor.keyForPassword(password, hmacSalt); iv = AES256JNCryptor.getSecureRandomData(AES256Ciphertext.AES_BLOCK_SIZE); passwordBased = true; createStreams(encryptionKey, hmacKey, iv, out); } /** * Creates an output stream for password-encrypted data. * * @param out * the {@code OutputStream} to write the JNCryptor data to * @param password * the password */ public AES256JNCryptorOutputStream(OutputStream out, char[] password) throws CryptorException { this(out, password, AES256JNCryptor.PBKDF_DEFAULT_ITERATIONS); } /** * Creates the cipher and MAC streams required, * * @param encryptionKey * the encryption key * @param hmacKey * the HMAC key * @param iv * the IV * @param out * the output stream we are wrapping * @throws CryptorException */ private void createStreams(SecretKey encryptionKey, SecretKey hmacKey, byte[] iv, OutputStream out) throws CryptorException { this.iv = iv; try { Cipher cipher = Cipher.getInstance(AES256JNCryptor.AES_CIPHER_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv)); try { Mac mac = Mac.getInstance(AES256JNCryptor.HMAC_ALGORITHM); mac.init(hmacKey); macOutputStream = new MacOutputStream(out, mac); cipherStream = new CipherOutputStream(macOutputStream, cipher); } catch (GeneralSecurityException e) { throw new CryptorException("Failed to initialize HMac", e); } } catch (GeneralSecurityException e) { throw new CryptorException("Failed to initialize AES cipher", e); } } /** * Writes the header data to the output stream. * * @throws IOException */ private void writeHeader() throws IOException { /* Write out the header */ if (passwordBased) { macOutputStream.write(AES256JNCryptor.VERSION); macOutputStream.write(AES256Ciphertext.FLAG_PASSWORD); macOutputStream.write(encryptionSalt); macOutputStream.write(hmacSalt); macOutputStream.write(iv); } else { macOutputStream.write(AES256JNCryptor.VERSION); macOutputStream.write(0); macOutputStream.write(iv); } } /** * Writes one byte to the encrypted output stream. * * @param b * the byte to write * @throws IOException * if an I/O error occurs */ @Override public void write(int b) throws IOException { if (!writtenHeader) { writeHeader(); writtenHeader = true; } cipherStream.write(b); } /** * Writes bytes to the encrypted output stream. * * @param b * a buffer of bytes to write * @param off * the offset into the buffer * @param len * the number of bytes to write (starting from the offset) * @throws IOException * if an I/O error occurs */ @Override public void write(byte[] b, int off, int len) throws IOException { if (!writtenHeader) { writeHeader(); writtenHeader = true; } cipherStream.write(b, off, len); } /** * Closes the stream. This causes the HMAC calculation to be concluded and * written to the output. * * @throws IOException * if an I/O error occurs */ @Override public void close() throws IOException { cipherStream.close(); } /** * An output stream to update a Mac object with all bytes passed through, then * write the Mac data to the stream upon close to complete the RNCryptor file * format. */ private static class MacOutputStream extends FilterOutputStream { private final Mac mac; MacOutputStream(OutputStream out, Mac mac) { super(out); this.mac = mac; } @Override public void write(int b) throws IOException { mac.update((byte) b); out.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { mac.update(b, off, len); out.write(b, off, len); } @Override public void close() throws IOException { byte[] macData = mac.doFinal(); out.write(macData); out.flush(); out.close(); } } }