Skip to content

Commit

Permalink
Use constant-time base64 implementation
Browse files Browse the repository at this point in the history
Replaces the outdated base64 depenency. Instead of updating to the
latest version, this switches to ct_codecs, which is better
suited for working with cryptographic keys. Using ct_codecs instead of
alternatives like base64ct, because ct_codecs is already a dependency
via jwt-simple.

This also removes the reexported base64 configuration options from the
API. Web Push uses URL-safe encoding without padding everywhere. If needed
for custom purposes, users can encode/decode on their end, without having to
use types from the exact base64 crate used in this library.
  • Loading branch information
niklasf committed Dec 19, 2024
1 parent 95d8146 commit ccf1f91
Show file tree
Hide file tree
Showing 7 changed files with 39 additions and 46 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ jwt-simple = "0.11.2"
ece = "^2.2"
pem = "1.1.0"
sec1_decode = "^0.1.0"
base64 = "^0.13"
chrono = "^0.4"
log = "^0.4"
async-trait = "^0.1"
ct-codecs = "1.1.3"

[dev-dependencies]
argparse = "^0.2"
Expand Down
7 changes: 0 additions & 7 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::string::FromUtf8Error;
use std::time::{Duration, SystemTime};
use std::{convert::From, error::Error, fmt, io::Error as IoError};

use base64::DecodeError;
use http::uri::InvalidUri;
use serde_json::error::Error as JsonError;

Expand Down Expand Up @@ -107,12 +106,6 @@ impl From<IoError> for WebPushError {
}
}

impl From<DecodeError> for WebPushError {
fn from(_: DecodeError) -> WebPushError {
WebPushError::InvalidCryptoKeys
}
}

impl WebPushError {
pub fn short_description(&self) -> &'static str {
match *self {
Expand Down
20 changes: 11 additions & 9 deletions src/http_ece.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Payload encryption algorithm
use ct_codecs::{Base64UrlSafeNoPadding, Decoder, Encoder};
use ece::encrypt;

use crate::error::WebPushError;
Expand Down Expand Up @@ -90,7 +91,7 @@ impl<'a> HttpEce<'a> {
self.add_vapid_headers(&mut headers);

// ECE library base64 encodes content in aesgcm, but not aes128gcm, so decode base64 here to match the 128 API
let data = base64::decode_config(data.body(), base64::URL_SAFE_NO_PAD)
let data = Base64UrlSafeNoPadding::decode_to_vec(data.body(), None)
.expect("ECE library should always base64 encode");

Ok(WebPushPayload {
Expand All @@ -111,7 +112,8 @@ impl<'a> HttpEce<'a> {
format!(
"vapid t={}, k={}",
signature.auth_t,
base64::encode_config(&signature.auth_k, base64::URL_SAFE_NO_PAD)
Base64UrlSafeNoPadding::encode_to_string(&signature.auth_k)
.expect("encoding a valid auth_k cannot overflow")
),
));
}
Expand All @@ -127,22 +129,22 @@ impl<'a> HttpEce<'a> {

#[cfg(test)]
mod tests {
use base64::{self, URL_SAFE};
use regex::Regex;

use crate::error::WebPushError;
use crate::http_ece::{ContentEncoding, HttpEce};
use crate::VapidSignature;
use crate::WebPushPayload;
use ct_codecs::{Base64UrlSafeNoPadding, Decoder};

#[test]
fn test_payload_too_big() {
let p256dh = base64::decode_config(
let p256dh = Base64UrlSafeNoPadding::decode_to_vec(
"BLMaF9ffKBiWQLCKvTHb6LO8Nb6dcUh6TItC455vu2kElga6PQvUmaFyCdykxY2nOSSL3yKgfbmFLRTUaGv4yV8",
URL_SAFE,
None,
)
.unwrap();
let auth = base64::decode_config("xS03Fj5ErfTNH_l9WHE9Ig", URL_SAFE).unwrap();
let auth = Base64UrlSafeNoPadding::decode_to_vec("xS03Fj5ErfTNH_l9WHE9Ig", None).unwrap();
let http_ece = HttpEce::new(ContentEncoding::Aes128Gcm, &p256dh, &auth, None);
//This content is one above limit.
let content = [0u8; 3801];
Expand Down Expand Up @@ -191,12 +193,12 @@ mod tests {
}

fn setup_payload(vapid_signature: Option<VapidSignature>, encoding: ContentEncoding) -> WebPushPayload {
let p256dh = base64::decode_config(
let p256dh = Base64UrlSafeNoPadding::decode_to_vec(
"BLMbF9ffKBiWQLCKvTHb6LO8Nb6dcUh6TItC455vu2kElga6PQvUmaFyCdykxY2nOSSL3yKgfbmFLRTUaGv4yV8",
URL_SAFE,
None,
)
.unwrap();
let auth = base64::decode_config("xS03Fi5ErfTNH_l9WHE9Ig", URL_SAFE).unwrap();
let auth = Base64UrlSafeNoPadding::decode_to_vec("xS03Fi5ErfTNH_l9WHE9Ig", None).unwrap();

let http_ece = HttpEce::new(encoding, &p256dh, &auth, vapid_signature);
let content = "Hello, world!".as_bytes();
Expand Down
4 changes: 1 addition & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
//!
//! ```no_run
//! # 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.
//! //You would likely get this by deserializing a browser `pushSubscription` object.
//! let subscription_info = SubscriptionInfo::new(
//! endpoint,
//! p256dh,
Expand Down Expand Up @@ -62,7 +61,6 @@ pub use crate::message::{
};
pub use crate::vapid::builder::PartialVapidSignatureBuilder;
pub use crate::vapid::{VapidSignature, VapidSignatureBuilder};
pub use base64::{Config, BCRYPT, BINHEX, CRYPT, IMAP_MUTF7, STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD};

mod clients;
mod error;
Expand Down
11 changes: 7 additions & 4 deletions src/message.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use ct_codecs::{Base64UrlSafeNoPadding, Decoder};
use http::uri::Uri;
use std::fmt::{Display, Formatter};

Expand All @@ -8,9 +9,9 @@ use crate::vapid::VapidSignature;
/// Encryption keys from the client.
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)]
pub struct SubscriptionKeys {
/// The public key. Base64 encoded.
/// The public key. Base64-encoded, URL-safe alphabet, no padding.
pub p256dh: String,
/// Authentication secret. Base64 encoded.
/// Authentication secret. Base64-encoded, URL-safe alphabet, no padding.
pub auth: String,
}

Expand Down Expand Up @@ -183,8 +184,10 @@ impl<'a> WebPushMessageBuilder<'a> {
.transpose()?;

if let Some(payload) = self.payload {
let p256dh = base64::decode_config(&self.subscription_info.keys.p256dh, base64::URL_SAFE)?;
let auth = base64::decode_config(&self.subscription_info.keys.auth, base64::URL_SAFE)?;
let p256dh = Base64UrlSafeNoPadding::decode_to_vec(&self.subscription_info.keys.p256dh, None)
.map_err(|_| WebPushError::InvalidCryptoKeys)?;
let auth = Base64UrlSafeNoPadding::decode_to_vec(&self.subscription_info.keys.auth, None)
.map_err(|_| WebPushError::InvalidCryptoKeys)?;

let http_ece = HttpEce::new(payload.encoding, &p256dh, &auth, self.vapid_signature);

Expand Down
39 changes: 18 additions & 21 deletions src/vapid/builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use std::io::Read;

use ct_codecs::Base64UrlSafeNoPadding;
use http::uri::Uri;
use jwt_simple::prelude::*;
use serde_json::Value;
Expand Down Expand Up @@ -35,7 +36,7 @@ use crate::vapid::{VapidKey, VapidSignature, VapidSigner};
/// openssl ec -in private.pem -text -noout -conv_form uncompressed
/// ```
///
/// ... or a base64 encoded string, which the client should convert into
/// ... or a base64-encoded string, which the client should convert into
/// byte form before using:
///
/// ```bash,ignore
Expand Down Expand Up @@ -73,7 +74,6 @@ use crate::vapid::{VapidKey, VapidSignature, VapidSigner};
/// let signature = sig_builder.build().unwrap();
/// # }
/// ```
pub struct VapidSignatureBuilder<'a> {
claims: Claims,
key: VapidKey,
Expand Down Expand Up @@ -148,43 +148,40 @@ impl<'a> VapidSignatureBuilder<'a> {
})
}

/// Creates a new builder from a raw base64 encoded private key. This isn't the base64 from a key
/// Creates a new builder from a raw base64-encoded private key. This isn't the base64 from a key
/// generated by openssl, but rather the literal bytes of the private key itself. This is the kind
/// of key given to you by most VAPID key generator sites, and also the kind used in the API of other
/// large web push libraries, such as PHP and Node.
///
/// # Config
/// base64 has multiple encodings itself, the most common of which for web push is URL_SAFE_NO_PAD.
/// This function does support other encodings however, if needed.
/// Base64 encoding must use URL-safe alphabet without padding.
///
/// # Example
///
/// ```
/// # use web_push::VapidSignatureBuilder;
/// // Use `from_base64` here if you have a sub
/// let builder = VapidSignatureBuilder::from_base64_no_sub("IQ9Ur0ykXoHS9gzfYX0aBjy9lvdrjx_PFUXmie9YRcY", base64::URL_SAFE_NO_PAD).unwrap();
/// let builder = VapidSignatureBuilder::from_base64_no_sub("IQ9Ur0ykXoHS9gzfYX0aBjy9lvdrjx_PFUXmie9YRcY").unwrap();
/// ```
pub fn from_base64(
encoded: &str,
config: base64::Config,
subscription_info: &'a SubscriptionInfo,
) -> Result<VapidSignatureBuilder<'a>, WebPushError> {
let pr_key = ES256KeyPair::from_bytes(
&base64::decode_config(encoded, config).map_err(|_| WebPushError::InvalidCryptoKeys)?,
&Base64UrlSafeNoPadding::decode_to_vec(encoded, None).map_err(|_| WebPushError::InvalidCryptoKeys)?,
)
.map_err(|_| WebPushError::InvalidCryptoKeys)?;

Ok(Self::from_ec(pr_key, subscription_info))
}

/// Creates a new builder from a raw base64 encoded private key. This function doesn't take a subscription,
/// Creates a new builder from a raw base64-encoded private key. This function doesn't take a subscription,
/// allowing the reuse of one builder for multiple messages by cloning the resulting builder.
pub fn from_base64_no_sub(
encoded: &str,
config: base64::Config,
) -> Result<PartialVapidSignatureBuilder, WebPushError> {
///
/// Base64 encoding must use URL-safe alphabet without padding.
///
pub fn from_base64_no_sub(encoded: &str) -> Result<PartialVapidSignatureBuilder, WebPushError> {
let pr_key = ES256KeyPair::from_bytes(
&base64::decode_config(encoded, config).map_err(|_| WebPushError::InvalidCryptoKeys)?,
&Base64UrlSafeNoPadding::decode_to_vec(encoded, None).map_err(|_| WebPushError::InvalidCryptoKeys)?,
)
.map_err(|_| WebPushError::InvalidCryptoKeys)?;

Expand Down Expand Up @@ -296,7 +293,8 @@ impl PartialVapidSignatureBuilder {
mod tests {
use std::fs::File;

use ::lazy_static::lazy_static;
use ct_codecs::{Base64UrlSafeNoPadding, Encoder};
use lazy_static::lazy_static;

use crate::message::SubscriptionInfo;
use crate::vapid::VapidSignatureBuilder;
Expand Down Expand Up @@ -328,7 +326,7 @@ mod tests {

assert_eq!(
"BMo1HqKF6skMZYykrte9duqYwBD08mDQKTunRkJdD3sTJ9E-yyN6sJlPWTpKNhp-y2KeS6oANHF-q3w37bClb7U",
base64::encode_config(&signature.auth_k, base64::URL_SAFE_NO_PAD)
Base64UrlSafeNoPadding::encode_to_string(&signature.auth_k).unwrap()
);

assert!(!signature.auth_t.is_empty());
Expand All @@ -341,21 +339,20 @@ mod tests {

assert_eq!(
"BMo1HqKF6skMZYykrte9duqYwBD08mDQKTunRkJdD3sTJ9E-yyN6sJlPWTpKNhp-y2KeS6oANHF-q3w37bClb7U",
base64::encode_config(&signature.auth_k, base64::URL_SAFE_NO_PAD)
Base64UrlSafeNoPadding::encode_to_string(&signature.auth_k).unwrap()
);

assert!(!signature.auth_t.is_empty());
}

#[test]
fn test_builder_from_base64() {
let builder =
VapidSignatureBuilder::from_base64(PRIVATE_BASE64, base64::URL_SAFE_NO_PAD, &SUBSCRIPTION_INFO).unwrap();
let builder = VapidSignatureBuilder::from_base64(PRIVATE_BASE64, &SUBSCRIPTION_INFO).unwrap();
let signature = builder.build().unwrap();

assert_eq!(
"BMjQIp55pdbU8pfCBKyXcZjlmER_mXt5LqNrN1hrXbdBS5EnhIbMu3Au-RV53iIpztzNXkGI56BFB1udQ8Bq_H4",
base64::encode_config(&signature.auth_k, base64::URL_SAFE_NO_PAD)
Base64UrlSafeNoPadding::encode_to_string(&signature.auth_k).unwrap()
);

assert!(!signature.auth_t.is_empty());
Expand Down
2 changes: 1 addition & 1 deletion src/vapid/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{error::WebPushError, vapid::VapidKey};
/// [VapidSignatureBuilder](struct.VapidSignatureBuilder.html).
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct VapidSignature {
/// The signed JWT, base64 encoded
/// The signed JWT, base64-encoded
pub auth_t: String,
/// The public key bytes
pub auth_k: Vec<u8>,
Expand Down

0 comments on commit ccf1f91

Please sign in to comment.