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.0.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 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    }