Skip to content

Commit 5ba0ddd

Browse files
committed
Update the public API to remove footguns, and document it.
This is a significant refactor of the public API of the crate, simplifying the API surface and removing some of the footgun potential noted by Martin in his review at mozilla/application-services#1068. In particular: * The public `encrypt` functions no longer take a `salt` parameter. The right thing to do is to generate a new random `salt` for each encryption so we just do that for you automatically. * Many internal implementation details are now `pub(crate)` rather than `pub`, to avoid potential confusion from consumers. * We refuse to encrypt or decrypt across multiple records, because our only consumer in practice is webpush, and webpush restricts consumers to using only a single record. We still have the code lying around to encrypt/decrypt across record boundaries, but we don't have high confidence that it works correctly and intend to remove it in a future commit. So, may as well adjust the interface to reflect that while we're in here making breaking changes. To go along with the revised interface, this commit also significantly expands to docs in order to help set consumer expectations and context.
1 parent 7945d7f commit 5ba0ddd

9 files changed

Lines changed: 364 additions & 294 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@ keywords = ["http-ece", "web-push"]
1212
byteorder = "1.3"
1313
thiserror = "1.0"
1414
base64 = "0.12"
15+
hex = "0.4"
1516
hkdf = { version = "0.9", optional = true }
1617
lazy_static = { version = "1.4", optional = true }
1718
once_cell = "1.4"
1819
openssl = { version = "0.10", optional = true }
1920
serde = { version = "1.0", features = ["derive"], optional = true }
2021
sha2 = { version = "0.9", optional = true }
2122

22-
[dev-dependencies]
23-
hex = "0.4"
24-
2523
[features]
2624
default = ["backend-openssl", "serializable-keys"]
2725
serializable-keys = ["serde"]

README.md

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,95 @@
55
[Latest Version]: https://img.shields.io/crates/v/ece.svg
66
[crates.io]: https://crates.io/crates/ece
77

8-
*This crate has not been security reviewed yet, use at your own risk ([tracking issue](https://github.com/mozilla/rust-ece/issues/18))*.
8+
*This crate has not been security reviewed yet, use at your own risk
9+
([tracking issue](https://github.com/mozilla/rust-ece/issues/18))*.
910

10-
[ece](https://crates.io/crates/ece) is a Rust implementation of the HTTP Encrypted Content-Encoding standard (RFC 8188). It is a port of the [ecec](https://github.com/web-push-libs/ecec) C library.
11-
This crate is destined to be used by higher-level Web Push libraries, both on the server and the client side.
11+
The [ece](https://crates.io/crates/ece) crate is a Rust implementation of Message Encryption for Web Push
12+
([RFC8291](https://tools.ietf.org/html/rfc8291)) and the HTTP Encrypted Content-Encoding scheme
13+
([RFC8188](https://tools.ietf.org/html/rfc8188)) on which it is based.
1214

13-
[Documentation](https://docs.rs/ece/)
15+
It provides low-level cryptographic "plumbing" and is destined to be used by higher-level Web Push libraries, both on
16+
the server and the client side. It is a port of the [ecec](https://github.com/web-push-libs/ecec) C library.
1417

15-
## Cryptographic backends
16-
17-
This crate is designed to be used with different crypto backends. At the moment only [openssl](https://github.com/sfackler/rust-openssl) is supported.
18+
[Full Documentation](https://docs.rs/ece/)
1819

1920
## Implemented schemes
2021

21-
Currently, two HTTP ece schemes are available to consumers of the crate:
22-
- The newer [RFC8188](https://tools.ietf.org/html/rfc8188) `aes128gcm` standard.
23-
- The legacy [draft-03](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03) `aesgcm` scheme.
22+
This crate implements both the published Web Push Encryption scheme, and a legacy scheme from earlier drafts
23+
that is still widely used in the wild:
24+
25+
* `aes128gcm`: the scheme described in [RFC8291](https://tools.ietf.org/html/rfc8291) and
26+
[RFC8188](https://tools.ietf.org/html/rfc8188)
27+
* `aesgcm`: the draft scheme described in
28+
[draft-ietf-webpush-encryption-04](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04) and
29+
[draft-ietf-httpbis-encryption-encoding-03](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03_)
30+
31+
## Usage
32+
33+
To receive messages via WebPush, the receiver must generate an EC keypair and a symmetric authentication secret,
34+
then distribute the public key and authentication secret to the sender:
35+
36+
```
37+
let (keypair, auth_secret) = ece::generate_keypair_and_auth_secret()?;
38+
let pubkey = keypair.pub_as_raw();
39+
// Base64-encode the `pubkey` and `auth_secret` bytes and distribute them to the sender.
40+
```
41+
42+
The sender can encrypt a Web Push message to the receiver's public key:
43+
44+
```
45+
let ciphertext = ece::encrypt(&pubkey, &auth_secret, b"payload")?;
46+
```
47+
48+
And the receiver can decrypt it using their private key:
49+
50+
```
51+
let plaintext = ece::decrypt(&keypair, &auth_secret, &ciphertext)?;
52+
```
53+
54+
That's pretty much all there is to it! It's up to the higher-level library to manage distributing the encrypted payload,
55+
typically by arranging for it to be included in a HTTP response with `Content-Encoding: aes128gcm` header.
56+
57+
### Legacy `aesgcm` encryption
58+
59+
The legacy `aesgcm` scheme is more complicated, because it communicates some encryption parameters in HTTP header fields
60+
rather than as part of the encrypted payload. When used for encryption, the sender must deal with `Encryption` and
61+
`Crypto-Key` headers in addition to the ciphertext:
62+
63+
```
64+
let encrypted_block = ece::legacy::encrypt_aesgcm(pubkey, auth_secret, b"payload")?;
65+
for (header, &value) in encrypted_block.headers().iter() {
66+
// Set header to corresponding value
67+
}
68+
// Send encrypted_block.body() as the body
69+
```
70+
71+
When receiving an `aesgcm` message, the receiver needs to parse encryption parameters from the `Encryption`
72+
and `Crypto-Key` fields:
73+
74+
```
75+
// Parse `rs`, `salt` and `dh` from the `Encryption` and `Crypto-Key` headers.
76+
// You'll need to consult the spec for how to do this; we might add some helpers one day.
77+
let encrypted_block = ece::AesGcmEncryptedBlock::new(dh, rs, salt, ciphertext);
78+
let plaintext = ece::legacy::decrypt_aesgcm(keypair, auth_secret, encrypted_block)?;
79+
```
80+
81+
### Unimplemented Features
82+
83+
* We do not implement streaming encryption or decryption, although the ECE scheme is designed to permit it.
84+
* We do not implement support for encrypting or decrypting across multiple records, although the ECE scheme is designed to permit it.
85+
* We do not support customizing the record size parameter during encryption, but do check it during decryption.
86+
* The default record size is 4096 bytes, which effectively limits the size of plaintext that can be encrypted to 3993 bytes.
87+
* We do not support customizing the padding bytes added during encryption.
88+
* We currently select the padding length at random for each encryption, but this is an implementation detail and
89+
should not be relied on.
90+
91+
These restrictions might be lifted in future, if it turns out that we need them.
92+
93+
## Cryptographic backends
94+
95+
This crate is designed to use pluggable backend implementations of low-level crypto primitives. different crypto
96+
backends. At the moment only [openssl](https://github.com/sfackler/rust-openssl) is supported.
2497

2598
## Release process
2699

@@ -33,5 +106,4 @@ make sure you have it installed and then:
33106
2. Run `cargo release --dry-run -vv [major|minor|patch]` and check that the things
34107
it's proposing to do seem sensible.
35108
3. Run `cargo release [major|minor|patch]` to prepare, commit, tag and publish the release.
36-
4. Make a PR from your `release-vX.Y.Z` branch to request it be merged to the main branch.
37-
109+
4. Make a PR from your `release-vX.Y.Z` branch to request it be merged to the main branch.

src/aes128gcm.rs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,8 @@ const ECE_AES128GCM_NONCE_INFO: &str = "Content-Encoding: nonce\0";
2929
/// Web Push encryption structure for the AES128GCM encoding scheme ([RFC8591](https://tools.ietf.org/html/rfc8291))
3030
///
3131
/// This structure is meant for advanced use. For simple encryption/decryption, use the top-level [`encrypt`](crate::encrypt) and [`decrypt`](crate::decrypt) functions.
32-
pub struct Aes128GcmEceWebPush;
32+
pub(crate) struct Aes128GcmEceWebPush;
3333
impl Aes128GcmEceWebPush {
34-
/// Encrypts a Web Push message using the "aes128gcm" scheme. This function
35-
/// automatically generates an ephemeral ECDH key pair.
36-
pub fn encrypt(
37-
remote_pub_key: &dyn RemotePublicKey,
38-
auth_secret: &[u8],
39-
plaintext: &[u8],
40-
params: WebPushParams,
41-
) -> Result<Vec<u8>> {
42-
let cryptographer = crypto::holder::get_cryptographer();
43-
let local_prv_key = cryptographer.generate_ephemeral_keypair()?;
44-
Self::encrypt_with_keys(
45-
&*local_prv_key,
46-
remote_pub_key,
47-
auth_secret,
48-
plaintext,
49-
params,
50-
)
51-
}
52-
5334
/// Encrypts a Web Push message using the "aes128gcm" scheme, with an explicit
5435
/// sender key. The sender key can be reused.
5536
pub fn encrypt_with_keys(

src/aesgcm.rs

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,20 @@ const ECE_WEBPUSH_RAW_KEY_LENGTH: usize = 65;
2525
const ECE_WEBPUSH_IKM_LENGTH: usize = 32;
2626

2727
pub struct AesGcmEncryptedBlock {
28-
pub dh: Vec<u8>,
29-
pub salt: Vec<u8>,
30-
pub rs: u32,
31-
pub ciphertext: Vec<u8>,
28+
pub(crate) dh: Vec<u8>,
29+
pub(crate) salt: Vec<u8>,
30+
pub(crate) rs: u32,
31+
pub(crate) ciphertext: Vec<u8>,
3232
}
3333

3434
impl AesGcmEncryptedBlock {
35-
pub fn aesgcm_rs(rs: u32) -> u32 {
35+
fn aesgcm_rs(rs: u32) -> u32 {
3636
if rs > u32::max_value() - ECE_TAG_LENGTH as u32 {
3737
return 0;
3838
}
3939
rs + ECE_TAG_LENGTH as u32
4040
}
4141

42-
/// Create a new block from the various header strings and body content.
4342
pub fn new(
4443
dh: &[u8],
4544
salt: &[u8],
@@ -87,35 +86,20 @@ impl AesGcmEncryptedBlock {
8786
}
8887

8988
/// Encode the body as a String.
90-
/// If you need the bytes, probably just call .ciphertext directly
9189
pub fn body(&self) -> String {
9290
base64::encode_config(&self.ciphertext, base64::URL_SAFE_NO_PAD)
9391
}
9492
}
95-
/// Web Push encryption structure for the legacy AESGCM encoding scheme ([Web Push Encryption Draft 4](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04))
93+
94+
/// Web Push encryption structure for the legacy AESGCM encoding scheme
95+
/// ([Web Push Encryption Draft 4](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04))
9696
///
97-
/// This structure is meant for advanced use. For simple encryption/decryption, use the top-level [`encrypt_aesgcm`](crate::legacy::encrypt_aesgcm) and [`decrypt_aesgcm`](crate::legacy::decrypt_aesgcm) functions.
98-
pub struct AesGcmEceWebPush;
99-
impl AesGcmEceWebPush {
100-
/// Encrypts a Web Push message using the "aesgcm" scheme. This function
101-
/// automatically generates an ephemeral ECDH key pair.
102-
pub fn encrypt(
103-
remote_pub_key: &dyn RemotePublicKey,
104-
auth_secret: &[u8],
105-
plaintext: &[u8],
106-
params: WebPushParams,
107-
) -> Result<AesGcmEncryptedBlock> {
108-
let cryptographer = crypto::holder::get_cryptographer();
109-
let local_prv_key = cryptographer.generate_ephemeral_keypair()?;
110-
Self::encrypt_with_keys(
111-
&*local_prv_key,
112-
remote_pub_key,
113-
auth_secret,
114-
plaintext,
115-
params,
116-
)
117-
}
97+
/// This structure is meant for advanced use. For simple encryption/decryption, use the top-level
98+
/// [`encrypt_aesgcm`](crate::legacy::encrypt_aesgcm) and [`decrypt_aesgcm`](crate::legacy::decrypt_aesgcm)
99+
/// functions.
100+
pub(crate) struct AesGcmEceWebPush;
118101

102+
impl AesGcmEceWebPush {
119103
/// Encrypts a Web Push message using the "aesgcm" scheme, with an explicit
120104
/// sender key. The sender key can be reused.
121105
pub fn encrypt_with_keys(
@@ -201,7 +185,7 @@ impl EceWebPush for AesGcmEceWebPush {
201185
Ok(&block[(2 + padding_size)..])
202186
}
203187

204-
/// Derives the "aesgcm" decryption keyn and nonce given the receiver private
188+
/// Derives the "aesgcm" decryption key and nonce given the receiver private
205189
/// key, sender public key, authentication secret, and sender salt.
206190
fn derive_key_and_nonce(
207191
ece_mode: EceMode,

src/common.rs

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,42 @@ use byteorder::{BigEndian, ByteOrder};
1010
use std::cmp::min;
1111

1212
// From keys.h:
13-
pub const ECE_AES_KEY_LENGTH: usize = 16;
14-
pub const ECE_NONCE_LENGTH: usize = 12;
13+
pub(crate) const ECE_AES_KEY_LENGTH: usize = 16;
14+
pub(crate) const ECE_NONCE_LENGTH: usize = 12;
1515

1616
// From ece.h:
17-
pub const ECE_SALT_LENGTH: usize = 16;
18-
pub const ECE_TAG_LENGTH: usize = 16;
19-
//const ECE_WEBPUSH_PRIVATE_KEY_LENGTH: usize = 32;
20-
pub const ECE_WEBPUSH_PUBLIC_KEY_LENGTH: usize = 65;
21-
pub const ECE_WEBPUSH_AUTH_SECRET_LENGTH: usize = 16;
22-
const ECE_WEBPUSH_DEFAULT_RS: u32 = 4096;
17+
pub(crate) const ECE_SALT_LENGTH: usize = 16;
18+
pub(crate) const ECE_TAG_LENGTH: usize = 16;
19+
pub(crate) const ECE_WEBPUSH_PUBLIC_KEY_LENGTH: usize = 65;
20+
pub(crate) const ECE_WEBPUSH_AUTH_SECRET_LENGTH: usize = 16;
21+
pub(crate) const ECE_WEBPUSH_DEFAULT_RS: u32 = 4096;
2322

2423
// TODO: Make it nicer to use with a builder pattern.
25-
pub struct WebPushParams {
24+
pub(crate) struct WebPushParams {
2625
pub rs: u32,
2726
pub pad_length: usize,
2827
pub salt: Option<Vec<u8>>,
2928
}
3029

31-
impl WebPushParams {
32-
/// Random salt, record size = 4096 and padding length = 0.
33-
pub fn default() -> Self {
30+
impl Default for WebPushParams {
31+
fn default() -> Self {
32+
// Random salt, record size = 4096 and padding length = 0.
3433
Self {
3534
rs: ECE_WEBPUSH_DEFAULT_RS,
36-
pad_length: 2,
35+
pad_length: 0,
3736
salt: None,
3837
}
3938
}
40-
41-
/// Never use the same salt twice as it will derive the same content encryption
42-
/// key for multiple messages if the same sender private key is used!
43-
pub fn new(rs: u32, pad_length: usize, salt: Vec<u8>) -> Self {
44-
Self {
45-
rs,
46-
pad_length,
47-
salt: Some(salt),
48-
}
49-
}
5039
}
5140

52-
pub enum EceMode {
41+
pub(crate) enum EceMode {
5342
ENCRYPT,
5443
DECRYPT,
5544
}
5645

57-
pub type KeyAndNonce = (Vec<u8>, Vec<u8>);
46+
pub(crate) type KeyAndNonce = (Vec<u8>, Vec<u8>);
5847

59-
pub trait EceWebPush {
48+
pub(crate) trait EceWebPush {
6049
fn common_encrypt(
6150
local_prv_key: &dyn LocalKeyPair,
6251
remote_pub_key: &dyn RemotePublicKey,
@@ -151,6 +140,12 @@ pub trait EceWebPush {
151140
plaintext_start = plaintext_end;
152141
counter += 1;
153142
}
143+
// Cheap way to error out if the plaintext didn't fit in a single record.
144+
// We're going to refactor away the multi-record stuff entirely in a future PR,
145+
// but doing this here now lets us set API expectations for the caller.
146+
if counter > 1 {
147+
return Err(Error::PlaintextTooLong);
148+
}
154149
Ok(ciphertext)
155150
}
156151

@@ -184,6 +179,12 @@ pub trait EceWebPush {
184179
)?;
185180
let chunks = ciphertext.chunks(rs as usize);
186181
let records_count = chunks.len();
182+
// Cheap way to error out if there are multiple records.
183+
// We're going to refactor away the multi-record stuff entirely in a future PR,
184+
// but doing this here now lets us set API expectations for the caller.
185+
if records_count > 1 {
186+
return Err(Error::MultipleRecordsNotSupported);
187+
}
187188
let items = chunks
188189
.enumerate()
189190
.map(|(count, record)| {

0 commit comments

Comments
 (0)