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}