junobuild-auth 0.4.2

Authentication toolkit for Juno.
Documentation
use crate::openid::credentials::delegation::types::interface::OpenIdDelegationCredentialKey;
use crate::state::types::state::Salt;
use ic_certification::Hash;
use sha2::{Digest, Sha256};

pub fn calculate_seed(
    key: &OpenIdDelegationCredentialKey,
    salt: &Option<Salt>,
) -> Result<Hash, String> {
    let salt =
        salt.ok_or("The salt has not been initialized. A seed cannot be calculated.".to_string())?;

    let mut blob: Vec<u8> = vec![];
    blob.push(salt.len() as u8);
    blob.extend_from_slice(&salt);

    blob.push(key.iss.len() as u8);
    blob.extend(key.iss.bytes());

    blob.push(key.sub.len() as u8);
    blob.extend(key.sub.bytes());

    let seed = hash_bytes(blob);
    Ok(seed)
}

fn hash_bytes(value: impl AsRef<[u8]>) -> Hash {
    let mut hasher = Sha256::new();
    hasher.update(value.as_ref());
    hasher.finalize().into()
}

#[cfg(test)]
mod tests {
    use super::calculate_seed;
    use crate::openid::credentials::delegation::types::interface::OpenIdDelegationCredentialKey;
    use crate::state::types::state::Salt;
    use ic_certification::Hash;
    use sha2::{Digest, Sha256};

    fn salt(bytes: u8) -> Salt {
        [bytes; 32]
    }

    fn key<'a>(iss: &'a String, sub: &'a String) -> OpenIdDelegationCredentialKey<'a> {
        OpenIdDelegationCredentialKey { iss, sub }
    }

    fn build_blob(key: &OpenIdDelegationCredentialKey, salt: &Salt) -> Vec<u8> {
        let mut blob: Vec<u8> = vec![];

        blob.push(salt.len() as u8);
        blob.extend_from_slice(salt);

        blob.push(key.iss.len() as u8);
        blob.extend(key.iss.as_bytes());

        blob.push(key.sub.len() as u8);
        blob.extend(key.sub.as_bytes());

        blob
    }

    fn sha256(bytes: &[u8]) -> Hash {
        let mut h = Sha256::new();
        h.update(bytes);
        h.finalize().into()
    }

    #[test]
    fn deterministic_for_same_inputs() {
        let salt = salt(0x11);
        let iss = "https://accounts.google.com".to_string();
        let sub = "abc123".to_string();
        let key = key(&iss, &sub);

        let a = calculate_seed(&key, &Some(salt)).expect("ok");
        let b = calculate_seed(&key, &Some(salt)).expect("ok");
        assert_eq!(a, b);
    }

    #[test]
    fn matches_manual_blob_hash() {
        let salt = salt(0x42);
        let iss = "https://accounts.google.com".to_string();
        let sub = "user-sub-001".to_string();
        let key = key(&iss, &sub);
        let client_id = "my-client";

        let expected = sha256(&build_blob(&key, &salt));
        let got = calculate_seed(&key, &Some(salt)).expect("ok");

        assert_eq!(
            got, expected,
            "seed must be SHA-256 over the length-prefixed blob"
        );
    }

    #[test]
    fn changes_when_iss_changes() {
        let salt = salt(0x7A);
        let iss_a = "https://accounts.google.com".to_string();
        let iss_b = "https://issuer.example".to_string();
        let sub = "abc123".to_string();

        let a_key = key(&iss_a, &sub);
        let b_key = key(&iss_b, &sub);

        let a = calculate_seed(&a_key, &Some(salt)).unwrap();
        let b = calculate_seed(&b_key, &Some(salt)).unwrap();
        assert_ne!(a, b);
    }

    #[test]
    fn changes_when_sub_changes() {
        let salt = salt(0x7A);
        let iss = "https://accounts.google.com".to_string();
        let sub_a = "sub-a".to_string();
        let sub_b = "sub-b".to_string();

        let a_key = key(&iss, &sub_a);
        let b_key = key(&iss, &sub_b);

        let a = calculate_seed(&a_key, &Some(salt)).unwrap();
        let b = calculate_seed(&b_key, &Some(salt)).unwrap();
        assert_ne!(a, b);
    }

    #[test]
    fn changes_when_salt_changes() {
        let iss = "https://accounts.google.com".to_string();
        let sub = "abc123".to_string();
        let key = key(&iss, &sub);

        let a = calculate_seed(&key, &Some(salt(0x00))).unwrap();
        let b = calculate_seed(&key, &Some(salt(0xFF))).unwrap();
        assert_ne!(a, b);
    }

    #[test]
    fn errors_when_no_salt() {
        let iss = "https://accounts.google.com".to_string();
        let sub = "abc123".to_string();
        let key = key(&iss, &sub);

        let err = calculate_seed(&key, &None).unwrap_err();
        assert!(
            err.contains("The salt has not been initialized"),
            "got: {err}"
        );
    }
}