mirror of
https://github.com/sbrow/envr.git
synced 2026-06-28 02:58:33 -04:00
244 lines
5.3 KiB
Markdown
244 lines
5.3 KiB
Markdown
# 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.
|