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}