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}