1 | /* |
2 | |
3 | Derby - Class org.apache.derby.impl.services.jce.JCECipherFactory |
4 | |
5 | Copyright 2000, 2004 The Apache Software Foundation or its licensors, as applicable. |
6 | |
7 | Licensed under the Apache License, Version 2.0 (the "License"); |
8 | you may not use this file except in compliance with the License. |
9 | You may obtain a copy of the License at |
10 | |
11 | http://www.apache.org/licenses/LICENSE-2.0 |
12 | |
13 | Unless required by applicable law or agreed to in writing, software |
14 | distributed under the License is distributed on an "AS IS" BASIS, |
15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
16 | See the License for the specific language governing permissions and |
17 | limitations under the License. |
18 | |
19 | */ |
20 | |
21 | package org.apache.derby.impl.services.jce; |
22 | |
23 | import org.apache.derby.iapi.services.crypto.CipherFactory; |
24 | import org.apache.derby.iapi.services.crypto.CipherProvider; |
25 | |
26 | import org.apache.derby.iapi.services.monitor.Monitor; |
27 | import org.apache.derby.iapi.services.sanity.SanityManager; |
28 | |
29 | import org.apache.derby.iapi.error.StandardException; |
30 | |
31 | import org.apache.derby.iapi.services.info.JVMInfo; |
32 | import org.apache.derby.iapi.util.StringUtil; |
33 | |
34 | import org.apache.derby.iapi.reference.SQLState; |
35 | import org.apache.derby.iapi.reference.Attribute; |
36 | import org.apache.derby.iapi.util.StringUtil; |
37 | |
38 | import java.util.Properties; |
39 | import java.util.Enumeration; |
40 | import java.security.Key; |
41 | import java.security.Provider; |
42 | import java.security.SecureRandom; |
43 | import java.security.Security; |
44 | import java.security.InvalidKeyException; |
45 | import java.security.NoSuchAlgorithmException; |
46 | import java.security.MessageDigest; |
47 | import java.security.spec.KeySpec; |
48 | import java.security.spec.InvalidKeySpecException; |
49 | import java.io.FileNotFoundException; |
50 | import java.io.IOException; |
51 | |
52 | import javax.crypto.KeyGenerator; |
53 | import javax.crypto.SecretKey; |
54 | import javax.crypto.SecretKeyFactory; |
55 | import javax.crypto.spec.DESKeySpec; |
56 | import javax.crypto.spec.SecretKeySpec; |
57 | import org.apache.derby.iapi.store.raw.RawStoreFactory; |
58 | |
59 | import org.apache.derby.io.StorageFactory; |
60 | import org.apache.derby.io.WritableStorageFactory; |
61 | import org.apache.derby.io.StorageFile; |
62 | import org.apache.derby.io.StorageRandomAccessFile; |
63 | /** |
64 | This CipherFactory creates new JCECipherProvider. |
65 | |
66 | @see CipherFactory |
67 | */ |
68 | public final class JCECipherFactory implements CipherFactory, java.security.PrivilegedExceptionAction |
69 | { |
70 | private final static String MESSAGE_DIGEST = "MD5"; |
71 | |
72 | private final static String DEFAULT_PROVIDER = "com.sun.crypto.provider.SunJCE"; |
73 | private final static String DEFAULT_ALGORITHM = "DES/CBC/NoPadding"; |
74 | private final static String DES = "DES"; |
75 | private final static String DESede = "DESede"; |
76 | private final static String TripleDES = "TripleDES"; |
77 | private final static String AES = "AES"; |
78 | |
79 | // minimum boot password length in bytes |
80 | private final static int BLOCK_LENGTH = 8; |
81 | |
82 | /** |
83 | AES encryption takes in an default Initialization vector length (IV) length of 16 bytes |
84 | This is needed to generate an IV to use for encryption and decryption process |
85 | @see CipherProvider |
86 | */ |
87 | private final static int AES_IV_LENGTH = 16; |
88 | |
89 | // key length in bytes |
90 | private int keyLengthBits; |
91 | private int encodedKeyLength; |
92 | private String cryptoAlgorithm; |
93 | private String cryptoAlgorithmShort; |
94 | private String cryptoProvider; |
95 | private String cryptoProviderShort; |
96 | private MessageDigest messageDigest; |
97 | |
98 | private SecretKey mainSecretKey; |
99 | private byte[] mainIV; |
100 | |
101 | // properties that needs to be stored in the |
102 | // in the service.properties file. |
103 | private Properties persistentProperties; |
104 | |
105 | |
106 | /** |
107 | Amount of data that is used for verification of external encryption key |
108 | This does not include the MD5 checksum bytes |
109 | */ |
110 | private final static int VERIFYKEY_DATALEN = 4096; |
111 | private StorageFile activeFile; |
112 | private int action; |
113 | private String activePerms; |
114 | |
115 | |
116 | /* |
117 | * Constructor of JCECipherFactory, initializes the new instances. |
118 | * |
119 | * @param create true, if the database is getting configured |
120 | * for encryption. |
121 | * @param props encryption properties/attributes to use |
122 | * for creating the cipher factory. |
123 | * @param newAttrs true, if cipher factory has to be created using |
124 | * should using the new attributes specified by the user. |
125 | * For example to reencrypt the database with |
126 | * a new password. |
127 | */ |
128 | public JCECipherFactory(boolean create, |
129 | Properties props, |
130 | boolean newAttributes) |
131 | throws StandardException |
132 | { |
133 | init(create, props, newAttributes); |
134 | } |
135 | |
136 | |
137 | |
138 | static String providerErrorName(String cps) { |
139 | |
140 | return cps == null ? "default" : cps; |
141 | } |
142 | |
143 | |
144 | private byte[] generateUniqueBytes() throws StandardException |
145 | { |
146 | try { |
147 | |
148 | String provider = cryptoProviderShort; |
149 | |
150 | KeyGenerator keyGen; |
151 | if (provider == null) |
152 | { |
153 | keyGen = KeyGenerator.getInstance(cryptoAlgorithmShort); |
154 | } |
155 | else |
156 | { |
157 | if( provider.equals("BouncyCastleProvider")) |
158 | provider = "BC"; |
159 | keyGen = KeyGenerator.getInstance(cryptoAlgorithmShort, provider); |
160 | } |
161 | |
162 | keyGen.init(keyLengthBits); |
163 | |
164 | SecretKey key = keyGen.generateKey(); |
165 | |
166 | return key.getEncoded(); |
167 | |
168 | } catch (java.security.NoSuchAlgorithmException nsae) { |
169 | throw StandardException.newException(SQLState.ENCRYPTION_NOSUCH_ALGORITHM, cryptoAlgorithm, |
170 | JCECipherFactory.providerErrorName(cryptoProviderShort)); |
171 | } catch (java.security.NoSuchProviderException nspe) { |
172 | throw StandardException.newException(SQLState.ENCRYPTION_BAD_PROVIDER, |
173 | JCECipherFactory.providerErrorName(cryptoProviderShort)); |
174 | } |
175 | } |
176 | |
177 | /** |
178 | Encrypt the secretKey with the boot password. |
179 | This includes the following steps, |
180 | getting muck from the boot password and then using this to generate a key, |
181 | generating an appropriate IV using the muck |
182 | using the key and IV thus generated to create the appropriate cipher provider |
183 | and encrypting the secretKey |
184 | @return hexadecimal string of the encrypted secretKey |
185 | |
186 | @exception StandardException Standard Cloudscape error policy |
187 | */ |
188 | private String encryptKey(byte[] secretKey, byte[] bootPassword) |
189 | throws StandardException |
190 | { |
191 | // In case of AES, care needs to be taken to allow for 16 bytes muck as well |
192 | // as to have the secretKey that needs encryption to be a aligned appropriately |
193 | // AES supports 16 bytes block size |
194 | |
195 | int muckLength = secretKey.length; |
196 | if(cryptoAlgorithmShort.equals(AES)) |
197 | muckLength = AES_IV_LENGTH; |
198 | |
199 | byte[] muck = getMuckFromBootPassword(bootPassword, muckLength); |
200 | SecretKey key = generateKey(muck); |
201 | byte[] IV = generateIV(muck); |
202 | CipherProvider tmpCipherProvider = createNewCipher(ENCRYPT,key,IV); |
203 | |
204 | // store the actual secretKey.length before any possible padding |
205 | encodedKeyLength = secretKey.length; |
206 | |
207 | // for the secretKey to be encrypted, first ensure that it is aligned to the block size of the |
208 | // encryption algorithm by padding bytes appropriately if needed |
209 | secretKey = padKey(secretKey,tmpCipherProvider.getEncryptionBlockSize()); |
210 | |
211 | byte[] result = new byte[secretKey.length]; |
212 | |
213 | // encrypt the secretKey using the key generated of muck from boot password and the generated IV |
214 | tmpCipherProvider.encrypt(secretKey, 0, secretKey.length, result, 0); |
215 | |
216 | return org.apache.derby.iapi.util.StringUtil.toHexString(result, 0, result.length); |
217 | |
218 | } |
219 | |
220 | /** |
221 | For block ciphers, and algorithms using the NoPadding scheme, the data that has |
222 | to be encrypted needs to be a multiple of the expected block size for the cipher |
223 | Pad the key with appropriate padding to make it blockSize align |
224 | @param secretKey the data that needs blocksize alignment |
225 | @param blockSizeAlign secretKey needs to be blocksize aligned |
226 | @return a byte array with the contents of secretKey along with padded bytes in the end |
227 | to make it blockSize aligned |
228 | */ |
229 | private byte[] padKey(byte[] secretKey,int blockSizeAlign) |
230 | { |
231 | byte [] result = secretKey; |
232 | if(secretKey.length % blockSizeAlign != 0 ) |
233 | { |
234 | int encryptedLength = secretKey.length + blockSizeAlign - (secretKey.length % blockSizeAlign); |
235 | result = new byte[encryptedLength]; |
236 | System.arraycopy(secretKey,0,result,0,secretKey.length); |
237 | } |
238 | return result; |
239 | } |
240 | |
241 | /** |
242 | Decrypt the secretKey with the user key . |
243 | This includes the following steps, |
244 | retrieve the encryptedKey, generate the muck from the boot password and generate an appropriate IV using |
245 | the muck,and using the key and IV decrypt the encryptedKey |
246 | @return decrypted key |
247 | @exception StandardException Standard Cloudscape error policy |
248 | */ |
249 | private byte[] decryptKey(String encryptedKey, int encodedKeyCharLength, byte[] bootPassword) |
250 | throws StandardException |
251 | { |
252 | byte[] secretKey = org.apache.derby.iapi.util.StringUtil.fromHexString(encryptedKey, 0, encodedKeyCharLength); |
253 | // In case of AES, care needs to be taken to allow for 16 bytes muck as well |
254 | // as to have the secretKey that needs encryption to be a aligned appropriately |
255 | // AES supports 16 bytes block size |
256 | int muckLength; |
257 | if(cryptoAlgorithmShort.equals(AES)) |
258 | muckLength = AES_IV_LENGTH; |
259 | else |
260 | muckLength = secretKey.length; |
261 | |
262 | byte[] muck = getMuckFromBootPassword(bootPassword, muckLength); |
263 | |
264 | |
265 | // decrypt the encryptedKey with the mucked up boot password to recover |
266 | // the secretKey |
267 | SecretKey key = generateKey(muck); |
268 | byte[] IV = generateIV(muck); |
269 | |
270 | |
271 | createNewCipher(DECRYPT, key, IV). |
272 | decrypt(secretKey, 0, secretKey.length, secretKey, 0); |
273 | |
274 | return secretKey; |
275 | } |
276 | |
277 | private byte[] getMuckFromBootPassword(byte[] bootPassword, int encodedKeyByteLength) { |
278 | int ulength = bootPassword.length; |
279 | |
280 | byte[] muck = new byte[encodedKeyByteLength]; |
281 | |
282 | |
283 | int rotation = 0; |
284 | for (int i = 0; i < bootPassword.length; i++) |
285 | rotation += bootPassword[i]; |
286 | |
287 | for (int i = 0; i < encodedKeyByteLength; i++) |
288 | muck[i] = (byte)(bootPassword[(i+rotation)%ulength] ^ |
289 | (bootPassword[i%ulength] << 4)); |
290 | |
291 | return muck; |
292 | } |
293 | |
294 | /** |
295 | Generate a Key object using the input secretKey that can be used by |
296 | JCECipherProvider to encrypt or decrypt. |
297 | |
298 | @exception StandardException Standard Cloudscape Error Policy |
299 | */ |
300 | private SecretKey generateKey(byte[] secretKey) throws StandardException |
301 | { |
302 | int length = secretKey.length; |
303 | |
304 | if (length < CipherFactory.MIN_BOOTPASS_LENGTH) |
305 | throw StandardException.newException(SQLState.ILLEGAL_BP_LENGTH, new Integer(MIN_BOOTPASS_LENGTH)); |
306 | |
307 | try |
308 | { |
309 | if (cryptoAlgorithmShort.equals(DES)) |
310 | { // single DES |
311 | if (DESKeySpec.isWeak(secretKey, 0)) |
312 | { |
313 | // OK, it is weak, spice it up |
314 | byte[] spice = StringUtil.getAsciiBytes("louDScap"); |
315 | for (int i = 0; i < 7; i++) |
316 | secretKey[i] = (byte)((spice[i] << 3) ^ secretKey[i]); |
317 | } |
318 | } |
319 | return new SecretKeySpec(secretKey, cryptoAlgorithmShort); |
320 | } |
321 | catch (InvalidKeyException ike) |
322 | { |
323 | throw StandardException.newException(SQLState.CRYPTO_EXCEPTION, ike); |
324 | } |
325 | |
326 | } |
327 | |
328 | /** |
329 | Generate an IV using the input secretKey that can be used by |
330 | JCECipherProvider to encrypt or decrypt. |
331 | */ |
332 | private byte[] generateIV(byte[] secretKey) |
333 | { |
334 | |
335 | // do a little simple minded muddling to make the IV not |
336 | // strictly alphanumeric and the number of total possible keys a little |
337 | // bigger. |
338 | int IVlen = BLOCK_LENGTH; |
339 | |
340 | byte[] iv = null; |
341 | if(cryptoAlgorithmShort.equals(AES)) |
342 | { |
343 | IVlen = AES_IV_LENGTH; |
344 | iv = new byte[IVlen]; |
345 | iv[0] = (byte)(((secretKey[secretKey.length-1] << 2) | 0xF) ^ secretKey[0]); |
346 | for (int i = 1; i < BLOCK_LENGTH; i++) |
347 | iv[i] = (byte)(((secretKey[i-1] << (i%5)) | 0xF) ^ secretKey[i]); |
348 | |
349 | for(int i = BLOCK_LENGTH ; i < AES_IV_LENGTH ; i++) |
350 | { |
351 | iv[i]=iv[i-BLOCK_LENGTH]; |
352 | } |
353 | |
354 | } |
355 | else |
356 | { |
357 | iv = new byte[BLOCK_LENGTH]; |
358 | iv[0] = (byte)(((secretKey[secretKey.length-1] << 2) | 0xF) ^ secretKey[0]); |
359 | for (int i = 1; i < BLOCK_LENGTH; i++) |
360 | iv[i] = (byte)(((secretKey[i-1] << (i%5)) | 0xF) ^ secretKey[i]); |
361 | } |
362 | |
363 | return iv; |
364 | } |
365 | |
366 | private int digest(byte[] input) |
367 | { |
368 | messageDigest.reset(); |
369 | byte[] digest = messageDigest.digest(input); |
370 | byte[] condenseDigest = new byte[2]; |
371 | |
372 | // no matter how long the digest is, condense it into an short. |
373 | for (int i = 0; i < digest.length; i++) |
374 | condenseDigest[i%2] ^= digest[i]; |
375 | |
376 | int retval = (condenseDigest[0] & 0xFF) | ((condenseDigest[1] << 8) & 0xFF00); |
377 | |
378 | return retval; |
379 | } |
380 | |
381 | public SecureRandom getSecureRandom() { |
382 | return new SecureRandom(mainIV); |
383 | } |
384 | |
385 | public CipherProvider createNewCipher(int mode) |
386 | throws StandardException { |
387 | return createNewCipher(mode, mainSecretKey, mainIV); |
388 | } |
389 | |
390 | |
391 | private CipherProvider createNewCipher(int mode, SecretKey secretKey, |
392 | byte[] iv) |
393 | throws StandardException |
394 | { |
395 | return new JCECipherProvider(mode, secretKey, iv, cryptoAlgorithm, cryptoProviderShort); |
396 | } |
397 | |
398 | |
399 | /* |
400 | * Initilize the new instance of this class. |
401 | */ |
402 | public void init(boolean create, Properties properties, boolean newAttrs) |
403 | throws StandardException |
404 | { |
405 | |
406 | boolean provider_or_algo_specified = false; |
407 | boolean storeProperties = create; |
408 | persistentProperties = new Properties(); |
409 | |
410 | // get the external key specified by the user to |
411 | // encrypt the database. If user is reencrypting the |
412 | // database with a new encryption key, read the value of |
413 | // the new encryption key. |
414 | String externalKey = properties.getProperty((newAttrs ? |
415 | Attribute.NEW_CRYPTO_EXTERNAL_KEY: |
416 | Attribute.CRYPTO_EXTERNAL_KEY)); |
417 | if (externalKey != null) { |
418 | storeProperties = false; |
419 | } |
420 | |
421 | cryptoProvider = properties.getProperty(Attribute.CRYPTO_PROVIDER); |
422 | |
423 | if (cryptoProvider == null) |
424 | { |
425 | // JDK 1.3 does not create providers by itself. |
426 | if (JVMInfo.JDK_ID == JVMInfo.J2SE_13) { |
427 | |
428 | String vendor; |
429 | try { |
430 | vendor = System.getProperty("java.vendor", ""); |
431 | } catch (SecurityException se) { |
432 | vendor = ""; |
433 | } |
434 | |
435 | vendor = StringUtil.SQLToUpperCase(vendor); |
436 | |
437 | if (vendor.startsWith("IBM ")) |
438 | cryptoProvider = "com.ibm.crypto.provider.IBMJCE"; |
439 | else if (vendor.startsWith("SUN ")) |
440 | cryptoProvider = "com.sun.crypto.provider.SunJCE"; |
441 | |
442 | } |
443 | } |
444 | else |
445 | { |
446 | provider_or_algo_specified = true; |
447 | |
448 | // explictly putting the properties back into the properties |
449 | // saves then in service.properties at create time. |
450 | // if (storeProperties) |
451 | // properties.put(Attribute.CRYPTO_PROVIDER, cryptoProvider); |
452 | |
453 | int dotPos = cryptoProvider.lastIndexOf('.'); |
454 | if (dotPos == -1) |
455 | cryptoProviderShort = cryptoProvider; |
456 | else |
457 | cryptoProviderShort = cryptoProvider.substring(dotPos+1); |
458 | |
459 | } |
460 | |
461 | cryptoAlgorithm = properties.getProperty(Attribute.CRYPTO_ALGORITHM); |
462 | if (cryptoAlgorithm == null) |
463 | cryptoAlgorithm = DEFAULT_ALGORITHM; |
464 | else { |
465 | provider_or_algo_specified = true; |
466 | |
467 | } |
468 | |
469 | // explictly putting the properties back into the properties |
470 | // saves then in service.properties at create time. |
471 | if (storeProperties) |
472 | persistentProperties.put(Attribute.CRYPTO_ALGORITHM, |
473 | cryptoAlgorithm); |
474 | |
475 | int firstSlashPos = cryptoAlgorithm.indexOf('/'); |
476 | int lastSlashPos = cryptoAlgorithm.lastIndexOf('/'); |
477 | if (firstSlashPos < 0 || lastSlashPos < 0 || firstSlashPos == lastSlashPos) |
478 | throw StandardException.newException(SQLState.ENCRYPTION_BAD_ALG_FORMAT, cryptoAlgorithm); |
479 | |
480 | cryptoAlgorithmShort = cryptoAlgorithm.substring(0,firstSlashPos); |
481 | |
482 | if (provider_or_algo_specified) |
483 | { |
484 | // Track 3715 - disable use of provider/aglo specification if |
485 | // jce environment is not 1.2.1. The ExemptionMechanism class |
486 | // exists in jce1.2.1 and not in jce1.2, so try and load the |
487 | // class and if you can't find it don't allow the encryption. |
488 | // This is a requirement from the government to give cloudscape |
489 | // export clearance for 3.6. Note that the check is not needed |
490 | // if no provider/algo is specified, in that case we default to |
491 | // a DES weak encryption algorithm which also is allowed for |
492 | // export (this is how 3.5 got it's clearance). |
493 | try |
494 | { |
495 | Class c = Class.forName("javax.crypto.ExemptionMechanism"); |
496 | } |
497 | catch (Throwable t) |
498 | { |
499 | throw StandardException.newException( |
500 | SQLState.ENCRYPTION_BAD_JCE); |
501 | } |
502 | } |
503 | |
504 | // If connecting to an existing database and Attribute.CRYPTO_KEY_LENGTH is set |
505 | // then obtain the encoded key length values without padding bytes and retrieve |
506 | // the keylength in bits if boot password mechanism is used |
507 | // note: Attribute.CRYPTO_KEY_LENGTH is set during creation time to a supported |
508 | // key length in the connection url. Internally , two values are stored in this property |
509 | // if encryptionKey is used, this property will have only the encoded key length |
510 | // if boot password mechanism is used, this property will have the following |
511 | // keylengthBits-EncodedKeyLength |
512 | |
513 | if(!create) |
514 | { |
515 | // if available, parse the keylengths stored in Attribute.CRYPTO_KEY_LENGTH |
516 | if(properties.getProperty(Attribute.CRYPTO_KEY_LENGTH) != null) |
517 | { |
518 | String keyLengths = properties.getProperty(Attribute.CRYPTO_KEY_LENGTH); |
519 | int pos = keyLengths.lastIndexOf('-'); |
520 | encodedKeyLength = Integer.parseInt(keyLengths.substring(pos+1)); |
521 | if(pos != -1) |
522 | keyLengthBits = Integer.parseInt(keyLengths.substring(0,pos)); |
523 | } |
524 | } |
525 | |
526 | |
527 | // case 1 - if 'encryptionKey' is not set and 'encryptionKeyLength' is set, then use |
528 | // the 'encryptionKeyLength' property value as the keyLength in bits. |
529 | // case 2 - 'encryptionKey' property is not set and 'encryptionKeyLength' is not set, then |
530 | // use the defaults keylength: 56bits for DES, 168 for DESede and 128 for any other encryption |
531 | // algorithm |
532 | |
533 | if (externalKey == null && create) { |
534 | if(properties.getProperty(Attribute.CRYPTO_KEY_LENGTH) != null) |
535 | { |
536 | keyLengthBits = Integer.parseInt(properties.getProperty(Attribute.CRYPTO_KEY_LENGTH)); |
537 | } |
538 | else if (cryptoAlgorithmShort.equals(DES)) { |
539 | keyLengthBits = 56; |
540 | } else if (cryptoAlgorithmShort.equals(DESede) || cryptoAlgorithmShort.equals(TripleDES)) { |
541 | keyLengthBits = 168; |
542 | |
543 | } else { |
544 | keyLengthBits = 128; |
545 | } |
546 | } |
547 | |
548 | // check the feedback mode |
549 | String feedbackMode = cryptoAlgorithm.substring(firstSlashPos+1,lastSlashPos); |
550 | |
551 | if (!feedbackMode.equals("CBC") && !feedbackMode.equals("CFB") && |
552 | !feedbackMode.equals("ECB") && !feedbackMode.equals("OFB")) |
553 | throw StandardException.newException(SQLState.ENCRYPTION_BAD_FEEDBACKMODE, feedbackMode); |
554 | |
555 | // check the NoPadding mode is used |
556 | String padding = cryptoAlgorithm.substring(lastSlashPos+1,cryptoAlgorithm.length()); |
557 | if (!padding.equals("NoPadding")) |
558 | throw StandardException.newException(SQLState.ENCRYPTION_BAD_PADDING, padding); |
559 | |
560 | Throwable t; |
561 | try |
562 | { |
563 | if (cryptoProvider != null) { |
564 | // provider package should be set by property |
565 | if (Security.getProvider(cryptoProviderShort) == null) |
566 | { |
567 | action = 1; |
568 | // add provider through privileged block. |
569 | java.security.AccessController.doPrivileged(this); |
570 | } |
571 | } |
572 | |
573 | // need this to check the boot password |
574 | messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST); |
575 | |
576 | byte[] generatedKey; |
577 | if (externalKey != null) { |
578 | |
579 | // incorrect to specify external key and boot password |
580 | if (properties.getProperty((newAttrs ? |
581 | Attribute.NEW_BOOT_PASSWORD : |
582 | Attribute.BOOT_PASSWORD)) != null) |
583 | throw StandardException.newException(SQLState.SERVICE_WRONG_BOOT_PASSWORD); |
584 | |
585 | generatedKey = |
586 | org.apache.derby.iapi.util.StringUtil.fromHexString(externalKey, |
587 | 0, |
588 | externalKey.length()); |
589 | if (generatedKey == null) { |
590 | throw StandardException.newException( |
591 | // If length is even, we assume invalid character(s), |
592 | // based on how 'fromHexString' behaves. |
593 | externalKey.length() % 2 == 0 |
594 | ? SQLState.ENCRYPTION_ILLEGAL_EXKEY_CHARS |
595 | : SQLState.ENCRYPTION_INVALID_EXKEY_LENGTH); |
596 | } |
597 | |
598 | } else { |
599 | |
600 | generatedKey = handleBootPassword(create, properties, newAttrs); |
601 | if(create || newAttrs) |
602 | persistentProperties.put(Attribute.CRYPTO_KEY_LENGTH, |
603 | keyLengthBits+"-"+generatedKey.length); |
604 | } |
605 | |
606 | // Make a key and IV object out of the generated key |
607 | mainSecretKey = generateKey(generatedKey); |
608 | mainIV = generateIV(generatedKey); |
609 | |
610 | if (create) |
611 | { |
612 | persistentProperties.put(Attribute.DATA_ENCRYPTION, "true"); |
613 | |
614 | // Set two new properties to allow for future changes to the log and data encryption |
615 | // schemes. This property is introduced in version 10 , value starts at 1. |
616 | persistentProperties.put(RawStoreFactory.DATA_ENCRYPT_ALGORITHM_VERSION, |
617 | String.valueOf(1)); |
618 | persistentProperties.put(RawStoreFactory.LOG_ENCRYPT_ALGORITHM_VERSION, |
619 | String.valueOf(1)); |
620 | } |
621 | |
622 | return; |
623 | } |
624 | catch (java.security.PrivilegedActionException pae) |
625 | { |
626 | t = pae.getException(); |
627 | } |
628 | catch (NoSuchAlgorithmException nsae) |
629 | { |
630 | t = nsae; |
631 | } |
632 | catch (SecurityException se) |
633 | { |
634 | t = se; |
635 | } catch (LinkageError le) { |
636 | t = le; |
637 | } catch (ClassCastException cce) { |
638 | t = cce; |
639 | } |
640 | |
641 | throw StandardException.newException(SQLState.MISSING_ENCRYPTION_PROVIDER, t); |
642 | } |
643 | |
644 | |
645 | private byte[] handleBootPassword(boolean create, |
646 | Properties properties, |
647 | boolean newPasswd) |
648 | throws StandardException { |
649 | |
650 | |
651 | // get the key specifed by the user. If user is reencrypting the |
652 | // database; read the value of the new password. |
653 | String inputKey = properties.getProperty((newPasswd ? |
654 | Attribute.NEW_BOOT_PASSWORD : |
655 | Attribute.BOOT_PASSWORD)); |
656 | if (inputKey == null) |
657 | { |
658 | throw StandardException.newException(SQLState.SERVICE_WRONG_BOOT_PASSWORD); |
659 | } |
660 | |
661 | byte[] bootPassword = StringUtil.getAsciiBytes(inputKey); |
662 | |
663 | if (bootPassword.length < CipherFactory.MIN_BOOTPASS_LENGTH) |
664 | { |
665 | String messageId = create ? SQLState.SERVICE_BOOT_PASSWORD_TOO_SHORT : |
666 | SQLState.SERVICE_WRONG_BOOT_PASSWORD; |
667 | |
668 | throw StandardException.newException(messageId); |
669 | } |
670 | |
671 | // Each database has its own unique encryption key that is |
672 | // not known even to the user. However, this key is masked |
673 | // with the user input key and stored in the |
674 | // services.properties file so that, with the user key, the |
675 | // encryption key can easily be recovered. |
676 | // To change the user encryption key to a database, simply |
677 | // recover the unique real encryption key and masked it |
678 | // with the new user key. |
679 | |
680 | byte[] generatedKey; |
681 | |
682 | if (create || newPasswd) |
683 | { |
684 | // |
685 | generatedKey = generateUniqueBytes(); |
686 | |
687 | persistentProperties.put(RawStoreFactory.ENCRYPTED_KEY, |
688 | saveSecretKey(generatedKey, bootPassword)); |
689 | |
690 | } |
691 | else |
692 | { |
693 | generatedKey = getDatabaseSecretKey(properties, bootPassword, SQLState.SERVICE_WRONG_BOOT_PASSWORD); |
694 | } |
695 | |
696 | return generatedKey; |
697 | } |
698 | |
699 | /* |
700 | * put all the encyrpion cipger related properties that has to |
701 | * be made peristent into the database service properties list. |
702 | * @param properties properties object that is used to store |
703 | * cipher properties persistently. |
704 | */ |
705 | public void saveProperties(Properties properties) |
706 | { |
707 | // put the cipher properties to be persistent into the |
708 | // system perisistent properties. |
709 | for (Enumeration e = persistentProperties.keys(); |
710 | e.hasMoreElements(); ) |
711 | { |
712 | String key = (String) e.nextElement(); |
713 | properties.put(key, persistentProperties.get(key)); |
714 | } |
715 | |
716 | // clear the cipher properties to be persistent. |
717 | persistentProperties = null; |
718 | } |
719 | |
720 | |
721 | /** |
722 | get the secretkey used for encryption and decryption when boot password mechanism is used for encryption |
723 | Steps include |
724 | retrieve the stored key, decrypt the stored key and verify if the correct boot password was passed |
725 | There is a possibility that the decrypted key includes the original key and padded bytes in order to have |
726 | been block size aligned during encryption phase. Hence extract the original key |
727 | |
728 | @param properties properties to retrieve the encrypted key |
729 | @param bootPassword boot password used to connect to the encrypted database |
730 | @param errorState errorstate to account for any errors during retrieval /creation of the secretKey |
731 | @return the original unencrypted key bytes to use for encryption and decrytion |
732 | |
733 | */ |
734 | private byte[] getDatabaseSecretKey(Properties properties, byte[] bootPassword, String errorState) throws StandardException { |
735 | |
736 | // recover the generated secret encryption key from the |
737 | // services.properties file and the user key. |
738 | String keyString = properties.getProperty(RawStoreFactory.ENCRYPTED_KEY); |
739 | if (keyString == null) |
740 | throw StandardException.newException(errorState); |
741 | |
742 | int encodedKeyCharLength = keyString.indexOf('-'); |
743 | |
744 | if (encodedKeyCharLength == -1) // bad form |
745 | throw StandardException.newException(errorState); |
746 | |
747 | int verifyKey = Integer.parseInt(keyString.substring(encodedKeyCharLength+1)); |
748 | byte[] generatedKey = decryptKey(keyString, encodedKeyCharLength, bootPassword); |
749 | |
750 | int checkKey = digest(generatedKey); |
751 | |
752 | if (checkKey != verifyKey) |
753 | throw StandardException.newException(errorState); |
754 | |
755 | // if encodedKeyLength is not defined, then either it is an old version with no support for different |
756 | // key sizes and padding except for defaults |
757 | byte[] result; |
758 | if(encodedKeyLength != 0) |
759 | { |
760 | result = new byte[encodedKeyLength]; |
761 | |
762 | // extract the generated key without the padding bytes |
763 | System.arraycopy(generatedKey,0,result,0,encodedKeyLength); |
764 | return result; |
765 | } |
766 | |
767 | return generatedKey; |
768 | } |
769 | |
770 | private String saveSecretKey(byte[] secretKey, byte[] bootPassword) throws StandardException { |
771 | String encryptedKey = encryptKey(secretKey, bootPassword); |
772 | |
773 | // make a verification key out of the message digest of |
774 | // the generated key |
775 | int verifyKey = digest(secretKey); |
776 | |
777 | return encryptedKey.concat("-" + verifyKey); |
778 | |
779 | } |
780 | |
781 | public String changeBootPassword(String changeString, Properties properties, CipherProvider verify) |
782 | throws StandardException { |
783 | |
784 | // the new bootPassword is expected to be of the form |
785 | // oldkey , newkey. |
786 | int seperator = changeString.indexOf(','); |
787 | if (seperator == -1) |
788 | throw StandardException.newException(SQLState.WRONG_PASSWORD_CHANGE_FORMAT); |
789 | |
790 | String oldBP = changeString.substring(0, seperator).trim(); |
791 | byte[] oldBPAscii = StringUtil.getAsciiBytes(oldBP); |
792 | if (oldBPAscii == null || oldBPAscii.length < CipherFactory.MIN_BOOTPASS_LENGTH) |
793 | throw StandardException.newException(SQLState.WRONG_BOOT_PASSWORD);; |
794 | |
795 | String newBP = changeString.substring(seperator+1).trim(); |
796 | byte[] newBPAscii = StringUtil.getAsciiBytes(newBP); |
797 | if (newBPAscii == null || newBPAscii.length < CipherFactory.MIN_BOOTPASS_LENGTH) |
798 | throw StandardException.newException(SQLState.ILLEGAL_BP_LENGTH, |
799 | new Integer(CipherFactory.MIN_BOOTPASS_LENGTH)); |
800 | |
801 | // verify old key |
802 | |
803 | byte[] generatedKey = getDatabaseSecretKey(properties, oldBPAscii, SQLState.WRONG_BOOT_PASSWORD); |
804 | |
805 | // make sure the oldKey is correct |
806 | byte[] IV = generateIV(generatedKey); |
807 | |
808 | if (!((JCECipherProvider) verify).verifyIV(IV)) |
809 | throw StandardException.newException(SQLState.WRONG_BOOT_PASSWORD); |
810 | |
811 | |
812 | // Make the new key. The generated key is unchanged, only the |
813 | // encrypted key is changed. |
814 | String newkey = saveSecretKey(generatedKey, newBPAscii); |
815 | |
816 | properties.put(Attribute.CRYPTO_KEY_LENGTH,keyLengthBits+"-"+encodedKeyLength); |
817 | |
818 | |
819 | return saveSecretKey(generatedKey, newBPAscii); |
820 | } |
821 | |
822 | /** |
823 | perform actions with privileges enabled. |
824 | */ |
825 | public final Object run() throws StandardException, InstantiationException, IllegalAccessException { |
826 | |
827 | try { |
828 | |
829 | switch(action) |
830 | { |
831 | case 1: |
832 | Security.addProvider( |
833 | (Provider)(Class.forName(cryptoProvider).newInstance())); |
834 | break; |
835 | case 2: |
836 | // SECURITY PERMISSION - MP1 and/or OP4 |
837 | // depends on the value of activePerms |
838 | return activeFile.getRandomAccessFile(activePerms); |
839 | |
840 | } |
841 | |
842 | } catch (ClassNotFoundException cnfe) { |
843 | throw StandardException.newException(SQLState.ENCRYPTION_NO_PROVIDER_CLASS,cryptoProvider); |
844 | } |
845 | catch(FileNotFoundException fnfe) { |
846 | throw StandardException.newException(SQLState.ENCRYPTION_UNABLE_KEY_VERIFICATION,cryptoProvider); |
847 | } |
848 | return null; |
849 | } |
850 | |
851 | |
852 | |
853 | /** |
854 | The database can be encrypted with an encryption key given in connection url. |
855 | For security reasons, this key is not made persistent in the database. |
856 | |
857 | But it is necessary to verify the encryption key when booting the database if it is similar |
858 | to the one used when creating the database |
859 | This needs to happen before we access the data/logs to avoid the risk of corrupting the |
860 | database because of a wrong encryption key. |
861 | |
862 | This method performs the steps necessary to verify the encryption key if an external |
863 | encryption key is given. |
864 | |
865 | At database creation, 4k of random data is generated using SecureRandom and MD5 is used |
866 | to compute the checksum for the random data thus generated. This 4k page of random data |
867 | is then encrypted using the encryption key. The checksum of unencrypted data and |
868 | encrypted data is made persistent in the database in file by name given by |
869 | Attribute.CRYPTO_EXTERNAL_KEY_VERIFYFILE (verifyKey.dat). This file exists directly under the |
870 | database root directory. |
871 | |
872 | When trying to boot an existing encrypted database, the given encryption key is used to decrypt |
873 | the data in the verifyKey.dat and the checksum is calculated and compared against the original |
874 | stored checksum. If these checksums dont match an exception is thrown. |
875 | |
876 | Please note, this process of verifying the key does not provide any added security but only is |
877 | intended to allow to fail gracefully if a wrong encryption key is used |
878 | |
879 | StandardException is thrown if there are any problems during the process of verification |
880 | of the encryption key or if there is any mismatch of the encryption key. |
881 | |
882 | */ |
883 | |
884 | public void verifyKey(boolean create, StorageFactory sf, Properties properties) |
885 | throws StandardException |
886 | { |
887 | |
888 | if(properties.getProperty(Attribute.CRYPTO_EXTERNAL_KEY) == null) |
889 | return; |
890 | |
891 | // if firstTime ( ie during creation of database, initial key used ) |
892 | // In order to allow for verifying the external key for future database boot, |
893 | // generate random 4k of data and store the encrypted random data and the checksum |
894 | // using MD5 of the unencrypted data. That way, on next database boot a check is performed |
895 | // to verify if the key is the same as used when the database was created |
896 | |
897 | StorageRandomAccessFile verifyKeyFile = null; |
898 | byte[] data = new byte[VERIFYKEY_DATALEN]; |
899 | try |
900 | { |
901 | if(create) |
902 | { |
903 | getSecureRandom().nextBytes(data); |
904 | // get the checksum |
905 | byte[] checksum = getMD5Checksum(data); |
906 | |
907 | CipherProvider tmpCipherProvider = createNewCipher(ENCRYPT,mainSecretKey,mainIV); |
908 | tmpCipherProvider.encrypt(data, 0, data.length, data, 0); |
909 | // openFileForWrite |
910 | verifyKeyFile = privAccessFile(sf,Attribute.CRYPTO_EXTERNAL_KEY_VERIFY_FILE,"rw"); |
911 | // write the checksum length as int, and then the checksum and then the encrypted data |
912 | verifyKeyFile.writeInt(checksum.length); |
913 | verifyKeyFile.write(checksum); |
914 | verifyKeyFile.write(data); |
915 | verifyKeyFile.sync(true); |
916 | } |
917 | else |
918 | { |
919 | // open file for reading only |
920 | verifyKeyFile = privAccessFile(sf,Attribute.CRYPTO_EXTERNAL_KEY_VERIFY_FILE,"r"); |
921 | // then read the checksum length |
922 | int checksumLen = verifyKeyFile.readInt(); |
923 | |
924 | byte[] originalChecksum = new byte[checksumLen]; |
925 | verifyKeyFile.readFully(originalChecksum); |
926 | |
927 | verifyKeyFile.readFully(data); |
928 | |
929 | // decrypt data with key |
930 | CipherProvider tmpCipherProvider = createNewCipher(DECRYPT,mainSecretKey,mainIV); |
931 | tmpCipherProvider.decrypt(data, 0, data.length, data, 0); |
932 | |
933 | byte[] verifyChecksum = getMD5Checksum(data); |
934 | |
935 | if(!MessageDigest.isEqual(originalChecksum,verifyChecksum)) |
936 | { |
937 | throw StandardException.newException(SQLState.ENCRYPTION_BAD_EXTERNAL_KEY); |
938 | } |
939 | |
940 | } |
941 | } |
942 | catch(IOException ioe) |
943 | { |
944 | throw StandardException.newException(SQLState.ENCRYPTION_UNABLE_KEY_VERIFICATION,ioe); |
945 | } |
946 | finally |
947 | { |
948 | try |
949 | { |
950 | if(verifyKeyFile != null) |
951 | verifyKeyFile.close(); |
952 | } |
953 | catch(IOException ioee) |
954 | { |
955 | throw StandardException.newException(SQLState.ENCRYPTION_UNABLE_KEY_VERIFICATION,ioee); |
956 | } |
957 | } |
958 | return ; |
959 | } |
960 | |
961 | |
962 | /** |
963 | Use MD5 MessageDigest algorithm to generate checksum |
964 | @param data data to be used to compute the hash value |
965 | @return returns the hash value computed using the data |
966 | |
967 | */ |
968 | private byte[] getMD5Checksum(byte[] data) |
969 | throws StandardException |
970 | { |
971 | try |
972 | { |
973 | // get the checksum |
974 | MessageDigest md5 = MessageDigest.getInstance("MD5"); |
975 | return md5.digest(data); |
976 | } |
977 | catch(NoSuchAlgorithmException nsae) |
978 | { |
979 | throw StandardException.newException(SQLState.ENCRYPTION_BAD_ALG_FORMAT,MESSAGE_DIGEST); |
980 | } |
981 | |
982 | } |
983 | |
984 | |
985 | /** |
986 | access a file for either read/write |
987 | @param storageFactory factory used for io access |
988 | @param fileName name of the file to create and open for write |
989 | The file will be created directly under the database root directory |
990 | @param filePerms file permissions, if "rw" open file with read and write permissions |
991 | if "r" , open file with read permissions |
992 | @return StorageRandomAccessFile returns file with fileName for writing |
993 | @exception IOException Any exception during accessing the file for read/write |
994 | */ |
995 | private StorageRandomAccessFile privAccessFile(StorageFactory storageFactory,String fileName,String filePerms) |
996 | throws java.io.IOException |
997 | { |
998 | StorageFile verifyKeyFile = storageFactory.newStorageFile("",fileName); |
999 | activeFile = verifyKeyFile; |
1000 | this.action = 2; |
1001 | activePerms = filePerms; |
1002 | try |
1003 | { |
1004 | return (StorageRandomAccessFile)java.security.AccessController.doPrivileged(this); |
1005 | } |
1006 | catch( java.security.PrivilegedActionException pae) |
1007 | { |
1008 | throw (java.io.IOException)pae.getException(); |
1009 | } |
1010 | } |
1011 | |
1012 | |
1013 | } |