· 7 years ago · Feb 23, 2018, 05:12 PM
1package cryptor;
2
3import java.security.InvalidAlgorithmParameterException;
4import java.security.InvalidKeyException;
5import java.security.NoSuchAlgorithmException;
6import java.security.NoSuchProviderException;
7import java.security.SecureRandom;
8import java.security.spec.InvalidKeySpecException;
9import java.security.spec.KeySpec;
10
11import javax.crypto.BadPaddingException;
12import javax.crypto.Cipher;
13import javax.crypto.IllegalBlockSizeException;
14import javax.crypto.NoSuchPaddingException;
15import javax.crypto.SecretKey;
16import javax.crypto.SecretKeyFactory;
17import javax.crypto.spec.GCMParameterSpec;
18import javax.crypto.spec.PBEKeySpec;
19import javax.crypto.spec.SecretKeySpec;
20
21/**
22 * @author trichner
23 * @created 19.02.18
24 */
25public class AesGcmCryptor {
26
27 // https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
28 private static final byte[] VERSION_BYTE = new byte[] { (byte) 0x01 };
29 private static final int AES_KEY_BITS_LENGTH = 128;
30 private static final int GCM_IV_BYTES_LENGTH = 12;
31 private static final int GCM_TAG_BYTES_LENGTH = 16;
32
33 private static final int PBKDF2_ITERATIONS = 16384;
34
35 private static final byte[] PBKDF2_SALT = hexStringToByteArray("4d3fe0d71d2abd2828e7a3196ea450d4");
36
37 /**
38 * Decrypts an AES-GCM encrypted ciphertext and is
39 * the reverse operation of {@link AesGcmCryptor#encrypt(char[], byte[])}
40 *
41 * @param password passphrase for decryption
42 * @param ciphertext encrypted bytes
43 *
44 * @return plaintext bytes
45 *
46 * @throws NoSuchPaddingException
47 * @throws NoSuchAlgorithmException
48 * @throws NoSuchProviderException
49 * @throws InvalidKeySpecException
50 * @throws InvalidAlgorithmParameterException
51 * @throws InvalidKeyException
52 * @throws BadPaddingException
53 * @throws IllegalBlockSizeException
54 * @throws IllegalArgumentException if the length or format of the ciphertext is bad
55 */
56 public byte[] decrypt(char[] password, byte[] ciphertext)
57 throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException,
58 InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
59 BadVersionException {
60
61 // input validation
62 if (ciphertext == null) {
63 throw new IllegalArgumentException("Ciphertext cannot be null.");
64 }
65
66 if (ciphertext.length <= VERSION_BYTE.length + GCM_IV_BYTES_LENGTH + GCM_TAG_BYTES_LENGTH) {
67 throw new IllegalArgumentException("Ciphertext too short.");
68 }
69
70 // The version byte must have a 0 MSB in this version,
71 // this allows us to expand the header to multiple bytes if ever necessary.
72 // The MSB indicates if the current octet is the last octet of the header.
73 if ((ciphertext[0] & (1 << 7)) != 0) {
74 throw new BadVersionException();
75 }
76
77 // The version must match.
78 for (int i = 0; i < VERSION_BYTE.length; i++) {
79 if (VERSION_BYTE[i] != ciphertext[i]) {
80 throw new BadVersionException();
81 }
82 }
83
84 // input seems legit, lets decrypt and check integrity
85
86 // derive key from password
87 SecretKey key = deriveAesKey(password, PBKDF2_SALT, AES_KEY_BITS_LENGTH);
88
89 // init cipher
90 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
91 GCMParameterSpec params = new GCMParameterSpec(GCM_TAG_BYTES_LENGTH * 8,
92 ciphertext,
93 VERSION_BYTE.length,
94 GCM_IV_BYTES_LENGTH
95 );
96 cipher.init(Cipher.DECRYPT_MODE, key, params);
97
98 // add version and IV to MAC
99 cipher.updateAAD(ciphertext, 0, GCM_IV_BYTES_LENGTH + VERSION_BYTE.length);
100
101 // decipher and check MAC
102 return cipher.doFinal(ciphertext, 13, ciphertext.length - GCM_IV_BYTES_LENGTH - VERSION_BYTE.length);
103 }
104
105 /**
106 * Encrypts a plaintext with a password.
107 *
108 * The encryption provides the following security properties:
109 * Confidentiality + Integrity
110 *
111 * This is achieved my using the AES-GCM AEAD blockmode with a randomized IV.
112 *
113 * The tag is calculated over the version byte, the IV as well as the ciphertext.
114 *
115 * Finally the encrypted bytes have the following structure:
116 * <pre>
117 * +-------------------------------------------------------------------+
118 * | | | | |
119 * | version | IV bytes | ciphertext bytes | tag |
120 * | | | | |
121 * +-------------------------------------------------------------------+
122 * Length: 1B 12B len(plaintext) bytes 16B
123 * </pre>
124 * Note: There is no padding required for AES-GCM, but this also implies that
125 * the exact plaintext length is revealed.
126 *
127 * @param password password to use for encryption
128 * @param plaintext plaintext to encrypt
129 *
130 * @throws NoSuchAlgorithmException
131 * @throws NoSuchProviderException
132 * @throws NoSuchPaddingException
133 * @throws InvalidAlgorithmParameterException
134 * @throws InvalidKeyException
135 * @throws BadPaddingException
136 * @throws IllegalBlockSizeException
137 * @throws InvalidKeySpecException
138 */
139 public byte[] encrypt(char[] password, byte[] plaintext)
140 throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException,
141 InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
142 InvalidKeySpecException {
143
144 // initialise random and generate IV (initialisation vector)
145 SecretKey key = deriveAesKey(password, PBKDF2_SALT, AES_KEY_BITS_LENGTH);
146 final byte[] iv = new byte[GCM_IV_BYTES_LENGTH];
147 SecureRandom random = SecureRandom.getInstanceStrong();
148 random.nextBytes(iv);
149
150 // encrypt
151 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
152 GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_BYTES_LENGTH * 8, iv);
153 cipher.init(Cipher.ENCRYPT_MODE, key, spec);
154
155 // add IV to MAC
156 cipher.updateAAD(VERSION_BYTE);
157 cipher.updateAAD(iv);
158
159 // encrypt and MAC plaintext
160 byte[] ciphertext = cipher.doFinal(plaintext);
161
162 // prepend VERSION and IV to ciphertext
163 byte[] encrypted = new byte[1 + GCM_IV_BYTES_LENGTH + ciphertext.length];
164 int pos = 0;
165 System.arraycopy(VERSION_BYTE, 0, encrypted, 0, VERSION_BYTE.length);
166 pos += VERSION_BYTE.length;
167 System.arraycopy(iv, 0, encrypted, pos, iv.length);
168 pos += iv.length;
169 System.arraycopy(ciphertext, 0, encrypted, pos, ciphertext.length);
170
171 return encrypted;
172 }
173
174 /**
175 * We derive a fixed length AES key with uniform entropy from a provided
176 * passphrase. This is done with PBKDF2/HMAC256 with a fixed count
177 * of iterations and a provided salt.
178 *
179 * @param password passphrase to derive key from
180 * @param salt salt for PBKDF2 if possible use a per-key salt, alternatively
181 * a random constant salt is better than no salt.
182 * @param keyLen number of key bits to output
183 *
184 * @return a SecretKey for AES derived from a passphrase
185 *
186 * @throws NoSuchAlgorithmException
187 * @throws InvalidKeySpecException
188 */
189 private SecretKey deriveAesKey(char[] password, byte[] salt, int keyLen)
190 throws NoSuchAlgorithmException, InvalidKeySpecException {
191
192 if (password == null || salt == null || keyLen <= 0) {
193 throw new IllegalArgumentException();
194 }
195 SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
196 KeySpec spec = new PBEKeySpec(password, salt, PBKDF2_ITERATIONS, AES_KEY_BITS_LENGTH);
197 SecretKey pbeKey = factory.generateSecret(spec);
198
199 return new SecretKeySpec(pbeKey.getEncoded(), "AES");
200 }
201
202 /**
203 * Helper to convert hex strings to bytes.
204 *
205 * This is neither null save nor does it go well with invalid hex strings.
206 * Therefore it is important that this method is not used with user provided strings.
207 */
208 private static byte[] hexStringToByteArray(String s) {
209
210 int len = s.length();
211
212 byte[] data = new byte[len / 2];
213 for (int i = 0; i < len - 1; i++) {
214 data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
215 }
216 return data;
217 }
218}