oo7-0.3.3/.cargo_vcs_info.json0000644000000001440000000000100115420ustar { "git": { "sha1": "bfe550f3bdeb1ad6649e5b688d107ef7c9dd6d59" }, "path_in_vcs": "client" }oo7-0.3.3/Cargo.lock0000644000001147130000000000100075250ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", "zeroize", ] [[package]] name = "async-broadcast" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "slab", ] [[package]] name = "async-fs" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock", "blocking", "futures-lite", ] [[package]] name = "async-io" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "tracing", "windows-sys 0.52.0", ] [[package]] name = "async-lock" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-net" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ "async-io", "blocking", "futures-lite", ] [[package]] name = "async-process" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix", "tracing", "windows-sys 0.52.0", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "async-signal" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", "windows-sys 0.52.0", ] [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ "async-channel", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ "cipher", ] [[package]] name = "cc" version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", "zeroize", ] [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "rand_core", "typenum", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "endi" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "event-listener" version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "inout" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ "block-padding", "generic-array", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" dependencies = [ "spin", ] [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", "digest", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "nix" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", "memoffset", ] [[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-bigint" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-bigint-dig" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ "byteorder", "lazy_static", "libm", "num-integer", "num-iter", "num-traits", "rand", "serde", "smallvec", "zeroize", ] [[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-bigint", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi 0.3.9", "libc", ] [[package]] name = "object" version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oo7" version = "0.3.3" dependencies = [ "aes", "async-fs", "async-io", "async-lock", "async-net", "blocking", "cbc", "cipher", "digest", "endi", "futures-lite", "futures-util", "hkdf", "hmac", "md-5", "num", "num-bigint-dig", "openssl", "pbkdf2", "rand", "serde", "sha2", "subtle", "tempfile", "tokio", "tracing", "zbus", "zeroize", "zvariant", ] [[package]] name = "openssl" version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-sys" version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "parking" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", "hmac", ] [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", "windows-sys 0.52.0", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "serde" version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_repr" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys 0.52.0", ] [[package]] name = "tokio" version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "tracing", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "toml_datetime" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" [[package]] name = "toml_edit" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset", "tempfile", "winapi", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.5", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ "windows_aarch64_gnullvm 0.52.5", "windows_aarch64_msvc 0.52.5", "windows_i686_gnu 0.52.5", "windows_i686_gnullvm", "windows_i686_msvc 0.52.5", "windows_x86_64_gnu 0.52.5", "windows_x86_64_gnullvm 0.52.5", "windows_x86_64_msvc 0.52.5", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] name = "windows_i686_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] name = "xdg-home" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "zbus" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23915fcb26e7a9a9dc05fd93a9870d336d6d032cd7e8cebf1c5c37666489fdd5" dependencies = [ "async-broadcast", "async-executor", "async-fs", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", "event-listener", "futures-core", "futures-sink", "futures-util", "hex", "nix", "ordered-stream", "rand", "serde", "serde_repr", "sha1", "static_assertions", "tokio", "tracing", "uds_windows", "windows-sys 0.52.0", "xdg-home", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02bcca0b586d2f8589da32347b4784ba424c4891ed86aa5b50d5e88f6b2c4f5d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zvariant_utils", ] [[package]] name = "zbus_names" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", "zvariant", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zvariant" version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa6d31a02fbfb602bfde791de7fedeb9c2c18115b3d00f3a36e489f46ffbbc7" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "642bf1b6b6d527988b3e8193d20969d53700a36eac734d21ae6639db168701c8" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" dependencies = [ "proc-macro2", "quote", "syn", ] oo7-0.3.3/Cargo.toml0000644000000076410000000000100075510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.75" name = "oo7" version = "0.3.3" authors = [ "Bilal Elmoussaoui", "Sophie Herold", "Maximiliano Sandoval", ] exclude = ["org.freedesktop.Secrets.xml"] description = "James Bond went on a new mission and this time as a Secret Service provider" homepage = "https://github.com/bilelmoussaoui/oo7" readme = "README.md" keywords = [ "keyring", "secret", "service", "portal", "keychain", ] categories = [ "os::linux-apis", "os", "api-bindings", ] license = "MIT" repository = "https://github.com/bilelmoussaoui/oo7" [package.metadata.docs.rs] features = ["unstable"] rustc-args = [ "--cfg", "docsrs", ] rustdoc-args = [ "--cfg", "docsrs", "--generate-link-to-definition", ] [[example]] name = "basic" path = "examples/basic.rs" required-features = ["tokio"] [[example]] name = "basic_2" path = "examples/basic_2.rs" required-features = ["tokio"] [[example]] name = "dbus_service" path = "examples/dbus_service.rs" required-features = ["tokio"] [dependencies.aes] version = "0.8" features = ["zeroize"] optional = true [dependencies.async-fs] version = "2.1.0" optional = true [dependencies.async-io] version = "2.2.2" optional = true [dependencies.async-lock] version = "3.2.0" optional = true [dependencies.async-net] version = "2.0.0" optional = true [dependencies.blocking] version = "1.5.1" optional = true [dependencies.cbc] version = "0.1" features = ["zeroize"] optional = true [dependencies.cipher] version = "0.4" features = [ "rand_core", "zeroize", ] optional = true [dependencies.digest] version = "0.10" optional = true [dependencies.endi] version = "1.1" [dependencies.futures-lite] version = "2.1" optional = true [dependencies.futures-util] version = "0.3" [dependencies.hkdf] version = "0.12" optional = true [dependencies.hmac] version = "0.12" optional = true [dependencies.md-5] version = "0.10" optional = true [dependencies.num] version = "0.4.0" [dependencies.num-bigint-dig] version = "0.8" features = ["zeroize"] [dependencies.openssl] version = "0.10" optional = true [dependencies.pbkdf2] version = "0.12" optional = true [dependencies.rand] version = "0.8" default-features = false [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.sha2] version = "0.10" optional = true [dependencies.subtle] version = "2.5" optional = true [dependencies.tokio] version = "1.17" features = [ "sync", "fs", "io-util", ] optional = true default-features = false [dependencies.tracing] version = "0.1" optional = true [dependencies.zbus] version = "4.0" default-features = false [dependencies.zeroize] version = "1" features = ["zeroize_derive"] [dependencies.zvariant] version = "4.0" features = ["gvariant"] default-features = false [dev-dependencies.tempfile] version = "3.10" [dev-dependencies.tokio] version = "1.17" features = [ "macros", "rt-multi-thread", ] default-features = false [features] async-std = [ "zbus/async-io", "dep:async-fs", "dep:async-io", "dep:async-lock", "dep:async-net", "dep:blocking", "dep:futures-lite", ] default = [ "local_tests", "async-std", "native_crypto", ] local_tests = [] native_crypto = [ "dep:aes", "dep:cbc", "dep:cipher", "dep:digest", "dep:hkdf", "dep:hmac", "dep:md-5", "dep:pbkdf2", "dep:sha2", "dep:subtle", ] openssl_crypto = ["dep:openssl"] tokio = [ "zbus/tokio", "dep:tokio", ] unstable = [] oo7-0.3.3/Cargo.toml.orig000064400000000000000000000056771046102023000132410ustar 00000000000000[package] name = "oo7" description = "James Bond went on a new mission and this time as a Secret Service provider" categories.workspace = true keywords.workspace = true authors.workspace = true edition.workspace = true exclude.workspace = true homepage.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true version.workspace = true [dependencies] aes = { version = "0.8", features = ["zeroize"], optional = true } async-fs = { version = "2.1.0", optional = true } async-io = { version = "2.2.2", optional = true } async-lock = { version = "3.2.0", optional = true } async-net = { version = "2.0.0", optional = true } blocking = { version = "1.5.1", optional = true } cbc = { version = "0.1", features = ["zeroize"], optional = true } cipher = { version = "0.4", features = [ "rand_core", "zeroize", ], optional = true } digest = { version = "0.10", optional = true } endi.workspace = true futures-lite = { workspace = true, optional = true } futures-util.workspace = true hkdf = { version = "0.12", optional = true } hmac = { version = "0.12", optional = true } md-5 = { version = "0.10", optional = true } num = "0.4.0" num-bigint-dig = { version = "0.8", features = ["zeroize"] } openssl = { version = "0.10", optional = true } pbkdf2 = { version = "0.12", optional = true } rand = { version = "0.8", default-features = false } serde.workspace = true sha2 = { version = "0.10", optional = true } subtle = { version = "2.5", optional = true } tokio = { workspace = true, features = [ "sync", "fs", "io-util", ], optional = true, default-features = false } tracing = { workspace = true, optional = true } zbus.workspace = true zvariant.workspace = true zeroize.workspace = true [dev-dependencies] tempfile.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [features] default = ["local_tests", "async-std", "native_crypto"] # Some tests requires a prompt to be displayed, which can't be easily # handled in CI unless we write a mock service. The feature allows to disabling those tests in CI local_tests = [] # Enables unstable low-level API unstable = [] async-std = [ "zbus/async-io", "dep:async-fs", "dep:async-io", "dep:async-lock", "dep:async-net", "dep:blocking", "dep:futures-lite", ] tokio = ["zbus/tokio", "dep:tokio"] native_crypto = [ "dep:aes", "dep:cbc", "dep:cipher", "dep:digest", "dep:hkdf", "dep:hmac", "dep:md-5", "dep:pbkdf2", "dep:sha2", "dep:subtle", ] openssl_crypto = ["dep:openssl"] [package.metadata.docs.rs] features = ["unstable"] rustc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] [[example]] name = "basic" path = "examples/basic.rs" required-features = ["tokio"] [[example]] name = "basic_2" path = "examples/basic_2.rs" required-features = ["tokio"] [[example]] name = "dbus_service" path = "examples/dbus_service.rs" required-features = ["tokio"] oo7-0.3.3/LICENSE000064400000000000000000000021261046102023000113410ustar 00000000000000MIT License Copyright (c) 2022 Bilal Elmoussaoui, Sophie Herold, Maximiliano Sandoval Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.oo7-0.3.3/README.md000064400000000000000000000121441046102023000116140ustar 00000000000000# OO7 [![docs](https://docs.rs/oo7/badge.svg)](https://docs.rs/oo7/) [![crates.io](https://img.shields.io/crates/v/oo7)](https://crates.io/crates/oo7) ![CI](https://github.com/bilelmoussaoui/oo7/workflows/CI/badge.svg) James Bond went on a new mission and this time as a [Secret Service provider](https://specifications.freedesktop.org/secret-service/latest/). The library consists of two modules: - An implementation of the Secret Service specifications using [zbus](https://lib.rs/zbus). Which sends the secrets to a DBus implementation of the `org.freedesktop.Secrets` interface that stores them somewhere safe. - A file backend using the `org.freedesktop.portal.Secrets` portal to retrieve the service's key to encrypt the file with. The file format is compatible with [libsecret](https://gitlab.gnome.org/GNOME/libsecret/). Sandboxed applications should prefer using the file backend as it doesn't expose the application secrets to other sandboxed applications if they can talk to the `org.freedesktop.Secrets` service. The library provides helper methods to store and retrieve secrets and uses either the DBus interface or the file backend based on whether the application is sandboxed or not. ## Goals - Async only API - Ease to use - Integration with the [Secret portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html) if sandboxed - Provide API to migrate from host secrets to sandboxed ones ## Examples ### Basic usage ```rust,no_run use std::collections::HashMap; async fn run() -> oo7::Result<()> { let keyring = oo7::Keyring::new().await?; let attributes = HashMap::from([("attribute", "attribute_value")]); // Store a secret keyring .create_item("Item Label", &attributes, b"secret", true).await?; // Find a stored secret let items = keyring.search_items(&attributes).await?; // Delete a stored secret keyring.delete(&attributes).await?; // Unlock the collection if the Secret Service is used keyring.unlock().await?; // Lock the collection if the Secret Service is used keyring.lock().await?; Ok(()) } ``` If your application makes heavy usage of the keyring like a password manager. You could store an instance of the `Keyring` in a `OnceCell` / `OnceLock` / `Lazy` ```rust,ignore use std::sync::OnceLock; use std::collections::HashMap; static KEYRING: OnceLock = OnceLock::new(); fn main() { // SOME_RUNTIME could be a tokio/async-std/glib runtime SOME_RUNTIME.block_on(async { let keyring = oo7::Keyring::new() .await .expect("Failed to start Secret Service"); KEYRING.set(keyring); }); // Then to use it SOME_RUNTIME.spawn(async { let items = KEYRING .get() .unwrap() .search_items(&HashMap::from([("attribute", "attribute_value")])) .await; }); } ``` ### Migrating your secrets to the file backend The library also comes with API to migrate your secrets from the host Secret Service to the sandboxed file backend. Note that the items are removed from the host keyring if they are migrated successfully. ```rust,ignore use std::collections::HashMap; // SOME_RUNTIME could be a tokio/async-std/glib runtime SOME_RUNTIME.block_on(async { match oo7::migrate(vec![HashMap::from([("attribute", "attribute_value")])], true).await { Ok(_) => { // Store somewhere the migration happened, to avoid re-doing it at every startup } Err(err) => log::error!("Failed to migrate secrets {err}"), } }); ``` ## Optional features | Feature | Description | Default | | --- | ----------- | ------ | | `tracing` | Record various debug information using the `tracing` library | No | | `async-std` | Use `async-std` APIs for IO/filesystem operations | Yes | | `tokio` | Use `tokio` APIs for IO/Filesystem operations | No | | `native_crypto` | Use Rust Crypto crates for cryptographic primitives | Yes | | `openssl_crypto` | Use `openssl` crate for cryptographic primitives | No | | `unstable` | Unlock internal APIs | No | ## How does it compare to other libraries? - [libsecret-rs](https://gitlab.gnome.org/World/Rust/libsecret-rs) provides Rust bindings of the C library [libsecret](https://gitlab.gnome.org/GNOME/libsecret/). The current main pain point with it is that it does assume things for you so it will either use the host or the sandbox file-based keyring which makes migrating your secrets to inside the sandbox a probably impossible task. There are also issues like that makes it not usable inside the Flatpak sandbox. - [secret-service-rs](https://github.com/hwchen/secret-service-rs/) uses [zbus](https://lib.rs/zbus) internally as well but does provide a sync only API, hasn't seen an update in a while, doesn't integrate with [Secret portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html) if sandboxed. ## License The project is released under the MIT license. ## Credits - [secret-service-rs](https://github.com/hwchen/secret-service-rs/) for the encrypted Secret Service implementation. oo7-0.3.3/examples/basic.rs000064400000000000000000000010421046102023000135750ustar 00000000000000use std::collections::HashMap; use oo7::Keyring; #[tokio::main] async fn main() -> oo7::Result<()> { let keyring = Keyring::new().await?; let attributes = HashMap::from([("attr", "value")]); keyring .create_item("Some Label", &attributes, b"secret", true) .await?; let items = keyring.search_items(&attributes).await?; for item in items { println!("{}", item.label().await?); println!("{:#?}", item.attributes().await?); println!("{:#?}", item.secret().await?); } Ok(()) } oo7-0.3.3/examples/basic_2.rs000064400000000000000000000012771046102023000140300ustar 00000000000000use std::{collections::HashMap, sync::OnceLock}; use oo7::Keyring; static KEYRING: OnceLock = OnceLock::new(); #[tokio::main] async fn main() -> oo7::Result<()> { let keyring = Keyring::new().await?; KEYRING.set(keyring).unwrap(); let attributes = HashMap::from([("attr", "value")]); KEYRING .get() .unwrap() .create_item("Some Label", &attributes, b"secret", true) .await?; let items = KEYRING.get().unwrap().search_items(&attributes).await?; for item in items { println!("{}", item.label().await?); println!("{:#?}", item.attributes().await?); println!("{:#?}", item.secret().await?); } Ok(()) } oo7-0.3.3/examples/dbus_service.rs000064400000000000000000000012331046102023000151730ustar 00000000000000use std::collections::HashMap; use oo7::dbus::Service; #[tokio::main] async fn main() -> oo7::Result<()> { let service = Service::new().await?; let attributes = HashMap::from([("type", "token")]); let collection = service.default_collection().await?; let items = collection.search_items(&attributes).await?; for item in items { println!("{}", item.label().await?); println!("{}", item.is_locked().await?); println!("{:#?}", item.created().await?); println!("{:#?}", item.modified().await?); println!("{:#?}", item.attributes().await?); println!("{:#?}", item.secret().await?); } Ok(()) } oo7-0.3.3/fixtures/default.keyring000064400000000000000000000003461046102023000152250ustar 00000000000000GnomeKeyring  ])r=4it} $OkOfxdg:schema"U4t?Btib )$氦 ,Q r1kg]S!|ase3Z n; type DecAlg = cbc::Decryptor; type MacAlg = hmac::Hmac; pub(crate) fn encrypt(data: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>) -> Vec { let mut blob = vec![0; data.as_ref().len() + EncAlg::block_size()]; // Unwrapping since adding `CIPHER_BLOCK_SIZE` to array is enough space for // PKCS7 let encrypted_len = EncAlg::new_from_slices(key.as_ref(), iv.as_ref()) .expect("Invalid key length") .encrypt_padded_b2b_mut::(data.as_ref(), &mut blob) .unwrap() .len(); blob.truncate(encrypted_len); blob } pub(crate) fn decrypt( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Zeroizing> { let mut data = blob.as_ref().to_vec(); DecAlg::new_from_slices(key.as_ref(), iv.as_ref()) .expect("Invalid key length") .decrypt_padded_mut::(&mut data) .unwrap() .to_vec() .into() } pub(crate) fn decrypt_no_padding( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Zeroizing> { let mut data = blob.as_ref().to_vec(); DecAlg::new_from_slices(key.as_ref(), iv.as_ref()) .expect("Invalid key length") .decrypt_padded_mut::(&mut data) .unwrap() .to_vec() .into() } pub(crate) fn iv_len() -> usize { DecAlg::iv_size() } pub(crate) fn generate_private_key() -> Zeroizing> { let generic_array = EncAlg::generate_key(cipher::rand_core::OsRng); Zeroizing::new(generic_array.to_vec()) } pub(crate) fn generate_public_key(private_key: impl AsRef<[u8]>) -> Vec { let private_key_uint = BigUint::from_bytes_be(private_key.as_ref()); let dh_generator = { static DH_GENERATOR: OnceLock = OnceLock::new(); DH_GENERATOR.get_or_init(|| BigUint::from_u64(0x2).unwrap()) }; let public_key_uint = powm(dh_generator, private_key_uint); public_key_uint.to_bytes_be() } pub(crate) fn generate_aes_key( private_key: impl AsRef<[u8]>, server_public_key: impl AsRef<[u8]>, ) -> Zeroizing> { let server_public_key_uint = BigUint::from_bytes_be(server_public_key.as_ref()); let private_key_uint = BigUint::from_bytes_be(private_key.as_ref()); let common_secret = powm(&server_public_key_uint, private_key_uint); let mut common_secret_bytes = common_secret.to_bytes_be(); let mut common_secret_padded = vec![0; 128 - common_secret_bytes.len()]; // inefficient, but ok for now common_secret_padded.append(&mut common_secret_bytes); // hkdf // input_keying_material let ikm = common_secret_padded; let salt = None; let info = []; // output keying material let mut okm = Zeroizing::new(vec![0; 16]); let (_, hk) = Hkdf::::extract(salt, &ikm); hk.expand(&info, okm.as_mut()) .expect("hkdf expand should never fail"); okm } pub(crate) fn generate_iv() -> Vec { EncAlg::generate_iv(cipher::rand_core::OsRng).to_vec() } pub(crate) fn mac_len() -> usize { MacAlg::output_size() } pub(crate) fn compute_mac(data: impl AsRef<[u8]>, key: &Key) -> Vec { let mut mac = MacAlg::new_from_slice(key.as_ref()).unwrap(); mac.update(data.as_ref()); mac.finalize().into_bytes().to_vec() } pub(crate) fn verify_mac(data: impl AsRef<[u8]>, key: &Key, expected: impl AsRef<[u8]>) -> bool { let mut mac = MacAlg::new_from_slice(key.as_ref()).unwrap(); mac.update(data.as_ref()); mac.verify_slice(expected.as_ref()).is_ok() } pub(crate) fn verify_checksum_md5(digest: impl AsRef<[u8]>, content: impl AsRef<[u8]>) -> bool { let mut hasher = Md5::new(); hasher.update(content.as_ref()); hasher.finalize_fixed().ct_eq(digest.as_ref()).into() } pub(crate) fn derive_key( secret: impl AsRef<[u8]>, key_strength: Result<(), portal::WeakKeyError>, salt: impl AsRef<[u8]>, iteration_count: usize, ) -> Key { let mut key = Key::new_with_strength(vec![0; EncAlg::block_size()], key_strength); pbkdf2::pbkdf2::>( secret.as_ref(), salt.as_ref(), iteration_count.try_into().unwrap(), key.as_mut(), ) .expect("HMAC can be initialized with any key length"); key } pub(crate) fn legacy_derive_key_and_iv( secret: impl AsRef<[u8]>, key_strength: Result<(), portal::WeakKeyError>, salt: impl AsRef<[u8]>, iteration_count: usize, ) -> (Key, Vec) { let mut buffer = vec![0; EncAlg::key_size() + EncAlg::iv_size()]; let mut hasher = Sha256::new(); let mut digest_buffer = vec![0; ::output_size()]; let digest = Output::::from_mut_slice(digest_buffer.as_mut_slice()); let mut pos = 0usize; loop { hasher.update(secret.as_ref()); hasher.update(salt.as_ref()); hasher.finalize_into_reset(digest); for _ in 1..iteration_count { hasher.update(&digest); hasher.finalize_into_reset(digest); } let to_read = usize::min(digest.len(), buffer.len() - pos); buffer[pos..].copy_from_slice(&digest[..to_read]); pos += to_read; if pos == buffer.len() { break; } hasher.update(&digest); } let iv = buffer.split_off(EncAlg::key_size()); (Key::new_with_strength(buffer, key_strength), iv) } /// from https://github.com/plietar/librespot/blob/master/core/src/util/mod.rs#L53 fn powm(base: &BigUint, mut exp: BigUint) -> BigUint { let modulus = { // for key exchange static DH_PRIME: OnceLock = OnceLock::new(); DH_PRIME.get_or_init(|| { BigUint::from_bytes_be(&[ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68, 0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, 0x29, 0x02, 0x4E, 0x08, 0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A, 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, 0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, 0xC2, 0x45, 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9, 0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, 0xEE, 0x38, 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6, 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE6, 0x53, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, ]) }) }; let mut base = base.clone(); let mut result: BigUint = One::one(); while !exp.is_zero() { if exp.is_odd() { result = result.mul(&base).rem(modulus); } exp = exp.shr(1); base = (&base).mul(&base).rem(modulus); } exp.zeroize(); result } oo7-0.3.3/src/crypto/openssl.rs000064400000000000000000000146571046102023000145100ustar 00000000000000use openssl::{ bn::BigNum, dh::Dh, hash::{hash, Hasher, MessageDigest}, md::Md, memcmp, nid::Nid, pkcs5::pbkdf2_hmac, pkey::{Id, PKey}, pkey_ctx::PkeyCtx, rand::rand_bytes, sign::Signer, symm::{Cipher, Crypter, Mode}, }; use zeroize::Zeroizing; use crate::{portal, Key}; const ENC_ALG: Nid = Nid::AES_128_CBC; const MAC_ALG: Nid = Nid::SHA256; pub(crate) fn encrypt(data: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>) -> Vec { let cipher = Cipher::from_nid(ENC_ALG).unwrap(); let mut encryptor = Crypter::new(cipher, Mode::Encrypt, key.as_ref(), Some(iv.as_ref())) .expect("Invalid key or IV length"); encryptor.pad(true); let mut blob = vec![0; data.as_ref().len() + cipher.block_size()]; // Unwrapping since adding `CIPHER_BLOCK_SIZE` to array is enough space for // PKCS7 let mut encrypted_len = encryptor.update(data.as_ref(), &mut blob).unwrap(); encrypted_len += encryptor.finalize(&mut blob[encrypted_len..]).unwrap(); blob.truncate(encrypted_len); blob } fn decrypt_with_padding( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, pad: bool, ) -> Zeroizing> { let cipher = Cipher::from_nid(ENC_ALG).unwrap(); let mut decrypter = Crypter::new(cipher, Mode::Decrypt, key.as_ref(), Some(iv.as_ref())) .expect("Invalid key or IV length"); decrypter.pad(pad); let mut data = Zeroizing::new(vec![0; blob.as_ref().len() + cipher.block_size()]); let mut decrypted_len = decrypter.update(blob.as_ref(), &mut data).unwrap(); decrypted_len += decrypter.finalize(&mut data[decrypted_len..]).unwrap(); data.truncate(decrypted_len); data } pub(crate) fn decrypt( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Zeroizing> { decrypt_with_padding(blob, key, iv, true) } pub(crate) fn decrypt_no_padding( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Zeroizing> { decrypt_with_padding(blob, key, iv, false) } pub(crate) fn iv_len() -> usize { let cipher = Cipher::from_nid(ENC_ALG).unwrap(); cipher.iv_len().unwrap() } pub(crate) fn generate_private_key() -> Zeroizing> { let cipher = Cipher::from_nid(ENC_ALG).unwrap(); let mut buf = Zeroizing::new(vec![0; cipher.key_len()]); // FIXME: should return an error? rand_bytes(&mut buf).unwrap(); buf } pub(crate) fn generate_public_key(private_key: impl AsRef<[u8]>) -> Vec { let private_key_bn = BigNum::from_slice(private_key.as_ref()).unwrap(); Dh::from_pqg( BigNum::get_rfc2409_prime_1024().unwrap(), None, BigNum::from_u32(2).unwrap(), ) .and_then(|key| key.set_private_key(private_key_bn)) .unwrap() .public_key() .to_vec() } pub(crate) fn generate_aes_key( private_key: impl AsRef<[u8]>, server_public_key: impl AsRef<[u8]>, ) -> Zeroizing> { let private_key_bn = BigNum::from_slice(private_key.as_ref()).unwrap(); let server_public_key_bn = BigNum::from_slice(server_public_key.as_ref()).unwrap(); let mut common_secret_bytes = Dh::from_pqg( BigNum::get_rfc2409_prime_1024().unwrap(), None, BigNum::from_u32(2).unwrap(), ) .and_then(|key| key.set_private_key(private_key_bn)) .and_then(|key| key.compute_key(&server_public_key_bn)) .unwrap(); let mut common_secret_padded = vec![0; 128 - common_secret_bytes.len()]; // inefficient, but ok for now common_secret_padded.append(&mut common_secret_bytes); // hkdf // input_keying_material let ikm = common_secret_padded; let mut okm = Zeroizing::new(vec![0; 16]); let mut ctx = PkeyCtx::new_id(Id::HKDF).unwrap(); ctx.derive_init().unwrap(); ctx.set_hkdf_md(Md::sha256()).unwrap(); ctx.set_hkdf_key(&ikm).unwrap(); ctx.derive(Some(okm.as_mut())) .expect("hkdf expand should never fail"); okm } pub(crate) fn generate_iv() -> Vec { let mut buf = vec![0; iv_len()]; // FIXME: should return an error? rand_bytes(&mut buf).unwrap(); buf } pub(crate) fn mac_len() -> usize { let md = MessageDigest::from_nid(MAC_ALG).unwrap(); md.size() } pub(crate) fn compute_mac(data: impl AsRef<[u8]>, key: &Key) -> Vec { let md = MessageDigest::from_nid(MAC_ALG).unwrap(); let mac_key = PKey::hmac(key.as_ref()).unwrap(); let mut signer = Signer::new(md, &mac_key).unwrap(); signer.update(data.as_ref()).unwrap(); signer.sign_to_vec().unwrap() } pub(crate) fn verify_mac(data: impl AsRef<[u8]>, key: &Key, expected: impl AsRef<[u8]>) -> bool { memcmp::eq(compute_mac(&data, key).as_slice(), expected.as_ref()) } pub(crate) fn verify_checksum_md5(digest: impl AsRef<[u8]>, content: impl AsRef<[u8]>) -> bool { memcmp::eq( &hash(MessageDigest::md5(), content.as_ref()).unwrap(), digest.as_ref(), ) } pub(crate) fn derive_key( secret: impl AsRef<[u8]>, key_strength: Result<(), portal::WeakKeyError>, salt: impl AsRef<[u8]>, iteration_count: usize, ) -> Key { let cipher = Cipher::from_nid(ENC_ALG).unwrap(); let mut key = Key::new_with_strength(vec![0; cipher.block_size()], key_strength); let md = MessageDigest::from_nid(MAC_ALG).unwrap(); pbkdf2_hmac( secret.as_ref(), salt.as_ref(), iteration_count, md, key.as_mut(), ) .unwrap(); key } pub(crate) fn legacy_derive_key_and_iv( secret: impl AsRef<[u8]>, key_strength: Result<(), portal::WeakKeyError>, salt: impl AsRef<[u8]>, iteration_count: usize, ) -> (Key, Vec) { let cipher = Cipher::from_nid(ENC_ALG).unwrap(); let mut buffer = vec![0; cipher.key_len() + cipher.iv_len().unwrap()]; let mut hasher = Hasher::new(MessageDigest::sha256()).unwrap(); let mut pos = 0usize; loop { hasher.update(secret.as_ref()).unwrap(); hasher.update(salt.as_ref()).unwrap(); let mut digest = hasher.finish().unwrap(); for _ in 1..iteration_count { hasher.update(&digest).unwrap(); digest = hasher.finish().unwrap(); } let to_read = usize::min(digest.len(), buffer.len() - pos); buffer[pos..].copy_from_slice(&(&*digest)[..to_read]); pos += to_read; if pos == buffer.len() { break; } hasher.update(&digest).unwrap(); } let iv = buffer.split_off(cipher.key_len()); (Key::new_with_strength(buffer, key_strength), iv) } oo7-0.3.3/src/dbus/algorithm.rs000064400000000000000000000017261046102023000144210ustar 00000000000000use serde::Serialize; #[derive(Debug, zvariant::Type, PartialEq, Eq, Copy, Clone)] #[zvariant(signature = "s")] /// Algorithm used to start a new session. /// /// The communication between the Secret Service and the application can either /// be encrypted or the items can be sent in plain text. pub enum Algorithm { /// Plain text, per . Plain, /// Encrypted, per . Encrypted, } impl Serialize for Algorithm { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { Algorithm::Plain => String::serialize(&"plain".to_owned(), serializer), Algorithm::Encrypted => String::serialize( &"dh-ietf1024-sha256-aes128-cbc-pkcs7".to_owned(), serializer, ), } } } oo7-0.3.3/src/dbus/api/collection.rs000064400000000000000000000150621046102023000153350ustar 00000000000000use std::{fmt, time::Duration}; use futures_util::{Stream, StreamExt}; use serde::Serialize; use zbus::{ zvariant::{ObjectPath, OwnedObjectPath, Type}, ProxyDefault, }; use super::{Item, Prompt, Properties, Secret, Unlockable, DESTINATION}; use crate::{ dbus::{Error, ServiceError}, AsAttributes, }; #[derive(Type)] #[zvariant(signature = "o")] #[doc(alias = "org.freedesktop.Secret.Collection")] pub struct Collection<'a>(zbus::Proxy<'a>); impl<'a> ProxyDefault for Collection<'a> { const INTERFACE: Option<&'static str> = Some("org.freedesktop.Secret.Collection"); const DESTINATION: Option<&'static str> = Some(DESTINATION); const PATH: Option<&'static str> = None; } impl<'a> From> for Collection<'a> { fn from(value: zbus::Proxy<'a>) -> Self { Self(value) } } impl<'a> Collection<'a> { pub async fn new

( connection: &zbus::Connection, object_path: P, ) -> Result, Error> where P: TryInto>, P::Error: Into, { zbus::ProxyBuilder::new(connection) .path(object_path)? .cache_properties(zbus::CacheProperties::No) .build() .await .map_err(From::from) } pub fn inner(&self) -> &zbus::Proxy { &self.0 } pub(crate) async fn from_paths

( connection: &zbus::Connection, paths: Vec

, ) -> Result>, Error> where P: TryInto>, P::Error: Into, { let mut collections = Vec::with_capacity(paths.capacity()); for path in paths.into_iter() { collections.push(Self::new(connection, path).await?); } Ok(collections) } #[doc(alias = "ItemCreated")] pub async fn receive_item_created(&self) -> Result> + '_, Error> { let stream = self.inner().receive_signal("ItemCreated").await?; let conn = self.inner().connection(); Ok(stream.filter_map(move |message| async move { let path = message.body().deserialize::().ok()?; Item::new(conn, path).await.ok() })) } #[doc(alias = "ItemDeleted")] pub async fn receive_item_deleted(&self) -> Result, Error> { let stream = self.inner().receive_signal("ItemDeleted").await?; Ok(stream.filter_map(move |message| async move { message.body().deserialize::().ok() })) } #[doc(alias = "ItemChanged")] pub async fn receive_item_changed(&self) -> Result> + '_, Error> { let stream = self.inner().receive_signal("ItemChanged").await?; let conn = self.inner().connection(); Ok(stream.filter_map(move |message| async move { let path = message.body().deserialize::().ok()?; Item::new(conn, path).await.ok() })) } pub async fn items(&self) -> Result>, Error> { let item_paths = self .inner() .get_property::>("Items") .await?; Item::from_paths(self.inner().connection(), item_paths).await } pub async fn label(&self) -> Result { self.inner().get_property("Label").await.map_err(From::from) } pub async fn set_label(&self, label: &str) -> Result<(), Error> { self.inner() .set_property("Label", label) .await .map_err::(From::from)?; Ok(()) } #[doc(alias = "Locked")] pub async fn is_locked(&self) -> Result { self.inner() .get_property("Locked") .await .map_err(From::from) } pub async fn created(&self) -> Result { let time = self.inner().get_property::("Created").await?; Ok(Duration::from_secs(time)) } pub async fn modified(&self) -> Result { let time = self.inner().get_property::("Modified").await?; Ok(Duration::from_secs(time)) } pub async fn delete(&self) -> Result<(), Error> { let prompt_path = self .inner() .call_method("Delete", &()) .await .map_err::(From::from)? .body() .deserialize::()?; if let Some(prompt) = Prompt::new(self.inner().connection(), prompt_path).await? { let _ = prompt.receive_completed().await?; } Ok(()) } #[doc(alias = "SearchItems")] pub async fn search_items( &self, attributes: &impl AsAttributes, ) -> Result>, Error> { let msg = self .inner() .call_method("SearchItems", &(attributes.as_attributes())) .await .map_err::(From::from)?; let item_paths = msg.body().deserialize::>()?; Item::from_paths(self.inner().connection(), item_paths).await } #[doc(alias = "CreateItem")] pub async fn create_item( &self, label: &str, attributes: &impl AsAttributes, secret: &Secret<'_>, replace: bool, ) -> Result, Error> { let properties = Properties::for_item(label, attributes); let (item_path, prompt_path) = self .inner() .call_method("CreateItem", &(properties, secret, replace)) .await .map_err::(From::from)? .body() .deserialize::<(OwnedObjectPath, OwnedObjectPath)>()?; let cnx = self.inner().connection(); let item_path = if let Some(prompt) = Prompt::new(cnx, prompt_path).await? { let response = prompt.receive_completed().await?; OwnedObjectPath::try_from(response).map_err::(From::from)? } else { item_path }; Item::new(self.inner().connection(), item_path).await } } impl<'a> Serialize for Collection<'a> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ObjectPath::serialize(self.inner().path(), serializer) } } impl<'a> fmt::Debug for Collection<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Collection") .field(&self.inner().path().as_str()) .finish() } } impl<'a> Unlockable for Collection<'a> {} oo7-0.3.3/src/dbus/api/item.rs000064400000000000000000000116261046102023000141420ustar 00000000000000use std::{collections::HashMap, fmt, hash::Hash, time::Duration}; use serde::Serialize; use zbus::{ zvariant::{ObjectPath, OwnedObjectPath, Type}, ProxyDefault, }; use super::{secret::SecretInner, Prompt, Secret, Session, Unlockable, DESTINATION}; use crate::{ dbus::{Error, ServiceError}, AsAttributes, }; #[derive(Type)] #[zvariant(signature = "o")] #[doc(alias = "org.freedesktop.Secret.Item")] pub struct Item<'a>(zbus::Proxy<'a>); impl<'a> ProxyDefault for Item<'a> { const INTERFACE: Option<&'static str> = Some("org.freedesktop.Secret.Item"); const DESTINATION: Option<&'static str> = Some(DESTINATION); const PATH: Option<&'static str> = None; } impl<'a> From> for Item<'a> { fn from(value: zbus::Proxy<'a>) -> Self { Self(value) } } impl<'a> Item<'a> { pub async fn new

(connection: &zbus::Connection, object_path: P) -> Result, Error> where P: TryInto>, P::Error: Into, { zbus::ProxyBuilder::new(connection) .path(object_path)? .build() .await .map_err(From::from) } pub(crate) async fn from_paths

( connection: &zbus::Connection, paths: Vec

, ) -> Result>, Error> where P: TryInto>, P::Error: Into, { let mut items = Vec::with_capacity(paths.capacity()); for path in paths.into_iter() { items.push(Self::new(connection, path).await?); } Ok(items) } pub fn inner(&self) -> &zbus::Proxy { &self.0 } #[doc(alias = "Locked")] pub async fn is_locked(&self) -> Result { self.inner() .get_property("Locked") .await .map_err(From::from) } pub async fn label(&self) -> Result { self.inner().get_property("Label").await.map_err(From::from) } pub async fn set_label(&self, label: &str) -> Result<(), Error> { self.inner() .set_property("Label", label) .await .map_err::(From::from)?; Ok(()) } pub async fn created(&self) -> Result { let secs = self.inner().get_property::("Created").await?; Ok(Duration::from_secs(secs)) } pub async fn modified(&self) -> Result { let secs = self.inner().get_property::("Modified").await?; Ok(Duration::from_secs(secs)) } pub async fn attributes(&self) -> Result, Error> { self.inner() .get_property("Attributes") .await .map_err(From::from) } pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<(), Error> { self.inner() .set_property("Attributes", attributes.as_attributes()) .await .map_err::(From::from)?; Ok(()) } pub async fn delete(&self) -> Result<(), Error> { let prompt_path = self .inner() .call_method("Delete", &()) .await .map_err::(From::from)? .body() .deserialize::()?; if let Some(prompt) = Prompt::new(self.inner().connection(), prompt_path).await? { let _ = prompt.receive_completed().await?; } Ok(()) } #[doc(alias = "GetSecret")] pub async fn secret(&self, session: &Session<'_>) -> Result, Error> { let inner = self .inner() .call_method("GetSecret", &(session)) .await .map_err::(From::from)? .body() .deserialize::()?; Secret::from_inner(self.inner().connection(), inner).await } #[doc(alias = "SetSecret")] pub async fn set_secret(&self, secret: &Secret<'_>) -> Result<(), Error> { self.inner() .call_method("SetSecret", &(secret,)) .await .map_err::(From::from)?; Ok(()) } } impl<'a> Serialize for Item<'a> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ObjectPath::serialize(self.inner().path(), serializer) } } impl<'a> PartialEq for Item<'a> { fn eq(&self, other: &Self) -> bool { self.inner().path() == other.inner().path() } } impl<'a> Eq for Item<'a> {} impl<'a> Hash for Item<'a> { fn hash(&self, state: &mut H) { self.inner().path().hash(state); } } impl<'a> fmt::Debug for Item<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Item") .field(&self.inner().path().as_str()) .finish() } } impl<'a> Unlockable for Item<'a> {} oo7-0.3.3/src/dbus/api/mod.rs000064400000000000000000000017071046102023000137620ustar 00000000000000pub(crate) const DESTINATION: &str = "org.freedesktop.secrets"; pub(crate) const PATH: &str = "/org/freedesktop/secrets"; /// A common trait implemented by objects that can be /// locked or unlocked. Like [`Collection`] or [`Item`]. pub trait Unlockable: serde::Serialize + zbus::zvariant::Type {} impl<'a> Unlockable for zbus::zvariant::ObjectPath<'a> {} impl Unlockable for zbus::zvariant::OwnedObjectPath {} impl<'a> Unlockable for &zbus::zvariant::ObjectPath<'a> {} impl Unlockable for &zbus::zvariant::OwnedObjectPath {} mod collection; mod item; mod prompt; mod properties; mod secret; mod service; mod session; pub use collection::Collection; pub use item::Item; pub(crate) use prompt::Prompt; #[cfg(not(feature = "unstable"))] pub(crate) use properties::Properties; #[cfg(feature = "unstable")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub use properties::Properties; pub use secret::Secret; pub use service::Service; pub use session::Session; oo7-0.3.3/src/dbus/api/prompt.rs000064400000000000000000000056631046102023000145310ustar 00000000000000use std::fmt; use futures_util::StreamExt; use serde::Serialize; use zbus::{ zvariant::{ObjectPath, OwnedValue, Type}, ProxyDefault, }; use super::DESTINATION; use crate::dbus::{Error, ServiceError}; #[derive(Type)] #[zvariant(signature = "o")] #[doc(alias = "org.freedesktop.Secret.Prompt")] pub struct Prompt<'a>(zbus::Proxy<'a>); impl<'a> ProxyDefault for Prompt<'a> { const INTERFACE: Option<&'static str> = Some("org.freedesktop.Secret.Prompt"); const DESTINATION: Option<&'static str> = Some(DESTINATION); const PATH: Option<&'static str> = None; } impl<'a> From> for Prompt<'a> { fn from(value: zbus::Proxy<'a>) -> Self { Self(value) } } impl<'a> Prompt<'a> { pub async fn new

( connection: &zbus::Connection, object_path: P, ) -> Result>, Error> where P: TryInto>, P::Error: Into, { let path = object_path.try_into().map_err(Into::into)?; if path != ObjectPath::default() { Ok(Some( zbus::ProxyBuilder::new(connection) .path(path)? .build() .await?, )) } else { Ok(None) } } pub fn inner(&self) -> &zbus::Proxy { &self.0 } pub async fn prompt(&self, window_id: &str) -> Result<(), Error> { self.inner() .call_method("Prompt", &(window_id)) .await .map_err::(From::from)?; Ok(()) } #[allow(unused)] pub async fn dismiss(&self) -> Result<(), Error> { self.inner() .call_method("Dismiss", &()) .await .map_err::(From::from)?; Ok(()) } pub async fn receive_completed(&self) -> Result { let mut stream = self.inner().receive_signal("Completed").await?; // TODO: figure out how to come with a window-id without depending on ashpd for // it WindowIdentifier thingy let (value, _) = futures_util::try_join!( async { let message = stream.next().await.unwrap(); let (dismissed, result) = message.body().deserialize::<(bool, OwnedValue)>()?; if dismissed { Err(Error::Dismissed) } else { Ok(result) } }, self.prompt("") )?; Ok(value) } } impl<'a> Serialize for Prompt<'a> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ObjectPath::serialize(self.inner().path(), serializer) } } impl<'a> fmt::Debug for Prompt<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Prompt") .field(&self.inner().path().as_str()) .finish() } } oo7-0.3.3/src/dbus/api/properties.rs000064400000000000000000000110571046102023000153760ustar 00000000000000use std::collections::HashMap; use serde::{ ser::{Serialize, SerializeMap}, Deserialize, }; use zbus::zvariant::{Type, Value}; use crate::AsAttributes; const ITEM_PROPERTY_LABEL: &str = "org.freedesktop.Secret.Item.Label"; const ITEM_PROPERTY_ATTRIBUTES: &str = "org.freedesktop.Secret.Item.Attributes"; const COLLECTION_PROPERTY_LABEL: &str = "org.freedesktop.Secret.Collection.Label"; #[derive(Debug, Type)] #[zvariant(signature = "a{sv}")] pub struct Properties { label: String, attributes: Option>, } impl Properties { pub fn for_item(label: &str, attributes: &impl AsAttributes) -> Self { Self { label: label.to_owned(), attributes: Some( attributes .as_attributes() .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(), ), } } pub fn for_collection(label: &str) -> Self { Self { label: label.to_owned(), attributes: None, } } pub fn label(&self) -> &str { &self.label } pub fn attributes(&self) -> Option<&HashMap> { self.attributes.as_ref() } } impl Serialize for Properties { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if self.attributes.is_none() { let mut map = serializer.serialize_map(Some(1))?; map.serialize_entry(COLLECTION_PROPERTY_LABEL, &Value::from(&self.label))?; map.end() } else { let mut map = serializer.serialize_map(Some(2))?; map.serialize_entry(ITEM_PROPERTY_LABEL, &Value::from(&self.label))?; let mut dict = zbus::zvariant::Dict::new(String::signature(), String::signature()); if let Some(attributes) = &self.attributes { for (key, value) in attributes { dict.add(key, value).expect("Key/Value of correct types"); } } map.serialize_entry(ITEM_PROPERTY_ATTRIBUTES, &Value::from(dict))?; map.end() } } } impl<'de> Deserialize<'de> for Properties { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let map: HashMap<&str, Value<'_>> = HashMap::deserialize(deserializer)?; if map.contains_key(COLLECTION_PROPERTY_LABEL) { let label = zvariant::Str::try_from(map.get(COLLECTION_PROPERTY_LABEL).unwrap()).unwrap(); Ok(Self::for_collection(&label)) } else { let label = zvariant::Str::try_from(map.get(ITEM_PROPERTY_LABEL).unwrap()).unwrap(); let attributes = HashMap::::try_from( map.get(ITEM_PROPERTY_ATTRIBUTES) .unwrap() .try_clone() .unwrap(), ) .unwrap(); Ok(Self::for_item(&label, &attributes)) } } } #[cfg(test)] mod tests { use zbus::zvariant::{serialized::Context, to_bytes, Endian, Type}; use super::*; #[test] fn serialize_label() { let properties = Properties::for_collection("some_label"); let ctxt = Context::new_dbus(Endian::Little, 0); let encoded = to_bytes(ctxt, &properties).unwrap(); let decoded: HashMap<&str, Value<'_>> = encoded.deserialize().unwrap().0; assert_eq!( decoded[COLLECTION_PROPERTY_LABEL], Value::from("some_label") ); assert!(!decoded.contains_key(ITEM_PROPERTY_ATTRIBUTES)); assert!(!decoded.contains_key(ITEM_PROPERTY_LABEL)); } #[test] fn serialize_label_with_attributes() { let mut attributes = HashMap::new(); attributes.insert("some", "attribute"); let properties = Properties::for_item("some_label", &attributes); let ctxt = Context::new_dbus(Endian::Little, 0); let encoded = to_bytes(ctxt, &properties).unwrap(); let decoded: HashMap<&str, Value<'_>> = encoded.deserialize().unwrap().0; assert_eq!(decoded[ITEM_PROPERTY_LABEL], Value::from("some_label")); assert!(!decoded.contains_key(COLLECTION_PROPERTY_LABEL)); assert!(decoded.contains_key(ITEM_PROPERTY_ATTRIBUTES)); assert_eq!( decoded[ITEM_PROPERTY_ATTRIBUTES], zvariant::Dict::from(attributes).into() ); } #[test] fn signature() { assert_eq!(Properties::signature(), "a{sv}"); } } oo7-0.3.3/src/dbus/api/secret.rs000064400000000000000000000056301046102023000144670ustar 00000000000000use std::sync::Arc; use serde::{ser::SerializeTuple, Deserialize, Serialize}; use zbus::zvariant::{OwnedObjectPath, Type}; use zeroize::{Zeroize, ZeroizeOnDrop}; use super::Session; use crate::{crypto, dbus::Error, Key}; #[derive(Debug, Serialize, Deserialize, Type)] #[zvariant(signature = "(oayays)")] pub(crate) struct SecretInner(pub OwnedObjectPath, pub Vec, pub Vec, pub String); #[derive(Debug, Type, Zeroize, ZeroizeOnDrop)] #[zvariant(signature = "(oayays)")] pub struct Secret<'a> { #[zeroize(skip)] pub(crate) session: Arc>, pub(crate) parameters: Vec, pub(crate) value: Vec, #[zeroize(skip)] pub(crate) content_type: String, } impl<'a> Secret<'a> { pub(crate) fn new( session: Arc>, secret: impl AsRef<[u8]>, content_type: &str, ) -> Self { Self { session, parameters: vec![], value: secret.as_ref().to_vec(), content_type: content_type.to_owned(), } } pub(crate) fn new_encrypted( session: Arc>, secret: impl AsRef<[u8]>, content_type: &str, aes_key: &Key, ) -> Self { let iv = crypto::generate_iv(); let secret = crypto::encrypt(secret.as_ref(), aes_key, &iv); Self { session, parameters: iv, value: secret, content_type: content_type.to_owned(), } } pub(crate) async fn from_inner( cnx: &zbus::Connection, inner: SecretInner, ) -> Result, Error> { let secret = Secret { session: Arc::new(Session::new(cnx, inner.0).await?), parameters: inner.1, value: inner.2, content_type: inner.3, }; Ok(secret) } /// Session used to encode the secret pub fn session(&self) -> &Session { &self.session } /// Algorithm dependent parameters for secret value encoding pub fn parameters(&self) -> &[u8] { &self.parameters } /// Possibly encoded secret value pub fn value(&self) -> &[u8] { &self.value } /// Content type of the secret pub fn content_type(&self) -> &str { &self.content_type } } impl<'a> Serialize for Secret<'a> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { let mut tuple_serializer = serializer.serialize_tuple(4)?; tuple_serializer.serialize_element(self.session().inner().path())?; tuple_serializer.serialize_element(self.parameters())?; tuple_serializer.serialize_element(self.value())?; tuple_serializer.serialize_element(self.content_type())?; tuple_serializer.end() } } #[cfg(test)] mod tests { use super::*; #[test] fn signature() { assert_eq!(Secret::signature(), "(oayays)"); } } oo7-0.3.3/src/dbus/api/service.rs000064400000000000000000000216741046102023000146500ustar 00000000000000use std::{collections::HashMap, fmt}; use futures_util::{Stream, StreamExt}; use zbus::{ zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Type, Value}, ProxyDefault, }; use super::{ secret::SecretInner, Collection, Item, Prompt, Properties, Secret, Session, Unlockable, DESTINATION, PATH, }; use crate::{ dbus::{Algorithm, Error, ServiceError}, AsAttributes, Key, }; #[derive(Type)] #[zvariant(signature = "o")] #[doc(alias = "org.freedesktop.secrets")] pub struct Service<'a>(zbus::Proxy<'a>); impl<'a> ProxyDefault for Service<'a> { const INTERFACE: Option<&'static str> = Some("org.freedesktop.Secret.Service"); const DESTINATION: Option<&'static str> = Some(DESTINATION); const PATH: Option<&'static str> = Some(PATH); } impl<'a> From> for Service<'a> { fn from(value: zbus::Proxy<'a>) -> Self { Self(value) } } impl<'a> Service<'a> { pub async fn new(connection: &zbus::Connection) -> Result, Error> { zbus::ProxyBuilder::new(connection) .cache_properties(zbus::CacheProperties::No) .build() .await .map_err(From::from) } pub fn inner(&self) -> &zbus::Proxy { &self.0 } #[doc(alias = "CollectionCreated")] pub async fn receive_collection_created( &self, ) -> Result> + '_, Error> { let stream = self.inner().receive_signal("CollectionCreated").await?; let conn = self.inner().connection(); Ok(stream.filter_map(move |message| async move { let path = message.body().deserialize::().ok()?; Collection::new(conn, path).await.ok() })) } #[doc(alias = "CollectionDeleted")] pub async fn receive_collection_deleted( &self, ) -> Result, Error> { let stream = self.inner().receive_signal("CollectionDeleted").await?; Ok(stream.filter_map(move |message| async move { message.body().deserialize::().ok() })) } #[doc(alias = "CollectionChanged")] pub async fn receive_collection_changed( &self, ) -> Result> + '_, Error> { let stream = self.inner().receive_signal("CollectionChanged").await?; let conn = self.inner().connection(); Ok(stream.filter_map(move |message| async move { let path = message.body().deserialize::().ok()?; Collection::new(conn, path).await.ok() })) } pub async fn collections(&self) -> Result>, Error> { let collections_paths = self .inner() .get_property::>("Collections") .await?; Collection::from_paths(self.inner().connection(), collections_paths).await } #[doc(alias = "OpenSession")] pub async fn open_session( &self, client_public_key: Option<&Key>, ) -> Result<(Option, Session<'a>), Error> { let (algorithm, key): (_, Value<'_>) = match client_public_key { None => (Algorithm::Plain, zvariant::Str::default().into()), Some(key) => (Algorithm::Encrypted, key.into()), }; let (service_key, session_path) = self .inner() .call_method("OpenSession", &(&algorithm, key)) .await .map_err::(From::from)? .body() .deserialize::<(OwnedValue, OwnedObjectPath)>()?; let session = Session::new(self.inner().connection(), session_path).await?; let key = match algorithm { Algorithm::Plain => None, Algorithm::Encrypted => Some(Key::from(service_key)), }; Ok((key, session)) } #[doc(alias = "CreateCollection")] pub async fn create_collection( &self, label: &str, alias: Option<&str>, ) -> Result, Error> { let properties = Properties::for_collection(label); let (collection_path, prompt_path) = self .inner() .call_method("CreateCollection", &(properties, alias.unwrap_or_default())) .await .map_err::(From::from)? .body() .deserialize::<(OwnedObjectPath, OwnedObjectPath)>()?; let collection_path = if let Some(prompt) = Prompt::new(self.inner().connection(), prompt_path).await? { let response = prompt.receive_completed().await?; OwnedObjectPath::try_from(response).map_err::(From::from)? } else { collection_path }; Collection::new(self.inner().connection(), collection_path).await } #[doc(alias = "SearchItems")] pub async fn search_items( &self, attributes: &impl AsAttributes, ) -> Result<(Vec>, Vec>), Error> { let (unlocked_item_paths, locked_item_paths) = self .inner() .call_method("SearchItems", &(attributes.as_attributes())) .await .map_err::(From::from)? .body() .deserialize::<(Vec, Vec)>()?; let cnx = self.inner().connection(); let unlocked_items = Item::from_paths(cnx, unlocked_item_paths).await?; let locked_items = Item::from_paths(cnx, locked_item_paths).await?; Ok((unlocked_items, locked_items)) } pub async fn unlock(&self, items: &[impl Unlockable]) -> Result, Error> { let (mut unlocked_item_paths, prompt_path) = self .inner() .call_method("Unlock", &(items)) .await .map_err::(From::from)? .body() .deserialize::<(Vec, OwnedObjectPath)>()?; let cnx = self.inner().connection(); if let Some(prompt) = Prompt::new(cnx, prompt_path).await? { let response = prompt.receive_completed().await?; let locked_paths = Vec::::try_from(response) .map_err::(From::from)?; unlocked_item_paths.extend(locked_paths); }; Ok(unlocked_item_paths) } pub async fn lock(&self, items: &[impl Unlockable]) -> Result, Error> { let (mut locked_item_paths, prompt_path) = self .inner() .call_method("Lock", &(items)) .await .map_err::(From::from)? .body() .deserialize::<(Vec, OwnedObjectPath)>()?; let cnx = self.inner().connection(); if let Some(prompt) = Prompt::new(cnx, prompt_path).await? { let response = prompt.receive_completed().await?; let locked_paths = Vec::::try_from(response) .map_err::(From::from)?; locked_item_paths.extend(locked_paths); }; Ok(locked_item_paths) } #[doc(alias = "GetSecrets")] pub async fn secrets( &self, items: &[Item<'_>], session: &Session<'_>, ) -> Result, Secret<'_>>, Error> { let secrets = self .inner() .call_method("GetSecrets", &(items, session)) .await .map_err::(From::from)? .body() .deserialize::>()?; let cnx = self.inner().connection(); let mut output = HashMap::with_capacity(secrets.capacity()); for (path, secret_inner) in secrets { output.insert( Item::new(cnx, path).await?, Secret::from_inner(cnx, secret_inner).await?, ); } Ok(output) } #[doc(alias = "ReadAlias")] pub async fn read_alias(&self, name: &str) -> Result>, Error> { let collection_path = self .inner() .call_method("ReadAlias", &(name)) .await .map_err::(From::from)? .body() .deserialize::()?; if collection_path != OwnedObjectPath::default() { let collection = Collection::new(self.inner().connection(), collection_path).await?; Ok(Some(collection)) } else { Ok(None) } } #[doc(alias = "SetAlias")] pub async fn set_alias(&self, name: &str, collection: &Collection<'_>) -> Result<(), Error> { self.inner() .call_method("SetAlias", &(name, collection)) .await .map_err::(From::from)?; Ok(()) } } impl<'a> fmt::Debug for Service<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Service") .field(&self.inner().path().as_str()) .finish() } } oo7-0.3.3/src/dbus/api/session.rs000064400000000000000000000033421046102023000146630ustar 00000000000000use std::fmt; use serde::Serialize; use zbus::{ zvariant::{ObjectPath, Type}, ProxyDefault, }; use super::DESTINATION; use crate::dbus::{Error, ServiceError}; #[derive(Type)] #[zvariant(signature = "o")] #[doc(alias = "org.freedesktop.Secret.Session")] pub struct Session<'a>(zbus::Proxy<'a>); impl<'a> ProxyDefault for Session<'a> { const INTERFACE: Option<&'static str> = Some("org.freedesktop.Secret.Session"); const DESTINATION: Option<&'static str> = Some(DESTINATION); const PATH: Option<&'static str> = None; } impl<'a> From> for Session<'a> { fn from(value: zbus::Proxy<'a>) -> Self { Self(value) } } impl<'a> Session<'a> { pub async fn new

(connection: &zbus::Connection, object_path: P) -> Result, Error> where P: TryInto>, P::Error: Into, { zbus::ProxyBuilder::new(connection) .path(object_path)? .build() .await .map_err(From::from) } pub fn inner(&self) -> &zbus::Proxy { &self.0 } pub async fn close(&self) -> Result<(), Error> { self.inner() .call_method("Close", &()) .await .map_err::(From::from)?; Ok(()) } } impl<'a> Serialize for Session<'a> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ObjectPath::serialize(self.inner().path(), serializer) } } impl<'a> fmt::Debug for Session<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Session") .field(&self.inner().path().as_str()) .finish() } } oo7-0.3.3/src/dbus/collection.rs000064400000000000000000000232111046102023000145570ustar 00000000000000use std::{sync::Arc, time::Duration}; #[cfg(feature = "async-std")] use async_lock::RwLock; use futures_util::{Stream, StreamExt}; #[cfg(feature = "tokio")] use tokio::sync::RwLock; use zbus::zvariant::{ObjectPath, OwnedObjectPath}; use super::{api, Algorithm, Error, Item}; use crate::{AsAttributes, Key}; /// A collection allows to store and retrieve items. /// /// The collection can be either in a locked or unlocked state, use /// [`Collection::lock`] or [`Collection::unlock`] to lock or unlock it. /// /// Using [`Collection::search_items`] or [`Collection::items`] will return no /// items if the collection is locked. /// /// **Note** /// /// If the collection is deleted using [`Collection::delete`] any future usage /// of it API will fail with [`Error::Deleted`]. #[derive(Debug)] pub struct Collection<'a> { inner: Arc>, service: Arc>, session: Arc>, algorithm: Algorithm, /// Defines whether the Collection has been deleted or not available: RwLock, aes_key: Option>, } impl<'a> Collection<'a> { pub(crate) fn new( service: Arc>, session: Arc>, algorithm: Algorithm, collection: api::Collection<'a>, aes_key: Option>, ) -> Collection<'a> { Self { inner: Arc::new(collection), session, service, algorithm, available: RwLock::new(true), aes_key, } } pub(crate) async fn is_available(&self) -> bool { *self.available.read().await } /// Retrieve the list of available [`Item`] in the collection. pub async fn items(&self) -> Result>, Error> { if !self.is_available().await { Err(Error::Deleted) } else { Ok(self .inner .items() .await? .into_iter() .map(|item| self.new_item(item)) .collect::>()) } } /// The collection label. pub async fn label(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.label().await } } /// Set the collection label. pub async fn set_label(&self, label: &str) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.set_label(label).await } } /// Get whether the collection is locked. #[doc(alias = "Locked")] pub async fn is_locked(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.is_locked().await } } /// The UNIX time when the collection was created. pub async fn created(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.created().await } } /// The UNIX time when the collection was modified. pub async fn modified(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.modified().await } } /// Search for items based on their attributes. pub async fn search_items( &self, attributes: &impl AsAttributes, ) -> Result>, Error> { if !self.is_available().await { Err(Error::Deleted) } else { let items = self.inner.search_items(attributes).await?; Ok(items .into_iter() .map(|item| { Item::new( Arc::clone(&self.service), Arc::clone(&self.session), self.algorithm, item, self.aes_key.clone(), // Cheap clone, it is an Arc, ) }) .collect::>()) } } /// Create a new item on the collection /// /// # Arguments /// /// * `label` - A user visible label of the item. /// * `attributes` - A map of key/value attributes, used to find the item /// later. /// * `secret` - The secret to store. /// * `replace` - Whether to replace the value if the `attributes` matches /// an existing `secret`. /// * `content_type` - The content type of the secret, usually something /// like `text/plain`. pub async fn create_item( &self, label: &str, attributes: &impl AsAttributes, secret: impl AsRef<[u8]>, replace: bool, content_type: &str, ) -> Result, Error> { if !self.is_available().await { Err(Error::Deleted) } else { let secret = match self.algorithm { Algorithm::Plain => { api::Secret::new(Arc::clone(&self.session), secret, content_type) } Algorithm::Encrypted => api::Secret::new_encrypted( Arc::clone(&self.session), secret, content_type, self.aes_key.as_ref().unwrap(), ), }; let item = self .inner .create_item(label, attributes, &secret, replace) .await?; Ok(self.new_item(item)) } } /// Unlock the collection. pub async fn unlock(&self) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.service.unlock(&[self.inner.inner().path()]).await?; Ok(()) } } /// Lock the collection. pub async fn lock(&self) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.service.lock(&[self.inner.inner().path()]).await?; Ok(()) } } /// Delete the collection. pub async fn delete(&self) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.delete().await?; *self.available.write().await = false; Ok(()) } } /// Returns collection path pub fn path(&self) -> &ObjectPath<'_> { self.inner.inner().path() } /// Stream yielding when new items get created pub async fn receive_item_created(&self) -> Result> + '_, Error> { Ok(self .inner .receive_item_created() .await? .map(|item| self.new_item(item))) } /// Stream yielding when existing items get changed pub async fn receive_item_changed(&self) -> Result> + '_, Error> { Ok(self .inner .receive_item_changed() .await? .map(|item| self.new_item(item))) } /// Stream yielding when existing items get deleted pub async fn receive_item_deleted(&self) -> Result, Error> { self.inner.receive_item_deleted().await } // Get public `Item`` from `api::Item` fn new_item(&self, item: api::Item<'a>) -> Item<'a> { Item::new( Arc::clone(&self.service), Arc::clone(&self.session), self.algorithm, item, self.aes_key.clone(), // Cheap clone, it is an Arc, ) } } #[cfg(test)] #[cfg(feature = "tokio")] mod tests { #[cfg(feature = "local_tests")] use super::*; #[cfg(feature = "local_tests")] use crate::dbus::{self, Service}; #[cfg(feature = "local_tests")] async fn create_item(service: Service<'_>, encrypted: bool) { let mut attributes = HashMap::new(); let value = if encrypted { "encrypted-type-test" } else { "plain-type-test" }; attributes.insert("type", value); let secret = "a password".as_bytes(); let collection = match service.default_collection().await { Err(dbus::Error::NotFound(_)) => { service .create_collection("Default", Some(dbus::DEFAULT_COLLECTION)) .await } e => e, } .unwrap(); let n_items = collection.items().await.unwrap().len(); let n_search_items = collection.search_items(&attributes).await.unwrap().len(); let item = collection .create_item("A secret", &attributes, secret, true, "text/plain") .await .unwrap(); assert_eq!(*item.secret().await.unwrap(), secret); assert_eq!(item.attributes().await.unwrap()["type"], value); assert_eq!(collection.items().await.unwrap().len(), n_items + 1); assert_eq!( collection.search_items(&attributes).await.unwrap().len(), n_search_items + 1 ); item.delete().await.unwrap(); assert_eq!(collection.items().await.unwrap().len(), n_items); assert_eq!( collection.search_items(&attributes).await.unwrap().len(), n_search_items ); } #[tokio::test] #[cfg(feature = "local_tests")] async fn create_plain_item() { let service = Service::plain().await.unwrap(); create_item(service, false).await; } #[tokio::test] #[cfg(feature = "local_tests")] async fn create_encrypted_item() { let service = Service::encrypted().await.unwrap(); create_item(service, true).await; } } oo7-0.3.3/src/dbus/error.rs000064400000000000000000000040501046102023000135550ustar 00000000000000use std::fmt; /// DBus Secret Service specific errors. /// #[derive(zbus::DBusError, Debug)] #[zbus(prefix = "org.freedesktop.Secret.Error")] pub enum ServiceError { #[zbus(error)] /// ZBus specific error. ZBus(zbus::Error), /// Collection/Item is locked. IsLocked, /// Session does not exist. NoSession, /// Collection/Item does not exist. NoSuchObject, } /// DBus backend specific errors. #[derive(Debug)] pub enum Error { /// Something went wrong on the wire. Zbus(zbus::Error), /// A service error. Service(ServiceError), /// The item/collection was removed. Deleted, /// The prompt request was dismissed. Dismissed, /// The collection doesn't exists NotFound(String), /// Input/Output. IO(std::io::Error), } impl From for Error { fn from(e: zbus::Error) -> Self { Self::Zbus(e) } } impl From for Error { fn from(e: zbus::fdo::Error) -> Self { Self::Zbus(zbus::Error::FDO(Box::new(e))) } } impl From for Error { fn from(e: zbus::zvariant::Error) -> Self { Self::Zbus(zbus::Error::Variant(e)) } } impl From for Error { fn from(e: ServiceError) -> Self { Self::Service(e) } } impl From for Error { fn from(e: std::io::Error) -> Self { Self::IO(e) } } impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Zbus(err) => write!(f, "zbus error {err}"), Self::Service(err) => write!(f, "service error {err}"), Self::IO(err) => write!(f, "IO error {err}"), Self::Deleted => write!(f, "Item/Collection was deleted, can no longer be used"), Self::NotFound(name) => write!(f, "The collection '{name}' doesn't exists"), Self::Dismissed => write!(f, "Prompt was dismissed"), } } } oo7-0.3.3/src/dbus/item.rs000064400000000000000000000137211046102023000133670ustar 00000000000000use std::{collections::HashMap, sync::Arc, time::Duration}; #[cfg(feature = "async-std")] use async_lock::RwLock; #[cfg(feature = "tokio")] use tokio::sync::RwLock; use zbus::zvariant::ObjectPath; use zeroize::Zeroizing; use super::{api, Algorithm, Error}; use crate::{crypto, AsAttributes, Key}; /// A secret with a label and attributes to identify it. /// /// An item might be locked or unlocked, use [`Item::lock`] or [`Item::unlock`] /// to lock or unlock it. Note that the Secret Service might not be able to /// lock/unlock individual items and may lock/unlock the entire collection in /// such case. /// /// The item is attributes are used to identify and find the item later using /// [`Collection::search_items`](crate::dbus::Collection::search_items). /// They are not stored or transferred in a secure manner. /// /// **Note** /// /// If the item is deleted using [`Item::delete`] any future usage of it API /// will fail with [`Error::Deleted`]. #[derive(Debug)] pub struct Item<'a> { inner: Arc>, session: Arc>, service: Arc>, algorithm: Algorithm, /// Defines whether the Item has been deleted or not available: RwLock, aes_key: Option>, } impl<'a> Item<'a> { pub(crate) fn new( service: Arc>, session: Arc>, algorithm: Algorithm, item: api::Item<'a>, aes_key: Option>, ) -> Item<'a> { Self { inner: Arc::new(item), service, session, algorithm, available: RwLock::new(true), aes_key, } } pub(crate) async fn is_available(&self) -> bool { *self.available.read().await } /// Get whether the item is locked. pub async fn is_locked(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.is_locked().await } } /// The item label. pub async fn label(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.label().await } } /// Set the item label. pub async fn set_label(&self, label: &str) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.set_label(label).await } } /// The UNIX time when the item was created. pub async fn created(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.created().await } } /// The UNIX time when the item was modified. pub async fn modified(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.modified().await } } /// Retrieve the item attributes. pub async fn attributes(&self) -> Result, Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.attributes().await } } /// Update the item attributes. pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.set_attributes(attributes).await } } /// Delete the item. pub async fn delete(&self) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.delete().await?; *self.available.write().await = false; Ok(()) } } /// Retrieve the currently stored secret. pub async fn secret(&self) -> Result>, Error> { if !self.is_available().await { Err(Error::Deleted) } else { let secret = self.inner.secret(&self.session).await?; let value = match self.algorithm { Algorithm::Plain => Zeroizing::new(secret.value.to_owned()), Algorithm::Encrypted => { let iv = &secret.parameters; // Safe unwrap as it is encrypted let aes_key = self.aes_key.as_ref().unwrap(); crypto::decrypt(&secret.value, aes_key, iv) } }; Ok(value) } } /// Modify the stored secret on the item. /// /// # Arguments /// /// * `secret` - The secret to store. /// * `content_type` - The content type of the secret, usually something /// like `text/plain`. #[doc(alias = "SetSecret")] pub async fn set_secret( &self, secret: impl AsRef<[u8]>, content_type: &str, ) -> Result<(), Error> { let secret = match self.algorithm { Algorithm::Plain => api::Secret::new(Arc::clone(&self.session), secret, content_type), Algorithm::Encrypted => { let aes_key = self.aes_key.as_ref().unwrap(); api::Secret::new_encrypted(Arc::clone(&self.session), secret, content_type, aes_key) } }; self.inner.set_secret(&secret).await?; Ok(()) } /// Unlock the item. pub async fn unlock(&self) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.service.unlock(&[self.inner.inner().path()]).await?; Ok(()) } } /// Lock the item. pub async fn lock(&self) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.service.lock(&[self.inner.inner().path()]).await?; Ok(()) } } /// Returns item path pub fn path(&self) -> &ObjectPath<'_> { self.inner.inner().path() } } oo7-0.3.3/src/dbus/mod.rs000064400000000000000000000036671046102023000132200ustar 00000000000000//! A [Secret Service](https://specifications.freedesktop.org/secret-service/latest/index.html) implementation. //! //! That is usually done with //! ```no_run //! use oo7::dbus::Service; //! //! # async fn run() -> oo7::Result<()> { //! let service = Service::new().await?; //! //! let mut attributes = std::collections::HashMap::new(); //! attributes.insert("type", "password"); //! attributes.insert("user_id", "some_other_identifier"); //! //! let collection = service.default_collection().await?; //! // Store a secret //! collection //! .create_item( //! "My App's secret", //! &attributes, //! b"password", //! true, //! "text/plain", //! ) //! .await?; //! //! // Retrieve it later thanks to it attributes //! let items = collection.search_items(&attributes).await?; //! let item = items.first().unwrap(); //! assert_eq!(*item.secret().await?, b"password"); //! //! # Ok(()) //! # } //! ``` /// The default collection alias. /// /// In general, you are supposed to use [`Service::default_collection`]. pub const DEFAULT_COLLECTION: &str = "default"; /// A session collection. /// /// The collection is cleared when the user ends the session. pub const SESSION_COLLECTION: &str = "session"; /// Barebone DBus API of the Secret Service specifications. /// /// The API is not supposed to be used by the applications in general unless /// the wrapper API doesn't provide functionality you need. #[cfg(feature = "unstable")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub mod api; #[cfg(not(feature = "unstable"))] #[allow(unused)] mod api; mod algorithm; #[cfg(not(feature = "unstable"))] pub(crate) use algorithm::Algorithm; #[cfg(feature = "unstable")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub use algorithm::Algorithm; mod item; pub use item::Item; mod error; mod service; pub use error::{Error, ServiceError}; pub use service::Service; mod collection; pub use collection::Collection; oo7-0.3.3/src/dbus/service.rs000064400000000000000000000160721046102023000140730ustar 00000000000000use std::sync::Arc; use futures_util::{Stream, StreamExt}; use zbus::zvariant::OwnedObjectPath; use super::{api, Algorithm, Collection, Error, DEFAULT_COLLECTION}; use crate::Key; /// The entry point of communicating with a [`org.freedesktop.Secrets`](https://specifications.freedesktop.org/secret-service/latest/index.html) implementation. /// /// It will automatically create a session for you and allow you to retrieve /// collections or create new ones. /// /// Certain actions requires on the Secret Service implementation requires a /// user prompt to complete like creating a collection, locking or unlocking a /// collection. The library handles that automatically for you. /// /// ```no_run /// use oo7::dbus::Service; /// /// # async fn run() -> oo7::Result<()> { /// let service = Service::new().await?; /// let collection = service.default_collection().await?; /// // Do something with the collection /// /// # Ok(()) /// } /// ``` #[derive(Debug)] pub struct Service<'a> { inner: Arc>, aes_key: Option>, session: Arc>, algorithm: Algorithm, } impl<'a> Service<'a> { /// Create a new instance of the Service, an encrypted communication would /// be attempted first and would fall back to a plain one if that fails. pub async fn new() -> Result, Error> { let service = match Self::encrypted().await { Ok(service) => Ok(service), Err(Error::Zbus(zbus::Error::MethodError(_, _, _))) => Self::plain().await, Err(e) => Err(e), }?; Ok(service) } /// Create a new instance of the Service with plain algorithm. pub async fn plain() -> Result, Error> { Self::with_algorithm(Algorithm::Plain).await } /// Create a new instance of the Service with encrypted algorithm. pub async fn encrypted() -> Result, Error> { Self::with_algorithm(Algorithm::Encrypted).await } /// Create a new instance of the Service. async fn with_algorithm(algorithm: Algorithm) -> Result, Error> { let cnx = zbus::Connection::session().await?; let service = Arc::new(api::Service::new(&cnx).await?); let (aes_key, session) = match algorithm { Algorithm::Plain => { #[cfg(feature = "tracing")] tracing::debug!("Starting an unencrypted Secret Service session"); let (_service_key, session) = service.open_session(None).await?; (None, session) } Algorithm::Encrypted => { #[cfg(feature = "tracing")] tracing::debug!("Starting an encrypted Secret Service session"); let private_key = Key::generate_private_key(); let public_key = Key::generate_public_key(&private_key); let (service_key, session) = service.open_session(Some(&public_key)).await?; let aes_key = service_key .map(|service_key| Arc::new(Key::generate_aes_key(&private_key, &service_key))); (aes_key, session) } }; Ok(Self { aes_key, inner: service, session: Arc::new(session), algorithm, }) } /// Retrieve the default collection. pub async fn default_collection(&self) -> Result, Error> { self.with_alias(DEFAULT_COLLECTION) .await? .ok_or_else(|| Error::NotFound(DEFAULT_COLLECTION.to_owned())) } /// Find a collection with it alias. /// /// Applications should make use of [`Service::default_collection`] instead. pub async fn with_alias(&self, alias: &str) -> Result>, Error> { Ok(self .inner .read_alias(alias) .await? .map(|collection| self.new_collection(collection))) } /// Get a list of all the available collections. pub async fn collections(&self) -> Result>, Error> { Ok(self .inner .collections() .await? .into_iter() .map(|collection| self.new_collection(collection)) .collect::>()) } /// Create a new collection. /// /// The alias can only be equal to [`DEFAULT_COLLECTION`] otherwise it must /// not be set. pub async fn create_collection( &self, label: &str, alias: Option<&str>, ) -> Result, Error> { self.inner .create_collection(label, alias) .await .map(|collection| self.new_collection(collection)) } /// Find a collection with it label. pub async fn with_label(&self, label: &str) -> Result>, Error> { let collections = self.collections().await?; for collection in collections.into_iter() { if collection.label().await? == label { return Ok(Some(collection)); } } Ok(None) } /// Stream yielding when new collections get created pub async fn receive_collection_created( &self, ) -> Result> + '_, Error> { Ok(self .inner .receive_collection_created() .await? .map(|collection| self.new_collection(collection))) } /// Stream yielding when existing collections get changed pub async fn receive_collection_changed( &self, ) -> Result> + '_, Error> { Ok(self .inner .receive_collection_changed() .await? .map(|collection| self.new_collection(collection))) } /// Stream yielding when existing collections get deleted pub async fn receive_collection_deleted( &self, ) -> Result, Error> { self.inner.receive_collection_deleted().await } // Get public `Collection` from `api::Collection` fn new_collection(&self, collection: api::Collection<'a>) -> Collection<'a> { Collection::new( Arc::clone(&self.inner), Arc::clone(&self.session), self.algorithm, collection, self.aes_key.clone(), // Cheap clone, it is an Arc, ) } } #[cfg(test)] #[cfg(feature = "tokio")] mod tests { #[cfg(feature = "local_tests")] use super::Service; #[tokio::test] #[cfg(feature = "local_tests")] async fn create_collection() { let service = Service::new().await.unwrap(); let collection = service.create_collection("somelabel", None).await.unwrap(); let found_collection = service.with_label("somelabel").await.unwrap(); assert!(found_collection.is_some()); assert_eq!( found_collection.unwrap().label().await.unwrap(), collection.label().await.unwrap() ); collection.delete().await.unwrap(); let found_collection = service.with_label("somelabel").await.unwrap(); assert!(found_collection.is_none()); } } oo7-0.3.3/src/error.rs000064400000000000000000000015271046102023000126260ustar 00000000000000use std::fmt; /// Alias for [`std::result::Result`] with the error type [`Error`]. pub type Result = std::result::Result; /// The error type for oo7. #[derive(Debug)] pub enum Error { /// File backend error. Portal(crate::portal::Error), /// Secret Service error. DBus(crate::dbus::Error), } impl From for Error { fn from(e: crate::portal::Error) -> Self { Self::Portal(e) } } impl From for Error { fn from(e: crate::dbus::Error) -> Self { Self::DBus(e) } } impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Portal(e) => write!(f, "Portal error {e}"), Self::DBus(e) => write!(f, "DBus error {e}"), } } } oo7-0.3.3/src/helpers.rs000064400000000000000000000060631046102023000131370ustar 00000000000000use std::path::PathBuf; #[cfg(feature = "async-std")] use async_fs::File; #[cfg(feature = "async-std")] use futures_lite::io::AsyncReadExt; #[cfg(feature = "tokio")] use tokio::{fs::File, io::AsyncReadExt}; pub(crate) fn data_dir() -> Option { std::env::var_os("XDG_DATA_HOME") .and_then(|h| if h.is_empty() { None } else { Some(h) }) .map(PathBuf::from) .and_then(|p| if p.is_absolute() { Some(p) } else { None }) .or_else(|| { std::env::var_os("HOME") .and_then(|h| if h.is_empty() { None } else { Some(h) }) .map(PathBuf::from) .map(|p| p.join(".local/share")) }) } pub(crate) async fn is_flatpak() -> bool { #[cfg(feature = "async-std")] { async_fs::metadata("/.flatpak-info").await.is_ok() } #[cfg(not(feature = "async-std"))] { std::path::PathBuf::from("/.flatpak-info").exists() } } pub(crate) async fn is_snap() -> bool { let pid = std::process::id(); let path = format!("/proc/{pid}/cgroup"); let mut file = match File::open(path).await { Ok(file) => file, Err(_) => return false, }; let mut buffer = String::new(); match file.read_to_string(&mut buffer).await { Ok(_) => cgroup_v2_is_snap(&buffer), Err(_) => false, } } fn cgroup_v2_is_snap(cgroups: &str) -> bool { cgroups .lines() .map(|line| { let (n, rest) = line.split_once(':')?; // Check that n is a number. n.parse::().ok()?; let unit = match rest.split_once(':') { Some(("", unit)) => Some(unit), Some(("freezer", unit)) => Some(unit), Some(("name=systemd", unit)) => Some(unit), _ => None, }?; let scope = std::path::Path::new(unit).file_name()?.to_str()?; Some(scope.starts_with("snap.")) }) .any(|x| x.unwrap_or(false)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_cgroup_v2_is_snap() { let data = "0::/user.slice/user-1000.slice/user@1000.service/apps.slice/snap.something.scope\n"; assert!(cgroup_v2_is_snap(data)); let data = "0::/user.slice/user-1000.slice/user@1000.service/apps.slice\n"; assert!(!cgroup_v2_is_snap(data)); let data = "12:pids:/user.slice/user-1000.slice/user@1000.service 11:perf_event:/ 10:net_cls,net_prio:/ 9:cpuset:/ 8:memory:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope 7:rdma:/ 6:devices:/user.slice 5:blkio:/user.slice 4:hugetlb:/ 3:freezer:/snap.portal-test 2:cpu,cpuacct:/user.slice 1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope 0::/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-228ae109-a869-4533-8988-65ea4c10b492.scope\n"; assert!(cgroup_v2_is_snap(data)); } } oo7-0.3.3/src/key.rs000064400000000000000000000110001046102023000122500ustar 00000000000000use zeroize::{Zeroize, ZeroizeOnDrop}; use zvariant::Type; use crate::{crypto, portal}; /// A key. #[derive(Debug, Zeroize, ZeroizeOnDrop)] pub struct Key { key: Vec, #[zeroize(skip)] strength: Result<(), portal::WeakKeyError>, } impl AsRef<[u8]> for Key { fn as_ref(&self) -> &[u8] { self.key.as_slice() } } impl AsMut<[u8]> for Key { fn as_mut(&mut self) -> &mut [u8] { &mut self.key } } impl Key { pub(crate) fn new(key: Vec) -> Self { Self::new_with_strength(key, Err(portal::WeakKeyError::StrengthUnknown)) } pub(crate) fn check_strength(&self) -> Result<(), portal::WeakKeyError> { self.strength } pub(crate) fn new_with_strength( key: Vec, strength: Result<(), portal::WeakKeyError>, ) -> Self { Self { key, strength } } pub(crate) fn generate_private_key() -> Self { Self::new(crypto::generate_private_key().to_vec()) } pub(crate) fn generate_public_key(private_key: &Self) -> Self { Self::new(crypto::generate_public_key(private_key)) } pub(crate) fn generate_aes_key(private_key: &Self, server_public_key: &Self) -> Self { Self::new(crypto::generate_aes_key(private_key, server_public_key).to_vec()) } } impl From<&Key> for zvariant::Value<'_> { fn from(key: &Key) -> Self { let mut array = zvariant::Array::new(u8::signature()); for byte in key.as_ref() { array .append(zvariant::Value::U8(*byte)) .expect("Element of valid type"); } array.into() } } impl From for Key { fn from(value: zvariant::OwnedValue) -> Self { let mut key = zeroize::Zeroizing::new(vec![]); for value in value.downcast_ref::().unwrap().inner() { key.push(value.downcast_ref::().unwrap()); } Key::new(key.to_vec()) } } #[cfg(test)] mod tests { use super::*; #[test] fn private_public_pair() { let private_key = Key::new(vec![ 41, 20, 63, 236, 246, 132, 109, 70, 172, 121, 45, 66, 129, 21, 247, 91, 96, 217, 56, 201, 205, 56, 17, 178, 202, 81, 71, 104, 233, 89, 87, 32, 88, 146, 107, 224, 56, 103, 111, 74, 143, 80, 170, 40, 5, 52, 48, 90, 75, 71, 193, 224, 222, 57, 91, 81, 66, 1, 6, 88, 137, 66, 102, 207, 55, 95, 67, 92, 140, 227, 242, 153, 185, 195, 89, 236, 146, 242, 88, 215, 1, 7, 135, 254, 85, 165, 236, 110, 22, 79, 107, 254, 149, 164, 243, 94, 129, 198, 45, 208, 132, 166, 0, 153, 243, 160, 255, 188, 59, 216, 99, 221, 85, 162, 116, 210, 160, 117, 201, 39, 179, 123, 107, 8, 242, 139, 207, 250, ]); let server_public_key = Key::new(vec![ 50, 233, 76, 88, 47, 206, 235, 107, 9, 232, 98, 14, 188, 214, 209, 77, 35, 66, 109, 119, 24, 191, 120, 90, 242, 198, 240, 115, 200, 66, 51, 180, 8, 164, 89, 9, 229, 31, 160, 31, 156, 101, 169, 60, 63, 247, 37, 255, 75, 198, 62, 235, 50, 29, 221, 245, 29, 248, 140, 209, 62, 215, 2, 137, 82, 77, 248, 242, 56, 176, 118, 183, 124, 74, 26, 133, 188, 47, 31, 141, 232, 194, 92, 18, 69, 3, 56, 153, 42, 9, 143, 81, 197, 159, 200, 197, 221, 74, 186, 157, 158, 36, 74, 125, 11, 234, 33, 2, 5, 36, 206, 248, 155, 157, 145, 159, 238, 19, 185, 194, 134, 3, 195, 198, 60, 100, 159, 31, ]); let expected_public_key = &[ 9, 192, 210, 81, 212, 191, 74, 119, 22, 172, 81, 142, 124, 89, 17, 71, 118, 190, 81, 71, 49, 149, 200, 204, 14, 47, 111, 165, 119, 103, 216, 102, 111, 93, 242, 64, 73, 224, 165, 11, 127, 219, 197, 188, 168, 222, 254, 10, 104, 81, 8, 206, 237, 119, 225, 100, 78, 196, 89, 163, 63, 169, 77, 236, 80, 241, 189, 49, 27, 40, 243, 229, 66, 53, 80, 86, 44, 213, 87, 186, 68, 55, 216, 56, 236, 51, 229, 44, 174, 18, 87, 141, 85, 71, 185, 203, 208, 144, 190, 117, 141, 255, 153, 106, 123, 28, 152, 200, 237, 189, 176, 20, 80, 211, 33, 158, 232, 194, 145, 45, 194, 35, 108, 106, 214, 221, 159, 137, ]; let expected_aes_key = &[ 132, 3, 113, 222, 81, 209, 49, 43, 81, 232, 243, 46, 1, 103, 184, 42, ]; let public_key = Key::generate_public_key(&private_key); let aes_key = Key::generate_aes_key(&private_key, &server_public_key); assert_eq!(public_key.as_ref(), expected_public_key); assert_eq!(aes_key.as_ref(), expected_aes_key); } } oo7-0.3.3/src/keyring.rs000064400000000000000000000254521046102023000131500ustar 00000000000000use std::{collections::HashMap, sync::Arc, time::Duration}; #[cfg(feature = "async-std")] use async_lock::RwLock; #[cfg(feature = "tokio")] use tokio::sync::RwLock; use zeroize::Zeroizing; use crate::{ dbus::{self, DEFAULT_COLLECTION}, portal, AsAttributes, Result, }; /// A [Secret Service](crate::dbus) or [file](crate::portal) backed keyring /// implementation. /// /// It will automatically use the file backend if the application is sandboxed /// and otherwise falls back to the DBus service. /// /// The File backend requires a [`org.freedesktop.portal.Secret`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html) implementation /// to retrieve the key that will be used to encrypt the backend file. #[derive(Debug)] pub enum Keyring { #[doc(hidden)] File(Arc), #[doc(hidden)] DBus(dbus::Collection<'static>), } impl Keyring { /// Create a new instance of the Keyring. pub async fn new() -> Result { let is_sandboxed = crate::is_sandboxed().await; if is_sandboxed { #[cfg(feature = "tracing")] tracing::debug!("Application is sandboxed, using the file backend"); match portal::Keyring::load_default().await { Ok(portal) => return Ok(Self::File(Arc::new(portal))), // Do nothing in this case, we are supposed to fallback to the host keyring Err(portal::Error::PortalNotAvailable) => { #[cfg(feature = "tracing")] tracing::debug!( "org.freedesktop.portal.Secrets is not available, falling back to the Sercret Service backend" ); } Err(e) => return Err(crate::Error::Portal(e)), }; } else { #[cfg(feature = "tracing")] tracing::debug!( "Application is not sandboxed, falling back to the Sercret Service backend" ); } let service = dbus::Service::new().await?; let collection = match service.default_collection().await { Ok(c) => Ok(c), Err(dbus::Error::NotFound(_)) => { #[cfg(feature = "tracing")] tracing::debug!("Default collection doesn't exists, trying to create it"); service .create_collection("Login", Some(DEFAULT_COLLECTION)) .await } Err(e) => Err(e), }?; Ok(Self::DBus(collection)) } /// Unlock the used collection if using the Secret service. /// /// The method does nothing if keyring is backed by a file backend. pub async fn unlock(&self) -> Result<()> { // No unlocking is needed for the file backend if let Self::DBus(backend) = self { backend.unlock().await?; }; Ok(()) } /// Lock the used collection if using the Secret service. /// /// The method does nothing if keyring is backed by a file backend. pub async fn lock(&self) -> Result<()> { // No locking is needed for the file backend if let Self::DBus(backend) = self { backend.lock().await?; }; Ok(()) } /// Remove items that matches the attributes. pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<()> { match self { Self::DBus(backend) => { let items = backend.search_items(attributes).await?; for item in items { item.delete().await?; } } Self::File(backend) => { backend.delete(attributes).await?; } }; Ok(()) } /// Retrieve all the items. /// /// If using the Secret Service, it will retrieve all the items in the /// [`DEFAULT_COLLECTION`]. pub async fn items(&self) -> Result> { let items = match self { Self::DBus(backend) => { let items = backend.items().await?; items.into_iter().map(Item::for_dbus).collect::>() } Self::File(backend) => { let items = backend.items().await; items .into_iter() // Ignore invalid items .flatten() .map(|i| Item::for_file(i, Arc::clone(backend))) .collect::>() } }; Ok(items) } /// Create a new item. pub async fn create_item( &self, label: &str, attributes: &impl AsAttributes, secret: impl AsRef<[u8]>, replace: bool, ) -> Result<()> { match self { Self::DBus(backend) => { backend .create_item(label, attributes, secret, replace, "text/plain") .await?; } Self::File(backend) => { backend .create_item(label, attributes, secret, replace) .await?; } }; Ok(()) } /// Find items based on their attributes. pub async fn search_items(&self, attributes: &impl AsAttributes) -> Result> { let items = match self { Self::DBus(backend) => { let items = backend.search_items(attributes).await?; items.into_iter().map(Item::for_dbus).collect::>() } Self::File(backend) => { let items = backend.search_items(attributes).await?; items .into_iter() .map(|i| Item::for_file(i, Arc::clone(backend))) .collect::>() } }; Ok(items) } } /// A generic secret with a label and attributes. #[derive(Debug)] pub enum Item { #[doc(hidden)] File(RwLock, Arc), #[doc(hidden)] DBus(dbus::Item<'static>), } impl Item { fn for_file(item: portal::Item, backend: Arc) -> Self { Self::File(RwLock::new(item), backend) } fn for_dbus(item: dbus::Item<'static>) -> Self { Self::DBus(item) } /// The item label. pub async fn label(&self) -> Result { let label = match self { Self::File(item, _) => item.read().await.label().to_owned(), Self::DBus(item) => item.label().await?, }; Ok(label) } /// Sets the item label. pub async fn set_label(&self, label: &str) -> Result<()> { match self { Self::File(item, backend) => { item.write().await.set_label(label); let item_guard = item.read().await; backend .create_item( item_guard.label(), &item_guard.attributes(), &*item_guard.secret(), true, ) .await?; } Self::DBus(item) => item.set_label(label).await?, }; Ok(()) } /// Retrieve the item attributes. pub async fn attributes(&self) -> Result> { let attributes = match self { Self::File(item, _) => item .read() .await .attributes() .iter() .map(|(k, v)| (k.to_owned(), v.to_string())) .collect::>(), Self::DBus(item) => item.attributes().await?, }; Ok(attributes) } /// Sets the item attributes. pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<()> { match self { Self::File(item, backend) => { item.write().await.set_attributes(attributes); let item_guard = item.read().await; backend .create_item(item_guard.label(), attributes, &*item_guard.secret(), true) .await?; } Self::DBus(item) => item.set_attributes(attributes).await?, }; Ok(()) } /// Sets a new secret. pub async fn set_secret(&self, secret: impl AsRef<[u8]>) -> Result<()> { match self { Self::File(item, backend) => { item.write().await.set_secret(secret); let item_guard = item.read().await; backend .create_item( item_guard.label(), &item_guard.attributes(), &*item_guard.secret(), true, ) .await?; } Self::DBus(item) => item.set_secret(secret, "text/plain").await?, }; Ok(()) } /// Retrieves the stored secret. pub async fn secret(&self) -> Result>> { let secret = match self { Self::File(item, _) => item.read().await.secret(), Self::DBus(item) => item.secret().await?, }; Ok(secret) } /// Whether the item is locked or not /// /// The method always returns `false` if keyring is backed by a file /// backend. pub async fn is_locked(&self) -> Result { if let Self::DBus(item) = self { item.is_locked().await.map_err(From::from) } else { Ok(false) } } /// Lock the item /// /// The method does nothing if keyring is backed by a file backend. pub async fn lock(&self) -> Result<()> { if let Self::DBus(item) = self { item.lock().await?; } Ok(()) } /// Unlock the item /// /// The method does nothing if keyring is backed by a file backend. pub async fn unlock(&self) -> Result<()> { if let Self::DBus(item) = self { item.unlock().await?; } Ok(()) } /// Delete the item. pub async fn delete(&self) -> Result<()> { match self { Self::File(item, backend) => { let item_guard = item.read().await; backend.delete(&item_guard.attributes()).await?; } Self::DBus(item) => { item.delete().await?; } }; Ok(()) } /// The UNIX time when the item was created. pub async fn created(&self) -> Result { match self { Self::DBus(item) => Ok(item.created().await?), Self::File(item, _) => Ok(item.read().await.created()), } } /// The UNIX time when the item was modified. pub async fn modified(&self) -> Result { match self { Self::DBus(item) => Ok(item.modified().await?), Self::File(item, _) => Ok(item.read().await.modified()), } } } oo7-0.3.3/src/lib.rs000064400000000000000000000047301046102023000122420ustar 00000000000000#![cfg_attr(docsrs, feature(doc_cfg))] #![deny(rustdoc::broken_intra_doc_links)] #![doc = include_str!("../README.md")] #[cfg(all(all(feature = "tokio", feature = "async-std"), not(doc)))] compile_error!("You can't enable both async-std & tokio features at once"); use std::collections::{BTreeMap, HashMap}; mod error; mod key; mod migration; #[cfg(feature = "unstable")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub use key::Key; #[cfg(not(feature = "unstable"))] pub(crate) use key::Key; mod crypto; pub mod dbus; pub mod portal; mod helpers; mod keyring; pub use error::{Error, Result}; pub use keyring::{Item, Keyring}; pub use migration::migrate; pub use zbus; /// Checks whether the application is sandboxed or not. pub async fn is_sandboxed() -> bool { helpers::is_flatpak().await || helpers::is_snap().await } /// An item/collection attributes. pub trait AsAttributes { fn as_attributes(&self) -> HashMap<&str, &str>; fn hash<'a>(&'a self, key: &Key) -> Vec<(&'a str, zeroize::Zeroizing>)> { self.as_attributes() .into_iter() .map(|(k, v)| (k, crate::portal::AttributeValue::from(v).mac(key))) .collect() } } impl AsAttributes for &HashMap where K: AsRef, V: AsRef, { fn as_attributes(&self) -> HashMap<&str, &str> { self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect() } } impl AsAttributes for HashMap where K: AsRef, V: AsRef, { fn as_attributes(&self) -> HashMap<&str, &str> { self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect() } } impl AsAttributes for BTreeMap where K: AsRef, V: AsRef, { fn as_attributes(&self) -> HashMap<&str, &str> { self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect() } } impl AsAttributes for &BTreeMap where K: AsRef, V: AsRef, { fn as_attributes(&self) -> HashMap<&str, &str> { self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect() } } impl AsAttributes for Vec<(K, V)> where K: AsRef, V: AsRef, { fn as_attributes(&self) -> HashMap<&str, &str> { self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect() } } impl AsAttributes for &Vec<(K, V)> where K: AsRef, V: AsRef, { fn as_attributes(&self) -> HashMap<&str, &str> { self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect() } } oo7-0.3.3/src/migration.rs000064400000000000000000000025751046102023000134720ustar 00000000000000use crate::{dbus::Service, portal::Keyring, AsAttributes, Result}; /// Helper to migrate your secrets from the host Secret Service /// to the sandboxed file backend. /// /// If the migration is successful, the items are removed from the host /// Secret Service. pub async fn migrate(attributes: Vec, replace: bool) -> Result<()> { let service = Service::new().await?; let file_backend = match Keyring::load_default().await { Ok(portal) => Ok(portal), Err(crate::portal::Error::PortalNotAvailable) => { #[cfg(feature = "tracing")] tracing::debug!("Portal not available, no migration to do"); return Ok(()); } Err(err) => Err(err), }?; let collection = service.default_collection().await?; let mut all_items = Vec::default(); for attrs in attributes { let items = collection.search_items(&attrs).await?; all_items.extend(items); } let mut new_items = Vec::with_capacity(all_items.capacity()); for item in all_items.iter() { let attributes = item.attributes().await?; let label = item.label().await?; let secret = item.secret().await?; new_items.push((label, attributes, secret, replace)); } file_backend.create_items(new_items).await?; for item in all_items.iter() { item.delete().await?; } Ok(()) } oo7-0.3.3/src/portal/api/attribute_value.rs000064400000000000000000000014701046102023000167430ustar 00000000000000use serde::{Deserialize, Serialize}; use zbus::zvariant::Type; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use crate::{crypto, Key}; /// An encrypted attribute value. #[derive(Deserialize, Serialize, Type, Clone, Debug, Eq, PartialEq, Zeroize, ZeroizeOnDrop)] pub struct AttributeValue(String); impl AttributeValue { pub(crate) fn mac(&self, key: &Key) -> Zeroizing> { Zeroizing::new(crypto::compute_mac(self.0.as_bytes(), key)) } } impl From for AttributeValue { fn from(value: S) -> Self { Self(value.to_string()) } } impl AsRef for AttributeValue { fn as_ref(&self) -> &str { self.0.as_str() } } impl std::ops::Deref for AttributeValue { type Target = str; fn deref(&self) -> &Self::Target { self.0.as_str() } } oo7-0.3.3/src/portal/api/encrypted_item.rs000064400000000000000000000033121046102023000165540ustar 00000000000000use std::collections::HashMap; use serde::{Deserialize, Serialize}; use zbus::zvariant::Type; use super::{Error, Item}; use crate::{crypto, Key}; #[derive(Deserialize, Serialize, Type, Debug, Clone)] pub(crate) struct EncryptedItem { pub(crate) hashed_attributes: HashMap>, pub(crate) blob: Vec, } impl EncryptedItem { pub fn has_attribute(&self, key: &str, blob: &[u8]) -> bool { self.hashed_attributes.get(key).map(|b| b.as_slice()) == Some(blob) } pub fn decrypt(mut self, key: &Key) -> Result { let mac_tag = self.blob.split_off(self.blob.len() - crypto::mac_len()); // verify item if !crypto::verify_mac(&self.blob, key, mac_tag) { return Err(Error::MacError); } let iv = self.blob.split_off(self.blob.len() - crypto::iv_len()); // decrypt item let decrypted = crypto::decrypt(self.blob, key, iv); let item = Item::try_from(decrypted.as_slice())?; Self::validate(&self.hashed_attributes, &item, key)?; Ok(item) } fn validate( hashed_attributes: &HashMap>, item: &Item, key: &Key, ) -> Result<(), Error> { for (attribute_key, hashed_attribute) in hashed_attributes.iter() { if let Some(attribute_plaintext) = item.attributes().get(attribute_key) { if !crypto::verify_mac(attribute_plaintext.as_bytes(), key, hashed_attribute) { return Err(Error::HashedAttributeMac(attribute_key.to_owned())); } } else { return Err(Error::HashedAttributeMac(attribute_key.to_owned())); } } Ok(()) } } oo7-0.3.3/src/portal/api/legacy_keyring.rs000064400000000000000000000220561046102023000165430ustar 00000000000000//! Legacy GNOME Keyring file format low level API. use std::{ collections::HashMap, io::{self, Cursor, Read}, }; use endi::{Endian, ReadBytes}; use super::{Item, Secret}; use crate::{ crypto, portal::{AttributeValue, Error, WeakKeyError}, AsAttributes, }; const FILE_HEADER: &[u8] = b"GnomeKeyring\n\r\0\n"; const FILE_HEADER_LEN: usize = FILE_HEADER.len(); pub const MAJOR_VERSION: u8 = 0; pub const MINOR_VERSION: u8 = 0; #[derive(Debug)] pub struct Keyring { salt: Vec, iteration_count: u32, encrypted_content: Vec, item_count: usize, } impl Keyring { pub fn decrypt_items(self, secret: &Secret) -> Result, Error> { let (key, iv) = crypto::legacy_derive_key_and_iv( &**secret, self.key_strength(secret), &self.salt, self.iteration_count.try_into().unwrap(), ); let decrypted = crypto::decrypt_no_padding(&self.encrypted_content, &key, iv); let (digest, content) = decrypted.split_at(16); if !crypto::verify_checksum_md5(digest, content) { return Err(Error::ChecksumMismatch); } self.read_items(content) } fn read_attributes<'a>( cursor: &mut Cursor<&'a [u8]>, count: usize, ) -> Result { let mut result = HashMap::new(); for _ in 0..count { let name = Self::read_string(cursor)?.ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, "empty attribute name") })?; let value: AttributeValue = match cursor.read_u32(Endian::Big)? { 0 => Self::read_string(cursor)? .ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, "empty attribute value") })? .into(), 1 => cursor.read_u32(Endian::Big)?.into(), _ => { return Err(io::Error::new( io::ErrorKind::InvalidInput, "unknown attribute type", ) .into()) } }; result.insert(name, value); } Ok(result) } fn read_items(self, decrypted: &[u8]) -> Result, Error> { let mut cursor = Cursor::new(decrypted); let mut items = Vec::new(); for _ in 0..self.item_count { let display_name = Self::read_string(&mut cursor)? .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "empty item label"))?; let secret = Self::read_byte_array(&mut cursor)? .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "empty item secret"))?; let _created_time = Self::read_time(&mut cursor)?; let _modified_time = Self::read_time(&mut cursor)?; let _reserved = Self::read_string(&mut cursor)?; for _ in 0..4 { let _ = cursor.read_u32(Endian::Big)?; } let attribute_count = cursor.read_u32(Endian::Big)? as usize; let attributes = Self::read_attributes(&mut cursor, attribute_count)?; items.push(Item::new(display_name, &attributes, secret)); let acl_count = cursor.read_u32(Endian::Big)? as usize; Self::skip_acls(&mut cursor, acl_count)?; } Ok(items) } fn key_strength(&self, _secret: &[u8]) -> Result<(), WeakKeyError> { Ok(()) } fn read_byte_array<'a>(cursor: &mut Cursor<&'a [u8]>) -> Result, Error> { let len = cursor.read_u32(Endian::Big)? as usize; if len == 0xffffffff { Ok(None) } else if len >= 0x7fffffff { Err(io::Error::new(io::ErrorKind::OutOfMemory, "").into()) } else if len > cursor.get_ref().len() { Err(Error::NoData) } else { let pos = cursor.position() as usize; let bytes = &cursor.get_ref()[pos..pos + len]; cursor.set_position((pos + len) as u64); Ok(Some(bytes)) } } fn read_string<'a>(cursor: &mut Cursor<&'a [u8]>) -> Result, Error> { match Self::read_byte_array(cursor) { Ok(Some(bytes)) => Ok(Some(std::str::from_utf8(bytes)?)), Ok(None) => Ok(None), Err(e) => Err(e), } } fn read_time(cursor: &mut Cursor<&[u8]>) -> Result { let hi = cursor.read_u32(Endian::Big)? as u64; let lo = cursor.read_u32(Endian::Big)? as u64; Ok((hi << 32) | lo) } fn skip_hashed_items(cursor: &mut Cursor<&[u8]>, count: usize) -> Result<(), Error> { for _ in 0..count { let _id = cursor.read_u32(Endian::Big)?; let _type = cursor.read_u32(Endian::Big)?; let num_attributes = cursor.read_u32(Endian::Big)?; for _ in 0..num_attributes { let _name = Self::read_string(cursor)?; match cursor.read_u32(Endian::Big)? { 0 => { let _value = Self::read_string(cursor); } 1 => { let _value = cursor.read_u32(Endian::Big); } _ => { return Err(io::Error::new( io::ErrorKind::InvalidInput, "unknown attribute type", ) .into()) } } } } Ok(()) } fn skip_acls(cursor: &mut Cursor<&[u8]>, count: usize) -> Result<(), Error> { for _ in 0..count { let _flags = cursor.read_u32(Endian::Big)?; let _display_name = Self::read_string(cursor)?; let _path = Self::read_string(cursor)?; let _reserved0 = Self::read_string(cursor)?; let _reserved1 = cursor.read_u32(Endian::Big)?; } Ok(()) } fn parse(data: &[u8]) -> Result { let mut cursor = Cursor::new(data); let crypto = cursor.read_u8(Endian::Big)?; if crypto != 0 { return Err(Error::AlgorithmMismatch(crypto)); } let hash = cursor.read_u8(Endian::Big)?; if hash != 0 { return Err(Error::AlgorithmMismatch(hash)); } let _display_name = Self::read_string(&mut cursor)?; let _created_time = Self::read_time(&mut cursor)?; let _modified_time = Self::read_time(&mut cursor)?; let _flags = cursor.read_u32(Endian::Big)?; let _lock_timeout = cursor.read_u32(Endian::Big)?; let iteration_count = cursor.read_u32(Endian::Big)?; let mut salt = vec![0; 8]; cursor.read_exact(salt.as_mut_slice())?; for _ in 0..4 { let _ = cursor.read_u32(Endian::Big)?; } let item_count = cursor.read_u32(Endian::Big)? as usize; Self::skip_hashed_items(&mut cursor, item_count)?; let mut size = cursor.read_u32(Endian::Big)? as usize; let pos = cursor.position() as usize; if size > cursor.get_ref()[pos..].len() { return Err(Error::NoData); } if size % 16 != 0 { size = (size / 16) * 16; } let encrypted_content = Vec::from(&cursor.get_ref()[pos..pos + size]); Ok(Self { salt, iteration_count, encrypted_content, item_count, }) } } impl TryFrom<&[u8]> for Keyring { type Error = Error; fn try_from(value: &[u8]) -> Result { let header = value.get(..FILE_HEADER.len()); if header != Some(FILE_HEADER) { return Err(Error::FileHeaderMismatch( header.map(|x| String::from_utf8_lossy(x).to_string()), )); } let version = value.get(FILE_HEADER_LEN..(FILE_HEADER_LEN + 2)); if version != Some(&[MAJOR_VERSION, MINOR_VERSION]) { return Err(Error::VersionMismatch(version.map(|x| x.to_vec()))); } if let Some(data) = value.get((FILE_HEADER_LEN + 2)..) { Self::parse(data) } else { Err(Error::NoData) } } } #[cfg(test)] mod tests { use std::path::PathBuf; use super::*; #[test] fn legacy_decrypt() -> Result<(), Error> { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("fixtures") .join("legacy.keyring"); let blob = std::fs::read(path)?; let keyring = Keyring::try_from(blob.as_slice())?; let password = b"test"; let secret = Secret::from(password.to_vec()); let items = keyring.decrypt_items(&secret)?; assert_eq!(items.len(), 1); assert_eq!(items[0].label(), "foo"); assert_eq!(items[0].secret().as_ref(), b"foo".to_vec()); let attributes = items[0].attributes(); assert_eq!(attributes.len(), 1); assert_eq!( attributes.get("xdg:schema").map(|v| v.as_ref()), Some("org.gnome.keyring.Note") ); Ok(()) } } oo7-0.3.3/src/portal/api/mod.rs000064400000000000000000000252261046102023000143300ustar 00000000000000//! GNOME Keyring file format low level API. // TODO: // - Order user calls // - Keep proxis around // - Make more things async #[cfg(feature = "async-std")] use std::io; use std::{ path::{Path, PathBuf}, sync::OnceLock, }; #[cfg(feature = "async-std")] use async_fs as fs; #[cfg(feature = "async-std")] use async_fs::unix::OpenOptionsExt; #[cfg(feature = "async-std")] use futures_lite::AsyncWriteExt; use rand::Rng; use serde::{Deserialize, Serialize}; #[cfg(feature = "tokio")] use tokio::{fs, io, io::AsyncWriteExt}; use zbus::zvariant::{serialized::Context, Endian, Type}; /// Used for newly created [`Keyring`]s const DEFAULT_ITERATION_COUNT: u32 = 100000; /// Used for newly created [`Keyring`]s const DEFAULT_SALT_SIZE: usize = 32; const MIN_ITERATION_COUNT: u32 = 100000; const MIN_SALT_SIZE: usize = 32; // FIXME: choose a reasonable value const MIN_PASSWORD_LENGTH: usize = 4; const FILE_HEADER: &[u8] = b"GnomeKeyring\n\r\0\n"; const FILE_HEADER_LEN: usize = FILE_HEADER.len(); pub(super) const MAJOR_VERSION: u8 = 1; const MINOR_VERSION: u8 = 0; mod attribute_value; mod encrypted_item; mod legacy_keyring; pub use attribute_value::AttributeValue; pub(super) use encrypted_item::EncryptedItem; pub(super) use legacy_keyring::{Keyring as LegacyKeyring, MAJOR_VERSION as LEGACY_MAJOR_VERSION}; use super::{Item, Secret}; use crate::{ crypto, portal::{Error, WeakKeyError}, AsAttributes, Key, }; pub(super) fn gvariant_encoding() -> &'static Context { static ENCODING: OnceLock = OnceLock::new(); ENCODING.get_or_init(|| Context::new_gvariant(Endian::Little, 0)) } /// Logical contents of a keyring file #[derive(Deserialize, Serialize, Type, Debug)] pub struct Keyring { salt_size: u32, salt: Vec, iteration_count: u32, modified_time: u64, usage_count: u32, pub(in crate::portal) items: Vec, } impl Keyring { #[allow(clippy::new_without_default)] pub(crate) fn new() -> Self { let salt = rand::thread_rng().gen::<[u8; DEFAULT_SALT_SIZE]>().to_vec(); Self { salt_size: salt.len() as u32, salt, iteration_count: DEFAULT_ITERATION_COUNT, // TODO: UTC? modified_time: std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap() .as_secs(), usage_count: 0, items: Vec::new(), } } pub fn key_strength(&self, secret: &[u8]) -> Result<(), WeakKeyError> { if self.iteration_count < MIN_ITERATION_COUNT { Err(WeakKeyError::IterationCountTooLow(self.iteration_count)) } else if self.salt.len() < MIN_SALT_SIZE { Err(WeakKeyError::SaltTooShort(self.salt.len())) } else if secret.len() < MIN_PASSWORD_LENGTH { Err(WeakKeyError::PasswordTooShort(secret.len())) } else { Ok(()) } } /// Write to a keyring file pub async fn dump( &mut self, path: impl AsRef, mtime: Option, ) -> Result<(), Error> { let tmp_path = if let Some(parent) = path.as_ref().parent() { let rnd: String = rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .take(16) .map(char::from) .collect(); let mut tmp_path = parent.to_path_buf(); tmp_path.push(format!(".tmpkeyring{rnd}")); if !parent.exists() { #[cfg(feature = "tracing")] tracing::debug!("Parent directory {:?} doesn't exists, creating it", parent); fs::DirBuilder::new().recursive(true).create(parent).await?; } Ok(tmp_path) } else { Err(Error::NoParentDir(path.as_ref().display().to_string())) }?; #[cfg(feature = "tracing")] tracing::debug!( "Created a temporary file to store the keyring on {:?}", tmp_path ); let mut tmpfile_builder = fs::OpenOptions::new(); tmpfile_builder.write(true).create_new(true); tmpfile_builder.mode(0o600); let mut tmpfile = tmpfile_builder.open(&tmp_path).await?; self.modified_time = std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap() .as_secs(); self.usage_count += 1; let blob = self.as_bytes()?; tmpfile.write_all(&blob).await?; tmpfile.sync_all().await?; let target_file = fs::File::open(path.as_ref()).await; let target_mtime = match target_file { Err(err) if err.kind() == io::ErrorKind::NotFound => None, Err(err) => return Err(err.into()), Ok(file) => file.metadata().await?.modified().ok(), }; if mtime != target_mtime { return Err(Error::TargetFileChanged( path.as_ref().display().to_string(), )); } fs::rename(tmp_path, path.as_ref()).await?; Ok(()) } pub fn search_items( &self, attributes: &impl AsAttributes, key: &Key, ) -> Result, Error> { let hashed_search = attributes.hash(key); self.items .iter() .filter(|e| hashed_search.iter().all(|(k, v)| e.has_attribute(k, v))) .map(|e| (*e).clone().decrypt(key)) .collect() } pub fn lookup_item( &self, attributes: &impl AsAttributes, key: &Key, ) -> Result, Error> { let hashed_search = attributes.hash(key); self.items .iter() .find(|e| hashed_search.iter().all(|(k, v)| e.has_attribute(k, v))) .map(|e| (*e).clone().decrypt(key)) .transpose() } pub fn remove_items(&mut self, attributes: &impl AsAttributes, key: &Key) -> Result<(), Error> { let hashed_search = attributes.hash(key); let (remove, keep): (Vec, _) = self .items .clone() .into_iter() .partition(|e| hashed_search.iter().all(|(k, v)| e.has_attribute(k, v))); // check hashes for the ones to be removed for item in remove { item.decrypt(key)?; } self.items = keep; Ok(()) } fn as_bytes(&self) -> Result, Error> { let mut blob = FILE_HEADER.to_vec(); blob.push(MAJOR_VERSION); blob.push(MINOR_VERSION); blob.append(&mut zvariant::to_bytes(*gvariant_encoding(), &self)?.to_vec()); Ok(blob) } pub(crate) fn path(name: &str, version: u8) -> Result { if let Some(mut path) = crate::helpers::data_dir() { path.push("keyrings"); if version > 0 { path.push(format!("v{}", version)); } path.push(format!("{}.keyring", name)); Ok(path) } else { Err(Error::NoDataDir) } } pub fn default_path() -> Result { Self::path("default", LEGACY_MAJOR_VERSION) } pub fn derive_key(&self, secret: &Secret) -> Key { crypto::derive_key( &**secret, self.key_strength(secret), &self.salt, self.iteration_count.try_into().unwrap(), ) } // Reset Keyring content pub(crate) fn reset(&mut self) { let salt = rand::thread_rng().gen::<[u8; DEFAULT_SALT_SIZE]>().to_vec(); self.salt_size = salt.len() as u32; self.salt = salt; self.iteration_count = DEFAULT_ITERATION_COUNT; self.usage_count = 0; self.items = Vec::new(); } } impl TryFrom<&[u8]> for Keyring { type Error = Error; fn try_from(value: &[u8]) -> Result { let header = value.get(..FILE_HEADER.len()); if header != Some(FILE_HEADER) { return Err(Error::FileHeaderMismatch( header.map(|x| String::from_utf8_lossy(x).to_string()), )); } let version = value.get(FILE_HEADER_LEN..(FILE_HEADER_LEN + 2)); if version != Some(&[MAJOR_VERSION, MINOR_VERSION]) { return Err(Error::VersionMismatch(version.map(|x| x.to_vec()))); } if let Some(data) = value.get((FILE_HEADER_LEN + 2)..) { let keyring: Self = zvariant::serialized::Data::new(data, *gvariant_encoding()) .deserialize()? .0; if keyring.salt.len() != keyring.salt_size as usize { Err(Error::SaltSizeMismatch( keyring.salt.len(), keyring.salt_size, )) } else { Ok(keyring) } } else { Err(Error::NoData) } } } #[cfg(test)] #[cfg(feature = "tokio")] mod tests { use std::collections::HashMap; use super::*; const SECRET: [u8; 64] = [ 44, 173, 251, 20, 203, 56, 241, 169, 91, 54, 51, 244, 40, 40, 202, 92, 71, 233, 174, 17, 145, 58, 7, 107, 31, 204, 175, 245, 112, 174, 31, 198, 162, 149, 13, 127, 119, 113, 13, 3, 191, 143, 162, 153, 183, 7, 21, 116, 81, 45, 51, 198, 73, 127, 147, 40, 52, 25, 181, 188, 48, 159, 0, 146, ]; #[tokio::test] async fn keyfile_add_remove() -> Result<(), Error> { let needle = HashMap::from([("key", "value")]); let mut keyring = Keyring::new(); let key = keyring.derive_key(&SECRET.to_vec().into()); keyring .items .push(Item::new("Label", &needle, b"MyPassword").encrypt(&key)?); assert_eq!(keyring.search_items(&needle, &key)?.len(), 1); keyring.remove_items(&needle, &key)?; assert_eq!(keyring.search_items(&needle, &key)?.len(), 0); Ok(()) } #[tokio::test] async fn keyfile_dump_load() -> Result<(), Error> { let _silent = std::fs::remove_file("/tmp/test.keyring"); let mut new_keyring = Keyring::new(); let key = new_keyring.derive_key(&SECRET.to_vec().into()); new_keyring.items.push( Item::new( "My Label", &HashMap::from([("my-tag", "my tag value")]), "A Password".as_bytes(), ) .encrypt(&key)?, ); new_keyring.dump("/tmp/test.keyring", None).await?; let blob = tokio::fs::read("/tmp/test.keyring").await?; let loaded_keyring = Keyring::try_from(blob.as_slice())?; let loaded_items = loaded_keyring.search_items(&HashMap::from([("my-tag", "my tag value")]), &key)?; assert_eq!(*loaded_items[0].secret(), "A Password".as_bytes()); let _silent = std::fs::remove_file("/tmp/test.keyring"); Ok(()) } } oo7-0.3.3/src/portal/error.rs000064400000000000000000000136741046102023000141350ustar 00000000000000/// File backend specific errors. #[derive(Debug)] pub enum Error { /// File header does not match `FILE_HEADER`. FileHeaderMismatch(Option), /// Version bytes do not match `MAJOR_VERSION` or `MINOR_VERSION`. VersionMismatch(Option>), /// No data behind header and version bytes. NoData, /// No Parent directory. NoParentDir(String), /// Bytes don't have the expected GVariant format. GVariantDeserialization(zvariant::Error), /// Mismatch between array length and length explicitly stored in keyring SaltSizeMismatch(usize, u32), /// Key for some reason too weak to trust it for writing WeakKey(WeakKeyError), /// Input/Output. Io(std::io::Error), /// Unexpected MAC digest value. MacError, /// Mismatch of checksum calculated over data. ChecksumMismatch, /// Failure to validate the attributes. HashedAttributeMac(String), /// XDG_DATA_HOME required for reading from default location. NoDataDir, /// Target file has changed. TargetFileChanged(String), /// Portal DBus communication error. PortalBus(zbus::Error), /// Portal request has been cancelled. CancelledPortalRequest, /// If the portal is not available on the host. /// Can happen if the host has an old xdg-desktop-portal /// or no secret service is available to store the secret. PortalNotAvailable, /// The addressed index does not exist. InvalidItemIndex(usize), /// UTF-8 encoding error. Utf8(std::str::Utf8Error), /// Mismatch of algorithms used in legacy keyring file. AlgorithmMismatch(u8), } impl From for Error { fn from(value: zvariant::Error) -> Self { Self::GVariantDeserialization(value) } } impl From for Error { fn from(value: WeakKeyError) -> Self { Self::WeakKey(value) } } impl From for Error { fn from(value: std::io::Error) -> Self { Self::Io(value) } } impl From for Error { fn from(value: zbus::Error) -> Self { Self::PortalBus(value) } } impl From for Error { fn from(value: std::str::Utf8Error) -> Self { Self::Utf8(value) } } impl std::error::Error for Error {} impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::FileHeaderMismatch(e) => { write!(f, "File header doesn't match FILE_HEADER {e:#?}") } Error::VersionMismatch(e) => write!( f, "Version doesn't match MAJOR_VERSION OR MICRO_VERSION {e:#?}", ), Error::NoData => write!(f, "No data behind header and version bytes"), Error::NoParentDir(e) => write!(f, "No Parent Directory {e}"), Error::GVariantDeserialization(e) => write!(f, "Failed to deserialize {e}"), Error::SaltSizeMismatch(arr, explicit) => write!( f, "Salt size is not as expected. Array: {arr}, Explicit: {explicit}" ), Error::WeakKey(err) => write!(f, "{err}"), Error::Io(e) => write!(f, "IO error {e}"), Error::MacError => write!(f, "Mac digest is not equal to the expected value"), Error::ChecksumMismatch => write!(f, "Checksum is not equal to the expected value"), Error::HashedAttributeMac(e) => write!(f, "Failed to validate hashed attribute {e}"), Error::NoDataDir => write!(f, "Couldn't retrieve XDG_DATA_DIR"), Error::TargetFileChanged(e) => write!(f, "The target file has changed {e}"), Error::PortalBus(e) => write!(f, "Portal communication failed {e}"), Error::CancelledPortalRequest => write!(f, "Portal request was cancelled"), Error::PortalNotAvailable => write!(f, "xdg-desktop-portal is too old on the host or secret service not available to store the secret"), Error::InvalidItemIndex(index) => write!(f, "The addressed item index {index} does not exist"), Error::Utf8(e) => write!(f, "UTF-8 encoding error {e}"), Error::AlgorithmMismatch(e) => write!(f, "Unknown algorithm {e}"), } } } #[derive(Debug)] /// All information that is available about an invalid (not decryptable) /// [`Item`](super::Item) pub struct InvalidItemError { error: Error, attribute_names: Vec, } impl InvalidItemError { pub(super) fn new(error: Error, attribute_names: Vec) -> Self { Self { error, attribute_names, } } } impl std::error::Error for InvalidItemError {} impl std::fmt::Display for InvalidItemError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Invalid item: {:?}. Property names: {:?}", self.error, self.attribute_names ) } } /// Details about why an encryption key is consider too weak for writing #[derive(Debug, Copy, Clone)] pub enum WeakKeyError { /// Avoid attack on existing files IterationCountTooLow(u32), /// Avoid attack on existing files SaltTooShort(usize), /// Just not secure enough to store password PasswordTooShort(usize), /// Should not occur /// /// Used by [`dbus`](crate::dbus) module that does not currently /// check key strength. StrengthUnknown, } impl std::error::Error for WeakKeyError {} impl std::fmt::Display for WeakKeyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::IterationCountTooLow(count) => write!(f, "Iteration count too low: {count}"), Self::SaltTooShort(length) => write!(f, "Salt too short: {length}"), Self::PasswordTooShort(length) => { write!(f, "Password (secret from portal) too short: {length}") } Self::StrengthUnknown => write!(f, "Strength unknown"), } } } oo7-0.3.3/src/portal/item.rs000064400000000000000000000072001046102023000137260ustar 00000000000000use std::{collections::HashMap, time::Duration}; use serde::{Deserialize, Serialize}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use super::{ api::{gvariant_encoding, AttributeValue, EncryptedItem}, Error, }; use crate::{crypto, AsAttributes, Key}; /// An item stored in the file backend. #[derive(Deserialize, Serialize, zvariant::Type, Clone, Debug, Zeroize, ZeroizeOnDrop)] pub struct Item { #[zeroize(skip)] attributes: HashMap, #[zeroize(skip)] label: String, #[zeroize(skip)] created: u64, #[zeroize(skip)] modified: u64, secret: Vec, } impl Item { pub(crate) fn new( label: impl ToString, attributes: &impl AsAttributes, secret: impl AsRef<[u8]>, ) -> Self { let now = std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap() .as_secs(); Self { attributes: attributes .as_attributes() .into_iter() .map(|(k, v)| (k.to_string(), v.into())) .collect(), label: label.to_string(), created: now, modified: now, secret: secret.as_ref().to_vec(), } } /// Retrieve the item attributes. pub fn attributes(&self) -> &HashMap { &self.attributes } /// Update the item attributes. pub fn set_attributes(&mut self, attributes: &impl AsAttributes) { self.attributes = attributes .as_attributes() .into_iter() .map(|(k, v)| (k.to_string(), v.into())) .collect(); } /// The item label. pub fn label(&self) -> &str { &self.label } /// Set the item label. pub fn set_label(&mut self, label: impl ToString) { self.modified = std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap() .as_secs(); self.label = label.to_string(); } /// Retrieve the currently stored secret. pub fn secret(&self) -> Zeroizing> { Zeroizing::new(self.secret.clone()) } /// Store a new secret. pub fn set_secret(&mut self, secret: impl AsRef<[u8]>) { self.modified = std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap() .as_secs(); self.secret = secret.as_ref().to_vec(); } /// The UNIX time when the item was created. pub fn created(&self) -> Duration { let secs = self.created; Duration::from_secs(secs) } /// The UNIX time when the item was modified. pub fn modified(&self) -> Duration { let secs = self.modified; Duration::from_secs(secs) } pub(crate) fn encrypt(&self, key: &Key) -> Result { key.check_strength()?; let decrypted = Zeroizing::new(zvariant::to_bytes(*gvariant_encoding(), &self)?.to_vec()); let iv = crypto::generate_iv(); let mut blob = crypto::encrypt(&*decrypted, key, &iv); blob.append(&mut iv.as_slice().into()); blob.append(&mut crypto::compute_mac(&blob, key).as_slice().into()); let hashed_attributes = self .attributes .iter() .map(|(k, v)| (k.to_owned(), v.mac(key).as_slice().into())) .collect(); Ok(EncryptedItem { hashed_attributes, blob, }) } } impl TryFrom<&[u8]> for Item { type Error = Error; fn try_from(value: &[u8]) -> Result { Ok(zvariant::serialized::Data::new(value, *gvariant_encoding()) .deserialize()? .0) } } oo7-0.3.3/src/portal/mod.rs000064400000000000000000000542731046102023000135630ustar 00000000000000//! File backend implementation backed by the [Secret portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html). //! //! ```no_run //! use std::collections::HashMap; //! //! use oo7::portal::Keyring; //! //! # async fn run() -> oo7::Result<()> { //! let keyring = Keyring::load_default().await?; //! keyring //! .create_item( //! "My Label", //! &HashMap::from([("account", "alice")]), //! b"My Password", //! true, //! ) //! .await?; //! //! let items = keyring //! .search_items(&HashMap::from([("account", "alice")])) //! .await?; //! assert_eq!(*items[0].secret(), b"My Password"); //! //! keyring //! .delete(&HashMap::from([("account", "alice")])) //! .await?; //! # Ok(()) //! # } //! ``` #[cfg(feature = "async-std")] use std::io; use std::{ collections::HashMap, path::{Path, PathBuf}, sync::Arc, }; #[cfg(feature = "async-std")] use async_fs as fs; #[cfg(feature = "async-std")] use async_lock::{Mutex, RwLock}; #[cfg(feature = "async-std")] use futures_lite::AsyncReadExt; #[cfg(feature = "tokio")] use tokio::{ fs, io, io::AsyncReadExt, sync::{Mutex, RwLock}, }; use zeroize::Zeroizing; use crate::{AsAttributes, Key}; #[cfg(feature = "unstable")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub mod api; #[cfg(not(feature = "unstable"))] mod api; pub(crate) use api::AttributeValue; mod error; mod item; mod secret; pub use error::{Error, InvalidItemError, WeakKeyError}; pub use item::Item; pub use secret::Secret; #[cfg(feature = "unstable")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub use secret::SecretProxy; type ItemDefinition = (String, HashMap, Zeroizing>, bool); /// File backed keyring. #[derive(Debug)] pub struct Keyring { keyring: Arc>, path: PathBuf, /// Times are stored before reading the file to detect /// file changes before writing mtime: Mutex>, key: Mutex>>, secret: Mutex>, } impl Keyring { /// Load from default keyring file pub async fn load_default() -> Result { #[cfg(feature = "tracing")] tracing::debug!("Loading default keyring file"); let secret = secret::retrieve().await?; Self::load(api::Keyring::default_path()?, secret).await } /// Load from a keyring file. /// /// # Arguments /// /// * `path` - The path to the file backend. /// * `secret` - The service key, usually retrieved from the Secrets portal. pub async fn load(path: impl AsRef, secret: Secret) -> Result { #[cfg(feature = "tracing")] tracing::debug!("Trying to load keyring file at {:?}", path.as_ref()); let (mtime, keyring) = match fs::File::open(path.as_ref()).await { Err(err) if err.kind() == io::ErrorKind::NotFound => { #[cfg(feature = "tracing")] tracing::debug!("Keyring file not found, creating a new one"); (None, api::Keyring::new()) } Err(err) => return Err(err.into()), Ok(mut file) => { #[cfg(feature = "tracing")] tracing::debug!("Keyring file found, loading it content"); let mtime = file.metadata().await?.modified().ok(); let mut content = Vec::new(); file.read_to_end(&mut content).await?; let keyring = api::Keyring::try_from(content.as_slice())?; (mtime, keyring) } }; Ok(Self { keyring: Arc::new(RwLock::new(keyring)), path: path.as_ref().to_path_buf(), mtime: Mutex::new(mtime), key: Default::default(), secret: Mutex::new(Arc::new(secret)), }) } async fn migrate( file: &mut fs::File, path: impl AsRef, secret: Secret, ) -> Result { let mut content = Vec::new(); file.read_to_end(&mut content).await?; match api::Keyring::try_from(content.as_slice()) { Ok(keyring) => Ok(Self { keyring: Arc::new(RwLock::new(keyring)), path: path.as_ref().to_path_buf(), mtime: Default::default(), key: Default::default(), secret: Mutex::new(Arc::new(secret)), }), Err(Error::VersionMismatch(Some(version))) if version[0] == api::LEGACY_MAJOR_VERSION => { #[cfg(feature = "tracing")] tracing::debug!("Migrating from legacy keyring format"); let legacy_keyring = api::LegacyKeyring::try_from(content.as_slice())?; let mut keyring = api::Keyring::new(); let key = keyring.derive_key(&secret); for item in legacy_keyring.decrypt_items(&secret)? { let encrypted_item = item.encrypt(&key)?; keyring.items.push(encrypted_item); } Ok(Self { keyring: Arc::new(RwLock::new(keyring)), path: path.as_ref().to_path_buf(), mtime: Default::default(), key: Default::default(), secret: Mutex::new(Arc::new(secret)), }) } Err(err) => Err(err), } } /// Open a keyring with given name from the default directory. /// /// This function will automatically migrate the keyring to the /// latest format. /// /// # Arguments /// /// * `name` - The name of the keyring. /// * `secret` - The service key, usually retrieved from the Secrets portal. pub async fn open(name: &str, secret: Secret) -> Result { let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?; if v1_path.exists() { return Keyring::load(v1_path, secret).await; } let v0_path = api::Keyring::path(name, api::LEGACY_MAJOR_VERSION)?; if v0_path.exists() { #[cfg(feature = "tracing")] tracing::debug!("Trying to load keyring file at {:?}", v0_path); match fs::File::open(&v0_path).await { Err(err) => Err(err.into()), Ok(mut file) => Self::migrate(&mut file, v1_path, secret).await, } } else { Ok(Self { keyring: Arc::new(RwLock::new(api::Keyring::new())), path: v1_path, mtime: Default::default(), key: Default::default(), secret: Mutex::new(Arc::new(secret)), }) } } /// Retrieve the number of items /// /// This function will not trigger a key derivation and can therefore be /// faster than [`items().len()`](Self::items). pub async fn n_items(&self) -> usize { self.keyring.read().await.items.len() } /// Retrieve the list of available [`Item`]s. /// /// If items cannot be decrypted, [`InvalidItemError`]s are returned for /// them instead of [`Item`]s. pub async fn items(&self) -> Vec> { let key = self.derive_key().await; let keyring = self.keyring.read().await; keyring .items .iter() .map(|e| { (*e).clone().decrypt(&key).map_err(|err| { InvalidItemError::new( err, e.hashed_attributes.keys().map(|x| x.to_string()).collect(), ) }) }) .collect() } /// Search items matching the attributes. pub async fn search_items(&self, attributes: &impl AsAttributes) -> Result, Error> { let key = self.derive_key().await; let keyring = self.keyring.read().await; keyring.search_items(attributes, &key) } /// Find the first item matching the attributes. pub async fn lookup_item(&self, attributes: &impl AsAttributes) -> Result, Error> { let key = self.derive_key().await; let keyring = self.keyring.read().await; keyring.lookup_item(attributes, &key) } /// Delete an item. pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<(), Error> { { let key = self.derive_key().await; let mut keyring = self.keyring.write().await; keyring.remove_items(attributes, &key)?; }; self.write().await } /// Create a new item /// /// # Arguments /// /// * `label` - A user visible label of the item. /// * `attributes` - A map of key/value attributes, used to find the item /// later. /// * `secret` - The secret to store. /// * `replace` - Whether to replace the value if the `attributes` matches /// an existing `secret`. pub async fn create_item( &self, label: &str, attributes: &impl AsAttributes, secret: impl AsRef<[u8]>, replace: bool, ) -> Result { let item = { let key = self.derive_key().await; let mut keyring = self.keyring.write().await; if replace { keyring.remove_items(attributes, &key)?; } let item = Item::new(label, attributes, secret); let encrypted_item = item.encrypt(&key)?; keyring.items.push(encrypted_item); item }; match self.write().await { Err(e) => Err(e), Ok(_) => Ok(item), } } /// Replaces item at the given index. /// /// The `index` refers to the index of the [`Vec`] returned by /// [`items()`](Self::items). If the index does not exist, the functions /// returns an error. pub async fn replace_item_index(&self, index: usize, item: &Item) -> Result<(), Error> { { let key = self.derive_key().await; let mut keyring = self.keyring.write().await; if let Some(item_store) = keyring.items.get_mut(index) { *item_store = item.encrypt(&key)?; } else { return Err(Error::InvalidItemIndex(index)); } } self.write().await } /// Deletes item at the given index. /// /// The `index` refers to the index of the [`Vec`](Vec) returned by /// [`items()`](Self::items). If the index does not exist, the functions /// returns an error. pub async fn delete_item_index(&self, index: usize) -> Result<(), Error> { { let mut keyring = self.keyring.write().await; if index < keyring.items.len() { keyring.items.remove(index); } else { return Err(Error::InvalidItemIndex(index)); } } self.write().await } /// Helper used for migration to avoid re-writing the file multiple times pub(crate) async fn create_items(&self, items: Vec) -> Result<(), Error> { let key = self.derive_key().await; let mut keyring = self.keyring.write().await; for (label, attributes, secret, replace) in items { if replace { keyring.remove_items(&attributes, &key)?; } let item = Item::new(label, &attributes, &*secret); let encrypted_item = item.encrypt(&key)?; keyring.items.push(encrypted_item); } #[cfg(feature = "tracing")] tracing::debug!("Writing keyring back to the file"); keyring.dump(&self.path, *self.mtime.lock().await).await?; Ok(()) } /// Write the changes to the keyring file. pub async fn write(&self) -> Result<(), Error> { #[cfg(feature = "tracing")] tracing::debug!("Writing keyring back to the file {:?}", self.path); let mut mtime = self.mtime.lock().await; { let mut keyring = self.keyring.write().await; #[cfg(feature = "tracing")] tracing::debug!("Current modified time {:?}", mtime); keyring.dump(&self.path, *mtime).await?; }; if let Ok(modified) = fs::metadata(&self.path).await?.modified() { #[cfg(feature = "tracing")] tracing::debug!("New modified time {:?}", modified); *mtime = Some(modified); } Ok(()) } /// Return key, derive and store it first if not initialized async fn derive_key(&self) -> Arc { let keyring = Arc::clone(&self.keyring); let secret_lock = self.secret.lock().await; let secret = Arc::clone(&secret_lock); drop(secret_lock); let mut key_lock = self.key.lock().await; if key_lock.is_none() { #[cfg(feature = "async-std")] let key = blocking::unblock(move || { async_io::block_on(async { keyring.read().await.derive_key(&secret) }) }) .await; #[cfg(feature = "tokio")] let key = tokio::task::spawn_blocking(move || keyring.blocking_read().derive_key(&secret)) .await .unwrap(); *key_lock = Some(Arc::new(key)); } Arc::clone(key_lock.as_ref().unwrap()) } /// Change keyring secret /// /// # Arguments /// /// * `secret` - The new secret to store. pub async fn change_secret(&self, secret: Secret) -> Result<(), Error> { #[cfg(feature = "tracing")] tracing::debug!("Changing keyring secret and key"); let keyring = self.keyring.read().await; let key = self.derive_key().await; let mut items = Vec::with_capacity(keyring.items.len()); for item in &keyring.items { items.push(item.clone().decrypt(&key)?); } drop(keyring); let mut secret_lock = self.secret.lock().await; *secret_lock = Arc::new(secret); drop(secret_lock); let mut key_lock = self.key.lock().await; // Unset the old key *key_lock = None; drop(key_lock); // Reset Keyring content before setting the new key let mut keyring = self.keyring.write().await; keyring.reset(); drop(keyring); // Set new key let key = self.derive_key().await; let mut keyring = self.keyring.write().await; for item in items { let encrypted_item = item.encrypt(&key)?; keyring.items.push(encrypted_item); } drop(keyring); self.write().await } } #[cfg(test)] #[cfg(feature = "tokio")] mod tests { use std::{collections::HashMap, path::PathBuf}; use tempfile::tempdir; use super::*; #[tokio::test] async fn repeated_write() -> Result<(), Error> { let path = PathBuf::from("../../tests/test.keyring"); let secret = Secret::from(vec![1, 2]); let keyring = Keyring::load(&path, secret).await?; keyring.write().await?; keyring.write().await?; Ok(()) } #[tokio::test] async fn delete() -> Result<(), Error> { let path = PathBuf::from("../../tests/test-delete.keyring"); let keyring = Keyring::load(&path, strong_key()).await?; let attributes: HashMap<&str, &str> = HashMap::default(); keyring .create_item("Label", &attributes, "secret", false) .await?; keyring.delete_item_index(0).await?; let result = keyring.delete_item_index(100).await; assert!(matches!(result, Err(Error::InvalidItemIndex(100)))); Ok(()) } #[tokio::test] async fn write_with_weak_key() -> Result<(), Error> { let path = PathBuf::from("../../tests/write_with_weak_key.keyring"); let secret = Secret::from(vec![1, 2]); let keyring = Keyring::load(&path, secret).await?; let attributes: HashMap<&str, &str> = HashMap::default(); let result = keyring .create_item("label", &attributes, "my-password", false) .await; assert!(matches!( result, Err(Error::WeakKey(WeakKeyError::PasswordTooShort(2))) )); Ok(()) } #[tokio::test] async fn write_with_strong_key() -> Result<(), Error> { let path = PathBuf::from("../../tests/write_with_strong_key.keyring"); let keyring = Keyring::load(&path, strong_key()).await?; let attributes: HashMap<&str, &str> = HashMap::default(); keyring .create_item("label", &attributes, "my-password", false) .await?; Ok(()) } fn strong_key() -> Secret { Secret::from([1, 2].into_iter().cycle().take(64).collect::>()) } #[tokio::test] async fn concurrent_writes() -> Result<(), Error> { let path = PathBuf::from("../../tests/concurrent_writes.keyring"); let keyring = Arc::new(Keyring::load(&path, strong_key()).await?); let keyring_clone = keyring.clone(); let handle_1 = tokio::task::spawn(async move { keyring_clone.write().await }); let handle_2 = tokio::task::spawn(async move { keyring.write().await }); let (res_1, res_2) = futures_util::future::join(handle_1, handle_2).await; res_1.unwrap()?; res_2.unwrap()?; Ok(()) } async fn check_items(keyring: &Keyring) -> Result<(), Error> { assert_eq!(keyring.n_items().await, 1); let items: Result, _> = keyring.items().await.into_iter().collect(); let items = items.expect("unable to retrieve items"); assert_eq!(items.len(), 1); assert_eq!(items[0].label(), "foo"); assert_eq!(items[0].secret().as_ref(), b"foo".to_vec()); let attributes = items[0].attributes(); assert_eq!(attributes.len(), 1); assert_eq!( attributes.get("xdg:schema").map(|v| v.as_ref()), Some("org.gnome.keyring.Note") ); Ok(()) } #[tokio::test] async fn migrate_from_legacy() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); let v1_dir = v0_dir.join("v1"); fs::create_dir_all(&v1_dir).await?; let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("fixtures") .join("legacy.keyring"); fs::copy(&fixture_path, &v0_dir.join("default.keyring")).await?; std::env::set_var("XDG_DATA_HOME", &data_dir.path()); assert!(!v1_dir.join("default.keyring").exists()); let password = b"test"; let secret = Secret::from(password.to_vec()); let keyring = Keyring::open("default", secret).await?; check_items(&keyring).await?; keyring.write().await?; assert!(v1_dir.join("default.keyring").exists()); Ok(()) } #[tokio::test] async fn migrate() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); let v1_dir = v0_dir.join("v1"); fs::create_dir_all(&v1_dir).await?; let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("fixtures") .join("default.keyring"); fs::copy(&fixture_path, &v0_dir.join("default.keyring")).await?; std::env::set_var("XDG_DATA_HOME", &data_dir.path()); let password = b"test"; let secret = Secret::from(password.to_vec()); let keyring = Keyring::open("default", secret).await?; assert!(!v1_dir.join("default.keyring").exists()); check_items(&keyring).await?; keyring.write().await?; assert!(v1_dir.join("default.keyring").exists()); Ok(()) } #[tokio::test] async fn open() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); let v1_dir = v0_dir.join("v1"); fs::create_dir_all(&v1_dir).await?; let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("fixtures") .join("default.keyring"); fs::copy(&fixture_path, &v1_dir.join("default.keyring")).await?; std::env::set_var("XDG_DATA_HOME", &data_dir.path()); let password = b"test"; let secret = Secret::from(password.to_vec()); let keyring = Keyring::open("default", secret).await?; assert!(v1_dir.join("default.keyring").exists()); check_items(&keyring).await?; keyring.write().await?; assert!(v1_dir.join("default.keyring").exists()); Ok(()) } #[tokio::test] async fn open_nonexistent() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); let v1_dir = v0_dir.join("v1"); fs::create_dir_all(&v1_dir).await?; std::env::set_var("XDG_DATA_HOME", &data_dir.path()); let password = b"test"; let secret = Secret::from(password.to_vec()); let keyring = Keyring::open("default", secret).await?; assert!(!v1_dir.join("default.keyring").exists()); keyring .create_item( "foo", &HashMap::from([("xdg:schema", "org.gnome.keyring.Note")]), b"foo", false, ) .await?; keyring.write().await?; assert!(v1_dir.join("default.keyring").exists()); Ok(()) } #[tokio::test] async fn change_secret() -> Result<(), Error> { let path = PathBuf::from("../../tests/test_rekeying.keyring"); let keyring = Keyring::load(&path, strong_key()).await?; let attributes = HashMap::from([("attr", "value")]); let item_before = keyring .create_item("test", &attributes, "password", false) .await?; let new_secret = Secret::from(b"password".to_vec()); keyring.change_secret(new_secret).await?; let new_secret = Secret::from(b"password".to_vec()); let keyring = Keyring::load(&path, new_secret).await?; let item_now = keyring.lookup_item(&attributes).await?.unwrap(); assert_eq!(item_before.label(), item_now.label()); assert_eq!(item_before.secret(), item_now.secret()); assert_eq!(item_before.attributes(), item_now.attributes()); fs::remove_file(path).await?; Ok(()) } } oo7-0.3.3/src/portal/secret.rs000064400000000000000000000124601046102023000142610ustar 00000000000000//! Implementation of the XDG secret portal. //! //! This is a modified copy from ASHPD. use std::{collections::HashMap, os::fd::AsFd}; #[cfg(feature = "async-std")] use async_net::unix::UnixStream; #[cfg(feature = "async-std")] use futures_util::AsyncReadExt; use futures_util::StreamExt; use rand::{distributions::Alphanumeric, thread_rng, Rng}; #[cfg(feature = "tokio")] use tokio::{io::AsyncReadExt, net::UnixStream}; use zbus::{ zvariant::{Fd, ObjectPath, OwnedValue, SerializeDict, Type}, ProxyDefault, }; use zeroize::{Zeroize, ZeroizeOnDrop}; use super::Error; /// Secret retrieved from the /// [XDG secret portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html). /// /// This secret is generated by the portal once per app. /// It is provided such that apps can store encrypted data with a key based on /// this secret. /// /// We use this secret to encrypt the app's [`Keyring`](crate::portal::Keyring). #[derive(Debug, Zeroize, ZeroizeOnDrop)] pub struct Secret(Vec); impl From> for Secret { fn from(secret: Vec) -> Self { Self(secret) } } impl std::ops::Deref for Secret { type Target = [u8]; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(SerializeDict, Type, Debug)] /// Specified options for a [`SecretProxy::retrieve_secret`] request. #[zvariant(signature = "dict")] struct RetrieveOptions { handle_token: String, } impl Default for RetrieveOptions { fn default() -> Self { let mut rng = thread_rng(); let token: String = (&mut rng) .sample_iter(Alphanumeric) .take(10) .map(char::from) .collect(); Self { handle_token: format!("oo7_{token}"), } } } #[derive(Debug)] pub struct SecretProxy<'a>(zbus::Proxy<'a>); impl<'a> ProxyDefault for SecretProxy<'a> { const INTERFACE: Option<&'static str> = Some("org.freedesktop.portal.Secret"); const DESTINATION: Option<&'static str> = Some("org.freedesktop.portal.Desktop"); const PATH: Option<&'static str> = Some("/org/freedesktop/portal/desktop"); } impl<'a> From> for SecretProxy<'a> { fn from(value: zbus::Proxy<'a>) -> Self { Self(value) } } impl<'a> SecretProxy<'a> { /// Create a new instance of [`SecretProxy`]. pub async fn new(connection: &zbus::Connection) -> Result, zbus::Error> { zbus::ProxyBuilder::new(connection) .build() .await .map_err(From::from) } /// Retrieves a master secret for a sandboxed application. /// /// # Arguments /// /// * `fd` - Writable file descriptor for transporting the secret. #[doc(alias = "RetrieveSecret")] pub async fn retrieve_secret(&self, fd: &impl AsFd) -> Result<(), Error> { let options = RetrieveOptions::default(); let cnx = self.0.connection(); let unique_name = cnx.unique_name().unwrap(); let unique_identifier = unique_name.trim_start_matches(':').replace('.', "_"); let path = ObjectPath::try_from(format!( "/org/freedesktop/portal/desktop/request/{unique_identifier}/{}", options.handle_token )) .unwrap(); #[cfg(feature = "tracing")] tracing::debug!( "Creating a '{}' proxy and listening for a response", path.as_str() ); let request_proxy: zbus::Proxy = zbus::ProxyBuilder::new(cnx) .interface("org.freedesktop.portal.Request")? .destination("org.freedesktop.portal.Desktop")? .path(path)? .build() .await?; let mut signal_stream = request_proxy.receive_signal("Response").await?; futures_util::try_join!( async { let message = signal_stream.next().await.unwrap(); let (response, _details) = message .body() .deserialize::<(u32, HashMap)>()?; if response == 0 { Ok(()) } else { Err(Error::CancelledPortalRequest) } }, async { match self .0 .call_method("RetrieveSecret", &(Fd::from(fd), &options)) .await { Ok(_) => Ok(()), Err(zbus::Error::MethodError(_, _, _)) => Err(Error::PortalNotAvailable), Err(e) => Err(e.into()), }?; Ok(()) }, )?; Ok(()) } } pub async fn retrieve() -> Result { let connection = zbus::Connection::session().await?; #[cfg(feature = "tracing")] tracing::debug!("Retrieve service key using org.freedesktop.portal.Secrets"); let proxy = match SecretProxy::new(&connection).await { Ok(proxy) => Ok(proxy), Err(zbus::Error::InterfaceNotFound) => Err(Error::PortalNotAvailable), Err(e) => Err(e.into()), }?; let (mut x1, x2) = UnixStream::pair()?; proxy.retrieve_secret(&x2).await?; drop(x2); let mut buf = Vec::new(); x1.read_to_end(&mut buf).await?; #[cfg(feature = "tracing")] tracing::debug!("Secret received from the portal successfully"); Ok(Secret::from(buf)) }