001 /*
002 * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org
003 * Copyright (C) 2011 NightLabs Consulting GmbH
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (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 Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018 package org.cumulus4j.store.crypto.keymanager;
019
020 import java.io.IOException;
021
022 import org.bouncycastle.crypto.CipherParameters;
023 import org.bouncycastle.crypto.params.KeyParameter;
024 import org.bouncycastle.crypto.params.ParametersWithIV;
025 import org.cumulus4j.crypto.Cipher;
026 import org.cumulus4j.crypto.CryptoRegistry;
027 import org.cumulus4j.crypto.MACCalculator;
028 import org.cumulus4j.keymanager.back.shared.GetActiveEncryptionKeyRequest;
029 import org.cumulus4j.keymanager.back.shared.GetActiveEncryptionKeyResponse;
030 import org.cumulus4j.keymanager.back.shared.GetKeyRequest;
031 import org.cumulus4j.keymanager.back.shared.GetKeyResponse;
032 import org.cumulus4j.keymanager.back.shared.KeyEncryptionUtil;
033 import org.cumulus4j.store.crypto.AbstractCryptoSession;
034 import org.cumulus4j.store.crypto.Ciphertext;
035 import org.cumulus4j.store.crypto.CryptoContext;
036 import org.cumulus4j.store.crypto.CryptoManager;
037 import org.cumulus4j.store.crypto.Plaintext;
038 import org.cumulus4j.store.crypto.keymanager.messagebroker.MessageBroker;
039 import org.cumulus4j.store.crypto.keymanager.messagebroker.MessageBrokerRegistry;
040 import org.cumulus4j.store.model.EncryptionCoordinateSet;
041 import org.slf4j.Logger;
042 import org.slf4j.LoggerFactory;
043
044 /**
045 * <p>
046 * Implementation of {@link org.cumulus4j.store.crypto.CryptoSession CryptoSession} working with a
047 * key-manager as shown in <a target="_blank" href="http://cumulus4j.org/1.2.0/documentation/deployment-scenarios.html">Deployment scenarios</a>.
048 * </p>
049 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
050 */
051 public class KeyManagerCryptoSession
052 extends AbstractCryptoSession
053 {
054 private static final Logger logger = LoggerFactory.getLogger(KeyManagerCryptoSession.class);
055
056 // private static final BouncyCastleProvider bouncyCastleProvider = new BouncyCastleProvider();
057 // static {
058 // Security.insertProviderAt(bouncyCastleProvider, 2);
059 // }
060
061 // private static final ChecksumAlgorithm _activeChecksumAlgorithm = ChecksumAlgorithm.SHA1;
062 // private ChecksumAlgorithm getActiveChecksumAlgorithm()
063 // {
064 // return _activeChecksumAlgorithm;
065 // }
066 //
067 // private static final EncryptionAlgorithm _activeEncryptionAlgorithm = EncryptionAlgorithm.Twofish_CBC_PKCS5Padding; // TODO this should be configurable!
068 // private EncryptionAlgorithm getActiveEncryptionAlgorithm()
069 // {
070 // return _activeEncryptionAlgorithm;
071 // }
072
073 private MessageBroker getMessageBroker() {
074 return MessageBrokerRegistry.sharedInstance().getActiveMessageBroker();
075 }
076
077 /**
078 * <p>
079 * The <b>a</b>symmetric encryption algorithm used to encrypt the keys when they are sent from key-manager
080 * to here (app-server).
081 * </p>
082 * <p>
083 * Alternatively, we could use "EC": http://en.wikipedia.org/wiki/Elliptic_curve_cryptography
084 * </p>
085 */
086 private static final String keyEncryptionTransformation = "RSA//OAEPWITHSHA1ANDMGF1PADDING";
087
088 private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
089
090 /**
091 * {@inheritDoc}
092 * <p>
093 * The implementation in {@link KeyManagerCryptoSession} stores every plaintext
094 * encoded in the following form:
095 * </p>
096 * <table border="1">
097 * <tbody>
098 * <tr>
099 * <td align="right" valign="top"><b>Bytes</b></td><td><b>Description</b></td>
100 * </tr><tr>
101 * <td align="right" valign="top">1</td><td>Version number</td>
102 * </tr><tr>
103 * <td align="right" valign="top">2</td><td>{@link EncryptionCoordinateSet#getEncryptionCoordinateSetID()} (only 2 bytes, thus limiting to 65K possible values)</td>
104 * </tr><tr>
105 * <td align="right" valign="top">1</td><td><i>ivLen</i>: Length of the IV in bytes</td>
106 * </tr><tr>
107 * <td align="right" valign="top"><i>ivLen</i></td><td>Actual IV (random initialisation vector).</td>
108 * </tr><tr>
109 * <td align="right" valign="top">1</td><td><i>macKeyLen</i>: <a target="_blank" href="http://en.wikipedia.org/wiki/Message_authentication_code">MAC</a>'s key length in bytes</td>
110 * </tr><tr>
111 * <td align="right" valign="top">1</td><td><i>macIVLen</i>: MAC's IV length in bytes</td>
112 * </tr><tr>
113 * <td align="right" valign="top">1</td><td><i>macLen</i>: Actual MAC's length in bytes</td>
114 * </tr><tr>
115 * <td colspan="2">
116 * <table bgcolor="#F0F0F0" border="1" width="100%">
117 * <tbody>
118 * <tr>
119 * <td bgcolor="#D0D0D0" colspan="2"><b>ENCRYPTED</b></td>
120 * </tr><tr>
121 * <td align="right" valign="top"><b>Bytes</b></td><td><b>Description</b></td>
122 * </tr><tr>
123 * <td align="right" valign="top"><i>macKeyLen</i></td><td>MAC's key (random)</td>
124 * </tr><tr>
125 * <td align="right" valign="top"><i>macIVLen</i></td><td>MAC's IV (random)</td>
126 * </tr><tr>
127 * <td align="right" valign="top"><i>all until MAC</i></td><td>Actual data</td>
128 * </tr><tr>
129 * <td align="right" valign="top"><i>macLen</i></td><td>Actual MAC</td>
130 * </tr>
131 * </tbody>
132 * </table>
133 * </td>
134 * </tr>
135 * </tbody>
136 * </table>
137 */
138 @Override
139 public Ciphertext encrypt(CryptoContext cryptoContext, Plaintext plaintext)
140 {
141 EncryptionCoordinateSet encryptionCoordinateSet = cryptoContext.getEncryptionCoordinateSetManager().createEncryptionCoordinateSet(
142 cryptoContext.getPersistenceManagerConnection(),
143 getCryptoManager().getEncryptionAlgorithm(),
144 getCryptoManager().getMACAlgorithm()
145 );
146 String activeEncryptionAlgorithm = encryptionCoordinateSet.getCipherTransformation();
147
148 if (encryptionCoordinateSet.getEncryptionCoordinateSetID() < 0)
149 throw new IllegalStateException("The encryptionCoordinateSetID = " + encryptionCoordinateSet.getEncryptionCoordinateSetID() + " is out of range! It must be >= 0!!!");
150
151 if (encryptionCoordinateSet.getEncryptionCoordinateSetID() > (2 * Short.MAX_VALUE))
152 throw new IllegalStateException("The encryptionCoordinateSetID is out of range! The maximum is " + (2 * Short.MAX_VALUE) + ", because the value is encoded as UNsigned 2-byte-number! This means, you changed the encryption algorithm or the MAC algorithm too often. Switch back to settings you already used before!");
153
154 CryptoCache cryptoCache = ((KeyManagerCryptoManager)getCryptoManager()).getCryptoCache();
155
156 CryptoCacheKeyDecrypterEntry keyDecryptor = null;
157 CryptoCacheCipherEntry encrypter = null;
158 try {
159 long activeEncryptionKeyID = cryptoCache.getActiveEncryptionKeyID();
160 if (activeEncryptionKeyID >= 0)
161 encrypter = cryptoCache.acquireEncrypter(activeEncryptionAlgorithm, activeEncryptionKeyID);
162
163 if (encrypter == null) {
164 keyDecryptor = cryptoCache.acquireKeyDecryptor(keyEncryptionTransformation);
165
166 GetActiveEncryptionKeyResponse getActiveEncryptionKeyResponse;
167 try {
168 GetActiveEncryptionKeyRequest getActiveEncryptionKeyRequest = new GetActiveEncryptionKeyRequest(
169 getCryptoSessionID(), keyEncryptionTransformation, keyDecryptor.getKeyEncryptionKey().getEncodedPublicKey()
170 );
171 getActiveEncryptionKeyResponse = getMessageBroker().query(
172 GetActiveEncryptionKeyResponse.class,
173 getActiveEncryptionKeyRequest
174 );
175 } catch (Exception e) {
176 logger.warn("Could not query active encryption key: " + e, e);
177 throw new RuntimeException(e);
178 }
179
180 byte[] keyEncodedPlain = KeyEncryptionUtil.decryptKey(keyDecryptor.getKeyDecryptor(), getActiveEncryptionKeyResponse.getKeyEncodedEncrypted());
181
182 activeEncryptionKeyID = getActiveEncryptionKeyResponse.getKeyID();
183 cryptoCache.setActiveEncryptionKeyID(activeEncryptionKeyID, getActiveEncryptionKeyResponse.getActiveUntilExcl());
184 encrypter = cryptoCache.acquireEncrypter(activeEncryptionAlgorithm, activeEncryptionKeyID, keyEncodedPlain);
185 }
186
187 Cipher cipher = encrypter.getCipher();
188
189 byte[] mac = EMPTY_BYTE_ARRAY;
190 byte[] macKey = EMPTY_BYTE_ARRAY;
191 byte[] macIV = EMPTY_BYTE_ARRAY;
192
193 if (!CryptoManager.MAC_ALGORITHM_NONE.equals(encryptionCoordinateSet.getMACAlgorithm())) {
194 MACCalculator macCalculator = CryptoRegistry.sharedInstance().createMACCalculator(encryptionCoordinateSet.getMACAlgorithm(), true);
195 mac = macCalculator.doFinal(plaintext.getData());
196
197 if (macCalculator.getParameters() instanceof ParametersWithIV) {
198 ParametersWithIV pwiv = (ParametersWithIV) macCalculator.getParameters();
199 macIV = pwiv.getIV();
200 macKey = ((KeyParameter)pwiv.getParameters()).getKey();
201 }
202 else if (macCalculator.getParameters() instanceof KeyParameter) {
203 macKey = ((KeyParameter)macCalculator.getParameters()).getKey();
204 }
205 else
206 throw new IllegalStateException("macCalculator.getParameters() returned an instance of an unknown type: " + (macCalculator.getParameters() == null ? null : macCalculator.getParameters().getClass().getName()));
207 }
208
209 byte[] iv = ((ParametersWithIV)cipher.getParameters()).getIV();
210
211 if (iv.length > 255)
212 throw new IllegalStateException("IV too long! Cannot encode length in 1 byte!");
213
214 if (macKey.length > 255)
215 throw new IllegalStateException("macKey too long! Cannot encode length in 1 byte!");
216
217 if (macIV.length > 255)
218 throw new IllegalStateException("macIV too long! Cannot encode length in 1 byte!");
219
220 if (mac.length > 255)
221 throw new IllegalStateException("mac too long! Cannot encode length in 1 byte!");
222
223 int outLength = (
224 1 // version
225 + 2 // encryptionCoordinateSetID
226 + 1 // IV length in bytes
227 + iv.length // actual IV
228 + 1 // macKeyLength in bytes
229 + 1 // macIVLength in bytes
230 + 1 // MAC length in bytes
231 + cipher.getOutputSize(
232 macKey.length // actual MAC key
233 + macIV.length // actual MAC IV
234 + plaintext.getData().length // actual plaintext
235 + mac.length // actual MAC
236 )
237 );
238
239 byte[] out = new byte[outLength];
240 int outOff = 0;
241 out[outOff++] = 1; // version 1
242
243 // encryptionCoordinateSetID as UNsigned short
244 out[outOff++] = (byte)(encryptionCoordinateSet.getEncryptionCoordinateSetID() >>> 8);
245 out[outOff++] = (byte)encryptionCoordinateSet.getEncryptionCoordinateSetID();
246
247 // IV length
248 out[outOff++] = (byte)iv.length;
249
250 // actual IV
251 System.arraycopy(iv, 0, out, outOff, iv.length);
252 outOff += iv.length;
253
254 out[outOff++] = (byte)macKey.length;
255 out[outOff++] = (byte)macIV.length;
256 out[outOff++] = (byte)mac.length;
257
258 outOff += cipher.update(macKey, 0, macKey.length, out, outOff);
259 outOff += cipher.update(macIV, 0, macIV.length, out, outOff);
260 outOff += cipher.update(plaintext.getData(), 0, plaintext.getData().length, out, outOff);
261 outOff += cipher.update(mac, 0, mac.length, out, outOff);
262 outOff += cipher.doFinal(out, outOff);
263
264 if (outOff < outLength) {
265 logger.warn(
266 "encrypt: Output byte array was created bigger than necessary. Will shrink it now. outOff={} encryptedLength={}",
267 outOff, outLength
268 );
269 byte tmp[] = new byte[outOff];
270 System.arraycopy(out, 0, tmp, 0, tmp.length);
271 out = tmp;
272 }
273
274 Ciphertext ciphertext = new Ciphertext();
275 ciphertext.setData(out);
276 ciphertext.setKeyID(activeEncryptionKeyID);
277 return ciphertext;
278 } catch (RuntimeException e) {
279 logger.error("encrypt: " + e, e);
280 throw e;
281 } catch (Exception e) {
282 logger.error("encrypt: " + e, e);
283 throw new RuntimeException(e);
284 } finally {
285 cryptoCache.releaseKeyDecryptor(keyDecryptor);
286 cryptoCache.releaseCipherEntry(encrypter);
287 }
288 }
289
290 @Override
291 public Plaintext decrypt(CryptoContext cryptoContext, Ciphertext ciphertext)
292 {
293 CryptoCache cryptoCache = ((KeyManagerCryptoManager)getCryptoManager()).getCryptoCache();
294
295 CryptoCacheKeyDecrypterEntry keyDecryptor = null;
296 CryptoCacheCipherEntry decrypter = null;
297 try {
298 long keyID = ciphertext.getKeyID();
299 int inOff = 0;
300 byte[] in = ciphertext.getData();
301 int version = in[inOff++] & 0xff;
302 if (version != 1)
303 throw new IllegalArgumentException("Ciphertext is of version " + version + " which is not supported!");
304
305 int encryptionCoordinateSetID = (in[inOff++] << 8) & 0xffff;
306 encryptionCoordinateSetID += in[inOff++] & 0xff;
307
308 EncryptionCoordinateSet encryptionCoordinateSet = cryptoContext.getEncryptionCoordinateSetManager().getEncryptionCoordinateSet(
309 cryptoContext.getPersistenceManagerConnection(), encryptionCoordinateSetID
310 );
311 if (encryptionCoordinateSet == null)
312 throw new IllegalStateException("There is no EncryptionCoordinateSet with encryptionCoordinateSetID=" + encryptionCoordinateSetID + "!");
313
314 int ivLength = in[inOff++] & 0xff;
315 byte[] iv = new byte[ivLength];
316 System.arraycopy(in, inOff, iv, 0, iv.length);
317 inOff += iv.length;
318
319 int macKeyLength = in[inOff++] & 0xff;
320 int macIVLength = in[inOff++] & 0xff;
321 int macLength = in[inOff++] & 0xff;
322
323 decrypter = cryptoCache.acquireDecrypter(encryptionCoordinateSet.getCipherTransformation(), keyID, iv);
324 if (decrypter == null) {
325 keyDecryptor = cryptoCache.acquireKeyDecryptor(keyEncryptionTransformation);
326
327 GetKeyResponse getKeyResponse;
328 try {
329 GetKeyRequest getKeyRequest = new GetKeyRequest(
330 getCryptoSessionID(), ciphertext.getKeyID(),
331 keyEncryptionTransformation, keyDecryptor.getKeyEncryptionKey().getEncodedPublicKey()
332 );
333 getKeyResponse = getMessageBroker().query(
334 GetKeyResponse.class, getKeyRequest
335 );
336 } catch (Exception e) {
337 logger.warn("Could not query key " + ciphertext.getKeyID() + ": " + e, e);
338 throw new RuntimeException(e);
339 }
340
341 byte[] keyEncodedPlain = KeyEncryptionUtil.decryptKey(keyDecryptor.getKeyDecryptor(), getKeyResponse.getKeyEncodedEncrypted());
342
343 decrypter = cryptoCache.acquireDecrypter(encryptionCoordinateSet.getCipherTransformation(), keyID, keyEncodedPlain, iv);
344 }
345
346 int inCryptLength = in.length - inOff;
347 int outLength = decrypter.getCipher().getOutputSize(inCryptLength);
348 byte[] out = new byte[outLength];
349 int outOff = 0;
350 outOff += decrypter.getCipher().update(in, inOff, inCryptLength, out, 0);
351 outOff += decrypter.getCipher().doFinal(out, outOff);
352
353 if (logger.isDebugEnabled() && outOff != outLength)
354 logger.debug("decrypt: precalculated output-size does not match actually written output: expected={} actual={}", outLength, outOff);
355
356 int dataOff = 0;
357 MACCalculator macCalculator = null;
358 if (!CryptoManager.MAC_ALGORITHM_NONE.equals(encryptionCoordinateSet.getMACAlgorithm())) {
359 macCalculator = CryptoRegistry.sharedInstance().createMACCalculator(encryptionCoordinateSet.getMACAlgorithm(), false);
360
361 CipherParameters macKeyParam = new KeyParameter(out, 0, macKeyLength);
362 dataOff += macKeyLength;
363
364 CipherParameters macParams;
365 if (macIVLength == 0)
366 macParams = macKeyParam;
367 else {
368 macParams = new ParametersWithIV(macKeyParam, out, dataOff, macIVLength);
369 dataOff += macIVLength;
370 }
371
372 macCalculator.init(macParams);
373 }
374
375 int dataLength = outOff - dataOff - macLength;
376 int macOff = dataOff + dataLength;
377
378 if (macCalculator != null) {
379 byte[] newMAC = new byte[macCalculator.getMacSize()];
380 macCalculator.update(out, dataOff, dataLength);
381 macCalculator.doFinal(newMAC, 0);
382
383 if (newMAC.length != macLength)
384 throw new IOException("MACs have different length! Expected MAC has " + macLength + " bytes and newly calculated MAC has " + newMAC.length + " bytes!");
385
386 for (int i = 0; i < macLength; ++i) {
387 byte expected = out[macOff + i];
388 if (expected != newMAC[i])
389 throw new IOException("MAC mismatch! mac[" + i + "] was expected to be " + expected + " but was " + newMAC[i]);
390 }
391 }
392
393 byte[] decrypted = new byte[dataLength];
394 System.arraycopy(out, dataOff, decrypted, 0, decrypted.length);
395 Plaintext plaintext = new Plaintext();
396 plaintext.setData(decrypted);
397 return plaintext;
398 } catch (RuntimeException e) {
399 logger.error("decrypt: " + e, e);
400 throw e;
401 } catch (Exception e) {
402 logger.error("decrypt: " + e, e);
403 throw new RuntimeException(e);
404 } finally {
405 cryptoCache.releaseKeyDecryptor(keyDecryptor);
406 cryptoCache.releaseCipherEntry(decrypter);
407 }
408 }
409
410 @Override
411 public void close() {
412 super.close();
413
414 // Our caches are used across multiple sessions for performance reasons,
415 // hence we cannot close the caches here (maybe we might consider closing the
416 // cache when the last session is closed, later).
417
418 doNothing(); // suppress PMD warning - I want this overridden method here in this class for documentation reasons. Marco :-)
419 }
420
421 private static final void doNothing() { }
422 }