MultiCipherOutputStream.java

/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.crypto;

import static org.syncany.crypto.CipherParams.CRYPTO_PROVIDER_ID;

import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

import javax.crypto.Mac;

import org.syncany.crypto.specs.HmacSha256CipherSpec;

/**
 * Implements an output stream that encrypts the underlying output
 * stream using one to many ciphers. 
 * 
 * Format:
 * <pre>
 *    Length           HMAC'd           Description
 *    ----------------------------------------------
 *    04               no               "Sy" 0x02 0x05 (4 bytes)
 *    01               no               Version (1 byte)
 *    12               no               HMAC salt             
 *    01               yes (in header)  Cipher count (=n, 1 byte)
 *    
 *    for i := 0..n-1:
 *      01             yes (in header)  Cipher spec ID (1 byte)
 *      12             yes (in header)  Salt for cipher i (12 bytes)
 *      aa             yes (in header)  IV for cipher i (cipher specific length, 0..x)
 *      
 *    20               no               Header HMAC (20 bytes, for "HmacSHA1")
 *    bb               yes (in mode)    Ciphertext (HMAC'd by mode, e.g. GCM)
 * </pre>
 * 
 * It follows a few Do's and Don'ts:
 * - http://blog.cryptographyengineering.com/2011/11/how-not-to-use-symmetric-encryption.html
 * - http://security.stackexchange.com/questions/30170/after-how-much-data-encryption-aes-256-we-should-change-key
 * 
 * Encryption and cipher rules
 * - Don't encrypt with ECB mode (throws exception if ECB is used)
 * - Don't re-use your IVs (IVs are never reused)
 * - Don't encrypt your IVs (IVs are prepended)
 * - Authenticate cipher configuration (algorithm, salts and IVs)
 * - Only use authenticated ciphers
 */
public class MultiCipherOutputStream extends OutputStream {
	public static final byte[] STREAM_MAGIC = new byte[] { 0x53, 0x79, 0x02, 0x05 };
	public static final byte STREAM_VERSION = 1;

	public static final int SALT_SIZE = 12;	
	public static final CipherSpec HMAC_SPEC = new HmacSha256CipherSpec();
	
	private OutputStream underlyingOutputStream;
	
	private List<CipherSpec> cipherSpecs;
	private CipherSession cipherSession;
	private OutputStream cipherOutputStream;

	private boolean headerWritten;	
	private Mac headerHmac;
	
	public MultiCipherOutputStream(OutputStream out, List<CipherSpec> cipherSpecs, CipherSession cipherSession) throws IOException {
		this.underlyingOutputStream = out;	
		
		this.cipherSpecs = cipherSpecs;		
		this.cipherSession = cipherSession;		
		this.cipherOutputStream = null;
		
		this.headerWritten = false;
		this.headerHmac = null;		
	}
	
	@Override
	public void write(int b) throws IOException {
		writeHeader();
		cipherOutputStream.write(b);		
	}
	
	@Override
	public void write(byte[] b) throws IOException {
		writeHeader();
		cipherOutputStream.write(b, 0, b.length);
	}
	
	@Override
	public void write(byte[] b, int off, int len) throws IOException {
		writeHeader();
		cipherOutputStream.write(b, off, len);
	}
	
	@Override
	public void close() throws IOException {
		cipherOutputStream.close();
	}
		
	private void writeHeader() throws IOException {
		if (!headerWritten) {
			try {
				// Initialize header HMAC
				SaltedSecretKey hmacSecretKey = cipherSession.getWriteSecretKey(HMAC_SPEC);

				headerHmac = Mac.getInstance(HMAC_SPEC.getAlgorithm(), CRYPTO_PROVIDER_ID);
				headerHmac.init(hmacSecretKey);

				// Write header
				writeNoHmac(underlyingOutputStream, STREAM_MAGIC);
				writeNoHmac(underlyingOutputStream, STREAM_VERSION);
				writeNoHmac(underlyingOutputStream, hmacSecretKey.getSalt());			
				writeAndUpdateHmac(underlyingOutputStream, cipherSpecs.size());

				cipherOutputStream = underlyingOutputStream;

				for (CipherSpec cipherSpec : cipherSpecs) { 
					SaltedSecretKey saltedSecretKey = cipherSession.getWriteSecretKey(cipherSpec);				
					byte[] iv = CipherUtil.createRandomArray(cipherSpec.getIvSize()/8);

					writeAndUpdateHmac(underlyingOutputStream, cipherSpec.getId());
					writeAndUpdateHmac(underlyingOutputStream, saltedSecretKey.getSalt());
					writeAndUpdateHmac(underlyingOutputStream, iv);

					cipherOutputStream = cipherSpec.newCipherOutputStream(cipherOutputStream, saltedSecretKey.getEncoded(), iv);	        
				}	

				writeNoHmac(underlyingOutputStream, headerHmac.doFinal());
			}
			catch (Exception e) {
				throw new IOException(e);
			}	
			headerWritten = true;
		}
	}	

	private void writeNoHmac(OutputStream outputStream, byte[] bytes) throws IOException {
		outputStream.write(bytes);
	}

	private void writeNoHmac(OutputStream outputStream, int abyte) throws IOException {
		outputStream.write(abyte);
	}	
	
	private void writeAndUpdateHmac(OutputStream outputStream, byte[] bytes) throws IOException {
		writeNoHmac(outputStream, bytes);
		headerHmac.update(bytes);
	}

	private void writeAndUpdateHmac(OutputStream outputStream, int abyte) throws IOException {
		writeNoHmac(outputStream, abyte);
		headerHmac.update((byte) abyte);
	}	
}