Skip to content

Commit

Permalink
Implement aes128gcm, remove legacy code, add exactor agnostic client,…
Browse files Browse the repository at this point in the history
… update docs, and more. (#30)

* WIP : implementing ECE with ECE crate. Missing tests. Not compiling yet

* Restaured front padding for both encryption methods

* Removed structures that are no longer in use

* Removed tests that are now handled by the rust_ece crate

* Removed obsolete comment

* Updated readme to include new standard

* Removed tests that are covered in the rust-ece crate
prepared tests for headers that are left to do

* Implemented some of the headers tests

* Fixed numbers with new double-padding
(some small padding is added in the rust-ece crate)

* Implemented last tests for aes128gcm

* Cargo fmt

* WIP : added option for testing in the example file

* Cargo fmt

* Remove padding from aes128Gcm.
This allows for aes128Gcm to fully operate in all tested browsers, including edge.

* Remove unneeded base64URL encoding.

* Remove uses of aesgcm.

* Fix tests, store VAPID keys as bytes, and add reusable VAPID builder.
This commit will fix the base64 encoding issues by directly reading the PEM key as bytes instead of immediately encoding, allowing consumers to choose to base64 encode or not.

* Fix doc-tests.

* Remove FCM specific code.
Use the autopush implementation for all connections. This is possible as web push has been standardised, leaving this legacy code redundant. The FCM crate should cover all uses of FCM by this crates consumers.

* Use isahc as the default client backend.
Add `hyper-client` feature to optionally use the old hyper client. Isach is runtime independent, allowing this crate to be used in non Tokio environments. The hyper client remains as a potentially faster and more maintained option. Isach is the default as I feel the crate should work out of the box on any async executor.

* Update docs and README.md.
Expose request_builder now that it's generic and documented.

* Bump to v0.8

* Add method to get the public key from VAPID signature.

* Add encryption test and change fake subs into real subs.

* Fix final doc test.

* Add migration info to README.md.

Co-authored-by: John Tiesselune <[email protected]>
  • Loading branch information
andyblarblar and John Tiesselune authored Aug 10, 2021
1 parent 85870ec commit 4cac72a
Show file tree
Hide file tree
Showing 18 changed files with 569 additions and 905 deletions.
17 changes: 12 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "web-push"
description = "Web push notification client with support for http-ece encryption and VAPID authentication."
version = "0.7.3"
version = "0.8.0"
authors = ["Julius de Bruijn <[email protected]>"]
license = "Apache-2.0"
homepage = "https://github.com/pimeys/rust-web-push"
Expand All @@ -15,23 +15,30 @@ edition = "2018"
[badges]
travis-ci = { repository = "pimeys/rust-web-push" }

[features]
default = ["isahc"]
hyper-client = ["hyper", "hyper-tls"] #use features = ["hyper-client"], default-features = false for about 300kb size decrease.

[dependencies]
futures = "^0.3"
hyper = {version = "^0.14", features = ["client", "http1"]}
hyper-tls = "^0.5"
hyper = { version = "^0.14", features = ["client", "http1"], optional = true }
hyper-tls = { version = "^0.5", optional = true }
isahc = { version = "^1.4.0", optional = true }
http = "^0.2"
serde = "^1.0"
serde_json = "^1.0"
serde_derive = "^1.0"
ring = "^0.16"
ece = "^2.1.0"
native-tls = "^0.2"
base64 = "^0.13"
openssl = "^0.10"
time = {version = "^0.2", features = ["std"]}
time = { version = "^0.2", features = ["std"] }
lazy_static = "^1.4"
chrono = "^0.4"
log = "^0.4"

[dev-dependencies]
argparse = "^0.2"
tokio = { version = "^1.1", features = ["macros","rt-multi-thread"] }
regex = "^1.5"
tokio = { version = "^1.1", features = ["macros", "rt-multi-thread"] }
109 changes: 76 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Rust Web Push
=============

[![Cargo tests](https://github.com/pimeys/rust-web-push/actions/workflows/test.yml/badge.svg)](https://github.com/pimeys/rust-web-push/actions/workflows/test.yml)
[![crates.io](http://meritbadge.herokuapp.com/web_push)](https://crates.io/crates/web_push)
[![crates.io](https://img.shields.io/crates/d/web-push)](https://crates.io/crates/web_push)
[![docs.rs](https://docs.rs/web-push/badge.svg)](https://docs.rs/web-push)

[Matrix chat](https://matrix.to/#/#rust-push:nauk.io?via=nauk.io&via=matrix.org&via=shine.horse)
Expand All @@ -11,13 +11,25 @@ Web push notification sender.

## Requirements

Needs a Tokio executor version 0.2 or later and Rust compiler version 1.39.0 or later.
Any async executor for use with client.

## Migration to v0.8

- The `aesgcm` variant of `ContentEncoding` has been removed. Aes128Gcm support was added in v0.8, so all uses
of `ContentEncoding::aesgcm` can simply be changed to `ContentEncoding::Aes128Gcm` with no change to functionality.
This will add support for Edge in the process.

- `WebPushClient::new()` now returns a `Result`, as the default client now has a fallible constructor. Please handle
this error in the case of resource starvation.

- All GCM/FCM support has been removed. If you relied on this functionality, consider
the [fcm crate](https://crates.io/crates/fcm). If you just require web push, you will need to use VAPID to send
payloads. See below for info.

## Usage

To send a web push from command line, first subscribe to receive push
notifications with your browser and store the subscription info into a json
file. It should have the following content:
To send a web push from command line, first subscribe to receive push notifications with your browser and store the
subscription info into a json file. It should have the following content:

``` json
{
Expand All @@ -30,83 +42,114 @@ file. It should have the following content:
```

Google has
[good instructions](https://developers.google.com/web/updates/2015/03/push-notifications-on-the-open-web) for
building a frontend to receive notifications.
[good instructions](https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user) for building a
frontend to receive notifications.

Store the subscription info to `examples/test.json` and send a notification with
`cargo run --example simple_send -- -f examples/test.json -p "It works!"`. If
using Google Chrome, you need to register yourself
into [Firebase](https://firebase.google.com/) and provide a GCM API Key with
parameter `-k GCM_API_KEY`.
`cargo run --example simple_send -- -f examples/test.json -p "It works!"`.

Examples
Example
--------

To see it used in a real project, take a look to the [XORC
Notifications](https://github.com/xray-tech/xorc-notifications), which is a
full-fledged consumer for sending push notifications.
```rust
use web_push::*;
use base64::URL_SAFE;
use std::fs::File;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
let endpoint = "https://updates.push.services.mozilla.com/wpush/v1/...";
let p256dh = "key_from_browser_as_base64";
let auth = "auth_from_browser_as_base64";

//You would likely get this by deserializing a browser `pushSubscription` object.
let subscription_info = SubscriptionInfo::new(
endpoint,
p256dh,
auth
);

//Read signing material for payload.
let file = File::open("private.pem").unwrap();
let mut sig_builder = VapidSignatureBuilder::from_pem(file, &subscription_info)?.build()?;

//Now add payload and encrypt.
let mut builder = WebPushMessageBuilder::new(&subscription_info)?;
let content = "Encrypted payload to be sent in the notification".as_bytes();
builder.set_payload(ContentEncoding::Aes128Gcm, content);
builder.set_vapid_signature(sig_builder);

let client = WebPushClient::new()?;

//Finally, send the notification!
client.send(builder.build()?).await?;
Ok(())
}
```

VAPID
-----

VAPID authentication prevents unknown sources sending notifications to the
client and allows sending notifications to Chrome without signing in to Firebase
and providing a GCM API key.
VAPID authentication prevents unknown sources sending notifications to the client and is required by all current
browsers when sending a payload.

The private key to be used by the server can be generated with OpenSSL:

```
openssl ecparam -genkey -name prime256v1 -out private_key.pem
```

To derive a public key from the just-generated private key, to be used in the
JavaScript client:
To derive a public key from the just-generated private key, to be used in the JavaScript client:

```
openssl ec -in private_key.pem -pubout -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n'
```

The signature is created with `VapidSignatureBuilder`. It automatically adds the
required claims `aud` and `exp`. Adding these claims to the builder manually
will override the default values.
The signature is created with `VapidSignatureBuilder`. It automatically adds the required claims `aud` and `exp`. Adding
these claims to the builder manually will override the default values.

Overview
--------

Currently implements
[HTTP-ECE Draft-3](https://datatracker.ietf.org/doc/draft-ietf-httpbis-encryption-encoding/03/?include_text=1)
content encryption for notification payloads. The client requires
[Tokio](https://tokio.rs) for asynchronious requests. The modular design allows
an easy extension for the upcoming aes128gcm when the browsers are getting
support for it.
Currently, implements
[RFC8188](https://datatracker.ietf.org/doc/html/rfc8188) content encryption for notification payloads. This is done by
delegating encryption to mozilla's [ece crate](https://crates.io/crates/ece). Our security is thus tied
to [theirs](https://github.com/mozilla/rust-ece/issues/18). The default client is built
on [isahc](https://crates.io/crates/isahc), but can be swapped out with a hyper based client using the
`hyper-client` feature. Custom clients can be made using the `request_builder` module.

Tested with Google's and Mozilla's push notification services.
Library tested with Google's and Mozilla's push notification services. Also verified to work on Edge.

Debugging
--------
If you get an error or the push notification doesn't work you can try to debug using the following instructions:

Add the following to your Cargo.toml:

```cargo
log = "0.4"
pretty_env_logger = "0.3"
```

Add the following to your main.rs:

```rust
extern crate pretty_env_logger;

// ...
fn main() {
pretty_env_logger::init();
// ...
pretty_env_logger::init();
// ...
}
```

Or use any other logging library compatible with https://docs.rs/log/

Then run your program with the following environment variables:

```bash
RUST_LOG="web_push::client=trace" cargo run
```

This should print some more information about the requests to the push service which may aid you or somebody else in finding the error.
This should print some more information about the requests to the push service which may aid you or somebody else in
finding the error.
27 changes: 16 additions & 11 deletions examples/simple_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,27 @@ use web_push::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
let mut subscription_info_file = String::new();
let mut gcm_api_key: Option<String> = None;
let mut vapid_private_key: Option<String> = None;
let mut push_payload: Option<String> = None;
let mut encoding: Option<String> = None;
let mut ttl: Option<u32> = None;

{
let mut ap = ArgumentParser::new();
ap.set_description("A web push sender");

ap.refer(&mut gcm_api_key)
.add_option(&["-k", "--gcm_api_key"], StoreOption, "Google GCM API Key");

ap.refer(&mut vapid_private_key).add_option(
&["-v", "--vapid_key"],
StoreOption,
"A NIST P256 EC private key to create a VAPID signature",
);

ap.refer(&mut encoding).add_option(
&["-e", "--encoding"],
StoreOption,
"Content Encoding Scheme : currently only accepts 'aes128gcm'. Defaults to 'aes128gcm'. Reserved for future standards.",
);

ap.refer(&mut subscription_info_file).add_option(
&["-f", "--subscription_info_file"],
Store,
Expand All @@ -42,16 +45,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();

let ece_scheme = match encoding.as_deref() {
Some("aes128gcm") => ContentEncoding::Aes128Gcm,
None => ContentEncoding::Aes128Gcm,
Some(_) => panic!("Content encoding can only be 'aes128gcm'"),
};

let subscription_info: SubscriptionInfo = serde_json::from_str(&contents).unwrap();

let mut builder = WebPushMessageBuilder::new(&subscription_info).unwrap();

if let Some(ref payload) = push_payload {
builder.set_payload(ContentEncoding::AesGcm, payload.as_bytes());
}

if let Some(ref gcm_key) = gcm_api_key {
builder.set_gcm_key(gcm_key);
builder.set_payload(ece_scheme, payload.as_bytes());
}

if let Some(time) = ttl {
Expand All @@ -70,10 +75,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>
let signature = sig_builder.build().unwrap();

builder.set_vapid_signature(signature);
builder.set_payload(ContentEncoding::AesGcm, "test".as_bytes());
builder.set_payload(ContentEncoding::Aes128Gcm, "test".as_bytes());
};

let client = WebPushClient::new();
let client = WebPushClient::new()?;

let response = client.send(builder.build()?).await?;
println!("Sent: {:?}", response);
Expand Down
91 changes: 0 additions & 91 deletions src/client.rs

This file was deleted.

Loading

0 comments on commit 4cac72a

Please sign in to comment.