BrightSide Workbench Full Report + Source Code
HttpEce.java
Go to the documentation of this file.
1 /*
2  * TurrĂ³ i Cutiller Foundation. License notice.
3  * Copyright (C) 2022 Lluis TurrĂ³ Cutiller <http://www.turro.org/>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Affero General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU Affero General Public License for more details.
14  *
15  * You should have received a copy of the GNU Affero General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  */
18 package org.turro.push.service;
19 
20 import java.nio.ByteBuffer;
21 import static java.nio.charset.StandardCharsets.UTF_8;
22 import java.security.GeneralSecurityException;
23 import java.security.InvalidAlgorithmParameterException;
24 import java.security.InvalidKeyException;
25 import java.security.KeyPair;
26 import java.security.NoSuchAlgorithmException;
27 import java.security.NoSuchProviderException;
28 import java.util.Arrays;
29 import java.util.HashMap;
30 import java.util.Map;
31 import javax.crypto.BadPaddingException;
32 import javax.crypto.Cipher;
33 import static javax.crypto.Cipher.DECRYPT_MODE;
34 import static javax.crypto.Cipher.ENCRYPT_MODE;
35 import javax.crypto.IllegalBlockSizeException;
36 import javax.crypto.KeyAgreement;
37 import javax.crypto.NoSuchPaddingException;
38 import javax.crypto.spec.GCMParameterSpec;
39 import javax.crypto.spec.SecretKeySpec;
40 import org.apache.commons.codec.binary.Base64;
41 import org.bouncycastle.crypto.digests.SHA256Digest;
42 import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
43 import org.bouncycastle.crypto.params.HKDFParameters;
44 import org.bouncycastle.jce.interfaces.ECPrivateKey;
45 import org.bouncycastle.jce.interfaces.ECPublicKey;
46 import org.turro.push.security.ServerKeys;
47 
52 public class HttpEce {
53 
54  public static final int KEY_LENGTH = 16;
55  public static final int SHA_256_LENGTH = 32;
56  public static final int TAG_SIZE = 16;
57  public static final int TWO_BYTE_MAX = 65_536;
58  public static final String WEB_PUSH_INFO = "WebPush: info\0";
59 
60  private Map<String, KeyPair> keys;
61  private Map<String, String> labels;
62 
63  public HttpEce() {
64  this(new HashMap<String, KeyPair>(), new HashMap<String, String>());
65  }
66 
67  public HttpEce(Map<String, KeyPair> keys, Map<String, String> labels) {
68  this.keys = keys;
69  this.labels = labels;
70  }
71 
88  public byte[] encrypt(byte[] plaintext, byte[] salt, byte[] privateKey, String keyid, ECPublicKey dh, byte[] authSecret, Encoding version) throws GeneralSecurityException {
89  log("encrypt", plaintext);
90 
91  byte[][] keyAndNonce = deriveKeyAndNonce(salt, privateKey, keyid, dh, authSecret, version, ENCRYPT_MODE);
92  byte[] key = keyAndNonce[0];
93  byte[] nonce = keyAndNonce[1];
94 
95  // Note: Cipher adds the tag to the end of the ciphertext
96  Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
97  GCMParameterSpec params = new GCMParameterSpec(TAG_SIZE * 8, nonce);
98  cipher.init(ENCRYPT_MODE, new SecretKeySpec(key, "AES"), params);
99 
100  // For AES128GCM suffix {0x02}, for AESGCM prefix {0x00, 0x00}.
101  if (version == Encoding.AES128GCM) {
102  byte[] header = buildHeader(salt, keyid);
103  log("header", header);
104 
105  byte[] padding = new byte[]{2};
106  log("padding", padding);
107 
108  byte[][] encrypted = {cipher.update(plaintext), cipher.update(padding), cipher.doFinal()};
109  log("encrypted", concat(encrypted));
110 
111  return log("ciphertext", concat(header, concat(encrypted)));
112  } else {
113  return concat(cipher.update(new byte[2]), cipher.doFinal(plaintext));
114  }
115  }
116 
126  public byte[] decrypt(byte[] payload, byte[] salt, byte[] key, String keyid, Encoding version) throws InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, InvalidAlgorithmParameterException, BadPaddingException, NoSuchProviderException, NoSuchPaddingException {
127  byte[] body;
128 
129  // Parse and strip the header
130  if (version == Encoding.AES128GCM) {
131  byte[][] header = parseHeader(payload);
132 
133  salt = header[0];
134  keyid = new String(header[2]);
135  body = header[3];
136  } else {
137  body = payload;
138  }
139 
140  // Derive key and nonce.
141  byte[][] keyAndNonce = deriveKeyAndNonce(salt, key, keyid, null, null, version, DECRYPT_MODE);
142 
143  return decryptRecord(body, keyAndNonce[0], keyAndNonce[1], version);
144  }
145 
146  public byte[][] parseHeader(byte[] payload) {
147  byte[] salt = Arrays.copyOfRange(payload, 0, KEY_LENGTH);
148  byte[] recordSize = Arrays.copyOfRange(payload, KEY_LENGTH, 20);
149  int keyIdLength = Arrays.copyOfRange(payload, 20, 21)[0];
150  byte[] keyId = Arrays.copyOfRange(payload, 21, 21 + keyIdLength);
151  byte[] body = Arrays.copyOfRange(payload, 21 + keyIdLength, payload.length);
152 
153  return new byte[][]{
154  salt,
155  recordSize,
156  keyId,
157  body
158  };
159  }
160 
161  public byte[] decryptRecord(byte[] ciphertext, byte[] key, byte[] nonce, Encoding version) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
162  Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
163  GCMParameterSpec params = new GCMParameterSpec(TAG_SIZE * 8, nonce);
164  cipher.init(DECRYPT_MODE, new SecretKeySpec(key, "AES"), params);
165 
166  byte[] plaintext = cipher.doFinal(ciphertext);
167 
168  if (version == Encoding.AES128GCM) {
169  // Remove one byte of padding at the end
170  return Arrays.copyOfRange(plaintext, 0, plaintext.length - 1);
171  } else {
172  // Remove two bytes of padding at the start
173  return Arrays.copyOfRange(plaintext, 2, plaintext.length);
174  }
175  }
176 
187  private byte[] buildHeader(byte[] salt, String keyid) {
188  byte[] keyIdBytes;
189 
190  if (keyid == null) {
191  keyIdBytes = new byte[0];
192  } else {
193  keyIdBytes = ServerKeys.encode(getPublicKey(keyid));
194  }
195 
196  if (keyIdBytes.length > 255) {
197  throw new IllegalArgumentException("They keyid is too large.");
198  }
199 
200  byte[] rs = toByteArray(4096, 4);
201  byte[] idlen = new byte[]{(byte) keyIdBytes.length};
202 
203  return concat(salt, rs, idlen, keyIdBytes);
204  }
205 
212  protected static byte[] buildInfo(String type, byte[] context) {
213  ByteBuffer buffer = ByteBuffer.allocate(19 + type.length() + context.length);
214 
215  buffer.put("Content-Encoding: ".getBytes(UTF_8), 0, 18);
216  buffer.put(type.getBytes(UTF_8), 0, type.length());
217  buffer.put(new byte[1], 0, 1);
218  buffer.put(context, 0, context.length);
219 
220  return buffer.array();
221  }
222 
227  protected static byte[] hkdfExpand(byte[] ikm, byte[] salt, byte[] info, int length) {
228  log("salt", salt);
229  log("ikm", ikm);
230  log("info", info);
231 
232  HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest());
233  hkdf.init(new HKDFParameters(ikm, salt, info));
234 
235  byte[] okm = new byte[length];
236  hkdf.generateBytes(okm, 0, length);
237 
238  log("expand", okm);
239 
240  return okm;
241  }
242 
243  public byte[][] extractSecretAndContext(byte[] key, String keyId, ECPublicKey dh, byte[] authSecret) throws InvalidKeyException, NoSuchAlgorithmException {
244  byte[] secret = null;
245  byte[] context = null;
246 
247  if (key != null) {
248  secret = key;
249  if (secret.length != KEY_LENGTH) {
250  throw new IllegalStateException("An explicit key must be " + KEY_LENGTH + " bytes.");
251  }
252  } else if (dh != null) {
253  byte[][] bytes = extractDH(keyId, dh);
254  secret = bytes[0];
255  context = bytes[1];
256  } else if (keyId != null) {
257  secret = keys.get(keyId).getPublic().getEncoded();
258  }
259 
260  if (secret == null) {
261  throw new IllegalStateException("Unable to determine key.");
262  }
263 
264  if (authSecret != null) {
265  secret = hkdfExpand(secret, authSecret, buildInfo("auth", new byte[0]), SHA_256_LENGTH);
266  }
267 
268  return new byte[][]{
269  secret,
270  context
271  };
272  }
273 
274  public byte[][] deriveKeyAndNonce(byte[] salt, byte[] key, String keyId, ECPublicKey dh, byte[] authSecret, Encoding version, int mode) throws NoSuchAlgorithmException, InvalidKeyException {
275  byte[] secret;
276  byte[] keyInfo;
277  byte[] nonceInfo;
278 
279  if (version == Encoding.AESGCM) {
280  byte[][] secretAndContext = extractSecretAndContext(key, keyId, dh, authSecret);
281  secret = secretAndContext[0];
282 
283  keyInfo = buildInfo("aesgcm", secretAndContext[1]);
284  nonceInfo = buildInfo("nonce", secretAndContext[1]);
285  } else if (version == Encoding.AES128GCM) {
286  keyInfo = "Content-Encoding: aes128gcm\0".getBytes();
287  nonceInfo = "Content-Encoding: nonce\0".getBytes();
288 
289  secret = extractSecret(key, keyId, dh, authSecret, mode);
290  } else {
291  throw new IllegalStateException("Unknown version: " + version);
292  }
293 
294  byte[] hkdf_key = hkdfExpand(secret, salt, keyInfo, 16);
295  byte[] hkdf_nonce = hkdfExpand(secret, salt, nonceInfo, 12);
296 
297  log("key", hkdf_key);
298  log("nonce", hkdf_nonce);
299 
300  return new byte[][]{
301  hkdf_key,
302  hkdf_nonce
303  };
304  }
305 
306  private byte[] extractSecret(byte[] key, String keyId, ECPublicKey dh, byte[] authSecret, int mode) throws InvalidKeyException, NoSuchAlgorithmException {
307  if (key != null) {
308  if (key.length != KEY_LENGTH) {
309  throw new IllegalArgumentException("An explicit key must be " + KEY_LENGTH + " bytes.");
310  }
311  return key;
312  }
313 
314  if (dh == null) {
315  KeyPair keyPair = keys.get(keyId);
316 
317  if (keyPair == null) {
318  throw new IllegalArgumentException("No saved key for keyid '" + keyId + "'.");
319  }
320 
321  return ServerKeys.encode((ECPublicKey) keyPair.getPublic());
322  }
323 
324  return webpushSecret(keyId, dh, authSecret, mode);
325  }
326 
341  public byte[] webpushSecret(String keyId, ECPublicKey dh, byte[] authSecret, int mode) throws NoSuchAlgorithmException, InvalidKeyException {
342  ECPublicKey senderPubKey;
343  ECPublicKey remotePubKey;
344  ECPublicKey receiverPubKey;
345 
346  if (mode == ENCRYPT_MODE) {
347  senderPubKey = getPublicKey(keyId);
348  remotePubKey = dh;
349  receiverPubKey = dh;
350  } else if (mode == DECRYPT_MODE) {
351  remotePubKey = getPublicKey(keyId);
352  senderPubKey = remotePubKey;
353  receiverPubKey = dh;
354  } else {
355  throw new IllegalArgumentException("Unsupported mode: " + mode);
356  }
357 
358  log("remote pubkey", ServerKeys.encode(remotePubKey));
359  log("sender pubkey", ServerKeys.encode(senderPubKey));
360  log("receiver pubkey", ServerKeys.encode(receiverPubKey));
361 
362  KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
363  keyAgreement.init(getPrivateKey(keyId));
364  keyAgreement.doPhase(remotePubKey, true);
365  byte[] secret = keyAgreement.generateSecret();
366 
367  byte[] ikm = secret;
368  byte[] salt = authSecret;
369  byte[] info = concat(WEB_PUSH_INFO.getBytes(), ServerKeys.encode(receiverPubKey), ServerKeys.encode(senderPubKey));
370 
371  return hkdfExpand(ikm, salt, info, SHA_256_LENGTH);
372  }
373 
382  private byte[][] extractDH(String keyid, ECPublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException {
383  ECPublicKey senderPubKey = getPublicKey(keyid);
384 
385  KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
386  keyAgreement.init(getPrivateKey(keyid));
387  keyAgreement.doPhase(publicKey, true);
388 
389  byte[] secret = keyAgreement.generateSecret();
390  byte[] context = concat(labels.get(keyid).getBytes(UTF_8), new byte[1], lengthPrefix(publicKey), lengthPrefix(senderPubKey));
391 
392  return new byte[][]{
393  secret,
394  context
395  };
396  }
397 
404  private ECPublicKey getPublicKey(String keyid) {
405  return (ECPublicKey) keys.get(keyid).getPublic();
406  }
407 
414  private ECPrivateKey getPrivateKey(String keyid) {
415  return (ECPrivateKey) keys.get(keyid).getPrivate();
416  }
417 
424  private static byte[] lengthPrefix(ECPublicKey publicKey) {
425  byte[] bytes = ServerKeys.encode(publicKey);
426 
427  return concat(intToBytes(bytes.length), bytes);
428  }
429 
439  private static byte[] intToBytes(int number) {
440  if (number < 0) {
441  throw new IllegalArgumentException("Cannot convert a negative number, " + number + " given.");
442  }
443 
444  if (number >= TWO_BYTE_MAX) {
445  throw new IllegalArgumentException("Cannot convert an integer larger than " + (TWO_BYTE_MAX - 1) + " to two bytes.");
446  }
447 
448  byte[] bytes = new byte[2];
449  bytes[1] = (byte) (number & 0xff);
450  bytes[0] = (byte) (number >> 8);
451 
452  return bytes;
453  }
454 
462  private static byte[] log(String info, byte[] array) {
463  if ("1".equals(System.getenv("ECE_KEYLOG"))) {
464  System.out.println(info + " [" + array.length + "]: " + Base64.encodeBase64URLSafeString(array));
465  }
466 
467  return array;
468  }
469 
470  public static byte[] concat(byte[]... arrays) {
471  int lastPos = 0;
472 
473  byte[] combined = new byte[combinedLength(arrays)];
474 
475  for (byte[] array : arrays) {
476  if (array == null) {
477  continue;
478  }
479 
480  System.arraycopy(array, 0, combined, lastPos, array.length);
481 
482  lastPos += array.length;
483  }
484 
485  return combined;
486  }
487 
488  public static int combinedLength(byte[]... arrays) {
489  int combinedLength = 0;
490 
491  for (byte[] array : arrays) {
492  if (array == null) {
493  continue;
494  }
495 
496  combinedLength += array.length;
497  }
498 
499  return combinedLength;
500  }
501 
502  public static byte[] toByteArray(int integer, int size) {
503  ByteBuffer buffer = ByteBuffer.allocate(size);
504  buffer.putInt(integer);
505 
506  return buffer.array();
507  }
508 
509 }
static byte[] encode(ECPublicKey publicKey)
static final int SHA_256_LENGTH
Definition: HttpEce.java:55
static final int TAG_SIZE
Definition: HttpEce.java:56
static byte[] toByteArray(int integer, int size)
Definition: HttpEce.java:502
byte[][] extractSecretAndContext(byte[] key, String keyId, ECPublicKey dh, byte[] authSecret)
Definition: HttpEce.java:243
static final String WEB_PUSH_INFO
Definition: HttpEce.java:58
static byte[] hkdfExpand(byte[] ikm, byte[] salt, byte[] info, int length)
Definition: HttpEce.java:227
byte[] encrypt(byte[] plaintext, byte[] salt, byte[] privateKey, String keyid, ECPublicKey dh, byte[] authSecret, Encoding version)
Definition: HttpEce.java:88
byte[][] parseHeader(byte[] payload)
Definition: HttpEce.java:146
static final int TWO_BYTE_MAX
Definition: HttpEce.java:57
static int combinedLength(byte[]... arrays)
Definition: HttpEce.java:488
static byte[] concat(byte[]... arrays)
Definition: HttpEce.java:470
HttpEce(Map< String, KeyPair > keys, Map< String, String > labels)
Definition: HttpEce.java:67
static final int KEY_LENGTH
Definition: HttpEce.java:54
byte[] decryptRecord(byte[] ciphertext, byte[] key, byte[] nonce, Encoding version)
Definition: HttpEce.java:161
static byte[] buildInfo(String type, byte[] context)
Definition: HttpEce.java:212
byte[] webpushSecret(String keyId, ECPublicKey dh, byte[] authSecret, int mode)
Definition: HttpEce.java:341
byte[][] deriveKeyAndNonce(byte[] salt, byte[] key, String keyId, ECPublicKey dh, byte[] authSecret, Encoding version, int mode)
Definition: HttpEce.java:274
byte[] decrypt(byte[] payload, byte[] salt, byte[] key, String keyid, Encoding version)
Definition: HttpEce.java:126