native/metamorphic_crypto_nif/metamorphic-crypto/src/b64.rs

//! Base64 utilities (standard alphabet, with padding).
//!
//! Matches JavaScript `btoa` / `atob` encoding.

use base64::{Engine as _, engine::general_purpose::STANDARD};

use crate::CryptoError;

/// Encode bytes to standard base64 with padding.
#[inline]
pub fn encode(bytes: &[u8]) -> String {
    STANDARD.encode(bytes)
}

/// Decode a standard base64 string to bytes.
#[inline]
pub fn decode(s: &str) -> Result<Vec<u8>, CryptoError> {
    STANDARD.decode(s).map_err(CryptoError::Base64)
}

/// Extract the salt portion from a key_hash string (`{salt_b64}$argon2id`).
pub fn parse_salt_from_key_hash(key_hash: &str) -> Result<&str, CryptoError> {
    let (salt, rest) = key_hash
        .split_once('$')
        .ok_or(CryptoError::InvalidKeyHash)?;
    if rest.contains('$') {
        return Err(CryptoError::InvalidKeyHash);
    }
    Ok(salt)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn roundtrip() {
        let data = b"hello world";
        let encoded = encode(data);
        assert_eq!(decode(&encoded).unwrap(), data);
    }

    #[test]
    fn matches_js_btoa() {
        // JS: btoa("hello") === "aGVsbG8="
        assert_eq!(encode(b"hello"), "aGVsbG8=");
        assert_eq!(decode("aGVsbG8=").unwrap(), b"hello");
    }

    #[test]
    fn parse_salt_valid() {
        let salt = parse_salt_from_key_hash("c2FsdA==$argon2id").unwrap();
        assert_eq!(salt, "c2FsdA==");
    }

    #[test]
    fn parse_salt_invalid() {
        assert!(parse_salt_from_key_hash("noseparator").is_err());
        assert!(parse_salt_from_key_hash("a$b$c").is_err());
    }
}