/
AES.java
333 lines (301 loc) · 12.8 KB
/
AES.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package net.ME1312.SubData.Client.Encryption;
import net.ME1312.Galaxi.Library.Container.ContainedPair;
import net.ME1312.Galaxi.Library.Container.Pair;
import net.ME1312.SubData.Client.DataClient;
import net.ME1312.SubData.Client.Library.Exception.EncryptionException;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Random;
/**
* A class to perform password-based AES encryption and decryption in CBC mode.
* 128, 192, and 256-bit encryption are supported, provided that the latter two
* are permitted by the Java runtime's jurisdiction policy files.
* <br/>
* The public interface for this class consists of the static methods
* {@link #encrypt} and {@link #decrypt}, which encrypt and decrypt arbitrary
* streams of data, respectively.
*
* @author dweymouth@gmail.com
*/
public final class AES implements net.ME1312.SubData.Client.Cipher {
// AES specification
private static final String CIPHER_SPEC = "AES/CBC/PKCS5Padding";
// Key derivation specification
private static final String KEYGEN_SPEC = "PBKDF2WithHmacSHA256";
private static final int SALT_LENGTH = 16; // in bytes
private static final int AUTH_KEY_LENGTH = 8; // in bytes
private static final int ITERATIONS = 32768;
// Process input/output streams in chunks
private static final int BUFFER_SIZE = 1024;
// Hold Data for use by SubData Cipher methods
private final int keyLength;
private final String key;
/**
* Initialize AES Cipher
*
* @param keyLength 128, 192, or 256 bit mode
* @param key Password
*/
public AES(int keyLength, String key) {
if (keyLength != 128 && keyLength != 192 && keyLength != 256) throw new IllegalArgumentException(Integer.toString(keyLength));
this.keyLength = keyLength;
this.key = key;
}
/**
* Initialize Random AES Cipher
*
* @param keyLength 128, 192, or 256 bit mode
*/
static Pair<net.ME1312.SubData.Client.Cipher, String> random(int keyLength) {
StringBuilder builder = new StringBuilder();
SecureRandom random = new SecureRandom();
int i = random.nextInt(keyLength + 1) + keyLength;
while (i > 0) {
builder.append((char) random.nextInt(Character.MAX_VALUE + 1));
i -= (16 / 2); // char uses 16 bytes
}
String key = builder.toString();
return new ContainedPair<>(new AES(keyLength, key), key);
}
/**
* @return a new pseudorandom salt of the specified length
*/
private static byte[] generateSalt(int length) {
Random r = new SecureRandom();
byte[] salt = new byte[length];
r.nextBytes(salt);
return salt;
}
/**
* Derive an AES encryption key and authentication key from given password and salt,
* using PBKDF2 key stretching. The authentication key is 64 bits long.
* @param keyLength
* length of the AES key in bits (128, 192, or 256)
* @param password
* the password from which to derive the keys
* @param salt
* the salt from which to derive the keys
* @return a Keys object containing the two generated keys
*/
private static Keys keygen(int keyLength, char[] password, byte[] salt) {
SecretKeyFactory factory;
try {
factory = SecretKeyFactory.getInstance(KEYGEN_SPEC);
} catch (NoSuchAlgorithmException impossible) { return null; }
// derive a longer key, then split into AES key and authentication key
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, keyLength + AUTH_KEY_LENGTH * 8);
SecretKey tmp = null;
try {
tmp = factory.generateSecret(spec);
} catch (InvalidKeySpecException impossible) { }
byte[] fullKey = tmp.getEncoded();
SecretKey authKey = new SecretKeySpec( // key for password authentication
Arrays.copyOfRange(fullKey, 0, AUTH_KEY_LENGTH), "AES");
SecretKey encKey = new SecretKeySpec( // key for AES encryption
Arrays.copyOfRange(fullKey, AUTH_KEY_LENGTH, fullKey.length), "AES");
return new Keys(encKey, authKey);
}
/**
* Encrypts a stream of data. The encrypted stream consists of a header
* followed by the raw AES data. The header is broken down as follows:<br/>
* <ul>
* <li><b>keyLength</b>: AES key length in bytes (valid for 16, 24, 32) (1 byte)</li>
* <li><b>salt</b>: pseudorandom salt used to derive keys from password (16 bytes)</li>
* <li><b>authentication key</b> (derived from password and salt, used to
* check validity of password upon decryption) (8 bytes)</li>
* <li><b>IV</b>: pseudorandom AES initialization vector (16 bytes)</li>
* </ul>
*
* @param keyLength
* key length to use for AES encryption (must be 128, 192, or 256)
* @param password
* password to use for encryption
* @param input
* an arbitrary byte stream to encrypt
* @param output
* stream to which encrypted data will be written
* @throws AES.InvalidKeyLengthException
* if keyLength is not 128, 192, or 256
* @throws AES.StrongEncryptionNotAvailableException
* if keyLength is 192 or 256, but the Java runtime's jurisdiction
* policy files do not allow 192- or 256-bit encryption
* @throws IOException
*/
public static void encrypt(int keyLength, String password, InputStream input, OutputStream output)
throws InvalidKeyLengthException, StrongEncryptionNotAvailableException, IOException {
// Check validity of key length
if (keyLength != 128 && keyLength != 192 && keyLength != 256) {
throw new InvalidKeyLengthException(keyLength);
}
// generate salt and derive keys for authentication and encryption
byte[] salt = generateSalt(SALT_LENGTH);
Keys keys = keygen(keyLength, password.toCharArray(), salt);
// initialize AES encryption
Cipher encrypt = null;
try {
encrypt = Cipher.getInstance(CIPHER_SPEC);
encrypt.init(Cipher.ENCRYPT_MODE, keys.encryption);
} catch (NoSuchAlgorithmException | NoSuchPaddingException impossible) { }
catch (InvalidKeyException e) { // 192 or 256-bit AES not available
throw new StrongEncryptionNotAvailableException(keyLength);
}
// get initialization vector
byte[] iv = null;
try {
iv = encrypt.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
} catch (InvalidParameterSpecException impossible) { }
// write authentication and AES initialization data
output.write(keyLength / 8);
output.write(salt);
output.write(keys.authentication.getEncoded());
output.write(iv);
// read data from input into buffer, encrypt and write to output
byte[] buffer = new byte[BUFFER_SIZE];
int numRead;
byte[] encrypted = null;
while ((numRead = input.read(buffer)) > 0) {
encrypted = encrypt.update(buffer, 0, numRead);
if (encrypted != null) {
output.write(encrypted);
}
}
try { // finish encryption - do final block
encrypted = encrypt.doFinal();
} catch (IllegalBlockSizeException | BadPaddingException impossible) { }
if (encrypted != null) {
output.write(encrypted);
}
output.flush();
}
@Override
public void encrypt(DataClient client, InputStream in, OutputStream out) throws Exception {
encrypt(keyLength, key, in, out);
}
public String getName() {
return "AES-" + keyLength;
}
/**
* Decrypts a stream of data that was encrypted by {@link #encrypt}.
* @param password
* the password used to encrypt/decrypt the stream
* @param input
* stream of encrypted data to be decrypted
* @param output
* stream to which decrypted data will be written
* @return the key length for the decrypted stream (128, 192, or 256)
* @throws AES.InvalidPasswordException
* if the given password was not used to encrypt the data
* @throws AES.InvalidAESStreamException
* if the given input stream is not a valid AES-encrypted stream
* @throws AES.StrongEncryptionNotAvailableException
* if the stream is 192 or 256-bit encrypted, and the Java runtime's
* jurisdiction policy files do not allow for AES-192 or 256
* @throws IOException
*/
public static int decrypt(String password, InputStream input, OutputStream output)
throws InvalidPasswordException, InvalidAESStreamException, IOException,
StrongEncryptionNotAvailableException {
int keyLength = input.read() * 8;
// Check validity of key length
if (keyLength != 128 && keyLength != 192 && keyLength != 256) {
throw new InvalidAESStreamException();
}
// read salt, generate keys, and authenticate password
byte[] salt = new byte[SALT_LENGTH];
input.read(salt);
Keys keys = keygen(keyLength, password.toCharArray(), salt);
byte[] authRead = new byte[AUTH_KEY_LENGTH];
input.read(authRead);
if (!Arrays.equals(keys.authentication.getEncoded(), authRead)) {
throw new InvalidPasswordException();
}
// initialize AES decryption
byte[] iv = new byte[16]; // 16-byte I.V. regardless of key size
input.read(iv);
Cipher decrypt = null;
try {
decrypt = Cipher.getInstance(CIPHER_SPEC);
decrypt.init(Cipher.DECRYPT_MODE, keys.encryption, new IvParameterSpec(iv));
} catch (NoSuchAlgorithmException | NoSuchPaddingException
| InvalidAlgorithmParameterException impossible) { }
catch (InvalidKeyException e) { // 192 or 256-bit AES not available
throw new StrongEncryptionNotAvailableException(keyLength);
}
// read data from input into buffer, decrypt and write to output
byte[] buffer = new byte[BUFFER_SIZE];
int numRead;
byte[] decrypted;
while ((numRead = input.read(buffer)) > 0) {
decrypted = decrypt.update(buffer, 0, numRead);
if (decrypted != null) {
output.write(decrypted);
}
}
try { // finish decryption - do final block
decrypted = decrypt.doFinal();
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new InvalidAESStreamException(e);
}
if (decrypted != null) {
output.write(decrypted);
}
output.flush();
return keyLength;
}
@SuppressWarnings("unchecked")
@Override
public void decrypt(DataClient client, InputStream in, OutputStream out) throws Exception {
decrypt(key, in, out);
}
/**
* A tuple of encryption and authentication keys returned by {@link #keygen}
*/
private static class Keys {
public final SecretKey encryption, authentication;
public Keys(SecretKey encryption, SecretKey authentication) {
this.encryption = encryption;
this.authentication = authentication;
}
}
/**
* Thrown if an attempt is made to decrypt a stream with an incorrect password.
*/
public static class InvalidPasswordException extends EncryptionException { }
/**
* Thrown if an attempt is made to encrypt a stream with an invalid AES key length.
*/
public static class InvalidKeyLengthException extends EncryptionException {
InvalidKeyLengthException(int length) {
super("Invalid AES key length: " + length);
}
}
/**
* Thrown if 192- or 256-bit AES encryption or decryption is attempted,
* but not available on the particular Java platform.
*/
public static class StrongEncryptionNotAvailableException extends EncryptionException {
public StrongEncryptionNotAvailableException(int keySize) {
super(keySize + "-bit AES encryption is not available on this Java platform.");
}
}
/**
* Thrown if an attempt is made to decrypt an invalid AES stream.
*/
public static class InvalidAESStreamException extends EncryptionException {
public InvalidAESStreamException() { super(); };
public InvalidAESStreamException(Exception e) { super(e); }
}
}