1 package com.github.davidmoten.security;
2
3 import static java.nio.charset.StandardCharsets.UTF_8;
4
5 import java.io.ByteArrayInputStream;
6 import java.io.ByteArrayOutputStream;
7 import java.io.File;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.io.OutputStream;
11 import java.nio.charset.Charset;
12 import java.nio.file.Files;
13 import java.security.InvalidKeyException;
14 import java.security.KeyFactory;
15 import java.security.NoSuchAlgorithmException;
16 import java.security.PrivateKey;
17 import java.security.PublicKey;
18 import java.security.spec.InvalidKeySpecException;
19 import java.security.spec.PKCS8EncodedKeySpec;
20 import java.security.spec.X509EncodedKeySpec;
21 import java.util.Base64;
22 import java.util.Optional;
23
24 import javax.crypto.Cipher;
25 import javax.crypto.CipherInputStream;
26 import javax.crypto.CipherOutputStream;
27 import javax.crypto.KeyGenerator;
28 import javax.crypto.NoSuchPaddingException;
29 import javax.crypto.SecretKey;
30 import javax.crypto.spec.SecretKeySpec;
31
32 import net.jcip.annotations.NotThreadSafe;
33
34
35
36
37
38 @NotThreadSafe
39 public final class PPK {
40
41
42
43
44
45
46
47
48
49
50
51
52 private static final String RSA_ALGORITHM = "RSA/ECB/OAEPWithSHA1AndMGF1Padding";
53 private static final String RSA = "RSA";
54 private static final String AES = "AES";
55 private static final int AES_KEY_BITS = 128;
56 private static final int AES_KEY_BYTES = AES_KEY_BITS / 8;
57 private final Optional<Cipher> publicCipher;
58 private final Optional<Cipher> privateCipher;
59 private final AesEncryption aes;
60 private final boolean unique;
61
62 private static class AesEncryption {
63
64 final byte[] encodedSecretKey;
65
66
67 final SecretKeySpec secretKeySpec;
68
69
70 final Cipher cipher;
71
72 final Optional<byte[]> rsaEncryptedSecretKeyBytes;
73
74 AesEncryption(Optional<Cipher> publicCipher) {
75 try {
76 KeyGenerator kgen = KeyGenerator.getInstance(AES);
77 kgen.init(AES_KEY_BITS);
78 SecretKey key = kgen.generateKey();
79 encodedSecretKey = key.getEncoded();
80 secretKeySpec = new SecretKeySpec(encodedSecretKey, AES);
81 cipher = Cipher.getInstance(AES);
82 if (publicCipher.isPresent()) {
83 rsaEncryptedSecretKeyBytes = Optional
84 .of(applyCipher(publicCipher.get(), encodedSecretKey));
85 if (rsaEncryptedSecretKeyBytes.get().length > 256)
86 throw new RuntimeException(
87 "unexpected length=" + rsaEncryptedSecretKeyBytes.get().length);
88 } else
89 rsaEncryptedSecretKeyBytes = Optional.empty();
90 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
91 throw new RuntimeException(e);
92 }
93 }
94
95 }
96
97 private PPK(Optional<Cipher> publicCipher, Optional<Cipher> privateCipher, boolean unique) {
98 this.publicCipher = publicCipher;
99 this.privateCipher = privateCipher;
100 this.aes = new AesEncryption(publicCipher);
101 this.unique = unique;
102 }
103
104
105
106
107
108
109
110
111
112
113
114 public static final Builder privateKey(Class<?> cls, String resource) {
115 return new Builder().privateKey(cls, resource);
116 }
117
118
119
120
121
122
123
124
125
126 public static final Builder privateKey(String resource) {
127 return new Builder().privateKey(resource);
128 }
129
130
131
132
133
134
135
136
137
138 public static final Builder privateKey(InputStream is) {
139 return new Builder().privateKey(is);
140 }
141
142
143
144
145
146
147
148
149 public static final Builder privateKey(File file) {
150 return new Builder().privateKey(file);
151 }
152
153
154
155
156
157
158
159
160
161 public static final Builder privateKey(byte[] bytes) {
162 return new Builder().privateKey(bytes);
163 }
164
165
166
167
168
169
170
171
172
173
174
175 public static final Builder publicKey(Class<?> cls, String resource) {
176 return new Builder().publicKey(cls, resource);
177 }
178
179
180
181
182
183
184
185
186
187 public static final Builder publicKey(String resource) {
188 return new Builder().publicKey(resource);
189 }
190
191 public static final Builder publicKey(File file) {
192 return new Builder().publicKey(file);
193 }
194
195 public static final Builder publicKey(InputStream is) {
196 return new Builder().publicKey(is);
197 }
198
199 public static final Builder publicKey(byte[] bytes) {
200 return new Builder().publicKey(bytes);
201 }
202
203 public static final class Builder {
204 private Optional<Cipher> publicCipher = Optional.empty();
205 private Optional<Cipher> privateCipher = Optional.empty();
206 private boolean unique = false;
207
208 private Builder() {
209
210 }
211
212 public Builder publicKey(InputStream is) {
213 Preconditions.checkNotNull(is);
214 return publicKey(Bytes.from(is));
215 }
216
217 public Builder privateKey(InputStream is) {
218 Preconditions.checkNotNull(is);
219 return privateKey(Bytes.from(is));
220 }
221
222 public Builder publicKey(byte[] bytes) {
223 Preconditions.checkNotNull(bytes);
224 publicCipher = Optional.of(readPublicCipher(bytes));
225 return this;
226 }
227
228 public Builder publicKey(String resource) {
229 Preconditions.checkNotNull(resource);
230 return publicKey(Classpath.bytesFrom(PPK.class, resource));
231 }
232
233 public Builder publicKey(Class<?> cls, String resource) {
234 Preconditions.checkNotNull(cls);
235 Preconditions.checkNotNull(resource);
236 return publicKey(Classpath.bytesFrom(cls, resource));
237 }
238
239 public Builder publicKey(File file) {
240 Preconditions.checkNotNull(file);
241 try {
242 return publicKey(Files.readAllBytes(file.toPath()));
243 } catch (IOException e) {
244 throw new RuntimeException(e);
245 }
246 }
247
248 public Builder privateKey(byte[] bytes) {
249 Preconditions.checkNotNull(bytes);
250 privateCipher = Optional.of(readPrivateCipher(bytes));
251 return this;
252 }
253
254 public Builder privateKey(String resource) {
255 Preconditions.checkNotNull(resource);
256 return privateKey(Classpath.bytesFrom(PPK.class, resource));
257 }
258
259 public Builder privateKey(Class<?> cls, String resource) {
260 Preconditions.checkNotNull(cls);
261 Preconditions.checkNotNull(resource);
262 return privateKey(Classpath.bytesFrom(cls, resource));
263 }
264
265 public Builder privateKey(File file) {
266 Preconditions.checkNotNull(file);
267 try {
268 return privateKey(Files.readAllBytes(file.toPath()));
269 } catch (IOException e) {
270 throw new RuntimeException(e);
271 }
272 }
273
274 public byte[] encrypt(byte[] bytes) {
275 return build().encrypt(bytes);
276 }
277
278 public byte[] encrypt(InputStream is) {
279 return build().encrypt(is);
280 }
281
282 public byte[] decrypt(byte[] bytes) {
283 return build().decrypt(bytes);
284 }
285
286 public byte[] encrypt(String string, Charset charset) {
287 return build().encrypt(string, charset);
288 }
289
290 public byte[] encryptRsa(byte[] bytes) {
291 return build().encryptRsa(bytes);
292 }
293
294 public byte[] encryptRsa(String string, Charset charset) {
295 return build().encryptRsa(string, charset);
296 }
297
298 public String encryptAsBase64(String string) {
299 return build().encryptAsBase64(string);
300 }
301
302 public String encryptRsaAsBase64(String string) {
303 return build().encryptRsaAsBase64(string);
304 }
305
306 public String decrypt(byte[] bytes, Charset charset) {
307 return build().decrypt(bytes, charset);
308 }
309
310 public byte[] decryptRsa(byte[] bytes) {
311 return build().decryptRsa(bytes);
312 }
313
314 public String decryptRsa(byte[] bytes, Charset charset) {
315 return build().decryptRsa(bytes, charset);
316 }
317
318 public String decryptRsaBase64(String base64) {
319 return build().decryptRsaBase64(base64);
320 }
321
322 public String decryptBase64(String base64) {
323 return build().decryptBase64(base64);
324 }
325
326 public void encrypt(InputStream is, OutputStream os) {
327 build().encrypt(is, os);
328 }
329
330 public void decrypt(InputStream is, OutputStream os) {
331 build().decrypt(is, os);
332 }
333
334 public Builder unique(boolean value) {
335 this.unique = value;
336 return this;
337 }
338
339 public Builder unique() {
340 return unique(true);
341 }
342
343 public PPK build() {
344 return new PPK(publicCipher, privateCipher, unique);
345 }
346
347 }
348
349 public void encrypt(InputStream is, OutputStream os) {
350 Preconditions.checkNotNull(is);
351 Preconditions.checkNotNull(os);
352 if (publicCipher.isPresent()) {
353 try {
354 final AesEncryption aes;
355 if (unique) {
356 aes = new AesEncryption(publicCipher);
357 } else {
358 aes = this.aes;
359 }
360 os.write(aes.rsaEncryptedSecretKeyBytes.get().length - 1);
361 os.write(aes.rsaEncryptedSecretKeyBytes.get());
362 encryptWithAes(aes, is, os);
363 } catch (IOException e) {
364 throw new RuntimeException(e);
365 }
366 } else
367 throw new PublicKeyNotSetException();
368 }
369
370 public String encryptAsBase64(String string) {
371 return Base64.getEncoder().encodeToString(encrypt(string, UTF_8));
372 }
373
374 public String decryptBase64(String base64) {
375 return decrypt(Base64.getDecoder().decode(base64), UTF_8);
376 }
377
378 public byte[] encrypt(InputStream is) {
379 Preconditions.checkNotNull(is);
380 return encrypt(Bytes.from(is));
381 }
382
383 public byte[] encrypt(byte[] bytes) {
384 Preconditions.checkNotNull(bytes);
385 try (ByteArrayInputStream is = new ByteArrayInputStream(bytes);
386 ByteArrayOutputStream os = new ByteArrayOutputStream()) {
387 encrypt(is, os);
388 return os.toByteArray();
389 } catch (IOException e) {
390 throw new RuntimeException(e);
391 }
392 }
393
394 private static void encryptWithAes(AesEncryption aes, InputStream is, OutputStream os) {
395 Preconditions.checkNotNull(is);
396 Preconditions.checkNotNull(os);
397 try {
398 aes.cipher.init(Cipher.ENCRYPT_MODE, aes.secretKeySpec);
399 applyCipher(aes.cipher, is, os);
400 } catch (InvalidKeyException e) {
401 throw new RuntimeException(e);
402 }
403 }
404
405 public void decrypt(InputStream is, OutputStream os) {
406 Preconditions.checkNotNull(is);
407 Preconditions.checkNotNull(os);
408 if (privateCipher.isPresent()) {
409 int rsaEncryptedAesSecretKeyLength;
410 byte[] raw;
411 try {
412 rsaEncryptedAesSecretKeyLength = is.read() + 1;
413 raw = new byte[rsaEncryptedAesSecretKeyLength];
414 is.read(raw);
415 } catch (IOException e) {
416 throw new RuntimeException(e);
417 }
418 ByteArrayInputStream rsaEncryptedAesSecretKeyInputStream = new ByteArrayInputStream(
419 raw);
420 byte[] aesKey = new byte[AES_KEY_BYTES];
421 try (CipherInputStream cis = new CipherInputStream(rsaEncryptedAesSecretKeyInputStream,
422 privateCipher.get())) {
423 cis.read(aesKey, 0, rsaEncryptedAesSecretKeyLength);
424 } catch (IOException e) {
425 throw new RuntimeException(e);
426 }
427 SecretKeySpec aesKeySpec = new SecretKeySpec(aesKey, AES);
428 try {
429 aes.cipher.init(Cipher.DECRYPT_MODE, aesKeySpec);
430 applyCipher(aes.cipher, is, os);
431 } catch (InvalidKeyException e) {
432 throw new RuntimeException(e);
433 }
434 } else
435 throw new PrivateKeyNotSetException();
436 }
437
438 public byte[] decrypt(byte[] bytes) {
439 Preconditions.checkNotNull(bytes);
440 try (InputStream is = new ByteArrayInputStream(bytes);
441 ByteArrayOutputStream os = new ByteArrayOutputStream()) {
442 decrypt(is, os);
443 return os.toByteArray();
444 } catch (IOException e) {
445 throw new RuntimeException(e);
446 }
447 }
448
449 public byte[] encrypt(String string, Charset charset) {
450 Preconditions.checkNotNull(string);
451 Preconditions.checkNotNull(charset);
452 return encrypt(string.getBytes(charset));
453 }
454
455 public String decrypt(byte[] bytes, Charset charset) {
456 Preconditions.checkNotNull(bytes);
457 Preconditions.checkNotNull(charset);
458 return new String(decrypt(bytes), charset);
459 }
460
461 public byte[] encryptRsa(byte[] bytes) {
462 Preconditions.checkNotNull(bytes);
463 if (bytes.length > 214) {
464 throw new InputTooLongException(
465 "Input is too long. Use encrypt()/decrypt() instead because RSA cannot encrypt more than 214 bytes.");
466 }
467 return applyCipher(publicCipher.get(), bytes);
468 }
469
470 public byte[] decryptRsa(byte[] bytes) {
471 Preconditions.checkNotNull(bytes);
472 return applyCipher(privateCipher.get(), bytes);
473 }
474
475 public byte[] encryptRsa(String string, Charset charset) {
476 Preconditions.checkNotNull(string);
477 Preconditions.checkNotNull(charset);
478 return encryptRsa(string.getBytes(charset));
479 }
480
481 public String encryptRsaAsBase64(String string) {
482 return Base64.getEncoder().encodeToString(encryptRsa(string, UTF_8));
483 }
484
485 public String decryptRsa(byte[] bytes, Charset charset) {
486 Preconditions.checkNotNull(bytes);
487 Preconditions.checkNotNull(charset);
488 return new String(decryptRsa(bytes), charset);
489 }
490
491 public String decryptRsaBase64(String base64) {
492 return decryptRsa(Base64.getDecoder().decode(base64), UTF_8);
493 }
494
495 private static Cipher readPublicCipher(byte[] bytes) {
496 try {
497 X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(bytes);
498 KeyFactory keyFactory = KeyFactory.getInstance(RSA);
499 PublicKey key = keyFactory.generatePublic(publicSpec);
500 Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
501 cipher.init(Cipher.ENCRYPT_MODE, key);
502 return cipher;
503 } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException
504 | InvalidKeyException e) {
505 throw new RuntimeException(e);
506 }
507 }
508
509 private static Cipher readPrivateCipher(byte[] bytes) {
510 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
511 try {
512 KeyFactory keyFactory = KeyFactory.getInstance(RSA);
513 PrivateKey key = keyFactory.generatePrivate(keySpec);
514 Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
515 cipher.init(Cipher.DECRYPT_MODE, key);
516 return cipher;
517 } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException
518 | InvalidKeyException e) {
519 throw new RuntimeException(e);
520 }
521 }
522
523 private static void applyCipher(Cipher cipher, InputStream is, OutputStream os) {
524 try (CipherOutputStream cos = new CipherOutputStream(os, cipher)) {
525 copy(is, cos);
526 } catch (IOException e) {
527 throw new RuntimeException(e);
528 }
529 }
530
531 private static byte[] applyCipher(Cipher cipher, byte[] bytes) {
532 ByteArrayInputStream input = new ByteArrayInputStream(bytes);
533 ByteArrayOutputStream output = new ByteArrayOutputStream();
534 applyCipher(cipher, input, output);
535 return output.toByteArray();
536 }
537
538 private static void copy(InputStream is, OutputStream os) throws IOException {
539 int i;
540 byte[] b = new byte[1024];
541 while ((i = is.read(b)) != -1) {
542 os.write(b, 0, i);
543 }
544 }
545
546 }