feat: Added age-ffi.

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

View File

@@ -0,0 +1,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.

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

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

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

View 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 {};
}