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,95 @@
//! ASCII armor utilities.
use crate::helpers::cstr_to_str;
use crate::helpers::string_to_cstr;
use crate::types::{AgeBuffer, AgeResult};
use std::io::{Read, Write};
use std::os::raw::c_char;
/// Wrap binary data in ASCII armor.
///
/// # Arguments
/// * `data` - Pointer to the binary data
/// * `data_len` - Length of the data
/// * `output` - Pointer to receive the armored string
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_armor(
data: *const u8,
data_len: usize,
output: *mut *mut c_char,
) -> AgeResult {
if data.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let data = unsafe { std::slice::from_raw_parts(data, data_len) };
let mut armored = Vec::new();
let mut writer = match age::armor::ArmoredWriter::wrap_output(&mut armored, age::armor::Format::AsciiArmor) {
Ok(w) => w,
Err(_) => return AgeResult::ArmorError,
};
if writer.write_all(data).is_err() {
return AgeResult::ArmorError;
}
if writer.finish().is_err() {
return AgeResult::ArmorError;
}
let armored_str = match String::from_utf8(armored) {
Ok(s) => s,
Err(_) => return AgeResult::ArmorError,
};
let c_output = match string_to_cstr(armored_str) {
Ok(s) => s,
Err(e) => return e,
};
unsafe {
*output = c_output;
}
AgeResult::Success
}
/// Remove ASCII armor from data.
///
/// # Arguments
/// * `armored` - The armored string
/// * `output` - Pointer to receive the binary buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_dearmor(
armored: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if output.is_null() {
return AgeResult::InvalidInput;
}
let armored_str = match unsafe { cstr_to_str(armored) } {
Ok(s) => s,
Err(e) => return e,
};
let mut reader = age::armor::ArmoredReader::new(armored_str.as_bytes());
let mut dearmored = Vec::new();
if reader.read_to_end(&mut dearmored).is_err() {
return AgeResult::ArmorError;
}
unsafe {
*output = AgeBuffer::from_vec(dearmored);
}
AgeResult::Success
}

View File

@@ -0,0 +1,175 @@
//! Tests for ASCII armor utilities.
use crate::armor::*;
use crate::encrypt::*;
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[test]
fn test_armor_basic() {
let data = b"Hello, this is binary data to armor!";
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_armor(data.as_ptr(), data.len(), &mut armored);
assert_eq!(result, AgeResult::Success);
assert!(!armored.is_null());
let armored_str = unsafe { CStr::from_ptr(armored).to_str().unwrap() };
assert!(armored_str.starts_with("-----BEGIN AGE ENCRYPTED FILE-----"));
assert!(armored_str.contains("-----END AGE ENCRYPTED FILE-----"));
age_free_string(armored);
}
#[test]
fn test_dearmor_basic() {
let data = b"Test data for dearmoring";
let mut armored: *mut c_char = std::ptr::null_mut();
age_armor(data.as_ptr(), data.len(), &mut armored);
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success);
let dearmored_slice = unsafe { std::slice::from_raw_parts(dearmored.data, dearmored.len) };
assert_eq!(dearmored_slice, data);
age_free_string(armored);
age_free_buffer(&mut dearmored);
}
#[test]
fn test_armor_round_trip() {
// Test with various data sizes (skip empty - armor requires data)
let test_data = [
b"A".to_vec(),
b"Short".to_vec(),
(0u16..256).map(|i| i as u8).collect::<Vec<u8>>(),
vec![0u8; 1000],
(0..10000).map(|i| (i % 256) as u8).collect::<Vec<u8>>(),
];
for data in &test_data {
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_armor(data.as_ptr(), data.len(), &mut armored);
assert_eq!(result, AgeResult::Success, "Failed to armor data of len {}", data.len());
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success, "Failed to dearmor data of len {}", data.len());
let dearmored_slice = unsafe { std::slice::from_raw_parts(dearmored.data, dearmored.len) };
assert_eq!(dearmored_slice, data.as_slice());
age_free_string(armored);
age_free_buffer(&mut dearmored);
}
}
#[test]
fn test_armor_null_input() {
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_armor(std::ptr::null(), 0, &mut armored);
assert_eq!(result, AgeResult::InvalidInput);
let result = age_armor(b"test".as_ptr(), 4, std::ptr::null_mut());
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_dearmor_null_input() {
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(std::ptr::null(), &mut dearmored);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_dearmor_null_output() {
let data = b"test";
let mut armored: *mut c_char = std::ptr::null_mut();
age_armor(data.as_ptr(), data.len(), &mut armored);
let result = age_dearmor(armored, std::ptr::null_mut());
assert_eq!(result, AgeResult::InvalidInput);
age_free_string(armored);
}
#[test]
fn test_dearmor_invalid_armor() {
let invalid_armor = CString::new("This is not valid armor").unwrap();
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(invalid_armor.as_ptr(), &mut dearmored);
// Should still succeed but return the data as-is or fail gracefully
// The ArmoredReader is forgiving and may just return the raw data
// Let's check that it doesn't crash at least
if result == AgeResult::Success {
age_free_buffer(&mut dearmored);
}
}
#[test]
fn test_encrypt_armor_and_dearmor() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Test encrypt -> armor -> dearmor -> decrypt";
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_encrypt_armor(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut armored,
);
assert_eq!(result, AgeResult::Success);
// Dearmor
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success);
// Decrypt
let mut decrypted = AgeBuffer::null();
let result = crate::decrypt::age_decrypt(
dearmored.data,
dearmored.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_string(armored);
age_free_buffer(&mut dearmored);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_armor_binary_data() {
// Test with binary data including null bytes
let binary_data: Vec<u8> = (0u16..256).map(|i| i as u8).collect();
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_armor(binary_data.as_ptr(), binary_data.len(), &mut armored);
assert_eq!(result, AgeResult::Success);
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success);
let dearmored_slice = unsafe { std::slice::from_raw_parts(dearmored.data, dearmored.len) };
assert_eq!(dearmored_slice, binary_data.as_slice());
age_free_string(armored);
age_free_buffer(&mut dearmored);
}

View File

@@ -0,0 +1,299 @@
//! In-memory decryption functions.
use crate::helpers::cstr_to_str;
use crate::types::{AgeBuffer, AgeResult};
use age::ssh;
use std::io::{BufReader, Read};
use std::os::raw::c_char;
use std::str::FromStr;
/// Decrypt data in memory using a single x25519 identity.
/// This is a simple API for common use cases.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `identity` - The private key string (AGE-SECRET-KEY-1...)
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt(
ciphertext: *const u8,
ciphertext_len: usize,
identity: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let identity_str = match unsafe { cstr_to_str(identity) } {
Ok(s) => s,
Err(e) => return e,
};
let identity = match age::x25519::Identity::from_str(identity_str) {
Ok(i) => i,
Err(_) => return AgeResult::InvalidIdentity,
};
let decrypted = match age::decrypt(&identity, ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt data in memory using multiple identities.
/// The library will try each identity until one succeeds.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `identities` - Array of identity C strings
/// * `identity_count` - Number of identities
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt_multi(
ciphertext: *const u8,
ciphertext_len: usize,
identities: *const *const c_char,
identity_count: usize,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || identities.is_null() || output.is_null() || identity_count == 0 {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let identity_ptrs = unsafe { std::slice::from_raw_parts(identities, identity_count) };
let mut parsed_identities: Vec<Box<dyn age::Identity>> = Vec::new();
for &ptr in identity_ptrs {
let identity_str = match unsafe { cstr_to_str(ptr) } {
Ok(s) => s.trim(),
Err(e) => return e,
};
// Try x25519 first
if let Ok(i) = age::x25519::Identity::from_str(identity_str) {
parsed_identities.push(Box::new(i));
continue;
}
// Skip comments and empty lines
if identity_str.is_empty() || identity_str.starts_with('#') {
continue;
}
return AgeResult::InvalidIdentity;
}
if parsed_identities.is_empty() {
return AgeResult::NoIdentities;
}
let decryptor = match age::Decryptor::new(ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(parsed_identities.iter().map(|i| i.as_ref())) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt data using an SSH private key.
/// Supports both Ed25519 and RSA SSH keys.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `ssh_key` - The SSH private key in PEM or OpenSSH format
/// * `passphrase` - Optional passphrase for encrypted SSH keys (can be null)
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt_ssh(
ciphertext: *const u8,
ciphertext_len: usize,
ssh_key: *const c_char,
passphrase: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let ssh_key_str = match unsafe { cstr_to_str(ssh_key) } {
Ok(s) => s,
Err(e) => return e,
};
// Parse SSH identity from buffer
let buf_reader = BufReader::new(ssh_key_str.as_bytes());
let identity = match ssh::Identity::from_buffer(buf_reader, None) {
Ok(id) => id,
Err(_) => return AgeResult::SshKeyError,
};
// Handle encrypted SSH keys - keep as ssh::Identity since it implements age::Identity
let identity: ssh::Identity = match identity {
ssh::Identity::Unencrypted(_) => identity,
ssh::Identity::Encrypted(enc) => {
let passphrase_str = if passphrase.is_null() {
return AgeResult::PassphraseRequired;
} else {
match unsafe { cstr_to_str(passphrase) } {
Ok(s) if !s.is_empty() => s,
_ => return AgeResult::PassphraseRequired,
}
};
match enc.decrypt(age::secrecy::SecretString::from(passphrase_str.to_string())) {
Ok(id) => ssh::Identity::Unencrypted(id),
Err(_) => return AgeResult::InvalidPassphrase,
}
}
ssh::Identity::Unsupported(_) => return AgeResult::UnsupportedKey,
};
let decryptor = match age::Decryptor::new(ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt data using an SSH private key file.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `ssh_key_path` - Path to the SSH private key file
/// * `passphrase` - Optional passphrase for encrypted SSH keys (can be null)
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt_ssh_file(
ciphertext: *const u8,
ciphertext_len: usize,
ssh_key_path: *const c_char,
passphrase: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let path_str = match unsafe { cstr_to_str(ssh_key_path) } {
Ok(s) => s,
Err(e) => return e,
};
// The filename is passed as a hint for error messages
let filename = Some(path_str.to_string());
// Read and parse SSH key file
let ssh_key_data = match std::fs::read(path_str) {
Ok(data) => data,
Err(_) => return AgeResult::IoError,
};
let buf_reader = BufReader::new(ssh_key_data.as_slice());
let identity = match ssh::Identity::from_buffer(buf_reader, filename) {
Ok(id) => id,
Err(_) => return AgeResult::SshKeyError,
};
// Handle encrypted SSH keys - keep as ssh::Identity since it implements age::Identity
let identity: ssh::Identity = match identity {
ssh::Identity::Unencrypted(_) => identity,
ssh::Identity::Encrypted(enc) => {
// Parse passphrase if provided
let passphrase_str = if passphrase.is_null() {
return AgeResult::PassphraseRequired;
} else {
match unsafe { cstr_to_str(passphrase) } {
Ok(s) if !s.is_empty() => s,
_ => return AgeResult::PassphraseRequired,
}
};
match enc.decrypt(age::secrecy::SecretString::from(passphrase_str.to_string())) {
Ok(id) => ssh::Identity::Unencrypted(id),
Err(_) => return AgeResult::InvalidPassphrase,
}
}
ssh::Identity::Unsupported(_) => return AgeResult::UnsupportedKey,
};
let decryptor = match age::Decryptor::new(ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}

View File

@@ -0,0 +1,430 @@
//! Tests for in-memory decryption functions.
use crate::decrypt::*;
use crate::encrypt::*;
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::CString;
use std::os::raw::c_char;
#[test]
fn test_basic_decrypt() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Decryption test message";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_null_ciphertext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
std::ptr::null(),
0,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_null_output() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_invalid_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let invalid_identity = CString::new("not-a-valid-identity").unwrap();
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
invalid_identity.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidIdentity);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_wrong_key() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"Secret message";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair1.public_key, &mut encrypted);
// Try to decrypt with wrong key
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair2.private_key, // Wrong key!
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_decrypt_corrupted_ciphertext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Original message";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Corrupt the ciphertext
if encrypted.len > 50 {
unsafe {
*encrypted.data.add(50) ^= 0xFF;
}
}
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
// Should fail (either DecryptionFailed or other error depending on what was corrupted)
assert_ne!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_with_multiple_identities() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"Multi-identity message";
let recipients: [*const c_char; 1] = [keypair1.public_key as *const c_char];
let mut encrypted = AgeBuffer::null();
age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
recipients.as_ptr(),
1,
false,
&mut encrypted,
);
// Decrypt with multiple identities (one valid, one invalid for this message)
let identities: [*const c_char; 2] = [
keypair2.private_key as *const c_char, // Wrong key first
keypair1.private_key as *const c_char, // Correct key
];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
2,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_decrypt_multi_empty_identities() {
let plaintext = b"test";
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
std::ptr::null(),
0,
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_null_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
std::ptr::null(),
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_null_identity_in_array() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Array with a null pointer inside
let identities: [*const c_char; 2] = [
keypair.private_key as *const c_char,
std::ptr::null(),
];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
2,
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_with_comments_and_empty() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test with comments";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Mix of comments, empty strings, and valid identity
let comment = CString::new("# This is a comment").unwrap();
let empty = CString::new("").unwrap();
let identities: [*const c_char; 3] = [
comment.as_ptr(),
empty.as_ptr(),
keypair.private_key as *const c_char,
];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
3,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_only_comments() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Only comments and empty - no valid identities
let comment1 = CString::new("# Comment 1").unwrap();
let comment2 = CString::new("# Comment 2").unwrap();
let empty = CString::new("").unwrap();
let identities: [*const c_char; 3] = [
comment1.as_ptr(),
comment2.as_ptr(),
empty.as_ptr(),
];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
3,
&mut decrypted,
);
assert_eq!(result, AgeResult::NoIdentities);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_invalid_identity_format() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Invalid identity (not a comment, not empty, not valid key)
let invalid = CString::new("invalid-key-format").unwrap();
let identities: [*const c_char; 1] = [invalid.as_ptr()];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
1,
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidIdentity);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_corrupted_ciphertext() {
let corrupted = b"not valid age encrypted data at all";
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let identities: [*const c_char; 1] = [keypair.private_key as *const c_char];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
corrupted.as_ptr(),
corrupted.len(),
identities.as_ptr(),
1,
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_wrong_key_only() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair1.public_key, &mut encrypted);
// Only provide wrong key
let identities: [*const c_char; 1] = [keypair2.private_key as *const c_char];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
1,
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}

View File

@@ -0,0 +1,210 @@
//! In-memory encryption functions.
use crate::helpers::{cstr_to_str, string_to_cstr};
use crate::types::{AgeBuffer, AgeResult};
use std::io::Write;
use std::os::raw::c_char;
/// Encrypt data in memory using a single x25519 recipient.
/// This is a simple API for common use cases.
///
/// # Arguments
/// * `plaintext` - Pointer to the plaintext data
/// * `plaintext_len` - Length of the plaintext
/// * `recipient` - The recipient public key (age1...)
/// * `output` - Pointer to receive the encrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt(
plaintext: *const u8,
plaintext_len: usize,
recipient: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if plaintext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient = match recipient_str.parse::<age::x25519::Recipient>() {
Ok(r) => r,
Err(_) => return AgeResult::InvalidRecipient,
};
let encrypted = match age::encrypt(&recipient, plaintext) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
unsafe {
*output = AgeBuffer::from_vec(encrypted);
}
AgeResult::Success
}
/// Encrypt data in memory using multiple recipients.
///
/// # Arguments
/// * `plaintext` - Pointer to the plaintext data
/// * `plaintext_len` - Length of the plaintext
/// * `recipients` - Array of recipient public key C strings
/// * `recipient_count` - Number of recipients
/// * `armor` - If true, output will be ASCII-armored
/// * `output` - Pointer to receive the encrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt_multi(
plaintext: *const u8,
plaintext_len: usize,
recipients: *const *const c_char,
recipient_count: usize,
armor: bool,
output: *mut AgeBuffer,
) -> AgeResult {
if plaintext.is_null() || recipients.is_null() || output.is_null() || recipient_count == 0 {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let recipient_ptrs = unsafe { std::slice::from_raw_parts(recipients, recipient_count) };
let mut parsed_recipients: Vec<Box<dyn age::Recipient + Send>> = Vec::new();
for &ptr in recipient_ptrs {
let recipient_str = match unsafe { cstr_to_str(ptr) } {
Ok(s) => s.trim(),
Err(e) => return e,
};
// Try x25519 first
if let Ok(r) = recipient_str.parse::<age::x25519::Recipient>() {
parsed_recipients.push(Box::new(r));
continue;
}
// Try SSH
if let Ok(r) = recipient_str.parse::<age::ssh::Recipient>() {
parsed_recipients.push(Box::new(r));
continue;
}
return AgeResult::InvalidRecipient;
}
if parsed_recipients.is_empty() {
return AgeResult::NoRecipients;
}
let encryptor = match age::Encryptor::with_recipients(
parsed_recipients.iter().map(|r| r.as_ref() as &dyn age::Recipient)
) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
let mut encrypted = Vec::new();
let result = if armor {
let armor_writer = age::armor::ArmoredWriter::wrap_output(&mut encrypted, age::armor::Format::AsciiArmor)
.map_err(|_| AgeResult::ArmorError);
match armor_writer {
Ok(armor) => {
match encryptor.wrap_output(armor) {
Ok(mut writer) => {
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
match writer.finish() {
Ok(armor) => armor.finish().map_err(|_| AgeResult::ArmorError),
Err(_) => return AgeResult::EncryptionFailed,
}
}
Err(_) => return AgeResult::EncryptionFailed,
}
}
Err(e) => return e,
}
} else {
match encryptor.wrap_output(&mut encrypted) {
Ok(mut writer) => {
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
writer.finish().map_err(|_| AgeResult::EncryptionFailed)
}
Err(_) => return AgeResult::EncryptionFailed,
}
};
if result.is_err() {
return AgeResult::EncryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(encrypted);
}
AgeResult::Success
}
/// Encrypt data with ASCII armor for text-safe output.
///
/// # Arguments
/// * `plaintext` - Pointer to the plaintext data
/// * `plaintext_len` - Length of the plaintext
/// * `recipient` - The recipient public key (age1...)
/// * `output` - Pointer to receive the armored string (null-terminated)
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt_armor(
plaintext: *const u8,
plaintext_len: usize,
recipient: *const c_char,
output: *mut *mut c_char,
) -> AgeResult {
if plaintext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient = match recipient_str.parse::<age::x25519::Recipient>() {
Ok(r) => r,
Err(_) => return AgeResult::InvalidRecipient,
};
let encrypted = match age::encrypt_and_armor(&recipient, plaintext) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
let c_output = match string_to_cstr(encrypted) {
Ok(s) => s,
Err(e) => return e,
};
unsafe {
*output = c_output;
}
AgeResult::Success
}

View File

@@ -0,0 +1,232 @@
//! Tests for in-memory encryption functions.
use crate::encrypt::*;
use crate::decrypt::*;
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::CString;
use std::os::raw::c_char;
#[test]
fn test_basic_encrypt() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Hello, encryption!";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
assert!(!encrypted.data.is_null());
assert!(encrypted.len > plaintext.len()); // Encrypted should be larger
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_null_plaintext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
std::ptr::null(),
0,
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_null_output() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_invalid_recipient() {
let invalid_recipient = CString::new("not-a-valid-recipient").unwrap();
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
invalid_recipient.as_ptr(),
&mut encrypted,
);
assert_eq!(result, AgeResult::InvalidRecipient);
}
#[test]
fn test_encrypt_multi_two_recipients() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"Message for both recipients";
let recipients: [*const c_char; 2] = [
keypair1.public_key as *const c_char,
keypair2.public_key as *const c_char,
];
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
recipients.as_ptr(),
2,
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Both recipients should be able to decrypt
let mut decrypted1 = AgeBuffer::null();
let result = age_decrypt(encrypted.data, encrypted.len, keypair1.private_key, &mut decrypted1);
assert_eq!(result, AgeResult::Success);
let mut decrypted2 = AgeBuffer::null();
let result = age_decrypt(encrypted.data, encrypted.len, keypair2.private_key, &mut decrypted2);
assert_eq!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted1);
age_free_buffer(&mut decrypted2);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_encrypt_multi_with_armor() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Armored multi-recipient message";
let recipients: [*const c_char; 1] = [keypair.public_key as *const c_char];
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
recipients.as_ptr(),
1,
true, // armor
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Check it's armored
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
let encrypted_str = std::str::from_utf8(encrypted_slice).unwrap();
assert!(encrypted_str.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_multi_empty_recipients() {
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
std::ptr::null(),
0,
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_encrypt_armor() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Armored message";
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_encrypt_armor(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut armored,
);
assert_eq!(result, AgeResult::Success);
assert!(!armored.is_null());
let armored_str = unsafe { std::ffi::CStr::from_ptr(armored).to_str().unwrap() };
assert!(armored_str.starts_with("-----BEGIN AGE ENCRYPTED FILE-----"));
assert!(armored_str.contains("-----END AGE ENCRYPTED FILE-----"));
age_free_string(armored);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_various_sizes() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let sizes = [0, 1, 16, 256, 1024, 4096, 65536];
for size in sizes {
let plaintext: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success, "Failed for size {}", size);
// Verify we can decrypt it back
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(encrypted.data, encrypted.len, keypair.private_key, &mut decrypted);
assert_eq!(result, AgeResult::Success, "Decrypt failed for size {}", size);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext.as_slice(), "Mismatch for size {}", size);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
age_free_keypair(&mut keypair);
}

View File

@@ -0,0 +1,351 @@
//! File-based encryption and decryption operations.
use crate::helpers::{cstr_to_str, cstr_to_string};
use crate::types::{AgeBuffer, AgeResult};
use age::secrecy::SecretString;
use std::fs::File;
use std::io::{Read, Write};
use std::os::raw::c_char;
use std::str::FromStr;
/// Encrypt data to a file using a recipient.
///
/// # Arguments
/// * `plaintext` - The data to encrypt
/// * `plaintext_len` - Length of the plaintext
/// * `output_path` - Path to write the encrypted .age file
/// * `recipient` - The recipient public key (age1...) or path to recipients file
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt_to_file(
plaintext: *const c_char,
plaintext_len: usize,
output_path: *const c_char,
recipient: *const c_char,
) -> AgeResult {
if plaintext.is_null() || output_path.is_null() || recipient.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext as *const u8, plaintext_len) };
let output_path = match unsafe { cstr_to_str(output_path) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(e) => return e,
};
// Parse recipients - could be a file path or a direct recipient key
// Supports: x25519 (age1...), plugin (age1<plugin>1...), and ssh (ssh-...)
let mut recipients: Vec<Box<dyn age::Recipient + Send>> = Vec::new();
let mut plugin_recipients: Vec<age::plugin::Recipient> = Vec::new();
let recipient_lines: Vec<&str> = if recipient_str.starts_with("age1") || recipient_str.starts_with("ssh-") {
vec![recipient_str]
} else {
// Assume it's a file path containing recipients
match std::fs::read_to_string(recipient_str) {
Ok(contents) => {
// We need to own the string for the lines
let contents_leaked: &'static str = Box::leak(contents.into_boxed_str());
contents_leaked
.lines()
.filter(|line| !line.starts_with('#') && !line.is_empty())
.map(|line| line.trim())
.collect()
}
Err(_) => return AgeResult::IoError,
}
};
for line in recipient_lines {
// Try x25519 first
if let Ok(r) = line.parse::<age::x25519::Recipient>() {
recipients.push(Box::new(r));
continue;
}
// Then try plugin recipient - collect these separately
if let Ok(r) = line.parse::<age::plugin::Recipient>() {
plugin_recipients.push(r);
continue;
}
// Finally try SSH
if let Ok(r) = line.parse::<age::ssh::Recipient>() {
recipients.push(Box::new(r));
continue;
}
// Skip unrecognized lines
}
// Create plugin recipients wrapper if we have any plugin recipients
// Group them by plugin name
if !plugin_recipients.is_empty() {
use std::collections::HashMap;
let mut by_plugin: HashMap<String, Vec<age::plugin::Recipient>> = HashMap::new();
for r in plugin_recipients {
by_plugin.entry(r.plugin().to_string()).or_default().push(r);
}
for (plugin_name, plugin_recs) in by_plugin {
match age::plugin::RecipientPluginV1::new(
&plugin_name,
&plugin_recs,
&[],
age::NoCallbacks,
) {
Ok(plugin) => recipients.push(Box::new(plugin)),
Err(_) => return AgeResult::InvalidRecipient,
}
}
}
if recipients.is_empty() {
return AgeResult::InvalidRecipient;
}
let output_file = match File::create(output_path) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
let encryptor = match age::Encryptor::with_recipients(recipients.iter().map(|r| r.as_ref() as &dyn age::Recipient)) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
let mut writer = match encryptor.wrap_output(output_file) {
Ok(w) => w,
Err(_) => return AgeResult::EncryptionFailed,
};
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
if writer.finish().is_err() {
return AgeResult::EncryptionFailed;
}
AgeResult::Success
}
/// Encrypt data to a file with ASCII armor.
#[no_mangle]
pub extern "C" fn age_encrypt_to_file_armor(
plaintext: *const u8,
plaintext_len: usize,
output_path: *const c_char,
recipient: *const c_char,
) -> AgeResult {
if plaintext.is_null() || output_path.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let output_path = match unsafe { cstr_to_str(output_path) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient = match recipient_str.parse::<age::x25519::Recipient>() {
Ok(r) => r,
Err(_) => return AgeResult::InvalidRecipient,
};
let encrypted = match age::encrypt_and_armor(&recipient, plaintext) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
if std::fs::write(output_path, encrypted).is_err() {
return AgeResult::IoError;
}
AgeResult::Success
}
/// Decrypt data from a file using an identity file.
///
/// This function supports all identity types including:
/// - Standard x25519 identities (AGE-SECRET-KEY-...)
/// - SSH identities
/// - Plugin identities (AGE-PLUGIN-...)
#[no_mangle]
pub extern "C" fn age_decrypt_file(
encrypted_path: *const c_char,
identity_path: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if output.is_null() {
return AgeResult::InvalidInput;
}
let encrypted_path = match unsafe { cstr_to_str(encrypted_path) } {
Ok(s) => s,
Err(e) => return e,
};
let identity_path = match unsafe { cstr_to_str(identity_path) } {
Ok(s) => s,
Err(e) => return e,
};
// Use IdentityFile to parse the identity file - this supports all identity types
// including plugin identities (AGE-PLUGIN-...)
let identity_file = match age::IdentityFile::from_file(identity_path.to_string()) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
// Get all identities from the file
let identities = match identity_file.into_identities() {
Ok(ids) => ids,
Err(_) => return AgeResult::InvalidIdentity,
};
if identities.is_empty() {
return AgeResult::InvalidIdentity;
}
let encrypted_file = match File::open(encrypted_path) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
let decryptor = match age::Decryptor::new(encrypted_file) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(identities.iter().map(|i| i.as_ref() as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt data from a file using a single identity string.
#[no_mangle]
pub extern "C" fn age_decrypt_file_with_identity(
encrypted_path: *const c_char,
identity: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if output.is_null() {
return AgeResult::InvalidInput;
}
let encrypted_path = match unsafe { cstr_to_str(encrypted_path) } {
Ok(s) => s,
Err(e) => return e,
};
let identity_str = match unsafe { cstr_to_str(identity) } {
Ok(s) => s,
Err(e) => return e,
};
let identity = match age::x25519::Identity::from_str(identity_str) {
Ok(i) => i,
Err(_) => return AgeResult::InvalidIdentity,
};
let encrypted_file = match File::open(encrypted_path) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
let decryptor = match age::Decryptor::new(encrypted_file) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt a file using a passphrase.
#[no_mangle]
pub extern "C" fn age_decrypt_file_passphrase(
encrypted_path: *const c_char,
passphrase: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if output.is_null() {
return AgeResult::InvalidInput;
}
let encrypted_path = match unsafe { cstr_to_str(encrypted_path) } {
Ok(s) => s,
Err(e) => return e,
};
let passphrase_str = match unsafe { cstr_to_string(passphrase) } {
Ok(s) => s,
Err(e) => return e,
};
let secret = SecretString::from(passphrase_str);
let identity = age::scrypt::Identity::new(secret);
let encrypted_file = match File::open(encrypted_path) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
let decryptor = match age::Decryptor::new(encrypted_file) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}

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);
}

View File

@@ -0,0 +1,27 @@
//! Internal helper functions for FFI conversions.
use crate::types::AgeResult;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
/// Safely convert a C string pointer to a Rust &str
pub unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> Result<&'a str, AgeResult> {
if ptr.is_null() {
return Err(AgeResult::InvalidInput);
}
CStr::from_ptr(ptr)
.to_str()
.map_err(|_| AgeResult::InvalidUtf8)
}
/// Safely convert a C string pointer to a Rust String
pub unsafe fn cstr_to_string(ptr: *const c_char) -> Result<String, AgeResult> {
cstr_to_str(ptr).map(|s| s.to_owned())
}
/// Convert a Rust String to a C string pointer (caller must free)
pub fn string_to_cstr(s: String) -> Result<*mut c_char, AgeResult> {
CString::new(s)
.map(|cs| cs.into_raw())
.map_err(|_| AgeResult::InvalidInput)
}

View File

@@ -0,0 +1,92 @@
//! Key generation and derivation functions.
use crate::helpers::{cstr_to_str, string_to_cstr};
use crate::types::{AgeKeypair, AgeResult};
use age::secrecy::ExposeSecret;
use std::ffi::CString;
use std::os::raw::c_char;
use std::str::FromStr;
/// Generate a new age x25519 keypair.
///
/// # Arguments
/// * `keypair` - Pointer to receive the generated keypair
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_generate_x25519(keypair: *mut AgeKeypair) -> AgeResult {
if keypair.is_null() {
return AgeResult::InvalidInput;
}
let identity = age::x25519::Identity::generate();
let public_key = identity.to_public().to_string();
let private_key = identity.to_string().expose_secret().to_string();
let c_public = match string_to_cstr(public_key) {
Ok(s) => s,
Err(e) => return e,
};
let c_private = match string_to_cstr(private_key) {
Ok(s) => s,
Err(e) => {
unsafe { drop(CString::from_raw(c_public)); }
return e;
}
};
unsafe {
(*keypair).public_key = c_public;
(*keypair).private_key = c_private;
}
AgeResult::Success
}
/// Alias for age_generate_x25519 for backwards compatibility.
#[no_mangle]
pub extern "C" fn age_generate_keypair(keypair: *mut AgeKeypair) -> AgeResult {
age_generate_x25519(keypair)
}
/// Derive the public key from a private x25519 identity.
///
/// # Arguments
/// * `private_key` - The private key string (AGE-SECRET-KEY-1...)
/// * `public_key` - Pointer to receive the public key string
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_x25519_to_public(
private_key: *const c_char,
public_key: *mut *mut c_char,
) -> AgeResult {
if public_key.is_null() {
return AgeResult::InvalidInput;
}
let private_str = match unsafe { cstr_to_str(private_key) } {
Ok(s) => s,
Err(e) => return e,
};
let identity = match age::x25519::Identity::from_str(private_str) {
Ok(i) => i,
Err(_) => return AgeResult::InvalidIdentity,
};
let public_str = identity.to_public().to_string();
let c_public = match string_to_cstr(public_str) {
Ok(s) => s,
Err(e) => return e,
};
unsafe {
*public_key = c_public;
}
AgeResult::Success
}

View File

@@ -0,0 +1,122 @@
//! Tests for key generation and derivation functions.
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::CStr;
#[test]
fn test_generate_x25519_keypair() {
let mut keypair = AgeKeypair::null();
let result = age_generate_x25519(&mut keypair);
assert_eq!(result, AgeResult::Success);
assert!(!keypair.public_key.is_null());
assert!(!keypair.private_key.is_null());
unsafe {
let public = CStr::from_ptr(keypair.public_key).to_str().unwrap();
let private = CStr::from_ptr(keypair.private_key).to_str().unwrap();
assert!(public.starts_with("age1"), "Public key should start with 'age1'");
assert!(private.starts_with("AGE-SECRET-KEY-1"), "Private key should start with 'AGE-SECRET-KEY-1'");
// Check key lengths are reasonable
assert!(public.len() > 50, "Public key should be at least 50 chars");
assert!(private.len() > 50, "Private key should be at least 50 chars");
}
age_free_keypair(&mut keypair);
}
#[test]
fn test_generate_keypair_alias() {
let mut keypair = AgeKeypair::null();
let result = age_generate_keypair(&mut keypair);
assert_eq!(result, AgeResult::Success);
assert!(!keypair.public_key.is_null());
assert!(!keypair.private_key.is_null());
age_free_keypair(&mut keypair);
}
#[test]
fn test_generate_x25519_null_pointer() {
let result = age_generate_x25519(std::ptr::null_mut());
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_derive_public_key() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut derived_public: *mut std::os::raw::c_char = std::ptr::null_mut();
let result = age_x25519_to_public(keypair.private_key, &mut derived_public);
assert_eq!(result, AgeResult::Success);
assert!(!derived_public.is_null());
// The derived public key should match the original
let original = unsafe { CStr::from_ptr(keypair.public_key).to_str().unwrap() };
let derived = unsafe { CStr::from_ptr(derived_public).to_str().unwrap() };
assert_eq!(original, derived);
age_free_string(derived_public);
age_free_keypair(&mut keypair);
}
#[test]
fn test_derive_public_key_invalid_input() {
use std::ffi::CString;
let mut derived_public: *mut std::os::raw::c_char = std::ptr::null_mut();
// Null output pointer
let result = age_x25519_to_public(std::ptr::null(), std::ptr::null_mut());
assert_eq!(result, AgeResult::InvalidInput);
// Invalid private key
let invalid_key = CString::new("not-a-valid-key").unwrap();
let result = age_x25519_to_public(invalid_key.as_ptr(), &mut derived_public);
assert_eq!(result, AgeResult::InvalidIdentity);
}
#[test]
fn test_derive_public_key_null_private_key() {
let mut derived_public: *mut std::os::raw::c_char = std::ptr::null_mut();
// Null private key but valid output pointer
let result = age_x25519_to_public(std::ptr::null(), &mut derived_public);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_multiple_keypair_generation() {
// Generate multiple keypairs and ensure they're all unique
let mut keypairs: Vec<AgeKeypair> = Vec::new();
for _ in 0..10 {
let mut keypair = AgeKeypair::null();
let result = age_generate_x25519(&mut keypair);
assert_eq!(result, AgeResult::Success);
keypairs.push(keypair);
}
// Check all public keys are unique
let public_keys: Vec<String> = keypairs.iter().map(|kp| {
unsafe { CStr::from_ptr(kp.public_key).to_str().unwrap().to_string() }
}).collect();
for i in 0..public_keys.len() {
for j in (i+1)..public_keys.len() {
assert_ne!(public_keys[i], public_keys[j], "Keypairs should be unique");
}
}
// Cleanup
for keypair in &mut keypairs {
age_free_keypair(keypair);
}
}

View File

@@ -0,0 +1,88 @@
//! Complete FFI wrapper for the age encryption library.
//!
//! Provides C-compatible functions for all age encryption operations:
//! - Key generation (x25519, SSH)
//! - Encryption/decryption (memory and file-based)
//! - Passphrase-based encryption (scrypt)
//! - ASCII armor support
//! - Multiple recipients support
extern crate libc;
// Internal modules
mod helpers;
// Public modules
pub mod types;
pub mod keys;
pub mod encrypt;
pub mod decrypt;
pub mod passphrase;
pub mod file;
pub mod armor;
pub mod validation;
pub mod memory;
// Re-export all public types
pub use types::{AgeBuffer, AgeEncryptConfig, AgeKeypair, AgeResult};
// Re-export all public functions
pub use keys::{age_generate_keypair, age_generate_x25519, age_x25519_to_public};
pub use encrypt::{age_encrypt, age_encrypt_armor, age_encrypt_multi};
pub use decrypt::{age_decrypt, age_decrypt_multi, age_decrypt_ssh, age_decrypt_ssh_file};
pub use passphrase::{age_decrypt_passphrase, age_encrypt_passphrase};
pub use file::{
age_decrypt_file, age_decrypt_file_passphrase, age_decrypt_file_with_identity,
age_encrypt_to_file, age_encrypt_to_file_armor,
};
pub use armor::{age_armor, age_dearmor};
pub use validation::{
age_is_valid_ssh_recipient, age_is_valid_x25519_identity, age_is_valid_x25519_recipient,
age_recipient_type,
};
pub use memory::{age_free_buffer, age_free_keypair, age_free_string};
use std::os::raw::c_char;
/// Get the version of the age-ffi library.
/// Returns a static string, do not free.
#[no_mangle]
pub extern "C" fn age_version() -> *const c_char {
static VERSION: &[u8] = b"0.1.0\0";
VERSION.as_ptr() as *const c_char
}
/// Get the version of the underlying age library.
/// Returns a static string, do not free.
#[no_mangle]
pub extern "C" fn age_lib_version() -> *const c_char {
static VERSION: &[u8] = b"0.11.0\0";
VERSION.as_ptr() as *const c_char
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod keys_tests;
#[cfg(test)]
mod encrypt_tests;
#[cfg(test)]
mod decrypt_tests;
#[cfg(test)]
mod passphrase_tests;
#[cfg(test)]
mod armor_tests;
#[cfg(test)]
mod validation_tests;
#[cfg(test)]
mod memory_tests;
#[cfg(test)]
mod file_tests;

View File

@@ -0,0 +1,60 @@
//! Memory management functions.
use crate::types::{AgeBuffer, AgeKeypair};
use std::ffi::CString;
use std::os::raw::c_char;
/// Free a buffer allocated by this library.
///
/// # Safety
/// The buffer must have been allocated by one of the age_* functions.
#[no_mangle]
pub extern "C" fn age_free_buffer(buffer: *mut AgeBuffer) {
if buffer.is_null() {
return;
}
unsafe {
let buf = &*buffer;
if !buf.data.is_null() && buf.capacity > 0 {
// Reconstruct the boxed slice and drop it
let slice = std::slice::from_raw_parts_mut(buf.data, buf.capacity);
drop(Box::from_raw(slice as *mut [u8]));
}
(*buffer) = AgeBuffer::null();
}
}
/// Free a string allocated by this library.
///
/// # Safety
/// The pointer must have been allocated by one of the age_* functions.
#[no_mangle]
pub extern "C" fn age_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}
/// Free a keypair allocated by age_generate_keypair.
///
/// # Safety
/// The keypair must have been allocated by age_generate_keypair.
#[no_mangle]
pub extern "C" fn age_free_keypair(keypair: *mut AgeKeypair) {
if keypair.is_null() {
return;
}
unsafe {
if !(*keypair).public_key.is_null() {
drop(CString::from_raw((*keypair).public_key));
}
if !(*keypair).private_key.is_null() {
drop(CString::from_raw((*keypair).private_key));
}
(*keypair) = AgeKeypair::null();
}
}

View File

@@ -0,0 +1,208 @@
//! Tests for memory management functions.
use crate::encrypt::*;
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::os::raw::c_char;
#[test]
fn test_free_buffer_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Test message for buffer freeing";
let mut encrypted = AgeBuffer::null();
age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
// Should not crash
age_free_buffer(&mut encrypted);
// Buffer should be nulled out
assert!(encrypted.data.is_null());
assert_eq!(encrypted.len, 0);
assert_eq!(encrypted.capacity, 0);
age_free_keypair(&mut keypair);
}
#[test]
fn test_free_buffer_null() {
// Should not crash on null pointer
age_free_buffer(std::ptr::null_mut());
}
#[test]
fn test_free_buffer_already_null() {
let mut buffer = AgeBuffer::null();
// Should not crash on already-null buffer
age_free_buffer(&mut buffer);
}
#[test]
fn test_free_string_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Test";
let mut armored: *mut c_char = std::ptr::null_mut();
crate::encrypt::age_encrypt_armor(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut armored,
);
// Should not crash
age_free_string(armored);
age_free_keypair(&mut keypair);
}
#[test]
fn test_free_string_null() {
// Should not crash on null pointer
age_free_string(std::ptr::null_mut());
}
#[test]
fn test_free_keypair_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Should not crash
age_free_keypair(&mut keypair);
// Keypair should be nulled out
assert!(keypair.public_key.is_null());
assert!(keypair.private_key.is_null());
}
#[test]
fn test_free_keypair_null() {
// Should not crash on null pointer
age_free_keypair(std::ptr::null_mut());
}
#[test]
fn test_free_keypair_already_null() {
let mut keypair = AgeKeypair::null();
// Should not crash on already-null keypair
age_free_keypair(&mut keypair);
}
#[test]
fn test_double_free_buffer() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Test";
let mut encrypted = AgeBuffer::null();
age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
age_free_buffer(&mut encrypted);
// Double free should be safe because we null out the pointer
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_double_free_keypair() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
age_free_keypair(&mut keypair);
// Double free should be safe because we null out the pointers
age_free_keypair(&mut keypair);
}
#[test]
fn test_multiple_allocations_and_frees() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Allocate and free multiple times
for _ in 0..100 {
let plaintext = b"Test message for repeated allocation";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
}
age_free_keypair(&mut keypair);
}
#[test]
fn test_large_allocation_and_free() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Allocate a large buffer (1MB)
let plaintext: Vec<u8> = vec![0x42; 1024 * 1024];
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
assert!(encrypted.len > 1024 * 1024);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_age_buffer_from_vec() {
// Test the internal from_vec function
let vec = vec![1u8, 2, 3, 4, 5];
let buffer = AgeBuffer::from_vec(vec);
assert!(!buffer.data.is_null());
assert_eq!(buffer.len, 5);
assert_eq!(buffer.capacity, 5);
// Verify data
let slice = unsafe { std::slice::from_raw_parts(buffer.data, buffer.len) };
assert_eq!(slice, &[1, 2, 3, 4, 5]);
// Clean up
let mut buffer = buffer;
age_free_buffer(&mut buffer);
}
#[test]
fn test_age_buffer_null() {
let buffer = AgeBuffer::null();
assert!(buffer.data.is_null());
assert_eq!(buffer.len, 0);
assert_eq!(buffer.capacity, 0);
}
#[test]
fn test_age_keypair_null() {
let keypair = AgeKeypair::null();
assert!(keypair.public_key.is_null());
assert!(keypair.private_key.is_null());
}

View File

@@ -0,0 +1,139 @@
//! Passphrase-based encryption and decryption (scrypt).
use crate::helpers::cstr_to_string;
use crate::types::{AgeBuffer, AgeResult};
use age::secrecy::SecretString;
use std::io::{Read, Write};
use std::os::raw::c_char;
/// Encrypt data using a passphrase.
///
/// # Arguments
/// * `plaintext` - Pointer to the plaintext data
/// * `plaintext_len` - Length of the plaintext
/// * `passphrase` - The passphrase string
/// * `armor` - If true, output will be ASCII-armored
/// * `output` - Pointer to receive the encrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt_passphrase(
plaintext: *const u8,
plaintext_len: usize,
passphrase: *const c_char,
armor: bool,
output: *mut AgeBuffer,
) -> AgeResult {
if plaintext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let passphrase_str = match unsafe { cstr_to_string(passphrase) } {
Ok(s) => s,
Err(e) => return e,
};
let secret = SecretString::from(passphrase_str);
let encryptor = age::Encryptor::with_user_passphrase(secret);
let mut encrypted = Vec::new();
let result = if armor {
let armor_writer = age::armor::ArmoredWriter::wrap_output(&mut encrypted, age::armor::Format::AsciiArmor)
.map_err(|_| AgeResult::ArmorError);
match armor_writer {
Ok(armor) => {
match encryptor.wrap_output(armor) {
Ok(mut writer) => {
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
match writer.finish() {
Ok(armor) => armor.finish().map_err(|_| AgeResult::ArmorError),
Err(_) => return AgeResult::EncryptionFailed,
}
}
Err(_) => return AgeResult::EncryptionFailed,
}
}
Err(e) => return e,
}
} else {
match encryptor.wrap_output(&mut encrypted) {
Ok(mut writer) => {
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
writer.finish().map_err(|_| AgeResult::EncryptionFailed)
}
Err(_) => return AgeResult::EncryptionFailed,
}
};
if result.is_err() {
return AgeResult::EncryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(encrypted);
}
AgeResult::Success
}
/// Decrypt data using a passphrase.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `passphrase` - The passphrase string
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt_passphrase(
ciphertext: *const u8,
ciphertext_len: usize,
passphrase: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let passphrase_str = match unsafe { cstr_to_string(passphrase) } {
Ok(s) => s,
Err(e) => return e,
};
let secret = SecretString::from(passphrase_str);
let identity = age::scrypt::Identity::new(secret);
let decryptor = match age::Decryptor::new(ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}

View File

@@ -0,0 +1,329 @@
//! Tests for passphrase-based encryption and decryption.
use crate::passphrase::*;
use crate::armor::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::CString;
#[test]
fn test_passphrase_encrypt_decrypt() {
let plaintext = b"Secret passphrase message";
let passphrase = CString::new("my-secure-passphrase").unwrap();
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);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_wrong_passphrase() {
let plaintext = b"Secret message";
let correct_passphrase = CString::new("correct-passphrase").unwrap();
let wrong_passphrase = CString::new("wrong-passphrase").unwrap();
let mut encrypted = AgeBuffer::null();
age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
correct_passphrase.as_ptr(),
false,
&mut encrypted,
);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
wrong_passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
age_free_buffer(&mut encrypted);
}
#[test]
fn test_passphrase_empty_passphrase() {
let plaintext = b"Message with empty passphrase";
let empty_passphrase = CString::new("").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
empty_passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
empty_passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_special_characters() {
let plaintext = b"Message with special passphrase";
let special_passphrase = CString::new("p@$$w0rd!#$%^&*()_+-=[]{}|;':\",./<>?").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
special_passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
special_passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_with_armor() {
let plaintext = b"Armored passphrase message";
let passphrase = CString::new("armor-test-pass").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
true, // armor = true
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Verify it's armored
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
let encrypted_str = std::str::from_utf8(encrypted_slice).unwrap();
assert!(encrypted_str.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
// Dearmor first
let armored_cstr = CString::new(encrypted_str).unwrap();
let mut dearmored = AgeBuffer::null();
age_dearmor(armored_cstr.as_ptr(), &mut dearmored);
// Then decrypt
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
dearmored.data,
dearmored.len,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut dearmored);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_null_input() {
let passphrase = CString::new("test").unwrap();
let mut output = AgeBuffer::null();
// Null plaintext
let result = age_encrypt_passphrase(
std::ptr::null(),
0,
passphrase.as_ptr(),
false,
&mut output,
);
assert_eq!(result, AgeResult::InvalidInput);
// Null output
let plaintext = b"test";
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_passphrase_long_passphrase() {
let plaintext = b"Message with very long passphrase";
// 1000 character passphrase
let long_passphrase = CString::new("a".repeat(1000)).unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
long_passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
long_passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_encrypt_null_passphrase() {
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
std::ptr::null(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_passphrase_decrypt_null_passphrase() {
let passphrase = CString::new("test").unwrap();
let plaintext = b"test";
// First encrypt with valid 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);
// Try to decrypt with null passphrase
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
std::ptr::null(),
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
}
#[test]
fn test_passphrase_decrypt_null_output() {
let passphrase = CString::new("test").unwrap();
let plaintext = b"test";
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);
// Try to decrypt with null output
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
passphrase.as_ptr(),
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
}
#[test]
fn test_passphrase_decrypt_null_ciphertext() {
let passphrase = CString::new("test").unwrap();
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
std::ptr::null(),
0,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_passphrase_decrypt_corrupted_data() {
let passphrase = CString::new("test").unwrap();
let corrupted = b"not valid encrypted data";
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
corrupted.as_ptr(),
corrupted.len(),
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
}

View File

@@ -0,0 +1,337 @@
//! Tests for the age-ffi library.
use crate::*;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[test]
fn test_keygen() {
let mut keypair = AgeKeypair::null();
let result = age_generate_x25519(&mut keypair);
assert_eq!(result, AgeResult::Success);
assert!(!keypair.public_key.is_null());
assert!(!keypair.private_key.is_null());
unsafe {
let public = CStr::from_ptr(keypair.public_key).to_str().unwrap();
let private = CStr::from_ptr(keypair.private_key).to_str().unwrap();
assert!(public.starts_with("age1"));
assert!(private.starts_with("AGE-SECRET-KEY-1"));
}
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_decrypt() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Hello, world!";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
assert!(!encrypted.data.is_null());
assert!(encrypted.len > 0);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_passphrase_encrypt_decrypt() {
let plaintext = b"Secret message";
let passphrase = CString::new("my-secret-passphrase").unwrap();
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);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_validation() {
let invalid = CString::new("not-a-key").unwrap();
assert!(!age_is_valid_x25519_recipient(invalid.as_ptr()));
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(age_is_valid_x25519_recipient(keypair.public_key));
assert!(age_is_valid_x25519_identity(keypair.private_key));
assert_eq!(age_recipient_type(keypair.public_key), 1);
age_free_keypair(&mut keypair);
}
#[test]
fn test_armor_encrypt_decrypt() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Armored message";
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_encrypt_armor(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut armored,
);
assert_eq!(result, AgeResult::Success);
assert!(!armored.is_null());
let armored_str = unsafe { CStr::from_ptr(armored).to_str().unwrap() };
assert!(armored_str.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
dearmored.data,
dearmored.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_string(armored);
age_free_buffer(&mut dearmored);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_derive_public_key() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut derived_public: *mut c_char = std::ptr::null_mut();
let result = age_x25519_to_public(keypair.private_key, &mut derived_public);
assert_eq!(result, AgeResult::Success);
let original = unsafe { CStr::from_ptr(keypair.public_key).to_str().unwrap() };
let derived = unsafe { CStr::from_ptr(derived_public).to_str().unwrap() };
assert_eq!(original, derived);
age_free_string(derived_public);
age_free_keypair(&mut keypair);
}
#[test]
fn test_multi_recipient_encrypt() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"Message for multiple recipients";
let recipients: [*const c_char; 2] = [
keypair1.public_key as *const c_char,
keypair2.public_key as *const c_char,
];
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
recipients.as_ptr(),
recipients.len(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Decrypt with first key
let mut decrypted1 = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair1.private_key,
&mut decrypted1,
);
assert_eq!(result, AgeResult::Success);
let slice1 = unsafe { std::slice::from_raw_parts(decrypted1.data, decrypted1.len) };
assert_eq!(slice1, plaintext);
// Decrypt with second key
let mut decrypted2 = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair2.private_key,
&mut decrypted2,
);
assert_eq!(result, AgeResult::Success);
let slice2 = unsafe { std::slice::from_raw_parts(decrypted2.data, decrypted2.len) };
assert_eq!(slice2, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted1);
age_free_buffer(&mut decrypted2);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_version_functions() {
let version = age_version();
assert!(!version.is_null());
let version_str = unsafe { CStr::from_ptr(version).to_str().unwrap() };
assert!(!version_str.is_empty());
let lib_version = age_lib_version();
assert!(!lib_version.is_null());
let lib_version_str = unsafe { CStr::from_ptr(lib_version).to_str().unwrap() };
assert!(lib_version_str.starts_with("0.11"));
}
#[test]
fn test_passphrase_with_armor() {
let plaintext = b"Armored passphrase message";
let passphrase = CString::new("test-passphrase-123").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
true, // armor = true
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Verify it's armored
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
let encrypted_str = std::str::from_utf8(encrypted_slice).unwrap();
assert!(encrypted_str.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
// Dearmor first, then decrypt
let armored_cstr = CString::new(encrypted_str).unwrap();
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored_cstr.as_ptr(), &mut dearmored);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
dearmored.data,
dearmored.len,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut dearmored);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_empty_plaintext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
assert_eq!(decrypted.len, 0);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_large_plaintext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// 1MB of data
let plaintext: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext.as_slice());
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}

View File

@@ -0,0 +1,92 @@
//! FFI-compatible data types for the age encryption library.
use std::os::raw::c_char;
/// Result codes for FFI functions
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgeResult {
Success = 0,
InvalidInput = 1,
EncryptionFailed = 2,
DecryptionFailed = 3,
KeygenFailed = 4,
IoError = 5,
InvalidRecipient = 6,
InvalidIdentity = 7,
NoRecipients = 8,
NoIdentities = 9,
ArmorError = 10,
PassphraseRequired = 11,
InvalidPassphrase = 12,
SshKeyError = 13,
MemoryAllocationFailed = 14,
InvalidUtf8 = 15,
UnsupportedKey = 16,
}
/// A buffer containing binary data allocated by the library.
/// Caller must free using age_free_buffer.
#[repr(C)]
pub struct AgeBuffer {
pub data: *mut u8,
pub len: usize,
pub capacity: usize,
}
impl AgeBuffer {
pub fn null() -> Self {
AgeBuffer {
data: std::ptr::null_mut(),
len: 0,
capacity: 0,
}
}
pub fn from_vec(v: Vec<u8>) -> Self {
let mut v = v.into_boxed_slice();
let data = v.as_mut_ptr();
let len = v.len();
std::mem::forget(v);
AgeBuffer {
data,
len,
capacity: len,
}
}
}
/// A keypair containing public and private keys as C strings.
/// Caller must free both strings using age_free_string.
#[repr(C)]
pub struct AgeKeypair {
pub public_key: *mut c_char,
pub private_key: *mut c_char,
}
impl AgeKeypair {
pub fn null() -> Self {
AgeKeypair {
public_key: std::ptr::null_mut(),
private_key: std::ptr::null_mut(),
}
}
}
/// Configuration for encryption operations.
#[repr(C)]
pub struct AgeEncryptConfig {
/// If true, output will be ASCII-armored
pub armor: bool,
/// Work factor for scrypt (0 = default, typically 18-22)
pub scrypt_work_factor: u8,
}
impl Default for AgeEncryptConfig {
fn default() -> Self {
AgeEncryptConfig {
armor: false,
scrypt_work_factor: 0,
}
}
}

View File

@@ -0,0 +1,81 @@
//! Recipient and identity validation functions.
use crate::helpers::cstr_to_str;
use std::os::raw::c_char;
use std::str::FromStr;
/// Check if a string is a valid x25519 recipient (public key).
///
/// # Arguments
/// * `recipient` - The recipient string to validate
///
/// # Returns
/// true if valid, false otherwise
#[no_mangle]
pub extern "C" fn age_is_valid_x25519_recipient(recipient: *const c_char) -> bool {
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(_) => return false,
};
recipient_str.parse::<age::x25519::Recipient>().is_ok()
}
/// Check if a string is a valid x25519 identity (private key).
///
/// # Arguments
/// * `identity` - The identity string to validate
///
/// # Returns
/// true if valid, false otherwise
#[no_mangle]
pub extern "C" fn age_is_valid_x25519_identity(identity: *const c_char) -> bool {
let identity_str = match unsafe { cstr_to_str(identity) } {
Ok(s) => s,
Err(_) => return false,
};
age::x25519::Identity::from_str(identity_str).is_ok()
}
/// Check if a string is a valid SSH recipient (public key).
///
/// # Arguments
/// * `recipient` - The recipient string to validate
///
/// # Returns
/// true if valid, false otherwise
#[no_mangle]
pub extern "C" fn age_is_valid_ssh_recipient(recipient: *const c_char) -> bool {
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(_) => return false,
};
recipient_str.parse::<age::ssh::Recipient>().is_ok()
}
/// Get the type of a recipient string.
///
/// # Arguments
/// * `recipient` - The recipient string
///
/// # Returns
/// 0 = invalid, 1 = x25519, 2 = ssh (ed25519 or rsa)
#[no_mangle]
pub extern "C" fn age_recipient_type(recipient: *const c_char) -> u8 {
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s.trim(),
Err(_) => return 0,
};
if recipient_str.parse::<age::x25519::Recipient>().is_ok() {
return 1;
}
if recipient_str.parse::<age::ssh::Recipient>().is_ok() {
return 2;
}
0
}

View File

@@ -0,0 +1,133 @@
//! Tests for recipient and identity validation functions.
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use crate::validation::*;
use std::ffi::CString;
#[test]
fn test_is_valid_x25519_recipient_valid() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(age_is_valid_x25519_recipient(keypair.public_key));
age_free_keypair(&mut keypair);
}
#[test]
fn test_is_valid_x25519_recipient_invalid() {
let invalid = CString::new("not-a-valid-key").unwrap();
assert!(!age_is_valid_x25519_recipient(invalid.as_ptr()));
let almost_valid = CString::new("age1qqqqqqqqqqqqqqqqqqqqq").unwrap();
assert!(!age_is_valid_x25519_recipient(almost_valid.as_ptr()));
// Private key should not be valid as recipient
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(!age_is_valid_x25519_recipient(keypair.private_key));
age_free_keypair(&mut keypair);
}
#[test]
fn test_is_valid_x25519_recipient_null() {
assert!(!age_is_valid_x25519_recipient(std::ptr::null()));
}
#[test]
fn test_is_valid_x25519_identity_valid() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(age_is_valid_x25519_identity(keypair.private_key));
age_free_keypair(&mut keypair);
}
#[test]
fn test_is_valid_x25519_identity_invalid() {
let invalid = CString::new("not-a-valid-key").unwrap();
assert!(!age_is_valid_x25519_identity(invalid.as_ptr()));
let almost_valid = CString::new("AGE-SECRET-KEY-1QQQQQQQQQQQQQ").unwrap();
assert!(!age_is_valid_x25519_identity(almost_valid.as_ptr()));
// Public key should not be valid as identity
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(!age_is_valid_x25519_identity(keypair.public_key));
age_free_keypair(&mut keypair);
}
#[test]
fn test_is_valid_x25519_identity_null() {
assert!(!age_is_valid_x25519_identity(std::ptr::null()));
}
#[test]
fn test_is_valid_ssh_recipient() {
// Test with an ed25519 SSH public key format
let ed25519_key = CString::new("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGXzDvU2fB2Z9R7z1q1q1q1q1q1q1q1q1q1q1q1q1q1q").unwrap();
// This might or might not be valid depending on exact format
// The important thing is the function doesn't crash
let _ = age_is_valid_ssh_recipient(ed25519_key.as_ptr());
// Invalid SSH key
let invalid = CString::new("not-an-ssh-key").unwrap();
assert!(!age_is_valid_ssh_recipient(invalid.as_ptr()));
}
#[test]
fn test_is_valid_ssh_recipient_null() {
assert!(!age_is_valid_ssh_recipient(std::ptr::null()));
}
#[test]
fn test_recipient_type_x25519() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert_eq!(age_recipient_type(keypair.public_key), 1);
age_free_keypair(&mut keypair);
}
#[test]
fn test_recipient_type_invalid() {
let invalid = CString::new("not-a-valid-key").unwrap();
assert_eq!(age_recipient_type(invalid.as_ptr()), 0);
}
#[test]
fn test_recipient_type_null() {
assert_eq!(age_recipient_type(std::ptr::null()), 0);
}
#[test]
fn test_recipient_type_with_whitespace() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Get the public key and add whitespace
let public_key_str = unsafe {
std::ffi::CStr::from_ptr(keypair.public_key).to_str().unwrap()
};
let with_whitespace = CString::new(format!(" {} ", public_key_str)).unwrap();
// Should still be recognized as x25519 after trimming
assert_eq!(age_recipient_type(with_whitespace.as_ptr()), 1);
age_free_keypair(&mut keypair);
}
#[test]
fn test_empty_string_validation() {
let empty = CString::new("").unwrap();
assert!(!age_is_valid_x25519_recipient(empty.as_ptr()));
assert!(!age_is_valid_x25519_identity(empty.as_ptr()));
assert!(!age_is_valid_ssh_recipient(empty.as_ptr()));
assert_eq!(age_recipient_type(empty.as_ptr()), 0);
}