oo7-0.5.0/.cargo_vcs_info.json0000644000000001440000000000100115410ustar { "git": { "sha1": "d6d717edbc41ad28c1babb9870f015ff2aada362" }, "path_in_vcs": "client" }oo7-0.5.0/Cargo.lock0000644000001372710000000000100075300ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[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 = "ashpd" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0986d5b4f0802160191ad75f8d33ada000558757db3defb70299ca95d9fcbd" dependencies = [ "async-fs", "async-net", "enumflags2", "futures-channel", "futures-util", "rand 0.9.2", "serde", "serde_repr", "tokio", "tracing", "url", "zbus", ] [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "pin-project-lite", "slab", ] [[package]] name = "async-fs" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" dependencies = [ "async-lock", "blocking", "futures-lite", ] [[package]] name = "async-io" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "windows-sys 0.60.2", ] [[package]] name = "async-lock" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" 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.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix", ] [[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.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", "windows-sys 0.60.2", ] [[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.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 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.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "bitflags" version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" 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.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[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.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[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.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[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 0.6.4", "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 = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[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.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "event-listener" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[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 = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", "futures-macro", "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.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[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 = "icu_collections" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] [[package]] name = "io-uring" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ "bitflags", "cfg-if", "libc", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] [[package]] name = "libc" version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[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.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 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 0.8.5", "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 = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oo7" version = "0.5.0" dependencies = [ "aes", "ashpd", "async-fs", "async-io", "async-lock", "blocking", "cbc", "cipher", "digest", "endi", "futures-lite", "futures-util", "getrandom 0.3.3", "hkdf", "hmac", "md-5", "num", "num-bigint-dig", "openssl", "pbkdf2", "rand 0.9.2", "serde", "sha2", "subtle", "tempfile", "tokio", "tracing", "zbus", "zbus_macros", "zeroize", "zvariant", ] [[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 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.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", "hmac", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[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.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix", "windows-sys 0.60.2", ] [[package]] name = "potential_utf" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "zerovec", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", ] [[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 0.6.4", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core 0.9.3", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.3", ] [[package]] name = "rustc-demangle" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.60.2", ] [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_repr" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[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.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tempfile" version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.60.2", ] [[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tokio" version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", "slab", "socket2", "tokio-macros", "tracing", "windows-sys 0.59.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[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.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "url" version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[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-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.3", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yoke" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zbus" version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" dependencies = [ "async-broadcast", "async-executor", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", "event-listener", "futures-core", "futures-lite", "hex", "nix", "ordered-stream", "serde", "serde_repr", "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", "winnow", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zbus_names", "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", "winnow", "zvariant", ] [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[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 = "zerotrie" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zvariant" version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" dependencies = [ "endi", "enumflags2", "serde", "url", "winnow", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", "syn", "winnow", ] oo7-0.5.0/Cargo.toml0000644000000104250000000000100075420ustar # 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 = "2024" rust-version = "1.85" name = "oo7" version = "0.5.0" authors = [ "Bilal Elmoussaoui", "Sophie Herold", "Maximiliano Sandoval", ] build = false exclude = ["org.freedesktop.Secrets.xml"] autolib = false autobins = false autoexamples = false autotests = false autobenches = false 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" resolver = "2" [package.metadata.docs.rs] features = ["unstable"] rustc-args = [ "--cfg", "docsrs", ] rustdoc-args = [ "--cfg", "docsrs", "--generate-link-to-definition", ] [features] async-std = [ "zbus/async-io", "dep:async-fs", "dep:async-io", "dep:async-lock", "dep:blocking", "dep:futures-lite", "ashpd/async-std", ] default = [ "local_tests", "tokio", "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", "ashpd/tokio", ] tracing = [ "dep:tracing", "ashpd/tracing", ] unstable = [] [lib] name = "oo7" path = "src/lib.rs" [[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.ashpd] version = "0.12" default-features = false [dependencies.async-fs] version = "2.1.3" optional = true [dependencies.async-io] version = "2.5.0" optional = true [dependencies.async-lock] version = "3.4.1" 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.6" optional = true [dependencies.futures-util] version = "0.3" [dependencies.getrandom] 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.9" features = ["thread_rng"] 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.47" features = [ "sync", "fs", "io-util", ] optional = true default-features = false [dependencies.tracing] version = "0.1" optional = true [dependencies.zbus] version = "5.9" default-features = false [dependencies.zbus_macros] version = "5.5" features = ["gvariant"] [dependencies.zeroize] version = "1" features = ["zeroize_derive"] [dependencies.zvariant] version = "5.2" features = ["gvariant"] default-features = false [dev-dependencies.tempfile] version = "3.21" [dev-dependencies.tokio] version = "1.47" features = [ "macros", "rt-multi-thread", ] default-features = false oo7-0.5.0/Cargo.toml.orig000064400000000000000000000060451046102023000132260ustar 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 } ashpd.workspace = true async-fs = { version = "2.1.3", optional = true } async-io = { version = "2.5.0", optional = true } async-lock = { version = "3.4.1", 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 getrandom = "0.3" 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.9", default-features = false, features = ["thread_rng"] } 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 zbus_macros.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", "tokio", "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:blocking", "dep:futures-lite", "ashpd/async-std", ] tokio = ["zbus/tokio", "dep:tokio", "ashpd/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"] tracing = ["dep:tracing", "ashpd/tracing"] [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.5.0/LICENSE000064400000000000000000000021531046102023000113400ustar 00000000000000MIT License Copyright (c) 2022 Bilal Elmoussaoui, Sophie Herold, Maximiliano Sandoval, Dhanuka Warusadura 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.5.0/README.md000064400000000000000000000120011046102023000116030ustar 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) This library allows to store secrets using two different backends: - `dbus` implements the [`org.freedesktop.Secret`](https://specifications.freedesktop.org/secret-service-spec/latest/) specification. - `file` stores secrets in an encrypted file compatible with libsecret. For sandboxed applications use case, the file can be encrypted using a secret retrieved from the [`org.freedesktop.portal.Secrets` portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html). Sandboxed applications should prefer using the file backend as it doesn't expose the application secrets to other applications that can talk to the `org.freedesktop.Secrets` service. The library provides types that automatically pick a backend based on whether the application is sandboxed or not. Applications developers should use those APIs. ## 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(&[("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 | No | | `tokio` | Use `tokio` APIs for IO/Filesystem operations | Yes | | `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](https://gitlab.gnome.org/GNOME/libsecret) is a C library that provides the same two backends. 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. - [libsecret-rs](https://gitlab.gnome.org/World/Rust/libsecret-rs) provides Rust bindings to libsecret. - [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.5.0/examples/basic.rs000064400000000000000000000010411046102023000135730ustar 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, "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.5.0/examples/basic_2.rs000064400000000000000000000012761046102023000140260ustar 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, "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.5.0/examples/dbus_service.rs000064400000000000000000000012331046102023000151720ustar 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.5.0/fixtures/default.keyring000064400000000000000000000003461046102023000152240ustar 00000000000000GnomeKeyring  ‰÷ì]Ï)rá=4Ïi“Ýtó}«ñŽ §ü”$‘OªkÏO †ÈÌfxdg:schema­"ê–øU4t«Ù?B¾tÓibà Œ©î)$Óæ°¦˜¥ ,…Q™Ä r1âkg¾]ÍýSó!|aseŠ 3ÛZ ÅÈn<ÞC¥(â¸v:˜a뎨}ÿvÏ7JwÅe/™a\þ/õh°ì:B6à2™c|#€$©™Ë3Âb%!Š*ò&ÌÛ|ÛÚX´Ú<-ž$oo7-0.5.0/fixtures/legacy.keyring000064400000000000000000000004321046102023000150400ustar 00000000000000GnomeKeyring testeù•ì ûî¯[þÀ¶ xdg:schema f1b0a5949a0ad7a6162eb5fdce543a40€ú¸ááðW‰â°3ƒµ?$’'°Äö~²Â³7öa»"Çp[tœ‚ G<§¾ºcq!„SDZe‹ÇíؾõߪfGrg³ÏDÄî4¶ÑÄ(*›L„§ F’cBÿ¨K[ ÚÍP¿ö\'™7_®ì-n==âgJ` dîŸåoo7-0.5.0/src/crypto/error.rs000064400000000000000000000033271046102023000141450ustar 00000000000000/// Cryptography specific errors. #[derive(Debug)] pub enum Error { #[cfg(feature = "openssl_crypto")] Openssl(openssl::error::ErrorStack), #[cfg(feature = "native_crypto")] PadError(cipher::inout::PadError), #[cfg(feature = "native_crypto")] UnpadError(cipher::block_padding::UnpadError), } #[cfg(feature = "openssl_crypto")] impl From for Error { fn from(value: openssl::error::ErrorStack) -> Self { Self::Openssl(value) } } #[cfg(feature = "native_crypto")] impl From for Error { fn from(value: cipher::block_padding::UnpadError) -> Self { Self::UnpadError(value) } } #[cfg(feature = "native_crypto")] impl From for Error { fn from(value: cipher::inout::PadError) -> Self { Self::PadError(value) } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { #[cfg(feature = "openssl_crypto")] Self::Openssl(e) => Some(e), #[cfg(feature = "native_crypto")] Self::UnpadError(_) | Self::PadError(_) => None, } } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { #[cfg(feature = "openssl_crypto")] Self::Openssl(e) => f.write_fmt(format_args!("Openssl error: {e}")), #[cfg(feature = "native_crypto")] Self::UnpadError(e) => f.write_fmt(format_args!("Wrong padding error: {e}")), #[cfg(feature = "native_crypto")] Self::PadError(e) => f.write_fmt(format_args!("Wrong padding error: {e}")), } } } oo7-0.5.0/src/crypto/mod.rs000064400000000000000000000041321046102023000135660ustar 00000000000000//! Cryptographic primitives using either native crates or openssl. #[cfg(feature = "native_crypto")] mod native; #[cfg(all(feature = "native_crypto", not(feature = "unstable")))] pub(crate) use native::*; #[cfg(all(feature = "native_crypto", feature = "unstable"))] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub use native::*; mod error; pub use error::Error; #[cfg(feature = "openssl_crypto")] mod openssl; #[cfg(all(feature = "openssl_crypto", not(feature = "unstable")))] pub(crate) use self::openssl::*; #[cfg(all(feature = "openssl_crypto", feature = "unstable"))] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub use self::openssl::*; #[cfg(test)] mod test { use super::*; use crate::Key; #[test] fn test_encrypt() { let data = b"some data"; let expected_encrypted = &[ 241, 233, 175, 173, 142, 44, 63, 240, 77, 154, 211, 233, 217, 170, 49, 142, ]; let aes_key = Key::new(vec![ 132, 3, 113, 222, 81, 209, 49, 43, 81, 232, 243, 46, 1, 103, 184, 42, ]); let aes_iv = &[ 78, 82, 67, 158, 214, 102, 48, 109, 84, 107, 94, 54, 225, 29, 186, 246, ]; let encrypted = encrypt(data, &aes_key, aes_iv).unwrap(); assert_eq!(encrypted, expected_encrypted); let decrypted = decrypt(&encrypted, &aes_key, aes_iv).unwrap(); assert_eq!(decrypted.to_vec(), data); } #[test] fn test_legacy_derive_key_and_iv() { let expected_key = &[ 0x1f, 0x35, 0x38, 0x40, 0xf2, 0x95, 0x73, 0x30, 0xa6, 0xcb, 0x01, 0xf9, 0x53, 0xba, 0x22, 0x12, ]; let expected_iv = &[ 0x7f, 0xf5, 0x65, 0xb2, 0x31, 0xa5, 0x77, 0x32, 0xf8, 0xd3, 0xd0, 0xa6, 0x45, 0x1c, 0x39, 0x97, ]; let salt = &[0x92, 0xf4, 0xc0, 0x34, 0x0f, 0x5f, 0x36, 0xf9]; let iteration_count = 1782; let password = b"test"; let (key, iv) = legacy_derive_key_and_iv(password, Ok(()), salt, iteration_count).unwrap(); assert_eq!(key.as_ref(), &expected_key[..]); assert_eq!(iv, &expected_iv[..]); } } oo7-0.5.0/src/crypto/native.rs000064400000000000000000000175521046102023000143070ustar 00000000000000use std::{ ops::{Mul, Rem, Shr}, sync::LazyLock, }; use cipher::{ BlockDecryptMut, BlockEncryptMut, BlockSizeUser, IvSizeUser, KeyIvInit, KeySizeUser, block_padding::{NoPadding, Pkcs7}, }; use digest::{Digest, FixedOutput, Mac, Output, OutputSizeUser}; use hkdf::Hkdf; use md5::Md5; use num::{FromPrimitive, Integer, One, Zero}; use num_bigint_dig::BigUint; use sha2::Sha256; use subtle::ConstantTimeEq; use zeroize::{Zeroize, Zeroizing}; use crate::{Key, file}; type EncAlg = cbc::Encryptor; type DecAlg = cbc::Decryptor; type MacAlg = hmac::Hmac; pub fn encrypt( data: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Result, super::Error> { 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)? .len(); blob.truncate(encrypted_len); Ok(blob) } pub fn decrypt( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Result>, super::Error> { let mut data = blob.as_ref().to_vec(); Ok(DecAlg::new_from_slices(key.as_ref(), iv.as_ref()) .expect("Invalid key length") .decrypt_padded_mut::(&mut data)? .to_vec() .into()) } pub(crate) fn decrypt_no_padding( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Result>, super::Error> { let mut data = blob.as_ref().to_vec(); Ok(DecAlg::new_from_slices(key.as_ref(), iv.as_ref()) .expect("Invalid key length") .decrypt_padded_mut::(&mut data)? .to_vec() .into()) } pub(crate) fn iv_len() -> usize { DecAlg::iv_size() } pub(crate) fn generate_private_key() -> Result>, super::Error> { let generic_array = EncAlg::generate_key(cipher::rand_core::OsRng); Ok(Zeroizing::new(generic_array.to_vec())) } pub(crate) fn generate_public_key(private_key: impl AsRef<[u8]>) -> Result, super::Error> { let private_key_uint = BigUint::from_bytes_be(private_key.as_ref()); static DH_GENERATOR: LazyLock = LazyLock::new(|| BigUint::from_u64(0x2).unwrap()); let public_key_uint = powm(&DH_GENERATOR, private_key_uint); Ok(public_key_uint.to_bytes_be()) } pub(crate) fn generate_aes_key( private_key: impl AsRef<[u8]>, server_public_key: impl AsRef<[u8]>, ) -> Result>, super::Error> { 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"); Ok(okm) } pub fn generate_iv() -> Result, super::Error> { Ok(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) -> Result { let mut mac = MacAlg::new_from_slice(key.as_ref()).unwrap(); mac.update(data.as_ref()); Ok(crate::Mac::new(mac.finalize().into_bytes().to_vec())) } pub(crate) fn verify_mac( data: impl AsRef<[u8]>, key: &Key, expected_mac: impl AsRef<[u8]>, ) -> Result { let mut mac = MacAlg::new_from_slice(key.as_ref()).unwrap(); mac.update(data.as_ref()); Ok(mac.verify_slice(expected_mac.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<(), file::WeakKeyError>, salt: impl AsRef<[u8]>, iteration_count: usize, ) -> Result { 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"); Ok(key) } pub(crate) fn legacy_derive_key_and_iv( secret: impl AsRef<[u8]>, key_strength: Result<(), file::WeakKeyError>, salt: impl AsRef<[u8]>, iteration_count: usize, ) -> Result<(Key, Vec), super::Error> { 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 { // We can't pass an instance, the borrow checker // would complain about digest being dropped at the end of // for block #[allow(clippy::needless_borrows_for_generic_args)] 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; } // We can't pass an instance, the borrow checker // would complain about digest being dropped at the end of // loop block #[allow(clippy::needless_borrows_for_generic_args)] hasher.update(&digest); } let iv = buffer.split_off(EncAlg::key_size()); Ok((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 { // for key exchange static DH_PRIME: LazyLock = LazyLock::new(|| { 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(&*DH_PRIME); } exp = exp.shr(1); base = (&base).mul(&base).rem(&*DH_PRIME); } exp.zeroize(); result } oo7-0.5.0/src/crypto/openssl.rs000064400000000000000000000156211046102023000144770ustar 00000000000000use openssl::{ bn::BigNum, dh::Dh, hash::{Hasher, MessageDigest, hash}, 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::{Key, Mac, file}; const ENC_ALG: Nid = Nid::AES_128_CBC; const MAC_ALG: Nid = Nid::SHA256; pub fn encrypt( data: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Result, super::Error> { 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)?; encrypted_len += encryptor.finalize(&mut blob[encrypted_len..])?; blob.truncate(encrypted_len); Ok(blob) } fn decrypt_with_padding( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, pad: bool, ) -> Result>, super::Error> { 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)?; decrypted_len += decrypter.finalize(&mut data[decrypted_len..])?; data.truncate(decrypted_len); Ok(data) } pub fn decrypt( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Result>, super::Error> { decrypt_with_padding(blob, key, iv, true) } pub(crate) fn decrypt_no_padding( blob: impl AsRef<[u8]>, key: &Key, iv: impl AsRef<[u8]>, ) -> Result>, super::Error> { 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() -> Result>, super::Error> { let cipher = Cipher::from_nid(ENC_ALG).unwrap(); let mut buf = Zeroizing::new(vec![0; cipher.key_len()]); rand_bytes(&mut buf)?; Ok(buf) } pub(crate) fn generate_public_key(private_key: impl AsRef<[u8]>) -> Result, super::Error> { let private_key_bn = BigNum::from_slice(private_key.as_ref()).unwrap(); let dh = Dh::from_pqg( BigNum::get_rfc2409_prime_1024().unwrap(), None, BigNum::from_u32(2).unwrap(), )?; Ok(dh.set_private_key(private_key_bn)?.public_key().to_vec()) } pub(crate) fn generate_aes_key( private_key: impl AsRef<[u8]>, server_public_key: impl AsRef<[u8]>, ) -> Result>, super::Error> { 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 dh = Dh::from_pqg( BigNum::get_rfc2409_prime_1024().unwrap(), None, BigNum::from_u32(2).unwrap(), )?; let mut common_secret_bytes = dh .set_private_key(private_key_bn)? .compute_key(&server_public_key_bn)?; 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)?; ctx.derive_init()?; ctx.set_hkdf_md(Md::sha256())?; ctx.set_hkdf_key(&ikm)?; ctx.derive(Some(okm.as_mut())) .expect("hkdf expand should never fail"); Ok(okm) } pub fn generate_iv() -> Result, super::Error> { let mut buf = vec![0; iv_len()]; rand_bytes(&mut buf)?; Ok(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) -> Result { let md = MessageDigest::from_nid(MAC_ALG).unwrap(); let mac_key = PKey::hmac(key.as_ref())?; let mut signer = Signer::new(md, &mac_key)?; signer.update(data.as_ref())?; signer.sign_to_vec().map_err(From::from).map(Mac::new) } pub(crate) fn verify_mac( data: impl AsRef<[u8]>, key: &Key, expected_mac: impl AsRef<[u8]>, ) -> Result { Ok(memcmp::eq( compute_mac(&data, key)?.as_slice(), expected_mac.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<(), file::WeakKeyError>, salt: impl AsRef<[u8]>, iteration_count: usize, ) -> Result { 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(), )?; Ok(key) } pub(crate) fn legacy_derive_key_and_iv( secret: impl AsRef<[u8]>, key_strength: Result<(), file::WeakKeyError>, salt: impl AsRef<[u8]>, iteration_count: usize, ) -> Result<(Key, Vec), super::Error> { 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())?; let mut pos = 0usize; loop { hasher.update(secret.as_ref())?; hasher.update(salt.as_ref())?; let mut digest = hasher.finish()?; for _ in 1..iteration_count { // We can't pass an instance, the borrow checker // would complain about digest being dropped at the end of // for block #[allow(clippy::needless_borrows_for_generic_args)] hasher.update(&digest)?; digest = hasher.finish()?; } 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; } // We can't pass an instance, the borrow checker // would complain about digest being dropped at the end of // for block #[allow(clippy::needless_borrows_for_generic_args)] hasher.update(&digest)?; } let iv = buffer.split_off(cipher.key_len()); Ok((Key::new_with_strength(buffer, key_strength), iv)) } oo7-0.5.0/src/dbus/algorithm.rs000064400000000000000000000026511046102023000144160ustar 00000000000000use serde::{Deserialize, 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, } const PLAIN_ALGORITHM: &str = "plain"; const ENCRYPTED_ALGORITHM: &str = "dh-ietf1024-sha256-aes128-cbc-pkcs7"; impl Serialize for Algorithm { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { Self::Plain => str::serialize(PLAIN_ALGORITHM, serializer), Self::Encrypted => str::serialize(ENCRYPTED_ALGORITHM, serializer), } } } impl<'de> Deserialize<'de> for Algorithm { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { match String::deserialize(deserializer)?.as_str() { PLAIN_ALGORITHM => Ok(Self::Plain), ENCRYPTED_ALGORITHM => Ok(Self::Encrypted), e => Err(serde::de::Error::custom(format!("Invalid algorithm {e}"))), } } } oo7-0.5.0/src/dbus/api/collection.rs000064400000000000000000000151671046102023000153420ustar 00000000000000use std::{fmt, time::Duration}; use ashpd::WindowIdentifier; use futures_util::{Stream, StreamExt}; use serde::Serialize; use zbus::zvariant::{ObjectPath, OwnedObjectPath, Type}; use super::{DBusSecret, DESTINATION, Item, Prompt, Properties, Unlockable}; use crate::{ AsAttributes, dbus::{Error, ServiceError}, }; #[derive(Type)] #[zvariant(signature = "o")] #[doc(alias = "org.freedesktop.Secret.Collection")] pub struct Collection<'a>(zbus::Proxy<'a>); impl zbus::proxy::Defaults for Collection<'_> { const INTERFACE: &'static Option> = &Some( zbus::names::InterfaceName::from_static_str_unchecked("org.freedesktop.Secret.Collection"), ); const DESTINATION: &'static Option> = &Some(DESTINATION); const PATH: &'static Option> = &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::proxy::Builder::new(connection) .path(object_path)? .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?; 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, window_id: Option) -> 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(window_id).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: &DBusSecret<'_>, replace: bool, window_id: Option, ) -> 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(window_id).await?; OwnedObjectPath::try_from(response)? } else { item_path }; Item::new(self.inner().connection(), item_path).await } } impl Serialize for Collection<'_> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ObjectPath::serialize(self.inner().path(), serializer) } } impl fmt::Debug for Collection<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Collection") .field(&self.inner().path().as_str()) .finish() } } impl Unlockable for Collection<'_> {} oo7-0.5.0/src/dbus/api/item.rs000064400000000000000000000117201046102023000141340ustar 00000000000000use std::{collections::HashMap, fmt, hash::Hash, time::Duration}; use ashpd::WindowIdentifier; use serde::Serialize; use zbus::zvariant::{ObjectPath, OwnedObjectPath, Type}; use super::{DBusSecret, DESTINATION, Prompt, Session, Unlockable}; use crate::{ AsAttributes, dbus::{Error, ServiceError}, }; #[derive(Type)] #[zvariant(signature = "o")] #[doc(alias = "org.freedesktop.Secret.Item")] pub struct Item<'a>(zbus::Proxy<'a>); impl zbus::proxy::Defaults for Item<'_> { const INTERFACE: &'static Option> = &Some( zbus::names::InterfaceName::from_static_str_unchecked("org.freedesktop.Secret.Item"), ); const DESTINATION: &'static Option> = &Some(DESTINATION); const PATH: &'static Option> = &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::proxy::Builder::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?; 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?; Ok(()) } pub async fn delete(&self, window_id: Option) -> 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(window_id).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::()?; DBusSecret::from_inner(self.inner().connection(), inner).await } #[doc(alias = "SetSecret")] pub async fn set_secret(&self, secret: &DBusSecret<'_>) -> Result<(), Error> { self.inner() .call_method("SetSecret", &(secret,)) .await .map_err::(From::from)?; Ok(()) } } impl Serialize for Item<'_> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ObjectPath::serialize(self.inner().path(), serializer) } } impl PartialEq for Item<'_> { fn eq(&self, other: &Self) -> bool { self.inner().path() == other.inner().path() } } impl Eq for Item<'_> {} impl Hash for Item<'_> { fn hash(&self, state: &mut H) { self.inner().path().hash(state); } } impl fmt::Debug for Item<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Item") .field(&self.inner().path().as_str()) .finish() } } impl Unlockable for Item<'_> {} oo7-0.5.0/src/dbus/api/mod.rs000064400000000000000000000024071046102023000137570ustar 00000000000000pub(crate) const DESTINATION: zbus::names::BusName<'static> = zbus::names::BusName::WellKnown( zbus::names::WellKnownName::from_static_str_unchecked("org.freedesktop.secrets"), ); pub(crate) const PATH: zbus::zvariant::ObjectPath<'static> = zbus::zvariant::ObjectPath::from_static_str_unchecked("/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 Unlockable for zbus::zvariant::ObjectPath<'_> {} impl Unlockable for zbus::zvariant::OwnedObjectPath {} impl Unlockable for &zbus::zvariant::ObjectPath<'_> {} 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::DBusSecret; #[cfg(feature = "unstable")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub use secret::DBusSecretInner; pub use service::Service; pub use session::Session; oo7-0.5.0/src/dbus/api/prompt.rs000064400000000000000000000062331046102023000145220ustar 00000000000000use std::fmt; use ashpd::WindowIdentifier; use futures_util::StreamExt; use serde::Serialize; use zbus::zvariant::{ObjectPath, OwnedValue, Type}; 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 zbus::proxy::Defaults for Prompt<'_> { const INTERFACE: &'static Option> = &Some( zbus::names::InterfaceName::from_static_str_unchecked("org.freedesktop.Secret.Prompt"), ); const DESTINATION: &'static Option> = &Some(DESTINATION); const PATH: &'static Option> = &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::proxy::Builder::new(connection) .path(path)? .build() .await?, )) } else { Ok(None) } } pub fn inner(&self) -> &zbus::Proxy<'_> { &self.0 } pub async fn prompt(&self, window_id: Option) -> Result<(), Error> { let id = match window_id { Some(id) => id.to_string(), None => Default::default(), }; self.inner() .call_method("Prompt", &(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, window_id: Option, ) -> Result { let mut stream = self.inner().receive_signal("Completed").await?; 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(window_id) )?; Ok(value) } } impl Serialize for Prompt<'_> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ObjectPath::serialize(self.inner().path(), serializer) } } impl fmt::Debug for Prompt<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Prompt") .field(&self.inner().path().as_str()) .finish() } } oo7-0.5.0/src/dbus/api/properties.rs000064400000000000000000000110511046102023000153670ustar 00000000000000use std::collections::HashMap; use serde::{ Deserialize, ser::{Serialize, SerializeMap}, }; 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::{Endian, Type, serialized::Context, to_bytes}; 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.5.0/src/dbus/api/secret.rs000064400000000000000000000065231046102023000144700ustar 00000000000000use std::sync::Arc; use serde::{Deserialize, Serialize, ser::SerializeTuple}; use zbus::zvariant::{OwnedObjectPath, Type}; use zeroize::{Zeroize, ZeroizeOnDrop}; use super::Session; use crate::{Key, Secret, crypto, dbus::Error, secret::ContentType}; #[derive(Debug, Serialize, Deserialize, Type)] #[zvariant(signature = "(oayays)")] /// Same as [`DBusSecret`] without tying the session path to a [`Session`] type. pub struct DBusSecretInner( pub OwnedObjectPath, pub Vec, pub Vec, pub ContentType, ); #[derive(Debug, Type, Zeroize, ZeroizeOnDrop)] #[zvariant(signature = "(oayays)")] pub struct DBusSecret<'a> { #[zeroize(skip)] pub(crate) session: Arc>, pub(crate) parameters: Vec, pub(crate) value: Vec, #[zeroize(skip)] pub(crate) content_type: ContentType, } impl<'a> DBusSecret<'a> { pub(crate) fn new(session: Arc>, secret: impl Into) -> Self { let secret = secret.into(); Self { session, parameters: vec![], value: secret.as_bytes().to_vec(), content_type: secret.content_type(), } } pub(crate) fn new_encrypted( session: Arc>, secret: impl Into, aes_key: &Key, ) -> Result { let iv = crypto::generate_iv()?; let secret = secret.into(); Ok(Self { session, value: crypto::encrypt(secret.as_bytes(), aes_key, &iv)?, parameters: iv, content_type: secret.content_type(), }) } pub(crate) async fn from_inner( cnx: &zbus::Connection, inner: DBusSecretInner, ) -> Result { Ok(Self { session: Arc::new(Session::new(cnx, inner.0).await?), parameters: inner.1, value: inner.2, content_type: inner.3, }) } pub(crate) fn decrypt(&self, key: Option<&Arc>) -> Result { let value = match key { Some(key) => &crypto::decrypt(&self.value, key, &self.parameters)?, None => &self.value, }; Ok(Secret::with_content_type(self.content_type, value)) } /// 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) -> ContentType { self.content_type } } impl Serialize for DBusSecret<'_> { 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().as_str())?; tuple_serializer.end() } } #[cfg(test)] mod tests { use super::*; #[test] fn signature() { assert_eq!(DBusSecret::SIGNATURE, "(oayays)"); } } oo7-0.5.0/src/dbus/api/service.rs000064400000000000000000000222621046102023000146410ustar 00000000000000use std::{collections::HashMap, fmt}; use ashpd::WindowIdentifier; use futures_util::{Stream, StreamExt}; use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Type, Value}; use super::{ Collection, DBusSecret, DESTINATION, Item, PATH, Prompt, Properties, Session, Unlockable, }; use crate::{ AsAttributes, Key, dbus::{Algorithm, Error, ServiceError}, }; #[derive(Type)] #[zvariant(signature = "o")] #[doc(alias = "org.freedesktop.secrets")] pub struct Service<'a>(zbus::Proxy<'a>); impl zbus::proxy::Defaults for Service<'_> { const INTERFACE: &'static Option> = &Some( zbus::names::InterfaceName::from_static_str_unchecked("org.freedesktop.Secret.Service"), ); const DESTINATION: &'static Option> = &Some(DESTINATION); const PATH: &'static Option> = &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::proxy::Builder::new(connection) .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, ) -> 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::try_from(service_key)?), }; Ok((key, session)) } #[doc(alias = "CreateCollection")] pub async fn create_collection( &self, label: &str, alias: &str, window_id: Option, ) -> Result, Error> { let properties = Properties::for_collection(label); let (collection_path, prompt_path) = self .inner() .call_method("CreateCollection", &(properties, alias)) .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(window_id).await?; OwnedObjectPath::try_from(response)? } 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], window_id: Option, ) -> 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(window_id).await?; let locked_paths = Vec::::try_from(response)?; unlocked_item_paths.extend(locked_paths); }; Ok(unlocked_item_paths) } pub async fn lock( &self, items: &[impl Unlockable], window_id: Option, ) -> 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(window_id).await?; let locked_paths = Vec::::try_from(response)?; locked_item_paths.extend(locked_paths); }; Ok(locked_item_paths) } #[doc(alias = "GetSecrets")] pub async fn secrets( &self, items: &[Item<'_>], session: &Session<'_>, ) -> Result, DBusSecret<'_>>, Error> { let secrets = self .inner() .call_method("GetSecrets", &(items, session)) .await .map_err::(From::from)? .body() .deserialize::>()?; let cnx = self.inner().connection(); // Item's Hash implementation doesn't make use of any mutable internals #[allow(clippy::mutable_key_type)] let mut output = HashMap::with_capacity(secrets.capacity()); for (path, secret_inner) in secrets { output.insert( Item::new(cnx, path).await?, DBusSecret::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 fmt::Debug for Service<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Service") .field(&self.inner().path().as_str()) .finish() } } oo7-0.5.0/src/dbus/api/session.rs000064400000000000000000000035351046102023000146660ustar 00000000000000use std::fmt; use serde::Serialize; use zbus::zvariant::{ObjectPath, Type}; 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 zbus::proxy::Defaults for Session<'_> { const INTERFACE: &'static Option> = &Some( zbus::names::InterfaceName::from_static_str_unchecked("org.freedesktop.Secret.Session"), ); const DESTINATION: &'static Option> = &Some(DESTINATION); const PATH: &'static Option> = &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::proxy::Builder::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 Serialize for Session<'_> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { ObjectPath::serialize(self.inner().path(), serializer) } } impl fmt::Debug for Session<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Session") .field(&self.inner().path().as_str()) .finish() } } oo7-0.5.0/src/dbus/collection.rs000064400000000000000000000225651046102023000145710ustar 00000000000000use std::{sync::Arc, time::Duration}; use ashpd::WindowIdentifier; #[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::{Algorithm, Error, Item, api}; use crate::{AsAttributes, Key, Secret}; /// 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`. pub async fn create_item( &self, label: &str, attributes: &impl AsAttributes, secret: impl Into, replace: bool, window_id: Option, ) -> Result, Error> { if !self.is_available().await { Err(Error::Deleted) } else { let secret = match self.algorithm { Algorithm::Plain => api::DBusSecret::new(Arc::clone(&self.session), secret), Algorithm::Encrypted => api::DBusSecret::new_encrypted( Arc::clone(&self.session), secret, self.aes_key.as_ref().unwrap(), )?, }; let item = self .inner .create_item(label, attributes, &secret, replace, window_id) .await?; Ok(self.new_item(item)) } } /// Unlock the collection. pub async fn unlock(&self, window_id: Option) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.service .unlock(&[self.inner.inner().path()], window_id) .await?; Ok(()) } } /// Lock the collection. pub async fn lock(&self, window_id: Option) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.service .lock(&[self.inner.inner().path()], window_id) .await?; Ok(()) } } /// Delete the collection. pub async fn delete(&self, window_id: Option) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.delete(window_id).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(all(feature = "tokio", feature = "local_tests"))] mod tests { use std::collections::HashMap; use crate::dbus::Service; 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 = crate::Secret::text("a password"); let collection = service.default_collection().await.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.clone(), true, None) .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(None).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] async fn create_plain_item() { let service = Service::plain().await.unwrap(); create_item(service, false).await; } #[tokio::test] async fn create_encrypted_item() { let service = Service::encrypted().await.unwrap(); create_item(service, true).await; } } oo7-0.5.0/src/dbus/error.rs000064400000000000000000000045431046102023000135630ustar 00000000000000/// 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(String), /// Session does not exist. NoSession(String), /// Collection/Item does not exist. NoSuchObject(String), } /// 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), /// Crypto related error. Crypto(crate::crypto::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 From for Error { fn from(value: crate::crypto::Error) -> Self { Self::Crypto(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 { 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"), Self::Crypto(e) => write!(f, "Failed to do a cryptography operation, {e}"), } } } oo7-0.5.0/src/dbus/item.rs000064400000000000000000000131511046102023000133630ustar 00000000000000use std::{collections::HashMap, sync::Arc, time::Duration}; use ashpd::WindowIdentifier; #[cfg(feature = "async-std")] use async_lock::RwLock; #[cfg(feature = "tokio")] use tokio::sync::RwLock; use zbus::zvariant::ObjectPath; use super::{Algorithm, Error, api}; use crate::{AsAttributes, Key, Secret}; /// 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, window_id: Option) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.inner.delete(window_id).await?; *self.available.write().await = false; Ok(()) } } /// Retrieve the currently stored secret. pub async fn secret(&self) -> Result { if !self.is_available().await { Err(Error::Deleted) } else { self.inner .secret(&self.session) .await? .decrypt(self.aes_key.as_ref()) } } /// Modify the stored secret on the item. /// /// # Arguments /// /// * `secret` - The secret to store. #[doc(alias = "SetSecret")] pub async fn set_secret(&self, secret: impl Into) -> Result<(), Error> { let secret = match self.algorithm { Algorithm::Plain => api::DBusSecret::new(Arc::clone(&self.session), secret), Algorithm::Encrypted => { let aes_key = self.aes_key.as_ref().unwrap(); api::DBusSecret::new_encrypted(Arc::clone(&self.session), secret, aes_key)? } }; self.inner.set_secret(&secret).await?; Ok(()) } /// Unlock the item. pub async fn unlock(&self, window_id: Option) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.service .unlock(&[self.inner.inner().path()], window_id) .await?; Ok(()) } } /// Lock the item. pub async fn lock(&self, window_id: Option) -> Result<(), Error> { if !self.is_available().await { Err(Error::Deleted) } else { self.service .lock(&[self.inner.inner().path()], window_id) .await?; Ok(()) } } /// Returns item path pub fn path(&self) -> &ObjectPath<'_> { self.inner.inner().path() } } oo7-0.5.0/src/dbus/mod.rs000064400000000000000000000031201046102023000131770ustar 00000000000000//! A [Secret Service](https://specifications.freedesktop.org/secret-service-spec/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, "password", true, None) //! .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?, oo7::Secret::text("password")); //! //! # Ok(()) //! # } //! ``` /// 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.5.0/src/dbus/service.rs000064400000000000000000000211361046102023000140670ustar 00000000000000use std::sync::Arc; use ashpd::WindowIdentifier; use futures_util::{Stream, StreamExt}; use zbus::zvariant::OwnedObjectPath; use super::{Algorithm, Collection, Error, ServiceError, api}; use crate::Key; /// The entry point of communicating with a [`org.freedesktop.Secrets`](https://specifications.freedesktop.org/secret-service-spec/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> { /// The default collection alias. /// /// In general, you are supposed to use [`Service::default_collection`]. pub const DEFAULT_COLLECTION: &'static str = "default"; /// A session collection. /// /// The collection is cleared when the user ends the session. pub const SESSION_COLLECTION: &'static str = "session"; /// 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(Error::Service(ServiceError::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| Key::generate_aes_key(&private_key, &service_key)) .transpose()? .map(Arc::new); (aes_key, session) } }; Ok(Self { aes_key, inner: service, session: Arc::new(session), algorithm, }) } /// Retrieve the default collection if any or create one. /// /// The created collection label is set to `Default`. If you want to /// translate the string, use [Self::with_alias_or_create] instead. pub async fn default_collection(&self) -> Result, Error> { // TODO: Figure how to make those labels translatable self.with_alias_or_create(Self::DEFAULT_COLLECTION, "Default", None) .await } /// Retrieve the session collection if any or create one. /// /// The created collection label is set to `Default`. If you want to /// translate the string, use [Self::with_alias_or_create] instead. pub async fn session_collection(&self) -> Result, Error> { // TODO: Figure how to make those labels translatable self.with_alias_or_create(Self::SESSION_COLLECTION, "Session", None) .await } pub async fn with_alias_or_create( &self, alias: &str, label: &str, window_id: Option, ) -> Result, Error> { match self.with_alias(alias).await { Ok(Some(collection)) => Ok(collection), Ok(None) => self.create_collection(label, alias, window_id).await, Err(err) => Err(err), } } /// 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. pub async fn create_collection( &self, label: &str, alias: &str, window_id: Option, ) -> Result, Error> { self.inner .create_collection(label, alias, window_id) .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 { 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(all(feature = "tokio", feature = "local_tests"))] mod tests { use super::Service; #[tokio::test] async fn create_collection() { let service = Service::new().await.unwrap(); let collection = service .create_collection("somelabel", "Some Label", 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(None).await.unwrap(); let found_collection = service.with_label("somelabel").await.unwrap(); assert!(found_collection.is_none()); } } oo7-0.5.0/src/error.rs000064400000000000000000000015211046102023000126170ustar 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. File(crate::file::Error), /// Secret Service error. DBus(crate::dbus::Error), } impl From for Error { fn from(e: crate::file::Error) -> Self { Self::File(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::File(e) => write!(f, "File backend error {e}"), Self::DBus(e) => write!(f, "DBus error {e}"), } } } oo7-0.5.0/src/file/api/attribute_value.rs000064400000000000000000000014471046102023000163640ustar 00000000000000use serde::{Deserialize, Serialize}; use zbus::zvariant::Type; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{Key, Mac, crypto}; /// An 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) -> Result { 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.5.0/src/file/api/encrypted_item.rs000064400000000000000000000037551046102023000162040ustar 00000000000000use std::collections::HashMap; use serde::{Deserialize, Serialize}; use zbus::zvariant::Type; use super::{Error, Item}; use crate::{Key, Mac, crypto}; #[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, value_mac: &Mac) -> bool { self.hashed_attributes.get(key) == Some(value_mac) } pub fn decrypt(self, key: &Key) -> Result { let n = self.blob.len(); let n_mac = crypto::mac_len(); let n_iv = crypto::iv_len(); // The encrypted data, the iv, and the mac are concatenated into blob. let (encrypted_data_with_iv, mac_tag) = &self.blob.split_at(n - n_mac); // verify item if !crypto::verify_mac(encrypted_data_with_iv, key, mac_tag)? { return Err(Error::MacError); } let (encrypted_data, iv) = encrypted_data_with_iv.split_at(n - n_mac - n_iv); // decrypt item let decrypted = crypto::decrypt(encrypted_data, 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.as_slice(), )? { return Err(Error::HashedAttributeMac(attribute_key.to_owned())); } } else { return Err(Error::HashedAttributeMac(attribute_key.to_owned())); } } Ok(()) } } oo7-0.5.0/src/file/api/legacy_keyring.rs000064400000000000000000000221021046102023000161500ustar 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::{ AsAttributes, crypto, file::{AttributeValue, Error, WeakKeyError}, }; 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 secret = Secret::blob("test"); let items = keyring.decrypt_items(&secret)?; assert_eq!(items.len(), 1); assert_eq!(items[0].label(), "foo"); assert_eq!(items[0].secret(), Secret::blob("foo")); let attributes = items[0].attributes(); assert_eq!(attributes.len(), 2); // also content-type assert_eq!( attributes .get(crate::XDG_SCHEMA_ATTRIBUTE) .map(|v| v.as_ref()), Some("org.gnome.keyring.Note") ); Ok(()) } } oo7-0.5.0/src/file/api/mod.rs000064400000000000000000000273401046102023000137440ustar 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::LazyLock, }; #[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::{Endian, Type, serialized::Context}; /// 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::{ AsAttributes, Key, crypto, file::{Error, WeakKeyError}, }; 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) static GVARIANT_ENCODING: LazyLock = LazyLock::new(|| 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::file) items: Vec, } impl Keyring { #[allow(clippy::new_without_default)] pub(crate) fn new() -> Self { let salt = rand::rng().random::<[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::rng() .sample_iter(&rand::distr::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)| v.as_ref().is_ok_and(|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)| v.as_ref().is_ok_and(|v| e.has_attribute(k, v))) }) .map(|e| (*e).clone().decrypt(key)) .transpose() } pub fn lookup_item_index(&self, attributes: &impl AsAttributes, key: &Key) -> Option { let hashed_search = attributes.hash(key); self.items.iter().position(|e| { hashed_search .iter() .all(|(k, v)| v.as_ref().is_ok_and(|v| e.has_attribute(k, v))) }) } 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)| v.as_ref().is_ok_and(|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) = data_dir() { path.push("keyrings"); if version > 0 { path.push(format!("v{version}")); } path.push(format!("{name}.keyring")); Ok(path) } else { Err(Error::NoDataDir) } } pub fn default_path() -> Result { Self::path("default", LEGACY_MAJOR_VERSION) } pub fn derive_key(&self, secret: &Secret) -> Result { 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::rng().random::<[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::*; use crate::secret::ContentType; 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, Secret::blob("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", ) .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(), Secret::text("A Password")); assert_eq!(loaded_items[0].secret().content_type(), ContentType::Text); let _silent = std::fs::remove_file("/tmp/test.keyring"); Ok(()) } } oo7-0.5.0/src/file/error.rs000064400000000000000000000135301046102023000135410ustar 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 request has been cancelled. Portal(ashpd::Error), /// 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), /// Incorrect secret IncorrectSecret, /// Crypto related error. Crypto(crate::crypto::Error), } 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: std::str::Utf8Error) -> Self { Self::Utf8(value) } } impl From for Error { fn from(value: ashpd::Error) -> Self { Self::Portal(value) } } impl From for Error { fn from(value: crate::crypto::Error) -> Self { Self::Crypto(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 { Self::FileHeaderMismatch(e) => { write!(f, "File header doesn't match FILE_HEADER {e:#?}") } Self::VersionMismatch(e) => write!( f, "Version doesn't match MAJOR_VERSION OR MICRO_VERSION {e:#?}", ), Self::NoData => write!(f, "No data behind header and version bytes"), Self::NoParentDir(e) => write!(f, "No Parent Directory {e}"), Self::GVariantDeserialization(e) => write!(f, "Failed to deserialize {e}"), Self::SaltSizeMismatch(arr, explicit) => write!( f, "Salt size is not as expected. Array: {arr}, Explicit: {explicit}" ), Self::WeakKey(err) => write!(f, "{err}"), Self::Io(e) => write!(f, "IO error {e}"), Self::MacError => write!(f, "Mac digest is not equal to the expected value"), Self::ChecksumMismatch => write!(f, "Checksum is not equal to the expected value"), Self::HashedAttributeMac(e) => write!(f, "Failed to validate hashed attribute {e}"), Self::NoDataDir => write!(f, "Couldn't retrieve XDG_DATA_DIR"), Self::TargetFileChanged(e) => write!(f, "The target file has changed {e}"), Self::Portal(e) => write!(f, "Portal communication failed {e}"), Self::InvalidItemIndex(index) => { write!(f, "The addressed item index {index} does not exist") } Self::Utf8(e) => write!(f, "UTF-8 encoding error {e}"), Self::AlgorithmMismatch(e) => write!(f, "Unknown algorithm {e}"), Self::IncorrectSecret => write!(f, "Incorrect secret"), Self::Crypto(e) => write!(f, "Failed to do a cryptography operation, {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.5.0/src/file/item.rs000064400000000000000000000172311046102023000133500ustar 00000000000000use std::{collections::HashMap, str::FromStr, time::Duration}; use serde::{Deserialize, Serialize}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use super::{ Error, api::{AttributeValue, EncryptedItem, GVARIANT_ENCODING}, }; use crate::{AsAttributes, CONTENT_TYPE_ATTRIBUTE, Key, Secret, crypto, secret::ContentType}; /// An item stored in the file backend. #[derive( Deserialize, Serialize, zvariant::Type, Clone, Debug, Zeroize, ZeroizeOnDrop, PartialEq, )] 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 Into, ) -> Self { let now = std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap() .as_secs(); let mut item_attributes: HashMap = attributes .as_attributes() .into_iter() .map(|(k, v)| (k.to_string(), v.into())) .collect(); let secret = secret.into(); // Set default MIME type if not provided if !item_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) { item_attributes.insert( CONTENT_TYPE_ATTRIBUTE.to_owned(), secret.content_type().as_str().into(), ); } Self { attributes: item_attributes, label: label.to_string(), created: now, modified: now, secret: secret.as_bytes().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) { let mut new_attributes: HashMap = attributes .as_attributes() .into_iter() .map(|(k, v)| (k.to_string(), v.into())) .collect(); // Preserve MIME type if not explicitly set in new attributes if !new_attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) { if let Some(existing_mime_type) = self.attributes.get(CONTENT_TYPE_ATTRIBUTE) { new_attributes.insert( CONTENT_TYPE_ATTRIBUTE.to_string(), existing_mime_type.clone(), ); } else { new_attributes.insert( CONTENT_TYPE_ATTRIBUTE.to_owned(), ContentType::default().as_str().into(), ); } } self.attributes = new_attributes; self.modified = std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap() .as_secs(); } /// 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) -> Secret { let content_type = self .attributes .get(CONTENT_TYPE_ATTRIBUTE) .and_then(|c| ContentType::from_str(c).ok()) .unwrap_or_default(); Secret::with_content_type(content_type, &self.secret) } /// Store a new secret. pub fn set_secret(&mut self, secret: impl Into) { self.modified = std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap() .as_secs(); self.secret = secret.into().as_bytes().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 iv = crypto::generate_iv()?; self.encrypt_inner(key, &iv) } fn encrypt_inner(&self, key: &Key, iv: &[u8]) -> Result { let decrypted = Zeroizing::new(zvariant::to_bytes(*GVARIANT_ENCODING, &self)?.to_vec()); let mut blob = crypto::encrypt(&*decrypted, key, iv)?; blob.extend_from_slice(iv); let mac = crypto::compute_mac(&blob, key)?; blob.extend_from_slice(mac.as_slice()); let hashed_attributes = self .attributes .iter() .filter_map(|(k, v)| Some((k.to_owned(), v.mac(key).ok()?))) .collect(); Ok(EncryptedItem { hashed_attributes, blob, }) } } impl TryFrom<&[u8]> for Item { type Error = Error; fn try_from(value: &[u8]) -> Result { let mut item: Item = zvariant::serialized::Data::new(value, *GVARIANT_ENCODING) .deserialize()? .0; // Ensure MIME type attribute exists for backward compatibility if !item.attributes.contains_key(CONTENT_TYPE_ATTRIBUTE) { item.attributes.insert( CONTENT_TYPE_ATTRIBUTE.to_owned(), ContentType::default().as_str().into(), ); } Ok(item) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_file_item_serialization() { let key = Key::new(vec![ 204, 53, 139, 40, 55, 167, 183, 240, 191, 252, 186, 174, 28, 36, 229, 26, ]); let n_mac = crypto::mac_len(); let n_iv = crypto::iv_len(); let iv = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0]; assert_eq!(iv.len(), n_iv); let attribute_value = AttributeValue::from("5"); let attribute_value_mac = attribute_value.mac(&key).unwrap(); let mut item = Item { attributes: HashMap::from([("fooness".to_string(), attribute_value)]), label: "foo".to_string(), created: 50, modified: 50, secret: b"bar".to_vec(), }; let encrypted = item.encrypt_inner(&key, &iv).unwrap(); assert!(encrypted.has_attribute("fooness", &attribute_value_mac)); let blob = &encrypted.blob; let n = blob.len(); // encrypted.blob should be the concatenation of the encrypted data, the // iv, and the mac. let encrypted_item_blob = &encrypted.blob[..n - n_mac - n_iv]; let item_mac = crypto::compute_mac(&encrypted.blob[..n - n_mac], &key).unwrap(); assert_eq!(&blob[n - n_mac..], item_mac.as_slice()); assert_eq!(&blob[n - n_mac - n_iv..n - n_mac], &iv); assert_eq!( encrypted_item_blob, vec![ 196, 246, 127, 53, 194, 30, 176, 37, 128, 145, 195, 96, 211, 161, 60, 150, 160, 126, 85, 125, 85, 238, 5, 93, 153, 128, 176, 205, 31, 87, 48, 82, 121, 230, 143, 152, 153, 193, 182, 114, 59, 157, 85, 41, 50, 1, 142, 112 ] ); let decrypted = encrypted.decrypt(&key).unwrap(); // The decrypted item matches the original one but with the content-type // attribute set. item.attributes.insert( crate::CONTENT_TYPE_ATTRIBUTE.to_string(), AttributeValue::from(crate::secret::ContentType::Blob.as_str()), ); assert_eq!(decrypted, item); } } oo7-0.5.0/src/file/mod.rs000064400000000000000000000761461046102023000132030ustar 00000000000000//! File backend implementation that can be 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::file::Keyring; //! //! # async fn run() -> oo7::Result<()> { //! let keyring = Keyring::load_default().await?; //! keyring //! .create_item( //! "My Label", //! &HashMap::from([("account", "alice")]), //! "My Password", //! true, //! ) //! .await?; //! //! let items = keyring //! .search_items(&HashMap::from([("account", "alice")])) //! .await?; //! assert_eq!(items[0].secret(), oo7::Secret::blob("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 crate::{AsAttributes, Key, Secret}; #[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; pub use error::{Error, InvalidItemError, WeakKeyError}; pub use item::Item; type ItemDefinition = (String, HashMap, Secret, bool); /// File backed keyring. #[derive(Debug)] pub struct Keyring { keyring: Arc>, path: Option, /// 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::from(ashpd::desktop::secret::retrieve().await?); Self::load(api::Keyring::default_path()?, secret).await } /// Load from default keyring file /// /// # Safety /// /// The secret validation is skipped. pub async unsafe fn load_default_unchecked() -> Result { #[cfg(feature = "tracing")] tracing::debug!("Loading default keyring file"); let secret = Secret::from(ashpd::desktop::secret::retrieve().await?); unsafe { Self::load_unchecked(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 { Self::load_inner(path, secret, true).await } /// Load from a keyring file. /// /// # Arguments /// /// * `path` - The path to the file backend. /// * `secret` - The service key, usually retrieved from the Secrets portal. /// /// # Safety /// /// The secret is not validated to be the correct one to decrypt the keyring /// items. Allowing the API user to write new items with a different /// secret on top of previously added items with a different secret. /// /// As it is not a supported behaviour, this API is mostly meant for /// recovering broken keyrings. pub async unsafe fn load_unchecked( path: impl AsRef, secret: Secret, ) -> Result { Self::load_inner(path, secret, false).await } async fn load_inner( path: impl AsRef, secret: Secret, validate_items: bool, ) -> 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 its 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) } }; let key = if validate_items { let key = keyring.derive_key(&secret)?; let mut n_broken_items = 0; let mut n_valid_items = 0; for encrypted_item in &keyring.items { if encrypted_item.clone().decrypt(&key).is_err() { n_broken_items += 1; } else { n_valid_items += 1; } } if n_valid_items == 0 && n_broken_items != 0 { #[cfg(feature = "tracing")] tracing::error!("Keyring cannot be decrypted. Invalid secret."); return Err(Error::IncorrectSecret); } else if n_broken_items > n_valid_items { #[cfg(feature = "tracing")] { tracing::warn!( "The file contains {n_broken_items} broken items and {n_valid_items} valid ones." ); tracing::info!( "Please switch to `Keyring::load_unchecked` to load the keyring without the secret validation. `Keyring::delete_broken_items` can be used to remove them or alternatively with `oo7-cli --repair`." ); } return Err(Error::IncorrectSecret); } Some(Arc::new(key)) } else { None }; Ok(Self { keyring: Arc::new(RwLock::new(keyring)), path: Some(path.as_ref().to_path_buf()), mtime: Mutex::new(mtime), key: Mutex::new(key), secret: Mutex::new(Arc::new(secret)), }) } /// Creates a temporary backend, that is never stored on disk. pub async fn temporary(secret: Secret) -> Result { let keyring = api::Keyring::new(); Ok(Self { keyring: Arc::new(RwLock::new(keyring)), path: None, mtime: Default::default(), 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: Some(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: Some(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: Some(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) -> Result>, Error> { let key = self.derive_key().await?; let keyring = self.keyring.read().await; Ok(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) } /// Find the index in the list of items of the first item matching the /// attributes. pub async fn lookup_item_index( &self, attributes: &impl AsAttributes, ) -> Result, Error> { let key = self.derive_key().await?; let keyring = self.keyring.read().await; Ok(keyring.lookup_item_index(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 Into, 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`] 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"); if let Some(ref path) = self.path { keyring.dump(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); if let Some(ref path) = self.path { keyring.dump(path, *mtime).await?; } }; let Some(ref path) = self.path else { return Ok(()); }; if let Ok(modified) = fs::metadata(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) -> Result, crate::crypto::Error> { 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)); } Ok(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 } /// Delete any item that cannot be decrypted with the key associated to the /// keyring. /// /// This can only happen if an item was created using /// [`Self::load_unchecked`] or prior to 0.4 where we didn't validate /// the secret when using [`Self::load`] or modified externally. pub async fn delete_broken_items(&self) -> Result { let key = self.derive_key().await?; let mut keyring = self.keyring.write().await; let mut broken_items = vec![]; for (index, encrypted_item) in keyring.items.iter().enumerate() { if let Err(_err) = encrypted_item.clone().decrypt(&key) { broken_items.push(index); } } let n_broken_items = broken_items.len(); for index in broken_items { keyring.items.remove(index); } drop(keyring); self.write().await?; Ok(n_broken_items) } } #[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(), Secret::blob("foo")); let attributes = items[0].attributes(); assert_eq!(attributes.len(), 2); assert_eq!( attributes .get(crate::XDG_SCHEMA_ATTRIBUTE) .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?; unsafe { std::env::set_var("XDG_DATA_HOME", data_dir.path()); } assert!(!v1_dir.join("default.keyring").exists()); let secret = Secret::blob("test"); 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?; unsafe { std::env::set_var("XDG_DATA_HOME", data_dir.path()); } let secret = Secret::blob("test"); 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_wrong_password() -> 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?; unsafe { std::env::set_var("XDG_DATA_HOME", data_dir.path()); } let secret = Secret::blob("wrong"); let keyring = Keyring::open("default", secret).await; assert!(keyring.is_err()); assert!(matches!(keyring.unwrap_err(), Error::IncorrectSecret)); let secret = Secret::blob("test"); let keyring = Keyring::open("default", secret).await; assert!(keyring.is_ok()); 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?; unsafe { std::env::set_var("XDG_DATA_HOME", data_dir.path()); } let secret = Secret::blob("test"); 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?; unsafe { std::env::set_var("XDG_DATA_HOME", data_dir.path()); } let secret = Secret::blob("test"); let keyring = Keyring::open("default", secret).await?; assert!(!v1_dir.join("default.keyring").exists()); keyring .create_item( "foo", &HashMap::from([(crate::XDG_SCHEMA_ATTRIBUTE, "org.gnome.keyring.Note")]), "foo", false, ) .await?; keyring.write().await?; assert!(v1_dir.join("default.keyring").exists()); Ok(()) } #[tokio::test] async fn delete_broken_items() -> 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"); let keyring_path = v1_dir.join("default.keyring"); fs::copy(&fixture_path, &keyring_path).await?; let keyring = Keyring::load(&keyring_path, Secret::blob("test")).await?; keyring .create_item( "test 3", &HashMap::from([("attr3", "value3")]), "password3", false, ) .await?; drop(keyring); let keyring = unsafe { Keyring::load_unchecked(&keyring_path, Secret::blob("wrong_password")).await? }; keyring .create_item( "test", &HashMap::from([("attr", "value")]), "password", false, ) .await?; drop(keyring); assert!( Keyring::load(&keyring_path, Secret::blob("wrong_password")) .await .is_err() ); let keyring = Keyring::load(&keyring_path, Secret::blob("test")).await?; keyring .create_item( "test 2", &HashMap::from([("attr2", "value2")]), "password2", false, ) .await?; assert_eq!(keyring.delete_broken_items().await?, 1); assert_eq!(keyring.delete_broken_items().await?, 0); fs::remove_file(keyring_path).await?; Ok(()) } #[tokio::test] async fn change_secret() -> 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"); let keyring_path = v1_dir.join("default.keyring"); fs::copy(&fixture_path, &keyring_path).await?; let keyring = Keyring::load(&keyring_path, Secret::blob("test")).await?; let attributes = HashMap::from([("attr", "value")]); let item_before = keyring .create_item("test", &attributes, "password", false) .await?; let secret = Secret::blob("new_secret"); keyring.change_secret(secret).await?; let secret = Secret::blob("new_secret"); let keyring = Keyring::load(&keyring_path, 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()); // No items were broken during the secret change assert_eq!(keyring.delete_broken_items().await?, 0); fs::remove_file(keyring_path).await?; Ok(()) } #[tokio::test] async fn content_type() -> Result<(), Error> { use crate::secret::ContentType; let keyring = Keyring::temporary(Secret::blob("test_password")).await?; // Add items with different MIME types keyring .create_item( "Text", &HashMap::from([("type", "text")]), Secret::text("Hello, World!"), false, ) .await?; keyring .create_item( "Password", &HashMap::from([("type", "password")]), Secret::blob("super_secret_password"), false, ) .await?; let items = keyring .search_items(&HashMap::from([("type", "text")])) .await?; assert_eq!(items.len(), 1); assert_eq!(items[0].secret().content_type(), ContentType::Text); let items = keyring .search_items(&HashMap::from([("type", "password")])) .await?; assert_eq!(items.len(), 1); assert_eq!(items[0].secret().content_type(), ContentType::Blob); Ok(()) } } oo7-0.5.0/src/key.rs000064400000000000000000000113101046102023000122530ustar 00000000000000use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{crypto, file}; /// A key. #[derive(Debug, Zeroize, ZeroizeOnDrop)] pub struct Key { key: Vec, #[zeroize(skip)] strength: Result<(), file::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 fn new(key: Vec) -> Self { Self::new_with_strength(key, Err(file::WeakKeyError::StrengthUnknown)) } pub(crate) fn check_strength(&self) -> Result<(), file::WeakKeyError> { self.strength } pub(crate) fn new_with_strength( key: Vec, strength: Result<(), file::WeakKeyError>, ) -> Self { Self { key, strength } } pub fn generate_private_key() -> Result { Ok(Self::new(crypto::generate_private_key()?.to_vec())) } pub fn generate_public_key(private_key: &Self) -> Result { Ok(Self::new(crypto::generate_public_key(private_key)?)) } pub fn generate_aes_key( private_key: &Self, server_public_key: &Self, ) -> Result { Ok(Self::new( crypto::generate_aes_key(private_key, server_public_key)?.to_vec(), )) } } impl From for zvariant::Value<'static> { fn from(key: Key) -> Self { let mut key = key; let inner: Vec = std::mem::take(&mut key.key); zvariant::Array::from(inner).into() } } impl From for zvariant::OwnedValue { fn from(key: Key) -> Self { zvariant::Value::from(key).try_to_owned().unwrap() } } impl TryFrom> for Key { type Error = zvariant::Error; fn try_from(value: zvariant::Value<'_>) -> Result { Ok(Key::new(value.try_into()?)) } } impl TryFrom for Key { type Error = zvariant::Error; fn try_from(value: zvariant::OwnedValue) -> Result { Self::try_from(zvariant::Value::from(value)) } } #[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.unwrap().as_ref(), expected_public_key); assert_eq!(aes_key.unwrap().as_ref(), expected_aes_key); } } oo7-0.5.0/src/keyring.rs000064400000000000000000000333541046102023000131470ustar 00000000000000use std::{collections::HashMap, sync::Arc, time::Duration}; #[cfg(feature = "async-std")] use async_lock::RwLock; #[cfg(feature = "tokio")] use tokio::sync::RwLock; use crate::{AsAttributes, Result, Secret, dbus, file}; /// A [Secret Service](crate::dbus) or [file](crate::file) backed keyring /// implementation. /// /// It will automatically use the file backend if the application is sandboxed /// and otherwise falls back to the DBus service using it [default /// collection](crate::dbus::Service::default_collection). /// /// 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 that automatically removes the /// broken items from the file backend keyring. /// /// This method will probably be removed in future versions if the /// misbehaviour is tracked and fixed. #[deprecated = "The method is no longer useful as the user can fix the keyring using oo7-cli"] pub async fn with_broken_item_cleanup() -> Result { Self::new_inner(true).await } /// Create a new instance of the Keyring. pub async fn new() -> Result { Self::new_inner(false).await } async fn new_inner(auto_delete_broken_items: bool) -> Result { let is_sandboxed = ashpd::is_sandboxed().await; if is_sandboxed { #[cfg(feature = "tracing")] tracing::debug!("Application is sandboxed, using the file backend"); match file::Keyring::load_default().await { Ok(file) => return Ok(Self::File(Arc::new(file))), // Do nothing in this case, we are supposed to fallback to the host keyring Err(super::file::Error::Portal(ashpd::Error::PortalNotFound(_))) => { #[cfg(feature = "tracing")] tracing::debug!( "org.freedesktop.portal.Secrets is not available, falling back to the Secret Service backend" ); } Err(e) => { if matches!(e, file::Error::IncorrectSecret) && auto_delete_broken_items { let keyring = unsafe { file::Keyring::load_default_unchecked().await? }; let deleted_items = keyring.delete_broken_items().await?; debug_assert!(deleted_items > 0); return Ok(Self::File(Arc::new(keyring))); } return Err(crate::Error::File(e)); } }; } else { #[cfg(feature = "tracing")] tracing::debug!( "Application is not sandboxed, falling back to the Secret Service backend" ); } let service = dbus::Service::new().await?; let collection = service.default_collection().await?; 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(None).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(None).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(None).await?; } } Self::File(backend) => { backend.delete(attributes).await?; } }; Ok(()) } /// Retrieve all the items. 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 Into, replace: bool, ) -> Result<()> { match self { Self::DBus(backend) => { backend .create_item(label, attributes, secret, replace, None) .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) } /// Get the inner file backend if the keyring is backed by one. pub fn as_file(&self) -> Arc { match self { Self::File(keyring) => keyring.clone(), _ => unreachable!(), } } /// Get the inner DBus backend if the keyring is backed by one. pub fn as_dbus(&self) -> &dbus::Collection<'_> { match self { Self::DBus(collection) => collection, _ => unreachable!(), } } } /// 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: file::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) => { let index = backend .lookup_item_index(item.read().await.attributes()) .await?; item.write().await.set_attributes(attributes); let item_guard = item.read().await; if let Some(index) = index { backend.replace_item_index(index, &item_guard).await?; } else { 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 Into) -> 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).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(None).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(None).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(None).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()), } } } #[cfg(test)] #[cfg(feature = "tokio")] mod tests { use tempfile::tempdir; use tokio::fs; use super::*; #[tokio::test] async fn portal_set_attributes() -> Result<()> { let data_dir = tempdir().unwrap(); let dir = data_dir.path().join("keyrings"); fs::create_dir_all(&dir).await.unwrap(); let path = dir.join("default.keyring"); let secret = crate::Secret::text("test"); let keyring = Keyring::File(file::Keyring::load(&path, secret).await?.into()); let items = keyring.items().await?; assert_eq!(items.len(), 0); keyring .create_item("my item", &vec![("key", "value")], "my_secret", false) .await?; let mut items = keyring.items().await?; assert_eq!(items.len(), 1); let item = items.remove(0); assert_eq!(item.label().await?, "my item"); assert_eq!(item.secret().await?, Secret::text("my_secret")); let attrs = item.attributes().await?; assert_eq!(attrs.len(), 2); assert_eq!(attrs.get("key").unwrap(), "value"); item.set_attributes(&vec![("key", "changed_value"), ("new_key", "new_value")]) .await?; let mut items = keyring.items().await?; assert_eq!(items.len(), 1); let item = items.remove(0); assert_eq!(item.label().await?, "my item"); assert_eq!(item.secret().await?, Secret::text("my_secret")); let attrs = item.attributes().await?; assert_eq!(attrs.len(), 3); assert_eq!(attrs.get("key").unwrap(), "changed_value"); assert_eq!(attrs.get("new_key").unwrap(), "new_value"); Ok(()) } } oo7-0.5.0/src/lib.rs000064400000000000000000000057601046102023000122450ustar 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"); #[cfg(all(not(feature = "tokio"), not(feature = "async-std"), not(doc)))] compile_error!("You you have to enable either openssl_crypto or native_crypto feature"); #[cfg(all(all(feature = "native_crypto", feature = "openssl_crypto"), not(doc)))] compile_error!("You can't enable both openssl_crypto & native_crypto features at once"); #[cfg(all( not(feature = "native_crypto"), not(feature = "openssl_crypto"), not(doc) ))] compile_error!("You you have to enable either openssl_crypto or native_crypto feature"); use std::collections::HashMap; mod error; mod key; mod mac; 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; pub use mac::Mac; #[cfg(not(feature = "unstable"))] mod crypto; #[cfg(feature = "unstable")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub mod crypto; pub mod dbus; pub mod file; mod keyring; mod secret; pub use ashpd; pub use error::{Error, Result}; pub use keyring::{Item, Keyring}; pub use migration::migrate; pub use secret::{ContentType, Secret}; pub use zbus; /// A schema attribute. /// /// Currently the key, is not really used but would allow /// to map a Rust struct of simple types to an item attributes with type check. pub const XDG_SCHEMA_ATTRIBUTE: &str = "xdg:schema"; /// A content type attribute. /// /// Defines the type of the secret stored in the item. pub const CONTENT_TYPE_ATTRIBUTE: &str = "xdg:content-type"; /// An item/collection attributes. pub trait AsAttributes { fn as_attributes(&self) -> HashMap<&str, &str>; #[allow(clippy::type_complexity)] fn hash<'a>( &'a self, key: &Key, ) -> Vec<(&'a str, std::result::Result)> { self.as_attributes() .into_iter() .map(|(k, v)| (k, crate::file::AttributeValue::from(v).mac(key))) .collect() } } macro_rules! impl_as_attributes { ($rust_type:ty) => { impl AsAttributes for $rust_type where K: AsRef, V: AsRef, { fn as_attributes(&self) -> std::collections::HashMap<&str, &str> { self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect() } } impl AsAttributes for &$rust_type where K: AsRef, V: AsRef, { fn as_attributes(&self) -> std::collections::HashMap<&str, &str> { self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect() } } }; } impl_as_attributes!([(K, V)]); impl_as_attributes!(HashMap); impl_as_attributes!(std::collections::BTreeMap); impl_as_attributes!(Vec<(K, V)>); oo7-0.5.0/src/mac.rs000064400000000000000000000021121046102023000122230ustar 00000000000000use serde::{Deserialize, Serialize}; #[cfg(feature = "native_crypto")] use subtle::ConstantTimeEq; use zbus::zvariant::Type; // There is no constructor to avoid performing sanity checks, e.g. length. /// A message authentication code. It provides constant-time comparison when /// compared against another mac or against a slice of bytes. #[derive(Deserialize, Serialize, Type, Debug, Clone)] pub struct Mac(Vec); impl Mac { pub(crate) fn new(inner: Vec) -> Self { Mac(inner) } /// Constant-time comparison against a slice of bytes. pub fn verify_slice(&self, other: &[u8]) -> bool { #[cfg(feature = "native_crypto")] { self.0.ct_eq(other).into() } #[cfg(feature = "openssl_crypto")] { openssl::memcmp::eq(&self.0, other) } } // This is made private to prevent non-constant-time comparisons. pub(crate) fn as_slice(&self) -> &[u8] { self.0.as_slice() } } impl PartialEq for Mac { fn eq(&self, other: &Self) -> bool { self.verify_slice(&other.0) } } oo7-0.5.0/src/migration.rs000064400000000000000000000026161046102023000134650ustar 00000000000000use crate::{AsAttributes, Result, dbus::Service, file::Keyring}; /// 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(file) => Ok(file), Err(super::file::Error::Portal(ashpd::Error::PortalNotFound(_))) => { #[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(None).await?; } Ok(()) } oo7-0.5.0/src/secret.rs000064400000000000000000000101431046102023000127530ustar 00000000000000use std::str::FromStr; use serde::{Deserialize, Serialize}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; #[derive(Default, PartialEq, Eq, Copy, Clone, Debug, zvariant::Type)] #[zvariant(signature = "s")] pub enum ContentType { Text, #[default] Blob, } impl Serialize for ContentType { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.as_str().serialize(serializer) } } impl<'de> Deserialize<'de> for ContentType { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; Self::from_str(&s).map_err(serde::de::Error::custom) } } impl FromStr for ContentType { type Err = String; fn from_str(s: &str) -> Result { match s { "text/plain" => Ok(Self::Text), "application/octet-stream" => Ok(Self::Blob), e => Err(format!("Invalid content type: {e}")), } } } impl ContentType { pub fn as_str(&self) -> &'static str { match self { Self::Text => "text/plain", Self::Blob => "application/octet-stream", } } } /// A wrapper around a combination of (secret, content-type). #[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] pub enum Secret { /// Corresponds to [`ContentType::Text`] Text(String), /// Corresponds to [`ContentType::Blob`] Blob(Vec), } impl Secret { /// Generate a random secret, used when creating a session collection. pub fn random() -> Result { let mut secret = [0; 64]; // Equivalent of `ring::rand::SecureRandom` getrandom::fill(&mut secret)?; Ok(Self::blob(secret)) } /// Create a text secret, stored with `text/plain` content type. pub fn text(value: impl AsRef) -> Self { Self::Text(value.as_ref().to_owned()) } /// Create a blob secret, stored with `application/octet-stream` content /// type. pub fn blob(value: impl AsRef<[u8]>) -> Self { Self::Blob(value.as_ref().to_owned()) } pub fn content_type(&self) -> ContentType { match self { Self::Text(_) => ContentType::Text, Self::Blob(_) => ContentType::Blob, } } pub fn as_bytes(&self) -> &[u8] { match self { Self::Text(text) => text.as_bytes(), Self::Blob(bytes) => bytes.as_ref(), } } pub fn with_content_type(content_type: ContentType, secret: impl AsRef<[u8]>) -> Self { match content_type { ContentType::Text => match String::from_utf8(secret.as_ref().to_owned()) { Ok(text) => Secret::text(text), Err(_e) => { #[cfg(feature = "tracing")] tracing::warn!( "Failed to decode secret as UTF-8: {}, falling back to blob", _e ); Secret::blob(secret) } }, _ => Secret::blob(secret), } } } impl From<&[u8]> for Secret { fn from(value: &[u8]) -> Self { Self::blob(value) } } impl From>> for Secret { fn from(value: Zeroizing>) -> Self { Self::blob(value) } } impl From> for Secret { fn from(value: Vec) -> Self { Self::blob(value) } } impl From<&Vec> for Secret { fn from(value: &Vec) -> Self { Self::blob(value) } } impl From<&[u8; N]> for Secret { fn from(value: &[u8; N]) -> Self { Self::blob(value) } } impl From for Secret { fn from(value: String) -> Self { Self::text(value) } } impl From<&str> for Secret { fn from(value: &str) -> Self { Self::text(value) } } impl std::ops::Deref for Secret { type Target = [u8]; fn deref(&self) -> &Self::Target { self.as_bytes() } } impl AsRef<[u8]> for Secret { fn as_ref(&self) -> &[u8] { self.as_bytes() } }