From 0db97b714f7d0f4198c93de908ebb45dfd861177 Mon Sep 17 00:00:00 2001 From: Spencer Brower Date: Sun, 26 Apr 2026 17:29:52 -0400 Subject: [PATCH] feat: Added age-ffi. --- flake.nix | 1 + zig-vendor/age-ffi/.gitignore | 1 + zig-vendor/age-ffi/Cargo.lock | 1936 ++++++++++++++++++++ zig-vendor/age-ffi/Cargo.toml | 15 + zig-vendor/age-ffi/README.md | 165 ++ zig-vendor/age-ffi/src/armor.rs | 95 + zig-vendor/age-ffi/src/armor_tests.rs | 175 ++ zig-vendor/age-ffi/src/decrypt.rs | 299 +++ zig-vendor/age-ffi/src/decrypt_tests.rs | 430 +++++ zig-vendor/age-ffi/src/encrypt.rs | 210 +++ zig-vendor/age-ffi/src/encrypt_tests.rs | 232 +++ zig-vendor/age-ffi/src/file.rs | 351 ++++ zig-vendor/age-ffi/src/file_tests.rs | 808 ++++++++ zig-vendor/age-ffi/src/helpers.rs | 27 + zig-vendor/age-ffi/src/keys.rs | 92 + zig-vendor/age-ffi/src/keys_tests.rs | 122 ++ zig-vendor/age-ffi/src/lib.rs | 88 + zig-vendor/age-ffi/src/memory.rs | 60 + zig-vendor/age-ffi/src/memory_tests.rs | 208 +++ zig-vendor/age-ffi/src/passphrase.rs | 139 ++ zig-vendor/age-ffi/src/passphrase_tests.rs | 329 ++++ zig-vendor/age-ffi/src/tests.rs | 337 ++++ zig-vendor/age-ffi/src/types.rs | 92 + zig-vendor/age-ffi/src/validation.rs | 81 + zig-vendor/age-ffi/src/validation_tests.rs | 133 ++ zig-vendor/age-ffi/zig/README.md | 243 +++ zig-vendor/age-ffi/zig/age.zig | 631 +++++++ zig-vendor/age-ffi/zig/build.zig | 90 + zig-vendor/age-ffi/zig/example.zig | 194 ++ zig-vendor/age-ffi/zig/test.zig | 317 ++++ 30 files changed, 7901 insertions(+) create mode 100644 zig-vendor/age-ffi/.gitignore create mode 100644 zig-vendor/age-ffi/Cargo.lock create mode 100644 zig-vendor/age-ffi/Cargo.toml create mode 100644 zig-vendor/age-ffi/README.md create mode 100644 zig-vendor/age-ffi/src/armor.rs create mode 100644 zig-vendor/age-ffi/src/armor_tests.rs create mode 100644 zig-vendor/age-ffi/src/decrypt.rs create mode 100644 zig-vendor/age-ffi/src/decrypt_tests.rs create mode 100644 zig-vendor/age-ffi/src/encrypt.rs create mode 100644 zig-vendor/age-ffi/src/encrypt_tests.rs create mode 100644 zig-vendor/age-ffi/src/file.rs create mode 100644 zig-vendor/age-ffi/src/file_tests.rs create mode 100644 zig-vendor/age-ffi/src/helpers.rs create mode 100644 zig-vendor/age-ffi/src/keys.rs create mode 100644 zig-vendor/age-ffi/src/keys_tests.rs create mode 100644 zig-vendor/age-ffi/src/lib.rs create mode 100644 zig-vendor/age-ffi/src/memory.rs create mode 100644 zig-vendor/age-ffi/src/memory_tests.rs create mode 100644 zig-vendor/age-ffi/src/passphrase.rs create mode 100644 zig-vendor/age-ffi/src/passphrase_tests.rs create mode 100644 zig-vendor/age-ffi/src/tests.rs create mode 100644 zig-vendor/age-ffi/src/types.rs create mode 100644 zig-vendor/age-ffi/src/validation.rs create mode 100644 zig-vendor/age-ffi/src/validation_tests.rs create mode 100644 zig-vendor/age-ffi/zig/README.md create mode 100644 zig-vendor/age-ffi/zig/age.zig create mode 100644 zig-vendor/age-ffi/zig/build.zig create mode 100644 zig-vendor/age-ffi/zig/example.zig create mode 100644 zig-vendor/age-ffi/zig/test.zig diff --git a/flake.nix b/flake.nix index 892f61d..e9b788c 100644 --- a/flake.nix +++ b/flake.nix @@ -105,6 +105,7 @@ # Build tools zip + unstable.cargo opencode diff --git a/zig-vendor/age-ffi/.gitignore b/zig-vendor/age-ffi/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/zig-vendor/age-ffi/.gitignore @@ -0,0 +1 @@ +target diff --git a/zig-vendor/age-ffi/Cargo.lock b/zig-vendor/age-ffi/Cargo.lock new file mode 100644 index 0000000..18b627c --- /dev/null +++ b/zig-vendor/age-ffi/Cargo.lock @@ -0,0 +1,1936 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "age" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07d86e4272c093c88caf7864a2d09af52a5159180848ca4832a3cdbd7d014d5" +dependencies = [ + "aes", + "aes-gcm", + "age-core", + "base64", + "bcrypt-pbkdf", + "bech32", + "cbc", + "chacha20poly1305", + "cipher", + "console", + "cookie-factory", + "ctr", + "curve25519-dalek", + "hmac", + "i18n-embed", + "i18n-embed-fl", + "is-terminal", + "lazy_static", + "nom", + "num-traits", + "pin-project", + "pinentry", + "rand", + "rpassword", + "rsa", + "rust-embed", + "scrypt", + "sha2", + "subtle", + "which", + "wsl", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" +dependencies = [ + "base64", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "io_tee", + "nom", + "rand", + "secrecy", + "sha2", + "tempfile", +] + +[[package]] +name = "age-ffi" +version = "0.1.0" +dependencies = [ + "age", + "libc", + "secrecy", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml", +] + +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot", + "rust-embed", + "thiserror", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim", + "syn", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pinentry" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a108bb5e4e654a3d20c834e5676b4b20f7cd2cc73b22f849d93e028b259340ec" +dependencies = [ + "log", + "nom", + "percent-encoding", + "secrecy", + "wait-timeout", + "which", + "zeroize", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rpassword" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.59.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rtoolbox" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.2", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.2", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wsl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dab7ac864710bdea6594becbea5b5050333cf34fefb0dc319567eb347950d4" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "serde", + "zerofrom", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/zig-vendor/age-ffi/Cargo.toml b/zig-vendor/age-ffi/Cargo.toml new file mode 100644 index 0000000..5c4e98d --- /dev/null +++ b/zig-vendor/age-ffi/Cargo.toml @@ -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 diff --git a/zig-vendor/age-ffi/README.md b/zig-vendor/age-ffi/README.md new file mode 100644 index 0000000..341def4 --- /dev/null +++ b/zig-vendor/age-ffi/README.md @@ -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 | `age11...` | `AGE-PLUGIN--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 + +// 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. diff --git a/zig-vendor/age-ffi/src/armor.rs b/zig-vendor/age-ffi/src/armor.rs new file mode 100644 index 0000000..8873313 --- /dev/null +++ b/zig-vendor/age-ffi/src/armor.rs @@ -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 +} diff --git a/zig-vendor/age-ffi/src/armor_tests.rs b/zig-vendor/age-ffi/src/armor_tests.rs new file mode 100644 index 0000000..9186b7a --- /dev/null +++ b/zig-vendor/age-ffi/src/armor_tests.rs @@ -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![0u8; 1000], + (0..10000).map(|i| (i % 256) as u8).collect::>(), + ]; + + 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 = (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); +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/decrypt.rs b/zig-vendor/age-ffi/src/decrypt.rs new file mode 100644 index 0000000..f9abb15 --- /dev/null +++ b/zig-vendor/age-ffi/src/decrypt.rs @@ -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> = 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 +} diff --git a/zig-vendor/age-ffi/src/decrypt_tests.rs b/zig-vendor/age-ffi/src/decrypt_tests.rs new file mode 100644 index 0000000..6281d65 --- /dev/null +++ b/zig-vendor/age-ffi/src/decrypt_tests.rs @@ -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); +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/encrypt.rs b/zig-vendor/age-ffi/src/encrypt.rs new file mode 100644 index 0000000..f01f432 --- /dev/null +++ b/zig-vendor/age-ffi/src/encrypt.rs @@ -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::() { + 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> = 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::() { + parsed_recipients.push(Box::new(r)); + continue; + } + + // Try SSH + if let Ok(r) = recipient_str.parse::() { + 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::() { + 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 +} diff --git a/zig-vendor/age-ffi/src/encrypt_tests.rs b/zig-vendor/age-ffi/src/encrypt_tests.rs new file mode 100644 index 0000000..81d1dd9 --- /dev/null +++ b/zig-vendor/age-ffi/src/encrypt_tests.rs @@ -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 = (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); +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/file.rs b/zig-vendor/age-ffi/src/file.rs new file mode 100644 index 0000000..9aa5d66 --- /dev/null +++ b/zig-vendor/age-ffi/src/file.rs @@ -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 (age11...), and ssh (ssh-...) + let mut recipients: Vec> = Vec::new(); + let mut plugin_recipients: Vec = 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::() { + recipients.push(Box::new(r)); + continue; + } + // Then try plugin recipient - collect these separately + if let Ok(r) = line.parse::() { + plugin_recipients.push(r); + continue; + } + // Finally try SSH + if let Ok(r) = line.parse::() { + 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> = 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::() { + 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 +} diff --git a/zig-vendor/age-ffi/src/file_tests.rs b/zig-vendor/age-ffi/src/file_tests.rs new file mode 100644 index 0000000..dcd265d --- /dev/null +++ b/zig-vendor/age-ffi/src/file_tests.rs @@ -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); +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/helpers.rs b/zig-vendor/age-ffi/src/helpers.rs new file mode 100644 index 0000000..36b068a --- /dev/null +++ b/zig-vendor/age-ffi/src/helpers.rs @@ -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 { + 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) +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/keys.rs b/zig-vendor/age-ffi/src/keys.rs new file mode 100644 index 0000000..3a15cf5 --- /dev/null +++ b/zig-vendor/age-ffi/src/keys.rs @@ -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 +} diff --git a/zig-vendor/age-ffi/src/keys_tests.rs b/zig-vendor/age-ffi/src/keys_tests.rs new file mode 100644 index 0000000..f60fbf9 --- /dev/null +++ b/zig-vendor/age-ffi/src/keys_tests.rs @@ -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 = 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 = 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); + } +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/lib.rs b/zig-vendor/age-ffi/src/lib.rs new file mode 100644 index 0000000..bcc16c3 --- /dev/null +++ b/zig-vendor/age-ffi/src/lib.rs @@ -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; \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/memory.rs b/zig-vendor/age-ffi/src/memory.rs new file mode 100644 index 0000000..cfab3a1 --- /dev/null +++ b/zig-vendor/age-ffi/src/memory.rs @@ -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(); + } +} diff --git a/zig-vendor/age-ffi/src/memory_tests.rs b/zig-vendor/age-ffi/src/memory_tests.rs new file mode 100644 index 0000000..524fe14 --- /dev/null +++ b/zig-vendor/age-ffi/src/memory_tests.rs @@ -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 = 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()); +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/passphrase.rs b/zig-vendor/age-ffi/src/passphrase.rs new file mode 100644 index 0000000..9f63427 --- /dev/null +++ b/zig-vendor/age-ffi/src/passphrase.rs @@ -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 +} diff --git a/zig-vendor/age-ffi/src/passphrase_tests.rs b/zig-vendor/age-ffi/src/passphrase_tests.rs new file mode 100644 index 0000000..9406fec --- /dev/null +++ b/zig-vendor/age-ffi/src/passphrase_tests.rs @@ -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); +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/tests.rs b/zig-vendor/age-ffi/src/tests.rs new file mode 100644 index 0000000..615d76e --- /dev/null +++ b/zig-vendor/age-ffi/src/tests.rs @@ -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 = (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); +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/types.rs b/zig-vendor/age-ffi/src/types.rs new file mode 100644 index 0000000..316a890 --- /dev/null +++ b/zig-vendor/age-ffi/src/types.rs @@ -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) -> 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, + } + } +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/src/validation.rs b/zig-vendor/age-ffi/src/validation.rs new file mode 100644 index 0000000..74ce6d0 --- /dev/null +++ b/zig-vendor/age-ffi/src/validation.rs @@ -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::().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::().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::().is_ok() { + return 1; + } + + if recipient_str.parse::().is_ok() { + return 2; + } + + 0 +} diff --git a/zig-vendor/age-ffi/src/validation_tests.rs b/zig-vendor/age-ffi/src/validation_tests.rs new file mode 100644 index 0000000..629be7e --- /dev/null +++ b/zig-vendor/age-ffi/src/validation_tests.rs @@ -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); +} \ No newline at end of file diff --git a/zig-vendor/age-ffi/zig/README.md b/zig-vendor/age-ffi/zig/README.md new file mode 100644 index 0000000..fc0483c --- /dev/null +++ b/zig-vendor/age-ffi/zig/README.md @@ -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. diff --git a/zig-vendor/age-ffi/zig/age.zig b/zig-vendor/age-ffi/zig/age.zig new file mode 100644 index 0000000..d4c9192 --- /dev/null +++ b/zig-vendor/age-ffi/zig/age.zig @@ -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); +} diff --git a/zig-vendor/age-ffi/zig/build.zig b/zig-vendor/age-ffi/zig/build.zig new file mode 100644 index 0000000..a5968b3 --- /dev/null +++ b/zig-vendor/age-ffi/zig/build.zig @@ -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); +} diff --git a/zig-vendor/age-ffi/zig/example.zig b/zig-vendor/age-ffi/zig/example.zig new file mode 100644 index 0000000..2f5d883 --- /dev/null +++ b/zig-vendor/age-ffi/zig/example.zig @@ -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(); +} diff --git a/zig-vendor/age-ffi/zig/test.zig b/zig-vendor/age-ffi/zig/test.zig new file mode 100644 index 0000000..8b47eeb --- /dev/null +++ b/zig-vendor/age-ffi/zig/test.zig @@ -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 {}; +}