From 63ab39b4b16cdf0f1e10d00a13b6def8ea9c0234 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sat, 16 Aug 2025 11:22:01 +0200 Subject: [PATCH] ... --- Cargo.lock | 708 ++++++++++++++++++++++++++++++++++- Cargo.toml | 4 + examples/age_persist_demo.rs | 83 ++++ src/age.rs | 308 +++++++++++++++ src/cmd.rs | 69 ++++ src/lib.rs | 1 + 6 files changed, 1160 insertions(+), 13 deletions(-) create mode 100644 examples/age_persist_demo.rs create mode 100644 src/age.rs diff --git a/Cargo.lock b/Cargo.lock index 893fe2d..ef07907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,49 @@ dependencies = [ "generic-array", ] +[[package]] +name = "age" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77de71da1ca673855aacea507a7aed363beb8934cf61b62364fc4b479d2e8cda" +dependencies = [ + "age-core", + "base64 0.21.7", + "bech32", + "chacha20poly1305", + "cookie-factory", + "hmac", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "nom", + "pin-project", + "rand", + "rust-embed", + "scrypt", + "sha2", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5f11899bc2bbddd135edbc30c36b1924fa59d0746bb45beb5933fafe3fe509b" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "io_tee", + "nom", + "rand", + "secrecy", + "sha2", +] + [[package]] name = "anstream" version = "0.6.15" @@ -82,6 +125,12 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "async-trait" version = "0.1.88" @@ -90,7 +139,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] [[package]] @@ -114,6 +163,39 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bincode" version = "1.3.3" @@ -216,7 +298,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -228,7 +310,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] [[package]] @@ -257,6 +339,21 @@ dependencies = [ "tokio-util", ] +[[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" @@ -277,6 +374,56 @@ dependencies = [ "typenum", ] +[[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", + "digest", + "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 2.0.69", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[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" @@ -285,6 +432,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -295,7 +443,90 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +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]] @@ -363,7 +594,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] [[package]] @@ -423,6 +654,12 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "heck" version = "0.5.0" @@ -435,6 +672,93 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[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 = "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.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94205d95764f5bb9db9ea98fa77f89653365ca748e27161f5bbea2ffd50e459c" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "lazy_static", + "log", + "parking_lot", + "rust-embed", + "thiserror", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc1f8715195dffc4caddcf1cf3128da15fe5d8a137606ea8856c9300047d5a2" +dependencies = [ + "dashmap", + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.69", + "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 2.0.69", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -551,6 +875,31 @@ dependencies = [ "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_polyfill" version = "1.70.1" @@ -563,6 +912,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.155" @@ -585,12 +940,24 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -611,6 +978,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[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_cpus" version = "1.16.0" @@ -630,6 +1007,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -659,12 +1042,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.69", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -677,6 +1090,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[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" @@ -706,6 +1129,30 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -788,16 +1235,20 @@ dependencies = [ name = "redis-rs" version = "0.0.1" dependencies = [ + "age", "anyhow", + "base64 0.22.1", "bincode", "byteorder", "bytes", "chacha20poly1305", "clap", + "ed25519-dalek", "futures", "rand", "redb", "redis", + "secrecy", "serde", "serde_json", "sha2", @@ -814,24 +1265,138 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rust-embed" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.69", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +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.0", +] + +[[package]] +name = "self_cell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.210" @@ -849,7 +1414,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] [[package]] @@ -890,6 +1455,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "slab" version = "0.4.9" @@ -925,12 +1499,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -943,6 +1533,16 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.69" @@ -962,7 +1562,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] [[package]] @@ -982,7 +1582,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] [[package]] @@ -1022,7 +1622,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] [[package]] @@ -1038,12 +1638,49 @@ dependencies = [ "tokio", ] +[[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.1", +] + [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[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.12" @@ -1089,6 +1726,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[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.0+wasi-snapshot-preview1" @@ -1111,6 +1758,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1262,6 +1918,18 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[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 = "yoke" version = "0.8.0" @@ -1282,7 +1950,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", "synstructure", ] @@ -1303,7 +1971,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] [[package]] @@ -1323,7 +1991,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", "synstructure", ] @@ -1332,6 +2000,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.69", +] [[package]] name = "zerotrie" @@ -1363,5 +2045,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.69", ] diff --git a/Cargo.toml b/Cargo.toml index 4b5e15f..3b1a2d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,10 @@ bincode = "1.3.3" chacha20poly1305 = "0.10.1" rand = "0.8" sha2 = "0.10" +age = "0.10" +secrecy = "0.8" +ed25519-dalek = "2" +base64 = "0.22" [dev-dependencies] redis = { version = "0.24", features = ["aio", "tokio-comp"] } diff --git a/examples/age_persist_demo.rs b/examples/age_persist_demo.rs new file mode 100644 index 0000000..9caf3bd --- /dev/null +++ b/examples/age_persist_demo.rs @@ -0,0 +1,83 @@ +use std::io::{Read, Write}; +use std::net::TcpStream; + +// Minimal RESP helpers +fn arr(parts: &[&str]) -> String { + let mut out = format!("*{}\r\n", parts.len()); + for p in parts { + out.push_str(&format!("${}\r\n{}\r\n", p.len(), p)); + } + out +} +fn read_reply(s: &mut TcpStream) -> String { + let mut buf = [0u8; 65536]; + let n = s.read(&mut buf).unwrap(); + String::from_utf8_lossy(&buf[..n]).to_string() +} +fn parse_two_bulk(reply: &str) -> Option<(String,String)> { + let mut lines = reply.split("\r\n"); + if lines.next()? != "*2" { return None; } + let _n = lines.next()?; + let a = lines.next()?.to_string(); + let _m = lines.next()?; + let b = lines.next()?.to_string(); + Some((a,b)) +} +fn parse_bulk(reply: &str) -> Option { + let mut lines = reply.split("\r\n"); + let hdr = lines.next()?; + if !hdr.starts_with('$') { return None; } + Some(lines.next()?.to_string()) +} +fn parse_simple(reply: &str) -> Option { + let mut lines = reply.split("\r\n"); + let hdr = lines.next()?; + if !hdr.starts_with('+') { return None; } + Some(hdr[1..].to_string()) +} + +fn main() { + let mut args = std::env::args().skip(1); + let host = args.next().unwrap_or_else(|| "127.0.0.1".into()); + let port = args.next().unwrap_or_else(|| "6379".into()); + let addr = format!("{host}:{port}"); + println!("Connecting to {addr}..."); + let mut s = TcpStream::connect(addr).expect("connect"); + + // Generate & persist X25519 enc keys under name "alice" + s.write_all(arr(&["age","keygen","alice"]).as_bytes()).unwrap(); + let (_alice_recip, _alice_ident) = parse_two_bulk(&read_reply(&mut s)).expect("gen enc"); + + // Generate & persist Ed25519 signing key under name "signer" + s.write_all(arr(&["age","signkeygen","signer"]).as_bytes()).unwrap(); + let (_verify, _secret) = parse_two_bulk(&read_reply(&mut s)).expect("gen sign"); + + // Encrypt by name + let msg = "hello from persistent keys"; + s.write_all(arr(&["age","encryptname","alice", msg]).as_bytes()).unwrap(); + let ct_b64 = parse_bulk(&read_reply(&mut s)).expect("ct b64"); + println!("ciphertext b64: {}", ct_b64); + + // Decrypt by name + s.write_all(arr(&["age","decryptname","alice", &ct_b64]).as_bytes()).unwrap(); + let pt = parse_bulk(&read_reply(&mut s)).expect("pt"); + assert_eq!(pt, msg); + println!("decrypted ok"); + + // Sign by name + s.write_all(arr(&["age","signname","signer", msg]).as_bytes()).unwrap(); + let sig_b64 = parse_bulk(&read_reply(&mut s)).expect("sig b64"); + + // Verify by name + s.write_all(arr(&["age","verifyname","signer", msg, &sig_b64]).as_bytes()).unwrap(); + let ok = parse_simple(&read_reply(&mut s)).expect("verify"); + assert_eq!(ok, "1"); + println!("signature verified"); + + // List names + s.write_all(arr(&["age","list"]).as_bytes()).unwrap(); + let list = read_reply(&mut s); + println!("LIST -> {list}"); + + println!("✔ persistent AGE workflow complete."); +} \ No newline at end of file diff --git a/src/age.rs b/src/age.rs new file mode 100644 index 0000000..77501da --- /dev/null +++ b/src/age.rs @@ -0,0 +1,308 @@ +//! age.rs — AGE (rage) helpers + persistent key management for your mini-Redis. +// +// Features: +// - X25519 encryption/decryption (age style) +// - Ed25519 detached signatures + verification +// - Persistent named keys in DB (strings): +// age:key:{name} -> X25519 recipient (public encryption key, "age1...") +// age:privkey:{name} -> X25519 identity (secret encryption key, "AGE-SECRET-KEY-1...") +// age:signpub:{name} -> Ed25519 verify pubkey (public, used to verify signatures) +// age:signpriv:{name} -> Ed25519 signing secret key (private, used to sign) +// - Base64 wrapping for ciphertext/signature binary blobs. + +use std::str::FromStr; + +use secrecy::ExposeSecret; +use age::{Decryptor, Encryptor}; +use age::x25519; + +use ed25519_dalek::{Signature, Signer, Verifier, SigningKey, VerifyingKey}; + +use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; + +use crate::protocol::Protocol; +use crate::server::Server; +use crate::error::DBError; + +// ---------- Internal helpers ---------- + +#[derive(Debug)] +pub enum AgeWireError { + ParseKey, + Crypto(String), + Utf8, + SignatureLen, + NotFound(&'static str), // which kind of key was missing + Storage(String), +} + +impl AgeWireError { + fn to_protocol(self) -> Protocol { + match self { + AgeWireError::ParseKey => Protocol::err("ERR age: invalid key"), + AgeWireError::Crypto(e) => Protocol::err(&format!("ERR age: {e}")), + AgeWireError::Utf8 => Protocol::err("ERR age: invalid UTF-8 plaintext"), + AgeWireError::SignatureLen => Protocol::err("ERR age: bad signature length"), + AgeWireError::NotFound(w) => Protocol::err(&format!("ERR age: missing {w}")), + AgeWireError::Storage(e) => Protocol::err(&format!("ERR storage: {e}")), + } + } +} + +fn parse_recipient(s: &str) -> Result { + x25519::Recipient::from_str(s).map_err(|_| AgeWireError::ParseKey) +} +fn parse_identity(s: &str) -> Result { + x25519::Identity::from_str(s).map_err(|_| AgeWireError::ParseKey) +} +fn parse_ed25519_signing_key(s: &str) -> Result { + // Parse base64-encoded signing key + let bytes = B64.decode(s).map_err(|_| AgeWireError::ParseKey)?; + if bytes.len() != 32 { + return Err(AgeWireError::ParseKey); + } + let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| AgeWireError::ParseKey)?; + Ok(SigningKey::from_bytes(&key_bytes)) +} +fn parse_ed25519_verifying_key(s: &str) -> Result { + // Parse base64-encoded verifying key + let bytes = B64.decode(s).map_err(|_| AgeWireError::ParseKey)?; + if bytes.len() != 32 { + return Err(AgeWireError::ParseKey); + } + let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| AgeWireError::ParseKey)?; + VerifyingKey::from_bytes(&key_bytes).map_err(|_| AgeWireError::ParseKey) +} + +// ---------- Stateless crypto helpers (string in/out) ---------- + +pub fn gen_enc_keypair() -> (String, String) { + let id = x25519::Identity::generate(); + let pk = id.to_public(); + (pk.to_string(), id.to_string().expose_secret().to_string()) // (recipient, identity) +} + +pub fn gen_sign_keypair() -> (String, String) { + use rand::RngCore; + use rand::rngs::OsRng; + + // Generate random 32 bytes for the signing key + let mut secret_bytes = [0u8; 32]; + OsRng.fill_bytes(&mut secret_bytes); + + let signing_key = SigningKey::from_bytes(&secret_bytes); + let verifying_key = signing_key.verifying_key(); + + // Encode as base64 for storage + let signing_key_b64 = B64.encode(signing_key.to_bytes()); + let verifying_key_b64 = B64.encode(verifying_key.to_bytes()); + + (verifying_key_b64, signing_key_b64) // (verify_pub, signing_secret) +} + +/// Encrypt `msg` for `recipient_str` (X25519). Returns base64(ciphertext). +pub fn encrypt_b64(recipient_str: &str, msg: &str) -> Result { + let recipient = parse_recipient(recipient_str)?; + let enc = Encryptor::with_recipients(vec![Box::new(recipient)]) + .expect("failed to create encryptor"); // Handle Option + let mut out = Vec::new(); + { + use std::io::Write; + let mut w = enc.wrap_output(&mut out).map_err(|e| AgeWireError::Crypto(e.to_string()))?; + w.write_all(msg.as_bytes()).map_err(|e| AgeWireError::Crypto(e.to_string()))?; + w.finish().map_err(|e| AgeWireError::Crypto(e.to_string()))?; + } + Ok(B64.encode(out)) +} + +/// Decrypt base64(ciphertext) with `identity_str`. Returns plaintext String. +pub fn decrypt_b64(identity_str: &str, ct_b64: &str) -> Result { + let id = parse_identity(identity_str)?; + let ct = B64.decode(ct_b64.as_bytes()).map_err(|e| AgeWireError::Crypto(e.to_string()))?; + let dec = Decryptor::new(&ct[..]).map_err(|e| AgeWireError::Crypto(e.to_string()))?; + + // The decrypt method returns a Result + let mut r = match dec { + Decryptor::Recipients(d) => d.decrypt(std::iter::once(&id as &dyn age::Identity)) + .map_err(|e| AgeWireError::Crypto(e.to_string()))?, + Decryptor::Passphrase(_) => return Err(AgeWireError::Crypto("Expected recipients, got passphrase".to_string())), + }; + + let mut pt = Vec::new(); + use std::io::Read; + r.read_to_end(&mut pt).map_err(|e| AgeWireError::Crypto(e.to_string()))?; + String::from_utf8(pt).map_err(|_| AgeWireError::Utf8) +} + +/// Sign bytes of `msg` (detached). Returns base64(signature bytes, 64 bytes). +pub fn sign_b64(signing_secret_str: &str, msg: &str) -> Result { + let signing_key = parse_ed25519_signing_key(signing_secret_str)?; + let sig = signing_key.sign(msg.as_bytes()); + Ok(B64.encode(sig.to_bytes())) +} + +/// Verify detached signature (base64) for `msg` with pubkey. +pub fn verify_b64(verify_pub_str: &str, msg: &str, sig_b64: &str) -> Result { + let verifying_key = parse_ed25519_verifying_key(verify_pub_str)?; + let sig_bytes = B64.decode(sig_b64.as_bytes()).map_err(|e| AgeWireError::Crypto(e.to_string()))?; + if sig_bytes.len() != 64 { + return Err(AgeWireError::SignatureLen); + } + let sig = Signature::from_bytes(sig_bytes[..].try_into().unwrap()); + Ok(verifying_key.verify(msg.as_bytes(), &sig).is_ok()) +} + +// ---------- Storage helpers ---------- + +fn sget(server: &Server, key: &str) -> Result, AgeWireError> { + let st = server.current_storage().map_err(|e| AgeWireError::Storage(e.0))?; + st.get(key).map_err(|e| AgeWireError::Storage(e.0)) +} +fn sset(server: &Server, key: &str, val: &str) -> Result<(), AgeWireError> { + let st = server.current_storage().map_err(|e| AgeWireError::Storage(e.0))?; + st.set(key.to_string(), val.to_string()).map_err(|e| AgeWireError::Storage(e.0)) +} + +fn enc_pub_key_key(name: &str) -> String { format!("age:key:{name}") } +fn enc_priv_key_key(name: &str) -> String { format!("age:privkey:{name}") } +fn sign_pub_key_key(name: &str) -> String { format!("age:signpub:{name}") } +fn sign_priv_key_key(name: &str) -> String { format!("age:signpriv:{name}") } + +// ---------- Command handlers (RESP Protocol) ---------- +// Basic (stateless) ones kept for completeness + +pub async fn cmd_age_genenc() -> Protocol { + let (recip, ident) = gen_enc_keypair(); + Protocol::Array(vec![Protocol::BulkString(recip), Protocol::BulkString(ident)]) +} + +pub async fn cmd_age_gensign() -> Protocol { + let (verify, secret) = gen_sign_keypair(); + Protocol::Array(vec![Protocol::BulkString(verify), Protocol::BulkString(secret)]) +} + +pub async fn cmd_age_encrypt(recipient: &str, message: &str) -> Protocol { + match encrypt_b64(recipient, message) { + Ok(b64) => Protocol::BulkString(b64), + Err(e) => e.to_protocol(), + } +} + +pub async fn cmd_age_decrypt(identity: &str, ct_b64: &str) -> Protocol { + match decrypt_b64(identity, ct_b64) { + Ok(pt) => Protocol::BulkString(pt), + Err(e) => e.to_protocol(), + } +} + +pub async fn cmd_age_sign(secret: &str, message: &str) -> Protocol { + match sign_b64(secret, message) { + Ok(b64sig) => Protocol::BulkString(b64sig), + Err(e) => e.to_protocol(), + } +} + +pub async fn cmd_age_verify(verify_pub: &str, message: &str, sig_b64: &str) -> Protocol { + match verify_b64(verify_pub, message, sig_b64) { + Ok(true) => Protocol::SimpleString("1".to_string()), + Ok(false) => Protocol::SimpleString("0".to_string()), + Err(e) => e.to_protocol(), + } +} + +// ---------- NEW: Persistent, named-key commands ---------- + +pub async fn cmd_age_keygen(server: &Server, name: &str) -> Protocol { + let (recip, ident) = gen_enc_keypair(); + if let Err(e) = sset(server, &enc_pub_key_key(name), &recip) { return e.to_protocol(); } + if let Err(e) = sset(server, &enc_priv_key_key(name), &ident) { return e.to_protocol(); } + Protocol::Array(vec![Protocol::BulkString(recip), Protocol::BulkString(ident)]) +} + +pub async fn cmd_age_signkeygen(server: &Server, name: &str) -> Protocol { + let (verify, secret) = gen_sign_keypair(); + if let Err(e) = sset(server, &sign_pub_key_key(name), &verify) { return e.to_protocol(); } + if let Err(e) = sset(server, &sign_priv_key_key(name), &secret) { return e.to_protocol(); } + Protocol::Array(vec![Protocol::BulkString(verify), Protocol::BulkString(secret)]) +} + +pub async fn cmd_age_encrypt_name(server: &Server, name: &str, message: &str) -> Protocol { + let recip = match sget(server, &enc_pub_key_key(name)) { + Ok(Some(v)) => v, + Ok(None) => return AgeWireError::NotFound("recipient (age:key:{name})").to_protocol(), + Err(e) => return e.to_protocol(), + }; + match encrypt_b64(&recip, message) { + Ok(ct) => Protocol::BulkString(ct), + Err(e) => e.to_protocol(), + } +} + +pub async fn cmd_age_decrypt_name(server: &Server, name: &str, ct_b64: &str) -> Protocol { + let ident = match sget(server, &enc_priv_key_key(name)) { + Ok(Some(v)) => v, + Ok(None) => return AgeWireError::NotFound("identity (age:privkey:{name})").to_protocol(), + Err(e) => return e.to_protocol(), + }; + match decrypt_b64(&ident, ct_b64) { + Ok(pt) => Protocol::BulkString(pt), + Err(e) => e.to_protocol(), + } +} + +pub async fn cmd_age_sign_name(server: &Server, name: &str, message: &str) -> Protocol { + let sec = match sget(server, &sign_priv_key_key(name)) { + Ok(Some(v)) => v, + Ok(None) => return AgeWireError::NotFound("signing secret (age:signpriv:{name})").to_protocol(), + Err(e) => return e.to_protocol(), + }; + match sign_b64(&sec, message) { + Ok(sig) => Protocol::BulkString(sig), + Err(e) => e.to_protocol(), + } +} + +pub async fn cmd_age_verify_name(server: &Server, name: &str, message: &str, sig_b64: &str) -> Protocol { + let pubk = match sget(server, &sign_pub_key_key(name)) { + Ok(Some(v)) => v, + Ok(None) => return AgeWireError::NotFound("verify pubkey (age:signpub:{name})").to_protocol(), + Err(e) => return e.to_protocol(), + }; + match verify_b64(&pubk, message, sig_b64) { + Ok(true) => Protocol::SimpleString("1".to_string()), + Ok(false) => Protocol::SimpleString("0".to_string()), + Err(e) => e.to_protocol(), + } +} + +pub async fn cmd_age_list(server: &Server) -> Protocol { + // Returns 4 arrays: ["encpub", ], ["encpriv", ...], ["signpub", ...], ["signpriv", ...] + let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) }; + + let pull = |pat: &str, prefix: &str| -> Result, DBError> { + let keys = st.keys(pat)?; + let mut names: Vec = keys.into_iter() + .filter_map(|k| k.strip_prefix(prefix).map(|x| x.to_string())) + .collect(); + names.sort(); + Ok(names) + }; + + let encpub = match pull("age:key:*", "age:key:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; + let encpriv = match pull("age:privkey:*", "age:privkey:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; + let signpub = match pull("age:signpub:*", "age:signpub:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; + let signpriv= match pull("age:signpriv:*", "age:signpriv:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; + + let to_arr = |label: &str, v: Vec| { + let mut out = vec![Protocol::BulkString(label.to_string())]; + out.push(Protocol::Array(v.into_iter().map(Protocol::BulkString).collect())); + Protocol::Array(out) + }; + + Protocol::Array(vec![ + to_arr("encpub", encpub), + to_arr("encpriv", encpriv), + to_arr("signpub", signpub), + to_arr("signpriv", signpriv), + ]) +} \ No newline at end of file diff --git a/src/cmd.rs b/src/cmd.rs index a230f98..c036b4a 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -50,6 +50,22 @@ pub enum Cmd { LRange(String, i64, i64), FlushDb, Unknow(String), + // AGE (rage) commands — stateless + AgeGenEnc, + AgeGenSign, + AgeEncrypt(String, String), // recipient, message + AgeDecrypt(String, String), // identity, ciphertext_b64 + AgeSign(String, String), // signing_secret, message + AgeVerify(String, String, String), // verify_pub, message, signature_b64 + + // NEW: persistent named-key commands + AgeKeygen(String), // name + AgeSignKeygen(String), // name + AgeEncryptName(String, String), // name, message + AgeDecryptName(String, String), // name, ciphertext_b64 + AgeSignName(String, String), // name, message + AgeVerifyName(String, String, String), // name, message, signature_b64 + AgeList, } impl Cmd { @@ -402,6 +418,43 @@ impl Cmd { } Cmd::FlushDb } + "age" => { + if cmd.len() < 2 { + return Err(DBError("wrong number of arguments for AGE".to_string())); + } + match cmd[1].to_lowercase().as_str() { + // stateless + "genenc" => { if cmd.len() != 2 { return Err(DBError("AGE GENENC takes no args".to_string())); } + Cmd::AgeGenEnc } + "gensign" => { if cmd.len() != 2 { return Err(DBError("AGE GENSIGN takes no args".to_string())); } + Cmd::AgeGenSign } + "encrypt" => { if cmd.len() != 4 { return Err(DBError("AGE ENCRYPT ".to_string())); } + Cmd::AgeEncrypt(cmd[2].clone(), cmd[3].clone()) } + "decrypt" => { if cmd.len() != 4 { return Err(DBError("AGE DECRYPT ".to_string())); } + Cmd::AgeDecrypt(cmd[2].clone(), cmd[3].clone()) } + "sign" => { if cmd.len() != 4 { return Err(DBError("AGE SIGN ".to_string())); } + Cmd::AgeSign(cmd[2].clone(), cmd[3].clone()) } + "verify" => { if cmd.len() != 5 { return Err(DBError("AGE VERIFY ".to_string())); } + Cmd::AgeVerify(cmd[2].clone(), cmd[3].clone(), cmd[4].clone()) } + + // persistent names + "keygen" => { if cmd.len() != 3 { return Err(DBError("AGE KEYGEN ".to_string())); } + Cmd::AgeKeygen(cmd[2].clone()) } + "signkeygen" => { if cmd.len() != 3 { return Err(DBError("AGE SIGNKEYGEN ".to_string())); } + Cmd::AgeSignKeygen(cmd[2].clone()) } + "encryptname" => { if cmd.len() != 4 { return Err(DBError("AGE ENCRYPTNAME ".to_string())); } + Cmd::AgeEncryptName(cmd[2].clone(), cmd[3].clone()) } + "decryptname" => { if cmd.len() != 4 { return Err(DBError("AGE DECRYPTNAME ".to_string())); } + Cmd::AgeDecryptName(cmd[2].clone(), cmd[3].clone()) } + "signname" => { if cmd.len() != 4 { return Err(DBError("AGE SIGNNAME ".to_string())); } + Cmd::AgeSignName(cmd[2].clone(), cmd[3].clone()) } + "verifyname" => { if cmd.len() != 5 { return Err(DBError("AGE VERIFYNAME ".to_string())); } + Cmd::AgeVerifyName(cmd[2].clone(), cmd[3].clone(), cmd[4].clone()) } + "list" => { if cmd.len() != 2 { return Err(DBError("AGE LIST".to_string())); } + Cmd::AgeList } + _ => return Err(DBError(format!("unsupported AGE subcommand {:?}", cmd))), + } + } _ => Cmd::Unknow(cmd[0].clone()), }, protocol, @@ -484,6 +537,22 @@ impl Cmd { Cmd::LIndex(key, index) => lindex_cmd(server, &key, index).await, Cmd::LRange(key, start, stop) => lrange_cmd(server, &key, start, stop).await, Cmd::FlushDb => flushdb_cmd(server).await, + // AGE (rage): stateless + Cmd::AgeGenEnc => Ok(crate::age::cmd_age_genenc().await), + Cmd::AgeGenSign => Ok(crate::age::cmd_age_gensign().await), + Cmd::AgeEncrypt(recipient, message) => Ok(crate::age::cmd_age_encrypt(&recipient, &message).await), + Cmd::AgeDecrypt(identity, ct_b64) => Ok(crate::age::cmd_age_decrypt(&identity, &ct_b64).await), + Cmd::AgeSign(secret, message) => Ok(crate::age::cmd_age_sign(&secret, &message).await), + Cmd::AgeVerify(vpub, msg, sig_b64) => Ok(crate::age::cmd_age_verify(&vpub, &msg, &sig_b64).await), + + // AGE (rage): persistent named keys + Cmd::AgeKeygen(name) => Ok(crate::age::cmd_age_keygen(server, &name).await), + Cmd::AgeSignKeygen(name) => Ok(crate::age::cmd_age_signkeygen(server, &name).await), + Cmd::AgeEncryptName(name, message) => Ok(crate::age::cmd_age_encrypt_name(server, &name, &message).await), + Cmd::AgeDecryptName(name, ct_b64) => Ok(crate::age::cmd_age_decrypt_name(server, &name, &ct_b64).await), + Cmd::AgeSignName(name, message) => Ok(crate::age::cmd_age_sign_name(server, &name, &message).await), + Cmd::AgeVerifyName(name, message, sig_b64) => Ok(crate::age::cmd_age_verify_name(server, &name, &message, &sig_b64).await), + Cmd::AgeList => Ok(crate::age::cmd_age_list(server).await), Cmd::Unknow(s) => Ok(Protocol::err(&format!("ERR unknown command `{}`", s))), } } diff --git a/src/lib.rs b/src/lib.rs index a73fec8..a66b56e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod age; // NEW pub mod cmd; pub mod crypto; pub mod error;