View Javadoc
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   * Stands for Public Private Key. Might also have been called PKC (Public Key
36   * Cryptography).
37   */
38  @NotThreadSafe
39  public final class PPK {
40  
41      /*
42       * We load the public cipher and private cipher and we generate an AES
43       * cipher. The AES cipher is more efficient for encryption and decryption of
44       * data when the data can be longer than the RSA cipher key size. We use the
45       * public key to encrypt the AES cipher and prepend the AES encrypted bytes
46       * with the rsa encrypted AES secret key. Thus the consumer has to read the
47       * first N bytes and decrypt the AES secret key using the rsa private key
48       * and then can decrypt the remaining bytes in the message using the AES
49       * secret key.
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;// multiple of 8
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          // used just for encryption not for decryption
64          final byte[] encodedSecretKey;
65  
66          // used just for encryption not decryption
67          final SecretKeySpec secretKeySpec;
68  
69          // used for encryption and decryption
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      * Returns a builder having loaded the private key from the classpath
106      * relative to the classloader used by {@code cls}.
107      * 
108      * @param cls
109      *            the class whose classloader is used to load the resource
110      * @param resource
111      *            the resource path
112      * @return the PPK builder
113      */
114     public static final Builder privateKey(Class<?> cls, String resource) {
115         return new Builder().privateKey(cls, resource);
116     }
117 
118     /**
119      * Returns a PPK builder having set the private key location on the
120      * classpath relative to the classloader used by {@link PPK}.
121      * 
122      * @param resource
123      *            the resource path
124      * @return the PPK builder
125      */
126     public static final Builder privateKey(String resource) {
127         return new Builder().privateKey(resource);
128     }
129 
130     /**
131      * Returns a PPK builder having read the private key from the given
132      * InputStream.
133      * 
134      * @param is
135      *            private key input stream
136      * @return the PPK builder
137      */
138     public static final Builder privateKey(InputStream is) {
139         return new Builder().privateKey(is);
140     }
141 
142     /**
143      * Returns a PPK builder having read the private key from the given file.
144      * 
145      * @param file
146      *            that contains the private key
147      * @return the PPK builder
148      */
149     public static final Builder privateKey(File file) {
150         return new Builder().privateKey(file);
151     }
152 
153     /**
154      * Returns a PPK builder having read the private key from the given byte
155      * array.
156      * 
157      * @param bytes
158      *            of the private key
159      * @return the PPK builder
160      */
161     public static final Builder privateKey(byte[] bytes) {
162         return new Builder().privateKey(bytes);
163     }
164 
165     /**
166      * Returns a builder having loaded the public key from the classpath
167      * relative to the classloader used by {@code cls}.
168      * 
169      * @param cls
170      *            the class whose classloader is used to load the resource
171      * @param resource
172      *            the resource path
173      * @return the PPK builder
174      */
175     public static final Builder publicKey(Class<?> cls, String resource) {
176         return new Builder().publicKey(cls, resource);
177     }
178 
179     /**
180      * Returns a builder having loaded the public key from the classpath
181      * relative to the classloader used by {@link PPK}.
182      * 
183      * @param resource
184      *            the resource path
185      * @return the PPK builder
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             // prevent instantiation
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 }