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 java.security.NoSuchAlgorithmException;
021import java.security.NoSuchProviderException;
022import java.security.spec.InvalidKeySpecException;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.LinkedHashMap;
026import java.util.Map;
027import java.util.logging.Level;
028import java.util.logging.Logger;
029
030import javax.crypto.SecretKey;
031
032import org.syncany.util.StringUtil;
033
034/**
035 * The cipher session is used by the {@link MultiCipherOutputStream} and the
036 * {@link MultiCipherInputStream} to reference the application's master key,
037 * and to temporarily store and retrieve derived secret keys.
038 *
039 * <p>While a cipher session does not create a master key, it creates and manages
040 * the derived keys using the {@link CipherUtil} class:
041 *
042 * <ul>
043 *   <li>Keys used by {@link MultiCipherOutputStream} (for writing new files) are
044 *       reused a number of times before a new salted key is created. The main
045 *       purpose of reusing keys is to increase performance. Because the master
046 *       key is cryptographically strong, the derived keys can be reused a few
047 *       times without any drawbacks on security. The class keeps one secret key
048 *       per {@link CipherSpec}.
049 *
050 *   <li>Keys used by {@link MultiCipherInputStream} (when reading files) are
051 *       cached in order to minimize the amount of keys that have to be created when
052 *       files are processed.
053 * </ul>
054 *
055 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
056 */
057public class CipherSession {
058        private static final Logger logger = Logger.getLogger(CipherSession.class.getSimpleName());
059        private static final int DEFAULT_SECRET_KEY_READ_CACHE_SIZE = 20;
060        private static final int DEFAULT_SECRET_KEY_WRITE_REUSE_COUNT = 100;
061
062        private SecretKey masterKey;
063
064        private Map<CipherSpecWithSalt, SecretKeyCacheEntry> secretKeyReadCache;
065        private int secretKeyReadCacheSize;
066
067        private Map<CipherSpec, SecretKeyCacheEntry> secretKeyWriteCache;
068        private int secretKeyWriteReuseCount;
069
070        /**
071         * Creates a new cipher session, using the given master key. Derived keys will be created
072         * from that master key.
073         *
074         * <p>The default settings for read/write key cache will be used. Refer to
075         * {@link CipherSession the class description} for more details. Default values:
076         * {@link #DEFAULT_SECRET_KEY_READ_CACHE_SIZE} and {@link #DEFAULT_SECRET_KEY_WRITE_REUSE_COUNT}
077         *
078         * @param masterKey The master key, used for deriving new read/write keys
079         */
080        public CipherSession(SaltedSecretKey masterKey) {
081                this(masterKey, DEFAULT_SECRET_KEY_READ_CACHE_SIZE, DEFAULT_SECRET_KEY_WRITE_REUSE_COUNT);
082        }
083
084        /**
085         * Creates a new cipher session, using the given master key. Derived keys will be created
086         * from that master key.
087         *
088         * <p>This method expects a reuse-count for write keys and a cache size for the read-key cache.
089         * Refer to {@link CipherSession the class description} for more details.
090         *
091         * @param masterKey The master key, used for deriving new read/write
092         * @param secretKeyReadCacheSize Number of read keys to store in the cache (higher means more performance, but more memory usage)
093         * @param secretKeyWriteReuseCount Number of times to reuse a write key (higher means more performance, but lower security)
094         */
095        public CipherSession(SaltedSecretKey masterKey, int secretKeyReadCacheSize, int secretKeyWriteReuseCount) {
096                this.masterKey = masterKey;
097
098                this.secretKeyReadCache = new LinkedHashMap<CipherSpecWithSalt, SecretKeyCacheEntry>();
099                this.secretKeyReadCacheSize = secretKeyReadCacheSize;
100
101                this.secretKeyWriteCache = new HashMap<CipherSpec, SecretKeyCacheEntry>();
102                this.secretKeyWriteReuseCount = secretKeyWriteReuseCount;
103        }
104
105        /**
106         * Returns the master key
107         */
108        public SecretKey getMasterKey() {
109                return masterKey;
110        }
111
112        /**
113         * Creates or retrieves a derived write secret key and updates the cache and re-use count. If a key is reused more
114         * than the threshold defined in {@link #secretKeyWriteReuseCount} (as set in {@link #CipherSession(SaltedSecretKey, int, int) the constructor},
115         * a new key is created and added to the cache.
116         *
117         * <p>If a new key needs to be created, {@link CipherUtil} is used to do so.
118         *
119         * <p>Contrary to the read cache, the write cache key is a only {@link CipherSpec}, i.e. only one secret key
120         * per cipher spec can be held in the cache.
121         *
122         * @param cipherSpec Defines the type of key to be created (or retrieved); used as key for the cache retrieval
123         * @return Returns a newly created secret key or a cached key
124         * @throws Exception If an error occurs with key creation
125         */
126        public SaltedSecretKey getWriteSecretKey(CipherSpec cipherSpec) throws Exception {
127                SecretKeyCacheEntry secretKeyCacheEntry = secretKeyWriteCache.get(cipherSpec);
128
129                // Remove key if use more than X times
130                if (secretKeyCacheEntry != null && secretKeyCacheEntry.getUseCount() >= secretKeyWriteReuseCount) {
131                        logger.log(Level.FINE, "- Removed WRITE secret key from cache, because it was used " + secretKeyCacheEntry.getUseCount() + " times.");
132
133                        secretKeyWriteCache.remove(cipherSpec);
134                        secretKeyCacheEntry = null;
135                }
136
137                // Return cached key, or create a new one
138                if (secretKeyCacheEntry != null) {
139                        secretKeyCacheEntry.increaseUseCount();
140
141                        logger.log(Level.FINE, "- Using CACHED WRITE secret key " + secretKeyCacheEntry.getSaltedSecretKey().getAlgorithm() + ", with salt "
142                                        + StringUtil.toHex(secretKeyCacheEntry.getSaltedSecretKey().getSalt()));
143                        return secretKeyCacheEntry.getSaltedSecretKey();
144                }
145                else {
146                        SaltedSecretKey saltedSecretKey = createSaltedSecretKey(cipherSpec);
147
148                        secretKeyCacheEntry = new SecretKeyCacheEntry(saltedSecretKey);
149                        secretKeyWriteCache.put(cipherSpec, secretKeyCacheEntry);
150
151                        logger.log(Level.FINE, "- Created NEW WRITE secret key " + secretKeyCacheEntry.getSaltedSecretKey().getAlgorithm()
152                                        + ", and added to cache, with salt " + StringUtil.toHex(saltedSecretKey.getSalt()));
153                        return saltedSecretKey;
154                }
155        }
156
157        /**
158         * Creates a new secret key or retrieves it from the read cache. If the given cipher spec / salt combination
159         * is found in the cache, the cached secret key is returned. If not, a new key is created. Keys are removed
160         * from the cache when the cache reached the size defined by {@link #secretKeyReadCacheSize} (as set in
161         * {@link #CipherSession(SaltedSecretKey, int, int) the constructor}.
162         *
163         * <p>If a new key needs to be created, {@link CipherUtil} is used to do so.
164         *
165         * <p>Contrary to the write cache, the read cache key is a combination of {@link CipherSpec} and a salt. For
166         * each cipher spec, multiple salted keys can reside in the cache at the same time.
167         *
168         * @param cipherSpec Defines the type of key to be created (or retrieved); used as one part of the key for cache retrieval
169         * @param salt Defines the salt for the key to be created (or retrieved); used as one part of the key for cache retrieval
170         * @return Returns a newly created secret key or a cached key
171         * @throws Exception If an error occurs with key creation
172         */
173        public SaltedSecretKey getReadSecretKey(CipherSpec cipherSpec, byte[] salt) throws Exception {
174                CipherSpecWithSalt cipherSpecWithSalt = new CipherSpecWithSalt(cipherSpec, salt);
175                SecretKeyCacheEntry secretKeyCacheEntry = secretKeyReadCache.get(cipherSpecWithSalt);
176
177                if (secretKeyCacheEntry != null) {
178                        logger.log(Level.FINE, "- Using CACHED READ secret key " + secretKeyCacheEntry.getSaltedSecretKey().getAlgorithm() + ", with salt "
179                                        + StringUtil.toHex(salt));
180                        return secretKeyCacheEntry.getSaltedSecretKey();
181                }
182                else {
183                        if (secretKeyReadCache.size() >= secretKeyReadCacheSize) {
184                                CipherSpecWithSalt firstKey = secretKeyReadCache.keySet().iterator().next();
185                                secretKeyReadCache.remove(firstKey);
186
187                                logger.log(Level.FINE, "- Removed oldest READ secret key from cache.");
188                        }
189
190                        SaltedSecretKey saltedSecretKey = createSaltedSecretKey(cipherSpec, salt);
191                        secretKeyCacheEntry = new SecretKeyCacheEntry(saltedSecretKey);
192
193                        secretKeyReadCache.put(cipherSpecWithSalt, secretKeyCacheEntry);
194
195                        logger.log(Level.FINE, "- Created NEW READ secret key " + secretKeyCacheEntry.getSaltedSecretKey().getAlgorithm()
196                                        + ", and added to cache, with salt " + StringUtil.toHex(salt));
197                        return saltedSecretKey;
198                }
199        }
200
201        private SaltedSecretKey createSaltedSecretKey(CipherSpec cipherSpec) throws InvalidKeySpecException, NoSuchAlgorithmException,
202        NoSuchProviderException {
203                byte[] salt = CipherUtil.createRandomArray(MultiCipherOutputStream.SALT_SIZE);
204                return createSaltedSecretKey(cipherSpec, salt);
205        }
206
207        private SaltedSecretKey createSaltedSecretKey(CipherSpec cipherSpec, byte[] salt) throws InvalidKeySpecException, NoSuchAlgorithmException,
208        NoSuchProviderException {
209                return CipherUtil.createDerivedKey(masterKey, salt, cipherSpec);
210        }
211
212        private static class SecretKeyCacheEntry {
213                private SaltedSecretKey saltedSecretKey;
214                private int useCount;
215
216                public SecretKeyCacheEntry(SaltedSecretKey saltedSecretKey) {
217                        this.saltedSecretKey = saltedSecretKey;
218                        this.useCount = 1;
219                }
220
221                public SaltedSecretKey getSaltedSecretKey() {
222                        return saltedSecretKey;
223                }
224
225                public int getUseCount() {
226                        return useCount;
227                }
228
229                public void increaseUseCount() {
230                        useCount++;
231                }
232        }
233
234        private static class CipherSpecWithSalt {
235                private CipherSpec cipherSpec;
236                private byte[] salt;
237
238                public CipherSpecWithSalt(CipherSpec cipherSpec, byte[] salt) {
239                        this.cipherSpec = cipherSpec;
240                        this.salt = salt;
241                }
242
243                @Override
244                public int hashCode() {
245                        final int prime = 31;
246                        int result = 1;
247                        result = prime * result + ((cipherSpec == null) ? 0 : cipherSpec.hashCode());
248                        result = prime * result + Arrays.hashCode(salt);
249                        return result;
250                }
251
252                @Override
253                public boolean equals(Object obj) {
254                        if (this == obj) {
255                                return true;
256                        }
257                        if (obj == null) {
258                                return false;
259                        }
260                        if (!(obj instanceof CipherSpecWithSalt)) {
261                                return false;
262                        }
263                        CipherSpecWithSalt other = (CipherSpecWithSalt) obj;
264                        if (cipherSpec == null) {
265                                if (other.cipherSpec != null) {
266                                        return false;
267                                }
268                        }
269                        else if (!cipherSpec.equals(other.cipherSpec)) {
270                                return false;
271                        }
272                        if (!Arrays.equals(salt, other.salt)) {
273                                return false;
274                        }
275                        return true;
276                }
277        }
278
279}