Skip to main content

metamorphic_crypto/
b64.rs

1//! Base64 utilities (standard alphabet, with padding).
2//!
3//! Matches JavaScript `btoa` / `atob` encoding.
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6
7use crate::CryptoError;
8
9/// Encode bytes to standard base64 with padding.
10#[inline]
11pub fn encode(bytes: &[u8]) -> String {
12    STANDARD.encode(bytes)
13}
14
15/// Decode a standard base64 string to bytes.
16#[inline]
17pub fn decode(s: &str) -> Result<Vec<u8>, CryptoError> {
18    STANDARD.decode(s).map_err(CryptoError::Base64)
19}
20
21/// Extract the salt portion from a key_hash string (`{salt_b64}$argon2id`).
22pub fn parse_salt_from_key_hash(key_hash: &str) -> Result<&str, CryptoError> {
23    let (salt, rest) = key_hash
24        .split_once('$')
25        .ok_or(CryptoError::InvalidKeyHash)?;
26    if rest.contains('$') {
27        return Err(CryptoError::InvalidKeyHash);
28    }
29    Ok(salt)
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35
36    #[test]
37    fn roundtrip() {
38        let data = b"hello world";
39        let encoded = encode(data);
40        assert_eq!(decode(&encoded).unwrap(), data);
41    }
42
43    #[test]
44    fn matches_js_btoa() {
45        // JS: btoa("hello") === "aGVsbG8="
46        assert_eq!(encode(b"hello"), "aGVsbG8=");
47        assert_eq!(decode("aGVsbG8=").unwrap(), b"hello");
48    }
49
50    #[test]
51    fn parse_salt_valid() {
52        let salt = parse_salt_from_key_hash("c2FsdA==$argon2id").unwrap();
53        assert_eq!(salt, "c2FsdA==");
54    }
55
56    #[test]
57    fn parse_salt_invalid() {
58        assert!(parse_salt_from_key_hash("noseparator").is_err());
59        assert!(parse_salt_from_key_hash("a$b$c").is_err());
60    }
61}