ecec
ecec is a C implementation of the HTTP Encrypted Content-Encoding draft. It's a port of the reference JavaScript implementation.
Encrypted content-coding is used to encrypt Web Push messages, and can be used standalone.
Table of Contents
Usage
Generating subscription keys
#include <ece.h>
int
main() {
// The subscription private key. This key should never be sent to the app
// server. It should be persisted with the endpoint and auth secret, and used
// to decrypt all messages sent to the subscription.
uint8_t rawRecvPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH];
// The subscription public key. This key should be sent to the app server,
// and used to encrypt messages. The Push DOM API exposes the public key via
// `pushSubscription.getKey("p256dh")`.
uint8_t rawRecvPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH];
// The shared auth secret. This secret should be persisted with the
// subscription information, and sent to the app server. The DOM API exposes
// the auth secret via `pushSubscription.getKey("auth")`.
uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH];
int err = ece_webpush_generate_keys(
rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, rawRecvPubKey,
ECE_WEBPUSH_PUBLIC_KEY_LENGTH, authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH);
if (err) {
return 1;
}
return 0;
}aes128gcm
This is the scheme from the latest version of the encrypted content-coding draft. It's not currently supported by any encryption library or browser, but will eventually replace aesgcm. This scheme removes the Crypto-Key and Encryption headers. Instead, the salt, record size, and sender public key are included in the payload as a binary header block.
// Assume `rawSubPrivKey` and `authSecret` contain the subscription private key
// and auth secret.
uint8_t rawSubPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH] = {0};
uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH] = {0};
// Assume `payload` points to the contents of the encrypted payload, and
// `payloadLen` specifies the length.
uint8_t* payload = NULL;
size_t payloadLen = 0;
size_t plaintextLen = ece_aes128gcm_plaintext_max_length(payload, payloadLen);
assert(plaintextLen > 0);
uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t));
assert(plaintext);
int err =
ece_webpush_aes128gcm_decrypt(rawSubPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH,
authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH,
payload, payloadLen, plaintext, &plaintextLen);
assert(err == ECE_OK);
// `plaintext[0..plaintextLen]` contains the decrypted message.
free(plaintext);aesgcm
All Web Push libraries support the "aesgcm" scheme, as well as Firefox 46+ and Chrome 50+. The app server includes its public key in the Crypto-Key HTTP header, the salt and record size in the Encryption header, and the encrypted payload in the body of the POST request.
- The
Crypto-Keyheader comprises one or more comma-delimited parameters. The first parameter must include adhname-value pair, containing the sender's Base64url-encoded public key. - The
Encryptionheader must include asaltname-value pair containing the sender's Base64url-encoded salt, and an optionalrspair specifying the record size.
If the Crypto-Key header contains multiple keys, the sender must also include a keyid to match the encryption parameters to the key. The drafts have examples for a single key without a keyid, and multiple keys with keyids.
uint8_t rawSubPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH] = {0};
uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH] = {0};
const char* cryptoKeyHeader = "dh=...";
const char* encryptionHeader = "salt=...; rs=...";
uint8_t* ciphertext = NULL;
size_t ciphertextLen = 0;
uint8_t salt[ECE_SALT_LENGTH];
uint8_t rawSenderPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH];
uint32_t rs = 0;
int err =
ece_webpush_aesgcm_headers_extract_params(cryptoKeyHeader, encryptionHeader,
salt, ECE_SALT_LENGTH,
rawSenderPubKey,
ECE_WEBPUSH_PUBLIC_KEY_LENGTH, &rs);
assert(err == ECE_OK);
size_t plaintextLen = ece_aesgcm_plaintext_max_length(rs, ciphertextLen);
assert(plaintextLen > 0);
uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t));
assert(plaintext);
err = ece_webpush_aesgcm_decrypt(
rawSubPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret,
ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, rawSenderPubKey,
ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, ciphertext, ciphertextLen, plaintext,
&plaintextLen);
assert(err == ECE_OK);
// `plaintext[0..plaintextLen]` contains the decrypted message.
free(plaintext);Building
Dependencies
- OpenSSL 1.1.0 or higher
- CMake 3.1 or higher
- A C99-capable compiler, like Clang 3.4, GCC 4.6, or Visual Studio 2015
macOS and *nix
OpenSSL 1.1.0 is new, and backward-incompatible with 1.0.x. If your package manager (MacPorts, Homebrew, APT, DNF, yum) doesn't have 1.1.0 yet, you'll need to compile it yourself. ecec does this to run its tests on Travis CI; please see .travis/install.sh for the commands.
In particular, you'll need to set the OPENSSL_ROOT_DIR cache entry for CMake to find your compiled version. To build the library:
> mkdir build
> cd build
> cmake -DOPENSSL_ROOT_DIR=/usr/local ..
> makeTo build the decryption tool:
> make ece-decrypt
> ./ece-decryptTo run the tests:
> make checkWindows
Shining Light provides OpenSSL binaries for Windows. The installer will ask if you want to copy the OpenSSL DLLs into the system directory, or the OpenSSL binaries directory. If you choose the binaries directory, you'll need to add it to your Path.
To do so, right-click the Start button, navigate to "System" > "Advanced system settings" > "Environment Variables...", find Path under "System variables", click "Edit" > "New", and enter the directory name. This will be C:\OpenSSL-Win64\bin if you've installed the 64-bit version in the default location.
You can then build the library like so:
> mkdir build
> cd build
> cmake -G "Visual Studio 14 2015 Win64" -DOPENSSL_ROOT_DIR=C:\OpenSSL-Win64 ..
> cmake --build . [--config Debug|Release]To build the decryption tool:
> cmake --build . --target ece-decrypt [--config Debug|Release]
> .\[Debug|Release]\ece-decryptTo run the tests:
> cmake --build . --target check [--config Debug|Release]What is encrypted content-coding?
Like TLS, encrypted content-coding uses Diffie-Hellman key exchange to derive a shared secret, which, in turn, is used to derive a symmetric encryption key for a block cipher. This encoding uses ECDH for key exchange, and AES GCM for the block cipher.
Key exchange is a process where a sender and a receiver generate public-private key pairs, then exchange public keys. The sender combines the receiver's public key with its own private key to obtain a secret. Meanwhile, the receiver combines the sender's public key with its private key to obtain the same secret. Wikipedia has a good visual explanation.
The shared ECDH secret isn't directly usable as an encryption key. Instead, both the sender and receiver combine the shared ECDH secret with an authentication secret, to produce a 32-byte pseudorandom key (PRK). The auth secret is a random 16-byte array generated by the receiver, and shared with the sender along with the receiver's public key. Both parties use HKDF to derive the PRK from the ECDH secret, using the formula PRK = HKDF-Expand(HKDF-Extract(authSecret, sharedSecret), prkInfo, 32). RFC 5869 describes the inputs to HKDF-Expand and HKDF-Extract, and how they work. prkInfo is different depending on the encryption scheme used; more on that later.
Next, the sender and receiver combine the PRK with a random 16-byte salt. The salt is generated by the sender, and shared with the receiver as part of the message payload. The PRK undergoes two rounds of HKDF to derive the symmetric key and nonce: key = HKDF-Expand(HKDF-Extract(salt, PRK), keyInfo, 16), and nonce = HKDF-Expand(HKDF-Extract(salt, PRK), nonceInfo, 12). As with prkInfo above, keyInfo and nonceInfo are different depending on the exact scheme.
Finally, the sender chunks the plaintext into fixed-size records, and includes this size in the message payload as the rs. The chunks are numbered 0 to N; this is called the sequence number (SEQ), and is used to derive the IV. All chunks should be rs bytes long, but the final chunk can be smaller if needed.
Each plaintext chunk is padded, then encrypted with AES using the 16-byte symmetric key and a 12-byte IV. The IV is generated from the nonce by XOR-ing the last 6 bytes of the 12-byte nonce with the sequence number. Afterward, the sender appends the GCM authentication tag to the encrypted chunk, producing the final encrypted record.
To decrypt the message, the receiver chunks the ciphertext into N encrypted records, decrypts each chunk, validates the auth tag, and removes the padding.
Web Push
In Web Push, the app server is the sender, and the browser ("user agent") is the receiver. The browser generates a public-private ECDH key pair and 16-byte auth secret for each push subscription. These keys are static; they're used to decrypt all messages sent to this subscription. The browser exposes the subscription endpoint, public key, and auth secret to the web app via the Push DOM API. The web app then delivers the endpoint and keys to the app server.
When the app server wants to send a push message, it generates its own public-private key pair, and computes the shared ECDH secret using the subscription public key. This key pair is ephemeral: it should be discarded after the message is sent, and a new key pair used for the next message. The app server encrypts the payload using the process outlined above, and includes the salt, sender public key, and ciphertext in a POST request to the endpoint. The push endpoint relays the encrypted payload to the browser. Finally, the browser decrypts the payload with the subscription private key, and delivers the plaintext to the web app. Because the endpoint doesn't know the private key, it can't decrypt or tamper with the message.
aes128gcm
prkInfois the string"WebPush: info\0", followed by the receiver and sender public keys in uncompressed form. Unlikeaesgcm, these are not length-prefixed.keyInfois the static string"Content-Encoding: aes128gcm\0".nonceInfois the static string"Content-Encoding: nonce\0".- Padding is at the end of each plaintext chunk. The padding block comprises the delimiter, which is
0x02for the last chunk, and0x01for the other chunks. Up tors - 16bytes of0x0padding can follow the delimiter.
aesgcm
prkInfois the static string"Content-Encoding: auth\0".keyInfois"Content-Encoding: aesgcm\0P-256\0", followed by the length-prefixed (unsigned 16-bit integers) receiver and sender public keys in uncompressed form.nonceInfois"Content-Encoding: nonce\0P-256\0", followed by the length-prefixed public keys in the same form askeyInfo.- Padding is at the beginning of each plaintext chunk. The padding block comprises the number (unsigned 16-bit integer) of padding bytes, followed by that many
0x0-valued bytes.
License
MIT.