web-push-libs / ecec Goto Github PK
View Code? Open in Web Editor NEWWeb Push encryption and decryption in C.
License: MIT License
Web Push encryption and decryption in C.
License: MIT License
How can I send push messages using vapid?
Most of the code coverage gaps are in error handling code. Covering every branch seems unrealistic, but testing at least one error path per function is better than what we have now.
Apart from bad input, one way to hit error paths might be to simulate memory allocation failures. This article explains how we can provoke allocation errors. SQLite has a sophisticated instrumented memory allocator for tests, too.
ece_webpush_aesgcm_headers_extract_params
uses a hand-rolled state machine parser. Parsing strings in C is scary, and Crypto-Key
and Encryption
are deprecated, anyway. It's better to use a higher-level language like Rust or Swift to extract the params, and pass them to ece_webpush_aes128gcm_decrypt
.
Hi to all!
I'm trying to use the ecec library to encrypt a webpush payload from my C program, but I find the documentation a bit obscure, so I ask you for help!
Mozilla Firefox 57 is giving me 'Error: Bad encryption'.
This is my code (I have removed error checking for summarize):
#include "../ecec/include/ece.h"
const char* b64_p256dh = "BAsRT....."; // from the PushManager keys.p256dh
const char* b64_auth = "R434..."; // from the PushManager keys.auth
const char* plaintext = "If the wind in my sail on the sea stays behind me, one day I'll know how far I'll go";
uint8_t p256dh[66] = { 0 }, auth[24] = { 0 };
uint8_t salt[ECE_SALT_LENGTH] = { 0 };
uint8_t rawSenderPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH] = { 0 };
uint8_t ciphertext[4096] = { 0 };
size_t ciphertextLen = ece_aesgcm_ciphertext_max_length(26, 6, strlen(plaintext));
char b64_ciphertext[4096*2] = { 0 };
int len_p256dh = ece_base64url_decode(b64_p256dh, strlen(b64_p256dh), ECE_BASE64URL_IGNORE_PADDING, p256dh, ECE_WEBPUSH_PUBLIC_KEY_LENGTH);
int len_auth = ece_base64url_decode(b64_auth, strlen(b64_auth), ECE_BASE64URL_IGNORE_PADDING, auth, ECE_WEBPUSH_AUTH_SECRET_LENGTH);
int res = ece_webpush_aesgcm_encrypt(p256dh, ECE_WEBPUSH_PUBLIC_KEY_LENGTH, auth, ECE_WEBPUSH_AUTH_SECRET_LENGTH, 26, 6, (const uint8_t*)plaintext, strlen(plaintext), salt, ECE_SALT_LENGTH, rawSenderPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH, ciphertext, &ciphertextLen);
size_t dhHeaderLen = 0;
size_t saltHeaderLen = 0;
ece_webpush_aesgcm_headers_from_params(salt, ECE_SALT_LENGTH, rawSenderPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH, 14, NULL, &dhHeaderLen, NULL, &saltHeaderLen);
char* dhHeader = (char*)malloc(dhHeaderLen + 1);
char* saltHeader = (char*)malloc(saltHeaderLen + 1);
ece_webpush_aesgcm_headers_from_params(salt, ECE_SALT_LENGTH, rawSenderPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH, 26, dhHeader, &dhHeaderLen, saltHeader, &saltHeaderLen);
dhHeader[dhHeaderLen] = '\0';
saltHeader[saltHeaderLen] = '\0';
int len_b64_ciphertext = ece_base64url_encode(ciphertext, ciphertextLen, ECE_BASE64URL_OMIT_PADDING, b64_ciphertext, sizeof(b64_ciphertext));
printf("curl -v -X POST \\\n");
printf(" -H \"Encryption: %s\" \\\n", saltHeader);
printf(" -H \"Authorization: WebPush %s\" \\\n", out);
printf(" -H \"Crypto-Key: %s; p256ecdsa=%s\" \\\n", dhHeader, vapid_pub_key);
printf(" -H \"Content-Length: %d\" \\\n", len_b64_ciphertext);
printf(" -H \"Content-Type: application/octet-stream\" \\\n");
printf(" -H \"Content-Encoding: aesgcm\" \\\n");
printf(" -H \"TTL: 0\" \\\n");
printf(" -d \"%s\" \\\n", b64_ciphertext);
printf(" \"%s\"\n", url);
When I execute the curl command that this program outputs, Mozilla Firefox gives me this error:
PushService:decryptAndNotifyApp: Error decrypting message "https://.../notificaciones/" gAAAA..... Error: Bad encryption
Stack trace:
CryptoError@resource://gre/modules/PushCrypto.jsm:60:5
decode@resource://gre/modules/PushCrypto.jsm:332:13
What can I be doing wrong? And also some doubts:
Buffers are convenient for internal use, but they complicate FFI callers. Let's make ece_buf_t
private, or remove it entirely.
Hi,
I'm really struggling how to use the aes128gcm
example. I have the pub and priv keys, as well as the PubSubscription data.
Many thanks in advance.
MSVC has a different set of warnings than clang and GCC. This might require manually installing OpenSSL 1.1.0 onto the build workers. This thread has some tips.
The signatures would look something like:
int
ece_aes128gcm_encrypt(const ece_buf_t* rawRecvPubKey,
const ece_buf_t* authSecret,
const ece_buf_t* plaintext,
uint32_t rs,
ece_buf_t* payload);
int
ece_aesgcm_encrypt(const ece_buf_t* rawRecvPubKey,
const ece_buf_t* authSecret,
const ece_buf_t* plaintext,
uint32_t rs,
char** cryptoKeyHeader,
char** encryptionHeader,
ece_buf_t* ciphertext);
So we can catch leaks and uses of uninitialized memory!
Even though it's now outdated, "aesgcm" is the most widely deployed version. Firefox Desktop doesn't support "aes128gcm" yet, and neither does the Node Web Push library. I think the key derivation process is identical, so we should be able to reuse those functions. The major differences are:
Crypto-Key
and Encryption
HTTP headers, instead of in the message).We can skip the even older "aesgcm128". It's significantly different, and the only browser to support it is pre-Firefox 45.
OpenSSL includes a replaceable memory allocator. If we decide to simulate failures using an instrumented allocator for #9, it makes sense to use OpenSSL's functions so that we don't need to write our own version of CRYPTO_set_mem_functions
.
On the other hand, this makes things harder for callers, who would need to import openssl/crypto.h
to free buffer contents. I'm also not sure how well this would work if we add an NSS backend later.
vapid target does not compile without adding header:
#include <getopt.h>
to the vapid.c
clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
Target: x86_64-pc-linux-gnu
When I tried to make a shared library uses libecec.a, ar shows error with explanation to use -fPIC options on libecec.a so I fixed it by adding line:
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
into cmake file.
Let's change the signature to ece_aesgcm_plaintext_max_length(rs, ciphertextLen)
, and subtract ECE_TAG_LENGTH * ((ciphertextLen / rs) + 1)
.
We currently allocate an array of zeroes and pass it to the encrypt_block_t
functions. There's no need to do this: since we know the padding length, we can write the delimiter and padding directly into the ciphertext
array using EVP_EncryptUpdate
, one byte at a time.
The dh
and salt
params use Base64url (-
and _
instead of +
and /
) encoding, without padding (=
). We can add a function to build these headers, like so:
// Determine the lengths of the `Crypto-Key` and `Encryption` headers.
size_t cryptoKeyHeaderLen = 0;
size_t encryptionHeaderLen = 0;
err = ece_webpush_aesgcm_headers_from_params(salt, ECE_SALT_LENGTH,
rawSenderPubKey,
ECE_WEBPUSH_PUBLIC_KEY_LENGTH,
/* rs */ 4096,
/* cryptoKeyHeader */ NULL,
&cryptoKeyHeaderLen,
/* encryptionHeader */ NULL,
&encryptionHeaderLen);
assert(!err);
// Allocate space and build the headers.
char* cryptoKeyHeader = malloc(cryptoKeyHeaderLen);
char* encryptionHeader = malloc(encryptionHeaderLen);
err = ece_webpush_aesgcm_headers_from_params(salt, ECE_SALT_LENGTH,
rawSenderPubKey,
ECE_WEBPUSH_PUBLIC_KEY_LENGTH,
/* rs */ 4096,
cryptoKeyHeader,
&cryptoKeyHeaderLen,
encryptionHeader,
&encryptionHeaderLen);
assert(!err);
free(cryptoKeyHeader);
free(encryptionHeader);
ece_decrypt
and ece_encrypt
operate on the full plaintext and ciphertext. This is fine for Web Push, which shouldn't be used for large messages, but we'll want to think about a streaming API for other uses. Here's a sketch of how such an API might look:
// Create a stream with a buffer size of 8k.
ece_decrypt_stream_t* s = ece_decrypt_stream_new(ECE_SCHEME_AES128GCM, 8192);
ece_buf_t ciphertext;
// Read encrypted blocks from a file, socket, libuv stream, etc.
size_t written = ece_decrypt_stream_write(s, &ciphertext);
if (written < ciphertext.length) {
// Once the buffer is full, or an error occurs, read the decrypted data.
// `chunk` is a slice of the stream's buffer, not a copy. This avoids an
// intermediate copy if we want to write the plaintext to another stream,
// but does mean we'll need to copy the slice if we want to keep the plaintext
// in memory.
ece_buf_t chunk;
int err = ece_decrypt_stream_read(s, &chunk);
if (err) {
// Decryption failed. Close the source stream and free `s`.
ece_decrypt_stream_free(s);
}
} else {
// We can keep writing to `s`.
}
// Once we've written all decrypted data from the source stream, flush all
// data remaining in the buffer. Returns true if we need to read from `s` again.
if (ece_decrypt_stream_flush(s)) {
ece_buf_t lastChunk;
int err = ece_decrypt_stream_read(s, &lastChunk);
if (err) {
ece_decrypt_stream_free(s);
}
}
Some services like Google's MCS using padding, e.g.
encryption: salt=3jFrNEVgtPynKcHPmHXawA==
ece_webpush_aesgcm_headers_extract_params() return -13 error in this case.
American fuzzy lop looks interesting.
Coverity Scan and Cppcheck look interesting. There's a nice comparison here. ecec is small, but the combination of crypto, string parsing, and C makes me nervous. 😁
This is a good place for a safe and expressive systems language, and would also provide some opportunities for learning about shipping Rust on iOS.
I used OpenSSL for expediency, but there's no reason we can't use NSS if it exposes similar primitives. @martinthomson, if you have cycles to help, or point me in the right direction, that would be most welcome.
For aesgcm, we need to check and fail if !(ciphertextLen % (rs + ECE_TAG_LENGTH))
. aes128gcm doesn't need this.
It's inefficient to copy values of ece_buf_t.
The PRK generation in ece_aes128gcm_derive_key_and_nonce
is Web Push-specific, but the key and nonce derivation, chunking, and decryption are generic. The PRK derivation could move to an ece_aes128gcm_webpush_derive_prk
function, and the generic decryption routine would then take the PRK as an input.
This would be more involved for aesgcm, but, given that it's an older encoding that's only used for Web Push, I think we can leave it be.
Now that the API has stabilized a bit, and this is a serious project™, let's make sure we're not doing anything silly.
assert
doesn't evaluate its argument in a non-debug build, so the examples are misleading.
I spent some time this afternoon thinking about how we handle padding, and I think it's still wrong. In particular, we'll underflow aes128gcm records if rs < plaintextLen < padLen
, but not if rs = overhead + 1
. The updated Node library emits truncated records for this case, too, so we'll need to change it.
I think this snippet will handle it correctly, but I'm not entirely sure:
// The maximum amount of data (plaintext and padding) that will fit into
// a block. The last block can be smaller.
size_t dataPerBlock = rs - overhead;
// The offset at which to start reading the plaintext.
size_t plaintextStart = 0;
// The record sequence number.
size_t counter = 0;
bool lastRecord = false;
while (!lastRecord) {
// Consume the padding first, leaving 1 byte for the plaintext. For
// "aesgcm", `blockPadLen` must also fit in a `uint16_t`.
size_t blockPadLen = dataPerBlock - 1;
if (padLen && !blockPadLen) {
// If `dataPerBlock` is 1, we can only include 1 byte of data, so write
// the padding first.
blockPadLen++;
}
if (blockPadLen > padLen) {
blockPadLen = padLen;
}
padLen -= blockPadLen;
// Fill the rest of the block with plaintext.
size_t plaintextEnd = plaintextStart + dataPerBlock - blockPadLen;
if (plaintextEnd > plaintextLen) {
plaintextEnd = plaintextLen;
if (!padLen) {
// We've reached the last record when the plaintext and padding are
// exhausted. For "aesgcm", `lastRecord = plaintextEnd % rs > 0`;
// we need to write an empty trailing block if the ciphertext is a
// multiple of the record size.
lastRecord = true;
}
}
size_t blockPlaintextLen = plaintextEnd - plaintextStart;
size_t blockLen = blockPadLen + blockPlaintextLen;
if (!lastRecord && blockLen < dataPerBlock) {
// We have padding left, but not enough plaintext to form a full record.
// Writing trailing padding-only records will still leak size information,
// so we fail encryption.
err = ECE_ERROR_ENCRYPT_PADDING;
goto error;
}
// ...
}
@martinthomson, does that look right to you?
So that bindings can link to it.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.