LCOV - code coverage report
Current view: top level - crates/tc-crypto/src - lib.rs (source / functions) Coverage Total Hit
Test: Rust Backend Coverage Lines: 66.7 % 15 10
Test Date: 2025-12-20 21:58:40 Functions: 60.0 % 5 3

            Line data    Source code
       1              : //! Shared cryptographic utilities for `TinyCongress`
       2              : //!
       3              : //! This crate provides cryptographic functions used by both the Rust backend
       4              : //! (as a native library) and the TypeScript frontend (compiled to WASM).
       5              : 
       6              : use base64::engine::general_purpose::URL_SAFE_NO_PAD;
       7              : use base64::Engine;
       8              : use sha2::{Digest, Sha256};
       9              : use wasm_bindgen::prelude::*;
      10              : 
      11              : /// Error type for base64url decoding failures
      12              : #[derive(Debug, thiserror::Error)]
      13              : #[error("invalid base64url encoding: {0}")]
      14              : pub struct DecodeError(#[from] base64::DecodeError);
      15              : 
      16              : /// Derive a key identifier (KID) from a public key.
      17              : ///
      18              : /// The KID is computed as: `base64url(SHA-256(pubkey)[0:16])`
      19              : ///
      20              : /// This produces a ~22 character string that uniquely identifies a public key
      21              : /// while being shorter than the full hash.
      22              : ///
      23              : /// # Arguments
      24              : /// * `public_key` - The public key bytes (typically 32 bytes for Ed25519)
      25              : ///
      26              : /// # Returns
      27              : /// A base64url-encoded string (without padding) of the first 16 bytes of the SHA-256 hash
      28              : #[wasm_bindgen]
      29              : #[must_use]
      30           23 : pub fn derive_kid(public_key: &[u8]) -> String {
      31           23 :     let hash = Sha256::digest(public_key);
      32              :     // Truncate to first 16 bytes for shorter KIDs
      33           23 :     URL_SAFE_NO_PAD.encode(&hash[..16])
      34           23 : }
      35              : 
      36              : /// Encode bytes as base64url (RFC 4648) without padding.
      37              : ///
      38              : /// # Arguments
      39              : /// * `bytes` - The bytes to encode
      40              : ///
      41              : /// # Returns
      42              : /// A base64url-encoded string without padding characters
      43              : #[wasm_bindgen]
      44              : #[must_use]
      45           16 : pub fn encode_base64url(bytes: &[u8]) -> String {
      46           16 :     URL_SAFE_NO_PAD.encode(bytes)
      47           16 : }
      48              : 
      49              : /// Decode a base64url-encoded string (RFC 4648) to bytes.
      50              : ///
      51              : /// Accepts input with or without padding.
      52              : ///
      53              : /// # Arguments
      54              : /// * `encoded` - The base64url-encoded string
      55              : ///
      56              : /// # Returns
      57              : /// The decoded bytes
      58              : ///
      59              : /// # Errors
      60              : /// Returns `JsError` if the input is not valid base64url
      61              : #[wasm_bindgen]
      62            0 : pub fn decode_base64url(encoded: &str) -> Result<Vec<u8>, JsError> {
      63            0 :     URL_SAFE_NO_PAD
      64            0 :         .decode(encoded)
      65            0 :         .map_err(|e| JsError::new(&e.to_string()))
      66            0 : }
      67              : 
      68              : /// Native Rust API for decoding base64url (returns proper Rust error type)
      69              : ///
      70              : /// # Errors
      71              : /// Returns `DecodeError` if the input is not valid base64url
      72           11 : pub fn decode_base64url_native(encoded: &str) -> Result<Vec<u8>, DecodeError> {
      73           11 :     URL_SAFE_NO_PAD.decode(encoded).map_err(DecodeError::from)
      74           11 : }
      75              : 
      76              : #[cfg(test)]
      77              : mod tests {
      78              :     use super::*;
      79              : 
      80              :     #[test]
      81              :     fn test_derive_kid_deterministic() {
      82              :         let pubkey = [1u8; 32];
      83              :         let kid1 = derive_kid(&pubkey);
      84              :         let kid2 = derive_kid(&pubkey);
      85              :         assert_eq!(kid1, kid2);
      86              :     }
      87              : 
      88              :     #[test]
      89              :     fn test_derive_kid_length() {
      90              :         let pubkey = [0u8; 32];
      91              :         let kid = derive_kid(&pubkey);
      92              :         // 16 bytes -> ~22 base64 chars (without padding)
      93              :         assert!(kid.len() >= 21 && kid.len() <= 22);
      94              :     }
      95              : 
      96              :     #[test]
      97              :     fn test_derive_kid_known_vector() {
      98              :         // Test vector: all-ones pubkey should produce consistent KID
      99              :         let pubkey = [1u8; 32];
     100              :         let kid = derive_kid(&pubkey);
     101              :         // This is the expected output - if this changes, the algorithm changed
     102              :         assert_eq!(kid, "cs1uhCLEB_ttCYaQ8RMLfQ");
     103              :     }
     104              : 
     105              :     #[test]
     106              :     fn test_encode_base64url() {
     107              :         let bytes = b"Hello";
     108              :         let encoded = encode_base64url(bytes);
     109              :         assert_eq!(encoded, "SGVsbG8");
     110              :     }
     111              : 
     112              :     #[test]
     113              :     fn test_decode_base64url_native() {
     114              :         let encoded = "SGVsbG8";
     115              :         let decoded = decode_base64url_native(encoded).expect("decode should succeed");
     116              :         assert_eq!(decoded, b"Hello");
     117              :     }
     118              : 
     119              :     #[test]
     120              :     fn test_roundtrip() {
     121              :         let original = b"test data for roundtrip";
     122              :         let encoded = encode_base64url(original);
     123              :         let decoded = decode_base64url_native(&encoded).expect("decode should succeed");
     124              :         assert_eq!(decoded, original);
     125              :     }
     126              : 
     127              :     #[test]
     128              :     fn test_decode_invalid_base64url() {
     129              :         let invalid = "not valid base64!!!";
     130              :         let result = decode_base64url_native(invalid);
     131              :         assert!(result.is_err());
     132              :     }
     133              : }
     134              : 
     135              : #[cfg(test)]
     136              : mod proptests {
     137              :     use super::*;
     138              :     use proptest::prelude::*;
     139              : 
     140              :     proptest! {
     141              :         /// Any byte sequence can be encoded and decoded back to the original
     142              :         #[test]
     143              :         fn roundtrip_encode_decode(bytes: Vec<u8>) {
     144              :             let encoded = encode_base64url(&bytes);
     145              :             let decoded = decode_base64url_native(&encoded).unwrap();
     146              :             prop_assert_eq!(decoded, bytes);
     147              :         }
     148              : 
     149              :         /// KID derivation is deterministic - same input always produces same output
     150              :         #[test]
     151              :         fn derive_kid_deterministic(pubkey: Vec<u8>) {
     152              :             let kid1 = derive_kid(&pubkey);
     153              :             let kid2 = derive_kid(&pubkey);
     154              :             prop_assert_eq!(kid1, kid2);
     155              :         }
     156              : 
     157              :         /// KID output length is always 21-22 chars (16 bytes base64url encoded)
     158              :         #[test]
     159              :         fn derive_kid_length_invariant(pubkey: Vec<u8>) {
     160              :             let kid = derive_kid(&pubkey);
     161              :             prop_assert!(kid.len() >= 21 && kid.len() <= 22,
     162              :                 "KID length {} not in expected range 21-22", kid.len());
     163              :         }
     164              : 
     165              :         /// Encoded output contains only valid base64url characters
     166              :         #[test]
     167              :         fn encode_produces_valid_base64url_chars(bytes: Vec<u8>) {
     168              :             let encoded = encode_base64url(&bytes);
     169              :             prop_assert!(encoded.chars().all(|c|
     170              :                 c.is_ascii_alphanumeric() || c == '-' || c == '_'
     171              :             ));
     172              :         }
     173              :     }
     174              : }
        

Generated by: LCOV version 2.0-1