Compare commits

...

4 commits

Author SHA1 Message Date
7866af166e
db: use a single-byte marker prefix for encryption detection
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8330fcde76dc983569f7c6bb859b62e06a6a6964
2026-04-04 23:30:24 +03:00
d013901396
db: *warn* the users when encrypted entries cannot be decrypted
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1cfe9994b640cdf571007b5c52b0a2bc6a6a6964
2026-04-04 23:30:23 +03:00
d78cbd6741
docs: document new regex file and command options & encrypted db
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I552f0c891a5d3b3c8b4944189f9ee35b6a6a6964
2026-04-04 22:56:16 +03:00
5153b4d19c
db: allow encrypting database entries via age on the storage layer
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I942e2aeba2f079323a55bf4455937ddd6a6a6964
2026-04-04 22:56:04 +03:00
4 changed files with 912 additions and 30 deletions

645
Cargo.lock generated
View file

@ -17,6 +17,59 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 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 = "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",
"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",
"secrecy",
"sha2",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@ -286,12 +339,33 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[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]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.5.3" version = "0.5.3"
@ -393,6 +467,41 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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 = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.0" version = "4.6.0"
@ -508,6 +617,15 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@ -581,6 +699,32 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[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.117",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.23.0" version = "0.23.0"
@ -660,6 +804,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
] ]
[[package]] [[package]]
@ -867,6 +1012,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]] [[package]]
name = "filedescriptor" name = "filedescriptor"
version = "0.8.3" version = "0.8.3"
@ -878,6 +1029,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "find-crate"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
dependencies = [
"toml",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@ -902,6 +1062,50 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[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]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -1129,12 +1333,96 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[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]] [[package]]
name = "humantime" name = "humantime"
version = "2.3.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[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",
"syn 2.0.117",
"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.117",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.2.0" version = "2.2.0"
@ -1283,6 +1571,15 @@ dependencies = [
"rustversion", "rustversion",
] ]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "inquire" name = "inquire"
version = "0.9.4" version = "0.9.4"
@ -1309,6 +1606,31 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.2" version = "1.70.2"
@ -1709,6 +2031,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
@ -1779,6 +2107,16 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -1891,6 +2229,26 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "pin-project"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@ -1928,6 +2286,17 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[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]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.13.1" version = "1.13.1"
@ -1958,6 +2327,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@ -1977,6 +2355,28 @@ dependencies = [
"toml_edit", "toml_edit",
] ]
[[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.117",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@ -2031,6 +2431,18 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core", "rand_core",
] ]
@ -2039,6 +2451,9 @@ name = "rand_core"
version = "0.6.4" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]] [[package]]
name = "ratatui" name = "ratatui"
@ -2199,12 +2614,58 @@ dependencies = [
"sqlite-wasm-rs", "sqlite-wasm-rs",
] ]
[[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.117",
"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]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.27" version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" 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.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -2239,12 +2700,65 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"zeroize",
]
[[package]]
name = "self_cell"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
dependencies = [
"self_cell 1.2.2",
]
[[package]]
name = "self_cell"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.27" version = "1.0.27"
@ -2419,8 +2933,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
name = "stash-clipboard" name = "stash-clipboard"
version = "0.3.6" version = "0.3.6"
dependencies = [ dependencies = [
"age",
"arc-swap", "arc-swap",
"base64", "base64 0.22.1",
"blocking", "blocking",
"clap", "clap",
"clap-verbosity-flag", "clap-verbosity-flag",
@ -2485,6 +3000,12 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@ -2571,7 +3092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64 0.22.1",
"bitflags 2.11.0", "bitflags 2.11.0",
"fancy-regex", "fancy-regex",
"filedescriptor", "filedescriptor",
@ -2683,9 +3204,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"serde_core",
"zerovec", "zerovec",
] ]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.1.1+spec-1.1.0" version = "1.1.1+spec-1.1.0"
@ -2780,6 +3311,15 @@ dependencies = [
"petgraph", "petgraph",
] ]
[[package]]
name = "type-map"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
dependencies = [
"rustc-hash 2.1.2",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.19.0" version = "1.19.0"
@ -2803,6 +3343,25 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
@ -2838,6 +3397,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 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]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@ -2902,6 +3471,16 @@ dependencies = [
"utf8parse", "utf8parse",
] ]
[[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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.1+wasi-snapshot-preview1" version = "0.11.1+wasi-snapshot-preview1"
@ -3164,6 +3743,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@ -3435,6 +4023,18 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[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]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.2" version = "0.8.2"
@ -3519,6 +4119,26 @@ dependencies = [
"zvariant", "zvariant",
] ]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.7" version = "0.1.7"
@ -3540,6 +4160,26 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.4" version = "0.2.4"
@ -3557,6 +4197,7 @@ version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [ dependencies = [
"serde",
"yoke", "yoke",
"zerofrom", "zerofrom",
"zerovec-derive", "zerovec-derive",

View file

@ -14,6 +14,7 @@ name = "stash" # actual binary name for Nix, Cargo, etc.
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
age = "0.11.2"
arc-swap = { version = "1.9.0", optional = true } arc-swap = { version = "1.9.0", optional = true }
base64 = "0.22.1" base64 = "0.22.1"
blocking = "1.6.2" blocking = "1.6.2"

119
README.md
View file

@ -20,9 +20,10 @@
</div> </div>
<div align="center"> <div align="center">
Lightweight & feature-rich Wayland clipboard "manager" with fast persistent history and Lightweight & feature-rich Wayland clipboard "manager" with fast persistent
robust multi-media support. Stores and previews clipboard entries (text, images) history and robust multi-media support. Stores and previews clipboard
on the clipboard with a neat TUI and advanced scripting capabilities. entries (text, images) on the clipboard with a neat TUI and advanced scripting
capabilities.
</div> </div>
<div align="center"> <div align="center">
@ -52,6 +53,7 @@ with many features such as but not necessarily limited to:
- Drop-in replacement for `wl-clipboard` tools (`wl-copy` and `wl-paste`) - Drop-in replacement for `wl-clipboard` tools (`wl-copy` and `wl-paste`)
- Sensitive clipboard filtering via regex (see below) - Sensitive clipboard filtering via regex (see below)
- Sensitive clipboard filtering by application (see below) - Sensitive clipboard filtering by application (see below)
- Database encryption using age (see below)
on top of the existing features of Cliphist, which are as follows: on top of the existing features of Cliphist, which are as follows:
@ -357,21 +359,38 @@ sensitive pattern, using a regular expression. This is useful for preventing
accidental storage of secrets, passwords, or other sensitive data. You don't accidental storage of secrets, passwords, or other sensitive data. You don't
want sensitive data ending up in your persistent clipboard, right? want sensitive data ending up in your persistent clipboard, right?
The filter can be configured in one of three ways, as part of two separate The filter can be configured in several ways, as part of two separate features.
features.
#### Clipboard Filtering by Entry Regex #### Clipboard Filtering by Entry Regex
This can be configured in one of two ways. You can use the **environment This can be configured in one of several ways. You can use the **environment
variable** `STASTH_SENSITIVE_REGEX` to a valid regex pattern, and if the variable** `STASH_SENSITIVE_REGEX` to a valid regex pattern, and if the
clipboard text matches the regex it will not be stored. This can be used for clipboard text matches the regex it will not be stored. This can be used for
trivial secrets such as but not limited to GitHub tokens or secrets that follow trivial secrets such as but not limited to GitHub tokens or secrets that follow
a rule, e.g. a prefix. You would typically set this in your `~/.bashrc` or a rule, e.g. a prefix. You would typically set this in your `~/.bashrc` or
similar but in some cases this might be a security flaw. similar but in some cases this might be a security flaw.
The safer alternative to this is using **Systemd LoadCrediental**. If Stash is The _less-insecure_ [^1] alternative to this is using
running as a Systemd service, you can provide a regex pattern using a crediental `STASH_SENSITIVE_REGEX_FILE` to read the regex from a file path. This is useful
file. For example, add to your `stash.service`: for NixOS secrets managers like agenix or sops-nix.
```bash
# You can set this in your configuration with `environment.sessionVariables`
# or similar, pointing to the *decryption path*.
$ export STASH_SENSITIVE_REGEX_FILE=/run/secrets/stash/clipboard_filter
```
Or use `STASH_SENSITIVE_REGEX_COMMAND` to execute a command that returns the
regex pattern. This works well with password managers:
```bash
# Stash will execute the command and consume the result
$ export STASH_SENSITIVE_REGEX_COMMAND="pass show stash/clipboard-filter"
```
The safest option is using **Systemd LoadCredential**. If Stash is running as a
Systemd service, you can provide a regex pattern using a credential file. For
example, add to your `stash.service`:
```dosini ```dosini
LoadCredential=clipboard_filter:/etc/stash/clipboard_filter LoadCredential=clipboard_filter:/etc/stash/clipboard_filter
@ -382,9 +401,9 @@ quotes). This is done automatically in the
[vendored Systemd service](./contrib/stash.service). Remember to set the [vendored Systemd service](./contrib/stash.service). Remember to set the
appropriate file permissions if using this option. appropriate file permissions if using this option.
The service will check the credential file first, then the environment variable. The service will check the credential file first, then the command, then the
If a clipboard entry matches the regex, it will be skipped and a warning will be file path, then the environment variable. If a clipboard entry matches the
logged. regex, it will be skipped and a warning will be logged.
> [!TIP] > [!TIP]
> **Example regex to block common password patterns**: > **Example regex to block common password patterns**:
@ -393,17 +412,21 @@ logged.
> >
> For security reasons, you are recommended to use the regex only for generic > For security reasons, you are recommended to use the regex only for generic
> tokens that follow a specific rule, for example a generic prefix or suffix. > tokens that follow a specific rule, for example a generic prefix or suffix.
> For blocking entries from applications that emit sensitive data, such as
> password managers, filter by application class instead.
#### Clipboard Filtering by Application Class #### Clipboard Filtering by Application Class
Stash allows blocking an entry from the persistent history if it has been copied Stash allows blocking an entry from the persistent history if it has been copied
from certain applications. This depends on the `use-toplevel` feature flag and from certain applications. This depends on the `use-toplevel` feature flag and
uses the the `wlr-foreign-toplevel-management-v1` protocol for precise focus uses the the `wlr-foreign-toplevel-management-v1` protocol for precise focus
detection. While this feature flag is enabled (the default) you may use detection.
`--excluded-apps` in, e.g., `stash watch` or set the `STASH_EXCLUDED_APPS`
environment variable to block entries from persisting in the database if they While this feature flag is enabled (the default) you may use `--excluded-apps`
are coming from your password manager for example. The entry is still copied to in, e.g., `stash watch` or set the `STASH_EXCLUDED_APPS` environment variable to
the clipboard, but it will never be put inside the database. block entries from persisting in the database if they are coming from your
password manager for example. The entry is still copied to the clipboard, but it
will never be put inside the database.
This is a more robust alternative to using the regex method above, since you This is a more robust alternative to using the regex method above, since you
likely do not want to catch your passwords with a regex. Simply pass your likely do not want to catch your passwords with a regex. Simply pass your
@ -516,6 +539,66 @@ figured out something new, e.g. a neat shell trick, feel free to add it here!
the packagers. While building from source, you may link the packagers. While building from source, you may link
`target/release/stash` manually. `target/release/stash` manually.
### Database Encryption
Stash supports encrypting clipboard entries at rest using the
[age](https://age-encryption.org/) encryption format. This provides protection
for sensitive data stored in the database.
> [!WARNING]
> If you enable encryption after already having entries in the database, the
> existing entries will remain unencrypted. Only new entries added after
> configuring encryption will be encrypted. To encrypt existing entries, you
> would need to wipe the database and re-copy your clipboard contents.
Encryption is **opt-in** and only takes effect when a passphrase is configured.
When one _is_ configured, all new clipboard entries are encrypted before being
stored in the database. Entries without encryption configured are stored in
plaintext instead.
Encrypted entries are detected by the `age-encryption.org/v1` header and
decrypted automatically on retrieval. Search operations (e.g.,
`stash delete --type query`) decrypt entries on-the-fly to match queries;
entries that fail decryption are skipped with a warning
#### Configuration
Encryption is configured using a passphrase, which can be provided in one of
several ways. The simplest is using the **environment variable**
`STASH_ENCRYPTION_PASSPHRASE`:
```bash
export STASH_ENCRYPTION_PASSPHRASE="your-secure-passphrase"
```
Alternatively, you can use `STASH_ENCRYPTION_PASSPHRASE_FILE` to read the
passphrase from a file path. This is useful for NixOS secrets managers:
```bash
export STASH_ENCRYPTION_PASSPHRASE_FILE=/run/secrets/stash/encryption_passphrase
```
Or use `STASH_ENCRYPTION_PASSPHRASE_COMMAND` to execute a command that returns
the passphrase. This works well with password managers:
```bash
export STASH_ENCRYPTION_PASSPHRASE_COMMAND="pass show stash/encryption-key"
```
The safest option is using **Systemd LoadCredential**. If Stash is running as a
Systemd service, you can provide the passphrase using a credential file:
```dosini
LoadCredential=stash_encryption_passphrase:/etc/stash/encryption_passphrase
```
This is done automatically in the
[vendored Systemd service](./contrib/stash.service).
> [!TIP]
> When using encryption, make sure to back up your passphrase. Without it, you
> will not be able to recover encrypted entries.
### Entry Expiration ### Entry Expiration
Stash supports time-to-live (TTL) for clipboard entries. When an entry's Stash supports time-to-live (TTL) for clipboard entries. When an entry's

View file

@ -215,12 +215,18 @@ pub enum StashError {
QueryDelete(Box<str>), QueryDelete(Box<str>),
#[error("Failed to delete entry with id {0}: {1}")] #[error("Failed to delete entry with id {0}: {1}")]
DeleteEntry(i64, Box<str>), DeleteEntry(i64, Box<str>),
#[error("Encryption error: {0}")]
Encryption(Box<str>),
#[error("Decryption error: {0}")]
Decryption(Box<str>),
} }
pub trait ClipboardDb { pub trait ClipboardDb {
/// Store a new clipboard entry. /// Store a new clipboard entry.
/// ///
/// # Arguments /// # Arguments
///
/// * `input` - Reader for the clipboard content /// * `input` - Reader for the clipboard content
/// * `max_dedupe_search` - Maximum number of recent entries to check for /// * `max_dedupe_search` - Maximum number of recent entries to check for
/// duplicates /// duplicates
@ -595,11 +601,24 @@ impl SqliteClipboardDb {
.get(2) .get(2)
.map_err(|e| StashError::ListDecode(e.to_string().into()))?; .map_err(|e| StashError::ListDecode(e.to_string().into()))?;
let decrypted_contents = if contents.starts_with(b"age-encryption.org/v1")
{
match decrypt_data(&contents) {
Ok(decrypted) => decrypted,
Err(e) => {
warn!("skipping entry {id}: decryption failed: {e}");
continue;
},
}
} else {
contents
};
let contents_str = match mime.as_deref() { let contents_str = match mime.as_deref() {
Some(m) if m.starts_with("text/") || m == "application/json" => { Some(m) if m.starts_with("text/") || m == "application/json" => {
String::from_utf8_lossy(&contents).into_owned() String::from_utf8_lossy(&decrypted_contents).into_owned()
}, },
_ => base64::prelude::BASE64_STANDARD.encode(&contents), _ => base64::prelude::BASE64_STANDARD.encode(&decrypted_contents),
}; };
entries.push(serde_json::json!({ entries.push(serde_json::json!({
"id": id, "id": id,
@ -689,13 +708,22 @@ impl ClipboardDb for SqliteClipboardDb {
None => None, None => None,
}; };
let encrypted_buf = if load_encryption_passphrase().is_some() {
Some(encrypt_data(&buf)?)
} else {
debug!("No encryption passphrase configured, storing entry unencrypted");
None
};
let contents_to_store = encrypted_buf.unwrap_or(buf);
self self
.conn .conn
.execute( .execute(
"INSERT INTO clipboard (contents, mime, content_hash, last_accessed, \ "INSERT INTO clipboard (contents, mime, content_hash, last_accessed, \
mime_types) VALUES (?1, ?2, ?3, ?4, ?5)", mime_types) VALUES (?1, ?2, ?3, ?4, ?5)",
params![ params![
buf, contents_to_store,
mime, mime,
content_hash, content_hash,
std::time::SystemTime::now() std::time::SystemTime::now()
@ -837,8 +865,20 @@ impl ClipboardDb for SqliteClipboardDb {
let mime: Option<String> = row let mime: Option<String> = row
.get(2) .get(2)
.map_err(|e| StashError::ListDecode(e.to_string().into()))?; .map_err(|e| StashError::ListDecode(e.to_string().into()))?;
let preview_contents = if contents.starts_with(&[0x01u8]) {
match decrypt_data(&contents) {
Ok(decrypted) => decrypted,
Err(e) => {
warn!("skipping entry {id}: decryption failed: {e}");
continue;
},
}
} else {
contents
};
let preview = preview_entry(&contents, mime.as_deref(), preview_width); let preview =
preview_entry(&preview_contents, mime.as_deref(), preview_width);
if writeln!(out, "{id}\t{preview}").is_ok() { if writeln!(out, "{id}\t{preview}").is_ok() {
listed += 1; listed += 1;
} }
@ -872,8 +912,15 @@ impl ClipboardDb for SqliteClipboardDb {
|row| Ok((row.get(0)?, row.get(1)?)), |row| Ok((row.get(0)?, row.get(1)?)),
) )
.map_err(|e| StashError::DecodeGet(e.to_string().into()))?; .map_err(|e| StashError::DecodeGet(e.to_string().into()))?;
let decrypted_contents = if contents.starts_with(&[0x01u8]) {
decrypt_data(&contents)?
} else {
contents
};
out out
.write_all(&contents) .write_all(&decrypted_contents)
.map_err(|e| StashError::DecodeWrite(e.to_string().into()))?; .map_err(|e| StashError::DecodeWrite(e.to_string().into()))?;
log::info!("decoded entry with id {id}"); log::info!("decoded entry with id {id}");
Ok(()) Ok(())
@ -898,7 +945,23 @@ impl ClipboardDb for SqliteClipboardDb {
let contents: Vec<u8> = row let contents: Vec<u8> = row
.get(1) .get(1)
.map_err(|e| StashError::QueryDelete(e.to_string().into()))?; .map_err(|e| StashError::QueryDelete(e.to_string().into()))?;
if contents.windows(query.len()).any(|w| w == query.as_bytes()) {
let searchable_contents = if contents.starts_with(&[0x01u8]) {
match decrypt_data(&contents) {
Ok(decrypted) => decrypted,
Err(e) => {
warn!("skipping entry {id}: decryption failed: {e}");
continue;
},
}
} else {
contents
};
if searchable_contents
.windows(query.len())
.any(|w| w == query.as_bytes())
{
self self
.conn .conn
.execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .execute("DELETE FROM clipboard WHERE id = ?1", params![id])
@ -963,7 +1026,13 @@ impl ClipboardDb for SqliteClipboardDb {
} }
} }
Ok((id, contents, mime)) let decrypted_contents = if contents.starts_with(&[0x01u8]) {
decrypt_data(&contents)?
} else {
contents
};
Ok((id, decrypted_contents, mime))
} }
} }
@ -1038,7 +1107,20 @@ impl SqliteClipboardDb {
let mime: Option<String> = row let mime: Option<String> = row
.get(2) .get(2)
.map_err(|e| StashError::ListDecode(e.to_string().into()))?; .map_err(|e| StashError::ListDecode(e.to_string().into()))?;
let preview = preview_entry(&contents, mime.as_deref(), preview_width); let decrypted_contents = if contents.starts_with(&[0x01u8]) {
match decrypt_data(&contents) {
Ok(decrypted) => decrypted,
Err(e) => {
warn!("skipping entry {id}: decryption failed: {e}");
continue;
},
}
} else {
contents
};
let preview =
preview_entry(&decrypted_contents, mime.as_deref(), preview_width);
let mime_str = mime.unwrap_or_default(); let mime_str = mime.unwrap_or_default();
window.push((id, preview, mime_str)); window.push((id, preview, mime_str));
} }
@ -1155,10 +1237,23 @@ impl SqliteClipboardDb {
/// changes made after daemon startup. Regex compilation is cached by /// changes made after daemon startup. Regex compilation is cached by
/// pattern to avoid recompilation. /// pattern to avoid recompilation.
fn load_sensitive_regex() -> Option<Regex> { fn load_sensitive_regex() -> Option<Regex> {
use std::process::Command;
// Get the current pattern from env vars // Get the current pattern from env vars
let pattern = if let Ok(regex_path) = env::var("CREDENTIALS_DIRECTORY") { let pattern = if let Ok(cred_dir) = env::var("CREDENTIALS_DIRECTORY") {
let file = format!("{regex_path}/clipboard_filter"); let file = format!("{cred_dir}/clipboard_filter");
fs::read_to_string(&file).ok().map(|s| s.trim().to_string()) fs::read_to_string(&file).ok().map(|s| s.trim().to_string())
} else if let Ok(cmd) = env::var("STASH_SENSITIVE_REGEX_COMMAND") {
Command::new("sh")
.args(["-c", &cmd])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
} else if let Ok(file_path) = env::var("STASH_SENSITIVE_REGEX_FILE") {
fs::read_to_string(&file_path)
.ok()
.map(|s| s.trim().to_string())
} else { } else {
env::var("STASH_SENSITIVE_REGEX").ok() env::var("STASH_SENSITIVE_REGEX").ok()
}?; }?;
@ -1185,6 +1280,68 @@ fn load_sensitive_regex() -> Option<Regex> {
}) })
} }
fn load_encryption_passphrase() -> Option<age::secrecy::SecretString> {
use std::process::Command;
static PASSPHRASE_CACHE: OnceLock<age::secrecy::SecretString> =
OnceLock::new();
if let Some(cached) = PASSPHRASE_CACHE.get() {
return Some(cached.clone());
}
let passphrase = if let Ok(cred_dir) = env::var("CREDENTIALS_DIRECTORY") {
let file = format!("{cred_dir}/stash_encryption_passphrase");
fs::read_to_string(&file).ok().map(|s| s.trim().to_owned())
} else if let Ok(cmd) = env::var("STASH_ENCRYPTION_PASSPHRASE_COMMAND") {
Command::new("sh")
.args(["-c", &cmd])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())
} else if let Ok(file_path) = env::var("STASH_ENCRYPTION_PASSPHRASE_FILE") {
fs::read_to_string(&file_path)
.ok()
.map(|s| s.trim().to_owned())
} else {
env::var("STASH_ENCRYPTION_PASSPHRASE").ok()
}?;
let secret = age::secrecy::SecretString::from(passphrase);
let _ = PASSPHRASE_CACHE.set(secret.clone());
Some(secret)
}
fn encrypt_data(data: &[u8]) -> Result<Vec<u8>, StashError> {
let passphrase = load_encryption_passphrase().ok_or_else(|| {
StashError::Encryption("No encryption passphrase configured".into())
})?;
let recipient = age::scrypt::Recipient::new(passphrase);
let encrypted = age::encrypt(&recipient, data)
.map_err(|e| StashError::Encryption(e.to_string().into()))?;
// Prepend marker byte to identify our encrypted data
let mut result = Vec::with_capacity(1 + encrypted.len());
result.push(0x01u8);
result.extend_from_slice(&encrypted);
Ok(result)
}
fn decrypt_data(encrypted: &[u8]) -> Result<Vec<u8>, StashError> {
let passphrase = load_encryption_passphrase().ok_or_else(|| {
StashError::Decryption("No encryption passphrase configured".into())
})?;
// Strip our marker byte if present
let data_to_decrypt = encrypted.strip_prefix(&[0x01u8]).unwrap_or(encrypted);
let identity = age::scrypt::Identity::new(passphrase);
let decrypted = age::decrypt(&identity, data_to_decrypt)
.map_err(|e| StashError::Decryption(e.to_string().into()))?;
Ok(decrypted)
}
pub fn extract_id(input: &str) -> Result<i64, &'static str> { pub fn extract_id(input: &str) -> Result<i64, &'static str> {
let id_str = input.split('\t').next().unwrap_or(""); let id_str = input.split('\t').next().unwrap_or("");
id_str.parse().map_err(|_| "invalid id") id_str.parse().map_err(|_| "invalid id")