feat(secrets): add codex-secrets crate (#10142)
## Summary This introduces the first working foundation for Codex managed secrets: a small Rust crate that can securely store and retrieve secrets locally. Concretely, it adds a `codex-secrets` crate that: - encrypts a local secrets file using `age` - generates a high-entropy encryption key - stores that key in the OS keyring ## What this enables - A secure local persistence model for secrets - A clean, isolated place for future provider backends - A clear boundary: Codex can become a credential broker without putting plaintext secrets in config files ## Implementation details - New crate: `codex-rs/secrets/` - Encryption: `age` with scrypt recipient/identity - Key generation: `OsRng` (32 random bytes) - Key storage: OS keyring via `codex-keyring-store` ## Testing - `cd codex-rs && just fmt` - `cd codex-rs && cargo test -p codex-secrets`
This commit is contained in:
parent
f956cc2a02
commit
9257d8451c
5 changed files with 1197 additions and 29 deletions
542
codex-rs/Cargo.lock
generated
542
codex-rs/Cargo.lock
generated
|
|
@ -175,6 +175,16 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[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"
|
||||
|
|
@ -186,6 +196,49 @@ dependencies = [
|
|||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "age"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf640be7658959746f1f0f2faab798f6098a9436a8e18e148d18bc9875e13c4b"
|
||||
dependencies = [
|
||||
"age-core",
|
||||
"base64 0.21.7",
|
||||
"bech32",
|
||||
"chacha20poly1305",
|
||||
"cookie-factory",
|
||||
"hmac",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
"lazy_static",
|
||||
"nom 7.1.3",
|
||||
"pin-project",
|
||||
"rand 0.8.5",
|
||||
"rust-embed",
|
||||
"scrypt",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"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 0.21.7",
|
||||
"chacha20poly1305",
|
||||
"cookie-factory",
|
||||
"hkdf",
|
||||
"io_tee",
|
||||
"nom 7.1.3",
|
||||
"rand 0.8.5",
|
||||
"secrecy",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
|
|
@ -330,7 +383,7 @@ name = "app_test_support"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-app-server-protocol",
|
||||
"codex-core",
|
||||
|
|
@ -685,6 +738,12 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
|
@ -697,6 +756,21 @@ 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 = "bech32"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
|
||||
|
||||
[[package]]
|
||||
name = "beef"
|
||||
version = "0.5.2"
|
||||
|
|
@ -716,7 +790,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"shlex",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
|
@ -919,6 +993,30 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[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 = "chardetng"
|
||||
version = "0.1.17"
|
||||
|
|
@ -958,6 +1056,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
|||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1090,7 +1189,7 @@ dependencies = [
|
|||
"app_test_support",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
|
|
@ -1309,7 +1408,7 @@ name = "codex-cloud-requirements"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"codex-backend-client",
|
||||
"codex-core",
|
||||
"codex-otel",
|
||||
|
|
@ -1328,7 +1427,7 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-cloud-tasks-client",
|
||||
|
|
@ -1391,7 +1490,7 @@ dependencies = [
|
|||
"assert_matches",
|
||||
"async-channel",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chardetng",
|
||||
"chrono",
|
||||
"clap",
|
||||
|
|
@ -1689,7 +1788,7 @@ name = "codex-login"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-app-server-protocol",
|
||||
"codex-core",
|
||||
|
|
@ -1892,6 +1991,25 @@ dependencies = [
|
|||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-secrets"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"age",
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"codex-keyring-store",
|
||||
"keyring",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.2",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-state"
|
||||
version = "0.0.0"
|
||||
|
|
@ -1933,7 +2051,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"arboard",
|
||||
"assert_matches",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-ansi-escape",
|
||||
|
|
@ -2049,7 +2167,7 @@ dependencies = [
|
|||
name = "codex-utils-image"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"codex-utils-cache",
|
||||
"image",
|
||||
"tempfile",
|
||||
|
|
@ -2102,7 +2220,7 @@ name = "codex-windows-sandbox"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
|
|
@ -2253,6 +2371,15 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie-factory"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
|
||||
dependencies = [
|
||||
"futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
|
|
@ -2285,7 +2412,7 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"codex-core",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
|
|
@ -2475,6 +2602,32 @@ version = "0.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
|
||||
[[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 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.21.3"
|
||||
|
|
@ -3124,6 +3277,12 @@ dependencies = [
|
|||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
|
|
@ -3135,6 +3294,15 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-crate"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
|
||||
dependencies = [
|
||||
"toml 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
|
|
@ -3195,6 +3363,50 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[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 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
|
|
@ -3847,7 +4059,7 @@ version = "0.1.19"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
|
@ -3867,6 +4079,72 @@ dependencies = [
|
|||
"windows-registry",
|
||||
]
|
||||
|
||||
[[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 1.0.69",
|
||||
"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 1.0.69",
|
||||
"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 0.11.1",
|
||||
"syn 2.0.114",
|
||||
"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.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
|
|
@ -4194,6 +4472,25 @@ dependencies = [
|
|||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[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 = "inventory"
|
||||
version = "0.3.21"
|
||||
|
|
@ -4203,6 +4500,12 @@ dependencies = [
|
|||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io_tee"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304"
|
||||
|
||||
[[package]]
|
||||
name = "ipconfig"
|
||||
version = "0.3.2"
|
||||
|
|
@ -5068,7 +5371,7 @@ version = "5.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"getrandom 0.2.17",
|
||||
"http 1.4.0",
|
||||
|
|
@ -5278,6 +5581,12 @@ version = "1.70.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
|
|
@ -5403,7 +5712,7 @@ version = "0.31.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"const-hex",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
|
|
@ -5554,6 +5863,16 @@ version = "0.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
|
|
@ -5696,6 +6015,17 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[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 = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
|
|
@ -5813,6 +6143,28 @@ dependencies = [
|
|||
"toml_edit 0.23.10+spec-1.0.0",
|
||||
]
|
||||
|
||||
[[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 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
|
|
@ -5943,7 +6295,7 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls",
|
||||
"socket2 0.6.2",
|
||||
"thiserror 2.0.18",
|
||||
|
|
@ -5963,7 +6315,7 @@ dependencies = [
|
|||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
|
|
@ -6109,7 +6461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "453d60af031e23af2d48995e41b17023f6150044738680508b63671f8d7417dd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"chrono",
|
||||
"const_format",
|
||||
|
|
@ -6189,7 +6541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9d74fe0cd9bd4440827dc6dc0f504cf66065396532e798891dee2c1b740b2285"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"const_format",
|
||||
"httpdate",
|
||||
|
|
@ -6593,7 +6945,7 @@ version = "0.12.28"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
|
|
@ -6663,7 +7015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "528d42f8176e6e5e71ea69182b17d1d0a19a6b3b894b564678b74cd7cab13cfa"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
|
|
@ -6729,12 +7081,52 @@ name = "runfiles"
|
|||
version = "0.1.0"
|
||||
source = "git+https://github.com/dzbarsky/rules_rust?rev=b56cbaa8465e74127f1ea216f813cd377295ad81#b56cbaa8465e74127f1ea216f813cd377295ad81"
|
||||
|
||||
[[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 2.0.114",
|
||||
"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-demangle"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||
|
||||
[[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"
|
||||
|
|
@ -6858,6 +7250,15 @@ version = "1.0.22"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
|
||||
[[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"
|
||||
|
|
@ -7001,6 +7402,17 @@ 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 = "sdd"
|
||||
version = "3.0.10"
|
||||
|
|
@ -7016,6 +7428,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secrecy"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secret-service"
|
||||
version = "4.0.0"
|
||||
|
|
@ -7071,6 +7492,21 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[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.27"
|
||||
|
|
@ -7315,7 +7751,7 @@ version = "3.16.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
|
|
@ -7603,7 +8039,7 @@ version = "0.8.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
|
|
@ -7681,7 +8117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
|
|
@ -7726,7 +8162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
|
|
@ -8546,7 +8982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"http 1.4.0",
|
||||
"http-body",
|
||||
|
|
@ -8849,6 +9285,15 @@ dependencies = [
|
|||
"utf-8",
|
||||
]
|
||||
|
||||
[[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.19.0"
|
||||
|
|
@ -8881,6 +9326,25 @@ version = "0.1.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||
|
||||
[[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 = "unicase"
|
||||
version = "2.9.0"
|
||||
|
|
@ -8955,6 +9419,16 @@ 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 = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
|
|
@ -8973,7 +9447,7 @@ version = "3.1.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"der",
|
||||
"log",
|
||||
"native-tls",
|
||||
|
|
@ -8990,7 +9464,7 @@ version = "0.5.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"http 1.4.0",
|
||||
"httparse",
|
||||
"log",
|
||||
|
|
@ -9956,7 +10430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"deadpool",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
|
|
@ -10019,6 +10493,18 @@ version = "0.13.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[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 0.6.4",
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.3.0"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ members = [
|
|||
"cli",
|
||||
"common",
|
||||
"core",
|
||||
"secrets",
|
||||
"exec",
|
||||
"exec-server",
|
||||
"execpolicy",
|
||||
|
|
@ -79,6 +80,7 @@ codex-cli = { path = "cli"}
|
|||
codex-client = { path = "codex-client" }
|
||||
codex-common = { path = "common" }
|
||||
codex-core = { path = "core" }
|
||||
codex-secrets = { path = "secrets" }
|
||||
codex-exec = { path = "exec" }
|
||||
codex-execpolicy = { path = "execpolicy" }
|
||||
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
|
||||
|
|
@ -114,6 +116,7 @@ exec_server_test_support = { path = "exec-server/tests/common" }
|
|||
mcp_test_support = { path = "mcp-server/tests/common" }
|
||||
|
||||
# External
|
||||
age = "0.11.1"
|
||||
allocative = "0.3.3"
|
||||
ansi-to-tui = "7.0.0"
|
||||
anyhow = "1"
|
||||
|
|
@ -291,7 +294,7 @@ unwrap_used = "deny"
|
|||
# cargo-shear cannot see the platform-specific openssl-sys usage, so we
|
||||
# silence the false positive here instead of deleting a real dependency.
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["icu_provider", "openssl-sys", "codex-utils-readiness"]
|
||||
ignored = ["icu_provider", "openssl-sys", "codex-utils-readiness", "codex-secrets"]
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
|
|
|||
25
codex-rs/secrets/Cargo.toml
Normal file
25
codex-rs/secrets/Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "codex-secrets"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
age = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
codex-keyring-store = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
keyring = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
243
codex-rs/secrets/src/lib.rs
Normal file
243
codex-rs/secrets/src/lib.rs
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_keyring_store::DefaultKeyringStore;
|
||||
use codex_keyring_store::KeyringStore;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
|
||||
mod local;
|
||||
|
||||
pub use local::LocalSecretsBackend;
|
||||
|
||||
const KEYRING_SERVICE: &str = "codex";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct SecretName(String);
|
||||
|
||||
impl SecretName {
|
||||
pub fn new(raw: &str) -> Result<Self> {
|
||||
let trimmed = raw.trim();
|
||||
anyhow::ensure!(!trimmed.is_empty(), "secret name must not be empty");
|
||||
anyhow::ensure!(
|
||||
trimmed
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_'),
|
||||
"secret name must contain only A-Z, 0-9, or _"
|
||||
);
|
||||
Ok(Self(trimmed.to_string()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SecretName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum SecretScope {
|
||||
Global,
|
||||
Environment(String),
|
||||
}
|
||||
|
||||
impl SecretScope {
|
||||
pub fn environment(environment_id: impl Into<String>) -> Result<Self> {
|
||||
let env_id = environment_id.into();
|
||||
let trimmed = env_id.trim();
|
||||
anyhow::ensure!(!trimmed.is_empty(), "environment id must not be empty");
|
||||
Ok(Self::Environment(trimmed.to_string()))
|
||||
}
|
||||
|
||||
pub fn canonical_key(&self, name: &SecretName) -> String {
|
||||
// Stable, env-safe identifier used as the on-disk map key.
|
||||
match self {
|
||||
Self::Global => format!("global/{}", name.as_str()),
|
||||
Self::Environment(environment_id) => {
|
||||
format!("env/{environment_id}/{}", name.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SecretListEntry {
|
||||
pub scope: SecretScope,
|
||||
pub name: SecretName,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SecretsBackendKind {
|
||||
#[default]
|
||||
Local,
|
||||
}
|
||||
|
||||
pub trait SecretsBackend: Send + Sync {
|
||||
fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -> Result<()>;
|
||||
fn get(&self, scope: &SecretScope, name: &SecretName) -> Result<Option<String>>;
|
||||
fn delete(&self, scope: &SecretScope, name: &SecretName) -> Result<bool>;
|
||||
fn list(&self, scope_filter: Option<&SecretScope>) -> Result<Vec<SecretListEntry>>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SecretsManager {
|
||||
backend: Arc<dyn SecretsBackend>,
|
||||
}
|
||||
|
||||
impl SecretsManager {
|
||||
pub fn new(codex_home: PathBuf, backend_kind: SecretsBackendKind) -> Self {
|
||||
let backend: Arc<dyn SecretsBackend> = match backend_kind {
|
||||
SecretsBackendKind::Local => {
|
||||
let keyring_store: Arc<dyn KeyringStore> = Arc::new(DefaultKeyringStore);
|
||||
Arc::new(LocalSecretsBackend::new(codex_home, keyring_store))
|
||||
}
|
||||
};
|
||||
Self { backend }
|
||||
}
|
||||
|
||||
pub fn new_with_keyring_store(
|
||||
codex_home: PathBuf,
|
||||
backend_kind: SecretsBackendKind,
|
||||
keyring_store: Arc<dyn KeyringStore>,
|
||||
) -> Self {
|
||||
let backend: Arc<dyn SecretsBackend> = match backend_kind {
|
||||
SecretsBackendKind::Local => {
|
||||
Arc::new(LocalSecretsBackend::new(codex_home, keyring_store))
|
||||
}
|
||||
};
|
||||
Self { backend }
|
||||
}
|
||||
|
||||
pub fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -> Result<()> {
|
||||
self.backend.set(scope, name, value)
|
||||
}
|
||||
|
||||
pub fn get(&self, scope: &SecretScope, name: &SecretName) -> Result<Option<String>> {
|
||||
self.backend.get(scope, name)
|
||||
}
|
||||
|
||||
pub fn delete(&self, scope: &SecretScope, name: &SecretName) -> Result<bool> {
|
||||
self.backend.delete(scope, name)
|
||||
}
|
||||
|
||||
pub fn list(&self, scope_filter: Option<&SecretScope>) -> Result<Vec<SecretListEntry>> {
|
||||
self.backend.list(scope_filter)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn environment_id_from_cwd(cwd: &Path) -> String {
|
||||
if let Some(repo_root) = get_git_repo_root(cwd)
|
||||
&& let Some(name) = repo_root.file_name()
|
||||
{
|
||||
let name = name.to_string_lossy().trim().to_string();
|
||||
if !name.is_empty() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
let canonical = cwd
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| cwd.to_path_buf())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(canonical.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
let hex = format!("{digest:x}");
|
||||
let short = hex.get(..12).unwrap_or(hex.as_str());
|
||||
format!("cwd-{short}")
|
||||
}
|
||||
|
||||
fn get_git_repo_root(base_dir: &Path) -> Option<PathBuf> {
|
||||
let mut dir = base_dir.to_path_buf();
|
||||
|
||||
loop {
|
||||
if dir.join(".git").exists() {
|
||||
return Some(dir);
|
||||
}
|
||||
|
||||
if !dir.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn compute_keyring_account(codex_home: &Path) -> String {
|
||||
let canonical = codex_home
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| codex_home.to_path_buf())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(canonical.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
let hex = format!("{digest:x}");
|
||||
let short = hex.get(..16).unwrap_or(hex.as_str());
|
||||
format!("secrets|{short}")
|
||||
}
|
||||
|
||||
pub(crate) fn keyring_service() -> &'static str {
|
||||
KEYRING_SERVICE
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_keyring_store::tests::MockKeyringStore;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn environment_id_fallback_has_cwd_prefix() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let env_id = environment_id_from_cwd(dir.path());
|
||||
let canonical = dir
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("tempdir canonical path should exist")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(canonical.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
let hex = format!("{digest:x}");
|
||||
let short = hex.get(..12).expect("digest has at least 12 chars");
|
||||
assert_eq!(env_id, format!("cwd-{short}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_round_trips_local_backend() -> Result<()> {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let keyring = Arc::new(MockKeyringStore::default());
|
||||
let manager = SecretsManager::new_with_keyring_store(
|
||||
codex_home.path().to_path_buf(),
|
||||
SecretsBackendKind::Local,
|
||||
keyring,
|
||||
);
|
||||
let scope = SecretScope::Global;
|
||||
let name = SecretName::new("GITHUB_TOKEN")?;
|
||||
|
||||
manager.set(&scope, &name, "token-1")?;
|
||||
assert_eq!(manager.get(&scope, &name)?, Some("token-1".to_string()));
|
||||
|
||||
let listed = manager.list(None)?;
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(listed[0].name, name);
|
||||
|
||||
assert!(manager.delete(&scope, &name)?);
|
||||
assert_eq!(manager.get(&scope, &name)?, None);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
411
codex-rs/secrets/src/local.rs
Normal file
411
codex-rs/secrets/src/local.rs
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::atomic::compiler_fence;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use age::decrypt;
|
||||
use age::encrypt;
|
||||
use age::scrypt::Identity as ScryptIdentity;
|
||||
use age::scrypt::Recipient as ScryptRecipient;
|
||||
use age::secrecy::ExposeSecret;
|
||||
use age::secrecy::SecretString;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_keyring_store::KeyringStore;
|
||||
use rand::TryRngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tracing::warn;
|
||||
|
||||
use super::SecretListEntry;
|
||||
use super::SecretName;
|
||||
use super::SecretScope;
|
||||
use super::SecretsBackend;
|
||||
use super::compute_keyring_account;
|
||||
use super::keyring_service;
|
||||
|
||||
const SECRETS_VERSION: u8 = 1;
|
||||
const LOCAL_SECRETS_FILENAME: &str = "local.age";
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct SecretsFile {
|
||||
version: u8,
|
||||
secrets: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl SecretsFile {
|
||||
fn new_empty() -> Self {
|
||||
Self {
|
||||
version: SECRETS_VERSION,
|
||||
secrets: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalSecretsBackend {
|
||||
codex_home: PathBuf,
|
||||
keyring_store: Arc<dyn KeyringStore>,
|
||||
}
|
||||
|
||||
impl LocalSecretsBackend {
|
||||
pub fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
keyring_store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -> Result<()> {
|
||||
anyhow::ensure!(!value.is_empty(), "secret value must not be empty");
|
||||
let canonical_key = scope.canonical_key(name);
|
||||
let mut file = self.load_file()?;
|
||||
file.secrets.insert(canonical_key, value.to_string());
|
||||
self.save_file(&file)
|
||||
}
|
||||
|
||||
pub fn get(&self, scope: &SecretScope, name: &SecretName) -> Result<Option<String>> {
|
||||
let canonical_key = scope.canonical_key(name);
|
||||
let file = self.load_file()?;
|
||||
Ok(file.secrets.get(&canonical_key).cloned())
|
||||
}
|
||||
|
||||
pub fn delete(&self, scope: &SecretScope, name: &SecretName) -> Result<bool> {
|
||||
let canonical_key = scope.canonical_key(name);
|
||||
let mut file = self.load_file()?;
|
||||
let removed = file.secrets.remove(&canonical_key).is_some();
|
||||
if removed {
|
||||
self.save_file(&file)?;
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
pub fn list(&self, scope_filter: Option<&SecretScope>) -> Result<Vec<SecretListEntry>> {
|
||||
let file = self.load_file()?;
|
||||
let mut entries = Vec::new();
|
||||
for canonical_key in file.secrets.keys() {
|
||||
let Some(entry) = parse_canonical_key(canonical_key) else {
|
||||
warn!("skipping invalid canonical secret key: {canonical_key}");
|
||||
continue;
|
||||
};
|
||||
if let Some(scope) = scope_filter
|
||||
&& entry.scope != *scope
|
||||
{
|
||||
continue;
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn secrets_dir(&self) -> PathBuf {
|
||||
self.codex_home.join("secrets")
|
||||
}
|
||||
|
||||
fn secrets_path(&self) -> PathBuf {
|
||||
self.secrets_dir().join(LOCAL_SECRETS_FILENAME)
|
||||
}
|
||||
|
||||
fn load_file(&self) -> Result<SecretsFile> {
|
||||
let path = self.secrets_path();
|
||||
if !path.exists() {
|
||||
return Ok(SecretsFile::new_empty());
|
||||
}
|
||||
|
||||
let ciphertext = fs::read(&path)
|
||||
.with_context(|| format!("failed to read secrets file at {}", path.display()))?;
|
||||
let passphrase = self.load_or_create_passphrase()?;
|
||||
let plaintext = decrypt_with_passphrase(&ciphertext, &passphrase)?;
|
||||
let mut parsed: SecretsFile = serde_json::from_slice(&plaintext).with_context(|| {
|
||||
format!(
|
||||
"failed to deserialize decrypted secrets file at {}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
if parsed.version == 0 {
|
||||
parsed.version = SECRETS_VERSION;
|
||||
}
|
||||
anyhow::ensure!(
|
||||
parsed.version <= SECRETS_VERSION,
|
||||
"secrets file version {} is newer than supported version {}",
|
||||
parsed.version,
|
||||
SECRETS_VERSION
|
||||
);
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn save_file(&self, file: &SecretsFile) -> Result<()> {
|
||||
let dir = self.secrets_dir();
|
||||
fs::create_dir_all(&dir)
|
||||
.with_context(|| format!("failed to create secrets dir {}", dir.display()))?;
|
||||
|
||||
let passphrase = self.load_or_create_passphrase()?;
|
||||
let plaintext = serde_json::to_vec(file).context("failed to serialize secrets file")?;
|
||||
let ciphertext = encrypt_with_passphrase(&plaintext, &passphrase)?;
|
||||
let path = self.secrets_path();
|
||||
write_file_atomically(&path, &ciphertext)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_or_create_passphrase(&self) -> Result<SecretString> {
|
||||
let account = compute_keyring_account(&self.codex_home);
|
||||
let loaded = self
|
||||
.keyring_store
|
||||
.load(keyring_service(), &account)
|
||||
.map_err(|err| anyhow::anyhow!(err.message()))
|
||||
.with_context(|| format!("failed to load secrets key from keyring for {account}"))?;
|
||||
match loaded {
|
||||
Some(existing) => Ok(SecretString::from(existing)),
|
||||
None => {
|
||||
// Generate a high-entropy key and persist it in the OS keyring.
|
||||
// This keeps secrets out of plaintext config while remaining
|
||||
// fully local/offline for the MVP.
|
||||
let generated = generate_passphrase()?;
|
||||
self.keyring_store
|
||||
.save(keyring_service(), &account, generated.expose_secret())
|
||||
.map_err(|err| anyhow::anyhow!(err.message()))
|
||||
.context("failed to persist secrets key in keyring")?;
|
||||
Ok(generated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretsBackend for LocalSecretsBackend {
|
||||
fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -> Result<()> {
|
||||
LocalSecretsBackend::set(self, scope, name, value)
|
||||
}
|
||||
|
||||
fn get(&self, scope: &SecretScope, name: &SecretName) -> Result<Option<String>> {
|
||||
LocalSecretsBackend::get(self, scope, name)
|
||||
}
|
||||
|
||||
fn delete(&self, scope: &SecretScope, name: &SecretName) -> Result<bool> {
|
||||
LocalSecretsBackend::delete(self, scope, name)
|
||||
}
|
||||
|
||||
fn list(&self, scope_filter: Option<&SecretScope>) -> Result<Vec<SecretListEntry>> {
|
||||
LocalSecretsBackend::list(self, scope_filter)
|
||||
}
|
||||
}
|
||||
|
||||
fn write_file_atomically(path: &Path, contents: &[u8]) -> Result<()> {
|
||||
let dir = path.parent().with_context(|| {
|
||||
format!(
|
||||
"failed to compute parent directory for secrets file at {}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let nonce = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |duration| duration.as_nanos());
|
||||
let tmp_path = dir.join(format!(
|
||||
".{LOCAL_SECRETS_FILENAME}.tmp-{}-{nonce}",
|
||||
std::process::id()
|
||||
));
|
||||
|
||||
{
|
||||
let mut tmp_file = fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&tmp_path)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create temp secrets file at {}",
|
||||
tmp_path.display()
|
||||
)
|
||||
})?;
|
||||
tmp_file.write_all(contents).with_context(|| {
|
||||
format!(
|
||||
"failed to write temp secrets file at {}",
|
||||
tmp_path.display()
|
||||
)
|
||||
})?;
|
||||
tmp_file.sync_all().with_context(|| {
|
||||
format!("failed to sync temp secrets file at {}", tmp_path.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
match fs::rename(&tmp_path, path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(initial_error) => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if path.exists() {
|
||||
fs::remove_file(path).with_context(|| {
|
||||
format!(
|
||||
"failed to remove existing secrets file at {} before replace",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
fs::rename(&tmp_path, path).with_context(|| {
|
||||
format!(
|
||||
"failed to replace secrets file at {} with {}",
|
||||
path.display(),
|
||||
tmp_path.display()
|
||||
)
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let _ = fs::remove_file(&tmp_path);
|
||||
Err(initial_error).with_context(|| {
|
||||
format!(
|
||||
"failed to atomically replace secrets file at {} with {}",
|
||||
path.display(),
|
||||
tmp_path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_passphrase() -> Result<SecretString> {
|
||||
let mut bytes = [0_u8; 32];
|
||||
let mut rng = OsRng;
|
||||
rng.try_fill_bytes(&mut bytes)
|
||||
.context("failed to generate random secrets key")?;
|
||||
// Base64 keeps the keyring payload ASCII-safe without reducing entropy.
|
||||
let encoded = BASE64_STANDARD.encode(bytes);
|
||||
wipe_bytes(&mut bytes);
|
||||
Ok(SecretString::from(encoded))
|
||||
}
|
||||
|
||||
fn wipe_bytes(bytes: &mut [u8]) {
|
||||
for byte in bytes {
|
||||
// Volatile writes make it much harder for the compiler to elide the wipe.
|
||||
// SAFETY: `byte` is a valid mutable reference into `bytes`.
|
||||
unsafe { std::ptr::write_volatile(byte, 0) };
|
||||
}
|
||||
compiler_fence(Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn encrypt_with_passphrase(plaintext: &[u8], passphrase: &SecretString) -> Result<Vec<u8>> {
|
||||
let recipient = ScryptRecipient::new(passphrase.clone());
|
||||
encrypt(&recipient, plaintext).context("failed to encrypt secrets file")
|
||||
}
|
||||
|
||||
fn decrypt_with_passphrase(ciphertext: &[u8], passphrase: &SecretString) -> Result<Vec<u8>> {
|
||||
let identity = ScryptIdentity::new(passphrase.clone());
|
||||
decrypt(&identity, ciphertext).context("failed to decrypt secrets file")
|
||||
}
|
||||
|
||||
fn parse_canonical_key(canonical_key: &str) -> Option<SecretListEntry> {
|
||||
let mut parts = canonical_key.split('/');
|
||||
let scope_kind = parts.next()?;
|
||||
match scope_kind {
|
||||
"global" => {
|
||||
let name = parts.next()?;
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
let name = SecretName::new(name).ok()?;
|
||||
Some(SecretListEntry {
|
||||
scope: SecretScope::Global,
|
||||
name,
|
||||
})
|
||||
}
|
||||
"env" => {
|
||||
let environment_id = parts.next()?;
|
||||
let name = parts.next()?;
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
let name = SecretName::new(name).ok()?;
|
||||
let scope = SecretScope::environment(environment_id.to_string()).ok()?;
|
||||
Some(SecretListEntry { scope, name })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_keyring_store::tests::MockKeyringStore;
|
||||
use keyring::Error as KeyringError;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn load_file_rejects_newer_schema_versions() -> Result<()> {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let keyring = Arc::new(MockKeyringStore::default());
|
||||
let backend = LocalSecretsBackend::new(codex_home.path().to_path_buf(), keyring);
|
||||
|
||||
let file = SecretsFile {
|
||||
version: SECRETS_VERSION + 1,
|
||||
secrets: BTreeMap::new(),
|
||||
};
|
||||
backend.save_file(&file)?;
|
||||
|
||||
let error = backend
|
||||
.load_file()
|
||||
.expect_err("must reject newer schema version");
|
||||
assert!(
|
||||
error.to_string().contains("newer than supported version"),
|
||||
"unexpected error: {error:#}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_fails_when_keyring_is_unavailable() -> Result<()> {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let keyring = Arc::new(MockKeyringStore::default());
|
||||
let account = compute_keyring_account(codex_home.path());
|
||||
keyring.set_error(
|
||||
&account,
|
||||
KeyringError::Invalid("error".into(), "load".into()),
|
||||
);
|
||||
|
||||
let backend = LocalSecretsBackend::new(codex_home.path().to_path_buf(), keyring);
|
||||
let scope = SecretScope::Global;
|
||||
let name = SecretName::new("TEST_SECRET")?;
|
||||
let error = backend
|
||||
.set(&scope, &name, "secret-value")
|
||||
.expect_err("must fail when keyring load fails");
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("failed to load secrets key from keyring"),
|
||||
"unexpected error: {error:#}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_file_does_not_leave_temp_files() -> Result<()> {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let keyring = Arc::new(MockKeyringStore::default());
|
||||
let backend = LocalSecretsBackend::new(codex_home.path().to_path_buf(), keyring);
|
||||
|
||||
let scope = SecretScope::Global;
|
||||
let name = SecretName::new("TEST_SECRET")?;
|
||||
backend.set(&scope, &name, "one")?;
|
||||
backend.set(&scope, &name, "two")?;
|
||||
|
||||
let secrets_dir = backend.secrets_dir();
|
||||
let entries = fs::read_dir(&secrets_dir)
|
||||
.with_context(|| format!("failed to read {}", secrets_dir.display()))?
|
||||
.collect::<std::io::Result<Vec<_>>>()
|
||||
.with_context(|| format!("failed to enumerate {}", secrets_dir.display()))?;
|
||||
|
||||
let filenames: Vec<String> = entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.file_name().to_str().map(ToString::to_string))
|
||||
.collect();
|
||||
assert_eq!(filenames, vec![LOCAL_SECRETS_FILENAME.to_string()]);
|
||||
assert_eq!(backend.get(&scope, &name)?, Some("two".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue