web-push-native 0.4.0

Generic Event Delivery Using Http Push (web-push) components implemented according to RFC8030
Documentation
use super::*;
use base64ct::{Base64UrlUnpadded, Encoding};

macro_rules! DECODE {
    ($e:expr) => {
        Lazy::new(|| {
            let decoded = Base64UrlUnpadded::decode_vec($e).unwrap();
            decoded.try_into().unwrap()
        })
    };
    ($extract:expr, $e:expr) => {
        Lazy::new(|| {
            let decoded = Base64UrlUnpadded::decode_vec($e).unwrap();
            Some($extract(&decoded).ok()).flatten().unwrap()
        })
    };
}

#[test]
fn test_encryption_decryption() {
    let vapid_pair = jwt_simple::algorithms::ES256KeyPair::generate();
    let ece_secret = p256::SecretKey::random(&mut OsRng);
    let mut auth = vec![0u8; 16];
    OsRng.fill_bytes(&mut auth);
    let auth = Auth::clone_from_slice(&auth);

    let builder = WebPushBuilder::new(
        "https://example.com/".parse().unwrap(),
        ece_secret.public_key(),
        auth,
    )
    .with_vapid(&vapid_pair, "mailto:nobody@example.com");

    let plaintext = b"I am the walrus".to_vec();
    let ciphertext = builder.build(plaintext.clone()).unwrap().into_body();
    let decrypted = decrypt(ciphertext, &ece_secret, &auth).unwrap();
    assert_eq!(decrypted, plaintext);
}

#[test]
fn test_encryption_decryption_with_serialization() {
    let vapid_pair = jwt_simple::algorithms::ES256KeyPair::generate();
    let ece_secret = p256::SecretKey::random(&mut OsRng);
    let mut auth = vec![0u8; 16];
    OsRng.fill_bytes(&mut auth);
    let auth = Auth::clone_from_slice(&auth);

    let json = serde_json::json!({
       "endpoint": "https://example.com/",
       "expirationTime": (),
       "keys": {
           "auth": Base64UrlUnpadded::encode_string(&auth),
           "p256dh": Base64UrlUnpadded::encode_string(&ece_secret.public_key().to_encoded_point(false).to_bytes()),
      }
    }).to_string();

    let builder = serde_json::from_str::<WebPushBuilder>(&json)
        .unwrap()
        .with_vapid(&vapid_pair, "mailto:nobody@example.com");

    let plaintext = b"I am the walrus".to_vec();
    let ciphertext = builder.build(plaintext.clone()).unwrap().into_body();
    let decrypted = decrypt(ciphertext, &ece_secret, &auth).unwrap();
    assert_eq!(decrypted, plaintext);
}

#[test]
fn test_deserialize_owned() -> Result<(), jwt_simple::Error> {
    let ece_secret = p256::SecretKey::random(&mut OsRng);
    let mut auth = vec![0u8; 16];
    OsRng.fill_bytes(&mut auth);
    let auth = Auth::clone_from_slice(&auth);

    let json = serde_json::json!({
       "endpoint": "https://example.com/",
       "expirationTime": (),
       "keys": {
           "auth": Base64UrlUnpadded::encode_string(&auth),
           "p256dh": Base64UrlUnpadded::encode_string(&ece_secret.public_key().to_encoded_point(false).to_bytes()),
      }
    });

    serde_json::from_value::<WebPushBuilder>(json)?;

    Ok(())
}

mod rfc8291_example {
    use super::*;
    use once_cell::sync::Lazy;

    const PLAINTEXT: &[u8] = b"When I grow up, I want to be a watermelon";
    const CIPHERTEXT: Lazy<Vec<u8>> = DECODE!("DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN");
    const AUTH: Lazy<Auth> = DECODE!(
        |it| Ok::<_, ()>(Auth::clone_from_slice(it)),
        "BTBZMqHH6r4Tts7J_aSIgg"
    );
    const UA_PRIVATE: Lazy<p256::SecretKey> = DECODE!(
        p256::SecretKey::from_slice,
        "q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94"
    );
    const UA_PUBLIC: Lazy<p256::PublicKey> = DECODE!(
        p256::PublicKey::from_sec1_bytes,
        "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4"
    );
    const AS_PRIVATE: Lazy<p256::SecretKey> = DECODE!(
        p256::SecretKey::from_slice,
        "yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw"
    );
    const AS_PUBLIC: Lazy<p256::PublicKey> = DECODE!(
        p256::PublicKey::from_sec1_bytes,
        "BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8"
    );
    const SALT: Lazy<[u8; 16]> = DECODE!("DGv6ra1nlYgDCS1FRnbzlw");

    const IKM: Lazy<[u8; 32]> = DECODE!("S4lYMb_L0FxCeq0WhDx813KgSYqU26kOyzWUdsXYyrg");
    const SHARED: Lazy<[u8; 32]> = DECODE!("kyrL1jIIOHEzg3sM2ZWRHDRB62YACZhhSlknJ672kSs");

    #[test]
    fn test_ikm_derivation() {
        let shared =
            p256::ecdh::diffie_hellman(AS_PRIVATE.to_nonzero_scalar(), UA_PUBLIC.as_affine());
        assert_eq!(shared.raw_secret_bytes().as_slice(), &*SHARED);

        let ikm = compute_ikm(&AUTH, &shared, &UA_PUBLIC, &AS_PUBLIC);
        assert_eq!(ikm, *IKM);
    }

    #[test]
    fn test_encryption() {
        let ciphertext =
            encrypt_predictably(*SALT, PLAINTEXT.to_vec(), &AS_PRIVATE, &UA_PUBLIC, &AUTH).unwrap();

        assert_eq!(&ciphertext[21..], &CIPHERTEXT[21..]);
    }

    #[test]
    fn test_encryption_decryption() {
        let ciphertext =
            encrypt_predictably(*SALT, PLAINTEXT.to_vec(), &AS_PRIVATE, &UA_PUBLIC, &AUTH).unwrap();

        let plaintext = decrypt(ciphertext, &UA_PRIVATE, &AUTH).unwrap();

        assert_eq!(plaintext, PLAINTEXT);
    }
}