001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.crypto; 019 020import static org.syncany.crypto.CipherParams.CRYPTO_PROVIDER_ID; 021 022import java.io.IOException; 023import java.io.OutputStream; 024import java.util.List; 025 026import javax.crypto.Mac; 027 028import org.syncany.crypto.specs.HmacSha256CipherSpec; 029 030/** 031 * Implements an output stream that encrypts the underlying output 032 * stream using one to many ciphers. 033 * 034 * Format: 035 * <pre> 036 * Length HMAC'd Description 037 * ---------------------------------------------- 038 * 04 no "Sy" 0x02 0x05 (4 bytes) 039 * 01 no Version (1 byte) 040 * 12 no HMAC salt 041 * 01 yes (in header) Cipher count (=n, 1 byte) 042 * 043 * for i := 0..n-1: 044 * 01 yes (in header) Cipher spec ID (1 byte) 045 * 12 yes (in header) Salt for cipher i (12 bytes) 046 * aa yes (in header) IV for cipher i (cipher specific length, 0..x) 047 * 048 * 20 no Header HMAC (20 bytes, for "HmacSHA1") 049 * bb yes (in mode) Ciphertext (HMAC'd by mode, e.g. GCM) 050 * </pre> 051 * 052 * It follows a few Do's and Don'ts: 053 * - http://blog.cryptographyengineering.com/2011/11/how-not-to-use-symmetric-encryption.html 054 * - http://security.stackexchange.com/questions/30170/after-how-much-data-encryption-aes-256-we-should-change-key 055 * 056 * Encryption and cipher rules 057 * - Don't encrypt with ECB mode (throws exception if ECB is used) 058 * - Don't re-use your IVs (IVs are never reused) 059 * - Don't encrypt your IVs (IVs are prepended) 060 * - Authenticate cipher configuration (algorithm, salts and IVs) 061 * - Only use authenticated ciphers 062 */ 063public class MultiCipherOutputStream extends OutputStream { 064 public static final byte[] STREAM_MAGIC = new byte[] { 0x53, 0x79, 0x02, 0x05 }; 065 public static final byte STREAM_VERSION = 1; 066 067 public static final int SALT_SIZE = 12; 068 public static final CipherSpec HMAC_SPEC = new HmacSha256CipherSpec(); 069 070 private OutputStream underlyingOutputStream; 071 072 private List<CipherSpec> cipherSpecs; 073 private CipherSession cipherSession; 074 private OutputStream cipherOutputStream; 075 076 private boolean headerWritten; 077 private Mac headerHmac; 078 079 public MultiCipherOutputStream(OutputStream out, List<CipherSpec> cipherSpecs, CipherSession cipherSession) throws IOException { 080 this.underlyingOutputStream = out; 081 082 this.cipherSpecs = cipherSpecs; 083 this.cipherSession = cipherSession; 084 this.cipherOutputStream = null; 085 086 this.headerWritten = false; 087 this.headerHmac = null; 088 } 089 090 @Override 091 public void write(int b) throws IOException { 092 writeHeader(); 093 cipherOutputStream.write(b); 094 } 095 096 @Override 097 public void write(byte[] b) throws IOException { 098 writeHeader(); 099 cipherOutputStream.write(b, 0, b.length); 100 } 101 102 @Override 103 public void write(byte[] b, int off, int len) throws IOException { 104 writeHeader(); 105 cipherOutputStream.write(b, off, len); 106 } 107 108 @Override 109 public void close() throws IOException { 110 cipherOutputStream.close(); 111 } 112 113 private void writeHeader() throws IOException { 114 if (!headerWritten) { 115 try { 116 // Initialize header HMAC 117 SaltedSecretKey hmacSecretKey = cipherSession.getWriteSecretKey(HMAC_SPEC); 118 119 headerHmac = Mac.getInstance(HMAC_SPEC.getAlgorithm(), CRYPTO_PROVIDER_ID); 120 headerHmac.init(hmacSecretKey); 121 122 // Write header 123 writeNoHmac(underlyingOutputStream, STREAM_MAGIC); 124 writeNoHmac(underlyingOutputStream, STREAM_VERSION); 125 writeNoHmac(underlyingOutputStream, hmacSecretKey.getSalt()); 126 writeAndUpdateHmac(underlyingOutputStream, cipherSpecs.size()); 127 128 cipherOutputStream = underlyingOutputStream; 129 130 for (CipherSpec cipherSpec : cipherSpecs) { 131 SaltedSecretKey saltedSecretKey = cipherSession.getWriteSecretKey(cipherSpec); 132 byte[] iv = CipherUtil.createRandomArray(cipherSpec.getIvSize()/8); 133 134 writeAndUpdateHmac(underlyingOutputStream, cipherSpec.getId()); 135 writeAndUpdateHmac(underlyingOutputStream, saltedSecretKey.getSalt()); 136 writeAndUpdateHmac(underlyingOutputStream, iv); 137 138 cipherOutputStream = cipherSpec.newCipherOutputStream(cipherOutputStream, saltedSecretKey.getEncoded(), iv); 139 } 140 141 writeNoHmac(underlyingOutputStream, headerHmac.doFinal()); 142 } 143 catch (Exception e) { 144 throw new IOException(e); 145 } 146 headerWritten = true; 147 } 148 } 149 150 private void writeNoHmac(OutputStream outputStream, byte[] bytes) throws IOException { 151 outputStream.write(bytes); 152 } 153 154 private void writeNoHmac(OutputStream outputStream, int abyte) throws IOException { 155 outputStream.write(abyte); 156 } 157 158 private void writeAndUpdateHmac(OutputStream outputStream, byte[] bytes) throws IOException { 159 writeNoHmac(outputStream, bytes); 160 headerHmac.update(bytes); 161 } 162 163 private void writeAndUpdateHmac(OutputStream outputStream, int abyte) throws IOException { 164 writeNoHmac(outputStream, abyte); 165 headerHmac.update((byte) abyte); 166 } 167}