· 7 years ago · Nov 15, 2018, 04:38 PM
1import java.io.*;
2import java.nio.file.*;
3import java.security.*;
4import java.security.spec.*;
5import java.util.*;
6
7import javax.crypto.*;
8import javax.crypto.spec.*;
9import javax.security.auth.*;
10
11final class PasswordBasedEncryption {
12
13 private static final String KEY_DERIVATION_FUNCTION = "PBKDF2WithHmacSHA256";
14 private static final String ENCRYPTION_ALGORITHM = "AES";
15 private static final String TRANSFORMATION = "AES/GCM/NoPadding";
16 private static final String GENERATOR_ALGORITHM = "SHA1PRNG";
17
18 private static final int KEY_SIZE_IN_BITS = 128;
19 private static final int IV_SIZE_IN_BITS = 128;
20 private static final int TAG_SIZE_IN_BITS = 128;
21 private static final int ITERATION_COUNT = 200_000;
22
23 private PasswordBasedEncryption() {}
24
25 /**
26 * Derives a secret key from a password.
27 * This operation needs to take long enough to avoid brute forcing by an attacker.
28 * Salts are used to limit the use of rainbow tables.
29 * If you need to store multiple passwords in a database, each should be derived using a different salt.
30 * Salts are not secret data, and can be stored plainly.
31 */
32 private static SecretKey deriveKey(char[] password, byte[] salt) {
33 // Put all of our data in a spec, so we can pass it to the key factory. This is essentially just a container for data.
34 // The iteration count is used to lengthen the process of key derivation to avoid brute forcing.
35 PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, ITERATION_COUNT, KEY_SIZE_IN_BITS);
36 SecretKey pbeKey = null;
37 byte[] keyBytes = null;
38
39 try {
40 // We use PBKDF2 because it's appropriate for turning passwords into keys.
41 SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION_FUNCTION);
42 pbeKey = factory.generateSecret(pbeKeySpec);
43
44 // Our encryption algorithm is based on AES, so we need to convert the PBE key to an AES key.
45 keyBytes = pbeKey.getEncoded();
46 return new SecretKeySpec(keyBytes, ENCRYPTION_ALGORITHM);
47 }
48
49 // These exceptions should not occur.
50 // If they do, our program is bugged and we should throw an assertion error.
51 catch (
52 NoSuchAlgorithmException |
53 InvalidKeySpecException ex
54 ) {
55 throw new AssertionError(ex);
56 }
57
58 // Always clean up sensitive data that we're not using anymore.
59 finally {
60 try {
61 pbeKeySpec.clearPassword();
62
63 if (keyBytes != null)
64 Arrays.fill(keyBytes, (byte) 0);
65
66 if (pbeKey != null && !pbeKey.isDestroyed())
67 pbeKey.destroy();
68 }
69
70 // For some reason, you can't destroy PBE keys, but I think it's best to try.
71 // We ignore the exception because it's not allowed to propagate exceptions from finally-blocks.
72 catch (DestroyFailedException ex) {}
73 }
74 }
75
76 /**
77 * Generates an Initialization Vector.
78 * IVs need to be cryptographically secure and completely random.
79 * You must always treat IVs as secret until you've used them in your encryption step.
80 * After the encryption step, you can store them plainly.
81 * IVs must be used only once per encryption step. After that, they must only be used to decrypt.
82 */
83 private static byte[] generateIV() {
84 try {
85 SecureRandom random = SecureRandom.getInstance(GENERATOR_ALGORITHM);
86 byte[] iv = new byte[IV_SIZE_IN_BITS / 8];
87 random.nextBytes(iv);
88 return iv;
89 }
90
91 // I'm pretty sure SHA1 PRNGs are supported by Java, so this exception should never occur.
92 catch (NoSuchAlgorithmException ex) {
93 throw new AssertionError(ex);
94 }
95 }
96
97 /**
98 * Creates and initializes a new cipher.
99 * A key is derived from the password, using the IV as the salt.
100 * The IV is also used for encryption.
101 * This is only appropriate when you require a user to enter a password when encrypting or decrypting a message.
102 * In other cases, ciphers should be initialized using an existing secret key.
103 */
104 private static Cipher initCipher(int mode, char[] password, byte[] iv) {
105 // We're using AES in Galois Counter Mode, here we're preparing the spec.
106 // The tag size is the size of the tag that AES-GCM uses to sign and authenticate the encrypted data with.
107 // Authentication is important, because it helps us detect that a message wasn't forged by an attacker.
108 GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_SIZE_IN_BITS, iv);
109
110 // As mentioned before, the key is derived using the IV as a salt.
111 // This is inappropriate if you store credentials in a database, in that case the IV should only be used for the encryption step
112 // and the salt should be stored in the database separately, per key.
113 SecretKey key = deriveKey(password, iv);
114
115 try {
116 // We're using AES in Galois Counter Mode.
117 // This transformation is a form of AEAD and performs authentication as well as encryption.
118 // Data should never be encrypted without being authenticated.
119 // Since GCM is a stream cipher mode and not a block cipher mode, it doesn't require padding.
120 Cipher cipher = Cipher.getInstance(TRANSFORMATION);
121 cipher.init(mode, key, gcmSpec);
122 return cipher;
123 }
124
125 // Again, these exceptions should not occur, since we have tight control over the parameters.
126 catch (
127 NoSuchAlgorithmException |
128 NoSuchPaddingException |
129 InvalidKeyException |
130 InvalidAlgorithmParameterException ex
131 ) {
132 throw new AssertionError(ex);
133 }
134
135 // Try to destroy the key.
136 // I'm actually not sure if this is correct, because I don't know whether the cipher still uses it or whether it has its own copy.
137 // Difficult to test, because it turns out that you also can't destroy SecretKeySpecs.
138 finally {
139 try {
140 key.destroy();
141 }
142
143 // Don't let exceptions escape from finally-blocks.
144 catch (DestroyFailedException ex) {}
145 }
146 }
147
148 /**
149 * Encrypts a message with a password.
150 * The result is a concatenation of the IV and the ciphertext.
151 * The message and the password should be treated as secret, obviously.
152 * The result can be stored plainly.
153 * A different IV is used each time, so encrypting the same message twice will (and SHOULD) have completely different results.
154 */
155 static byte[] encrypt(byte[] message, char[] password) {
156 try {
157 byte[] iv = generateIV();
158
159 Cipher cipher = initCipher(Cipher.ENCRYPT_MODE, password, iv);
160 byte[] ciphertext = cipher.doFinal(message);
161
162 // The IV needs to be stored with the ciphertext, otherwise we can't decrypt the message later.
163 byte[] result = new byte[iv.length + ciphertext.length];
164 System.arraycopy(iv, 0, result, 0, iv.length);
165 System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);
166
167 return result;
168 }
169
170 // GCM is a stream mode, so block sizes are irrelevant.
171 // BadPaddingException happens during decryption, not during encryption.
172 catch (
173 IllegalBlockSizeException |
174 BadPaddingException ex
175 ) {
176 throw new AssertionError(ex);
177 }
178 }
179
180 /**
181 * Decrypts a message that was encrypted with the given password.
182 * The encrypted message must be a concatenation of IV and ciphertext.
183 * @throws BadPaddingException If the password and encrypted message don't match.
184 */
185 static byte[] decrypt(byte[] encrypted, char[] password) throws BadPaddingException {
186 try {
187 byte[] iv = Arrays.copyOfRange(encrypted, 0, IV_SIZE_IN_BITS / 8);
188 byte[] ciphertext = Arrays.copyOfRange(encrypted, iv.length, encrypted.length);
189
190 Cipher cipher = initCipher(Cipher.DECRYPT_MODE, password, iv);
191 byte[] message = cipher.doFinal(ciphertext);
192
193 return message;
194 }
195
196 // Block sizes are a property of block ciphers, not stream ciphers.
197 catch (IllegalBlockSizeException ex) {
198 throw new AssertionError(ex);
199 }
200 }
201}