use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::sync::Arc;
fn run(src: &str, fn_name: &str, args: Vec<Value>) -> Value {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors:\n{errs:#?}");
}
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(Policy::pure()).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(fn_name, args).unwrap_or_else(|e| panic!("call {fn_name}: {e}"))
}
fn b(xs: &[u8]) -> Value { Value::Bytes(xs.to_vec()) }
fn unwrap_ok(v: Value) -> Value {
match v {
Value::Variant { name, args } if name == "Ok" && args.len() == 1 => args.into_iter().next().unwrap(),
other => panic!("expected Ok(_), got {other:?}"),
}
}
fn unwrap_err(v: Value) -> String {
match v {
Value::Variant { name, args } if name == "Err" && args.len() == 1 => match args.into_iter().next().unwrap() {
Value::Str(s) => s,
other => panic!("Err payload not Str: {other:?}"),
},
other => panic!("expected Err(_), got {other:?}"),
}
}
fn unwrap_aead_result(v: Value) -> (Vec<u8>, Vec<u8>) {
let rec = match v {
Value::Record(r) => r,
other => panic!("expected AeadResult record, got {other:?}"),
};
let ct = match rec.get("ciphertext") {
Some(Value::Bytes(b)) => b.clone(),
other => panic!("AeadResult.ciphertext: expected Bytes, got {other:?}"),
};
let tag = match rec.get("tag") {
Some(Value::Bytes(b)) => b.clone(),
other => panic!("AeadResult.tag: expected Bytes, got {other:?}"),
};
(ct, tag)
}
const SRC: &str = r#"
import "std.crypto" as crypto
# AES-GCM round-trip wrappers
fn aes_seal(key :: Bytes, nonce :: Bytes, aad :: Bytes, pt :: Bytes) -> Result[AeadResult, Str] {
crypto.aes_gcm_seal(key, nonce, aad, pt)
}
fn aes_open(key :: Bytes, nonce :: Bytes, aad :: Bytes, ct :: Bytes, tag :: Bytes) -> Result[Bytes, Str] {
crypto.aes_gcm_open(key, nonce, aad, ct, tag)
}
# ChaCha20-Poly1305 round-trip wrappers
fn cc_seal(key :: Bytes, nonce :: Bytes, aad :: Bytes, pt :: Bytes) -> Result[AeadResult, Str] {
crypto.chacha20_poly1305_seal(key, nonce, aad, pt)
}
fn cc_open(key :: Bytes, nonce :: Bytes, aad :: Bytes, ct :: Bytes, tag :: Bytes) -> Result[Bytes, Str] {
crypto.chacha20_poly1305_open(key, nonce, aad, ct, tag)
}
"#;
const AES128_KEY: [u8; 16] = [0u8; 16];
const AES256_KEY: [u8; 32] = [0u8; 32];
const CHACHA_KEY: [u8; 32] = [0u8; 32];
const NONCE_12: [u8; 12] = [0u8; 12];
const PLAINTEXT: &[u8] = b"the quick brown fox jumps over the lazy dog";
const AAD: &[u8] = b"v1:metadata=foo";
#[test]
fn aes_gcm_128_round_trip() {
let r = run(
SRC, "aes_seal",
vec![b(&AES128_KEY), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let (ct, tag) = unwrap_aead_result(unwrap_ok(r));
assert_eq!(ct.len(), PLAINTEXT.len(), "AES-GCM ciphertext = plaintext length");
assert_eq!(tag.len(), 16, "AES-GCM tag is 16 bytes");
let pt = run(
SRC, "aes_open",
vec![b(&AES128_KEY), b(&NONCE_12), b(AAD), b(&ct), b(&tag)],
);
let recovered = match unwrap_ok(pt) {
Value::Bytes(p) => p,
other => panic!("expected Bytes, got {other:?}"),
};
assert_eq!(recovered, PLAINTEXT, "round-trip must recover plaintext");
}
#[test]
fn aes_gcm_256_round_trip() {
let r = run(
SRC, "aes_seal",
vec![b(&AES256_KEY), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let (ct, tag) = unwrap_aead_result(unwrap_ok(r));
let pt = run(
SRC, "aes_open",
vec![b(&AES256_KEY), b(&NONCE_12), b(AAD), b(&ct), b(&tag)],
);
let recovered = match unwrap_ok(pt) {
Value::Bytes(p) => p,
other => panic!("expected Bytes, got {other:?}"),
};
assert_eq!(recovered, PLAINTEXT);
}
#[test]
fn aes_gcm_rejects_modified_ciphertext() {
let r = run(
SRC, "aes_seal",
vec![b(&AES128_KEY), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let (mut ct, tag) = unwrap_aead_result(unwrap_ok(r));
ct[0] ^= 1; let pt = run(
SRC, "aes_open",
vec![b(&AES128_KEY), b(&NONCE_12), b(AAD), b(&ct), b(&tag)],
);
let _ = unwrap_err(pt); }
#[test]
fn aes_gcm_rejects_modified_aad() {
let r = run(
SRC, "aes_seal",
vec![b(&AES128_KEY), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let (ct, tag) = unwrap_aead_result(unwrap_ok(r));
let bad_aad = b"v2:different-aad";
let pt = run(
SRC, "aes_open",
vec![b(&AES128_KEY), b(&NONCE_12), b(bad_aad), b(&ct), b(&tag)],
);
let _ = unwrap_err(pt);
}
#[test]
fn aes_gcm_rejects_bad_key_length() {
let bad_key = [0u8; 20]; let r = run(
SRC, "aes_seal",
vec![b(&bad_key), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let msg = unwrap_err(r);
assert!(msg.contains("16 or 32"), "expected key-length error message: {msg}");
}
#[test]
fn aes_gcm_rejects_bad_nonce_length() {
let short_nonce = [0u8; 8];
let r = run(
SRC, "aes_seal",
vec![b(&AES128_KEY), b(&short_nonce), b(AAD), b(PLAINTEXT)],
);
let msg = unwrap_err(r);
assert!(msg.contains("12 bytes"), "expected nonce-length error message: {msg}");
}
#[test]
fn chacha20_round_trip() {
let r = run(
SRC, "cc_seal",
vec![b(&CHACHA_KEY), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let (ct, tag) = unwrap_aead_result(unwrap_ok(r));
assert_eq!(ct.len(), PLAINTEXT.len());
assert_eq!(tag.len(), 16);
let pt = run(
SRC, "cc_open",
vec![b(&CHACHA_KEY), b(&NONCE_12), b(AAD), b(&ct), b(&tag)],
);
let recovered = match unwrap_ok(pt) {
Value::Bytes(p) => p,
other => panic!("expected Bytes, got {other:?}"),
};
assert_eq!(recovered, PLAINTEXT);
}
#[test]
fn chacha20_rejects_modified_tag() {
let r = run(
SRC, "cc_seal",
vec![b(&CHACHA_KEY), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let (ct, mut tag) = unwrap_aead_result(unwrap_ok(r));
tag[0] ^= 1;
let pt = run(
SRC, "cc_open",
vec![b(&CHACHA_KEY), b(&NONCE_12), b(AAD), b(&ct), b(&tag)],
);
let _ = unwrap_err(pt);
}
#[test]
fn chacha20_rejects_wrong_key() {
let r = run(
SRC, "cc_seal",
vec![b(&CHACHA_KEY), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let (ct, tag) = unwrap_aead_result(unwrap_ok(r));
let other_key = [0xffu8; 32];
let pt = run(
SRC, "cc_open",
vec![b(&other_key), b(&NONCE_12), b(AAD), b(&ct), b(&tag)],
);
let _ = unwrap_err(pt);
}
#[test]
fn chacha20_rejects_bad_key_length() {
let bad_key = [0u8; 16]; let r = run(
SRC, "cc_seal",
vec![b(&bad_key), b(&NONCE_12), b(AAD), b(PLAINTEXT)],
);
let msg = unwrap_err(r);
assert!(msg.contains("32 bytes"), "expected key-length error: {msg}");
}
#[test]
fn aead_empty_plaintext_is_handled() {
let r = run(SRC, "cc_seal", vec![b(&CHACHA_KEY), b(&NONCE_12), b(AAD), b(&[])]);
let (ct, tag) = unwrap_aead_result(unwrap_ok(r));
assert!(ct.is_empty(), "ciphertext of empty plaintext must be empty");
assert_eq!(tag.len(), 16);
let pt = run(SRC, "cc_open", vec![b(&CHACHA_KEY), b(&NONCE_12), b(AAD), b(&ct), b(&tag)]);
let recovered = match unwrap_ok(pt) {
Value::Bytes(p) => p,
other => panic!("expected Bytes, got {other:?}"),
};
assert!(recovered.is_empty());
}