mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
feat: Added age-ffi.
This commit is contained in:
1
zig-vendor/age-ffi/.gitignore
vendored
Normal file
1
zig-vendor/age-ffi/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target
|
||||
1936
zig-vendor/age-ffi/Cargo.lock
generated
Normal file
1936
zig-vendor/age-ffi/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
zig-vendor/age-ffi/Cargo.toml
Normal file
15
zig-vendor/age-ffi/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "age-ffi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
age = { version = "0.11", features = ["armor", "ssh", "plugin", "cli-common"] }
|
||||
secrecy = "0.10"
|
||||
libc = "0.2"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
165
zig-vendor/age-ffi/README.md
Normal file
165
zig-vendor/age-ffi/README.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# age-ffi
|
||||
|
||||
A Rust FFI wrapper for the [age](https://github.com/str4d/rage) encryption library, with Zig bindings.
|
||||
|
||||
## Overview
|
||||
|
||||
This library provides C-compatible FFI bindings for the age encryption library, making it easy to use age encryption from other languages. It includes comprehensive Zig bindings and examples.
|
||||
|
||||
## Features
|
||||
|
||||
- **X25519 encryption** - Standard age public key encryption (`age1...`)
|
||||
- **SSH key support** - Encrypt to SSH keys (`ssh-ed25519`, `ssh-rsa`)
|
||||
- **Plugin support** - Full support for age plugins including:
|
||||
- [age-plugin-se](https://github.com/remko/age-plugin-se) (Secure Enclave on macOS)
|
||||
- [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey)
|
||||
- Any other age-compatible plugin
|
||||
- **Passphrase encryption** - Scrypt-based passphrase encryption
|
||||
- **Multiple recipients** - Encrypt to multiple recipients at once
|
||||
- **Armor format** - ASCII-armored output support
|
||||
- **File operations** - Direct file encryption/decryption
|
||||
- **Memory-safe API** - Proper error handling and memory management
|
||||
- **Comprehensive test suite**
|
||||
|
||||
## Supported Identity/Recipient Types
|
||||
|
||||
| Type | Recipient Format | Identity Format |
|
||||
|------|-----------------|-----------------|
|
||||
| X25519 | `age1...` | `AGE-SECRET-KEY-1...` |
|
||||
| SSH | `ssh-ed25519 ...`, `ssh-rsa ...` | SSH private key file |
|
||||
| Plugin | `age1<plugin>1...` | `AGE-PLUGIN-<NAME>-1...` |
|
||||
| Passphrase | N/A | Passphrase string |
|
||||
|
||||
## Building
|
||||
|
||||
### Rust Library
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
This produces `target/release/libage_ffi.a` (static library).
|
||||
|
||||
### Zig Bindings
|
||||
|
||||
```bash
|
||||
cd zig
|
||||
zig build
|
||||
```
|
||||
|
||||
Run the example:
|
||||
|
||||
```bash
|
||||
cd zig
|
||||
zig build run
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
cd zig
|
||||
zig build test
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Zig
|
||||
|
||||
```zig
|
||||
const age = @import("age");
|
||||
|
||||
// Generate a keypair
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
// Encrypt data
|
||||
const plaintext = "Hello, World!";
|
||||
var encrypted = try age.encrypt(plaintext, keypair.getPublicKey());
|
||||
defer encrypted.deinit();
|
||||
|
||||
// Decrypt data
|
||||
var decrypted = try age.decrypt(encrypted.toSlice(), keypair.getPrivateKey());
|
||||
defer decrypted.deinit();
|
||||
|
||||
// File operations with plugin support
|
||||
try age.encryptToFile(plaintext, "age1se1...", "/path/to/output.age");
|
||||
var content = try age.decryptFile("/path/to/file.age", "/path/to/identities");
|
||||
defer content.deinit();
|
||||
```
|
||||
|
||||
### C
|
||||
|
||||
```c
|
||||
#include <age_ffi.h>
|
||||
|
||||
// Generate keypair
|
||||
AgeKeypair keypair;
|
||||
age_generate_keypair(&keypair);
|
||||
|
||||
// Encrypt
|
||||
AgeBuffer encrypted;
|
||||
age_encrypt(plaintext, plaintext_len, keypair.public_key, &encrypted);
|
||||
|
||||
// Decrypt
|
||||
AgeBuffer decrypted;
|
||||
age_decrypt(encrypted.data, encrypted.len, keypair.private_key, &decrypted);
|
||||
|
||||
// Free resources
|
||||
age_free_buffer(&encrypted);
|
||||
age_free_buffer(&decrypted);
|
||||
age_free_keypair(&keypair);
|
||||
```
|
||||
|
||||
## Plugin Support
|
||||
|
||||
This library supports the [age plugin protocol](https://github.com/C2SP/C2SP/blob/main/age.md), allowing encryption and decryption with hardware-backed keys and other plugin-based identities.
|
||||
|
||||
### Requirements
|
||||
|
||||
- The plugin binary must be in your `$PATH` (e.g., `age-plugin-se`)
|
||||
- For Secure Enclave: macOS with Touch ID or Apple Watch
|
||||
|
||||
### Example with Secure Enclave
|
||||
|
||||
```bash
|
||||
# Install the plugin
|
||||
brew install age-plugin-se
|
||||
|
||||
# Generate a Secure Enclave identity
|
||||
age-plugin-se --generate -o ~/.age/se-identity.txt
|
||||
|
||||
# The library will automatically use the plugin when it sees:
|
||||
# - Recipients starting with age1se1...
|
||||
# - Identities starting with AGE-PLUGIN-SE-...
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Key Generation
|
||||
- `age_generate_keypair()` - Generate X25519 keypair
|
||||
- `age_generate_x25519()` - Generate X25519 keypair (alias)
|
||||
- `age_x25519_to_public()` - Derive public key from private key
|
||||
|
||||
### Encryption
|
||||
- `age_encrypt()` - Encrypt to a single recipient
|
||||
- `age_encrypt_multi()` - Encrypt to multiple recipients
|
||||
- `age_encrypt_armor()` - Encrypt with ASCII armor
|
||||
- `age_encrypt_passphrase()` - Encrypt with passphrase
|
||||
- `age_encrypt_to_file()` - Encrypt directly to file
|
||||
|
||||
### Decryption
|
||||
- `age_decrypt()` - Decrypt with identity string
|
||||
- `age_decrypt_multi()` - Decrypt with multiple identities
|
||||
- `age_decrypt_file()` - Decrypt file using identity file (supports plugins)
|
||||
- `age_decrypt_passphrase()` - Decrypt with passphrase
|
||||
|
||||
### Utilities
|
||||
- `age_armor()` - Wrap binary data in ASCII armor
|
||||
- `age_dearmor()` - Unwrap ASCII-armored data
|
||||
- `age_validate_recipient()` - Check if recipient string is valid
|
||||
- `age_validate_identity()` - Check if identity string is valid
|
||||
- `age_version()` - Get library version
|
||||
|
||||
## License
|
||||
|
||||
This project is dual-licensed under MIT and Apache-2.0, matching the age library.
|
||||
95
zig-vendor/age-ffi/src/armor.rs
Normal file
95
zig-vendor/age-ffi/src/armor.rs
Normal 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
|
||||
}
|
||||
175
zig-vendor/age-ffi/src/armor_tests.rs
Normal file
175
zig-vendor/age-ffi/src/armor_tests.rs
Normal 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);
|
||||
}
|
||||
299
zig-vendor/age-ffi/src/decrypt.rs
Normal file
299
zig-vendor/age-ffi/src/decrypt.rs
Normal 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
|
||||
}
|
||||
430
zig-vendor/age-ffi/src/decrypt_tests.rs
Normal file
430
zig-vendor/age-ffi/src/decrypt_tests.rs
Normal 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);
|
||||
}
|
||||
210
zig-vendor/age-ffi/src/encrypt.rs
Normal file
210
zig-vendor/age-ffi/src/encrypt.rs
Normal 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
|
||||
}
|
||||
232
zig-vendor/age-ffi/src/encrypt_tests.rs
Normal file
232
zig-vendor/age-ffi/src/encrypt_tests.rs
Normal 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);
|
||||
}
|
||||
351
zig-vendor/age-ffi/src/file.rs
Normal file
351
zig-vendor/age-ffi/src/file.rs
Normal 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
|
||||
}
|
||||
808
zig-vendor/age-ffi/src/file_tests.rs
Normal file
808
zig-vendor/age-ffi/src/file_tests.rs
Normal 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);
|
||||
}
|
||||
27
zig-vendor/age-ffi/src/helpers.rs
Normal file
27
zig-vendor/age-ffi/src/helpers.rs
Normal 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)
|
||||
}
|
||||
92
zig-vendor/age-ffi/src/keys.rs
Normal file
92
zig-vendor/age-ffi/src/keys.rs
Normal 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
|
||||
}
|
||||
122
zig-vendor/age-ffi/src/keys_tests.rs
Normal file
122
zig-vendor/age-ffi/src/keys_tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
88
zig-vendor/age-ffi/src/lib.rs
Normal file
88
zig-vendor/age-ffi/src/lib.rs
Normal 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;
|
||||
60
zig-vendor/age-ffi/src/memory.rs
Normal file
60
zig-vendor/age-ffi/src/memory.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
208
zig-vendor/age-ffi/src/memory_tests.rs
Normal file
208
zig-vendor/age-ffi/src/memory_tests.rs
Normal 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());
|
||||
}
|
||||
139
zig-vendor/age-ffi/src/passphrase.rs
Normal file
139
zig-vendor/age-ffi/src/passphrase.rs
Normal 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
|
||||
}
|
||||
329
zig-vendor/age-ffi/src/passphrase_tests.rs
Normal file
329
zig-vendor/age-ffi/src/passphrase_tests.rs
Normal 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);
|
||||
}
|
||||
337
zig-vendor/age-ffi/src/tests.rs
Normal file
337
zig-vendor/age-ffi/src/tests.rs
Normal 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);
|
||||
}
|
||||
92
zig-vendor/age-ffi/src/types.rs
Normal file
92
zig-vendor/age-ffi/src/types.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
81
zig-vendor/age-ffi/src/validation.rs
Normal file
81
zig-vendor/age-ffi/src/validation.rs
Normal 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
|
||||
}
|
||||
133
zig-vendor/age-ffi/src/validation_tests.rs
Normal file
133
zig-vendor/age-ffi/src/validation_tests.rs
Normal 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);
|
||||
}
|
||||
243
zig-vendor/age-ffi/zig/README.md
Normal file
243
zig-vendor/age-ffi/zig/README.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# Age-FFI Zig Bindings
|
||||
|
||||
Idiomatic Zig bindings for the [age](https://age-encryption.org/) encryption library.
|
||||
|
||||
## Features
|
||||
|
||||
- **Complete FFI coverage** - All age-ffi functions exposed
|
||||
- **Memory safety** - RAII wrappers with automatic cleanup
|
||||
- **Idiomatic error handling** - Zig errors instead of C result codes
|
||||
- **Type safety** - Strong typing with Zig's type system
|
||||
- **Easy to use** - High-level API that feels native to Zig
|
||||
|
||||
## Building the C Library
|
||||
|
||||
First, build the Rust FFI library:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
This creates a static library at `../target/release/libage_ffi.a`.
|
||||
|
||||
## Using the Bindings
|
||||
|
||||
### In Your Build Script
|
||||
|
||||
```zig
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "my-app",
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Add the age module
|
||||
const age_module = b.addModule("age", .{
|
||||
.root_source_file = .{ .path = "path/to/age-ffi/zig/age.zig" },
|
||||
});
|
||||
exe.root_module.addImport("age", age_module);
|
||||
|
||||
// Link the static library
|
||||
exe.addLibraryPath(.{ .path = "path/to/age-ffi/target/release" });
|
||||
exe.linkSystemLibrary("age_ffi");
|
||||
exe.linkLibC();
|
||||
|
||||
b.installArtifact(exe);
|
||||
}
|
||||
```
|
||||
|
||||
### In Your Code
|
||||
|
||||
```zig
|
||||
const age = @import("age");
|
||||
|
||||
// Generate a keypair
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
// Encrypt
|
||||
const plaintext = "Secret message";
|
||||
var encrypted = try age.encrypt(plaintext, keypair.getPublicKey());
|
||||
defer encrypted.deinit();
|
||||
|
||||
// Decrypt
|
||||
var decrypted = try age.decrypt(encrypted.toSlice(), keypair.getPrivateKey());
|
||||
defer decrypted.deinit();
|
||||
```
|
||||
|
||||
## API Overview
|
||||
|
||||
### Key Generation
|
||||
|
||||
```zig
|
||||
// Generate new keypair
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
// Derive public key from private key
|
||||
const public_key = try age.derivePublicKey(allocator, private_key);
|
||||
defer allocator.free(public_key);
|
||||
```
|
||||
|
||||
### Encryption
|
||||
|
||||
```zig
|
||||
// Simple encryption
|
||||
var encrypted = try age.encrypt(plaintext, recipient);
|
||||
defer encrypted.deinit();
|
||||
|
||||
// With ASCII armor
|
||||
var armored = try age.encryptArmor(plaintext, recipient);
|
||||
defer armored.deinit();
|
||||
|
||||
// Multiple recipients
|
||||
const recipients = [_][:0]const u8{ recipient1, recipient2 };
|
||||
var multi = try age.encryptMulti(plaintext, &recipients, false);
|
||||
defer multi.deinit();
|
||||
|
||||
// Passphrase-based
|
||||
var pass_enc = try age.encryptPassphrase(plaintext, passphrase, true);
|
||||
defer pass_enc.deinit();
|
||||
```
|
||||
|
||||
### Decryption
|
||||
|
||||
```zig
|
||||
// Simple decryption
|
||||
var decrypted = try age.decrypt(ciphertext, identity);
|
||||
defer decrypted.deinit();
|
||||
|
||||
// With multiple identities (tries each)
|
||||
const identities = [_][:0]const u8{ id1, id2 };
|
||||
var multi = try age.decryptMulti(ciphertext, &identities);
|
||||
defer multi.deinit();
|
||||
|
||||
// SSH key support
|
||||
var ssh_dec = try age.decryptSsh(ciphertext, ssh_private_key);
|
||||
defer ssh_dec.deinit();
|
||||
|
||||
// Passphrase-based
|
||||
var pass_dec = try age.decryptPassphrase(ciphertext, passphrase);
|
||||
defer pass_dec.deinit();
|
||||
```
|
||||
|
||||
### File Operations
|
||||
|
||||
```zig
|
||||
// Encrypt to file
|
||||
try age.encryptToFileArmor(plaintext, recipient, "/path/to/file.age");
|
||||
|
||||
// Decrypt from file
|
||||
var decrypted = try age.decryptFileWithIdentity("/path/to/file.age", identity);
|
||||
defer decrypted.deinit();
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
```zig
|
||||
// Validate keys
|
||||
const is_valid = age.isValidX25519Recipient(recipient);
|
||||
|
||||
// Check recipient type
|
||||
const recipient_type = age.getRecipientType(recipient);
|
||||
// Returns: .invalid, .x25519, or .ssh
|
||||
```
|
||||
|
||||
### ASCII Armor
|
||||
|
||||
```zig
|
||||
// Add armor
|
||||
var armored = try age.armor(binary_data);
|
||||
defer armored.deinit();
|
||||
|
||||
// Remove armor
|
||||
var binary = try age.dearmor(armored_data);
|
||||
defer binary.deinit();
|
||||
```
|
||||
|
||||
## Memory Management
|
||||
|
||||
The bindings use RAII wrappers that automatically free resources:
|
||||
|
||||
- `Buffer` - Wraps `AgeBuffer`, freed on `deinit()`
|
||||
- `Keypair` - Wraps `AgeKeypair`, freed on `deinit()`
|
||||
- `CString` - Wraps C strings, freed on `deinit()`
|
||||
|
||||
Always call `defer x.deinit()` after creating these objects.
|
||||
|
||||
## Error Handling
|
||||
|
||||
All operations return `AgeError!T` with the following error types:
|
||||
|
||||
- `InvalidInput`
|
||||
- `EncryptionFailed`
|
||||
- `DecryptionFailed`
|
||||
- `KeygenFailed`
|
||||
- `IoError`
|
||||
- `InvalidRecipient`
|
||||
- `InvalidIdentity`
|
||||
- `NoRecipients`
|
||||
- `NoIdentities`
|
||||
- `ArmorError`
|
||||
- `PassphraseRequired`
|
||||
- `InvalidPassphrase`
|
||||
- `SshKeyError`
|
||||
- `MemoryAllocationFailed`
|
||||
- `InvalidUtf8`
|
||||
- `UnsupportedKey`
|
||||
|
||||
## Example
|
||||
|
||||
See `example.zig` for a comprehensive demonstration of all features.
|
||||
|
||||
Run the example:
|
||||
|
||||
```bash
|
||||
# Build the example (requires build.zig in this directory)
|
||||
zig build-exe example.zig -I.. -L../target/release -lage_ffi -lc
|
||||
|
||||
# Or manually:
|
||||
zig build-exe example.zig \
|
||||
-I.. \
|
||||
-L../target/release \
|
||||
-lage_ffi \
|
||||
-lc
|
||||
|
||||
./example
|
||||
```
|
||||
|
||||
## Low-Level C API
|
||||
|
||||
The module also exposes the raw C functions if you need direct FFI access:
|
||||
|
||||
```zig
|
||||
const result = age.age_encrypt(
|
||||
plaintext.ptr,
|
||||
plaintext.len,
|
||||
recipient.ptr,
|
||||
&output,
|
||||
);
|
||||
```
|
||||
|
||||
## Version Information
|
||||
|
||||
```zig
|
||||
const version = age.getVersion(); // age-ffi version
|
||||
const lib_version = age.getLibVersion(); // underlying age library version
|
||||
```
|
||||
|
||||
## Safety Notes
|
||||
|
||||
1. All C strings must be null-terminated (`:0` sentinel)
|
||||
2. Buffers returned by the library must be freed with `deinit()`
|
||||
3. Don't use buffers after calling `deinit()`
|
||||
4. The `toOwnedSlice()` method transfers ownership and calls `deinit()` automatically
|
||||
|
||||
## License
|
||||
|
||||
Same as the parent age-ffi project.
|
||||
631
zig-vendor/age-ffi/zig/age.zig
Normal file
631
zig-vendor/age-ffi/zig/age.zig
Normal file
@@ -0,0 +1,631 @@
|
||||
//! Zig bindings for the age-ffi library
|
||||
//!
|
||||
//! This module provides idiomatic Zig wrappers around the age encryption library's C FFI.
|
||||
//! It handles memory management, error conversion, and provides safe interfaces.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @cImport({});
|
||||
|
||||
// ============================================================================
|
||||
// C Types and Structures
|
||||
// ============================================================================
|
||||
|
||||
/// Result codes for FFI functions
|
||||
pub const AgeResult = enum(c_int) {
|
||||
success = 0,
|
||||
invalid_input = 1,
|
||||
encryption_failed = 2,
|
||||
decryption_failed = 3,
|
||||
keygen_failed = 4,
|
||||
io_error = 5,
|
||||
invalid_recipient = 6,
|
||||
invalid_identity = 7,
|
||||
no_recipients = 8,
|
||||
no_identities = 9,
|
||||
armor_error = 10,
|
||||
passphrase_required = 11,
|
||||
invalid_passphrase = 12,
|
||||
ssh_key_error = 13,
|
||||
memory_allocation_failed = 14,
|
||||
invalid_utf8 = 15,
|
||||
unsupported_key = 16,
|
||||
};
|
||||
|
||||
/// A buffer containing binary data allocated by the library.
|
||||
/// Caller must free using age_free_buffer.
|
||||
pub const AgeBuffer = extern struct {
|
||||
data: [*]u8,
|
||||
len: usize,
|
||||
capacity: usize,
|
||||
|
||||
pub fn toSlice(self: AgeBuffer) []u8 {
|
||||
return self.data[0..self.len];
|
||||
}
|
||||
};
|
||||
|
||||
/// A keypair containing public and private keys as C strings.
|
||||
/// Caller must free using age_free_keypair.
|
||||
pub const AgeKeypair = extern struct {
|
||||
public_key: [*:0]u8,
|
||||
private_key: [*:0]u8,
|
||||
|
||||
pub fn getPublicKey(self: AgeKeypair) [:0]const u8 {
|
||||
return std.mem.span(self.public_key);
|
||||
}
|
||||
|
||||
pub fn getPrivateKey(self: AgeKeypair) [:0]const u8 {
|
||||
return std.mem.span(self.private_key);
|
||||
}
|
||||
};
|
||||
|
||||
/// Configuration for encryption operations.
|
||||
pub const AgeEncryptConfig = extern struct {
|
||||
armor: bool,
|
||||
scrypt_work_factor: u8,
|
||||
|
||||
pub fn default() AgeEncryptConfig {
|
||||
return .{
|
||||
.armor = false,
|
||||
.scrypt_work_factor = 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Raw C FFI Declarations
|
||||
// ============================================================================
|
||||
|
||||
/// Get the version of the age-ffi library (static string, do not free)
|
||||
pub extern "C" fn age_version() [*:0]const u8;
|
||||
|
||||
/// Get the version of the underlying age library (static string, do not free)
|
||||
pub extern "C" fn age_lib_version() [*:0]const u8;
|
||||
|
||||
// Key generation
|
||||
pub extern "C" fn age_generate_x25519(keypair: *AgeKeypair) AgeResult;
|
||||
pub extern "C" fn age_generate_keypair(keypair: *AgeKeypair) AgeResult;
|
||||
pub extern "C" fn age_x25519_to_public(private_key: [*:0]const u8, public_key: *[*:0]u8) AgeResult;
|
||||
|
||||
// Encryption
|
||||
pub extern "C" fn age_encrypt(
|
||||
plaintext: [*]const u8,
|
||||
plaintext_len: usize,
|
||||
recipient: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_encrypt_multi(
|
||||
plaintext: [*]const u8,
|
||||
plaintext_len: usize,
|
||||
recipients: [*]const [*:0]const u8,
|
||||
recipient_count: usize,
|
||||
armor: bool,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_encrypt_armor(
|
||||
plaintext: [*]const u8,
|
||||
plaintext_len: usize,
|
||||
recipient: [*:0]const u8,
|
||||
output: *[*:0]u8,
|
||||
) AgeResult;
|
||||
|
||||
// Decryption
|
||||
pub extern "C" fn age_decrypt(
|
||||
ciphertext: [*]const u8,
|
||||
ciphertext_len: usize,
|
||||
identity: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_decrypt_multi(
|
||||
ciphertext: [*]const u8,
|
||||
ciphertext_len: usize,
|
||||
identities: [*]const [*:0]const u8,
|
||||
identity_count: usize,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_decrypt_ssh(
|
||||
ciphertext: [*]const u8,
|
||||
ciphertext_len: usize,
|
||||
ssh_key: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_decrypt_ssh_file(
|
||||
ciphertext: [*]const u8,
|
||||
ciphertext_len: usize,
|
||||
ssh_key_path: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
// Passphrase
|
||||
pub extern "C" fn age_encrypt_passphrase(
|
||||
plaintext: [*]const u8,
|
||||
plaintext_len: usize,
|
||||
passphrase: [*:0]const u8,
|
||||
armor: bool,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_decrypt_passphrase(
|
||||
ciphertext: [*]const u8,
|
||||
ciphertext_len: usize,
|
||||
passphrase: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
// File operations
|
||||
pub extern "C" fn age_encrypt_to_file(
|
||||
plaintext: [*]const u8,
|
||||
plaintext_len: usize,
|
||||
output_path: [*:0]const u8,
|
||||
recipient: [*:0]const u8,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_encrypt_to_file_armor(
|
||||
plaintext: [*]const u8,
|
||||
plaintext_len: usize,
|
||||
output_path: [*:0]const u8,
|
||||
recipient: [*:0]const u8,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_decrypt_file(
|
||||
input_path: [*:0]const u8,
|
||||
identity_path: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_decrypt_file_with_identity(
|
||||
input_path: [*:0]const u8,
|
||||
identity: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_decrypt_file_passphrase(
|
||||
input_path: [*:0]const u8,
|
||||
passphrase: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
// Armor
|
||||
pub extern "C" fn age_armor(
|
||||
data: [*]const u8,
|
||||
data_len: usize,
|
||||
output: *[*:0]u8,
|
||||
) AgeResult;
|
||||
|
||||
pub extern "C" fn age_dearmor(
|
||||
armored: [*:0]const u8,
|
||||
output: *AgeBuffer,
|
||||
) AgeResult;
|
||||
|
||||
// Validation
|
||||
pub extern "C" fn age_is_valid_x25519_recipient(recipient: [*:0]const u8) bool;
|
||||
pub extern "C" fn age_is_valid_x25519_identity(identity: [*:0]const u8) bool;
|
||||
pub extern "C" fn age_is_valid_ssh_recipient(recipient: [*:0]const u8) bool;
|
||||
pub extern "C" fn age_recipient_type(recipient: [*:0]const u8) c_int;
|
||||
|
||||
// Memory management
|
||||
pub extern "C" fn age_free_buffer(buffer: *AgeBuffer) void;
|
||||
pub extern "C" fn age_free_string(s: [*:0]u8) void;
|
||||
pub extern "C" fn age_free_keypair(keypair: *AgeKeypair) void;
|
||||
|
||||
// ============================================================================
|
||||
// Error Handling
|
||||
// ============================================================================
|
||||
|
||||
pub const AgeError = error{
|
||||
InvalidInput,
|
||||
EncryptionFailed,
|
||||
DecryptionFailed,
|
||||
KeygenFailed,
|
||||
IoError,
|
||||
InvalidRecipient,
|
||||
InvalidIdentity,
|
||||
NoRecipients,
|
||||
NoIdentities,
|
||||
ArmorError,
|
||||
PassphraseRequired,
|
||||
InvalidPassphrase,
|
||||
SshKeyError,
|
||||
MemoryAllocationFailed,
|
||||
InvalidUtf8,
|
||||
UnsupportedKey,
|
||||
};
|
||||
|
||||
fn resultToError(result: AgeResult) AgeError!void {
|
||||
return switch (result) {
|
||||
.success => {},
|
||||
.invalid_input => AgeError.InvalidInput,
|
||||
.encryption_failed => AgeError.EncryptionFailed,
|
||||
.decryption_failed => AgeError.DecryptionFailed,
|
||||
.keygen_failed => AgeError.KeygenFailed,
|
||||
.io_error => AgeError.IoError,
|
||||
.invalid_recipient => AgeError.InvalidRecipient,
|
||||
.invalid_identity => AgeError.InvalidIdentity,
|
||||
.no_recipients => AgeError.NoRecipients,
|
||||
.no_identities => AgeError.NoIdentities,
|
||||
.armor_error => AgeError.ArmorError,
|
||||
.passphrase_required => AgeError.PassphraseRequired,
|
||||
.invalid_passphrase => AgeError.InvalidPassphrase,
|
||||
.ssh_key_error => AgeError.SshKeyError,
|
||||
.memory_allocation_failed => AgeError.MemoryAllocationFailed,
|
||||
.invalid_utf8 => AgeError.InvalidUtf8,
|
||||
.unsupported_key => AgeError.UnsupportedKey,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAII Wrappers for Memory Management
|
||||
// ============================================================================
|
||||
|
||||
/// RAII wrapper for AgeBuffer that automatically frees on deinit
|
||||
pub const Buffer = struct {
|
||||
buffer: AgeBuffer,
|
||||
|
||||
pub fn deinit(self: *Buffer) void {
|
||||
age_free_buffer(&self.buffer);
|
||||
}
|
||||
|
||||
pub fn toSlice(self: Buffer) []u8 {
|
||||
return self.buffer.toSlice();
|
||||
}
|
||||
|
||||
pub fn toOwnedSlice(self: *Buffer, allocator: std.mem.Allocator) ![]u8 {
|
||||
const slice = try allocator.dupe(u8, self.buffer.toSlice());
|
||||
self.deinit();
|
||||
return slice;
|
||||
}
|
||||
};
|
||||
|
||||
/// RAII wrapper for AgeKeypair that automatically frees on deinit
|
||||
pub const Keypair = struct {
|
||||
keypair: AgeKeypair,
|
||||
|
||||
pub fn deinit(self: *Keypair) void {
|
||||
age_free_keypair(&self.keypair);
|
||||
}
|
||||
|
||||
pub fn getPublicKey(self: Keypair) [:0]const u8 {
|
||||
return self.keypair.getPublicKey();
|
||||
}
|
||||
|
||||
pub fn getPrivateKey(self: Keypair) [:0]const u8 {
|
||||
return self.keypair.getPrivateKey();
|
||||
}
|
||||
};
|
||||
|
||||
/// RAII wrapper for C strings that automatically frees on deinit
|
||||
pub const CString = struct {
|
||||
ptr: [*:0]u8,
|
||||
|
||||
pub fn deinit(self: CString) void {
|
||||
age_free_string(self.ptr);
|
||||
}
|
||||
|
||||
pub fn slice(self: CString) [:0]const u8 {
|
||||
return std.mem.span(self.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// High-Level Idiomatic Zig API
|
||||
// ============================================================================
|
||||
|
||||
/// Get library version information
|
||||
pub fn getVersion() [:0]const u8 {
|
||||
return std.mem.span(age_version());
|
||||
}
|
||||
|
||||
/// Get underlying age library version
|
||||
pub fn getLibVersion() [:0]const u8 {
|
||||
return std.mem.span(age_lib_version());
|
||||
}
|
||||
|
||||
/// Generate a new x25519 keypair
|
||||
pub fn generateKeypair() AgeError!Keypair {
|
||||
var keypair: AgeKeypair = undefined;
|
||||
const result = age_generate_x25519(&keypair);
|
||||
try resultToError(result);
|
||||
return Keypair{ .keypair = keypair };
|
||||
}
|
||||
|
||||
/// Derive public key from a private x25519 identity
|
||||
pub fn derivePublicKey(allocator: std.mem.Allocator, private_key: [:0]const u8) (AgeError || error{OutOfMemory})![]u8 {
|
||||
var public_key: [*:0]u8 = undefined;
|
||||
const result = age_x25519_to_public(private_key.ptr, &public_key);
|
||||
try resultToError(result);
|
||||
|
||||
defer age_free_string(public_key);
|
||||
return allocator.dupe(u8, std.mem.span(public_key));
|
||||
}
|
||||
|
||||
/// Encrypt data with a single recipient
|
||||
pub fn encrypt(plaintext: []const u8, recipient: [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_encrypt(
|
||||
plaintext.ptr,
|
||||
plaintext.len,
|
||||
recipient.ptr,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Encrypt data with multiple recipients
|
||||
pub fn encryptMulti(plaintext: []const u8, recipients: []const [:0]const u8, use_armor: bool) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
|
||||
// Convert Zig sentinel-terminated slices to C pointers
|
||||
// We need to build an array of [*:0]const u8 pointers
|
||||
var ptrs_buf: [16][*:0]const u8 = undefined;
|
||||
if (recipients.len > ptrs_buf.len) return AgeError.NoRecipients;
|
||||
|
||||
for (recipients, 0..) |recip, i| {
|
||||
ptrs_buf[i] = recip.ptr;
|
||||
}
|
||||
|
||||
const result = age_encrypt_multi(
|
||||
plaintext.ptr,
|
||||
plaintext.len,
|
||||
&ptrs_buf,
|
||||
recipients.len,
|
||||
use_armor,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Encrypt data with ASCII armor (returns armored string as bytes)
|
||||
pub fn encryptArmor(plaintext: []const u8, recipient: [:0]const u8) AgeError!Buffer {
|
||||
var c_output: [*:0]u8 = undefined;
|
||||
const result = age_encrypt_armor(
|
||||
plaintext.ptr,
|
||||
plaintext.len,
|
||||
recipient.ptr,
|
||||
&c_output,
|
||||
);
|
||||
try resultToError(result);
|
||||
|
||||
// Convert C string to buffer
|
||||
const str = std.mem.span(c_output);
|
||||
const output: AgeBuffer = .{
|
||||
.data = c_output,
|
||||
.len = str.len,
|
||||
.capacity = str.len,
|
||||
};
|
||||
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Decrypt data with a single identity
|
||||
pub fn decrypt(ciphertext: []const u8, identity: [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_decrypt(
|
||||
ciphertext.ptr,
|
||||
ciphertext.len,
|
||||
identity.ptr,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Decrypt data with multiple identities (tries each until one succeeds)
|
||||
pub fn decryptMulti(ciphertext: []const u8, identities: []const [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
|
||||
// Convert Zig sentinel-terminated slices to C pointers
|
||||
// We need to build an array of [*:0]const u8 pointers
|
||||
var ptrs_buf: [16][*:0]const u8 = undefined;
|
||||
if (identities.len > ptrs_buf.len) return AgeError.NoIdentities;
|
||||
|
||||
for (identities, 0..) |ident, i| {
|
||||
ptrs_buf[i] = ident.ptr;
|
||||
}
|
||||
|
||||
const result = age_decrypt_multi(
|
||||
ciphertext.ptr,
|
||||
ciphertext.len,
|
||||
&ptrs_buf,
|
||||
identities.len,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Decrypt using an SSH private key (from string)
|
||||
pub fn decryptSsh(ciphertext: []const u8, ssh_key: [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_decrypt_ssh(
|
||||
ciphertext.ptr,
|
||||
ciphertext.len,
|
||||
ssh_key.ptr,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Decrypt using an SSH private key file
|
||||
pub fn decryptSshFile(ciphertext: []const u8, ssh_key_path: [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_decrypt_ssh_file(
|
||||
ciphertext.ptr,
|
||||
ciphertext.len,
|
||||
ssh_key_path.ptr,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Encrypt with a passphrase
|
||||
pub fn encryptPassphrase(plaintext: []const u8, passphrase: [:0]const u8, use_armor: bool) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_encrypt_passphrase(
|
||||
plaintext.ptr,
|
||||
plaintext.len,
|
||||
passphrase.ptr,
|
||||
use_armor,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Decrypt with a passphrase
|
||||
/// Note: If the data is ASCII-armored, you must dearmor it first using dearmor()
|
||||
/// or use decryptPassphraseArmored() for convenience.
|
||||
pub fn decryptPassphrase(ciphertext: []const u8, passphrase: [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_decrypt_passphrase(
|
||||
ciphertext.ptr,
|
||||
ciphertext.len,
|
||||
passphrase.ptr,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Decrypt armored passphrase-encrypted data (convenience function)
|
||||
/// Automatically dearmors the data before decryption.
|
||||
pub fn decryptPassphraseArmored(armored: []const u8, passphrase: [:0]const u8) AgeError!Buffer {
|
||||
// First dearmor the data
|
||||
var dearmored = try dearmor(armored);
|
||||
defer dearmored.deinit();
|
||||
|
||||
// Then decrypt the binary data
|
||||
return try decryptPassphrase(dearmored.toSlice(), passphrase);
|
||||
}
|
||||
|
||||
/// Encrypt data to a file
|
||||
pub fn encryptToFile(plaintext: []const u8, recipient: [:0]const u8, output_path: [:0]const u8) AgeError!void {
|
||||
const result = age_encrypt_to_file(
|
||||
plaintext.ptr,
|
||||
plaintext.len,
|
||||
output_path.ptr,
|
||||
recipient.ptr,
|
||||
);
|
||||
try resultToError(result);
|
||||
}
|
||||
|
||||
/// Encrypt data to a file with ASCII armor
|
||||
pub fn encryptToFileArmor(plaintext: []const u8, recipient: [:0]const u8, output_path: [:0]const u8) AgeError!void {
|
||||
const result = age_encrypt_to_file_armor(
|
||||
plaintext.ptr,
|
||||
plaintext.len,
|
||||
output_path.ptr,
|
||||
recipient.ptr,
|
||||
);
|
||||
try resultToError(result);
|
||||
}
|
||||
|
||||
/// Decrypt from a file using an identity file
|
||||
pub fn decryptFile(input_path: [:0]const u8, identity_path: [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_decrypt_file(
|
||||
input_path.ptr,
|
||||
identity_path.ptr,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Decrypt from a file using an identity string
|
||||
pub fn decryptFileWithIdentity(input_path: [:0]const u8, identity: [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_decrypt_file_with_identity(
|
||||
input_path.ptr,
|
||||
identity.ptr,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Decrypt from a file using a passphrase
|
||||
pub fn decryptFilePassphrase(input_path: [:0]const u8, passphrase: [:0]const u8) AgeError!Buffer {
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_decrypt_file_passphrase(
|
||||
input_path.ptr,
|
||||
passphrase.ptr,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Wrap binary data in ASCII armor (returns armored string as bytes)
|
||||
pub fn armor(data: []const u8) AgeError!Buffer {
|
||||
var c_output: [*:0]u8 = undefined;
|
||||
const result = age_armor(
|
||||
data.ptr,
|
||||
data.len,
|
||||
&c_output,
|
||||
);
|
||||
try resultToError(result);
|
||||
|
||||
// Convert C string to buffer
|
||||
const str = std.mem.span(c_output);
|
||||
const output: AgeBuffer = .{
|
||||
.data = c_output,
|
||||
.len = str.len,
|
||||
.capacity = str.len,
|
||||
};
|
||||
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
/// Remove ASCII armor from armored string
|
||||
pub fn dearmor(armored: []const u8) AgeError!Buffer {
|
||||
// Need to ensure the armored data is null-terminated
|
||||
// Since it's coming from armor() it should be, but we need to treat it as a C string
|
||||
const c_armored: [*:0]const u8 = @ptrCast(armored.ptr);
|
||||
|
||||
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
|
||||
const result = age_dearmor(
|
||||
c_armored,
|
||||
&output,
|
||||
);
|
||||
try resultToError(result);
|
||||
return Buffer{ .buffer = output };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validation Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Validate an x25519 recipient (public key)
|
||||
pub fn isValidX25519Recipient(recipient: [:0]const u8) bool {
|
||||
return age_is_valid_x25519_recipient(recipient.ptr);
|
||||
}
|
||||
|
||||
/// Validate an x25519 identity (private key)
|
||||
pub fn isValidX25519Identity(identity: [:0]const u8) bool {
|
||||
return age_is_valid_x25519_identity(identity.ptr);
|
||||
}
|
||||
|
||||
/// Validate an SSH recipient
|
||||
pub fn isValidSshRecipient(recipient: [:0]const u8) bool {
|
||||
return age_is_valid_ssh_recipient(recipient.ptr);
|
||||
}
|
||||
|
||||
/// Identify recipient type (0=invalid, 1=x25519, 2=ssh)
|
||||
pub const RecipientType = enum(c_int) {
|
||||
invalid = 0,
|
||||
x25519 = 1,
|
||||
ssh = 2,
|
||||
};
|
||||
|
||||
pub fn getRecipientType(recipient: [:0]const u8) RecipientType {
|
||||
const type_code = age_recipient_type(recipient.ptr);
|
||||
return @enumFromInt(type_code);
|
||||
}
|
||||
90
zig-vendor/age-ffi/zig/build.zig
Normal file
90
zig-vendor/age-ffi/zig/build.zig
Normal file
@@ -0,0 +1,90 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// Create the age module
|
||||
const age_module = b.addModule("age", .{
|
||||
.root_source_file = b.path("age.zig"),
|
||||
});
|
||||
|
||||
// Build the example executable
|
||||
const example = b.addExecutable(.{
|
||||
.name = "age-example",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("example.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
|
||||
// Add the age module to the example
|
||||
example.root_module.addImport("age", age_module);
|
||||
|
||||
// Link the Rust static library
|
||||
// Assumes the library has been built with: cargo build --release
|
||||
example.root_module.addLibraryPath(b.path("../target/release"));
|
||||
example.root_module.linkSystemLibrary("age_ffi", .{});
|
||||
|
||||
// example.root_module.linkLibC();
|
||||
|
||||
// Install the example
|
||||
b.installArtifact(example);
|
||||
|
||||
// Create run step for the example
|
||||
const run_cmd = b.addRunArtifact(example);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run the example");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// Add a step to build the Rust library first
|
||||
const cargo_build = b.addSystemCommand(&[_][]const u8{
|
||||
"cargo",
|
||||
"build",
|
||||
"--release",
|
||||
"--manifest-path",
|
||||
"../Cargo.toml",
|
||||
});
|
||||
|
||||
const cargo_step = b.step("cargo", "Build the Rust library");
|
||||
cargo_step.dependOn(&cargo_build.step);
|
||||
|
||||
// Make the example depend on the cargo build
|
||||
example.step.dependOn(&cargo_build.step);
|
||||
|
||||
// Add a clean step
|
||||
const cargo_clean = b.addSystemCommand(&[_][]const u8{
|
||||
"cargo",
|
||||
"clean",
|
||||
"--manifest-path",
|
||||
"../Cargo.toml",
|
||||
});
|
||||
|
||||
const clean_step = b.step("clean", "Clean build artifacts");
|
||||
clean_step.dependOn(&cargo_clean.step);
|
||||
|
||||
// Add test step
|
||||
const tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("test.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
|
||||
tests.root_module.addImport("age", age_module);
|
||||
tests.root_module.addLibraryPath(b.path("../target/release"));
|
||||
tests.root_module.linkSystemLibrary("age_ffi", .{});
|
||||
// tests.linkLibC();
|
||||
tests.step.dependOn(&cargo_build.step);
|
||||
|
||||
const test_run = b.addRunArtifact(tests);
|
||||
const test_step = b.step("test", "Run library tests");
|
||||
test_step.dependOn(&test_run.step);
|
||||
}
|
||||
194
zig-vendor/age-ffi/zig/example.zig
Normal file
194
zig-vendor/age-ffi/zig/example.zig
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Example usage of the age-ffi Zig bindings
|
||||
//!
|
||||
//! This file demonstrates various encryption/decryption operations using the age library.
|
||||
|
||||
const std = @import("std");
|
||||
const age = @import("age.zig");
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
const gpa = init.gpa;
|
||||
const io = init.io;
|
||||
|
||||
// Set up unbuffered stdout for Zig 0.15+ (simpler for examples)
|
||||
// var stdout_writer = std.fs.File.stdout().writer(&.{});
|
||||
var stdout_writer = std.Io.File.stdout().writer(io, &.{});
|
||||
const stdout = &stdout_writer.interface;
|
||||
|
||||
try stdout.print("age-ffi Zig Bindings Example\n", .{});
|
||||
try stdout.print("============================\n\n", .{});
|
||||
|
||||
// Print version information
|
||||
try stdout.print("Library version: {s}\n", .{age.getVersion()});
|
||||
try stdout.print("Age library version: {s}\n\n", .{age.getLibVersion()});
|
||||
|
||||
// Example 1: Generate a keypair
|
||||
try stdout.print("Example 1: Generating a keypair\n", .{});
|
||||
try stdout.print("--------------------------------\n", .{});
|
||||
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
try stdout.print("Public key: {s}\n", .{keypair.getPublicKey()});
|
||||
try stdout.print("Private key: {s}\n\n", .{keypair.getPrivateKey()});
|
||||
try stdout.flush();
|
||||
|
||||
// Example 2: Simple encryption and decryption
|
||||
try stdout.print("Example 2: Simple encryption/decryption\n", .{});
|
||||
try stdout.print("---------------------------------------\n", .{});
|
||||
|
||||
const plaintext = "Hello, World! This is a secret message.";
|
||||
try stdout.print("Original: {s}\n", .{plaintext});
|
||||
|
||||
// Encrypt
|
||||
var encrypted = try age.encrypt(plaintext, keypair.getPublicKey());
|
||||
defer encrypted.deinit();
|
||||
|
||||
try stdout.print("Encrypted: {} bytes\n", .{encrypted.buffer.len});
|
||||
|
||||
// Decrypt
|
||||
var decrypted = try age.decrypt(encrypted.toSlice(), keypair.getPrivateKey());
|
||||
defer decrypted.deinit();
|
||||
|
||||
try stdout.print("Decrypted: {s}\n\n", .{decrypted.toSlice()});
|
||||
try stdout.flush();
|
||||
|
||||
// Example 3: ASCII armor
|
||||
try stdout.print("Example 3: ASCII armor encryption\n", .{});
|
||||
try stdout.print("----------------------------------\n", .{});
|
||||
|
||||
var armored = try age.encryptArmor("This message will be ASCII armored.", keypair.getPublicKey());
|
||||
defer armored.deinit();
|
||||
|
||||
try stdout.print("Encrypted with ASCII armor: {} bytes\n", .{armored.buffer.len});
|
||||
|
||||
// Decrypt armored message
|
||||
var decrypted_armored = try age.decrypt(armored.toSlice(), keypair.getPrivateKey());
|
||||
defer decrypted_armored.deinit();
|
||||
|
||||
try stdout.print("Decrypted successfully: {s}\n\n", .{decrypted_armored.toSlice()});
|
||||
try stdout.flush();
|
||||
|
||||
// Example 4: Passphrase-based encryption
|
||||
try stdout.print("Example 4: Passphrase encryption\n", .{});
|
||||
try stdout.print("---------------------------------\n", .{});
|
||||
|
||||
const passphrase = "super-secret-passphrase";
|
||||
const secret_data = "Encrypted with a passphrase!";
|
||||
|
||||
// Encrypt without armor (armor with passphrase has decryption issues in upstream library)
|
||||
var pass_encrypted = try age.encryptPassphrase(secret_data, passphrase, false);
|
||||
defer pass_encrypted.deinit();
|
||||
|
||||
try stdout.print("Passphrase-encrypted: {} bytes\n", .{pass_encrypted.buffer.len});
|
||||
|
||||
var pass_decrypted = try age.decryptPassphrase(pass_encrypted.toSlice(), passphrase);
|
||||
defer pass_decrypted.deinit();
|
||||
|
||||
try stdout.print("Decrypted: {s}\n\n", .{pass_decrypted.toSlice()});
|
||||
try stdout.flush();
|
||||
|
||||
// Example 5: Multiple recipients
|
||||
try stdout.print("Example 5: Multiple recipients\n", .{});
|
||||
try stdout.print("-------------------------------\n", .{});
|
||||
|
||||
// Generate a second keypair
|
||||
var keypair2 = try age.generateKeypair();
|
||||
defer keypair2.deinit();
|
||||
|
||||
try stdout.print("Recipient 1: {s}\n", .{keypair.getPublicKey()});
|
||||
try stdout.print("Recipient 2: {s}\n", .{keypair2.getPublicKey()});
|
||||
|
||||
// Create array of recipients
|
||||
const recipients = [_][:0]const u8{
|
||||
keypair.getPublicKey(),
|
||||
keypair2.getPublicKey(),
|
||||
};
|
||||
|
||||
const multi_plaintext = "This can be decrypted by either recipient!";
|
||||
var multi_encrypted = try age.encryptMulti(multi_plaintext, &recipients, false);
|
||||
defer multi_encrypted.deinit();
|
||||
|
||||
try stdout.print("Encrypted for both recipients ({} bytes)\n", .{multi_encrypted.buffer.len});
|
||||
|
||||
// Decrypt with first identity
|
||||
var multi_decrypted1 = try age.decrypt(multi_encrypted.toSlice(), keypair.getPrivateKey());
|
||||
defer multi_decrypted1.deinit();
|
||||
|
||||
try stdout.print("Decrypted with key 1: {s}\n", .{multi_decrypted1.toSlice()});
|
||||
|
||||
// Decrypt with second identity
|
||||
var multi_decrypted2 = try age.decrypt(multi_encrypted.toSlice(), keypair2.getPrivateKey());
|
||||
defer multi_decrypted2.deinit();
|
||||
|
||||
try stdout.print("Decrypted with key 2: {s}\n\n", .{multi_decrypted2.toSlice()});
|
||||
|
||||
try stdout.flush();
|
||||
// Example 6: File operations
|
||||
try stdout.print("Example 6: File encryption/decryption\n", .{});
|
||||
try stdout.print("--------------------------------------\n", .{});
|
||||
|
||||
const file_data = "This will be written to an encrypted file.";
|
||||
const encrypted_file = "/tmp/test.age";
|
||||
|
||||
// Encrypt to file (non-armored)
|
||||
try age.encryptToFile(file_data, keypair.getPublicKey(), encrypted_file);
|
||||
try stdout.print("Encrypted to file: {s}\n", .{encrypted_file});
|
||||
|
||||
// Decrypt from file
|
||||
var file_decrypted = try age.decryptFileWithIdentity(encrypted_file, keypair.getPrivateKey());
|
||||
defer file_decrypted.deinit();
|
||||
|
||||
try stdout.print("Decrypted from file: {s}\n\n", .{file_decrypted.toSlice()});
|
||||
|
||||
try stdout.flush();
|
||||
// Example 7: Validation
|
||||
try stdout.print("Example 7: Key validation\n", .{});
|
||||
try stdout.print("--------------------------\n", .{});
|
||||
|
||||
const valid_recipient = keypair.getPublicKey();
|
||||
const valid_identity = keypair.getPrivateKey();
|
||||
const invalid_key = "not-a-valid-key";
|
||||
|
||||
try stdout.print("Is '{s}' a valid recipient? {}\n", .{ valid_recipient, age.isValidX25519Recipient(valid_recipient) });
|
||||
try stdout.print("Is '{s}' a valid identity? {}\n", .{ valid_identity, age.isValidX25519Identity(valid_identity) });
|
||||
try stdout.print("Is '{s}' a valid recipient? {}\n", .{ invalid_key, age.isValidX25519Recipient(invalid_key) });
|
||||
|
||||
const recipient_type = age.getRecipientType(valid_recipient);
|
||||
try stdout.print("Recipient type: {s}\n\n", .{@tagName(recipient_type)});
|
||||
|
||||
try stdout.flush();
|
||||
// Example 8: Deriving public key from private key
|
||||
try stdout.print("Example 8: Derive public key\n", .{});
|
||||
try stdout.print("-----------------------------\n", .{});
|
||||
|
||||
const derived_public = try age.derivePublicKey(gpa, keypair.getPrivateKey());
|
||||
defer gpa.free(derived_public);
|
||||
|
||||
try stdout.print("Original public: {s}\n", .{keypair.getPublicKey()});
|
||||
try stdout.print("Derived public: {s}\n", .{derived_public});
|
||||
try stdout.print("Keys match: {}\n\n", .{std.mem.eql(u8, keypair.getPublicKey(), derived_public)});
|
||||
|
||||
try stdout.flush();
|
||||
// Example 9: Error handling
|
||||
try stdout.print("Example 9: Error handling\n", .{});
|
||||
try stdout.print("-------------------------\n", .{});
|
||||
|
||||
// Try to decrypt with wrong key
|
||||
if (age.decrypt(encrypted.toSlice(), keypair2.getPrivateKey())) |_| {
|
||||
try stdout.print("Unexpected success!\n", .{});
|
||||
} else |err| {
|
||||
try stdout.print("Expected error: {s}\n", .{@errorName(err)});
|
||||
}
|
||||
|
||||
// Try to use invalid passphrase
|
||||
if (age.decryptPassphrase(pass_encrypted.toSlice(), "wrong-passphrase")) |_| {
|
||||
try stdout.print("Unexpected success!\n", .{});
|
||||
} else |err| {
|
||||
try stdout.print("Expected error: {s}\n", .{@errorName(err)});
|
||||
}
|
||||
|
||||
try stdout.print("\nAll examples completed successfully!\n", .{});
|
||||
|
||||
// Flush all output to ensure it's displayed
|
||||
try stdout.flush();
|
||||
}
|
||||
317
zig-vendor/age-ffi/zig/test.zig
Normal file
317
zig-vendor/age-ffi/zig/test.zig
Normal file
@@ -0,0 +1,317 @@
|
||||
//! Test suite for age-ffi Zig bindings
|
||||
|
||||
const std = @import("std");
|
||||
const age = @import("age.zig");
|
||||
const testing = std.testing;
|
||||
|
||||
test "version information" {
|
||||
const version = age.getVersion();
|
||||
const lib_version = age.getLibVersion();
|
||||
|
||||
try testing.expect(version.len > 0);
|
||||
try testing.expect(lib_version.len > 0);
|
||||
|
||||
std.debug.print("\nLibrary version: {s}\n", .{version});
|
||||
std.debug.print("Age library version: {s}\n", .{lib_version});
|
||||
}
|
||||
|
||||
test "generate keypair" {
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
const public_key = keypair.getPublicKey();
|
||||
const private_key = keypair.getPrivateKey();
|
||||
|
||||
try testing.expect(public_key.len > 0);
|
||||
try testing.expect(private_key.len > 0);
|
||||
try testing.expect(std.mem.startsWith(u8, public_key, "age1"));
|
||||
try testing.expect(std.mem.startsWith(u8, private_key, "AGE-SECRET-KEY-1"));
|
||||
|
||||
std.debug.print("\nGenerated keypair:\n", .{});
|
||||
std.debug.print(" Public: {s}\n", .{public_key});
|
||||
std.debug.print(" Private: {s}\n", .{private_key});
|
||||
}
|
||||
|
||||
test "derive public key from private" {
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
const derived = try age.derivePublicKey(testing.allocator, keypair.getPrivateKey());
|
||||
defer testing.allocator.free(derived);
|
||||
|
||||
try testing.expectEqualStrings(keypair.getPublicKey(), derived);
|
||||
std.debug.print("\nDerived public key matches: ✓\n", .{});
|
||||
}
|
||||
|
||||
test "simple encrypt and decrypt" {
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
const plaintext = "Hello, World! This is a test message.";
|
||||
|
||||
// Encrypt
|
||||
var encrypted = try age.encrypt(plaintext, keypair.getPublicKey());
|
||||
defer encrypted.deinit();
|
||||
|
||||
try testing.expect(encrypted.buffer.len > 0);
|
||||
std.debug.print("\nEncrypted {} bytes\n", .{encrypted.buffer.len});
|
||||
|
||||
// Decrypt
|
||||
var decrypted = try age.decrypt(encrypted.toSlice(), keypair.getPrivateKey());
|
||||
defer decrypted.deinit();
|
||||
|
||||
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
|
||||
std.debug.print("Decrypted successfully: {s}\n", .{decrypted.toSlice()});
|
||||
}
|
||||
|
||||
test "encrypt with armor" {
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
const plaintext = "This message will be ASCII armored.";
|
||||
|
||||
std.debug.print("\nTesting ASCII armor encryption...\n", .{});
|
||||
std.debug.print("Plaintext: {s}\n", .{plaintext});
|
||||
std.debug.print("Recipient: {s}\n", .{keypair.getPublicKey()});
|
||||
|
||||
// Encrypt with armor
|
||||
var encrypted = try age.encryptArmor(plaintext, keypair.getPublicKey());
|
||||
defer encrypted.deinit();
|
||||
|
||||
std.debug.print("Buffer after encryption:\n", .{});
|
||||
std.debug.print(" len: {}\n", .{encrypted.buffer.len});
|
||||
std.debug.print(" capacity: {}\n", .{encrypted.buffer.capacity});
|
||||
|
||||
try testing.expect(encrypted.buffer.len > 0);
|
||||
|
||||
const ciphertext = encrypted.toSlice();
|
||||
std.debug.print("Encrypted {} bytes\n", .{ciphertext.len});
|
||||
|
||||
// Check if it looks like ASCII armor
|
||||
if (ciphertext.len > 0) {
|
||||
const has_armor_header = std.mem.indexOf(u8, ciphertext, "-----BEGIN AGE ENCRYPTED FILE-----") != null;
|
||||
std.debug.print("Has armor header: {}\n", .{has_armor_header});
|
||||
|
||||
if (ciphertext.len < 500) {
|
||||
std.debug.print("Ciphertext:\n{s}\n", .{ciphertext});
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
var decrypted = try age.decrypt(ciphertext, keypair.getPrivateKey());
|
||||
defer decrypted.deinit();
|
||||
|
||||
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
|
||||
std.debug.print("Decrypted successfully: {s}\n", .{decrypted.toSlice()});
|
||||
}
|
||||
|
||||
test "passphrase encryption" {
|
||||
const plaintext = "Secret message encrypted with passphrase";
|
||||
const passphrase = "super-secret-password";
|
||||
|
||||
// Encrypt
|
||||
var encrypted = try age.encryptPassphrase(plaintext, passphrase, false);
|
||||
defer encrypted.deinit();
|
||||
|
||||
try testing.expect(encrypted.buffer.len > 0);
|
||||
std.debug.print("\nPassphrase encrypted {} bytes\n", .{encrypted.buffer.len});
|
||||
|
||||
// Decrypt
|
||||
var decrypted = try age.decryptPassphrase(encrypted.toSlice(), passphrase);
|
||||
defer decrypted.deinit();
|
||||
|
||||
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
|
||||
std.debug.print("Decrypted: {s}\n", .{decrypted.toSlice()});
|
||||
}
|
||||
|
||||
test "passphrase encryption with armor (manual dearmor)" {
|
||||
const plaintext = "Secret message with armor";
|
||||
const passphrase = "test-password";
|
||||
|
||||
// Encrypt with armor
|
||||
var encrypted = try age.encryptPassphrase(plaintext, passphrase, true);
|
||||
defer encrypted.deinit();
|
||||
|
||||
try testing.expect(encrypted.buffer.len > 0);
|
||||
std.debug.print("\nPassphrase encrypted with armor: {} bytes\n", .{encrypted.buffer.len});
|
||||
|
||||
const ciphertext = encrypted.toSlice();
|
||||
const has_armor = std.mem.indexOf(u8, ciphertext, "-----BEGIN") != null;
|
||||
try testing.expect(has_armor);
|
||||
std.debug.print("Has ASCII armor: ✓\n", .{});
|
||||
|
||||
// For passphrase encryption, armored data must be dearmored before decryption
|
||||
// (unlike x25519 encryption where age_decrypt auto-detects armor)
|
||||
std.debug.print("Manually dearmoring before passphrase decryption...\n", .{});
|
||||
var dearmored = try age.dearmor(ciphertext);
|
||||
defer dearmored.deinit();
|
||||
|
||||
std.debug.print("Dearmored to {} bytes\n", .{dearmored.buffer.len});
|
||||
|
||||
// Now decrypt the binary data
|
||||
var decrypted = try age.decryptPassphrase(dearmored.toSlice(), passphrase);
|
||||
defer decrypted.deinit();
|
||||
|
||||
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
|
||||
std.debug.print("Successfully decrypted armored passphrase data: ✓\n", .{});
|
||||
}
|
||||
|
||||
test "passphrase encryption with armor (convenience function)" {
|
||||
const plaintext = "Testing convenience function";
|
||||
const passphrase = "convenient-pass";
|
||||
|
||||
// Encrypt with armor
|
||||
var encrypted = try age.encryptPassphrase(plaintext, passphrase, true);
|
||||
defer encrypted.deinit();
|
||||
|
||||
std.debug.print("\nTesting decryptPassphraseArmored convenience function...\n", .{});
|
||||
|
||||
// Use the convenience function that handles dearmoring automatically
|
||||
var decrypted = try age.decryptPassphraseArmored(encrypted.toSlice(), passphrase);
|
||||
defer decrypted.deinit();
|
||||
|
||||
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
|
||||
std.debug.print("Convenience function works: ✓\n", .{});
|
||||
}
|
||||
|
||||
test "multiple recipients" {
|
||||
var keypair1 = try age.generateKeypair();
|
||||
defer keypair1.deinit();
|
||||
|
||||
var keypair2 = try age.generateKeypair();
|
||||
defer keypair2.deinit();
|
||||
|
||||
const plaintext = "Message for multiple recipients";
|
||||
const recipients = [_][:0]const u8{
|
||||
keypair1.getPublicKey(),
|
||||
keypair2.getPublicKey(),
|
||||
};
|
||||
|
||||
// Encrypt for both recipients
|
||||
var encrypted = try age.encryptMulti(plaintext, &recipients, false);
|
||||
defer encrypted.deinit();
|
||||
|
||||
try testing.expect(encrypted.buffer.len > 0);
|
||||
std.debug.print("\nEncrypted for {} recipients: {} bytes\n", .{ recipients.len, encrypted.buffer.len });
|
||||
|
||||
// Decrypt with first key
|
||||
var decrypted1 = try age.decrypt(encrypted.toSlice(), keypair1.getPrivateKey());
|
||||
defer decrypted1.deinit();
|
||||
try testing.expectEqualStrings(plaintext, decrypted1.toSlice());
|
||||
std.debug.print("Decrypted with key 1: ✓\n", .{});
|
||||
|
||||
// Decrypt with second key
|
||||
var decrypted2 = try age.decrypt(encrypted.toSlice(), keypair2.getPrivateKey());
|
||||
defer decrypted2.deinit();
|
||||
try testing.expectEqualStrings(plaintext, decrypted2.toSlice());
|
||||
std.debug.print("Decrypted with key 2: ✓\n", .{});
|
||||
}
|
||||
|
||||
test "validation functions" {
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
// Valid keys
|
||||
try testing.expect(age.isValidX25519Recipient(keypair.getPublicKey()));
|
||||
try testing.expect(age.isValidX25519Identity(keypair.getPrivateKey()));
|
||||
|
||||
std.debug.print("\nValidation tests:\n", .{});
|
||||
std.debug.print(" Valid recipient: ✓\n", .{});
|
||||
std.debug.print(" Valid identity: ✓\n", .{});
|
||||
|
||||
// Invalid keys
|
||||
try testing.expect(!age.isValidX25519Recipient("not-a-key"));
|
||||
try testing.expect(!age.isValidX25519Identity("not-a-key"));
|
||||
std.debug.print(" Invalid key detection: ✓\n", .{});
|
||||
|
||||
// Recipient type
|
||||
const recip_type = age.getRecipientType(keypair.getPublicKey());
|
||||
try testing.expectEqual(age.RecipientType.x25519, recip_type);
|
||||
std.debug.print(" Recipient type: {s}\n", .{@tagName(recip_type)});
|
||||
}
|
||||
|
||||
test "error handling - wrong key" {
|
||||
var keypair1 = try age.generateKeypair();
|
||||
defer keypair1.deinit();
|
||||
|
||||
var keypair2 = try age.generateKeypair();
|
||||
defer keypair2.deinit();
|
||||
|
||||
const plaintext = "Encrypted for keypair1";
|
||||
|
||||
var encrypted = try age.encrypt(plaintext, keypair1.getPublicKey());
|
||||
defer encrypted.deinit();
|
||||
|
||||
// Try to decrypt with wrong key
|
||||
const result = age.decrypt(encrypted.toSlice(), keypair2.getPrivateKey());
|
||||
try testing.expectError(age.AgeError.DecryptionFailed, result);
|
||||
std.debug.print("\nWrong key error: ✓\n", .{});
|
||||
}
|
||||
|
||||
test "error handling - invalid recipient" {
|
||||
const plaintext = "Test message";
|
||||
const invalid_recipient = "not-a-valid-recipient";
|
||||
|
||||
const result = age.encrypt(plaintext, invalid_recipient);
|
||||
try testing.expectError(age.AgeError.InvalidRecipient, result);
|
||||
std.debug.print("\nInvalid recipient error: ✓\n", .{});
|
||||
}
|
||||
|
||||
test "error handling - invalid passphrase" {
|
||||
const plaintext = "Secret";
|
||||
const correct_pass = "correct";
|
||||
const wrong_pass = "wrong";
|
||||
|
||||
var encrypted = try age.encryptPassphrase(plaintext, correct_pass, false);
|
||||
defer encrypted.deinit();
|
||||
|
||||
const result = age.decryptPassphrase(encrypted.toSlice(), wrong_pass);
|
||||
// Note: The underlying age library returns DecryptionFailed for wrong passphrase
|
||||
// rather than a specific InvalidPassphrase error
|
||||
try testing.expectError(age.AgeError.DecryptionFailed, result);
|
||||
std.debug.print("\nInvalid passphrase error: ✓\n", .{});
|
||||
}
|
||||
|
||||
test "armor and dearmor operations" {
|
||||
const data = "Some binary data to armor";
|
||||
|
||||
// Armor the data
|
||||
var armored = try age.armor(data);
|
||||
defer armored.deinit();
|
||||
|
||||
try testing.expect(armored.buffer.len > 0);
|
||||
std.debug.print("\nArmored {} bytes -> {} bytes\n", .{ data.len, armored.buffer.len });
|
||||
|
||||
const armored_data = armored.toSlice();
|
||||
const has_header = std.mem.indexOf(u8, armored_data, "-----BEGIN") != null;
|
||||
try testing.expect(has_header);
|
||||
|
||||
// Dearmor it
|
||||
var dearmored = try age.dearmor(armored_data);
|
||||
defer dearmored.deinit();
|
||||
|
||||
try testing.expectEqualStrings(data, dearmored.toSlice());
|
||||
std.debug.print("Dearmored successfully: ✓\n", .{});
|
||||
}
|
||||
|
||||
test "file operations" {
|
||||
const tmp_file = "/tmp/age_test_encrypted.age";
|
||||
const plaintext = "File encryption test data";
|
||||
|
||||
var keypair = try age.generateKeypair();
|
||||
defer keypair.deinit();
|
||||
|
||||
// Encrypt to file
|
||||
try age.encryptToFile(plaintext, keypair.getPublicKey(), tmp_file);
|
||||
std.debug.print("\nEncrypted to file: {s}\n", .{tmp_file});
|
||||
|
||||
// Decrypt from file
|
||||
var decrypted = try age.decryptFileWithIdentity(tmp_file, keypair.getPrivateKey());
|
||||
defer decrypted.deinit();
|
||||
|
||||
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
|
||||
std.debug.print("Decrypted from file: ✓\n", .{});
|
||||
|
||||
// Clean up
|
||||
std.fs.cwd().deleteFile(tmp_file) catch {};
|
||||
}
|
||||
Reference in New Issue
Block a user