Skip to content
This repository has been archived by the owner. It is now read-only.
2.0.0
Switch branches/tags
This branch is 13 commits ahead, 675 commits behind master.

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
src
 
 
 
 
 
 
 
 
 
 

ecec

GitHub version Build Status Coverage

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-Key header comprises one or more comma-delimited parameters. The first parameter must include a dh name-value pair, containing the sender's Base64url-encoded public key.
  • The Encryption header must include a salt name-value pair containing the sender's Base64url-encoded salt, and an optional rs pair 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

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 ..
> make

To build the decryption tool:

> make ece-decrypt
> ./ece-decrypt

To run the tests:

> make check

Windows

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-decrypt

To 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

  • prkInfo is the string "WebPush: info\0", followed by the receiver and sender public keys in uncompressed form. Unlike aesgcm, these are not length-prefixed.
  • keyInfo is the static string "Content-Encoding: aes128gcm\0".
  • nonceInfo is the static string "Content-Encoding: nonce\0".
  • Padding is at the end of each plaintext chunk. The padding block comprises the delimiter, which is 0x02 for the last chunk, and 0x01 for the other chunks. Up to rs - 16 bytes of 0x0 padding can follow the delimiter.

aesgcm

  • prkInfo is the static string "Content-Encoding: auth\0".
  • keyInfo is "Content-Encoding: aesgcm\0P-256\0", followed by the length-prefixed (unsigned 16-bit integers) receiver and sender public keys in uncompressed form.
  • nonceInfo is "Content-Encoding: nonce\0P-256\0", followed by the length-prefixed public keys in the same form as keyInfo.
  • 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.