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.keymanager.back.shared;
019    
020    import java.io.IOException;
021    import java.security.GeneralSecurityException;
022    import java.security.NoSuchAlgorithmException;
023    
024    import org.bouncycastle.crypto.CipherParameters;
025    import org.bouncycastle.crypto.CryptoException;
026    import org.bouncycastle.crypto.params.KeyParameter;
027    import org.bouncycastle.crypto.params.ParametersWithIV;
028    import org.cumulus4j.crypto.Cipher;
029    import org.cumulus4j.crypto.CipherOperationMode;
030    import org.cumulus4j.crypto.CryptoRegistry;
031    import org.cumulus4j.crypto.MACCalculator;
032    import org.slf4j.Logger;
033    import org.slf4j.LoggerFactory;
034    
035    /**
036     * <p>
037     * Utility class to en- &amp; decrypt symmetric secret keys using asymmetric encryption.
038     * </p>
039     * <p>
040     * TODO the MAC algorithm should be communicated between key-manager and app-server (maybe
041     * the app-server specifies it, but with the possibility that the key-manager can override, i.e. use another one?!
042     * thus requiring the GetKeyResponse to tell the app-server, which one was actually used - or maybe encode this into the
043     * binary result here? Or maybe only specify it here on the key-manager-side (and encode in the binary)?
044     * less work and probably sufficient).
045     * </p>
046     *
047     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
048     */
049    public final class KeyEncryptionUtil
050    {
051            private static final Logger logger = LoggerFactory.getLogger(KeyEncryptionUtil.class);
052    
053            private KeyEncryptionUtil() { }
054    
055            private static final String MAC_ALGORITHM = "HMAC-SHA1";
056    
057            private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
058    
059            /**
060             * Encrypt the given symmetric secret <code>key</code> with the given {@link Cipher}.
061             * The key will be protected against manipulation/corruption by a MAC.
062             *
063             * @param key the symmetric secret key to be encrypted.
064             * @param encrypter the cipher used for encryption.
065             * @return the key together with the MAC's key + IV - all encrypted.
066             * @throws CryptoException in case the encryption fails.
067             * @throws NoSuchAlgorithmException in case a crypto algorithm's name (e.g. for the MAC) does not exist in the {@link CryptoRegistry}.
068             * @see #encryptKey(byte[], String, byte[])
069             */
070            public static byte[] encryptKey(byte[] key, Cipher encrypter) throws CryptoException, NoSuchAlgorithmException
071            {
072                    byte[] mac = EMPTY_BYTE_ARRAY;
073                    byte[] macKey = EMPTY_BYTE_ARRAY;
074                    byte[] macIV = EMPTY_BYTE_ARRAY;
075    
076                    MACCalculator macCalculator = CryptoRegistry.sharedInstance().createMACCalculator(MAC_ALGORITHM, true);
077                    mac = macCalculator.doFinal(key);
078                    if (macCalculator.getParameters() instanceof ParametersWithIV) {
079                            ParametersWithIV pwiv = (ParametersWithIV) macCalculator.getParameters();
080                            macIV = pwiv.getIV();
081                            macKey = ((KeyParameter)pwiv.getParameters()).getKey();
082                    }
083                    else if (macCalculator.getParameters() instanceof KeyParameter) {
084                            macKey = ((KeyParameter)macCalculator.getParameters()).getKey();
085                    }
086                    else
087                            throw new IllegalStateException("macCalculator.getParameters() returned an instance of an unknown type: " + (macCalculator.getParameters() == null ? null : macCalculator.getParameters().getClass().getName()));
088    
089                    int resultSize = (
090                                    1 // version
091                                    + 3 // macKeySize, macIVSize, macSize
092                                    + encrypter.getOutputSize(macKey.length + macIV.length + key.length + mac.length)
093                    );
094    
095                    byte[] out = new byte[resultSize];
096    
097                    if (macKey.length > 255)
098                            throw new IllegalStateException("MAC key length too long!");
099    
100                    if (macIV.length > 255)
101                            throw new IllegalStateException("MAC IV length too long!");
102    
103                    if (mac.length > 255)
104                            throw new IllegalStateException("MAC length too long!");
105    
106                    int outOff = 0;
107                    out[outOff++] = (byte)1; // version
108                    out[outOff++] = (byte)macKey.length;
109                    out[outOff++] = (byte)macIV.length;
110                    out[outOff++] = (byte)mac.length;
111    
112                    outOff += encrypter.update(macKey, 0, macKey.length, out, outOff);
113                    outOff += encrypter.update(macIV,  0,  macIV.length, out, outOff);
114                    outOff += encrypter.update(key,    0,    key.length, out, outOff);
115                    outOff += encrypter.update(mac,    0,    mac.length, out, outOff);
116                    outOff += encrypter.doFinal(out, outOff);
117    
118                    if (out.length == outOff)
119                            return out;
120    
121                    logger.warn("Precalculated size ({}) does not match the actually written size ({})! Truncating byte array.", out.length, outOff);
122    
123                    byte[] result = new byte[outOff];
124                    System.arraycopy(out, 0, result, 0, result.length);
125                    return result;
126            }
127    
128            /**
129             * Encrypt the given symmetric secret <code>key</code>.
130             * The key will be protected against manipulation/corruption by a MAC (the algorithm is currently hard-coded, but this might be changed, soon).
131             *
132             * @param key the symmetric secret key to be encrypted.
133             * @param keyEncryptionTransformation the transformation to be used to encrypt (see {@link CryptoRegistry#createCipher(String)}).
134             * @param keyEncryptionPublicKey the public key to be used to encrypt the given <code>key</code>.
135             * @return the key together with the MAC's key + IV - all encrypted.
136             * @throws GeneralSecurityException if there's a problem {@link CryptoRegistry#createCipher(String) obtaining the cipher from the CryptoRegistry}.
137             * @throws IOException if decoding the public key from its binary representation fails.
138             * @throws CryptoException in case the encryption fails.
139             * @see #encryptKey(byte[], Cipher)
140             * @see #decryptKey(Cipher, byte[])
141             */
142            public static byte[] encryptKey(byte[] key, String keyEncryptionTransformation, byte[] keyEncryptionPublicKey)
143            throws GeneralSecurityException, IOException, CryptoException
144            {
145                    Cipher keyEncrypter = CryptoRegistry.sharedInstance().createCipher(keyEncryptionTransformation);
146                    CipherParameters publicKey = CryptoRegistry.sharedInstance().decodePublicKey(keyEncryptionPublicKey);
147                    keyEncrypter.init(CipherOperationMode.ENCRYPT, publicKey);
148                    byte[] keyEncodedEncrypted = KeyEncryptionUtil.encryptKey(key, keyEncrypter);
149                    return keyEncodedEncrypted;
150            }
151    
152            /**
153             * Decrypt a previously {@link #encryptKey(byte[], String, byte[]) encrypted} secret key and verify its integrity
154             * via a MAC.
155             *
156             * @param decrypter the cipher to be used for decryption (already initialised with key + IV).
157             * @param keyEncodedEncrypted the encrypted key as produced by {@link #encryptKey(byte[], Cipher)}
158             * @return the decrypted secret key (as originally passed to {@link #encryptKey(byte[], Cipher)}.
159             * @throws CryptoException if decryption failed.
160             * @throws IOException if data cannot be read or is corrupted - e.g. if MAC verification failed.
161             * @throws NoSuchAlgorithmException if the {@link CryptoRegistry} does not know the (MAC) algorithm.
162             * @see #encryptKey(byte[], Cipher)
163             * @see #encryptKey(byte[], String, byte[])
164             */
165            public static byte[] decryptKey(Cipher decrypter, byte[] keyEncodedEncrypted) throws CryptoException, IOException, NoSuchAlgorithmException
166            {
167                    int encryptedOff = 0;
168                    int version = keyEncodedEncrypted[encryptedOff++] & 0xff;
169                    if (version != 1)
170                            throw new IllegalArgumentException("keyEncodedEncrypted is of version " + version + " which is not supported!");
171    
172                    int macKeyLength = keyEncodedEncrypted[encryptedOff++] & 0xff;
173                    int macIVLength = keyEncodedEncrypted[encryptedOff++] & 0xff;
174                    int macLength = keyEncodedEncrypted[encryptedOff++] & 0xff;
175    
176                    int outputSize = decrypter.getOutputSize(keyEncodedEncrypted.length - encryptedOff);
177                    byte[] out = new byte[outputSize];
178    
179                    int outOff = 0;
180                    outOff += decrypter.update(keyEncodedEncrypted, encryptedOff, keyEncodedEncrypted.length - encryptedOff, out, outOff);
181                    outOff += decrypter.doFinal(out, outOff);
182    
183                    int dataOff = 0;
184                    MACCalculator macCalculator = CryptoRegistry.sharedInstance().createMACCalculator(MAC_ALGORITHM, false);
185    
186                    CipherParameters macKeyParam = new KeyParameter(out, 0, macKeyLength);
187                    dataOff += macKeyLength;
188    
189                    CipherParameters macParams;
190                    if (macIVLength == 0)
191                            macParams = macKeyParam;
192                    else {
193                            macParams = new ParametersWithIV(macKeyParam, out, dataOff, macIVLength);
194                            dataOff += macIVLength;
195                    }
196    
197                    macCalculator.init(macParams);
198    
199                    int dataLength = outOff - dataOff - macLength;
200                    int macOff = dataOff + dataLength;
201    
202                    if (macCalculator != null) {
203                            byte[] newMAC = new byte[macCalculator.getMacSize()];
204                            macCalculator.update(out, dataOff, dataLength);
205                            macCalculator.doFinal(newMAC, 0);
206    
207                            if (newMAC.length != macLength)
208                                    throw new IOException("MACs have different length! Expected MAC has " + macLength + " bytes and newly calculated MAC has " + newMAC.length + " bytes!");
209    
210                            for (int i = 0; i < macLength; ++i) {
211                                    byte expected = out[macOff + i];
212                                    if (expected != newMAC[i])
213                                            throw new IOException("MAC mismatch! mac[" + i + "] was expected to be " + expected + " but was " + newMAC[i]);
214                            }
215                    }
216    
217                    byte[] decrypted = new byte[dataLength];
218                    System.arraycopy(out, dataOff, decrypted, 0, decrypted.length);
219    
220                    return decrypted;
221            }
222    }