View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.accumulo.core.security.crypto;
18  
19  import java.io.BufferedInputStream;
20  import java.io.BufferedOutputStream;
21  import java.io.ByteArrayOutputStream;
22  import java.io.DataInputStream;
23  import java.io.DataOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.io.PushbackInputStream;
28  import java.security.InvalidAlgorithmParameterException;
29  import java.security.InvalidKeyException;
30  import java.security.SecureRandom;
31  import java.util.HashMap;
32  import java.util.Map;
33  
34  import javax.crypto.Cipher;
35  import javax.crypto.CipherInputStream;
36  import javax.crypto.CipherOutputStream;
37  import javax.crypto.spec.IvParameterSpec;
38  import javax.crypto.spec.SecretKeySpec;
39  
40  import org.apache.accumulo.core.conf.Property;
41  import org.apache.log4j.Logger;
42  
43  /**
44   * This class contains the gritty details around setting up encrypted streams for reading and writing the log file. It obeys the interface CryptoModule, which
45   * other developers can implement to change out this logic as necessary.
46   * 
47   */
48  
49  @SuppressWarnings("deprecation")
50  public class DefaultCryptoModule implements CryptoModule {
51    
52    // This is how *I* like to format my variable declarations. Your mileage may vary.
53    
54    private static final String ENCRYPTION_HEADER_MARKER = "---Log File Encrypted (v1)---";
55    private static Logger log = Logger.getLogger(DefaultCryptoModule.class);
56    
57    public DefaultCryptoModule() {}
58    
59    @Override
60    public OutputStream getEncryptingOutputStream(OutputStream out, Map<String,String> cryptoOpts) throws IOException {
61      
62      log.debug("Initializing crypto output stream");
63      
64      String cipherSuite = cryptoOpts.get(Property.CRYPTO_CIPHER_SUITE.getKey());
65      
66      if (cipherSuite.equals("NullCipher")) {
67        return out;
68      }
69      
70      String algorithmName = cryptoOpts.get(Property.CRYPTO_CIPHER_ALGORITHM_NAME.getKey());
71      String secureRNG = cryptoOpts.get(Property.CRYPTO_SECURE_RNG.getKey());
72      String secureRNGProvider = cryptoOpts.get(Property.CRYPTO_SECURE_RNG_PROVIDER.getKey());
73      SecureRandom secureRandom = DefaultCryptoModuleUtils.getSecureRandom(secureRNG, secureRNGProvider);
74      int keyLength = Integer.parseInt(cryptoOpts.get(Property.CRYPTO_CIPHER_KEY_LENGTH.getKey()));
75      
76      byte[] randomKey = new byte[keyLength / 8];
77      
78      Map<CryptoInitProperty,Object> cryptoInitParams = new HashMap<CryptoInitProperty,Object>();
79      
80      secureRandom.nextBytes(randomKey);
81      cryptoInitParams.put(CryptoInitProperty.PLAINTEXT_SESSION_KEY, randomKey);
82      
83      SecretKeyEncryptionStrategy keyEncryptionStrategy = CryptoModuleFactory.getSecretKeyEncryptionStrategy(cryptoOpts
84          .get(Property.CRYPTO_SECRET_KEY_ENCRYPTION_STRATEGY_CLASS.getKey()));
85      SecretKeyEncryptionStrategyContext keyEncryptionStrategyContext = keyEncryptionStrategy.getNewContext();
86      
87      keyEncryptionStrategyContext.setPlaintextSecretKey(randomKey);
88      keyEncryptionStrategyContext.setContext(cryptoOpts);
89      
90      keyEncryptionStrategyContext = keyEncryptionStrategy.encryptSecretKey(keyEncryptionStrategyContext);
91      
92      byte[] encryptedRandomKey = keyEncryptionStrategyContext.getEncryptedSecretKey();
93      String opaqueId = keyEncryptionStrategyContext.getOpaqueKeyEncryptionKeyID();
94      
95      OutputStream cipherOutputStream = getEncryptingOutputStream(out, cryptoOpts, cryptoInitParams);
96      
97      // Get the IV from the init params, since we didn't create it but the other getEncryptingOutputStream did
98      byte[] initVector = (byte[]) cryptoInitParams.get(CryptoInitProperty.INITIALIZATION_VECTOR);
99      
100     DataOutputStream dataOut = new DataOutputStream(out);
101     
102     // Write a marker to indicate this is an encrypted log file (in case we read it a plain one and need to
103     // not try to decrypt it. Can happen during a failure when the log's encryption settings are changing.
104     dataOut.writeUTF(ENCRYPTION_HEADER_MARKER);
105     
106     // Write out the cipher suite and algorithm used to encrypt this file. In case the admin changes, we want to still
107     // decode the old format.
108     dataOut.writeUTF(cipherSuite);
109     dataOut.writeUTF(algorithmName);
110     
111     // Write the init vector to the log file
112     dataOut.writeInt(initVector.length);
113     dataOut.write(initVector);
114     
115     // Write out the encrypted session key and the opaque ID
116     dataOut.writeUTF(opaqueId);
117     dataOut.writeInt(encryptedRandomKey.length);
118     dataOut.write(encryptedRandomKey);
119     
120     // Write the secret key (encrypted) into the log file
121     // dataOut.writeInt(randomKey.length);
122     // dataOut.write(randomKey);
123     
124     return cipherOutputStream;
125   }
126   
127   @Override
128   public InputStream getDecryptingInputStream(InputStream in, Map<String,String> cryptoOpts) throws IOException {
129     DataInputStream dataIn = new DataInputStream(in);
130     
131     String marker = dataIn.readUTF();
132     
133     log.debug("Read encryption header");
134     if (marker.equals(ENCRYPTION_HEADER_MARKER)) {
135       
136       String cipherSuiteFromFile = dataIn.readUTF();
137       String algorithmNameFromFile = dataIn.readUTF();
138       
139       // Read the secret key and initialization vector from the file
140       int initVectorLength = dataIn.readInt();
141       byte[] initVector = new byte[initVectorLength];
142       dataIn.read(initVector, 0, initVectorLength);
143       
144       // Read the opaque ID and encrypted session key
145       String opaqueId = dataIn.readUTF();
146       int encryptedSecretKeyLength = dataIn.readInt();
147       byte[] encryptedSecretKey = new byte[encryptedSecretKeyLength];
148       dataIn.read(encryptedSecretKey);
149       
150       SecretKeyEncryptionStrategy keyEncryptionStrategy = CryptoModuleFactory.getSecretKeyEncryptionStrategy(cryptoOpts
151           .get(Property.CRYPTO_SECRET_KEY_ENCRYPTION_STRATEGY_CLASS.getKey()));
152       SecretKeyEncryptionStrategyContext keyEncryptionStrategyContext = keyEncryptionStrategy.getNewContext();
153       
154       keyEncryptionStrategyContext.setOpaqueKeyEncryptionKeyID(opaqueId);
155       keyEncryptionStrategyContext.setContext(cryptoOpts);
156       keyEncryptionStrategyContext.setEncryptedSecretKey(encryptedSecretKey);
157       
158       keyEncryptionStrategyContext = keyEncryptionStrategy.decryptSecretKey(keyEncryptionStrategyContext);
159       
160       byte[] secretKey = keyEncryptionStrategyContext.getPlaintextSecretKey();
161       
162       // int secretKeyLength = dataIn.readInt();
163       // byte[] secretKey = new byte[secretKeyLength];
164       // dataIn.read(secretKey, 0, secretKeyLength);
165       
166       Map<CryptoModule.CryptoInitProperty,Object> cryptoInitParams = new HashMap<CryptoModule.CryptoInitProperty,Object>();
167       cryptoInitParams.put(CryptoInitProperty.CIPHER_SUITE, cipherSuiteFromFile);
168       cryptoInitParams.put(CryptoInitProperty.ALGORITHM_NAME, algorithmNameFromFile);
169       cryptoInitParams.put(CryptoInitProperty.PLAINTEXT_SESSION_KEY, secretKey);
170       cryptoInitParams.put(CryptoInitProperty.INITIALIZATION_VECTOR, initVector);
171       
172       InputStream cipherInputStream = getDecryptingInputStream(dataIn, cryptoOpts, cryptoInitParams);
173       return cipherInputStream;
174       
175     } else {
176       // Push these bytes back on to the stream. This method is a bit roundabout but isolates our code
177       // from having to understand the format that DataOuputStream uses for its bytes.
178       ByteArrayOutputStream tempByteOut = new ByteArrayOutputStream();
179       DataOutputStream tempOut = new DataOutputStream(tempByteOut);
180       tempOut.writeUTF(marker);
181       
182       byte[] bytesToPutBack = tempByteOut.toByteArray();
183       
184       PushbackInputStream pushbackStream = new PushbackInputStream(in, bytesToPutBack.length);
185       pushbackStream.unread(bytesToPutBack);
186       
187       return pushbackStream;
188     }
189     
190   }
191   
192   @Override
193   public OutputStream getEncryptingOutputStream(OutputStream out, Map<String,String> conf, Map<CryptoModule.CryptoInitProperty,Object> cryptoInitParams) {
194     
195     log.debug("Initializing crypto output stream");
196     
197     String cipherSuite = conf.get(Property.CRYPTO_CIPHER_SUITE.getKey());
198     
199     if (cipherSuite.equals("NullCipher")) {
200       return out;
201     }
202     
203     String algorithmName = conf.get(Property.CRYPTO_CIPHER_ALGORITHM_NAME.getKey());
204     String secureRNG = conf.get(Property.CRYPTO_SECURE_RNG.getKey());
205     String secureRNGProvider = conf.get(Property.CRYPTO_SECURE_RNG_PROVIDER.getKey());
206     int keyLength = Integer.parseInt(conf.get(Property.CRYPTO_CIPHER_KEY_LENGTH.getKey()));
207     String keyStrategyName = conf.get(Property.CRYPTO_SECRET_KEY_ENCRYPTION_STRATEGY_CLASS.getKey());
208     
209     log.debug(String.format(
210         "Using cipher suite \"%s\" (algorithm \"%s\") with key length %d with RNG \"%s\" and RNG provider \"%s\" and key encryption strategy %s", cipherSuite,
211         algorithmName, keyLength, secureRNG, secureRNGProvider, keyStrategyName));
212     
213     SecureRandom secureRandom = DefaultCryptoModuleUtils.getSecureRandom(secureRNG, secureRNGProvider);
214     Cipher cipher = DefaultCryptoModuleUtils.getCipher(cipherSuite);
215     byte[] randomKey = (byte[]) cryptoInitParams.get(CryptoInitProperty.PLAINTEXT_SESSION_KEY);
216     byte[] initVector = (byte[]) cryptoInitParams.get(CryptoInitProperty.INITIALIZATION_VECTOR);
217     
218     // If they pass us an IV, use it...
219     if (initVector != null) {
220       
221       try {
222         cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(randomKey, algorithmName), new IvParameterSpec(initVector));
223       } catch (InvalidKeyException e) {
224         log.error("Accumulo encountered an unknown error in generating the secret key object (SecretKeySpec) for an encrypted stream");
225         throw new RuntimeException(e);
226       } catch (InvalidAlgorithmParameterException e) {
227         log.error("Accumulo encountered an unknown error in generating the secret key object (SecretKeySpec) for an encrypted stream");
228         throw new RuntimeException(e);
229       }
230       
231     } else {
232       // We didn't get an IV, so we'll let the cipher make one for us and then put its value back into the map so
233       // that the caller has access to it, to persist it.
234       try {
235         cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(randomKey, algorithmName), secureRandom);
236       } catch (InvalidKeyException e) {
237         log.error("Accumulo encountered an unknown error in generating the secret key object (SecretKeySpec) for the write-ahead log");
238         throw new RuntimeException(e);
239       }
240       
241       // Since the IV length is determined by the algorithm, we let the cipher generate our IV for us,
242       // rather than calling secure random directly.
243       initVector = cipher.getIV();
244       cryptoInitParams.put(CryptoInitProperty.INITIALIZATION_VECTOR, initVector);
245     }
246     
247     CipherOutputStream cipherOutputStream = new CipherOutputStream(out, cipher);
248     BufferedOutputStream bufferedCipherOutputStream = new BufferedOutputStream(cipherOutputStream);
249     
250     return bufferedCipherOutputStream;
251   }
252   
253   @Override
254   public InputStream getDecryptingInputStream(InputStream in, Map<String,String> cryptoOpts, Map<CryptoModule.CryptoInitProperty,Object> cryptoInitParams)
255       throws IOException {
256     String cipherSuite = cryptoOpts.get(Property.CRYPTO_CIPHER_SUITE.getKey());
257     String algorithmName = cryptoOpts.get(Property.CRYPTO_CIPHER_ALGORITHM_NAME.getKey());
258     String cipherSuiteFromInitParams = (String) cryptoInitParams.get(CryptoInitProperty.CIPHER_SUITE);
259     String algorithmNameFromInitParams = (String) cryptoInitParams.get(CryptoInitProperty.ALGORITHM_NAME);
260     byte[] initVector = (byte[]) cryptoInitParams.get(CryptoInitProperty.INITIALIZATION_VECTOR);
261     byte[] secretKey = (byte[]) cryptoInitParams.get(CryptoInitProperty.PLAINTEXT_SESSION_KEY);
262     
263     if (initVector == null || secretKey == null || cipherSuiteFromInitParams == null || algorithmNameFromInitParams == null) {
264       log.error("Called getDecryptingInputStream() without proper crypto init params.  Need initVector, plaintext key, cipher suite and algorithm name");
265       throw new RuntimeException("Called getDecryptingInputStream() without initialization vector and/or plaintext session key");
266     }
267     
268     // Always use the init param's cipher suite, but check it against configured one and warn about discrepencies.
269     if (!cipherSuiteFromInitParams.equals(cipherSuite) || !algorithmNameFromInitParams.equals(algorithmName))
270       log.warn(String.format("Configured cipher suite and algorithm (\"%s\" and \"%s\") is different "
271           + "from cipher suite found in log file (\"%s\" and \"%s\")", cipherSuite, algorithmName, cipherSuiteFromInitParams, algorithmNameFromInitParams));
272     
273     Cipher cipher = DefaultCryptoModuleUtils.getCipher(cipherSuiteFromInitParams);
274     
275     try {
276       cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secretKey, algorithmNameFromInitParams), new IvParameterSpec(initVector));
277     } catch (InvalidKeyException e) {
278       log.error("Error when trying to initialize cipher with secret key");
279       throw new RuntimeException(e);
280     } catch (InvalidAlgorithmParameterException e) {
281       log.error("Error when trying to initialize cipher with initialization vector");
282       throw new RuntimeException(e);
283     }
284     
285     BufferedInputStream bufferedDecryptingInputStream = new BufferedInputStream(new CipherInputStream(in, cipher));
286     
287     return bufferedDecryptingInputStream;
288     
289   }
290 }