18 package org.turro.push.service;
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;
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;
60 private Map<String, KeyPair> keys;
61 private Map<String, String> labels;
64 this(
new HashMap<String, KeyPair>(),
new HashMap<String, String>());
67 public HttpEce(Map<String, KeyPair> keys, Map<String, String> labels) {
88 public byte[]
encrypt(
byte[] plaintext,
byte[] salt,
byte[] privateKey, String keyid, ECPublicKey dh,
byte[] authSecret,
Encoding version)
throws GeneralSecurityException {
89 log(
"encrypt", plaintext);
91 byte[][] keyAndNonce =
deriveKeyAndNonce(salt, privateKey, keyid, dh, authSecret, version, ENCRYPT_MODE);
92 byte[] key = keyAndNonce[0];
93 byte[] nonce = keyAndNonce[1];
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);
102 byte[] header = buildHeader(salt, keyid);
103 log(
"header", header);
105 byte[] padding =
new byte[]{2};
106 log(
"padding", padding);
108 byte[][] encrypted = {cipher.update(plaintext), cipher.update(padding), cipher.doFinal()};
109 log(
"encrypted",
concat(encrypted));
111 return log(
"ciphertext",
concat(header,
concat(encrypted)));
113 return concat(cipher.update(
new byte[2]), cipher.doFinal(plaintext));
126 public byte[]
decrypt(
byte[] payload,
byte[] salt,
byte[] key, String keyid,
Encoding version)
throws InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, InvalidAlgorithmParameterException, BadPaddingException, NoSuchProviderException, NoSuchPaddingException {
134 keyid =
new String(header[2]);
141 byte[][] keyAndNonce =
deriveKeyAndNonce(salt, key, keyid,
null,
null, version, DECRYPT_MODE);
143 return decryptRecord(body, keyAndNonce[0], keyAndNonce[1], version);
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);
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);
166 byte[] plaintext = cipher.doFinal(ciphertext);
170 return Arrays.copyOfRange(plaintext, 0, plaintext.length - 1);
173 return Arrays.copyOfRange(plaintext, 2, plaintext.length);
187 private byte[] buildHeader(
byte[] salt, String keyid) {
191 keyIdBytes =
new byte[0];
196 if (keyIdBytes.length > 255) {
197 throw new IllegalArgumentException(
"They keyid is too large.");
201 byte[] idlen =
new byte[]{(byte) keyIdBytes.length};
203 return concat(salt, rs, idlen, keyIdBytes);
212 protected static byte[]
buildInfo(String type,
byte[] context) {
213 ByteBuffer buffer = ByteBuffer.allocate(19 + type.length() + context.length);
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);
220 return buffer.array();
227 protected static byte[]
hkdfExpand(
byte[] ikm,
byte[] salt,
byte[] info,
int length) {
232 HKDFBytesGenerator hkdf =
new HKDFBytesGenerator(
new SHA256Digest());
233 hkdf.init(
new HKDFParameters(ikm, salt, info));
235 byte[] okm =
new byte[length];
236 hkdf.generateBytes(okm, 0, length);
243 public byte[][]
extractSecretAndContext(
byte[] key, String keyId, ECPublicKey dh,
byte[] authSecret)
throws InvalidKeyException, NoSuchAlgorithmException {
244 byte[] secret =
null;
245 byte[] context =
null;
250 throw new IllegalStateException(
"An explicit key must be " +
KEY_LENGTH +
" bytes.");
252 }
else if (dh !=
null) {
253 byte[][] bytes = extractDH(keyId, dh);
256 }
else if (keyId !=
null) {
257 secret = keys.get(keyId).getPublic().getEncoded();
260 if (secret ==
null) {
261 throw new IllegalStateException(
"Unable to determine key.");
264 if (authSecret !=
null) {
274 public byte[][]
deriveKeyAndNonce(
byte[] salt,
byte[] key, String keyId, ECPublicKey dh,
byte[] authSecret,
Encoding version,
int mode)
throws NoSuchAlgorithmException, InvalidKeyException {
281 secret = secretAndContext[0];
283 keyInfo =
buildInfo(
"aesgcm", secretAndContext[1]);
284 nonceInfo =
buildInfo(
"nonce", secretAndContext[1]);
286 keyInfo =
"Content-Encoding: aes128gcm\0".getBytes();
287 nonceInfo =
"Content-Encoding: nonce\0".getBytes();
289 secret = extractSecret(key, keyId, dh, authSecret, mode);
291 throw new IllegalStateException(
"Unknown version: " + version);
294 byte[] hkdf_key =
hkdfExpand(secret, salt, keyInfo, 16);
295 byte[] hkdf_nonce =
hkdfExpand(secret, salt, nonceInfo, 12);
297 log(
"key", hkdf_key);
298 log(
"nonce", hkdf_nonce);
306 private byte[] extractSecret(
byte[] key, String keyId, ECPublicKey dh,
byte[] authSecret,
int mode)
throws InvalidKeyException, NoSuchAlgorithmException {
309 throw new IllegalArgumentException(
"An explicit key must be " +
KEY_LENGTH +
" bytes.");
315 KeyPair keyPair = keys.get(keyId);
317 if (keyPair ==
null) {
318 throw new IllegalArgumentException(
"No saved key for keyid '" + keyId +
"'.");
321 return ServerKeys.encode((ECPublicKey) keyPair.getPublic());
341 public byte[]
webpushSecret(String keyId, ECPublicKey dh,
byte[] authSecret,
int mode)
throws NoSuchAlgorithmException, InvalidKeyException {
342 ECPublicKey senderPubKey;
343 ECPublicKey remotePubKey;
344 ECPublicKey receiverPubKey;
346 if (mode == ENCRYPT_MODE) {
347 senderPubKey = getPublicKey(keyId);
350 }
else if (mode == DECRYPT_MODE) {
351 remotePubKey = getPublicKey(keyId);
352 senderPubKey = remotePubKey;
355 throw new IllegalArgumentException(
"Unsupported mode: " + mode);
362 KeyAgreement keyAgreement = KeyAgreement.getInstance(
"ECDH");
363 keyAgreement.init(getPrivateKey(keyId));
364 keyAgreement.doPhase(remotePubKey,
true);
365 byte[] secret = keyAgreement.generateSecret();
368 byte[] salt = authSecret;
382 private byte[][] extractDH(String keyid, ECPublicKey publicKey)
throws NoSuchAlgorithmException, InvalidKeyException {
383 ECPublicKey senderPubKey = getPublicKey(keyid);
385 KeyAgreement keyAgreement = KeyAgreement.getInstance(
"ECDH");
386 keyAgreement.init(getPrivateKey(keyid));
387 keyAgreement.doPhase(publicKey,
true);
389 byte[] secret = keyAgreement.generateSecret();
390 byte[] context =
concat(labels.get(keyid).getBytes(UTF_8),
new byte[1], lengthPrefix(publicKey), lengthPrefix(senderPubKey));
404 private ECPublicKey getPublicKey(String keyid) {
405 return (ECPublicKey) keys.get(keyid).getPublic();
414 private ECPrivateKey getPrivateKey(String keyid) {
415 return (ECPrivateKey) keys.get(keyid).getPrivate();
424 private static byte[] lengthPrefix(ECPublicKey publicKey) {
425 byte[] bytes = ServerKeys.encode(publicKey);
427 return concat(intToBytes(bytes.length), bytes);
439 private static byte[] intToBytes(
int number) {
441 throw new IllegalArgumentException(
"Cannot convert a negative number, " + number +
" given.");
445 throw new IllegalArgumentException(
"Cannot convert an integer larger than " + (
TWO_BYTE_MAX - 1) +
" to two bytes.");
448 byte[] bytes =
new byte[2];
449 bytes[1] = (byte) (number & 0xff);
450 bytes[0] = (byte) (number >> 8);
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));
470 public static byte[]
concat(
byte[]... arrays) {
475 for (
byte[] array : arrays) {
480 System.arraycopy(array, 0, combined, lastPos, array.length);
482 lastPos += array.length;
491 for (
byte[] array : arrays) {
503 ByteBuffer buffer = ByteBuffer.allocate(size);
504 buffer.putInt(integer);
506 return buffer.array();
static byte[] encode(ECPublicKey publicKey)
static final int SHA_256_LENGTH
static final int TAG_SIZE
static byte[] toByteArray(int integer, int size)
byte[][] extractSecretAndContext(byte[] key, String keyId, ECPublicKey dh, byte[] authSecret)
static final String WEB_PUSH_INFO
static byte[] hkdfExpand(byte[] ikm, byte[] salt, byte[] info, int length)
byte[] encrypt(byte[] plaintext, byte[] salt, byte[] privateKey, String keyid, ECPublicKey dh, byte[] authSecret, Encoding version)
byte[][] parseHeader(byte[] payload)
static final int TWO_BYTE_MAX
static int combinedLength(byte[]... arrays)
static byte[] concat(byte[]... arrays)
HttpEce(Map< String, KeyPair > keys, Map< String, String > labels)
static final int KEY_LENGTH
byte[] decryptRecord(byte[] ciphertext, byte[] key, byte[] nonce, Encoding version)
static byte[] buildInfo(String type, byte[] context)
byte[] webpushSecret(String keyId, ECPublicKey dh, byte[] authSecret, int mode)
byte[][] deriveKeyAndNonce(byte[] salt, byte[] key, String keyId, ECPublicKey dh, byte[] authSecret, Encoding version, int mode)
byte[] decrypt(byte[] payload, byte[] salt, byte[] key, String keyid, Encoding version)