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.InputStream;
024import java.util.Arrays;
025
026import javax.crypto.Mac;
027import javax.crypto.SecretKey;
028
029public class MultiCipherInputStream extends InputStream {
030        private InputStream underlyingInputStream;
031
032        private InputStream cipherInputStream;
033        private CipherSession cipherSession;
034        
035        private boolean headerRead;
036        private Mac headerHmac;
037                
038        public MultiCipherInputStream(InputStream in, CipherSession cipherSession) throws IOException {
039                this.underlyingInputStream = in;                
040
041                this.cipherInputStream = null;
042                this.cipherSession = cipherSession;
043                
044                this.headerRead = false;                
045                this.headerHmac = null;         
046        }
047
048        @Override
049        public int read() throws IOException {
050                readHeader();
051                return cipherInputStream.read();
052        }
053        
054        @Override
055        public int read(byte[] b) throws IOException {
056                readHeader();
057                return cipherInputStream.read(b, 0, b.length);
058        }
059        
060        @Override
061        public int read(byte[] b, int off, int len) throws IOException {
062                readHeader();
063                return cipherInputStream.read(b, off, len);
064        }
065        
066        @Override
067        public void close() throws IOException {
068                cipherInputStream.close();
069        }       
070        
071        private void readHeader() throws IOException {
072                if (!headerRead) {
073                        try {
074                                readAndVerifyMagicNoHmac(underlyingInputStream);
075                                readAndVerifyVersionNoHmac(underlyingInputStream);
076
077                                headerHmac = readHmacSaltAndInitHmac(underlyingInputStream, cipherSession);                             
078                                cipherInputStream = readCipherSpecsAndUpdateHmac(underlyingInputStream, headerHmac, cipherSession);
079
080                                readAndVerifyHmac(underlyingInputStream, headerHmac);                   
081                        }
082                        catch (Exception e) {
083                                throw new IOException(e);
084                        }
085                        
086                        headerRead = true;
087                }
088        }
089
090        private void readAndVerifyMagicNoHmac(InputStream inputStream) throws IOException {
091                byte[] streamMagic = new byte[MultiCipherOutputStream.STREAM_MAGIC.length];
092                inputStream.read(streamMagic);
093                
094                if (!Arrays.equals(MultiCipherOutputStream.STREAM_MAGIC, streamMagic)) {
095                        throw new IOException("Not a Syncany-encrypted file, no magic!");
096                }
097        }
098
099        private void readAndVerifyVersionNoHmac(InputStream inputStream) throws IOException {
100                byte streamVersion = (byte) inputStream.read();
101                
102                if (streamVersion != MultiCipherOutputStream.STREAM_VERSION) {
103                        throw new IOException("Stream version not supported: "+streamVersion);
104                }               
105        }
106        
107        private Mac readHmacSaltAndInitHmac(InputStream inputStream, CipherSession cipherSession) throws Exception {
108                byte[] hmacSalt = readNoHmac(inputStream, MultiCipherOutputStream.SALT_SIZE);
109                SecretKey hmacSecretKey = cipherSession.getReadSecretKey(MultiCipherOutputStream.HMAC_SPEC, hmacSalt);
110                
111                Mac hmac = Mac.getInstance(MultiCipherOutputStream.HMAC_SPEC.getAlgorithm(), CRYPTO_PROVIDER_ID);
112                hmac.init(hmacSecretKey);       
113                
114                return hmac;
115        }
116        
117        private InputStream readCipherSpecsAndUpdateHmac(InputStream underlyingInputStream, Mac hmac, CipherSession cipherSession) throws Exception {
118                int cipherSpecCount = readByteAndUpdateHmac(underlyingInputStream, hmac);               
119                InputStream nestedCipherInputStream = underlyingInputStream;
120                
121                for (int i=0; i<cipherSpecCount; i++) {
122                        int cipherSpecId = readByteAndUpdateHmac(underlyingInputStream, hmac);                          
123                        CipherSpec cipherSpec = CipherSpecs.getCipherSpec(cipherSpecId);
124                        
125                        if (cipherSpec == null) {
126                                throw new IOException("Cannot find cipher spec with ID "+cipherSpecId);
127                        }
128
129                        byte[] salt = readAndUpdateHmac(underlyingInputStream, MultiCipherOutputStream.SALT_SIZE, hmac);
130                        byte[] iv = readAndUpdateHmac(underlyingInputStream, cipherSpec.getIvSize()/8, hmac);
131                        
132                        SecretKey secretKey = cipherSession.getReadSecretKey(cipherSpec, salt);                 
133                        nestedCipherInputStream = cipherSpec.newCipherInputStream(nestedCipherInputStream, secretKey.getEncoded(), iv);         
134                }        
135                
136                return nestedCipherInputStream;
137        }
138
139        private void readAndVerifyHmac(InputStream inputStream, Mac hmac) throws Exception {
140                byte[] calculatedHeaderHmac = hmac.doFinal();
141                byte[] readHeaderHmac = readNoHmac(inputStream, calculatedHeaderHmac.length);
142                
143                if (!Arrays.equals(calculatedHeaderHmac, readHeaderHmac)) {
144                        throw new Exception("Integrity exception: Calculated HMAC and read HMAC do not match.");
145                }                       
146        }
147
148        private byte[] readNoHmac(InputStream inputStream, int size) throws IOException {
149                byte[] bytes = new byte[size];          
150                inputStream.read(bytes);        
151                
152                return bytes;
153        }
154
155        private byte[] readAndUpdateHmac(InputStream inputStream, int size, Mac hmac) throws IOException {
156                byte[] bytes = readNoHmac(inputStream, size);           
157                hmac.update(bytes);
158                
159                return bytes;
160        }
161
162        private int readByteAndUpdateHmac(InputStream inputStream, Mac hmac) throws IOException {
163                int abyte = inputStream.read();
164                hmac.update((byte) abyte);
165                
166                return abyte;
167        }
168}