feat: Added age-ffi.

This commit is contained in:
2026-04-26 17:29:52 -04:00
parent a13264c80c
commit 02ce5e46b7
30 changed files with 7901 additions and 0 deletions

View File

@@ -0,0 +1,808 @@
//! Tests for file-based encryption and decryption functions.
use crate::file::*;
use crate::keys::*;
use crate::memory::*;
use crate::passphrase::*;
use crate::types::*;
use std::ffi::CString;
use std::fs;
use std::io::Write;
fn create_temp_file(suffix: &str) -> String {
let temp_dir = std::env::temp_dir();
let unique_id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("{}/age_test_{}_{}", temp_dir.display(), unique_id, suffix)
}
// ============= age_encrypt_to_file tests =============
#[test]
fn test_encrypt_to_file_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Hello, file encryption!";
let output_path = create_temp_file("encrypted.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
output_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
assert!(std::path::Path::new(&output_path).exists());
// Clean up
fs::remove_file(&output_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_to_file_null_plaintext() {
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let recipient = CString::new("age1test").unwrap();
let result = age_encrypt_to_file(
std::ptr::null(),
0,
output_path_c.as_ptr(),
recipient.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_encrypt_to_file_null_output_path() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
std::ptr::null(),
keypair.public_key,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_to_file_null_recipient() {
let plaintext = b"test";
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
output_path_c.as_ptr(),
std::ptr::null(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_encrypt_to_file_invalid_recipient() {
let plaintext = b"test";
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let invalid_recipient = CString::new("age1invalid_not_a_real_key").unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
output_path_c.as_ptr(),
invalid_recipient.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidRecipient);
}
#[test]
fn test_encrypt_to_file_and_decrypt_with_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Round trip file encryption test!";
let output_path = create_temp_file("roundtrip.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
// Encrypt to file
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
output_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Decrypt with identity string
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
output_path_c.as_ptr(),
keypair.private_key,
&mut output,
);
assert_eq!(result, AgeResult::Success);
let decrypted = unsafe { std::slice::from_raw_parts(output.data, output.len) };
assert_eq!(decrypted, plaintext);
// Clean up
fs::remove_file(&output_path).ok();
age_free_buffer(&mut output);
age_free_keypair(&mut keypair);
}
// ============= age_encrypt_to_file_armor tests =============
#[test]
fn test_encrypt_to_file_armor_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Armored file test";
let output_path = create_temp_file("armored.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let result = age_encrypt_to_file_armor(
plaintext.as_ptr(),
plaintext.len(),
output_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Verify the file is armored
let contents = fs::read_to_string(&output_path).unwrap();
assert!(contents.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
assert!(contents.contains("-----END AGE ENCRYPTED FILE-----"));
// Clean up
fs::remove_file(&output_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_to_file_armor_null_plaintext() {
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let recipient = CString::new("age1test").unwrap();
let result = age_encrypt_to_file_armor(
std::ptr::null(),
0,
output_path_c.as_ptr(),
recipient.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_encrypt_to_file_armor_null_output_path() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let result = age_encrypt_to_file_armor(
plaintext.as_ptr(),
plaintext.len(),
std::ptr::null(),
keypair.public_key,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_to_file_armor_invalid_recipient() {
let plaintext = b"test";
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let invalid_recipient = CString::new("not-a-recipient").unwrap();
let result = age_encrypt_to_file_armor(
plaintext.as_ptr(),
plaintext.len(),
output_path_c.as_ptr(),
invalid_recipient.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidRecipient);
}
// ============= age_decrypt_file tests =============
#[test]
fn test_decrypt_file_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Decrypt from identity file test";
// Create encrypted file
let encrypted_path = create_temp_file("encrypted.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Create identity file
let identity_path = create_temp_file("identity.txt");
let private_key = unsafe { std::ffi::CStr::from_ptr(keypair.private_key).to_str().unwrap() };
fs::write(&identity_path, private_key).unwrap();
let identity_path_c = CString::new(identity_path.as_str()).unwrap();
// Decrypt
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path_c.as_ptr(),
identity_path_c.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::Success);
let decrypted = unsafe { std::slice::from_raw_parts(output.data, output.len) };
assert_eq!(decrypted, plaintext);
// Clean up
fs::remove_file(&encrypted_path).ok();
fs::remove_file(&identity_path).ok();
age_free_buffer(&mut output);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_null_output() {
let encrypted_path = CString::new("/tmp/test.age").unwrap();
let identity_path = CString::new("/tmp/identity.txt").unwrap();
let result = age_decrypt_file(
encrypted_path.as_ptr(),
identity_path.as_ptr(),
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_null_encrypted_path() {
let identity_path = CString::new("/tmp/identity.txt").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
std::ptr::null(),
identity_path.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_nonexistent_identity_file() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create a real encrypted file
let plaintext = b"test";
let encrypted_path = create_temp_file("test_enc.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Try to decrypt with nonexistent identity file
let identity_path = CString::new("/nonexistent/identity.txt").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path_c.as_ptr(),
identity_path.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::IoError);
fs::remove_file(&encrypted_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_nonexistent_encrypted_file() {
// Create a valid identity file
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let identity_path = create_temp_file("identity.txt");
let private_key = unsafe { std::ffi::CStr::from_ptr(keypair.private_key).to_str().unwrap() };
fs::write(&identity_path, private_key).unwrap();
let identity_path_c = CString::new(identity_path.as_str()).unwrap();
let encrypted_path = CString::new("/nonexistent/encrypted.age").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path.as_ptr(),
identity_path_c.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::IoError);
fs::remove_file(&identity_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_empty_identity_file() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create encrypted file
let plaintext = b"test";
let encrypted_path = create_temp_file("enc.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Create empty identity file
let identity_path = create_temp_file("empty_identity.txt");
fs::write(&identity_path, "").unwrap();
let identity_path_c = CString::new(identity_path.as_str()).unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path_c.as_ptr(),
identity_path_c.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidIdentity);
fs::remove_file(&encrypted_path).ok();
fs::remove_file(&identity_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_with_comments_in_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create encrypted file
let plaintext = b"test with comments";
let encrypted_path = create_temp_file("enc_comments.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Create identity file with comments
let identity_path = create_temp_file("identity_with_comments.txt");
let private_key = unsafe { std::ffi::CStr::from_ptr(keypair.private_key).to_str().unwrap() };
let content = format!("# This is a comment\n\n{}\n# Another comment", private_key);
fs::write(&identity_path, content).unwrap();
let identity_path_c = CString::new(identity_path.as_str()).unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path_c.as_ptr(),
identity_path_c.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::Success);
let decrypted = unsafe { std::slice::from_raw_parts(output.data, output.len) };
assert_eq!(decrypted, plaintext);
fs::remove_file(&encrypted_path).ok();
fs::remove_file(&identity_path).ok();
age_free_buffer(&mut output);
age_free_keypair(&mut keypair);
}
// ============= age_decrypt_file_with_identity tests =============
#[test]
fn test_decrypt_file_with_identity_null_output() {
let encrypted_path = CString::new("/tmp/test.age").unwrap();
let identity = CString::new("AGE-SECRET-KEY-1TEST").unwrap();
let result = age_decrypt_file_with_identity(
encrypted_path.as_ptr(),
identity.as_ptr(),
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_with_identity_null_path() {
let identity = CString::new("AGE-SECRET-KEY-1TEST").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
std::ptr::null(),
identity.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_with_identity_invalid_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create encrypted file
let plaintext = b"test";
let encrypted_path = create_temp_file("enc_invalid_id.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
let invalid_identity = CString::new("not-a-valid-identity").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
invalid_identity.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidIdentity);
fs::remove_file(&encrypted_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_with_identity_wrong_key() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
// Encrypt with keypair1
let plaintext = b"secret message";
let encrypted_path = create_temp_file("wrong_key.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair1.public_key,
);
assert_eq!(result, AgeResult::Success);
// Try to decrypt with keypair2
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
keypair2.private_key,
&mut output,
);
assert_eq!(result, AgeResult::DecryptionFailed);
fs::remove_file(&encrypted_path).ok();
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_decrypt_file_with_identity_nonexistent_file() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let encrypted_path = CString::new("/nonexistent/file.age").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path.as_ptr(),
keypair.private_key,
&mut output,
);
assert_eq!(result, AgeResult::IoError);
age_free_keypair(&mut keypair);
}
// ============= age_decrypt_file_passphrase tests =============
#[test]
fn test_decrypt_file_passphrase_basic() {
let passphrase = CString::new("mysecretpassword").unwrap();
let plaintext = b"Passphrase protected content";
// Encrypt with passphrase first (using in-memory function)
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Write encrypted content to file
let encrypted_path = create_temp_file("passphrase.age");
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
fs::write(&encrypted_path, encrypted_slice).unwrap();
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
// Decrypt file with passphrase
let mut output = AgeBuffer::null();
let result = age_decrypt_file_passphrase(
encrypted_path_c.as_ptr(),
passphrase.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::Success);
let decrypted = unsafe { std::slice::from_raw_parts(output.data, output.len) };
assert_eq!(decrypted, plaintext);
// Clean up
fs::remove_file(&encrypted_path).ok();
age_free_buffer(&mut encrypted);
age_free_buffer(&mut output);
}
#[test]
fn test_decrypt_file_passphrase_null_output() {
let encrypted_path = CString::new("/tmp/test.age").unwrap();
let passphrase = CString::new("password").unwrap();
let result = age_decrypt_file_passphrase(
encrypted_path.as_ptr(),
passphrase.as_ptr(),
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_passphrase_null_path() {
let passphrase = CString::new("password").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_passphrase(
std::ptr::null(),
passphrase.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_passphrase_wrong_passphrase() {
let passphrase = CString::new("correctpassword").unwrap();
let wrong_passphrase = CString::new("wrongpassword").unwrap();
let plaintext = b"Secret content";
// Encrypt with passphrase
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Write to file
let encrypted_path = create_temp_file("wrong_pass.age");
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
fs::write(&encrypted_path, encrypted_slice).unwrap();
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
// Try to decrypt with wrong passphrase
let mut output = AgeBuffer::null();
let result = age_decrypt_file_passphrase(
encrypted_path_c.as_ptr(),
wrong_passphrase.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::DecryptionFailed);
// Clean up
fs::remove_file(&encrypted_path).ok();
age_free_buffer(&mut encrypted);
}
#[test]
fn test_decrypt_file_passphrase_nonexistent_file() {
let passphrase = CString::new("password").unwrap();
let encrypted_path = CString::new("/nonexistent/passphrase.age").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_passphrase(
encrypted_path.as_ptr(),
passphrase.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::IoError);
}
// ============= Recipient file tests =============
#[test]
fn test_encrypt_to_file_with_recipients_file() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
// Create recipients file
let recipients_path = create_temp_file("recipients.txt");
let pub_key1 = unsafe { std::ffi::CStr::from_ptr(keypair1.public_key).to_str().unwrap() };
let pub_key2 = unsafe { std::ffi::CStr::from_ptr(keypair2.public_key).to_str().unwrap() };
let content = format!("# Comment line\n{}\n{}\n", pub_key1, pub_key2);
fs::write(&recipients_path, content).unwrap();
let recipients_path_c = CString::new(recipients_path.as_str()).unwrap();
// Encrypt to file
let plaintext = b"Multi-recipient from file test";
let encrypted_path = create_temp_file("multi_recip.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
recipients_path_c.as_ptr(),
);
assert_eq!(result, AgeResult::Success);
// Both recipients should be able to decrypt
let mut output1 = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
keypair1.private_key,
&mut output1,
);
assert_eq!(result, AgeResult::Success);
let mut output2 = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
keypair2.private_key,
&mut output2,
);
assert_eq!(result, AgeResult::Success);
// Clean up
fs::remove_file(&recipients_path).ok();
fs::remove_file(&encrypted_path).ok();
age_free_buffer(&mut output1);
age_free_buffer(&mut output2);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_encrypt_to_file_empty_recipients_file() {
let plaintext = b"test";
let encrypted_path = create_temp_file("empty_recip.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
// Create empty recipients file
let recipients_path = create_temp_file("empty_recipients.txt");
fs::write(&recipients_path, "# Only comments\n\n").unwrap();
let recipients_path_c = CString::new(recipients_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
recipients_path_c.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidRecipient);
// Clean up
fs::remove_file(&recipients_path).ok();
}
#[test]
fn test_encrypt_to_file_nonexistent_recipients_file() {
let plaintext = b"test";
let encrypted_path = create_temp_file("test.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let recipients_path = CString::new("/nonexistent/recipients.txt").unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
recipients_path.as_ptr(),
);
assert_eq!(result, AgeResult::IoError);
}
#[test]
fn test_decrypt_file_corrupted_file() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create corrupted encrypted file
let encrypted_path = create_temp_file("corrupted.age");
fs::write(&encrypted_path, "not valid age encrypted content").unwrap();
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
keypair.private_key,
&mut output,
);
assert_eq!(result, AgeResult::DecryptionFailed);
// Clean up
fs::remove_file(&encrypted_path).ok();
age_free_keypair(&mut keypair);
}