use std::collections::HashMap;
use crate::{ffi, mem};
#[derive(Debug, thiserror::Error)]
pub enum CryptoError {
#[error("crypto: host returned no result")]
NoResult,
#[error("crypto: invalid base64 ciphertext")]
BadBase64,
}
pub fn hash(algorithm: &str, input: &str) -> Option<String> {
let args = serde_json::json!({
"algorithm": algorithm,
"input": input,
});
let args_str = args.to_string();
let (args_ptr, args_len) = mem::host_arg_str(&args_str);
let result = unsafe { ffi::crypto_hash(args_ptr, args_len) };
unsafe { mem::read_packed_string(result) }
}
pub fn hmac(secret_key: &str, input: &str) -> Option<String> {
let args = serde_json::json!({
"secret_key": secret_key,
"input": input,
});
let args_str = args.to_string();
let (args_ptr, args_len) = mem::host_arg_str(&args_str);
let result = unsafe { ffi::crypto_hmac(args_ptr, args_len) };
unsafe { mem::read_packed_string(result) }
}
pub fn sign_jwt(
algorithm: &str,
secret_key: &str,
claims: &HashMap<String, String>,
) -> Option<String> {
let args = serde_json::json!({
"algorithm": algorithm,
"secret_key": secret_key,
"claims": claims,
});
let args_str = args.to_string();
let (args_ptr, args_len) = mem::host_arg_str(&args_str);
let result = unsafe { ffi::crypto_sign_jwt(args_ptr, args_len) };
unsafe { mem::read_packed_string(result) }
}
pub fn encrypt_aes(secret_key: &str, plaintext: &str) -> Result<String, CryptoError> {
let args = serde_json::json!({
"secret_key": secret_key,
"input": plaintext,
});
let args_str = args.to_string();
let (args_ptr, args_len) = mem::host_arg_str(&args_str);
let result = unsafe { ffi::crypto_encrypt_aes(args_ptr, args_len) };
unsafe { mem::read_packed_string(result) }.ok_or(CryptoError::NoResult)
}
pub fn decrypt_aes(secret_key: &str, ciphertext_b64: &str) -> Result<Vec<u8>, CryptoError> {
let args = serde_json::json!({
"secret_key": secret_key,
"input": ciphertext_b64,
});
let args_str = args.to_string();
let (args_ptr, args_len) = mem::host_arg_str(&args_str);
let result = unsafe { ffi::crypto_decrypt_aes(args_ptr, args_len) };
unsafe { mem::read_packed_bytes(result) }.ok_or(CryptoError::NoResult)
}
#[derive(Debug, thiserror::Error)]
pub enum RandomBytesError {
#[error("random_bytes: host returned no data")]
NoData,
#[error("random_bytes: host returned malformed hex")]
BadHex,
}
pub fn random_bytes(length: u32) -> Result<Vec<u8>, RandomBytesError> {
let result = unsafe { ffi::crypto_random_bytes(length as i32) };
let hex_str = unsafe { mem::read_packed_string(result) }.ok_or(RandomBytesError::NoData)?;
mem::hex_decode(&hex_str).ok_or(RandomBytesError::BadHex)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ffi::test_host;
fn parse_args(s: &str) -> serde_json::Value {
serde_json::from_str(s).expect("captured args must be valid JSON")
}
#[test]
fn hash_constructs_args_and_returns_response() {
test_host::reset();
test_host::with_mock(|m| {
m.crypto_hash_response = Some("abcdef1234".into());
});
let result = hash("sha256", "hello world").unwrap();
assert_eq!(result, "abcdef1234");
let captured = test_host::read_mock(|m| m.last_crypto_hash_args.clone()).unwrap();
let args = parse_args(&captured);
assert_eq!(args["algorithm"], "sha256");
assert_eq!(args["input"], "hello world");
}
#[test]
fn hash_returns_none_when_host_empty() {
test_host::reset();
assert!(hash("sha256", "x").is_none());
}
#[test]
fn hmac_constructs_args() {
test_host::reset();
test_host::with_mock(|m| {
m.crypto_hmac_response = Some("deadbeef".into());
});
let mac = hmac("signing-key", "payload").unwrap();
assert_eq!(mac, "deadbeef");
let captured = test_host::read_mock(|m| m.last_crypto_hmac_args.clone()).unwrap();
let args = parse_args(&captured);
assert_eq!(args["secret_key"], "signing-key");
assert_eq!(args["input"], "payload");
}
#[test]
fn sign_jwt_serializes_claims() {
test_host::reset();
test_host::with_mock(|m| {
m.crypto_sign_jwt_response = Some("h.p.s".into());
});
let mut claims = HashMap::new();
claims.insert("sub".to_string(), "user-42".to_string());
claims.insert("aud".to_string(), "api".to_string());
let jwt = sign_jwt("HS256", "session-key", &claims).unwrap();
assert_eq!(jwt, "h.p.s");
let captured = test_host::read_mock(|m| m.last_crypto_sign_jwt_args.clone()).unwrap();
let args = parse_args(&captured);
assert_eq!(args["algorithm"], "HS256");
assert_eq!(args["secret_key"], "session-key");
assert_eq!(args["claims"]["sub"], "user-42");
assert_eq!(args["claims"]["aud"], "api");
}
#[test]
fn encrypt_aes_returns_ciphertext() {
test_host::reset();
test_host::with_mock(|m| {
m.crypto_encrypt_aes_response = Some("base64ciphertext==".into());
});
let ct = encrypt_aes("key", "plaintext").unwrap();
assert_eq!(ct, "base64ciphertext==");
let captured = test_host::read_mock(|m| m.last_crypto_encrypt_aes_args.clone()).unwrap();
let args = parse_args(&captured);
assert_eq!(args["secret_key"], "key");
assert_eq!(args["input"], "plaintext");
}
#[test]
fn encrypt_aes_no_result_is_error() {
test_host::reset();
match encrypt_aes("key", "x").unwrap_err() {
CryptoError::NoResult => {}
other => panic!("expected NoResult, got {:?}", other),
}
}
#[test]
fn decrypt_aes_returns_plaintext_bytes() {
test_host::reset();
test_host::with_mock(|m| {
m.crypto_decrypt_aes_response = Some(b"plaintext".to_vec());
});
let pt = decrypt_aes("key", "base64ct").unwrap();
assert_eq!(pt, b"plaintext");
}
#[test]
fn decrypt_aes_no_result_is_error() {
test_host::reset();
match decrypt_aes("key", "x").unwrap_err() {
CryptoError::NoResult => {}
other => panic!("expected NoResult, got {:?}", other),
}
}
#[test]
fn random_bytes_decodes_hex() {
test_host::reset();
test_host::with_mock(|m| {
m.crypto_random_bytes_response = Some("deadbeef".into());
});
let bytes = random_bytes(4).unwrap();
assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
assert_eq!(
test_host::read_mock(|m| m.last_random_bytes_length),
Some(4)
);
}
#[test]
fn random_bytes_no_data_is_error() {
test_host::reset();
match random_bytes(8).unwrap_err() {
RandomBytesError::NoData => {}
other => panic!("expected NoData, got {:?}", other),
}
}
#[test]
fn random_bytes_bad_hex_is_error() {
test_host::reset();
test_host::with_mock(|m| {
m.crypto_random_bytes_response = Some("xxxx".into());
});
match random_bytes(2).unwrap_err() {
RandomBytesError::BadHex => {}
other => panic!("expected BadHex, got {:?}", other),
}
}
}