sequoia-gpg-agent-0.4.2/.cargo_vcs_info.json0000644000000001360000000000100143540ustar { "git": { "sha1": "99dc53dd01792015115c56fb64f29f6782eaeaa0" }, "path_in_vcs": "" }sequoia-gpg-agent-0.4.2/.ci/all_commits.sh000075500000000000000000000032721046102023000164630ustar 00000000000000#!/usr/bin/env bash # Test all commits on this branch but the last one. # # Used in the all_commits ci job to ensure all commits build # and tests pass at least for the sequoia-openpgp crate. # Use dummy identity to make git rebase happy. git config user.name "C.I. McTestface" git config user.email "ci.mctestface@example.com" # We are only interested in fast-forward merges. If main is not an # ancestor of HEAD, then we're not doing a fast-forward merge. if ! git merge-base --is-ancestor origin/main HEAD then echo "***" echo "*** WARNING: main cannot be fast-forwarded to HEAD" echo "***" fi MERGE_BASE=$(git merge-base origin/main HEAD) if test "x$MERGE_BASE" = x then echo "Failed to find a common ancestor for main and HEAD." exit 1 fi # Show the commit graph from the merge base to HEAD and from the merge # base to main. git --no-pager log --pretty=oneline --graph \ HEAD origin/main --boundary ^$MERGE_BASE # If the previous commit already is on main we're done. git merge-base --is-ancestor HEAD~ origin/main && echo "All commits tested already" && exit 0 # Show what we are going to check. echo "Checking:" git --no-pager log --pretty=oneline $MERGE_BASE..HEAD~ # Leave out the last commit - it has already been checked. git checkout HEAD~ # Now, run cargo test on each commit. Also fail if it leaves the tree # dirty. git rebase $MERGE_BASE \ --exec 'echo ===; echo ===; echo ===; git log -n 1;' \ --exec 'cargo test --features sequoia-openpgp/crypto-nettle; EC=$?; git status -u all; git diff --exit-code; exit $EC' && echo "All commits passed tests" && exit 0 # The rebase failed - probably because a test failed. git rebase --abort; exit 1 sequoia-gpg-agent-0.4.2/.gitignore000064400000000000000000000000031046102023000151250ustar 00000000000000*~ sequoia-gpg-agent-0.4.2/.gitlab-ci.yml000064400000000000000000000002571046102023000156040ustar 00000000000000stages: - pre-check - build - test - deploy include: - component: "gitlab.com/sequoia-pgp/common-ci/sequoia-pipeline@main" variables: SEQUOIA_CRYPTO_POLICY: "" sequoia-gpg-agent-0.4.2/Cargo.lock0000644000001570310000000000100123360ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", "generic-array 0.14.7", ] [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", ] [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", "aes", "cipher", "ctr", "ghash", "subtle", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "anyhow" version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" [[package]] name = "ascii-canvas" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" dependencies = [ "term", ] [[package]] name = "async-stream" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", "pin-project-lite", ] [[package]] name = "async-stream-impl" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bindgen" version = "0.68.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" dependencies = [ "bitflags", "cexpr", "clang-sys", "lazy_static", "lazycell", "peeking_take_while", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", ] [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array 0.14.7", ] [[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array 0.14.7", ] [[package]] name = "buffered-reader" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd098763fdb64579407a8c83cf0d751e6d4a7e161d0114c89cc181a2ca760ec8" dependencies = [ "bzip2", "flate2", "lazy_static", "libc", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bzip2" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ "bzip2-sys", "libc", ] [[package]] name = "bzip2-sys" version = "0.1.11+1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" dependencies = [ "cc", "libc", "pkg-config", ] [[package]] name = "capnp" version = "0.19.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de71387912cac7dd3cb7c219e09628411620a18061bba58c71453c26ae7bf66a" dependencies = [ "embedded-io", ] [[package]] name = "capnp-futures" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fac483cb34e3bc0be251dba7ce318f465143dd18f948c7bd7ad035f6fecfb1b" dependencies = [ "capnp", "futures", ] [[package]] name = "capnp-rpc" version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b692f9454566fa16c5897a5d329e77496b6c8012777025f18cc82f7a65617e" dependencies = [ "capnp", "capnp-futures", "futures", ] [[package]] name = "cc" version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets 0.52.5", ] [[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 = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_derive" version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "cmac" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" dependencies = [ "cipher", "dbl", "digest", ] [[package]] name = "colorchoice" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.7", "rand_core", "typenum", ] [[package]] name = "ctor" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", "syn", ] [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ "cipher", ] [[package]] name = "curve25519-dalek" version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "digest", "fiat-crypto", "rustc_version", "subtle", "zeroize", ] [[package]] name = "curve25519-dalek-derive" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dbl" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" dependencies = [ "generic-array 0.14.7", ] [[package]] name = "der" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "zeroize", ] [[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 = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dyn-clone" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "eax" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" dependencies = [ "aead", "cipher", "cmac", "ctr", "subtle", ] [[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "signature", ] [[package]] name = "ed25519-dalek" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", "rand_core", "serde", "sha2", "subtle", "zeroize", ] [[package]] name = "either" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "embedded-io" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "ena" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ "log", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fs2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ "libc", "winapi", ] [[package]] name = "futures" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "generic-array" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe739944a5406424e080edccb6add95685130b9f160d5407c639c7df0c5836b0" dependencies = [ "typenum", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "ghash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", "polyval", ] [[package]] name = "gimli" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "inout" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ "block-padding", "generic-array 0.14.7", ] [[package]] name = "is_terminal_polyfill" version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "lalrpop" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", "bit-set", "ena", "itertools", "lalrpop-util", "petgraph", "regex", "regex-syntax", "string_cache", "term", "tiny-keccak", "unicode-xid", "walkdir", ] [[package]] name = "lalrpop-util" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ "regex-automata", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" dependencies = [ "spin", ] [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", "windows-targets 0.52.5", ] [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags", "libc", ] [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memsec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa0916b001582d253822171bd23f4a0229d32b9507fae236f5da8cad515ba7c" [[package]] name = "memsec" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "nettle" version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44e6ff4a94e5d34a1fd5abbd39418074646e2fa51b257198701330f22fcd6936" dependencies = [ "getrandom", "libc", "nettle-sys", "thiserror", "typenum", ] [[package]] name = "nettle-sys" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b495053a10a19a80e3a26bf1212e92e29350797b5f5bdc58268c3f3f818e66ec" dependencies = [ "bindgen", "cc", "libc", "pkg-config", "tempfile", "vcpkg", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-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", "smallvec", ] [[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-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "object" version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.5", ] [[package]] name = "peeking_take_while" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", ] [[package]] name = "phf_shared" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", ] [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polyval" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", "universal-hash", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags", ] [[package]] name = "redox_users" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", "thiserror", ] [[package]] name = "regex" version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustversion" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "sequoia-gpg-agent" version = "0.4.2" dependencies = [ "anyhow", "chrono", "clap", "futures", "lalrpop", "lalrpop-util", "lazy_static", "libc", "sequoia-ipc", "sequoia-openpgp", "stfu8", "tempfile", "thiserror", "tokio", "tokio-test", ] [[package]] name = "sequoia-ipc" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4a7e644ec9e1055fde8dcdaa65c58fa4636c615b5e955a9b1942444145e308a" dependencies = [ "anyhow", "buffered-reader", "capnp-rpc", "ctor", "dirs", "fs2", "lalrpop", "lalrpop-util", "lazy_static", "libc", "memsec 0.7.0", "rand", "sequoia-openpgp", "socket2", "tempfile", "thiserror", "tokio", "tokio-util", "winapi", ] [[package]] name = "sequoia-openpgp" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06f82708c8568218b8544b4abbba1f6483067dca0a946a54991c1d3f424dcade" dependencies = [ "aes-gcm", "anyhow", "base64", "buffered-reader", "bzip2", "chrono", "cipher", "dyn-clone", "eax", "ed25519", "ed25519-dalek", "flate2", "getrandom", "idna", "lalrpop", "lalrpop-util", "lazy_static", "libc", "memsec 0.6.3", "nettle", "num-bigint-dig", "once_cell", "rand_core", "regex", "regex-syntax", "sha1collisiondetection", "thiserror", "win-crypto-ng", "winapi", "xxhash-rust", ] [[package]] name = "serde" version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha1collisiondetection" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f606421e4a6012877e893c399822a4ed4b089164c5969424e1b9d1e66e6964b" dependencies = [ "digest", "generic-array 1.0.0", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "rand_core", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spki" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", ] [[package]] name = "stfu8" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978" [[package]] name = "string_cache" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", "parking_lot", "phf_shared", "precomputed-hash", ] [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" [[package]] name = "syn" version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys 0.52.0", ] [[package]] name = "term" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", "winapi", ] [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix", "windows-sys 0.48.0", ] [[package]] name = "thiserror" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-stream" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-test" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" dependencies = [ "async-stream", "bytes", "futures-core", "tokio", "tokio-stream", ] [[package]] name = "tokio-util" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-xid" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle", ] [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "win-crypto-ng" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99abfb435a71e54ab2971d8d8c32f1a7e006cdbf527f71743b1d45b93517bb92" dependencies = [ "cipher", "doc-comment", "rand_core", "winapi", "zeroize", ] [[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-util" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ "windows-sys 0.52.0", ] [[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-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.5", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.5", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ "windows_aarch64_gnullvm 0.52.5", "windows_aarch64_msvc 0.52.5", "windows_i686_gnu 0.52.5", "windows_i686_gnullvm", "windows_i686_msvc 0.52.5", "windows_x86_64_gnu 0.52.5", "windows_x86_64_gnullvm 0.52.5", "windows_x86_64_msvc 0.52.5", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] name = "windows_i686_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "xxhash-rust" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" sequoia-gpg-agent-0.4.2/Cargo.toml0000644000000050520000000000100123540ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70" name = "sequoia-gpg-agent" version = "0.4.2" authors = [ "Justus Winter ", "Neal H. Walfield ", ] build = "build.rs" description = "A library for interacting with GnuPG's gpg-agent" homepage = "https://sequoia-pgp.org/" documentation = "https://docs.rs/sequoia-gpg-agent" readme = "README.md" keywords = [ "cryptography", "openpgp", "pgp", "encryption", "signing", ] categories = [ "cryptography", "authentication", ] license = "LGPL-2.0-or-later" repository = "https://gitlab.com/sequoia-pgp/sequoia-gpg-agent" [package.metadata.docs.rs] features = ["sequoia-openpgp/default"] [profile.release] debug = true [dependencies.anyhow] version = "1" [dependencies.chrono] version = "0.4" [dependencies.futures] version = "0.3" [dependencies.lalrpop-util] version = ">=0.17, <0.21" [dependencies.libc] version = "0.2.152" [dependencies.sequoia-ipc] version = "0.35.0" [dependencies.sequoia-openpgp] version = "1.19" features = ["compression"] default-features = false [dependencies.stfu8] version = "0.2" [dependencies.tempfile] version = "3.1" [dependencies.thiserror] version = "1.0.38" [dependencies.tokio] version = "1.35.0" [dev-dependencies.clap] version = "4" features = [ "derive", "env", "string", "wrap_help", ] [dev-dependencies.lazy_static] version = "1.4.0" [dev-dependencies.tempfile] version = "3.1" [dev-dependencies.tokio] version = "1.35.0" features = ["macros"] [dev-dependencies.tokio-test] version = "0.4" [build-dependencies.lalrpop] version = ">=0.17, <0.21" default-features = false [target."cfg(not(windows))".dev-dependencies.sequoia-openpgp] version = "1" features = [ "crypto-nettle", "__implicit-crypto-backend-for-tests", ] default-features = false [target."cfg(windows)".dev-dependencies.sequoia-openpgp] version = "1" features = [ "crypto-cng", "__implicit-crypto-backend-for-tests", ] default-features = false [badges.gitlab] repository = "sequoia-pgp/sequoia-gpg-agent" [badges.maintenance] status = "actively-developed" sequoia-gpg-agent-0.4.2/Cargo.toml.orig000064400000000000000000000035531046102023000160410ustar 00000000000000[package] name = "sequoia-gpg-agent" description = "A library for interacting with GnuPG's gpg-agent" version = "0.4.2" authors = [ "Justus Winter ", "Neal H. Walfield ", ] edition = "2021" keywords = ["cryptography", "openpgp", "pgp", "encryption", "signing"] categories = ["cryptography", "authentication"] license = "LGPL-2.0-or-later" documentation = "https://docs.rs/sequoia-gpg-agent" homepage = "https://sequoia-pgp.org/" repository = "https://gitlab.com/sequoia-pgp/sequoia-gpg-agent" readme = "README.md" rust-version = "1.70" build = "build.rs" [badges] gitlab = { repository = "sequoia-pgp/sequoia-gpg-agent" } maintenance = { status = "actively-developed" } [dependencies] anyhow = "1" chrono = "0.4" futures = "0.3" lalrpop-util = ">=0.17, <0.21" libc = "0.2.152" sequoia-openpgp = { version = "1.19", default-features = false, features = ["compression"] } sequoia-ipc = "0.35.0" stfu8 = "0.2" tempfile = "3.1" thiserror = "1.0.38" tokio = "1.35.0" [dev-dependencies] clap = { version = "4", features = ["derive", "env", "string", "wrap_help"] } lazy_static = "1.4.0" tempfile = "3.1" tokio = { version = "1.35.0", features = ["macros"] } tokio-test = "0.4" [build-dependencies] lalrpop = { version = ">=0.17, <0.21", default-features = false } [profile.release] debug = true [target.'cfg(not(windows))'.dev-dependencies] # Enables a crypto backend for the tests: sequoia-openpgp = { version = "1", default-features = false, features = ["crypto-nettle", "__implicit-crypto-backend-for-tests"] } [target.'cfg(windows)'.dev-dependencies] # Enables a crypto backend for the tests: sequoia-openpgp = { version = "1", default-features = false, features = ["crypto-cng", "__implicit-crypto-backend-for-tests"] } [package.metadata.docs.rs] # Enables a crypto backend for the docs.rs generation: features = ["sequoia-openpgp/default"] sequoia-gpg-agent-0.4.2/LICENSE.txt000064400000000000000000000627341046102023000150030ustar 00000000000000Sequoia PGP is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Sequoia PGP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. --- GNU LIBRARY GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. GNU LIBRARY GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! sequoia-gpg-agent-0.4.2/README.md000064400000000000000000000011221046102023000144170ustar 00000000000000This crate includes functionality for interacting with GnuPG's `gpg-agent`. `gpg-agent` is a secret key store, which is shipped as part of GnuPG. It is used to manage secret key material, and hardware devices that contain secret key material. It provides an RPC interface using the [Assuan protocol]. [Assuan protocol]: https://gnupg.org/software/libassuan This is how `gpg`, GnuPG's primary command-line interface, communicates with it. This crate provides a Rust API for interacting with `gpg-agent`. Note: this crate communicates directly with `gpg-agent`; it does not go via `gpg`. sequoia-gpg-agent-0.4.2/build.rs000064400000000000000000000023551046102023000146160ustar 00000000000000use std::env; use std::fs; use std::io::{self, Write}; use std::path::PathBuf; fn main() { lalrpop::process_root().unwrap(); include_test_data().unwrap(); } /// Builds the index of the test data for use with the `::tests` /// module. fn include_test_data() -> io::Result<()> { let cwd = env::current_dir()?; let mut sink = fs::File::create( PathBuf::from(env::var_os("OUT_DIR").unwrap()) .join("tests.index.rs.inc")).unwrap(); writeln!(&mut sink, "{{")?; let mut dirs = vec![PathBuf::from("tests/data")]; while let Some(dir) = dirs.pop() { println!("rerun-if-changed={}", dir.to_str().unwrap()); for entry in fs::read_dir(dir).unwrap() { let entry = entry?; let path = entry.path(); if path.is_file() { writeln!( &mut sink, " add!({:?}, {:?});", path.components().skip(2) .map(|c| c.as_os_str().to_str().expect("valid UTF-8")) .collect::>().join("/"), cwd.join(path))?; } else if path.is_dir() { dirs.push(path.clone()); } } } writeln!(&mut sink, "}}")?; Ok(()) } sequoia-gpg-agent-0.4.2/deny.toml000064400000000000000000000013441046102023000150020ustar 00000000000000[advisories] ignore = [ "RUSTSEC-2020-0071", # chrono not affected by time 0.1 issue # fehler is unmaintained. # # fehler is used by subplot and thus an indirect dependency. Remove # when a new version subplot is released without fehler. See # https://gitlab.com/subplot/subplot/-/issues/340. "RUSTSEC-2023-0067", ] unmaintained = "deny" yanked = "deny" [bans] multiple-versions = "allow" deny = [ # does not have responsible disclosure policy: # https://github.com/briansmith/ring#bug-reporting {name = "ring"}, ] [licenses] allow = [ "Apache-2.0", "BSD-3-Clause", "BSL-1.0", "CC0-1.0", "GPL-2.0", "GPL-3.0", "ISC", "LGPL-2.0", "LGPL-3.0", "MIT", "MIT-0", "MPL-2.0", "Unicode-DFS-2016", ] sequoia-gpg-agent-0.4.2/examples/assuan-client.rs000064400000000000000000000026441046102023000201040ustar 00000000000000use std::path::PathBuf; use clap::CommandFactory; use clap::FromArgMatches; use clap::Parser; use futures::StreamExt; use sequoia_openpgp as openpgp; use openpgp::Result; use sequoia_gpg_agent as gpg_agent; use gpg_agent::assuan::Client; /// Defines the CLI. #[derive(Parser, Debug)] #[clap( name = "assuan-client", about = "Connects to and sends commands to assuan servers.", )] pub struct Cli { #[clap( long, value_name = "PATH", help = "Server to connect to", )] server: PathBuf, #[clap( long, value_name = "COMMAND", help = "Commands to send to the server", required = true, )] commands: Vec, } fn main() -> Result<()> { let version = format!( "{} (sequoia-openpgp {}, using {})", env!("CARGO_PKG_VERSION"), sequoia_openpgp::VERSION, sequoia_openpgp::crypto::backend() ); let cli = Cli::command().version(version); let matches = Cli::from_arg_matches(&cli.get_matches())?; let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let mut c = Client::connect(matches.server).await.unwrap(); for command in matches.commands { eprintln!("> {}", command); c.send(command).unwrap(); while let Some(response) = c.next().await { eprintln!("< {:?}", response); } } }); Ok(()) } sequoia-gpg-agent-0.4.2/examples/export-sexp.rs000064400000000000000000000024521046102023000176310ustar 00000000000000use std::io::Write; use std::path::PathBuf; use sequoia_openpgp as openpgp; use sequoia_gpg_agent::Agent; use sequoia_gpg_agent::gnupg; #[tokio::main] async fn main() -> openpgp::Result<()> { let args = std::env::args().collect::>(); let (homedir, keygrip) = match args.len() { 2 if std::env::var("GNUPGHOME").is_ok() => { (PathBuf::from(std::env::var("GNUPGHOME").unwrap()), &args[1][..]) } 3 => { (PathBuf::from(&args[1]), &args[2][..]) } _ => { panic!("Usage: export-sexp [GNUPGHOME] KEYGRIP"); } }; let ctx = gnupg::Context::with_homedir(homedir)?; let mut agent = Agent::connect(&ctx).await?; let kek = agent.send_simple("KEYWRAP_KEY --export").await?; let encrypted_key = agent.send_simple( format!("EXPORT_KEY {}", keygrip.to_string())).await?; let key = openpgp::crypto::ecdh::aes_key_unwrap( openpgp::types::SymmetricAlgorithm::AES128, &kek, &encrypted_key.as_ref())?; let mut key = key.as_ref(); // Strip any trailing NULs. They are only there for padding // purposes. while ! key.is_empty() && key[key.len() - 1] == 0 { key = &key[..key.len() - 1]; } std::io::stdout().write_all(key)?; Ok(()) } sequoia-gpg-agent-0.4.2/examples/gpg-agent-client.rs000064400000000000000000000032511046102023000204560ustar 00000000000000/// Connects to and sends commands to gpg-agent. use std::path::PathBuf; use clap::CommandFactory; use clap::FromArgMatches; use clap::Parser; use futures::StreamExt; use sequoia_openpgp as openpgp; use openpgp::Result; use sequoia_gpg_agent as gpg_agent; use gpg_agent::gnupg::{Context, Agent}; /// Defines the CLI. #[derive(Parser, Debug)] #[clap( name = "gpg-agent-client", about = "Connects to and sends commands to gpg-agent.", )] pub struct Cli { #[clap( long, value_name = "PATH", env = "GNUPGHOME", help = "Use this GnuPG home directory, default: $GNUPGHOME", )] homedir: Option, #[clap( long, value_name = "commands", help = "Commands to send to the server", required = true, )] commands: Vec, } fn main() -> Result<()> { let version = format!( "{} (sequoia-openpgp {}, using {})", env!("CARGO_PKG_VERSION"), sequoia_openpgp::VERSION, sequoia_openpgp::crypto::backend()); let cli = Cli::command().version(version); let matches = Cli::from_arg_matches(&cli.get_matches())?; let ctx = if let Some(homedir) = matches.homedir { Context::with_homedir(homedir)? } else { Context::new()? }; let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let mut agent = Agent::connect(&ctx).await.unwrap(); for command in matches.commands { eprintln!("> {}", command); agent.send(command).unwrap(); while let Some(response) = agent.next().await { eprintln!("< {:?}", response); } } }); Ok(()) } sequoia-gpg-agent-0.4.2/examples/gpg-agent-decrypt.rs000064400000000000000000000150351046102023000206550ustar 00000000000000/// Decrypts data using the openpgp crate and secrets in gpg-agent. use std::collections::HashMap; use std::io; use std::path::PathBuf; use clap::CommandFactory; use clap::FromArgMatches; use clap::Parser; use sequoia_openpgp as openpgp; use openpgp::cert::prelude::*; use openpgp::crypto::SessionKey; use openpgp::types::SymmetricAlgorithm; use openpgp::parse::{ Parse, stream::{ DecryptionHelper, DecryptorBuilder, VerificationHelper, GoodChecksum, VerificationError, MessageStructure, MessageLayer, }, }; use openpgp::policy::Policy; use openpgp::policy::StandardPolicy as P; use sequoia_gpg_agent as gpg_agent; use gpg_agent::gnupg::{Context, KeyPair}; /// Defines the CLI. #[derive(Parser, Debug)] #[clap( name = "gpg-agent-decrypt", about = "Connects to gpg-agent and decrypts a message.", )] pub struct Cli { #[clap( long, value_name = "PATH", env = "GNUPGHOME", help = "Use this GnuPG home directory, default: $GNUPGHOME", )] homedir: Option, #[clap( long, value_name = "CERT", help = "Public part of the secret keys managed by gpg-agent", required = true, )] cert: Vec, } fn main() -> openpgp::Result<()> { let p = &P::new(); let version = format!( "{} (sequoia-openpgp {}, using {})", env!("CARGO_PKG_VERSION"), sequoia_openpgp::VERSION, sequoia_openpgp::crypto::backend()); let cli = Cli::command().version(version); let matches = Cli::from_arg_matches(&cli.get_matches())?; let ctx = if let Some(homedir) = matches.homedir { Context::with_homedir(homedir)? } else { Context::new()? }; // Read the Certs from the given files. let certs = matches.cert.into_iter().map( openpgp::Cert::from_file ).collect::>()?; // Now, create a decryptor with a helper using the given Certs. let mut decryptor = DecryptorBuilder::from_reader(io::stdin())? .with_policy(p, None, Helper::new(&ctx, p, certs)?)?; // Finally, stream the decrypted data to stdout. io::copy(&mut decryptor, &mut io::stdout())?; Ok(()) } /// This helper provides secrets for the decryption, fetches public /// keys for the signature verification and implements the /// verification policy. struct Helper { keys: HashMap, } impl Helper { /// Creates a Helper for the given Certs with appropriate secrets. fn new(ctx: &Context, policy: &dyn Policy, certs: Vec) -> openpgp::Result { // Map (sub)KeyIDs to secrets. let mut keys = HashMap::new(); for cert in certs { for ka in cert.keys().with_policy(policy, None) .for_storage_encryption().for_transport_encryption() { let pair = KeyPair::new(ctx, ka.key())? .with_cert(ka.cert()); keys.insert(ka.key().keyid(), pair); } } Ok(Helper { keys }) } } impl DecryptionHelper for Helper { fn decrypt(&mut self, pkesks: &[openpgp::packet::PKESK], _skesks: &[openpgp::packet::SKESK], sym_algo: Option, mut decrypt: D) -> openpgp::Result> where D: FnMut(SymmetricAlgorithm, &SessionKey) -> bool { // Try each PKESK until we succeed. for pkesk in pkesks { if let Some(pair) = self.keys.get_mut(pkesk.recipient()) { if pkesk.decrypt(pair, sym_algo) .map(|(algo, session_key)| decrypt(algo, &session_key)) .unwrap_or(false) { break; } } } // XXX: In production code, return the Fingerprint of the // recipient's Cert here Ok(None) } } impl VerificationHelper for Helper { fn get_certs(&mut self, _ids: &[openpgp::KeyHandle]) -> openpgp::Result> { Ok(Vec::new()) // Feed the Certs to the verifier here. } fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> { use self::VerificationError::*; for layer in structure.iter() { match layer { MessageLayer::Compression { algo } => eprintln!("Compressed using {}", algo), MessageLayer::Encryption { sym_algo, aead_algo } => if let Some(aead_algo) = aead_algo { eprintln!("Encrypted and protected using {}/{}", sym_algo, aead_algo); } else { eprintln!("Encrypted using {}", sym_algo); }, MessageLayer::SignatureGroup { ref results } => for result in results { match result { Ok(GoodChecksum { ka, .. }) => { eprintln!("Good signature from {}", ka.cert()); }, Err(MalformedSignature { error, .. }) => { eprintln!("Signature is malformed: {}", error); }, Err(MissingKey { sig, .. }) => { let issuers = sig.get_issuers(); eprintln!("Missing key {:X}, which is needed to \ verify signature.", issuers.first().unwrap()); }, Err(UnboundKey { cert, error, .. }) => { eprintln!("Signing key on {:X} is not bound: {}", cert.fingerprint(), error); }, Err(BadKey { ka, error, .. }) => { eprintln!("Signing key on {:X} is bad: {}", ka.cert().fingerprint(), error); }, Err(BadSignature { error, .. }) => { eprintln!("Verifying signature: {}.", error); }, } } } } Ok(()) // Implement your verification policy here. } } sequoia-gpg-agent-0.4.2/examples/gpg-agent-sign.rs000064400000000000000000000056301046102023000201430ustar 00000000000000/// Signs data using the openpgp crate and secrets in gpg-agent. use std::io; use std::path::PathBuf; use clap::CommandFactory; use clap::FromArgMatches; use clap::Parser; use sequoia_openpgp as openpgp; use openpgp::parse::Parse; use openpgp::serialize::stream::{Armorer, Message, LiteralWriter, Signer}; use openpgp::policy::StandardPolicy as P; use sequoia_gpg_agent as gpg_agent; use gpg_agent::gnupg::{Context, KeyPair}; /// Defines the CLI. #[derive(Parser, Debug)] #[clap( name = "gpg-agent-sign", about = "Connects to gpg-agent and creates a dummy signature.", )] pub struct Cli { #[clap( long, value_name = "PATH", env = "GNUPGHOME", help = "Use this GnuPG home directory, default: $GNUPGHOME", )] homedir: Option, #[clap( long, value_name = "CERT", help = "Public part of the secret keys managed by gpg-agent", required = true, )] cert: Vec, } fn main() -> openpgp::Result<()> { let p = &P::new(); let version = format!( "{} (sequoia-openpgp {}, using {})", env!("CARGO_PKG_VERSION"), sequoia_openpgp::VERSION, sequoia_openpgp::crypto::backend()); let cli = Cli::command().version(version); let matches = Cli::from_arg_matches(&cli.get_matches())?; let ctx = if let Some(homedir) = matches.homedir { Context::with_homedir(homedir)? } else { Context::new()? }; // Read the Certs from the given files. let certs = matches.cert.into_iter().map( openpgp::Cert::from_file ).collect::, _>>()?; // Construct a KeyPair for every signing-capable (sub)key. use openpgp::cert::amalgamation::ValidAmalgamation; let mut signers = certs.iter().flat_map(|cert| { cert.keys().with_policy(p, None).alive().revoked(false).for_signing() .filter_map(|ka| { KeyPair::new(&ctx, ka.key()) .map(|kp| kp.with_cert(ka.cert())) .ok() }) }).collect::>(); // Compose a writer stack corresponding to the output format and // packet structure we want. // Stream an OpenPGP message. let message = Message::new(io::stdout()); // We want the output to be ASCII armored. let message = Armorer::new(message).build()?; // Now, create a signer that emits the signature(s). let mut signer = Signer::new(message, signers.pop().expect("No key for signing")); for s in signers { signer = signer.add_signer(s); } let signer = signer.build()?; // Then, create a literal writer to wrap the data in a literal // message packet. let mut literal = LiteralWriter::new(signer).build()?; // Copy all the data. io::copy(&mut io::stdin(), &mut literal)?; // Finally, teardown the stack to ensure all the data is written. literal.finalize()?; Ok(()) } sequoia-gpg-agent-0.4.2/openpgp-policy.toml000064400000000000000000002404061046102023000170140ustar 00000000000000version = 0 commit_goodlist = [] [authorization.dvn] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 0CC0 11A7 C3DC 6EB4 7567 9BC9 AC0A BA31 866A 7E76 Comment: Devan Carpenter Comment: dvn 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 /g== =nP6D -----END PGP PUBLIC KEY BLOCK----- """ [authorization.justus] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: D2F2 C5D4 5BE9 FDE6 A4EE 0AAF 3185 5247 6038 31FD Comment: Justus Winter (Code Signing Key) Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter 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 =uq6w -----END PGP PUBLIC KEY BLOCK----- """ [authorization.neal] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: F717 3B3C 7C68 5CD9 ECC4 191B 74E4 45BA 0E15 C957 Comment: Neal H. Walfield (Code Signing Key) Comment: Neal H. Walfield Comment: Neal H. Walfield Comment: Neal H. Walfield Comment: Neal H. Walfield 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 HCL+AFMw7YtSwqT2TxbLYPc/7bU+pWAe1YT6pk0OLZhuRzyG3UuZaMVFHcp26sLP rGROgdo4R79MUUDMLYkuYKRcTrdi4bceZlmIpJeUt+SY8zcAP3m4epeEOu3vrJER 1xglFmeNDEcjD32iTJHlkGcyHw+NWhl6l8LvFov4ZVyW4pXiYjnjbOHq8ukbPIDP eafMkishG18ekELGoGuLpDqnPYG9AYXiy03D6ZwduLI770bMkG5zhKzDksIGQ8x0 JUPvtroSlgNQwe1PFlDS7c2bIs3JqwCpuJClEEqKkvg7samKXWNCO23vl9ucmOwk xMm2w4v09WooFlG28hSsH+FL+ROEU2Px1MJbUahReTbsho828NstqZ46UWmcWe9q 9YDwnHBj2sKEy47f2mZ3ZoR1wchRIxze7dcPLUTPMoEyjAFKzsoozpE+Ah/hUwIj PSEK0kfPaQx4jR9QKCwTnMrHNs16nVUM9zhXLNlwb2rdG6Y0dAyBllLvE4UHAF8x 3Gv/6O3xC/PU6LrAUitH0mViV+GHHn3jWV7oO3NmV27x+Vw1DqZO9XZMPqmgRoRz MEQXzSROZWFsIEguIFdhbGZpZWxkIDxuZWFsQHdhbGZpZWxkLm9yZz7CwWgEEwEK AD4CGwMICwkIBw0MCwoFFQoJCAsCHgECF4ACGQEWIQSPF3dxGKM92pukjmKqyzJD YwBS2QUCZCarHgUJEORDtQAKCRCqyzJDYwBS2YLaDp0fMZrwfLGqC8LJiRfI+HwV +29E7EWaJmOF6S24sewr1t//2Vz4mc41E6bDprHsHte4JEmzyCnXnXlPaobyONWU IbYcnM8723myJml0Du4EvvvJPUieMiwFWzRdmFxc/eQ+6aeTTUknNJKVhY18/qsA pvYTIkvmlePgLQVGvpnYqQ1ElgMf768KJ4+lVMmIT2+fHlLe5RyJRlZcjVryV5H9 fOy+TQqhLLoTFhxAiJBACYIUCglGgjTQejivXXRXjhvMpvlBzSxwI9iZGZUL4FE1 kjK99epoictN9iv1O5XTZYTqkdBf1PD79GicHAbG6Io8F07xR/CijZl27l6rLb55 9/33Iswi6Ma8mdnmeTJ1d+io9XbtAGhPZqKxM2XtKbVO8uJli6YCqeEpVP+/Wy2c g4RSj42b5yrplbqVvnbddx8ZYCm5SXD6Pb/ZrrUmKlayuM2UnMs9+snIGG/SMRUv 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 =MESu -----END PGP PUBLIC KEY BLOCK----- """ sequoia-gpg-agent-0.4.2/src/assuan/grammar.lalrpop000064400000000000000000000127341046102023000202550ustar 00000000000000// -*- mode: Rust; -*- // // This implements parsing of [Assuan] messages. // // [Assuan]: https://www.gnupg.org/documentation/manuals/assuan/index.html use crate::assuan::{ Response, lexer::{self, LexicalError}, }; grammar; // 3.1 Server responses pub Response: Response = { Ok, Error, Status, Comment, Data, Inquire, }; // 'OK []' // Request was successful. Ok: Response = O K => Response::Ok { message }; // 'ERR ERRORCODE []' // Request could not be fulfilled. The possible error codes are // defined by 'libgpg-error'. Error: Response = E R R SPACE => Response::Error { code, message }; // 'S KEYWORD ' // Informational output by the server, which is still processing the // request. A client may not send such lines to the server while // processing an Inquiry command. KEYWORD shall start with a letter // or an underscore. Status: Response = S SPACE => Response::Status { keyword, message }; Keyword: String = { => String::from_utf8_lossy( &std::iter::once(&p).chain(q.iter()).map(|t| u8::from(*t)).collect::>() ).into_owned(), => String::from_utf8_lossy( &std::iter::once(&p).chain(q.iter()).map(|t| u8::from(*t)).collect::>() ).into_owned(), }; KeywordChar: lexer::Token = { UNDERSCORE, DASH, Digit, Letter, }; // '# ' // Comment line issued only for debugging purposes. Totally ignored. Comment: Response = HASH => Response::Comment { message }; // 'D ' // Raw data returned to client. There must be exactly one space after // the 'D'. The values for '%', CR and LF must be percent escaped; // these are encoded as %25, %0D and %0A, respectively. Only // uppercase letters should be used in the hexadecimal representation. // Other characters may be percent escaped for easier debugging. All // Data lines are considered one data stream up to the OK or ERR // response. Status and Inquiry Responses may be mixed with the Data // lines. Data: Response = D => Response::Data { partial }; // 'INQUIRE KEYWORD ' // The server needs further information from the client. The client // should respond with data (using the "D" command and terminated by // "END"). Alternatively, the client may cancel the current operation // by responding with "CAN". Inquire: Response = I N Q U I R E SPACE => Response::Inquire { keyword, parameters }; // A string without %-escaping. HumanReadable: String = SPACE => String::from_utf8_lossy( &<>.iter().map(|t| u8::from(*t)).collect::>() ).into_owned(); // A raw string with %-escaping. RawData: Vec = SPACE => <>.iter().map(|t| u8::from(*t)).collect(); ANY_ESCAPED: lexer::Token = { EscapedOctet, SPACE, UNDERSCORE, HASH, DASH, Digit, Letter, OTHER, }; EscapedOctet: lexer::Token = PERCENT => lexer::Token::OTHER( (msn.hex_value().unwrap() << 4) + lsn.hex_value().unwrap() ); Integer: usize = => <>.iter().fold(0, |acc, d| { acc * 10 + (u8::from(*d) - 0x30) as usize }); Digit: lexer::Token = { N0, N1, N2, N3, N4, N5, N6, N7, N8, N9, }; HexDigit: lexer::Token = { Digit, A, B, C, D, E, F, }; Letter: lexer::Token = { A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, }; ANY: lexer::Token = { SPACE, HASH, PERCENT, DASH, UNDERSCORE, Digit, Letter, OTHER, }; extern { type Location = usize; type Error = LexicalError; enum lexer::Token { SPACE => lexer::Token::SPACE, HASH => lexer::Token::HASH, PERCENT => lexer::Token::PERCENT, DASH => lexer::Token::DASH, N0 => lexer::Token::N0, N1 => lexer::Token::N1, N2 => lexer::Token::N2, N3 => lexer::Token::N3, N4 => lexer::Token::N4, N5 => lexer::Token::N5, N6 => lexer::Token::N6, N7 => lexer::Token::N7, N8 => lexer::Token::N8, N9 => lexer::Token::N9, A => lexer::Token::A, B => lexer::Token::B, C => lexer::Token::C, D => lexer::Token::D, E => lexer::Token::E, F => lexer::Token::F, G => lexer::Token::G, H => lexer::Token::H, I => lexer::Token::I, J => lexer::Token::J, K => lexer::Token::K, L => lexer::Token::L, M => lexer::Token::M, N => lexer::Token::N, O => lexer::Token::O, P => lexer::Token::P, Q => lexer::Token::Q, R => lexer::Token::R, S => lexer::Token::S, T => lexer::Token::T, U => lexer::Token::U, V => lexer::Token::V, W => lexer::Token::W, X => lexer::Token::X, Y => lexer::Token::Y, Z => lexer::Token::Z, UNDERSCORE => lexer::Token::UNDERSCORE, OTHER => lexer::Token::OTHER(_), } } sequoia-gpg-agent-0.4.2/src/assuan/lexer.rs000064400000000000000000000116441046102023000167200ustar 00000000000000use std::sync::atomic::AtomicBool; use std::fmt; // Controls tracing in the lexer. const TRACE: AtomicBool = AtomicBool::new(false); #[derive(Clone, PartialEq, Eq, Debug)] pub enum LexicalError { } impl fmt::Display for LexicalError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } } pub type Spanned = Result<(Loc, Token, Loc), LexicalError>; // The type of the parser's input. // // The parser iterators over tuples consisting of the token's starting // position, the token itself, and the token's ending position. pub(crate) type LexerItem = Spanned; #[derive(Debug, Clone, Copy)] #[allow(clippy::upper_case_acronyms)] pub enum Token { SPACE, HASH, PERCENT, DASH, N0, N1, N2, N3, N4, N5, N6, N7, N8, N9, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, UNDERSCORE, // XXX a-f OTHER(u8), } impl Token { pub fn digit_value(&self) -> Option { use self::Token::*; match self { N0 | N1 | N2 | N3 | N4 | N5 | N6 | N7 | N8 | N9 => Some(u8::from(*self) - 0x30), _ => None, } } pub fn hex_value(&self) -> Option { use self::Token::*; match self { N0 | N1 | N2 | N3 | N4 | N5 | N6 | N7 | N8 | N9 => self.digit_value(), A | B | C | D | E | F => Some(10 + u8::from(*self) - 0x41), _ => None, } } } impl From for u8 { fn from(t: Token) -> Self { use self::Token::*; match t { SPACE => 0x20, HASH => 0x23, PERCENT => 0x25, DASH => 0x2d, N0 => 0x30, N1 => 0x31, N2 => 0x32, N3 => 0x33, N4 => 0x34, N5 => 0x35, N6 => 0x36, N7 => 0x37, N8 => 0x38, N9 => 0x39, A => 0x41, B => 0x42, C => 0x43, D => 0x44, E => 0x45, F => 0x46, G => 0x47, H => 0x48, I => 0x49, J => 0x4a, K => 0x4b, L => 0x4c, M => 0x4d, N => 0x4e, O => 0x4f, P => 0x50, Q => 0x51, R => 0x52, S => 0x53, T => 0x54, U => 0x55, V => 0x56, W => 0x57, X => 0x58, Y => 0x59, Z => 0x5a, UNDERSCORE => 0x5f, OTHER(x) => x, } } } impl fmt::Display for Token { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "T({:x})", u8::from(*self)) } } impl From for String { fn from(t: Token) -> Self { t.to_string() } } #[derive(Debug)] pub(crate) struct Lexer<'input> { offset: usize, input: &'input [u8], } impl<'input> Lexer<'input> { pub fn new(input: &'input [u8]) -> Self { Lexer { offset: 0, input } } } impl<'input> Iterator for Lexer<'input> { type Item = LexerItem; fn next(&mut self) -> Option { tracer!(TRACE, "Lexer::next", 0); t!("input is {:?}", String::from_utf8_lossy(self.input)); use self::Token::*; let token = match *self.input.get(0)? { 0x20 => SPACE, 0x23 => HASH, 0x25 => PERCENT, 0x2d => DASH, 0x30 => N0, 0x31 => N1, 0x32 => N2, 0x33 => N3, 0x34 => N4, 0x35 => N5, 0x36 => N6, 0x37 => N7, 0x38 => N8, 0x39 => N9, 0x41 => A, 0x42 => B, 0x43 => C, 0x44 => D, 0x45 => E, 0x46 => F, 0x47 => G, 0x48 => H, 0x49 => I, 0x4a => J, 0x4b => K, 0x4c => L, 0x4d => M, 0x4e => N, 0x4f => O, 0x50 => P, 0x51 => Q, 0x52 => R, 0x53 => S, 0x54 => T, 0x55 => U, 0x56 => V, 0x57 => W, 0x58 => X, 0x59 => Y, 0x5a => Z, 0x5f => UNDERSCORE, n => OTHER(n), }; self.input = &self.input[1..]; let start = self.offset; self.offset += 1; let end = self.offset; t!("Returning token at offset {}: '{:?}'", start, token); Some(Ok((start, token, end))) } } impl<'input> From<&'input [u8]> for Lexer<'input> { fn from(i: &'input [u8]) -> Lexer<'input> { Lexer::new(i) } } sequoia-gpg-agent-0.4.2/src/assuan/socket/windows.rs000064400000000000000000000147621046102023000205670ustar 00000000000000//! Unix Domain Socket emulation for Windows. use std::io::Read; use std::path::Path; use std::fs::File; use anyhow::anyhow; use anyhow::Context; type Result = std::result::Result; /// Socket connection data. #[derive(Debug)] pub struct Rendezvous { pub port: u16, pub uds_emulation: UdsEmulation, pub nonce: [u8; 16], } /// Unix domain socket emulation type (Windows only). /// /// Until Windows 10 Update 1803, Windows did not support native UNIX domain /// sockets. To work around that, developers historically used TCP (readily /// available) connection coupled with an authentication nonce (or "cookie"). #[derive(Debug)] pub enum UdsEmulation { /// Cygwin socket emulation. /// /// File format: `!%u %c %08x-%08x-%08x-%08x` (scanf style) /// %u: local TCP port /// %c: socket type ("s" for `SOCK_STREAM`, "d" for `SOCK_DGRAM`) /// %08x-%08x-%08x-%08x: authentication nonce /// /// Starting with client, both sides first exchange the 16-byte authentication /// nonce, after which they exchange `ucred` structure (socket.h). Cygwin, /// Libassuan's custom socket emulation. /// /// File format: `\n` /// PORT: textual local TCP port (e.g. "12345") /// NONCE: raw 16-byte authentication nonce /// /// After connecting, client has to authenticate itself by sending the /// 16-byte authentication nonce. Libassuan, } /// Reads socket connection info from a Windows file emulating a Unix socket. /// /// Inspired by `read_port_and nonce` from assuan-socket.c. pub fn read_port_and_nonce(fname: &Path) -> Result { let mut file = File::open(fname).with_context(|| { format!("Opening gpg-agent socket {}", fname.display()) })?; // Socket connection info will be in either a <= 54 byte long Cygwin format // or ~5+1+16 (modulo whitespace separators) custom libassuan format let mut contents = Vec::with_capacity(64); file.read_to_end(&mut contents)?; read_port_and_nonce_from_string(&contents) } fn read_port_and_nonce_from_string(contents: &[u8]) -> Result { let maybe_utf8 = std::str::from_utf8(contents).ok(); match maybe_utf8.and_then(|buf| buf.strip_prefix("!")) { // libassuan's Cygwin compatible socket emulation. // Format: "!%u %c %08x-%08x-%08x-%08x\x00" (scanf-like) Some(buf) => { let opt_skip_nul = buf.strip_suffix('\x00').unwrap_or(buf); // Split into parts: port, kind of socket and nonce let mut iter = opt_skip_nul.split_terminator(' '); match (iter.next(), iter.next(), iter.next()) { (Some(port), Some("s"), Some(nonce)) => { let port = port.parse()?; // This is wasteful but an allocation-free alternative is // even more verbose and also does not warrant pulling a // hex string parser dependency. let nonce_chunks = nonce.split_terminator('-') .map(|dword| u32::from_str_radix(dword, 16).map_err(Into::into)) .collect::>>(); let nonce = match nonce_chunks.ok().as_deref() { Some(&[d0, d1, d2, d3, ..]) => { let mut nonce = [0u8; 16]; nonce[0..4].copy_from_slice(&d0.to_ne_bytes()); nonce[4..8].copy_from_slice(&d1.to_ne_bytes()); nonce[8..12].copy_from_slice(&d2.to_ne_bytes()); nonce[12..16].copy_from_slice(&d3.to_ne_bytes()); nonce }, _ => return Err(anyhow!("Couldn't parse Cygwin socket nonce: {}", nonce)), }; Ok(Rendezvous { port, nonce, uds_emulation: UdsEmulation::Cygwin }) }, _ => Err(anyhow!("Couldn't parse Cygwin socket: {}", buf)), } }, // libassuan's own socket emulation // Format: [?, port, .., '\n', <16 byte nonce>] None => { let pos = match contents.iter().position(|&x| x == b'\n') { // Also ensure that there are exactly 16 bytes following Some(pos) if pos + 1 + 16 == contents.len() => pos, _ => return Err(anyhow!("Malformed socket description: {:?}", contents)), }; let port = std::str::from_utf8(&contents[..pos])?.trim().parse()?; let mut nonce = [0u8; 16]; nonce[..].copy_from_slice(&contents[pos + 1..]); Ok(Rendezvous { port, nonce, uds_emulation: UdsEmulation::Libassuan }) } } } #[cfg(test)] mod tests { use super::*; #[test] fn read_port_and_nonce() -> Result<()> { let test_fn = super::read_port_and_nonce_from_string; assert!(test_fn(b"\t 12 \n1234567890123456").is_ok()); assert!(test_fn(b"\t 12 \n123456789012345").is_err()); assert!(test_fn(b"\t 12 \n12345678901234567").is_err()); assert!(matches!( test_fn(b" 12345\n\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"), Ok(Rendezvous { port: 12345, uds_emulation: UdsEmulation::Libassuan, nonce: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], }) )); assert!(matches!( test_fn(b" -152\n\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"), Err(..) )); assert!(matches!( test_fn(b"!12345 s AABBCCDD-DDCCBBAA-01234567-890ABCDE\x00"), Ok(Rendezvous { port: 12345, uds_emulation: UdsEmulation::Cygwin, nonce: [ 0xDD, 0xCC, 0xBB, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0x67, 0x45, 0x23, 0x01, 0xDE, 0xBC, 0x0A, 0x89, ] }) )); assert!(matches!( test_fn(b"!12345 s AABBCCDD-DDCCBBAA-01234567-890ABCDE"), Ok(Rendezvous { port: 12345, uds_emulation: UdsEmulation::Cygwin, nonce: [ 0xDD, 0xCC, 0xBB, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0x67, 0x45, 0x23, 0x01, 0xDE, 0xBC, 0x0A, 0x89, ] }) )); Ok(()) } } sequoia-gpg-agent-0.4.2/src/assuan/socket.rs000064400000000000000000000052121046102023000170630ustar 00000000000000//! Select functionality from [assuan-socket.c]. //! //! [assuan-socket.c]: https://github.com/gpg/libassuan/blob/master/src/assuan-socket.c use std::path::Path; use crate::Result; #[cfg(windows)] mod windows; #[cfg(windows)] pub(crate) type IpcStream = tokio::net::TcpStream; #[cfg(unix)] pub(crate) type IpcStream = tokio::net::UnixStream; /// Connects to a local socket, returning a Tokio-enabled async connection. /// /// Supports regular local domain sockets under Unix-like systems and /// either Cygwin or libassuan's socket emulation on Windows. /// /// # Panic /// /// This function panics if not called from within a Tokio runtime. pub(crate) fn sock_connect(path: impl AsRef) -> Result { platform! { unix => { let stream = std::os::unix::net::UnixStream::connect(path)?; stream.set_nonblocking(true)?; Ok(tokio::net::UnixStream::from_std(stream)?) }, windows => { use std::io::{Write, Read}; use std::net::{Ipv4Addr, TcpStream}; use windows::{ read_port_and_nonce, Rendezvous, UdsEmulation, }; let rendezvous = read_port_and_nonce(path.as_ref())?; let Rendezvous { port, uds_emulation, nonce } = rendezvous; let mut stream = TcpStream::connect((Ipv4Addr::LOCALHOST, port))?; stream.set_nodelay(true)?; // Authorize ourselves with nonce read from the file stream.write(&nonce)?; if let UdsEmulation::Cygwin = uds_emulation { // The client sends the nonce back - not useful. Do a dummy read stream.read_exact(&mut [0u8; 16])?; // Send our credentials as expected by libassuan: // [ pid |uid|gid] (8 bytes) // [_|_|_|_|_|_|_|_] let mut creds = [0u8; 8]; // uid = gid = 0 creds[..4].copy_from_slice(&std::process::id().to_ne_bytes()); stream.write_all(&creds)?; // FIXME: libassuan in theory reads only 8 bytes here, but // somehow 12 have to be written for the server to progress (tested // on mingw-x86_64-gnupg). // My bet is that mingw socket API does that transparently instead // and expects to read an actual `ucred` struct (socket.h) which is // three `__u32`s. // Here, neither client nor server uses it, so just send dummy bytes stream.write_all(&[0u8; 4])?; // Receive back credentials. We don't need them. stream.read_exact(&mut [0u8; 12])?; } stream.set_nonblocking(true)?; Ok(tokio::net::TcpStream::from_std(stream)?) }, } } sequoia-gpg-agent-0.4.2/src/assuan.rs000064400000000000000000000521261046102023000156010ustar 00000000000000//! Assuan RPC support. #![warn(missing_docs)] use std::cmp; use std::io::Write; use std::mem; use std::path::Path; use std::pin::Pin; use std::task::{Poll, Context}; use lalrpop_util::ParseError; use futures::{Future, Stream, StreamExt}; use tokio::io::{BufReader, ReadHalf, WriteHalf}; use tokio::io::{AsyncRead, AsyncWriteExt}; use crate::openpgp; use openpgp::crypto::mem::Protected; use crate::Result; mod lexer; mod socket; use socket::IpcStream; // Maximum line length of the reference implementation. const MAX_LINE_LENGTH: usize = 1000; // Load the generated code. lalrpop_util::lalrpop_mod!( #[allow(clippy::all)] #[allow(missing_docs, unused_parens)] grammar, "/assuan/grammar.rs" ); #[derive(thiserror::Error, Debug)] /// Errors returned from the Assuan routines. pub enum Error { /// Handshake failed. #[error("Handshake failed: {0}")] HandshakeFailed(String), /// The caller violated the protocol. #[error("Invalid operation: {0}")] InvalidOperation(String), /// The remote party violated the protocol. #[error("Protocol violation: {0}")] ProtocolError(String), /// The remote operation failed. #[error("Operation failed: {0}")] OperationFailed(String), } /// A connection to an Assuan server. /// /// Commands may be issued using [`Connection::send`]. Note that the /// command is sent lazily, i.e. it is only sent if you poll for the /// responses. /// /// [`Connection::send`]: Client::send() /// /// `Client` implements [`Stream`] to return all server responses /// until the first [`Response::Ok`], [`Response::Error`], or /// [`Response::Inquire`]. /// /// [`Stream`]: #impl-Stream /// /// [`Response::Ok`] and [`Response::Error`] indicate success and /// failure. [`Response::Inquire`] means that the server requires /// more information to complete the request. This information may be /// provided using [`Connection::data()`], or the operation may be /// canceled using [`Connection::cancel()`]. /// /// [`Connection::data()`]: Client::data() /// [`Connection::cancel()`]: Client::cancel() pub struct Client { r: BufReader>, // xxx: abstract over buffer: Vec, done: bool, w: WriteState, trace_send: Option>, trace_receive: Option>, } assert_send_and_sync!(Client); enum WriteState { Ready(WriteHalf), Sending(Pin>> + Send + Sync>>), Transitioning, Dead, } assert_send_and_sync!(WriteState); impl std::fmt::Debug for WriteState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { use WriteState::*; match self { Ready(_) => write!(f, "WriteState::Ready"), Sending(_) => write!(f, "WriteState::Sending"), Transitioning => write!(f, "WriteState::Transitioning"), Dead => write!(f, "WriteState::Dead"), } } } /// Percent-escapes the given string. pub fn escape>(s: S) -> String { let mut r = String::with_capacity(s.as_ref().len()); for c in s.as_ref().chars() { match c { '%' => r.push_str("%25"), ' ' => r.push('+'), n if n.is_ascii() && (n as u8) < 32 => r.push_str(&format!("%{:02X}", n as u8)), _ => r.push(c), } } r } impl Client { /// Connects to the server. pub async fn connect

(path: P) -> Result where P: AsRef { let connection = socket::sock_connect(path)?; Ok(ConnectionFuture::new(connection).await?) } /// Lazily sends a command to the server. /// /// For the command to be actually executed, stream the responses /// using this objects [`Stream`] implementation. /// /// Note: It is very important to poll the client object until it /// returns `None`. Otherwise, the server and client will lose /// synchronization, and requests and responses will no longer be /// correctly associated. /// /// [`Stream`]: #impl-Stream /// /// The response stream ends in either a [`Response::Ok`], /// [`Response::Error`], or [`Response::Inquire`]. `Ok` and /// `Error` indicate success and failure of the current operation. /// `Inquire` means that the server requires more information to /// complete the request. This information may be provided using /// [`Connection::data()`], or the operation may be canceled using /// [`Connection::cancel()`]. /// /// [`Response::Ok`]: super::assuan::Response::Ok /// [`Response::Error`]: super::assuan::Response::Error /// [`Response::Inquire`]: super::assuan::Response::Inquire /// [`Connection::data()`]: Client::data() /// [`Connection::cancel()`]: Client::cancel() /// /// Note: `command` is passed as-is. Control characters, like /// `%`, must be %-escaped using [`escape`]. pub fn send<'a, C: 'a>(&'a mut self, command: C) -> Result<()> where C: AsRef<[u8]> { if let WriteState::Sending(_) = self.w { return Err(Error::InvalidOperation( "Busy, poll responses first".into()).into()); } self.w = match mem::replace(&mut self.w, WriteState::Transitioning) { WriteState::Ready(mut sink) => { let command = command.as_ref(); let mut c = command.to_vec(); if ! c.ends_with(b"\n") { c.push(0x0a); } if let Some(t) = self.trace_send.as_ref() { t(&c); } WriteState::Sending(Box::pin(async move { sink.write_all(&c).await?; Ok(sink) })) }, WriteState::Dead => { // We're still dead. self.w = WriteState::Dead; return Err(Error::OperationFailed( "Connection dropped".into()).into()); } s => panic!("Client state machine desynchronized with servers: \ in {:?}, should be in WriteState::Ready", s), }; Ok(()) } /// Sends a simple command to the server and returns the response. /// /// This method can only be used with simple commands, i.e. those /// which do not require handling inquiries from the server. To /// send complex commands, use [`Client::send`] and handle the /// inquiries. pub async fn send_simple(&mut self, cmd: C) -> Result where C: AsRef, { self.send(cmd.as_ref())?; let mut data = Vec::new(); while let Some(response) = self.next().await { match response? { Response::Data { partial } => { // Securely erase partial. let partial = Protected::from(partial); data.extend_from_slice(&partial); }, Response::Ok { .. } | Response::Comment { .. } | Response::Status { .. } => (), // Ignore. Response::Error { ref message, .. } => return operation_failed(self, message).await, response => return protocol_error(&response), } } Ok(data.into()) } /// Lazily cancels a pending operation. /// /// For the command to be actually executed, stream the responses /// using this objects [`Stream`] implementation. /// /// [`Stream`]: #impl-Stream pub fn cancel(&mut self) -> Result<()> { self.send("CAN") } /// Lazily sends data in response to an inquire. /// /// For the command to be actually executed, stream the responses /// using this objects [`Stream`] implementation. /// /// [`Stream`]: #impl-Stream /// /// The response stream ends in either a [`Response::Ok`], /// [`Response::Error`], or another [`Response::Inquire`]. `Ok` /// and `Error` indicate success and failure of the original /// operation that lead to the current inquiry. /// /// [`Response::Ok`]: super::assuan::Response::Ok /// [`Response::Error`]: super::assuan::Response::Error /// [`Response::Inquire`]: super::assuan::Response::Inquire pub fn data<'a, C: 'a>(&'a mut self, data: C) -> Result<()> where C: AsRef<[u8]> { let mut data = data.as_ref(); let mut request = Vec::with_capacity(data.len()); while ! data.is_empty() { if !request.is_empty() { request.push(0x0a); } write!(&mut request, "D ").unwrap(); let mut line_len = 2; while ! data.is_empty() && line_len < MAX_LINE_LENGTH - 3 { let c = data[0]; data = &data[1..]; match c as char { '%' | '\n' | '\r' => { line_len += 3; write!(&mut request, "%{:02X}", c).unwrap(); }, _ => { line_len += 1; request.push(c); }, } } } write!(&mut request, "\nEND").unwrap(); self.send(request) } /// Start tracing the data that is sent to the server. /// /// Note: if a tracing function is already registered, this /// replaces it. pub fn trace_data_sent(&mut self, fun: Box) { self.trace_send = Some(fun); } /// Start tracing the data that is received from the server. /// /// Note: if a tracing function is already registered, this /// replaces it. pub fn trace_data_received(&mut self, fun: Box) { self.trace_receive = Some(fun); } } /// Returns a convenient Err value for use in the state machines. /// /// This function must only be called after the assuan server returns /// an ERR. message is the error message returned from the server. /// This function first checks that the server hasn't sent anything /// else, which would be a protocol violation. If that is not the /// case, it turns the message into an Err. pub(crate) async fn operation_failed(agent: &mut Client, message: &Option) -> Result { if let Some(response) = agent.next().await { protocol_error(&response?) } else { Err(Error::OperationFailed( message.as_ref().map(|e| e.to_string()) .unwrap_or_else(|| "Unknown reason".into())) .into()) } } /// Returns a convenient Err value for use in the state machines. pub(crate) fn protocol_error(response: &Response) -> Result { Err(Error::ProtocolError( format!("Got unexpected response {:?}", response)) .into()) } /// A future that will resolve to a `Client`. struct ConnectionFuture(Option); impl ConnectionFuture { fn new(c: IpcStream) -> Self { let (r, w) = tokio::io::split(c); let buffer = Vec::with_capacity(MAX_LINE_LENGTH); Self(Some(Client { r: BufReader::new(r), buffer, done: false, w: WriteState::Ready(w), trace_send: None, trace_receive: None, })) } } impl Future for ConnectionFuture { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { // Consume the initial message from the server. let client: &mut Client = self.0.as_mut().expect("future polled after completion"); let mut responses = client.by_ref().collect::>(); match Pin::new(&mut responses).poll(cx) { Poll::Ready(response) => { Poll::Ready(match response.iter().last() { Some(Ok(Response::Ok { .. })) => Ok(self.0.take().unwrap()), Some(Ok(Response::Error { code, message })) => Err(Error::HandshakeFailed( format!("Error {}: {:?}", code, message)).into()), l @ Some(_) => Err(Error::HandshakeFailed( format!("Unexpected server response: {:?}", l) ).into()), None => // XXX does that happen? Err(Error::HandshakeFailed( "No data received from server".into()).into()), }) }, Poll::Pending => Poll::Pending, } } } impl Stream for Client { type Item = Result; /// Attempt to pull out the next value of this stream, returning /// None if the stream is finished. /// /// Note: It _is_ safe to call this again after the stream /// finished, i.e. returned `Ready(None)`. fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // First, handle sending of the command. match self.w { WriteState::Ready(_) => (), // Nothing to do, poll for responses below. WriteState::Sending(_) => { self.w = if let WriteState::Sending(mut f) = mem::replace(&mut self.w, WriteState::Transitioning) { match f.as_mut().poll(cx) { Poll::Ready(Ok(sink)) => WriteState::Ready(sink), Poll::Pending => WriteState::Sending(f), Poll::Ready(Err(e)) => { self.w = WriteState::Dead; return Poll::Ready(Some(Err(e))); }, } } else { unreachable!() }; }, WriteState::Transitioning => unreachable!(), WriteState::Dead => (), // Nothing left to do, poll for responses below. } // Recheck if we are still sending the command. if let WriteState::Sending(_) = self.w { return Poll::Pending; } // Check if the previous response was one of ok, error, or // inquire. if self.done { // If so, we signal end of stream here. self.done = false; return Poll::Ready(None); } // The compiler is not smart enough to figure out disjoint borrows // through Pin via DerefMut (which wholly borrows `self`), so unwrap it let Self { buffer, done, r, trace_receive, .. } = Pin::into_inner(self); let mut reader = Pin::new(r); loop { // Try to yield a line from the buffer. For that, try to // find linebreaks. if let Some(p) = buffer.iter().position(|&b| b == 0x0a) { let line: Vec = buffer.drain(..p+1).collect(); // xxx: rtrim linebreak even more? crlf maybe? if let Some(t) = trace_receive { t(&line[..line.len()-1]); } let r = Response::parse(&line[..line.len()-1])?; // If this response is one of ok, error, or inquire, // we want to surrender control to the client next // time she asks for an item. *done = r.is_done(); return Poll::Ready(Some(Ok(r))); } // No more linebreaks in the buffer. We need to get more. // First, get a new read buffer. // Later, append the read data to the Client's buffer let mut vec = vec![0u8; MAX_LINE_LENGTH]; let mut read_buf = tokio::io::ReadBuf::new(&mut vec); match reader.as_mut().poll_read(cx, &mut read_buf)? { Poll::Ready(()) => { if read_buf.filled().is_empty() { // End of stream. return Poll::Ready(None) } else { buffer.extend_from_slice(read_buf.filled()); continue; } }, Poll::Pending => { return Poll::Pending; }, } } } } /// Server response. #[derive(Debug, PartialEq)] pub enum Response { /// Operation successful. Ok { /// Optional human-readable message. message: Option, }, /// An error occurred. Error { /// Error code. /// /// This code is defined in `libgpg-error`. code: usize, /// Optional human-readable message. message: Option, }, /// Information about the ongoing operation. Status { /// Indicates what the status message is about. keyword: String, /// Human-readable message. message: String, }, /// A comment for debugging purposes. Comment { /// Human-readable message. message: String, }, /// Raw data returned to the client. Data { /// A chunk of raw data. /// /// Consecutive `Data` responses must be joined. partial: Vec, }, /// Request for information from the client. Inquire { /// The subject of the inquiry. keyword: String, /// Optional parameters. parameters: Option>, }, } impl Response { /// Parses the given response. pub fn parse(b: &[u8]) -> Result { match self::grammar::ResponseParser::new().parse(lexer::Lexer::new(b)) { Ok(r) => Ok(r), Err(err) => { let mut msg = Vec::new(); writeln!(&mut msg, "Parsing: {:?}: {:?}", b, err)?; if let ParseError::UnrecognizedToken { token: (start, _, end), .. } = err { writeln!(&mut msg, "Context:")?; let chars = b.iter().enumerate() .filter_map(|(i, c)| { if cmp::max(8, start) - 8 <= i && i <= end + 8 { Some((i, c)) } else { None } }); for (i, c) in chars { writeln!(&mut msg, "{} {} {}: {:?}", if i == start { "*" } else { " " }, i, *c as char, c)?; } } Err(anyhow::anyhow!( String::from_utf8_lossy(&msg).to_string()).into()) }, } } /// Returns true if this message indicates success. pub fn is_ok(&self) -> bool { matches!(self, Response::Ok { .. } ) } /// Returns true if this message indicates an error. pub fn is_err(&self) -> bool { matches!(self, Response::Error { .. }) } /// Returns true if this message is an inquiry. pub fn is_inquire(&self) -> bool { matches!(self, Response::Inquire { .. }) } /// Returns true if this response concludes the server's response. pub fn is_done(&self) -> bool { // All server responses end in either OK or ERR. self.is_ok() || self.is_err() // However, the server may inquire more // information. We also surrender control to the // caller by yielding the responses we have seen // so far, and allow her to respond to the // inquiry. || self.is_inquire() } } #[cfg(test)] mod tests { use super::*; #[test] fn basics() { assert_eq!( Response::parse(b"OK Pleased to meet you, process 7745") .unwrap(), Response::Ok { message: Some("Pleased to meet you, process 7745".into()), }); assert_eq!( Response::parse(b"ERR 67109139 Unknown IPC command ") .unwrap(), Response::Error { code: 67109139, message :Some("Unknown IPC command ".into()), }); let status = b"S KEYINFO 151BCDB0C293927B7E36660BE47F28DA8729BD19 D - - - C - - -"; assert_eq!( Response::parse(status).unwrap(), Response::Status { keyword: "KEYINFO".into(), message: "151BCDB0C293927B7E36660BE47F28DA8729BD19 D - - - C - - -" .into(), }); assert_eq!( Response::parse(b"D (7:sig-val(3:rsa(1:s1:%25%0D)))") .unwrap(), Response::Data { partial: b"(7:sig-val(3:rsa(1:s1:%\x0d)))".to_vec(), }); assert_eq!( Response::parse(b"INQUIRE CIPHERTEXT") .unwrap(), Response::Inquire { keyword: "CIPHERTEXT".into(), parameters: None, }); } } sequoia-gpg-agent-0.4.2/src/babel.rs000064400000000000000000000162351046102023000153550ustar 00000000000000//! Translates GnuPG-speak from and to Sequoia-speak. use std::{ fmt, str::FromStr, }; use sequoia_openpgp as openpgp; use openpgp::{ types::*, }; /// Until Sequoia 2.0, we have to match on the OID to recognize this /// curve. pub const BRAINPOOL_P384_OID: &[u8] = &[0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0B]; /// Translates values to and from human-readable forms. pub struct Fish(pub T); impl fmt::Display for Fish { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use PublicKeyAlgorithm::*; #[allow(deprecated)] match self.0 { RSAEncryptSign => f.write_str("RSA"), RSAEncrypt => f.write_str("RSA"), RSASign => f.write_str("RSA"), ElGamalEncrypt => f.write_str("ELG"), DSA => f.write_str("DSA"), ECDSA => f.write_str("ECDSA"), ElGamalEncryptSign => f.write_str("ELG"), ECDH => f.write_str("ECDH"), EdDSA => f.write_str("EDDSA"), Private(u) => write!(f, "Private({})", u), Unknown(u) => write!(f, "Unknown({})", u), catchall => write!(f, "{:?}", catchall), } } } impl FromStr for Fish { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "rsa" | "openpgp-rsa" | "oid.1.2.840.113549.1.1.1" => Ok(Fish(PublicKeyAlgorithm::RSAEncryptSign)), _ => { if let Ok(o) = u8::from_str(s) { Ok(Fish(o.into())) } else { Err(openpgp::Error::InvalidArgument( format!("Unknown public key algorithm: {}", s)).into()) } }, } } } impl fmt::Display for Fish<&Curve> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Curve::*; match self.0 { NistP256 => f.write_str("nistp256"), NistP384 => f.write_str("nistp384"), NistP521 => f.write_str("nistp521"), BrainpoolP256 => f.write_str("brainpoolP256r1"), Unknown(oid) if &oid[..] == BRAINPOOL_P384_OID => f.write_str("brainpoolP384r1"), BrainpoolP512 => f.write_str("brainpoolP512r1"), Ed25519 => f.write_str("ed25519"), Cv25519 => f.write_str("cv25519"), Unknown(ref oid) => write!(f, "Unknown curve {:?}", oid), } } } impl FromStr for Fish { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "ed25519" => Ok(Fish(Curve::Ed25519)), "cv25519" | "curve25519" => Ok(Fish(Curve::Cv25519)), "nistp256" => Ok(Fish(Curve::NistP256)), "nistp384" => Ok(Fish(Curve::NistP384)), "nistp521" => Ok(Fish(Curve::NistP521)), "brainpoolp256" => Ok(Fish(Curve::BrainpoolP256)), "brainpoolp384" => Ok(Fish(Curve::Unknown(BRAINPOOL_P384_OID.into()))), "brainpoolp512" => Ok(Fish(Curve::BrainpoolP512)), _ => Err(openpgp::Error::InvalidArgument( format!("Unknown curve: {}", s)).into()), } } } impl fmt::Display for Fish { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use SymmetricAlgorithm::*; #[allow(deprecated)] match self.0 { Unencrypted => f.write_str("Unencrypted"), IDEA => f.write_str("IDEA"), TripleDES => f.write_str("3DES"), CAST5 => f.write_str("CAST5"), Blowfish => f.write_str("BLOWFISH"), AES128 => f.write_str("AES"), AES192 => f.write_str("AES192"), AES256 => f.write_str("AES256"), Twofish => f.write_str("TWOFISH"), Camellia128 => f.write_str("CAMELLIA128"), Camellia192 => f.write_str("CAMELLIA192"), Camellia256 => f.write_str("CAMELLIA256"), Private(u) => write!(f, "Private({})", u), Unknown(u) => write!(f, "Unknown({})", u), catchall => write!(f, "{:?}", catchall), } } } impl FromStr for Fish { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let sl = s.to_lowercase(); if sl.starts_with('s') { if let Ok(a) = sl[1..].parse::() { return Ok(a.into()).map(Fish); } } match sl.as_str() { "idea" => Ok(SymmetricAlgorithm::IDEA), "3des" => Ok(SymmetricAlgorithm::TripleDES), "cast5" => Ok(SymmetricAlgorithm::CAST5), "blowfish" => Ok(SymmetricAlgorithm::Blowfish), "aes" => Ok(SymmetricAlgorithm::AES128), "aes192" => Ok(SymmetricAlgorithm::AES192), "aes256" => Ok(SymmetricAlgorithm::AES256), "twofish" => Ok(SymmetricAlgorithm::Twofish), "twofish128" => Ok(SymmetricAlgorithm::Twofish), "camellia128" => Ok(SymmetricAlgorithm::Camellia128), "camellia192" => Ok(SymmetricAlgorithm::Camellia192), "camellia256" => Ok(SymmetricAlgorithm::Camellia256), _ => Err(anyhow::anyhow!("Unknown cipher algorithm {:?}", s)), }.map(Fish) } } impl fmt::Display for Fish { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use AEADAlgorithm::*; #[allow(deprecated)] match self.0 { EAX => f.write_str("EAX"), OCB => f.write_str("OCB"), GCM => f.write_str("GCM"), catchall => write!(f, "{:?}", catchall), } } } impl fmt::Display for Fish { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use HashAlgorithm::*; #[allow(deprecated)] match self.0 { MD5 => f.write_str("MD5"), SHA1 => f.write_str("SHA1"), RipeMD => f.write_str("RIPEMD160"), SHA256 => f.write_str("SHA256"), SHA384 => f.write_str("SHA384"), SHA512 => f.write_str("SHA512"), SHA224 => f.write_str("SHA224"), Private(u) => write!(f, "Private({})", u), Unknown(u) => write!(f, "Unknown({})", u), catchall => write!(f, "{:?}", catchall), } } } impl FromStr for Fish { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let sl = s.to_lowercase(); if sl.starts_with('h') { if let Ok(a) = sl[1..].parse::() { return Ok(a.into()).map(Fish); } } match sl.as_str() { "md5" => Ok(HashAlgorithm::MD5), "sha1" => Ok(HashAlgorithm::SHA1), "ripemd160" => Ok(HashAlgorithm::RipeMD), "sha256" => Ok(HashAlgorithm::SHA256), "sha384" => Ok(HashAlgorithm::SHA384), "sha512" => Ok(HashAlgorithm::SHA512), "sha224" => Ok(HashAlgorithm::SHA224), _ => Err(anyhow::anyhow!("Unknown hash algorithm {:?}", s)), }.map(Fish) } } sequoia-gpg-agent-0.4.2/src/cardinfo.rs000064400000000000000000000124431046102023000160720ustar 00000000000000//! CardInfo and related data structures. //! //! This module defines the [`CardInfo`] family of data structures. //! It is returned by [`Agent::card_info`](crate::Agent::card_info). use std::str::FromStr; use sequoia_openpgp as openpgp; use openpgp::Fingerprint; use sequoia_ipc as ipc; use ipc::Keygrip; use crate::Result; /// KeyInfo-related errors. #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Error parsing card info data: {0}")] ParseError(String), } /// Information about a key. /// /// Returned by `Agent::card_info`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CardInfo { raw: Vec<(String, String)>, } impl CardInfo { /// Returns the raw information. /// /// The first string is the keyword, e.g., `KEY-FPR` and the /// second string is the value `3 /// 2DC50AB55BE2F3B04C2D2CF8A3506AFB820ABD08`. The same keyword /// may be returned multiple times. pub fn raw(&self) -> impl Iterator { self.raw.iter() } /// Returns the available keys. pub fn keys<'a>(&'a self) -> impl Iterator + 'a { self.raw() .filter_map(|(keyword, value)| { if keyword == "KEY-FPR" { if let Some(fpr) = value.split(" ").nth(1) { return Fingerprint::from_str(fpr).ok(); } } None }) } /// Returns the available keys by their keygrip. pub fn keys_keygrips<'a>(&'a self) -> impl Iterator + 'a { self.raw() .filter_map(|(keyword, value)| { if keyword == "KEYPAIRINFO" { if let Some(fpr) = value.split(" ").nth(0) { return Keygrip::from_str(fpr).ok(); } } None }) } /// Parses the status lines returned by `learn --sendinfo`. /// /// The output looks like: /// /// ```text /// S KDF �%01%00 /// S SIG-COUNTER 793 /// S CHV-STATUS +1+127+127+127+3+0+3 /// S KEY-TIME 3 1428399828 /// S KEY-TIME 2 1428399800 /// S KEY-TIME 1 1428399766 /// S KEY-FPR 3 2DC50AB55BE2F3B04C2D2CF8A3506AFB820ABD08 /// S KEY-FPR 2 50E6D924308DBF223CFB510AC2B819056C652598 /// S KEY-FPR 1 C03FA6411B03AE12576461187223B56678E02528 /// S DISP-SEX 9 /// S MANUFACTURER 6 Yubico /// S EXTCAP gc=1+ki=1+fc=1+pd=1+mcl3=2048+aac=1+sm=0+si=5+dec=0+bt=1+kdf=1 /// S APPTYPE openpgp /// S SERIALNO D2760001240103040006181329630000 /// S READER 1050:0407:X:0 /// S KEYPAIRINFO 9483454871CC1239D4C2A1416F2742D39A14DB14 OPENPGP.3 /// S KEYPAIRINFO 9873FD355DE470DDC151CD9919AC9785C3C2FDDE OPENPGP.2 /// S KEYPAIRINFO BE2FE8C8793141322AC30E3EAFD1E4F9D8DACCC4 OPENPGP.1 /// ``` pub(crate) fn parse(raw: Vec<(String, String)>) -> Result { Ok(Self { raw, }) } } #[cfg(test)] mod test { use super::*; #[test] fn parse_cardinfo() -> Result<()> { let lines = &[ ("KDF", "�%01%00"), ("SIG-COUNTER", "793"), ("CHV-STATUS", "+1+127+127+127+3+0+3"), ("KEY-TIME", "3 1428399828"), ("KEY-TIME", "2 1428399800"), ("KEY-TIME", "1 1428399766"), ("KEY-FPR", "3 2DC50AB55BE2F3B04C2D2CF8A3506AFB820ABD08"), ("KEY-FPR", "2 50E6D924308DBF223CFB510AC2B819056C652598"), ("KEY-FPR", "1 C03FA6411B03AE12576461187223B56678E02528"), ("DISP-SEX", "9"), ("MANUFACTURER", "6 Yubico"), ("EXTCAP", "gc=1+ki=1+fc=1+pd=1+mcl3=2048+aac=1+sm=0+si=5+dec=0+bt=1+kdf=1"), ("APPTYPE", "openpgp"), ("SERIALNO", "D2760001240103040006181329630000"), ("READER", "1050:0407:X:0"), ("KEYPAIRINFO", "9483454871CC1239D4C2A1416F2742D39A14DB14 OPENPGP.3"), ("KEYPAIRINFO", "9873FD355DE470DDC151CD9919AC9785C3C2FDDE OPENPGP.2"), ("KEYPAIRINFO", "BE2FE8C8793141322AC30E3EAFD1E4F9D8DACCC4 OPENPGP.1"), ][..]; let info = CardInfo::parse( lines.into_iter() .map(|(k, m)| { (k.to_string(), m.to_string()) }) .collect::>()) .expect("valid"); let fprs = info.keys().collect::>(); assert_eq!(fprs.len(), 3); assert_eq!( fprs[0], "2DC50AB55BE2F3B04C2D2CF8A3506AFB820ABD08".parse::().expect("ok")); assert_eq!( fprs[1], "50E6D924308DBF223CFB510AC2B819056C652598".parse::().expect("ok")); assert_eq!( fprs[2], "C03FA6411B03AE12576461187223B56678E02528".parse::().expect("ok")); let keygrips = info.keys_keygrips().collect::>(); assert_eq!(keygrips.len(), 3); assert_eq!( keygrips[0], "9483454871CC1239D4C2A1416F2742D39A14DB14".parse::().expect("ok")); assert_eq!( keygrips[1], "9873FD355DE470DDC151CD9919AC9785C3C2FDDE".parse::().expect("ok")); assert_eq!( keygrips[2], "BE2FE8C8793141322AC30E3EAFD1E4F9D8DACCC4".parse::().expect("ok")); Ok(()) } } sequoia-gpg-agent-0.4.2/src/gnupg.rs000064400000000000000000000736111046102023000154310ustar 00000000000000//! GnuPG RPC support. #![warn(missing_docs)] use std::collections::BTreeMap; use std::convert::TryFrom; use std::ffi::OsStr; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use futures::{Stream, StreamExt}; use std::task::{Poll, self}; use std::pin::Pin; use sequoia_openpgp as openpgp; use openpgp::types::{HashAlgorithm, Timestamp}; use openpgp::fmt::hex; use openpgp::cert::ValidCert; use openpgp::crypto; use openpgp::packet::prelude::*; use openpgp::parse::Parse; use sequoia_ipc as ipc; use ipc::sexp::Sexp; use crate::Result; use crate::assuan; use crate::Keygrip; use crate::utils::new_background_command; #[derive(thiserror::Error, Debug)] /// Errors used in this module. pub enum Error { /// Errors related to `gpgconf`. #[error("gpgconf: {0}")] GPGConf(String), } /// A GnuPG context. #[derive(Debug)] pub struct Context { homedir: Option, sockets: BTreeMap, ephemeral: Option, // XXX: Remove me once hack for Cygwin won't be necessary. #[cfg(windows)] cygwin: bool, } impl Context { /// Creates a new context for the default GnuPG home directory. pub fn new() -> Result { Self::make(None, None) } /// Creates a new context for the given GnuPG home directory. pub fn with_homedir

(homedir: P) -> Result where P: AsRef { Self::make(Some(homedir.as_ref()), None) } /// Creates a new ephemeral context. /// /// The created home directory will be deleted once this object is /// dropped. pub fn ephemeral() -> Result { Self::make(None, Some(tempfile::tempdir()?)) } fn make(homedir: Option<&Path>, ephemeral: Option) -> Result { let mut sockets: BTreeMap = Default::default(); let ephemeral_dir = ephemeral.as_ref().map(|tmp| tmp.path()); let homedir = ephemeral_dir.or(homedir); // Guess if we're dealing with Unix/Cygwin or native Windows variant // We need to do that in order to pass paths in correct style to gpgconf let a_gpg_path = Self::gpgconf(&None, &["--list-dirs", "homedir"], 1)?; let first_byte = a_gpg_path.get(0).and_then(|c| c.get(0)).and_then(|c| c.get(0)); let gpg_style = match first_byte { Some(b'/') => Mode::Unix, _ => Mode::native(), }; let homedir = homedir.map(|dir| convert_path(dir, gpg_style) .unwrap_or_else(|_| PathBuf::from(dir)) ); for fields in Self::gpgconf(&homedir, &["--list-dirs"], 2)? { let key = std::str::from_utf8(&fields[0])?; // For now, we're only interested in sockets. let socket = match key.strip_suffix("-socket") { Some(socket) => socket, _ => continue, }; // NOTE: Directories and socket paths are percent-encoded if no // argument to "--list-dirs" is given let mut value = std::str::from_utf8(&fields[1])?.to_owned(); // FIXME: Percent-decode everything, but for now at least decode // colons to support Windows drive letters value = value.replace("%3a", ":"); // Store paths in native format, following the least surprise rule. let path = convert_path(&value, Mode::native())?; sockets.insert(socket.into(), path); } /// Whether we're dealing with gpg that expects Windows or Unix-style paths. #[derive(Copy, Clone)] #[allow(dead_code)] enum Mode { Windows, Unix } impl Mode { fn native() -> Self { platform! { unix => Mode::Unix, windows => Mode::Windows, } } } #[cfg(not(windows))] fn convert_path(path: impl AsRef, mode: Mode) -> Result { match mode { Mode::Unix => Ok(PathBuf::from(path.as_ref())), Mode::Windows => Err(anyhow::anyhow!( "Converting to Windows-style paths is only supported on Windows" ).into()), } } #[cfg(windows)] fn convert_path(path: impl AsRef, mode: Mode) -> Result { let conversion_type = match mode { Mode::Windows => "--windows", Mode::Unix => "--unix", }; Ok(new_background_command("cygpath") .arg(conversion_type) .arg(path.as_ref()) .output() .map_err(Into::into) .and_then(|out| if out.status.success() { let output = std::str::from_utf8(&out.stdout)?.trim(); Ok(PathBuf::from(output)) } else { Err(anyhow::anyhow!( "Executing cygpath encountered error for path {}", path.as_ref().to_string_lossy() )) } )?) } Ok(Context { homedir, sockets, ephemeral, #[cfg(windows)] cygwin: cfg!(windows) && matches!(gpg_style, Mode::Unix), }) } fn gpgconf(homedir: &Option, arguments: &[&str], nfields: usize) -> Result>>> { let nl = |&c: &u8| c as char == '\n'; let colon = |&c: &u8| c as char == ':'; let mut gpgconf = new_background_command("gpgconf"); if let Some(homedir) = homedir { gpgconf.arg("--homedir").arg(homedir); // https://dev.gnupg.org/T4496 gpgconf.env("GNUPGHOME", homedir); } gpgconf.args(arguments); let output = gpgconf.output().map_err(|e| { Error::GPGConf(e.to_string()) })?; if output.status.success() { let mut result = Vec::new(); for mut line in output.stdout.split(nl) { if line.is_empty() { // EOF. break; } // Make sure to also skip \r on Windows if line[line.len() - 1] == b'\r' { line = &line[..line.len() - 1]; } let fields = line.splitn(nfields, colon).map(|f| f.to_vec()) .collect::>(); if fields.len() != nfields { return Err(Error::GPGConf( format!("Malformed response, expected {} fields, \ on line: {:?}", nfields, line)).into()); } result.push(fields); } Ok(result) } else { Err(Error::GPGConf(String::from_utf8_lossy( &output.stderr).into_owned()).into()) } } /// Returns the path to `homedir` directory. /// /// The path returned will be in a local format, i. e. one accepted by /// available `gpgconf` or `gpg` tools. /// /// pub fn homedir(&self) -> Option<&Path> { self.homedir.as_deref() } /// Returns the path to a GnuPG socket. pub fn socket(&self, socket: C) -> Result<&Path> where C: AsRef { self.sockets.get(socket.as_ref()) .map(|p| p.as_path()) .ok_or_else(|| { Error::GPGConf(format!("No such socket {:?}", socket.as_ref())).into() }) } /// Creates directories for RPC communication. pub fn create_socket_dir(&self) -> Result<()> { // FIXME: GnuPG as packaged by MinGW fails to create socketdir because // it follows upstream Unix logic, which expects Unix-like `/var/run` // sockets to work. Additionally, GnuPG expects to work with and set // correct POSIX permissions that MinGW does not even support/emulate, // so this fails loudly. // Instead, don't do anything and rely on on homedir being treated // (correctly) as a fallback here. #[cfg(windows)] if self.cygwin { return Ok(()); } Self::gpgconf(&self.homedir, &["--create-socketdir"], 1)?; Ok(()) } /// Removes directories for RPC communication. /// /// Note: This will stop all servers once they note that their /// socket is gone. pub fn remove_socket_dir(&self) -> Result<()> { Self::gpgconf(&self.homedir, &["--remove-socketdir"], 1)?; Ok(()) } /// Starts a GnuPG component. pub fn start(&self, component: &str) -> Result<()> { let _ = self.create_socket_dir(); // Best effort. Self::gpgconf(&self.homedir, &["--launch", component], 1)?; Ok(()) } /// Stops a GnuPG component. pub fn stop(&self, component: &str) -> Result<()> { Self::gpgconf(&self.homedir, &["--kill", component], 1)?; Ok(()) } /// Stops all GnuPG components. pub fn stop_all(&self) -> Result<()> { self.stop("all") } } impl Drop for Context { fn drop(&mut self) { if self.ephemeral.is_some() { let _ = self.stop_all(); let _ = self.remove_socket_dir(); } } } /// A connection to a GnuPG agent. pub struct Agent { c: assuan::Client, } impl Deref for Agent { type Target = assuan::Client; fn deref(&self) -> &Self::Target { &self.c } } impl DerefMut for Agent { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.c } } impl Stream for Agent { type Item = Result; /// Attempt to pull out the next value of this stream, returning /// None if the stream is finished. /// /// Note: It _is_ safe to call this again after the stream /// finished, i.e. returned `Ready(None)`. fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { Pin::new(&mut self.c).poll_next(cx) } } impl Agent { /// Connects to the agent. /// /// Note: This function does not try to start the server. If no /// server is running for the given context, this operation will /// fail. pub async fn connect(ctx: &Context) -> Result { let path = ctx.socket("agent")?; Self::connect_to(path).await } /// Connects to the agent at the given path. /// /// Note: This function does not try to start the server. If no /// server is running for the given context, this operation will /// fail. pub async fn connect_to

(path: P) -> Result where P: AsRef { Ok(Agent { c: assuan::Client::connect(path).await? }) } /// Creates a signature over the `digest` produced by `algo` using /// `key` with the secret bits managed by the agent. pub async fn sign<'a>(&'a mut self, key: &'a KeyPair, algo: HashAlgorithm, digest: &'a [u8]) -> Result { for option in Self::options() { self.send_simple(option).await?; } if key.password.is_some() { self.send_simple("OPTION pinentry-mode=loopback").await?; } let grip = Keygrip::of(key.public.mpis())?; self.send_simple(format!("SIGKEY {}", grip)).await?; self.send_simple( format!("SETKEYDESC {}", assuan::escape(&key.password_prompt))).await?; let algo = u8::from(algo); let digest = hex::encode(&digest); self.send_simple(format!("SETHASH {} {}", algo, digest)).await?; self.send("PKSIGN")?; let mut data = Vec::new(); while let Some(r) = self.next().await { match r? { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Inquire { keyword, .. } => match (keyword.as_str(), &key.password) { ("PASSPHRASE", Some(p)) => { p.map(|p| self.data(p))?; // Dummy read to send the data. self.next().await; }, _ => acknowledge_inquiry(self).await?, }, assuan::Response::Error { ref message, .. } => return assuan::operation_failed(self, message).await, assuan::Response::Data { ref partial } => data.extend_from_slice(partial), } } Ok(Sexp::from_bytes(&data)?.to_signature()?) } /// Decrypts `ciphertext` using `key` with the secret bits managed /// by the agent. pub async fn decrypt<'a>(&'a mut self, key: &'a KeyPair, ciphertext: &'a crypto::mpi::Ciphertext, plaintext_len: Option) -> Result { for option in Self::options() { self.send_simple(option).await?; } if key.password.is_some() { self.send_simple("OPTION pinentry-mode=loopback").await?; } let grip = Keygrip::of(key.public.mpis())?; self.send_simple(format!("SETKEY {}", grip)).await?; self.send_simple(format!("SETKEYDESC {}", assuan::escape(&key.password_prompt))).await?; self.send("PKDECRYPT")?; let mut padding = true; let mut data = Vec::new(); while let Some(r) = self.next().await { match r? { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } => (), // Ignore. assuan::Response::Inquire { ref keyword, .. } => match (keyword.as_str(), &key.password) { ("PASSPHRASE", Some(p)) => { p.map(|p| self.data(p))?; // Dummy read to send the data. self.next().await; }, ("CIPHERTEXT", _) => { let mut buf = Vec::new(); Sexp::try_from(ciphertext)?.serialize(&mut buf)?; self.data(&buf)?; // Dummy read to send the data. self.next().await; }, _ => acknowledge_inquiry(self).await?, }, assuan::Response::Status { ref keyword, ref message } => if keyword == "PADDING" { padding = message != "0"; }, assuan::Response::Error { ref message, .. } => return assuan::operation_failed(self, message).await, assuan::Response::Data { ref partial } => data.extend_from_slice(partial), } } // Get rid of the safety-0. // // gpg-agent seems to add a trailing 0, supposedly for good // measure. if data.iter().last() == Some(&0) { let l = data.len(); data.truncate(l - 1); } Ok(Sexp::from_bytes(&data)?.finish_decryption( &key.public, ciphertext, plaintext_len, padding)?) } /// Computes options that we want to communicate. fn options() -> Vec { use std::env::var; let mut r = Vec::new(); if let Ok(tty) = var("GPG_TTY") { r.push(format!("OPTION ttyname={}", tty)); } else { #[cfg(unix)] unsafe { use std::ffi::CStr; let tty = libc::ttyname(0); if ! tty.is_null() { if let Ok(tty) = CStr::from_ptr(tty).to_str() { r.push(format!("OPTION ttyname={}", tty)); } } } } if let Ok(term) = var("TERM") { r.push(format!("OPTION ttytype={}", term)); } if let Ok(display) = var("DISPLAY") { r.push(format!("OPTION display={}", display)); } if let Ok(xauthority) = var("XAUTHORITY") { r.push(format!("OPTION xauthority={}", xauthority)); } if let Ok(dbus) = var("DBUS_SESSION_BUS_ADDRESS") { r.push(format!("OPTION putenv=DBUS_SESSION_BUS_ADDRESS={}", dbus)); } // We're going to pop() options off the end, therefore reverse // the vec here to preserve the above ordering, which is the // one GnuPG uses. r.reverse(); r } /// Start tracing the data that is sent to the server. /// /// Note: if a tracing function is already registered, this /// replaces it. pub fn trace_data_sent(&mut self, fun: Box) { self.c.trace_data_sent(fun); } /// Start tracing the data that is received from the server. /// /// Note: if a tracing function is already registered, this /// replaces it. pub fn trace_data_received(&mut self, fun: Box) { self.c.trace_data_received(fun); } } /// A cryptographic key pair. /// /// A `KeyPair` is a combination of public and secret key. This /// particular implementation does not have the secret key, but /// diverges the cryptographic operations to `gpg-agent`. pub struct KeyPair { public: Key, agent_socket: PathBuf, password: Option, password_prompt: String, } impl KeyPair { /// Returns a `KeyPair` for `key` with the secret bits managed by /// the agent. /// /// This provides a convenient, synchronous interface for use with /// the low-level Sequoia crate. pub fn new(ctx: &Context, key: &Key) -> Result where R: key::KeyRole { Ok(KeyPair { password: None, password_prompt: format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {}.", key.keyid(), Timestamp::try_from(key.creation_time()).unwrap()), public: key.role_as_unspecified().clone(), agent_socket: ctx.socket("agent")?.into(), }) } /// Changes the password prompt to include information about the /// cert. /// /// Use this function to give more context to the user when she is /// prompted for a password. This function will generate a prompt /// that is very similar to the prompts that GnuPG generates. /// /// To set an arbitrary password prompt, use /// [`KeyPair::with_password_prompt`]. pub fn with_cert(self, cert: &ValidCert) -> Self { let primary_id = cert.keyid(); let keyid = self.public.keyid(); let prompt = match (primary_id == keyid, cert.primary_userid() .map(|uid| uid.clone()) .ok()) { (true, Some(uid)) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {}.", uid.userid(), keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), ), (false, Some(uid)) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {} (main key ID {}).", uid.userid(), keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), primary_id, ), (true, None) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {}.", keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), ), (false, None) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {} (main key ID {}).", keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), primary_id, ), }; self.with_password_prompt(prompt) } /// Supplies a password to unlock the secret key. /// /// This will be used when the secret key operation is performed, /// e.g. when signing or decrypting a message. /// /// Note: This is the equivalent of GnuPG's /// `--pinentry-mode=loopback` and requires explicit opt-in in the /// gpg-agent configuration using the `allow-loopback-pinentry` /// option. If this is not enabled in the agent, the secret key /// operation will fail. It is likely only useful during testing. pub fn with_password(mut self, password: crypto::Password) -> Self { self.password = Some(password); self } /// Changes the password prompt. /// /// Use this function to give more context to the user when she is /// prompted for a password. /// /// To set an password prompt that uses information from the /// OpenPGP certificate, use [`KeyPair::with_cert`]. pub fn with_password_prompt(mut self, prompt: String) -> Self { self.password_prompt = prompt; self } } impl crypto::Signer for KeyPair { fn public(&self) -> &Key { &self.public } fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result { use crate::openpgp::types::PublicKeyAlgorithm::*; use crate::openpgp::crypto::mpi::PublicKey; #[allow(deprecated)] match (self.public.pk_algo(), self.public.mpis()) { (RSASign, PublicKey::RSA { .. }) | (RSAEncryptSign, PublicKey::RSA { .. }) | (DSA, PublicKey::DSA { .. }) | (EdDSA, PublicKey::EdDSA { .. }) | (ECDSA, PublicKey::ECDSA { .. }) => { use tokio::runtime::{Handle, Runtime}; // Connect to the agent and do the signing // operation. let do_it = async move { let mut a = Agent::connect_to(&self.agent_socket).await?; let sig = a.sign(self, hash_algo, digest).await?; Ok(sig) }; // See if the current thread is managed by a tokio // runtime. if Handle::try_current().is_err() { // Doesn't seem to be the case, so it is safe // to create a new runtime and block. let rt = Runtime::new()?; rt.block_on(do_it) } else { // It is! We must not create a second runtime // on this thread, but instead we will // delegate this to a new thread. std::thread::scope(|s| { s.spawn(move || { let rt = Runtime::new()?; rt.block_on(do_it) }).join() }).map_err(map_panic)? } }, (pk_algo, _) => Err(openpgp::Error::InvalidOperation(format!( "unsupported combination of algorithm {:?} and key {:?}", pk_algo, self.public)).into()), } } } impl crypto::Decryptor for KeyPair { fn public(&self) -> &Key { &self.public } fn decrypt(&mut self, ciphertext: &crypto::mpi::Ciphertext, plaintext_len: Option) -> openpgp::Result { use crate::openpgp::crypto::mpi::{PublicKey, Ciphertext}; match (self.public.mpis(), ciphertext) { (PublicKey::RSA { .. }, Ciphertext::RSA { .. }) | (PublicKey::ElGamal { .. }, Ciphertext::ElGamal { .. }) | (PublicKey::ECDH { .. }, Ciphertext::ECDH { .. }) => { use tokio::runtime::{Handle, Runtime}; // Connect to the agent and do the decryption // operation. let do_it = async move { let mut a = Agent::connect_to(&self.agent_socket).await?; let sk = a.decrypt(self, ciphertext, plaintext_len).await?; Ok(sk) }; // See if the current thread is managed by a tokio // runtime. if Handle::try_current().is_err() { // Doesn't seem to be the case, so it is safe // to create a new runtime and block. let rt = Runtime::new()?; rt.block_on(do_it) } else { // It is! We must not create a second runtime // on this thread, but instead we will // delegate this to a new thread. std::thread::scope(|s| { s.spawn(move || { let rt = Runtime::new()?; rt.block_on(do_it) }).join() }).map_err(map_panic)? } }, (public, ciphertext) => Err(openpgp::Error::InvalidOperation(format!( "unsupported combination of key pair {:?} \ and ciphertext {:?}", public, ciphertext)).into()), } } } /// Maps a panic of a worker thread to an error. /// /// Unfortunately, there is nothing useful to do with the error, but /// returning a generic error is better than panicking. fn map_panic(_: Box) -> anyhow::Error { anyhow::anyhow!("worker thread panicked") } /// Helper function to respond to inquiries. /// /// This function doesn't do something useful, but it ends the current /// inquiry. async fn acknowledge_inquiry(agent: &mut Agent) -> Result<()> { agent.send("END")?; agent.next().await; // Dummy read to send END. Ok(()) } #[cfg(test)] mod tests { use super::*; use openpgp::{ Cert, crypto::{ hash::Digest, mpi::Ciphertext, Decryptor, Signer, }, }; /// Asserts that a is usable from an async /// context. /// /// Previously, the test died with /// /// thread 'gnupg::tests::signer_in_async_context' panicked at /// 'Cannot start a runtime from within a runtime. This /// happens because a function (like `block_on`) attempted to /// block the current thread while the thread is being used to /// drive asynchronous tasks.' #[test] fn signer_in_async_context() -> Result<()> { async fn async_context() -> Result<()> { let ctx = match Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let key = Cert::from_bytes(crate::tests::key("testy-new.pgp"))? .primary_key().key().clone(); let mut pair = KeyPair::new(&ctx, &key)?; let algo = HashAlgorithm::default(); let digest = algo.context()?.into_digest()?; let _ = pair.sign(algo, &digest); Ok(()) } let rt = tokio::runtime::Runtime::new()?; rt.block_on(async_context()) } /// Asserts that a is usable from an async /// context. /// /// Previously, the test died with /// /// thread 'gnupg::tests::decryptor_in_async_context' panicked /// at 'Cannot start a runtime from within a runtime. This /// happens because a function (like `block_on`) attempted to /// block the current thread while the thread is being used to /// drive asynchronous tasks.' #[test] fn decryptor_in_async_context() -> Result<()> { async fn async_context() -> Result<()> { let ctx = match Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let key = Cert::from_bytes(crate::tests::key("testy-new.pgp"))? .keys().nth(1).unwrap().key().clone(); let mut pair = KeyPair::new(&ctx, &key)?; let ciphertext = Ciphertext::ECDH { e: vec![].into(), key: vec![].into_boxed_slice(), }; let _ = pair.decrypt(&ciphertext, None); Ok(()) } let rt = tokio::runtime::Runtime::new()?; rt.block_on(async_context()) } } sequoia-gpg-agent-0.4.2/src/keyinfo.rs000064400000000000000000000331751046102023000157560ustar 00000000000000//! KeyInfo and related data structures. //! //! This module defines the [`KeyInfo`] family of data structures. //! [`KeyInfoList`] is a collection of `KeyInfo`, and is returned by //! [`Agent::list_keys`](crate::Agent::list_keys). `KeyInfoList` can //! be iterated over, and individual keys can be efficiently looked up //! by their keygrip. Note: a keygrip is not an OpenPGP fingerprint. //! gpg-agent does not support addressing keys by their OpenPGP //! fingerprint; they can only be looked up by their keygrip. use std::borrow::Borrow; use std::collections::HashMap; use std::str::FromStr; use sequoia_openpgp as openpgp; use openpgp::packet::key; use openpgp::packet::Key; use sequoia_ipc as ipc; use ipc::Keygrip; use anyhow::Context; use crate::Result; /// KeyInfo-related errors. #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Error parsing keyinfo data: {0}")] ParseError(String), } /// The type of key. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum KeyType { /// Regular key stored on disk, Regular, /// Key is stored on a smartcard (token), Smartcard, /// Unknown type, Unknown, /// Key type is missing. Missing, } /// A key's protection, if any. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum KeyProtection { /// The key is protected with a passphrase. Protected, /// The key is not protected. NotProtected, /// Unknown protection. UnknownProtection, } /// A key's flags, if any. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] enum KeyFlag { /// The key has been disabled. Disabled, /// The key is listed in sshcontrol. SSHControl, /// Use of the key needs to be confirmed. ConfirmationRequired, } /// Information about a key. /// /// Returned by `Agent::list_keys`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct KeyInfo { keygrip: Keygrip, /// TYPE is describes the type of the key: /// /// - 'D' - Regular key stored on disk, /// - 'T' - Key is stored on a smartcard (token), /// - 'X' - Unknown type, /// - '-' - Key is missing. keytype: KeyType, /// SERIALNO is an ASCII string with the serial number of the /// smartcard. If the serial number is not known a single dash /// '-' is used instead. serialno: Option, /// If the key is on a smartcard, this is used to distinguish the /// keys on the smartcard. If it is not known a dash is used /// instead. /// /// Example: `OPENPGP.1`. idstr: Option, /// If the passphrase for the key was found in the key cache. passphrase_cached: bool, /// The key protection type: /// /// - 'P' - The key is protected with a passphrase, /// - 'C' - The key is not protected, /// - '-' - Unknown protection. protection: KeyProtection, /// The TTL in seconds for the key or None if not available. ttl: Option, /// The key's flag. flags: Vec, } impl KeyInfo { /// Returns the key's keygrip. pub fn keygrip(&self) -> &ipc::Keygrip { &self.keygrip } /// Returns the key's type. pub fn keytype(&self) -> KeyType { self.keytype.clone() } /// Returns the serial number of the smartcard. /// /// If the key is not on a smartcard, this returns `None`. pub fn serialno(&self) -> Option<&str> { self.serialno.as_deref() } /// Returns the key's identifier on the smartcard. /// /// If the key is not on a smartcard, this returns `None`. /// Example: `OPENPGP.1`. pub fn idstr(&self) -> Option<&str> { self.idstr.as_deref() } /// Returns whether the passphrase for the key was found in the /// key cache. pub fn passphrase_cached(&self) -> bool { self.passphrase_cached } /// Returns the key's protection, if any. pub fn protection(&self) -> &KeyProtection { &self.protection } /// Returns the TTL in seconds for the key. /// /// If not available, returns `None`. pub fn ttl(&self) -> Option { self.ttl } /// Returns whether the key has been disabled. pub fn key_disabled(&self) -> bool { self.flags.contains(&KeyFlag::Disabled) } /// Returns whether the key is listed in sshcontrol. pub fn in_ssh_control(&self) -> bool { self.flags.contains(&KeyFlag::SSHControl) } /// Returns whether use of the key needs to be confirmed. pub fn confirmation_required(&self) -> bool { self.flags.contains(&KeyFlag::ConfirmationRequired) } /// Parses a single keyinfo entry. /// /// An entry is of the form: /// /// ```text /// KEYINFO /// ``` /// /// Note: `keyinfo` should be the whole line without the leading /// `KEYINFO`. pub(crate) fn parse(keyinfo: &str) -> Result { let mut iter = keyinfo.split(' '); let Some(keygrip) = iter.next() else { return Err(Error::ParseError("KEYGRIP field missing".into()).into()); }; let keygrip = Keygrip::from_str(keygrip) .with_context(|| { format!("Parsing {:?} as a keygrip", keygrip) })?; // TYPE is describes the type of the key: // 'D' - Regular key stored on disk, // 'T' - Key is stored on a smartcard (token), // 'X' - Unknown type, // '-' - Key is missing. let Some(keytype) = iter.next() else { return Err(Error::ParseError("TYPE field missing".into()).into()); }; let keytype = match keytype { "D" => KeyType::Regular, "T" => KeyType::Smartcard, "-" => KeyType::Missing, "X"|_ => KeyType::Unknown, }; // SERIALNO is an ASCII string with the serial number of the // smartcard. If the serial number is not known a single // dash '-' is used instead. let Some(serialno) = iter.next() else { return Err(Error::ParseError("SERIALNO field missing".into()).into()); }; let serialno = if serialno == "-" { None } else { Some(serialno.to_string()) }; // IDSTR is the IDSTR used to distinguish keys on a smartcard. If it // is not known a dash is used instead. let Some(idstr) = iter.next() else { return Err(Error::ParseError("IDSTR field missing".into()).into()); }; let idstr = if idstr == "-" { None } else { Some(idstr.to_string()) }; // CACHED is 1 if the passphrase for the key was found in the key cache. // If not, a '-' is used instead. let Some(cached) = iter.next() else { return Err(Error::ParseError("CACHED field missing".into()).into()); }; let passphrase_cached = match cached { "1" => true, _ => false, }; // PROTECTION describes the key protection type: // 'P' - The key is protected with a passphrase, // 'C' - The key is not protected, // '-' - Unknown protection. let Some(protection) = iter.next() else { return Err(Error::ParseError("PROTECTION field missing".into()).into()); }; let protection = match protection { "P" => KeyProtection::Protected, "C" => KeyProtection::NotProtected, _ => KeyProtection::UnknownProtection, }; // FPR returns the formatted ssh-style fingerprint of the key. It is only // printed if the option --ssh-fpr has been used. If ALGO is not given // to that option the default ssh fingerprint algo is used. Without the // option a '-' is printed. let Some(_ssh_fpr) = iter.next() else { return Err(Error::ParseError("FPR field missing".into()).into()); }; // TTL is the TTL in seconds for that key or '-' if n/a. let Some(ttl) = iter.next() else { return Err(Error::ParseError("TTL field missing".into()).into()); }; let ttl = if ttl == "-" { None } else { Some(u64::from_str_radix(ttl, 10).with_context(|| { format!("Parsing {:?} as a u64", ttl) })?) }; // FLAGS is a word consisting of one-letter flags: // 'D' - The key has been disabled, // 'S' - The key is listed in sshcontrol (requires --with-ssh), // 'c' - Use of the key needs to be confirmed, // '-' - No flags given. let Some(flags) = iter.next() else { return Err(Error::ParseError("FLAGS field missing".into()).into()); }; let flags = flags.chars() .filter_map(|c| { match c { 'D' => Some(KeyFlag::Disabled), 'S' => Some(KeyFlag::SSHControl), 'c' => Some(KeyFlag::ConfirmationRequired), _ => None, } }) .collect::>(); Ok(KeyInfo { keygrip, keytype, serialno, idstr, passphrase_cached, protection, ttl, flags, }) } } /// A collection of `KeyInfo`. pub struct KeyInfoList { keys: HashMap, } impl FromIterator for KeyInfoList { fn from_iter(iter: I) -> Self where I: IntoIterator { KeyInfoList { keys: HashMap::from_iter( iter.into_iter().map(|i| (i.keygrip().clone(), i))), } } } impl Default for KeyInfoList { fn default() -> Self { Self { keys: Default::default(), } } } impl KeyInfoList { /// Returns an empty `KeyInfoList`. pub fn empty() -> Self { Self::default() } /// Returns the information about the specified key, if any. pub fn lookup(&self, keygrip: K) -> Option<&KeyInfo> where K: Borrow { let keygrip = keygrip.borrow(); self.keys.get(keygrip) } /// Returns the information about the specified key, if any. pub fn lookup_by_key(&self, key: K) -> Option<&KeyInfo> where K: Borrow>, P: key:: KeyParts, R: key:: KeyRole, { let key = key.borrow(); // The mapping from Key to Keygrip is not total so this might // fail. But if there isn't a keygrip for a key, then // gpg-agent can't manage it, and thus can't return it. // Therefore if a key doesn't have a keygrip, the correct // answer is not to return an error, but to return `None`. let keygrip = Keygrip::of(key.mpis()).ok()?; self.lookup(keygrip) } /// Returns an iterator over the elements of the `KeyInfoList`. pub fn iter(&self) -> KeyInfoListIter { KeyInfoListIter { iter: self.keys.values(), } } /// Returns the number of elements. pub fn len(&self) -> usize { self.keys.len() } } /// An iterator over a `KeyInfoList`. pub struct KeyInfoListIter<'a> { iter: std::collections::hash_map::Values<'a, Keygrip, KeyInfo>, } impl<'a> IntoIterator for &'a KeyInfoList { type Item = &'a KeyInfo; type IntoIter = KeyInfoListIter<'a>; fn into_iter(self) -> Self::IntoIter { KeyInfoListIter { iter: self.keys.values() } } } impl<'a> Iterator for KeyInfoListIter<'a> { type Item = &'a KeyInfo; fn next(&mut self) -> Option { self.iter.next() } } #[cfg(test)] mod test { use super::*; #[test] fn parse_keyinfo_list() -> Result<()> { let entry = "EF8CE31AE9E310D660C7C9709A028442A8B52112 D - - - C - - -"; eprintln!("{}", entry); assert_eq!( KeyInfo::parse(entry).unwrap(), KeyInfo { keygrip: Keygrip::from_str( "EF8CE31AE9E310D660C7C9709A028442A8B52112").unwrap(), keytype: KeyType::Regular, serialno: None, idstr: None, passphrase_cached: false, protection: KeyProtection::NotProtected, ttl: None, flags: vec![ ], }); let entry = "EAFE58E4F5269129D7233A0FA6307C366A3EA2CC D - - - P - - -"; eprintln!("{}", entry); assert_eq!( KeyInfo::parse(entry).unwrap(), KeyInfo { keygrip: Keygrip::from_str( "EAFE58E4F5269129D7233A0FA6307C366A3EA2CC").unwrap(), keytype: KeyType::Regular, serialno: None, idstr: None, passphrase_cached: false, protection: KeyProtection::Protected, ttl: None, flags: vec![ ], }); let entry = "9483454871CC1239D4C2A1416F2742D39A14DB14 T D2760001240103040006181329630000 OPENPGP.3 - - - - -"; eprintln!("{}", entry); assert_eq!( KeyInfo::parse(entry).unwrap(), KeyInfo { keygrip: Keygrip::from_str( "9483454871CC1239D4C2A1416F2742D39A14DB14").unwrap(), keytype: KeyType::Smartcard, serialno: Some("D2760001240103040006181329630000".to_string()), idstr: Some("OPENPGP.3".to_string()), passphrase_cached: false, protection: KeyProtection::UnknownProtection, ttl: None, flags: vec![ ], }); Ok(()) } } sequoia-gpg-agent-0.4.2/src/lib.rs000064400000000000000000002415721046102023000150620ustar 00000000000000//! This crate includes functionality for interacting with GnuPG's //! `gpg-agent`. //! //! `gpg-agent` is a secret key store, which is shipped as part of GnuPG. //! It is used to manage secret key material, and hardware devices that //! contain secret key material. It provides an RPC interface using the //! [Assuan protocol]. //! //! [Assuan protocol]: https://gnupg.org/software/libassuan //! //! This is how `gpg`, GnuPG's primary command-line interface, //! communicates with it. //! //! This crate provides a Rust API for interacting with `gpg-agent`. //! //! Note: this crate communicates directly with `gpg-agent`; it does not //! go via `gpg`. //! //! # Examples //! //! Import secret key material, list the keys, and sign a message: //! //! ```rust //! use std::io::{Read, Write}; //! //! use sequoia_openpgp as openpgp; //! use openpgp::Cert; //! # use openpgp::crypto::hash::Digest; //! use openpgp::packet::signature; //! use openpgp::parse::Parse; //! use openpgp::policy::StandardPolicy; //! use openpgp::serialize::stream::{Message, Signer, LiteralWriter}; //! use openpgp::types::HashAlgorithm; //! use openpgp::types::SignatureType; //! //! use sequoia_gpg_agent as gpg_agent; //! use gpg_agent::KeyPair; //! //! const P: &StandardPolicy = &StandardPolicy::new(); //! //! # let _: Result<(), anyhow::Error> = tokio_test::block_on(async { //! # //! // Load a TSK, i.e., a certificate with secret key material. //! let key = // ... //! # "\ //! # -----BEGIN PGP PRIVATE KEY BLOCK----- //! # Comment: B6E6 9975 5F27 FF3D 002D BD26 0390 037C 6D2B DA9A //! # //! # xVgEZewcTxYJKwYBBAHaRw8BAQdAitNrSMaEq1ZCXBtefsgJfGHdipTrZnTKXf6R //! # mnueg9oAAQC8xOJk0vjPPQNEkXc5xlm7w+f2C7EGQ0/GQYuxTVZAoRC6wsALBB8W //! # CgB9BYJl7BxPAwsJBwkQA5ADfG0r2ppHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu //! # c2VxdW9pYS1wZ3Aub3JnNHQtwZonz3Z3xLL1M7jK/Sl4R1TNDqZ+xeBhXSzbOAkD //! # FQoIApsBAh4BFiEEtuaZdV8n/z0ALb0mA5ADfG0r2poAAKCuAP4ncgpWq32cIDQU //! # ApwklDTPpWpahIuYsL0LdIjkOnVNrQD/XXxnSIVPpD9ax6Gkwdjrq21rUSnQOQcb //! # LPJ9jbSS2wHHWARl7BxPFgkrBgEEAdpHDwEBB0Brn/OSMDjcjVvcUrY+wYjtpQDw //! # BFpow6NrHs4X5K2wkgAA/3sqHgNHP16HDEwfBteL4YfdXIj0fDBHmvLy6csYieke //! # D/nCwL8EGBYKATEFgmXsHE8JEAOQA3xtK9qaRxQAAAAAAB4AIHNhbHRAbm90YXRp //! # b25zLnNlcXVvaWEtcGdwLm9yZyUyW5yXDcLc93rxkYdz7tnHpOKjviJn17/hCQw/ //! # p3YkApsCvqAEGRYKAG8FgmXsHE8JEAiqGShDW50RRxQAAAAAAB4AIHNhbHRAbm90 //! # YXRpb25zLnNlcXVvaWEtcGdwLm9yZx0ribmnTiQaPn4ftkFsyOYwo4cerHsFSjch //! # sIRTjGosFiEEHXtwtDIzaa3fXh2iCKoZKENbnREAAH6OAP9aIMae+BJWPfxlf1cL //! # 4wwqTihcctO8LjaUk/TZVc8I6AEAlCMAn9SinU0FhgGr8RThuY2Nj8dwzEdCbhVu //! # l9jxrQ0WIQS25pl1Xyf/PQAtvSYDkAN8bSvamgAA1Q0A/A4aKmIY7aNShNoJ5cT3 //! # SmqCrmFBQuIAXY5QhKu44FtcAQDZ4nB8GNORlAe7q719lLV+jo/BymIRstcU0R3R //! # lfqgCg== //! # =B79/ //! # -----END PGP PRIVATE KEY BLOCK----- //! # "; //! let cert = Cert::from_bytes(key)?; //! assert!(cert.is_tsk()); //! //! // Connect to a temporary GnuPG home directory. Usually, //! // you'll want to connect to the default home directory using //! // Agent::connect_to_default. //! let mut agent = gpg_agent::Agent::connect_to_ephemeral().await?; //! //! // Import the secret key material into gpg-agent. //! for k in cert.keys().secret() { //! agent.import(P, //! &cert, k.parts_as_secret().expect("have secret"), //! true, true).await?; //! } //! //! // List the keys. //! let list = agent.list_keys().await?; //! # assert_eq!(list.len(), cert.keys().secret().count()); //! println!("gpg agent manages {} keys:", list.len()); //! for k in list.iter() { //! eprintln!(" - {}", k.keygrip()); //! } //! //! // Sign a message using the signing key we just imported. //! let signing_key = cert.with_policy(P, None)? //! .keys().for_signing() //! .next().expect("Have a signing-capable subkey."); //! //! let mut keypair = agent.keypair(&signing_key)?; //! //! let mut signed_message = Vec::new(); //! let message = Message::new(&mut signed_message); //! let message = Signer::new(message, keypair).build()?; //! let mut message = LiteralWriter::new(message).build()?; //! message.write_all(b"I love you!")?; //! message.finalize()?; //! //! # Ok(()) }); //! ``` use std::{ convert::TryFrom, path::Path, path::PathBuf, ops::Deref, ops::DerefMut, pin::Pin, }; use sequoia_openpgp as openpgp; use sequoia_ipc as ipc; use openpgp::{ Cert, cert::ValidCert, crypto::{ self, Password, S2K, mem::Protected, mpi::SecretKeyChecksum, }, fmt::hex, packet::{ Key, key::{ KeyRole, PublicParts, SecretParts, UnspecifiedRole, SecretKeyMaterial, }, SKESK, }, parse::Parse, policy::Policy, types::{ HashAlgorithm, Timestamp, }, }; use ipc::{ Keygrip, sexp::Sexp, }; use futures::stream::StreamExt; #[macro_use] mod macros; mod babel; mod utils; pub mod assuan; use assuan::Response; use assuan::escape; pub mod gnupg; pub mod keyinfo; pub mod cardinfo; #[cfg(test)] mod tests; trace_module!(TRACE); /// Reexport sequoia_ipc. /// /// Some of our public types use types defined in this crate. pub use sequoia_ipc; #[derive(thiserror::Error, Debug)] /// Errors used in this module. pub enum Error { /// GnuPG's home directory doesn't exist. #[error("GnuPG's home directory ({0}) doesn't exist")] GnuPGHomeMissing(PathBuf), /// Unknown key. #[error("Unknown key: {0}")] UnknownKey(Keygrip), /// No smartcards are connected. #[error("No smartcards are connected")] NoSmartcards, /// The key already exists. #[error("{0} already exists: {1}")] KeyExists(Keygrip, String), #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Utf8(#[from] std::str::Utf8Error), #[error(transparent)] Assuan(#[from] assuan::Error), #[error(transparent)] GnuPG(#[from] gnupg::Error), #[error(transparent)] KeyInfo(#[from] keyinfo::Error), #[error(transparent)] OpenPGP(#[from] openpgp::Error), #[error(transparent)] Other(#[from] anyhow::Error), } type Result = std::result::Result; /// Controls how gpg-agent inquires passwords. #[derive(Debug, Clone, PartialEq, Eq)] pub enum PinentryMode { /// Ask using pinentry. This is the default. Ask, /// Cancel all inquiries. Cancel, /// Refuse all inquiries. Error, /// Ask the frontend (us) for passwords. Loopback, } impl Default for PinentryMode { fn default() -> Self { PinentryMode::Ask } } impl PinentryMode { /// Returns a string representation usable with the gpg-agent. pub fn as_str(&self) -> &'static str { match self { PinentryMode::Ask => "ask", PinentryMode::Cancel => "cancel", PinentryMode::Error => "error", PinentryMode::Loopback => "loopback", } } } impl std::fmt::Display for PinentryMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } impl std::str::FromStr for PinentryMode { type Err = anyhow::Error; fn from_str(s: &str) -> std::result::Result { match s.to_lowercase().as_str() { "ask" => Ok(PinentryMode::Ask), "default" => Ok(PinentryMode::Ask), "cancel" => Ok(PinentryMode::Cancel), "error" => Ok(PinentryMode::Error), "loopback" => Ok(PinentryMode::Loopback), _ => Err(anyhow::anyhow!("Unknown pinentry mode {:?}", s)), } } } fn trace_data_sent(data: &[u8]) { tracer!(TRACE, "trace_data_sent"); let mut data = stfu8::encode_u8(data); if data.len() > 80 && data.starts_with("D ") { data = format!("{}... ({} bytes)", data.chars().take(65).collect::(), data.len()); } t!("SENT: {}", data); } fn trace_data_received(data: &[u8]) { tracer!(TRACE, "trace_data_received"); let mut data = stfu8::encode_u8(data); if data.len() > 80 && data.starts_with("D ") { data = format!("{}... ({} bytes)", data.chars().take(65).collect::(), data.len()); } t!("RECV: {}", data); } /// A connection to a GnuPG agent. pub struct Agent { socket_path: PathBuf, c: assuan::Client, pinentry_mode: Option, } impl Deref for Agent { type Target = assuan::Client; fn deref(&self) -> &Self::Target { &self.c } } impl DerefMut for Agent { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.c } } impl futures::Stream for Agent { type Item = Result; /// Attempt to pull out the next value of this stream, returning /// None if the stream is finished. /// /// Note: It _is_ safe to call this again after the stream /// finished, i.e. returned `Ready(None)`. fn poll_next(mut self: Pin<&mut Self>, cx: &mut futures::task::Context<'_>) -> futures::task::Poll> { Pin::new(&mut self.c).poll_next(cx) } } impl Agent { /// Connects to the `gpg-agent` serving the specified context. /// /// If a `gpg-agent` isn't listening on the specified context, /// this function tries to start one. /// /// The GnuPG context, [`gnupg::Context`], specifies the GnuPG /// home directory. pub async fn connect(ctx: &gnupg::Context) -> Result { tracer!(TRACE, "connect"); async fn transaction(ctx: &gnupg::Context) -> Result { t!("Starting daemon if not running"); // If the home directory doesn't exist, gpg-agent won't // start, and we'll later get obscure errors. Check // eagerly so avoid confusing the eagerly. let homedir = ctx.homedir(); // Disabled on Windows. Re-enable when this issue is fixed: // // https://gitlab.com/sequoia-pgp/sequoia/-/issues/1093 // // SEE OTHER INSTANCES OF THIS BELOW (search for comment). #[cfg(not(windows))] if let Some(homedir) = homedir { if ! homedir.exists() { return Err(Error::GnuPGHomeMissing( homedir.to_path_buf()).into()); } } if false { // XXX: Currently, this will invoke gpgconf // --create-socketdir, and fail if that fails. It will // also spew all sorts of output to stderr. ctx.start("gpg-agent")?; } else { // In the mean time, manually start the agent. let mut c = std::process::Command::new("gpgconf"); if let Some(h) = ctx.homedir() { c.env("GNUPGHOME", h); c.arg("--homedir").arg(&h.display().to_string()); } c.arg("--launch").arg("gpg-agent"); c.status()?; } t!("Connecting to daemon"); let path = ctx.socket("agent")?; let mut connection = assuan::Client::connect(path).await?; if TRACE.load(std::sync::atomic::Ordering::Relaxed) { connection.trace_data_sent(Box::new(trace_data_sent)); connection.trace_data_received(Box::new(trace_data_received)); } Ok(Agent { socket_path: path.to_path_buf(), c: connection, pinentry_mode: None, }) } transaction(ctx).await.map_err(|e| { t!("failed: {}", e); e }) } /// Connects to the `gpg-agent` serving the specified context. /// /// If a `gpg-agent` isn't listening on the specified context, /// this function tries to start one. /// /// A default [`gnupg::Context`] with the specified home directory /// is used as the context. pub async fn connect_to

(homedir: P) -> Result where P: AsRef { let ctx = gnupg::Context::with_homedir(homedir)?; Self::connect(&ctx).await } /// Connects to the default `gpg-agent`. /// /// If a `gpg-agent` isn't listening on the specified context, /// this function tries to start one. /// /// A default [`gnupg::Context`] with the default home directory /// is used as the context. pub async fn connect_to_default() -> Result { let ctx = gnupg::Context::new()?; Self::connect(&ctx).await } /// Connects to a new, ephemeral `gpg-agent`. /// /// A default [`gnupg::Context`] with an ephemeral home directory /// is used as the context. pub async fn connect_to_ephemeral() -> Result { let ctx = gnupg::Context::ephemeral()?; Self::connect(&ctx).await } /// Connects to the `gpg-agent` listening on the specified socket. /// /// If a `gpg-agent` isn't listening on the specified socket, /// this function does *not* try to start one. pub async fn connect_to_agent

(socket_path: P) -> Result where P: AsRef { let socket_path = socket_path.as_ref(); let mut connection = assuan::Client::connect(socket_path).await?; if TRACE.load(std::sync::atomic::Ordering::Relaxed) { connection.trace_data_sent(Box::new(trace_data_sent)); connection.trace_data_received(Box::new(trace_data_received)); } Ok(Agent { socket_path: socket_path.to_path_buf(), c: connection, pinentry_mode: None, }) } /// Tells the gpg-agent to reload its configuration file. pub async fn reload(&mut self) -> Result<()> { self.send_simple("RELOADAGENT").await?; Ok(()) } /// Overrides the pinentry mode. pub fn set_pinentry_mode(&mut self, mode: PinentryMode) { self.pinentry_mode = Some(mode); } /// Disables gpg's pinentry. /// /// Changes the pinentry mode to `PinEntryMode::Error`, which /// configures the agent to not ask for a password. pub fn suppress_pinentry(mut self) -> Self { self.pinentry_mode = Some(PinentryMode::Error); self } /// Returns whether the agent has a secret key. pub async fn has_key(&mut self, key: &Key) -> Result { let grip = Keygrip::of(key.mpis())?; Ok(self.send_simple(format!("HAVEKEY {}", grip)).await.is_ok()) } /// Returns the keys managed by the agent. pub async fn list_keys(&mut self) -> Result { self.send("KEYINFO --list")?; let mut keys = Vec::new(); while let Some(response) = self.next().await { let response = response?; match response { Response::Ok { .. } => break, Response::Comment { .. } => (), Response::Status { ref keyword, ref message } => { if keyword == "KEYINFO" { keys.push(keyinfo::KeyInfo::parse(message)?) } else { return protocol_error(&response); } } // KEYINFO should not send an inquire. Response::Inquire { .. } => return protocol_error(&response), Response::Error { ref message, .. } => return self.operation_failed(message).await, response => return protocol_error(&response), } } self.next().await; // Dummy read to send END. Ok(keyinfo::KeyInfoList::from_iter(keys.into_iter())) } /// Returns information about a key managed by the agent. pub async fn key_info(&mut self, keygrip: &Keygrip) -> Result { self.send(format!("KEYINFO {}", keygrip.to_string()))?; let mut keyinfo = None; while let Some(response) = self.next().await { let response = response?; match response { Response::Ok { .. } => break, Response::Comment { .. } => (), Response::Status { ref keyword, ref message } => { if keyword == "KEYINFO" { if keyinfo.is_none() { keyinfo = Some(keyinfo::KeyInfo::parse(message)?); } else { // Only expect exactly one keyinfo line. return protocol_error(&response); } } else { return protocol_error(&response); } } // KEYINFO should not send an inquire. Response::Inquire { .. } => return protocol_error(&response), Response::Error { ref message, .. } => return self.operation_failed(message).await, response => return protocol_error(&response), } } self.next().await; // Dummy read to send END. if let Some(keyinfo) = keyinfo { Ok(keyinfo) } else { Err(Error::UnknownKey(keygrip.clone()).into()) } } /// Returns information about any *connected* smartcards. /// /// This sends `LEARN --sendinfo` to gpg-agent. pub async fn card_info(&mut self) -> Result { self.send("learn --sendinfo")?; let mut raw: Vec<(String, String)> = Vec::new(); while let Some(response) = self.next().await { let response = response?; match response { Response::Ok { .. } => break, Response::Comment { .. } => (), Response::Status { ref keyword, ref message } => { // Skip progress lines; they are just informative. if keyword != "PROGRESS" { raw.push((keyword.clone(), message.clone())); } } // KEYINFO should not send an inquire. Response::Inquire { .. } => return protocol_error(&response), Response::Error { ref message, .. } => { if message.as_ref().map(|m| m.starts_with("100663406 ")) .unwrap_or(false) { // Card removed. return self.operation_failed_as( Error::NoSmartcards.into()).await; } else { return self.operation_failed(message).await; } } response => return protocol_error(&response), } } self.next().await; // Dummy read to send END. if raw.is_empty() { Err(Error::NoSmartcards.into()) } else { cardinfo::CardInfo::parse(raw) } } /// Imports a secret key into the agent. /// /// `key` is the secret key that will be imported. /// /// `policy` and `cert` are decorative. They are only used to /// create a better password prompt. /// /// `unattended` tells `gpg-agent` whether it should import the /// key without reencrypting it. If `false`, the user is prompted /// to decrypt the secret key material, and it is reencrypted. /// /// `overwrite` tells the agent to overwrite an existing version /// of the key. If `overwrite` is not set, and a variant of the /// key already exists, then `Error::KeyExists` is returned. pub async fn import(&mut self, policy: &dyn Policy, cert: &Cert, key: &Key, unattended: bool, overwrite: bool) -> Result { // The gpg-agent shipped with GnuPG 2.4.x calculates the checksum // over ECC artifacts differently. Oddly, this seems to amount to // adding 8 to the checksum. See GnuPG commit // 2b118516240b4bddd34c68c23a99bea56682a509. use sequoia_openpgp::types::PublicKeyAlgorithm::*; let mut r = self.import_int( policy, cert, key, unattended, overwrite, 0).await; if r.is_err() && (key.pk_algo() == ECDSA || key.pk_algo() == EdDSA || key.pk_algo() == ECDH) { r = self.import_int( policy, cert, key, unattended, overwrite, 8).await; } r } async fn import_int(&mut self, policy: &dyn Policy, cert: &Cert, key: &Key, unattended: bool, overwrite: bool, csum_offset: u16) -> Result { use ipc::sexp::*; let keygrip = Keygrip::of(key.mpis())?; /// Makes a tuple cell, i.e. a *Cons*. fn c(name: &str, data: &[u8]) -> Sexp { Sexp::List(vec![Sexp::String(name.as_bytes().into()), Sexp::String(data.into())]) } /// Makes a tuple cell with a string value, i.e. a *String* cons. fn s(name: &str, data: impl ToString) -> Sexp { c(name, data.to_string().as_bytes()) } fn add_signed_mpi(list: &mut Vec, v: &[u8]) { let mut v = v.to_vec(); // If the high bit is set, we need to prepend a zero byte, // otherwise the agent will interpret the value as signed, and // thus negative. if v[0] & 0x80 > 0 { v.insert(0, 0); } add_raw(list, "_", &v); } fn add(list: &mut Vec, mpi: &mpi::MPI) { add_signed_mpi(list, mpi.value()); } fn addp(list: &mut Vec, checksum: &mut u16, mpi: &mpi::ProtectedMPI) { add_signed_mpi(list, mpi.value()); use openpgp::serialize::MarshalInto; *checksum = checksum.wrapping_add( mpi.to_vec().expect("infallible").iter() .fold(0u16, |acc, v| acc.wrapping_add(*v as u16))); } fn add_raw(list: &mut Vec, name: &str, data: &[u8]) { list.push(Sexp::String(name.into())); list.push(Sexp::String(data.into())); } use openpgp::crypto::mpi::{self, PublicKey}; let mut skey = vec![Sexp::String("skey".into())]; let curve = match key.mpis() { PublicKey::RSA { e, n, } => { add(&mut skey, n); add(&mut skey, e); None }, PublicKey::DSA { p, q, g, y, } => { add(&mut skey, p); add(&mut skey, q); add(&mut skey, g); add(&mut skey, y); None }, PublicKey::ElGamal { p, g, y, } => { add(&mut skey, p); add(&mut skey, g); add(&mut skey, y); None }, PublicKey::EdDSA { curve, q, } | PublicKey::ECDSA { curve, q, } | PublicKey::ECDH { curve, q, .. } => { add(&mut skey, q); Some(curve.clone()) }, PublicKey::Unknown { mpis, rest, } => { for m in mpis.iter() { add(&mut skey, m); } add_raw(&mut skey, "_", rest); None }, _ => return Err(openpgp::Error::UnsupportedPublicKeyAlgorithm(key.pk_algo()) .into()), }; // Now we append the secret bits. We also compute a checksum over // the MPIs. let mut checksum = 0u16; let protection = match key.secret() { SecretKeyMaterial::Encrypted(e) => { let mut p = vec![Sexp::String("protection".into())]; p.push(Sexp::String(match e.checksum() { Some(SecretKeyChecksum::SHA1) => "sha1", Some(SecretKeyChecksum::Sum16) => "sum", None => "none", // XXX: does that happen? }.into())); p.push(Sexp::String(babel::Fish(e.algo()).to_string().as_str().into())); let iv_len = e.algo().block_size().unwrap_or(0); let iv = e.ciphertext().map(|c| &c[..iv_len.min(c.len())]) .unwrap_or(&[]); p.push(Sexp::String(iv.into())); #[allow(deprecated)] match e.s2k() { S2K::Iterated { hash, salt, hash_bytes, } => { p.push(Sexp::String("3".into())); p.push(Sexp::String(babel::Fish(*hash).to_string().as_str().into())); p.push(Sexp::String(salt[..].into())); p.push(Sexp::String( utils::s2k_encode_iteration_count(*hash_bytes) .unwrap_or_default().to_string().as_str().into())); }, S2K::Salted { hash, salt } => { p.push(Sexp::String("1".into())); p.push(Sexp::String(babel::Fish(*hash).to_string().as_str().into())); p.push(Sexp::String(salt[..].into())); p.push(Sexp::String("0".into())); }, S2K::Simple { hash } => { p.push(Sexp::String("0".into())); p.push(Sexp::String(babel::Fish(*hash).to_string().as_str().into())); p.push(Sexp::String([][..].into())); p.push(Sexp::String("0".into())); }, S2K::Private { .. } | S2K::Unknown { .. } | _ => { return Err(anyhow::anyhow!("Unsupported protection mode").into()); }, } if let Ok(c) = e.ciphertext() { skey.push(Sexp::String("e".into())); // We must omit the IV here. skey.push(Sexp::String(c[iv_len.min(c.len())..].into())); } else { return Err(anyhow::anyhow!("Failed to parse ciphertext").into()); } Sexp::List(p) }, SecretKeyMaterial::Unencrypted(u) => { u.map(|s| match s { mpi::SecretKeyMaterial::RSA { d, p, q, u, } => { addp(&mut skey, &mut checksum, d); addp(&mut skey, &mut checksum, p); addp(&mut skey, &mut checksum, q); addp(&mut skey, &mut checksum, u); }, mpi::SecretKeyMaterial::DSA { x, } | mpi::SecretKeyMaterial::ElGamal { x, } => addp(&mut skey, &mut checksum, x), mpi::SecretKeyMaterial::EdDSA { scalar, } | mpi::SecretKeyMaterial::ECDSA { scalar, } | mpi::SecretKeyMaterial::ECDH { scalar, } => addp(&mut skey, &mut checksum, scalar), mpi::SecretKeyMaterial::Unknown { mpis, rest, } => { for m in mpis.iter() { addp(&mut skey, &mut checksum, m); } add_raw(&mut skey, "_", rest); checksum = checksum.wrapping_add( rest.iter() .fold(0u16, |acc, v| acc.wrapping_add(*v as u16))); }, _ => (), // XXX This will fail anyway. }); s("protection", "none") }, }; let mut transfer_key = vec![ Sexp::String("openpgp-private-key".into()), s("version", key.version()), s("algo", babel::Fish(key.pk_algo())), // XXX does that map correctly? ]; if let Some(curve) = curve { transfer_key.push(s("curve", curve.to_string())); } transfer_key.push(Sexp::List(skey)); transfer_key.push(s("csum", checksum.wrapping_add(csum_offset))); transfer_key.push(protection); let transfer_key = Sexp::List(transfer_key); // Pad to a multiple of 64 bits so that we can AESWRAP it. let mut buf = Vec::new(); transfer_key.serialize(&mut buf)?; while buf.len() % 8 > 0 { buf.push(0); } let padded_transfer_key = Protected::from(buf); self.send_simple( format!("SETKEYDESC {}", escape(Self::make_import_prompt(policy, cert, key)))).await?; // Get the Key Encapsulation Key for transferring the key. let kek = self.send_simple("KEYWRAP_KEY --import").await?; // Now encrypt the key. let encrypted_transfer_key = openpgp::crypto::ecdh::aes_key_wrap( openpgp::types::SymmetricAlgorithm::AES128, &kek, &padded_transfer_key)?; assert_eq!(padded_transfer_key.len() + 8, encrypted_transfer_key.len()); // Did we import it? let mut imported = false; // And send it! if let Some(mode) = self.pinentry_mode.as_ref().map(|m| m.to_string()) { self.send_simple( format!("OPTION pinentry-mode={}", mode)).await?; } self.send(format!("IMPORT_KEY --timestamp={}{}{}", chrono::DateTime::::from(key.creation_time()) .format("%Y%m%dT%H%M%S"), if unattended { " --unattended" } else { "" }, if overwrite { " --force" } else { "" }, ))?; while let Some(response) = self.next().await { match response? { Response::Ok { .. } | Response::Comment { .. } | Response::Status { .. } => (), // Ignore. Response::Inquire { keyword, .. } => { match keyword.as_str() { "KEYDATA" => { self.data(&encrypted_transfer_key)?; // Dummy read to send data. self.next().await; // Then, handle the inquiry. while let Some(r) = self.next().await { match r? { // May send CACHE_NONCE Response::Status { .. } => (), // Ignore. Response::Ok { .. } => { imported = true; break; }, // May send PINENTRY_LAUNCHED when // importing locked keys. Response::Inquire { .. } => self.acknowledge_inquiry().await?, Response::Error { code, message } => { match code { 0x4008023 => { // Key exists. return self.operation_failed_as( Error::KeyExists( keygrip.clone(), message.unwrap_or_else(|| { "key exists".into() })).into() ).await; } _ => { return self.operation_failed( &message).await; }, } }, response => return protocol_error(&response), } } // Sending the data acknowledges the inquiry. }, _ => self.acknowledge_inquiry().await?, } }, Response::Error { ref message, .. } => return self.operation_failed(message).await, response => return protocol_error(&response), } } Ok(imported) } fn make_import_prompt(policy: &dyn Policy, cert: &Cert, key: &Key) -> String { let primary_id = cert.keyid(); let keyid = key.keyid(); let uid = utils::best_effort_primary_uid(policy, cert); match (primary_id == keyid, Some(uid)) { (true, Some(uid)) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {}.", uid, keyid, Timestamp::try_from(key.creation_time()) .expect("creation time is representable"), ), (false, Some(uid)) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {} (main key ID {}).", uid, keyid, Timestamp::try_from(key.creation_time()) .expect("creation time is representable"), primary_id, ), (true, None) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {}.", keyid, Timestamp::try_from(key.creation_time()) .expect("creation time is representable"), ), (false, None) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {} (main key ID {}).", keyid, Timestamp::try_from(key.creation_time()) .expect("creation time is representable"), primary_id, ), } } /// Presets the password for a key. /// /// This does not check that the password is correct. /// /// This will fail if the user has not enabled presetting /// passwords. In particular, the user must explicitly opt-in by /// adding `allow-preset-passphrase` to their `gpg-agent.conf`. /// If this is not set, which will usually be the case since it is /// not the default, this operation will fail and return /// [`assuan::Error::OperationFailed`]. pub async fn preset_passphrase(&mut self, keygrip: &Keygrip, password: Password) -> Result<()> { let escaped = password .map(|p| { p.iter().map(|b| format!("{:02X}", b)) .collect::() }); self.send_simple( format!("PRESET_PASSPHRASE {} -1 {}", keygrip, escaped)).await .map_err(|err| { err })?; Ok(()) } /// Makes the agent ask for a password. /// /// The callback is invoked once for each response. If the /// callback returns an error, any outstanding inquiry is /// acknowledged, and the error is returned. /// /// The result of the callback is only used if gpg-agent sent an /// inquiry. Otherwise, the result is silently ignored. /// /// If the response to an inquiry is `Ok(None)`, the inquiry is /// acknowledged, and this function waits for another response. pub async fn get_passphrase

(&mut self, cache_id: &Option, err_msg: &Option, prompt: Option, desc_msg: Option, newsymkey: bool, repeat: usize, check: bool, qualitybar: bool, mut pinentry_cb: P) -> Result where P: FnMut(&mut Agent, Response) -> Result>, { self.send(format!( "GET_PASSPHRASE --data --repeat={}{}{}{} -- {} {} {} {}", repeat, if (repeat > 0 && check) || newsymkey { " --check" } else { "" }, if qualitybar { " --qualitybar" } else { "" }, if newsymkey { " --newsymkey" } else { "" }, cache_id.as_ref().map(escape).unwrap_or_else(|| "X".into()), err_msg.as_ref().map(escape).unwrap_or_else(|| "X".into()), prompt.as_ref().map(escape).unwrap_or_else(|| "X".into()), desc_msg.as_ref().map(escape).unwrap_or_else(|| "X".into()), ))?; let mut password = Vec::new(); while let Some(response) = self.next().await { match response? { r @ Response::Ok { .. } | r @ Response::Comment { .. } | r @ Response::Status { .. } => { pinentry_cb(self, r)?; }, r @ Response::Inquire { .. } => { match pinentry_cb(self, r) { Ok(Some(data)) => { self.data(&data[..])?; // Dummy read to send data. while let Some(r) = self.next().await { if matches!(r?, Response::Ok { .. }) { break; } } // Sending the data acknowledges the inquiry. } Ok(None) => { self.acknowledge_inquiry().await?; } Err(err) => { self.acknowledge_inquiry().await?; return Err(err); } } }, Response::Data { partial } => { // Securely erase partial. let partial = Protected::from(partial); password.extend_from_slice(&partial); }, Response::Error { ref message, .. } => return self.operation_failed(message).await, } } let password = Password::from(password); Ok(password) } /// Makes the agent forget a password. /// /// The cache_id is usually a keygrip, but gpg-agent can also /// cache the password for a SKESK. /// /// When a passphrase is cleared, `gpg-agent` launches a pinentry /// and sends it the `CLEARPASSPHRASE` command so that it also /// flushes the passphrase from its cache, if any. You can see /// whether gpg does that by providing a callback. It will be /// called for `PINENTRY_LAUNCHED` inquire line. Most of the time /// you won't care and can just pass `|_| ()`. pub async fn forget_passphrase(&mut self, cache_id: C, mut pinentry_cb: P) -> Result<()> where C: AsRef, P: FnMut(Vec), { self.send(format!("CLEAR_PASSPHRASE {}", escape(cache_id.as_ref())))?; while let Some(response) = self.next().await { match response? { Response::Ok { .. } | Response::Comment { .. } | Response::Status { .. } => (), // Ignore. Response::Inquire { keyword, parameters } => { match keyword.as_str() { "PINENTRY_LAUNCHED" => { pinentry_cb(parameters.unwrap_or_default()); }, _ => (), } self.acknowledge_inquiry().await? }, Response::Error { ref message, .. } => return self.operation_failed(message).await, response => return protocol_error(&response), } } Ok(()) } /// Helper function to respond to inquiries. /// /// This function doesn't do something useful, but it ends the /// current inquiry. async fn acknowledge_inquiry(&mut self) -> Result<()> { self.send("END")?; self.next().await; // Dummy read to send END. Ok(()) } /// Returns a convenient Err value for use in the state machines. /// /// This function must only be called after the assuan server /// returns an ERR. message is the error message returned from /// the server. This function first checks that the server hasn't /// sent anything else, which would be a protocol violation. If /// that is not the case, it turns the message into an Err. async fn operation_failed(&mut self, message: &Option) -> Result { self.operation_failed_as( assuan::Error::OperationFailed( message.as_ref().map(|e| e.to_string()) .unwrap_or_else(|| "Unknown reason".into())) .into()).await } async fn operation_failed_as(&mut self, err: Error) -> Result { tracer!(TRACE, "operation_failed_as"); if let Some(response) = self.next().await { t!("Got unexpected response {:?}", response); Err(assuan::Error::ProtocolError( format!("Got unexpected response {:?}", response)) .into()) } else { t!("Operation failed: {}", err); Err(err) } } /// Computes options that we want to communicate. fn options() -> Vec { use std::env::var; let mut r = Vec::new(); if let Ok(tty) = var("GPG_TTY") { r.push(format!("OPTION ttyname={}", tty)); } else { #[cfg(unix)] unsafe { use std::ffi::CStr; let tty = libc::ttyname(0); if ! tty.is_null() { if let Ok(tty) = CStr::from_ptr(tty).to_str() { r.push(format!("OPTION ttyname={}", tty)); } } } } if let Ok(term) = var("TERM") { r.push(format!("OPTION ttytype={}", term)); } if let Ok(display) = var("DISPLAY") { r.push(format!("OPTION display={}", display)); } if let Ok(xauthority) = var("XAUTHORITY") { r.push(format!("OPTION xauthority={}", xauthority)); } if let Ok(dbus) = var("DBUS_SESSION_BUS_ADDRESS") { r.push(format!("OPTION putenv=DBUS_SESSION_BUS_ADDRESS={}", dbus)); } // We're going to pop() options off the end, therefore reverse // the vec here to preserve the above ordering, which is the // one GnuPG uses. r.reverse(); r } /// Returns a `KeyPair` for `key` with the secret bits managed by /// the agent. /// /// `KeyPair` implements `crypto::Signer` and `crypto::Decryptor`. /// This makes it easy for code to use secret key material managed /// by an instance of gpg agent from code that uses Sequoia. /// /// This function does not check that the agent actually manages /// the secret key material. If the agent doesn't manage the /// secret key material, operations that attempt to use it will /// fail. If necessary, you can use [`Agent::has_key`] to check /// if the agent manages the secret key material. pub fn keypair(&self, key: &Key) -> Result where R: KeyRole { let mut pair = KeyPair::new_for_socket(&self.socket_path, key)?; pair.pinentry_mode = self.pinentry_mode.clone(); Ok(pair) } /// Exports a secret key from the agent. /// /// `key` is the secret key that will be exported. pub async fn export(&mut self, key: Key) -> Result> { let keygrip = Keygrip::of(key.mpis())?; let kek = self.send_simple("KEYWRAP_KEY --export").await?; let encrypted_key = self.send_simple( format!("EXPORT_KEY {}", keygrip.to_string())).await?; let secret_key = openpgp::crypto::ecdh::aes_key_unwrap( openpgp::types::SymmetricAlgorithm::AES128, &kek, &encrypted_key.as_ref())?; // Strip any trailing NULs. They are only there for padding // purposes. let mut secret_key = secret_key.as_ref(); while ! secret_key.is_empty() && secret_key[secret_key.len() - 1] == 0 { secret_key = &secret_key[..secret_key.len() - 1]; } let sexp = Sexp::from_bytes(secret_key)?; let secret_key = sexp.to_secret_key(Some(key.mpis()))?; let (key, _) = key.add_secret(secret_key.into()); Ok(key) } } /// Returns a convenient Err value for use in the state machines /// below. fn protocol_error(response: &Response) -> Result { tracer!(TRACE, "operation_failed"); t!("Got unexpected response {:?}", response); Err(assuan::Error::ProtocolError( format!("Got unexpected response {:?}", response)) .into()) } /// Computes the cache id for a SKESK. /// /// If an S2K algorithm unsupported by the caching id algorithm is /// given, this function returns `None`. pub fn cacheid_of(s2k: &S2K) -> Option { #[allow(deprecated)] let salt = match s2k { S2K::Iterated { salt, .. } => &salt[..8], S2K::Salted { salt, .. } => &salt[..8], _ => return None, }; Some(format!("S{}", openpgp::fmt::hex::encode(&salt))) } /// Computes the cache id for a set of SKESKs. /// /// GnuPG prompts for a password for each SKESK separately, and uses /// the first eight bytes of salt from the S2K. We ask for one /// password and try it with every SKESK. Therefore, we have to cache /// that we asked for a set of SKESKs, i.e. this message. To that /// end, we xor the first eight bytes of salt from every S2K, matching /// GnuPG's result in the common case of having just one SKESK. Xor /// is also nice because it is commutative, so the order of the SKESKs /// doesn't matter. /// /// Unsupported SKESK versions or S2K algorithms unsupported by the /// caching id algorithm are ignored. We cannot use them anyway. /// /// Further, if no SKESKs are given, this function returns `None`. pub fn cacheid_over_all(skesks: &[SKESK]) -> Option { if skesks.is_empty() { return None; } let mut cacheid = [0; 8]; for skesk in skesks { let s2k = match skesk { SKESK::V4(skesk) => skesk.s2k(), _ => continue, }; #[allow(deprecated)] let salt = match s2k { S2K::Iterated { salt, .. } => &salt[..8], S2K::Salted { salt, .. } => &salt[..8], _ => continue, }; cacheid.iter_mut().zip(salt.iter()).for_each(|(p, s)| *p ^= *s); } Some(format!("S{}", openpgp::fmt::hex::encode(&cacheid))) } /// A cryptographic key pair. /// /// A `KeyPair` is a combination of public and secret key. This /// particular implementation does not have the secret key, but /// diverges the cryptographic operations to `gpg-agent`. /// /// This provides a convenient, synchronous interface for use with the /// low-level Sequoia crate. pub struct KeyPair { public: Key, agent_socket: PathBuf, password: Option, pinentry_mode: Option, password_prompt: String, delete_prompt: String, } impl KeyPair { fn default_password_prompt(key: &Key) -> String { format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {}.", key.keyid(), Timestamp::try_from(key.creation_time()).unwrap()) } fn default_delete_prompt(key: &Key) -> String { format!( "Do you really want to permanently delete the OpenPGP secret key:\n\ ID {:X}, created {}.", key.keyid(), Timestamp::try_from(key.creation_time()).unwrap()) } /// Returns a `KeyPair` for `key` with the secret bits managed by /// the agent. /// /// This provides a convenient, synchronous interface for use with /// the low-level Sequoia crate. pub fn new_for_gnupg_context(ctx: &gnupg::Context, key: &Key) -> Result where R: KeyRole { let key = key.role_as_unspecified(); Ok(KeyPair { password: None, pinentry_mode: None, password_prompt: Self::default_password_prompt(key), delete_prompt: Self::default_delete_prompt(key), public: key.clone(), agent_socket: ctx.socket("agent")?.into(), }) } /// Returns a `KeyPair` for `key` with the secret bits managed by /// the agent. /// /// If you have a [`Agent`], then you should create a `KeyPair` /// using [`Agent::keypair`]. pub fn new_for_socket(agent_socket: P, key: &Key) -> Result where P: AsRef, R: KeyRole { let key = key.role_as_unspecified(); Ok(KeyPair { password: None, pinentry_mode: None, password_prompt: Self::default_password_prompt(key), delete_prompt: Self::default_delete_prompt(key), public: key.clone(), agent_socket: agent_socket.as_ref().to_path_buf(), }) } /// Changes the password prompt to include information about the /// cert. /// /// Use this function to give more context to the user when she is /// prompted for a password. This function will generate a prompt /// that is very similar to the prompts that GnuPG generates. /// /// To set an arbitrary password prompt, use /// [`KeyPair::with_password_prompt`]. pub fn with_cert(self, cert: &ValidCert) -> Self { let primary_id = cert.keyid(); let keyid = self.public.keyid(); let password_prompt = match (primary_id == keyid, cert.primary_userid() .map(|uid| uid.clone()) .ok()) { (true, Some(uid)) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {}.", uid.userid(), keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), ), (false, Some(uid)) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {} (main key ID {}).", uid.userid(), keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), primary_id, ), (true, None) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {}.", keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), ), (false, None) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {} (main key ID {}).", keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), primary_id, ), }; let delete_prompt = match cert.primary_userid() .map(|uid| uid.clone()) .ok() { Some(uid) => format!( "Do you really want to permanently delete the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {}.", uid.userid(), keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), ), None => format!( "Do you really want to permanently delete the OpenPGP secret key:\n\ ID {:X}, created {}.", keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), ), }; self.with_password_prompt(password_prompt) .with_delete_prompt(delete_prompt) } /// Supplies a password to unlock the secret key. /// /// This will be used when the secret key operation is performed, /// e.g. when signing or decrypting a message. /// /// Note: This is the equivalent of GnuPG's /// `--pinentry-mode=loopback` and requires explicit opt-in in the /// gpg-agent configuration using the `allow-loopback-pinentry` /// option. If this is not enabled in the agent, the secret key /// operation will fail. It is likely only useful during testing. pub fn with_password(mut self, password: crypto::Password) -> Self { self.password = Some(password); self } /// Overrides the pinentry mode. pub fn set_pinentry_mode(mut self, mode: PinentryMode) -> Self { self.pinentry_mode = Some(mode); self } /// Disables gpg's pinentry. /// /// Changes the pinentry mode to `PinEntryMode::Error`, which /// configures the agent to not ask for a password. pub fn suppress_pinentry(mut self) -> Self { self.pinentry_mode = Some(PinentryMode::Error); self } /// Changes the password prompt. /// /// Use this function to give more context to the user when she is /// prompted for a password. /// /// To set an password prompt that uses information from the /// OpenPGP certificate, use [`KeyPair::with_cert`]. pub fn with_password_prompt(mut self, prompt: String) -> Self { self.password_prompt = prompt; self } /// Changes the delete prompt. /// /// Use this function to give more context to the user when she is /// prompted to delete a key. /// /// To set an password prompt that uses information from the /// OpenPGP certificate, use [`KeyPair::with_cert`]. pub fn with_delete_prompt(mut self, prompt: String) -> Self { self.delete_prompt = prompt; self } /// Changes the key's password. /// /// If `preset_password` is true, then the password will also be /// added to the password cache. pub async fn password(&mut self, preset_password: bool) -> Result<()> { let keygrip = Keygrip::of(self.public.mpis())?; // Connect to the agent. let mut agent = Agent::connect_to_agent(&self.agent_socket).await?; for option in Agent::options() { agent.send_simple(option).await?; } if let Some(mode) = agent.pinentry_mode.clone() { agent.send_simple( format!("OPTION pinentry-mode={}", mode.as_str())).await?; } agent.send_simple( format!("SETKEYDESC {}", assuan::escape(&self.password_prompt))).await?; agent.send_simple( format!("PASSWD {}{}", if preset_password { "--preset " } else { "" }, keygrip.to_string())) .await?; Ok(()) } /// Deletes the specified key. /// /// If `stub_only` is true, then the key will be removed if it is /// a stub that corresponds to a key on a token. pub async fn delete_key(&mut self, stub_only: bool) -> Result<()> { let keygrip = Keygrip::of(self.public.mpis())?; // Connect to the agent. let mut agent = Agent::connect_to_agent(&self.agent_socket).await?; for option in Agent::options() { agent.send_simple(option).await?; } if let Some(mode) = agent.pinentry_mode.clone() { agent.send_simple( format!("OPTION pinentry-mode={}", mode.as_str())).await?; } agent.send_simple( format!("SETKEYDESC {}", assuan::escape(&self.delete_prompt))).await?; agent.send_simple( format!("DELETE_KEY {}{}", if stub_only { "--stub-only " } else { "" }, keygrip.to_string())) .await?; Ok(()) } } impl KeyPair { /// Signs a message. /// /// An async implementation of /// [`sequoia_openpgp::crypto::Signer::sign`]. pub async fn sign_async(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result { // Connect to the agent and do the signing // operation. let mut agent = Agent::connect_to_agent(&self.agent_socket).await?; for option in Agent::options() { agent.send_simple(option).await?; } if self.password.is_some() { agent.send_simple("OPTION pinentry-mode=loopback").await?; } else if let Some(ref mode) = self.pinentry_mode { agent.send_simple( format!("OPTION pinentry-mode={}", mode.as_str())).await?; } let grip = Keygrip::of(self.public.mpis())?; agent.send_simple(format!("SIGKEY {}", grip)).await?; agent.send_simple( format!("SETKEYDESC {}", assuan::escape(&self.password_prompt))).await?; let algo = u8::from(hash_algo); let digest = hex::encode(&digest); agent.send_simple(format!("SETHASH {} {}", algo, digest)).await?; agent.send("PKSIGN")?; let mut data = Vec::new(); while let Some(r) = agent.next().await { match r? { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Inquire { keyword, .. } => match (keyword.as_str(), &self.password) { ("PASSPHRASE", Some(p)) => { p.map(|p| agent.data(p))?; // Dummy read to send the data. agent.next().await; }, _ => agent.acknowledge_inquiry().await?, }, assuan::Response::Error { ref message, .. } => return Ok(agent.operation_failed(message).await?), assuan::Response::Data { ref partial } => data.extend_from_slice(partial), } } Sexp::from_bytes(&data)?.to_signature() } } impl crypto::Signer for KeyPair { fn public(&self) -> &Key { &self.public } fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result { use tokio::runtime::{Handle, Runtime}; // See if the current thread is managed by a tokio // runtime. if Handle::try_current().is_err() { // Doesn't seem to be the case, so it is safe // to create a new runtime and block. let rt = Runtime::new()?; rt.block_on(self.sign_async(hash_algo, digest)) } else { // It is! We must not create a second runtime // on this thread, but instead we will // delegate this to a new thread. std::thread::scope(|s| { s.spawn(move || { let rt = Runtime::new()?; rt.block_on(self.sign_async(hash_algo, digest)) }).join() }).map_err(map_panic)? } } } impl KeyPair { /// Decrypts a message. /// /// An async implementation of /// [`sequoia_openpgp::crypto::Decryptor::decrypt`]. pub async fn decrypt_async(&mut self, ciphertext: &crypto::mpi::Ciphertext, plaintext_len: Option) -> openpgp::Result { // Connect to the agent and do the decryption operation. let mut agent = Agent::connect_to_agent(&self.agent_socket).await?; for option in Agent::options() { agent.send_simple(option).await?; } if self.password.is_some() { agent.send_simple("OPTION pinentry-mode=loopback").await?; } else if let Some(ref mode) = self.pinentry_mode { agent.send_simple( format!("OPTION pinentry-mode={}", mode.as_str())).await?; } let grip = Keygrip::of(self.public.mpis())?; agent.send_simple(format!("SETKEY {}", grip)).await?; agent.send_simple(format!("SETKEYDESC {}", assuan::escape(&self.password_prompt))).await?; agent.send("PKDECRYPT")?; let mut padding = true; let mut data = Vec::new(); while let Some(r) = agent.next().await { match r? { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } => (), // Ignore. assuan::Response::Inquire { ref keyword, .. } => match (keyword.as_str(), &self.password) { ("PASSPHRASE", Some(p)) => { p.map(|p| agent.data(p))?; // Dummy read to send the data. agent.next().await; }, ("CIPHERTEXT", _) => { let mut buf = Vec::new(); Sexp::try_from(ciphertext)?.serialize(&mut buf)?; agent.data(&buf)?; // Dummy read to send the data. agent.next().await; }, _ => agent.acknowledge_inquiry().await?, }, assuan::Response::Status { ref keyword, ref message } => if keyword == "PADDING" { padding = message != "0"; }, assuan::Response::Error { ref message, .. } => return Ok(agent.operation_failed(message).await?), assuan::Response::Data { ref partial } => data.extend_from_slice(partial), } } // Get rid of the safety-0. // // gpg-agent seems to add a trailing 0, supposedly for good // measure. if data.iter().last() == Some(&0) { let l = data.len(); data.truncate(l - 1); } Sexp::from_bytes(&data)?.finish_decryption( &self.public, ciphertext, plaintext_len, padding) } } impl crypto::Decryptor for KeyPair { fn public(&self) -> &Key { &self.public } fn decrypt(&mut self, ciphertext: &crypto::mpi::Ciphertext, plaintext_len: Option) -> openpgp::Result { use tokio::runtime::{Handle, Runtime}; // See if the current thread is managed by a tokio // runtime. if Handle::try_current().is_err() { // Doesn't seem to be the case, so it is safe // to create a new runtime and block. let rt = Runtime::new()?; rt.block_on(self.decrypt_async(ciphertext, plaintext_len)) } else { // It is! We must not create a second runtime // on this thread, but instead we will // delegate this to a new thread. std::thread::scope(|s| { s.spawn(move || { let rt = Runtime::new()?; rt.block_on(self.decrypt_async(ciphertext, plaintext_len)) }).join() }).map_err(map_panic)? } } } /// Maps a panic of a worker thread to an error. /// /// Unfortunately, there is nothing useful to do with the error, but /// returning a generic error is better than panicking. fn map_panic(_: Box) -> anyhow::Error { anyhow::anyhow!("worker thread panicked") } #[cfg(test)] mod test { use super::*; use std::fs::File; use std::io::Write; use anyhow::Context; use openpgp::{ Cert, crypto::{ hash::Digest, mpi::Ciphertext, Decryptor, Signer, }, policy::StandardPolicy, }; use ipc::Keygrip; const P: &StandardPolicy = &StandardPolicy::new(); async fn import_keys(agent: &mut Agent, cert: &Cert) -> Result<()> { assert!(cert.is_tsk(), "{} does not contain secret key material", cert.fingerprint()); for k in cert.keys().secret() { agent.import(P, &cert, k.parts_as_secret().expect("have secret"), true, true).await?; } Ok(()) } async fn import_testy_new(agent: &mut Agent) -> Result { let cert = Cert::from_bytes(crate::tests::key("testy-new-private.pgp"))?; import_keys(agent, &cert).await?; Ok(cert) } /// Asserts that a is usable from an async /// context. /// /// Previously, the test died with /// /// thread 'gnupg::tests::signer_in_async_context' panicked at /// 'Cannot start a runtime from within a runtime. This /// happens because a function (like `block_on`) attempted to /// block the current thread while the thread is being used to /// drive asynchronous tasks.' #[test] fn signer_in_async_context() -> Result<()> { async fn async_context() -> Result<()> { let ctx = match gnupg::Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let key = Cert::from_bytes(crate::tests::key("testy-new.pgp"))? .primary_key().key().clone(); let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?; let algo = HashAlgorithm::default(); let digest = algo.context()?.into_digest()?; let _ = pair.sign(algo, &digest); Ok(()) } let rt = tokio::runtime::Runtime::new()?; rt.block_on(async_context()) } /// Asserts that a is usable from an async /// context. /// /// Previously, the test died with /// /// thread 'gnupg::tests::decryptor_in_async_context' panicked /// at 'Cannot start a runtime from within a runtime. This /// happens because a function (like `block_on`) attempted to /// block the current thread while the thread is being used to /// drive asynchronous tasks.' #[test] fn decryptor_in_async_context() -> Result<()> { async fn async_context() -> Result<()> { let ctx = match gnupg::Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let key = Cert::from_bytes(crate::tests::key("testy-new.pgp"))? .keys().nth(1).unwrap().key().clone(); let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?; let ciphertext = Ciphertext::ECDH { e: vec![].into(), key: vec![].into_boxed_slice(), }; let _ = pair.decrypt(&ciphertext, None); Ok(()) } let rt = tokio::runtime::Runtime::new()?; rt.block_on(async_context()) } // Disabled on Windows. Re-enable when this issue is fixed: // // https://gitlab.com/sequoia-pgp/sequoia/-/issues/1093 // // SEE OTHER INSTANCES OF THIS ABOVE (search for comment). #[cfg(not(windows))] #[tokio::test] async fn non_existent_home_directory() -> Result<()> { let tempdir = tempfile::tempdir()?; let homedir = tempdir.path().join("foo"); let result = Agent::connect_to(&homedir).await; match result { Ok(_agent) => panic!("Created an agent for a non-existent home!"), Err(err) => { if let Error::GnuPGHomeMissing(_) = err { // Correct error. } else { panic!("Expected Error::GnuPGHomeMissing, got: {}", err); } } } Ok(()) } // Test that we can import a key and sign a message. #[tokio::test] async fn import_key_and_sign() -> Result<()> { let ctx = match gnupg::Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let mut agent = Agent::connect(&ctx).await?; let testy = import_testy_new(&mut agent).await?; let key = testy.primary_key().key(); let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)?; let algo = HashAlgorithm::default(); let digest = algo.context()?.into_digest()?; if let Err(err) = pair.sign(algo, &digest) { panic!("Signing: {}", err); } Ok(()) } // Test that we can list imported keys. #[tokio::test] async fn list_keys() -> Result<()> { use std::collections::HashSet; let ctx = match gnupg::Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let mut agent = Agent::connect(&ctx).await?; let keys = agent.list_keys().await.expect("list keys works"); assert_eq!(keys.len(), 0); let testy = import_testy_new(&mut agent).await?; let testy_keygrips = testy.keys().into_iter() .map(|ka| Keygrip::of(ka.key().mpis())) .collect::, _>>() .expect("can compute keygrip"); assert!(testy_keygrips.len() > 0); let keys = agent.list_keys().await.expect("list keys works"); let keys = keys.into_iter() .map(|k| k.keygrip().clone()) .collect::>(); assert_eq!(testy_keygrips, keys); Ok(()) } // Test Agent::key_info. #[tokio::test] async fn key_info() -> Result<()> { use std::collections::HashSet; let ctx = match gnupg::Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let mut agent = Agent::connect(&ctx).await?; let testy = import_testy_new(&mut agent).await?; let testy_keygrips = testy.keys().into_iter() .map(|ka| Keygrip::of(ka.key().mpis())) .collect::, _>>() .expect("can compute keygrip"); assert!(testy_keygrips.len() > 0); for ka in testy.keys() { let keygrip = Keygrip::of(ka.key().mpis()).expect("has a keygrip"); match agent.key_info(&keygrip).await { Ok(info) => { assert_eq!(info.keygrip(), &keygrip); } Err(err) => { panic!("Getting keyinfo for {}: {}", keygrip, err); } } } Ok(()) } // Test Agent::preset_passphrase // // Disabled on Windows. Re-enable when this issue is fixed: // // https://gitlab.com/sequoia-pgp/sequoia/-/issues/1093 // // SEE OTHER INSTANCES OF THIS ABOVE (search for comment). #[cfg(not(windows))] #[tokio::test] async fn preset_passphrase() -> Result<()> { trace(true); tracer!(TRACE, "preset_passphrase"); async fn test(file: &str, password: &str) -> Result<()> { let ctx = match gnupg::Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let mut agent = Agent::connect(&ctx).await?; let cert = Cert::from_bytes(crate::tests::key(file))?; import_keys(&mut agent, &cert).await?; let vc = cert.with_policy(P, None).expect("valid cert"); let key = vc.keys().for_signing().next().expect("have signing key"); let key_keygrip = Keygrip::of(key.mpis()).expect("has a keygrip"); t!("Testing with {} ({})", key.fingerprint(), key_keygrip); t!("gpg knows about:"); let mut saw = false; for (i, info) in agent.list_keys().await.expect("works").iter().enumerate() { t!("{}. {}: {:?}, password cached: {}", i + 1, info.keygrip(), info.protection(), info.passphrase_cached()); if info.keygrip() == &key_keygrip { saw = true; } } assert!(saw, "Failed to load test key"); // Try with allow-preset-passphrase not set and an // incorrect password. This should fail. let r = agent.preset_passphrase(&key_keygrip, "wrong".into()).await; match r { Ok(()) => panic!("preset_passphrase should fail if \ 'allow-preset-passphrase' is not set."), Err(err) => { if let Error::Assuan(assuan::Error::OperationFailed(_)) = err { // That's what we expect. } else { panic!("Expected assuan::Error::OperationFailed, \ but got {}", err); } } } // Set allow-preset-passphrase. let dot_gnupg = ctx.homedir().expect("have a homedir"); let conf = dot_gnupg.join("gpg-agent.conf"); let mut f = File::options().create(true).append(true).open(&conf) .with_context(|| { format!("Opening {}", conf.display()) }).expect("can open gpg-agent.conf"); writeln!(&mut f, "allow-preset-passphrase").expect("can write"); drop(f); agent.reload().await.expect("valid"); // Try with allow-preset-passphrase set, but still an // incorrect password. This should still fail. let r = agent.preset_passphrase( &key_keygrip, "this is the wrong passphrase".into()).await; match r { Ok(()) => (), Err(err) => { panic!("Failed to set password: {}", err); } } let algo = HashAlgorithm::default(); let digest = algo.context()?.into_digest()?; let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)? // Don't prompt for the passphrase. .suppress_pinentry(); match pair.sign(algo, &digest) { Ok(_) => { panic!("Signing should have failed"); } Err(err) => { t!("Signing failed (expected): {}", err); } } // Finally, try to sign a message using the correct // passphrase. let r = agent.preset_passphrase(&key_keygrip, password.into()).await; match r { Ok(()) => (), Err(err) => { panic!("Failed to set password: {}", err); } } t!("gpg-agent's key info:"); for (i, info) in agent.list_keys().await.unwrap().iter().enumerate() { t!(" {}. {}: {:?}, password cached: {}", i + 1, info.keygrip(), info.protection(), info.passphrase_cached()); } let algo = HashAlgorithm::default(); let digest = algo.context()?.into_digest()?; let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)? // Don't prompt for the passphrase. .suppress_pinentry(); match pair.sign(algo, &digest) { Ok(_) => { } Err(err) => { panic!("Signing failed (unexpected): {}", err); } } // Now, forget the passphrase and try to use the key. It // should fail again. agent.forget_passphrase(&key_keygrip.to_string(), |_| ()).await .expect("can forget"); let algo = HashAlgorithm::default(); let digest = algo.context()?.into_digest()?; let mut pair = KeyPair::new_for_gnupg_context(&ctx, &key)? // Don't prompt for the passphrase. .suppress_pinentry(); match pair.sign(algo, &digest) { Ok(_) => { panic!("Signing should have failed"); } Err(err) => { t!("Signing failed (expected): {}", err); } } Ok(()) } test("password-xyzzy-private.pgp", "xyzzy").await.expect("all okay"); test("password-foospacespacebar-private.pgp", "foo bar").await.expect("all okay"); Ok(()) } // Test Agent::export #[tokio::test] async fn export() -> Result<()> { use std::collections::HashSet; let ctx = match gnupg::Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let mut agent = Agent::connect(&ctx).await?; let testy = import_testy_new(&mut agent).await?; let testy_keygrips = testy.keys().into_iter() .map(|ka| Keygrip::of(ka.key().mpis())) .collect::, _>>() .expect("can compute keygrip"); assert!(testy_keygrips.len() > 0); for ka in testy.keys().secret() { let keygrip = Keygrip::of(ka.key().mpis()).expect("has a keygrip"); match agent.export(ka.key().parts_as_public().clone()).await { Ok(key) => { assert_eq!(ka.key(), &key); } Err(err) => { panic!("Exporting key {}: {}", keygrip, err); } } } Ok(()) } } sequoia-gpg-agent-0.4.2/src/macros.rs000064400000000000000000000206061046102023000155710ustar 00000000000000macro_rules! trace_module { ( $I:ident ) => { /// Controls tracing in this module. pub fn trace(enable: bool) { $I.store(enable, std::sync::atomic::Ordering::Relaxed); } /// Returns whether tracing is enabled in this module. pub fn traced() -> bool { $I.load(std::sync::atomic::Ordering::Relaxed) } static $I: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); }; } macro_rules! trace { ( $TRACE:expr, $fmt:expr, $($pargs:expr),* ) => { if $TRACE.load(std::sync::atomic::Ordering::Relaxed) { eprintln!($fmt, $($pargs),*); } }; ( $TRACE:expr, $fmt:expr ) => { trace!($TRACE, $fmt, ); }; } // Converts an indentation level to whitespace. pub(crate) fn indent(i: isize) -> &'static str { let s = " "; &s[0..std::cmp::min(usize::try_from(i).unwrap_or(0), s.len())] } macro_rules! tracer { // Make tracer!(true, ...) work ( true, $func:expr ) => { tracer!(std::sync::atomic::AtomicBool::new(true), $func, 0) }; // as well as tracer!(false, ...). ( false, $func:expr ) => { tracer!(std::sync::atomic::AtomicBool::new(false), $func, 0) }; ( $TRACE:expr, $func:expr ) => { tracer!($TRACE, $func, 0) }; ( $TRACE:expr, $func:expr, $indent:expr ) => { // Currently, Rust doesn't support $( ... ) in a nested // macro's definition. See: // https://users.rust-lang.org/t/nested-macros-issue/8348/2 macro_rules! t { ( $fmt:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, $fmt) }; ( $fmt:expr, $a:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a)) }; ( $fmt:expr, $a:expr, $b:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr, $k:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k)) }; } } } /// Platform abstraction. /// /// Using this macro makes sure that missing support for new platform /// is a compile-time error. macro_rules! platform { { unix => $unix:expr, windows => $windows:expr $(,)? } => { if cfg!(unix) { #[cfg(unix)] { $unix } #[cfg(not(unix))] { unreachable!() } } else if cfg!(windows) { #[cfg(windows)] { $windows } #[cfg(not(windows))] { unreachable!() } } else { #[cfg(not(any(unix, windows)))] compile_error!("Unsupported platform"); unreachable!() } } } /// A simple shortcut for ensuring a type is send and sync. /// /// For most types just call it after defining the type: /// /// ```ignore /// pub struct MyStruct {} /// assert_send_and_sync!(MyStruct); /// ``` /// /// For types with lifetimes, use the anonymous lifetime: /// /// ```ignore /// pub struct WithLifetime<'a> { _p: std::marker::PhantomData<&'a ()> } /// assert_send_and_sync!(WithLifetime<'_>); /// ``` /// /// For a type generic over another type `W`, /// pass the type `W` as a where clause /// including a trait bound when needed: /// /// ```ignore /// pub struct MyWriter { _p: std::marker::PhantomData } /// assert_send_and_sync!(MyWriter where W: std::io::Write); /// ``` /// /// This will assert that `MyWriterStruct` is `Send` and `Sync` /// if `W` is `Send` and `Sync`. /// /// You can also combine the two and be generic over multiple types. /// Just make sure to list all the types - even those without additional /// trait bounds: /// /// ```ignore /// pub struct MyWriterWithLifetime<'a, C, W: std::io::Write> { /// _p: std::marker::PhantomData<&'a (C, W)>, /// } /// assert_send_and_sync!(MyWriterWithLifetime<'_, C, W> where C, W: std::io::Write); /// ``` /// /// If you need multiple additional trait bounds on a single type /// you can add them separated by `+` like in normal where clauses. /// However you have to make sure they are `Identifiers` like `Write`. /// In macro patterns `Paths` (like `std::io::Write`) may not be followed /// by `+` characters. // Note: We cannot test the macro in doctests, because the macro is // not public. We test the cases in the test module below, instead. // If you change the examples here, propagate the changes to the // module below. #[allow(unused_macros)] macro_rules! assert_send_and_sync { ( $x:ty where $( $g:ident$( : $a:path )? $(,)?)*) => { impl<$( $g ),*> crate::macros::Sendable for $x where $( $g: Send + Sync $( + $a )? ),* {} impl<$( $g ),*> crate::macros::Syncable for $x where $( $g: Send + Sync $( + $a )? ),* {} }; ( $x:ty where $( $g:ident$( : $a:ident $( + $b:ident )* )? $(,)?)*) => { impl<$( $g ),*> crate::macros::Sendable for $x where $( $g: Send + Sync $( + $a $( + $b )* )? ),* {} impl<$( $g ),*> crate::macros::Syncable for $x where $( $g: Send + Sync $( + $a $( + $b )* )? ),* {} }; ( $x:ty ) => { impl crate::macros::Sendable for $x {} impl crate::macros::Syncable for $x {} }; } pub(crate) trait Sendable : Send {} pub(crate) trait Syncable : Sync {} /// We cannot test the macro in doctests, because the macro is not /// public. We test the cases here, instead. If you change the /// examples here, propagate the changes to the docstring above. #[cfg(test)] mod test { /// For most types just call it after defining the type: pub struct MyStruct {} assert_send_and_sync!(MyStruct); /// For types with lifetimes, use the anonymous lifetime: pub struct WithLifetime<'a> { _p: std::marker::PhantomData<&'a ()> } assert_send_and_sync!(WithLifetime<'_>); /// For a type generic over another type `W`, pass the type `W` as /// a where clause including a trait bound when needed: pub struct MyWriter { _p: std::marker::PhantomData } assert_send_and_sync!(MyWriter where W: std::io::Write); /// This will assert that `MyWriterStruct` is `Send` and `Sync` /// if `W` is `Send` and `Sync`. /// /// You can also combine the two and be generic over multiple /// types. Just make sure to list all the types - even those /// without additional trait bounds: pub struct MyWriterWithLifetime<'a, C, W: std::io::Write> { _p: std::marker::PhantomData<&'a (C, W)>, } assert_send_and_sync!(MyWriterWithLifetime<'_, C, W> where C, W: std::io::Write); } sequoia-gpg-agent-0.4.2/src/tests.rs000064400000000000000000000025011046102023000154410ustar 00000000000000//! Test data for Sequoia. //! //! This module includes the test data from `tests/data` in a //! structured way. use std::fmt; use std::collections::BTreeMap; pub struct Test { path: &'static str, pub bytes: &'static [u8], } impl fmt::Display for Test { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "tests/data/{}", self.path) } } /// Returns the content of the given file below `openpgp/tests/data`. pub fn file(name: &str) -> &'static [u8] { lazy_static::lazy_static! { static ref FILES: BTreeMap<&'static str, &'static [u8]> = { let mut m: BTreeMap<&'static str, &'static [u8]> = Default::default(); macro_rules! add { ( $key: expr, $path: expr ) => { m.insert($key, include_bytes!($path)) } } include!(concat!(env!("OUT_DIR"), "/tests.index.rs.inc")); // Sanity checks. assert!(m.contains_key("keys/testy-new.pgp")); assert!(m.contains_key("keys/testy-new-private.pgp")); m }; } FILES.get(name).unwrap_or_else(|| panic!("No such file {:?}", name)) } /// Returns the content of the given file below `tests/data/keys`. pub fn key(name: &str) -> &'static [u8] { file(&format!("keys/{}", name)) } sequoia-gpg-agent-0.4.2/src/utils.rs000064400000000000000000000062231046102023000154440ustar 00000000000000//! Miscellaneous utilities. use std::process::Command; use anyhow::Result; use sequoia_openpgp as openpgp; use openpgp::{ Cert, policy::Policy, }; /// Best-effort heuristic to compute the primary User ID of a given cert. pub fn best_effort_primary_uid(policy: &dyn Policy, cert: &Cert) -> String { // Try to be more helpful by including a User ID in the // listing. We'd like it to be the primary one. Use // decreasingly strict policies. let mut primary_uid = None; // First, apply our policy. if let Ok(vcert) = cert.with_policy(policy, None) { if let Ok(primary) = vcert.primary_userid() { primary_uid = Some(primary.value().to_vec()); } } // Second, apply the null policy. if primary_uid.is_none() { let null = openpgp::policy::NullPolicy::new(); if let Ok(vcert) = cert.with_policy(&null, None) { if let Ok(primary) = vcert.primary_userid() { primary_uid = Some(primary.value().to_vec()); } } } // As a last resort, pick the first user id. if primary_uid.is_none() { if let Some(primary) = cert.userids().next() { primary_uid = Some(primary.value().to_vec()); } else { // Special case, there is no user id. primary_uid = Some(b"(NO USER ID)"[..].into()); } } String::from_utf8_lossy(&primary_uid.expect("set at this point")).into() } /// Converts S2K::Iterated's `hash_bytes` into coded count /// representation. /// /// # Errors /// /// Fails with `Error::InvalidArgument` if `hash_bytes` cannot be /// encoded. See also `S2K::nearest_hash_count()`. /// // Notes: Copied from S2K::encode_count. pub fn s2k_encode_iteration_count(hash_bytes: u32) -> Result { use openpgp::Error; // eeee.mmmm -> (16 + mmmm) * 2^(6 + e) let msb = 32 - hash_bytes.leading_zeros(); let (mantissa_mask, tail_mask) = match msb { 0..=10 => { return Err(Error::InvalidArgument( format!("S2K: cannot encode iteration count of {}", hash_bytes)).into()); } 11..=32 => { let m = 0b11_1100_0000 << (msb - 11); let t = 1 << (msb - 11); (m, t - 1) } _ => unreachable!() }; let exp = if msb < 11 { 0 } else { msb - 11 }; let mantissa = (hash_bytes & mantissa_mask) >> (msb - 5); if tail_mask & hash_bytes != 0 { return Err(Error::InvalidArgument( format!("S2K: cannot encode iteration count of {}", hash_bytes)).into()); } Ok(mantissa as u8 | (exp as u8) << 4) } #[allow(clippy::let_and_return)] pub(crate) fn new_background_command(program: S) -> Command where S: AsRef, { let command = Command::new(program); #[cfg(windows)] let command = { use std::os::windows::process::CommandExt; // see https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags const CREATE_NO_WINDOW: u32 = 0x08000000; let mut command = command; command.creation_flags(CREATE_NO_WINDOW); command }; command } sequoia-gpg-agent-0.4.2/tests/data/keys/password-foospacespacebar-private.pgp000064400000000000000000000054601046102023000255170ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 33F6 C6B3 8A65 BE94 4326 81AC 787F 037E 84D7 68CB Comment: Password xYYEZfrWDhYJKwYBBAHaRw8BAQdAm2ske3/zEGNjprEZn/xY7Jxhlr4IgT8ttSQ3 NCHGEe7+CQMIkZhjbHH8U8j/LEFiJXXgGo5UvJIVAORyCni0HqX3el2VjiAjXeah KSU0IpSIR0Gv8j9bS/5E4CMNu9zYlsm60NZn48G+4rBA1jVRg1KYysLACwQfFgoA fQWCZfrWDgMLCQcJEHh/A36E12jLRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZwg1Fj8FACxJLQpdH73jmfY2n3rCElntzhXvFHkSKJi/AxUK CAKbAQIeARYhBDP2xrOKZb6UQyaBrHh/A36E12jLAAA+mwEArZfQUC7HwsQyliG/ T4SR9WTcMoZqEeDdhILcIIk/n+YBAKxIxne/hBoQuYK/XnvLanV2UK1iH3fikoBm qcDkVUIOzSdQYXNzd29yZCA8Zm9vc3BhY2VzcGFjZWJhckBleGFtcGxlLm9yZz7C wA4EExYKAIAFgmX61g4DCwkHCRB4fwN+hNdoy0cUAAAAAAAeACBzYWx0QG5vdGF0 aW9ucy5zZXF1b2lhLXBncC5vcmeY9tet7HfUjNV8ajq1xlX2H6qsrKYpgLweIUoT kYockwMVCggCmQECmwECHgEWIQQz9sazimW+lEMmgax4fwN+hNdoywAAaL4BANR2 U+7L9CgrCFNU/spNuGJzxytGMDNyWOkmAY7lSdjmAQDOmLqfc95Dt/xJeLuahyCk e52rXSvP4ioyxk2YEh2eA8eGBGX61g4WCSsGAQQB2kcPAQEHQKxg1qpR9siKL6IX qLCcbNDu1b1q34mGY4Ug+yy3P5bM/gkDCOqhmBYY/sZf/x2lZVcXg4M98ECJgA8S FQCu50mQWaK7tLldFpbLCavdznLwukrK6K/HgFBAOuuqfuiuiPhOXGbzxhiX9Sdi nHImxS1kybzCwL8EGBYKATEFgmX61g4JEHh/A36E12jLRxQAAAAAAB4AIHNhbHRA bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ1ovgzKF5qMsjWShWlJxoFk+ZcP5ScDc jvzNbl07BjalApsgvqAEGRYKAG8FgmX61g4JEEGcKWozs8AQRxQAAAAAAB4AIHNh bHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ3aRQYrx9Y43F++KclaMzfLoVs9m Q1X7rn2e53Ugi8xWFiEEIK/jySGGHcCXLBnVQZwpajOzwBAAAMTaAQDnThvDogCs 8jufs5mTUzQSaubZX72LfQ5FTMo/f1vFuwEAiuVr1No6VKBqHFqz7yx5ARfkYIvN M2pIaDueNyFzGQoWIQQz9sazimW+lEMmgax4fwN+hNdoywAA8MMBAKjPRFvikP9N Xspk/dEXOMqDeT54QFY0lES+oDYRmpWGAP94DWYytk3qlXUOMsttu020bZkoqPYK QIuHv71uNJASCceGBGX61g4WCSsGAQQB2kcPAQEHQPuEfz1smRL05TA71UV2w1ve ICQPU8qgpZzro7NGPaP5/gkDCFvF+DVHWe4//0DfLmJg5FxH44yoOhyaCAeRtZ2B JSEcWEeDS5XviJM0OjIXHExKkiy7VxAC3o45V6a4oeo2vXBObms8m08CXqKNOo+s 5iXCwL8EGBYKATEFgmX61g4JEHh/A36E12jLRxQAAAAAAB4AIHNhbHRAbm90YXRp b25zLnNlcXVvaWEtcGdwLm9yZwc5cTO3bvpV6ZTUQKUBe+knMcrpq4lP44YeQ9XB WX6PApsCvqAEGRYKAG8FgmX61g4JEJEwcMe36q5ZRxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZy7scon/U/GbizIxCpEUN6wJflur7kGSMHdv A9HXZDGIFiEE/D3w5qlQFN4t+9LSkTBwx7fqrlkAAIZ9AQDILKY+f6lU6oGozNnT gRbXngDe8BocyAjOSNT7WRMGhwEA8NeqDqGJnjX5t0jfMetoR2OquanGmAoA3Q4H dIVF2gEWIQQz9sazimW+lEMmgax4fwN+hNdoywAAGEYA/0YtksE7nC84Aj6ukfgQ 45sggsXL8sa9uveWSKUr/QnKAQDag7d5/YHfwwMJcGoC8Fwwmlp3/cePrHerH9yo fzi8D8eLBGX61g4SCisGAQQBl1UBBQEBB0AN7ZCAJ1bfmFaWZk+TnESolMRiKi/N yjupKX0Hnq5rdQMBCAf+CQMIBPXDurIqO3X/4RUhPpcLxdgJuI+TNsOyYBk5aNqT +RvTj8POuJLtPtm06nbKLORv1xNrOI0nYxY6oCPeKLfI/I6WzLOb9kJ9DHbnECiC 3sLAAAQYFgoAcgWCZfrWDgkQeH8DfoTXaMtHFAAAAAAAHgAgc2FsdEBub3RhdGlv bnMuc2VxdW9pYS1wZ3Aub3JnywHmpqE/KWHD/PFP63TD5mlDmQYq9KW3CZpIzBh1 o+kCmwwWIQQz9sazimW+lEMmgax4fwN+hNdoywAA2fEBAMWusCTJpgc5OEfkRIeT rj4ywOhTVrMf4S45XubosaAhAP0TgFU63xgKlQ3leAETJdOwfsDm+OLoYRSmqIi/ a4QKAg== =0EKx -----END PGP PRIVATE KEY BLOCK----- sequoia-gpg-agent-0.4.2/tests/data/keys/password-foospacespacebar.pgp000064400000000000000000000045531046102023000240510ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 33F6 C6B3 8A65 BE94 4326 81AC 787F 037E 84D7 68CB Comment: Password xjMEZfrWDhYJKwYBBAHaRw8BAQdAm2ske3/zEGNjprEZn/xY7Jxhlr4IgT8ttSQ3 NCHGEe7CwAsEHxYKAH0FgmX61g4DCwkHCRB4fwN+hNdoy0cUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcINRY/BQAsSS0KXR+945n2Np96whJZ 7c4V7xR5EiiYvwMVCggCmwECHgEWIQQz9sazimW+lEMmgax4fwN+hNdoywAAPpsB AK2X0FAux8LEMpYhv0+EkfVk3DKGahHg3YSC3CCJP5/mAQCsSMZ3v4QaELmCv157 y2p1dlCtYh934pKAZqnA5FVCDs0nUGFzc3dvcmQgPGZvb3NwYWNlc3BhY2ViYXJA ZXhhbXBsZS5vcmc+wsAOBBMWCgCABYJl+tYOAwsJBwkQeH8DfoTXaMtHFAAAAAAA HgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnmPbXrex31IzVfGo6tcZV 9h+qrKymKYC8HiFKE5GKHJMDFQoIApkBApsBAh4BFiEEM/bGs4plvpRDJoGseH8D foTXaMsAAGi+AQDUdlPuy/QoKwhTVP7KTbhic8crRjAzcljpJgGO5UnY5gEAzpi6 n3PeQ7f8SXi7mocgpHudq10rz+IqMsZNmBIdngPOMwRl+tYOFgkrBgEEAdpHDwEB B0CsYNaqUfbIii+iF6iwnGzQ7tW9at+JhmOFIPsstz+WzMLAvwQYFgoBMQWCZfrW DgkQeH8DfoTXaMtHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Au b3JnWi+DMoXmoyyNZKFaUnGgWT5lw/lJwNyO/M1uXTsGNqUCmyC+oAQZFgoAbwWC ZfrWDgkQQZwpajOzwBBHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1w Z3Aub3JndpFBivH1jjcX74pyVozN8uhWz2ZDVfuufZ7ndSCLzFYWIQQgr+PJIYYd wJcsGdVBnClqM7PAEAAAxNoBAOdOG8OiAKzyO5+zmZNTNBJq5tlfvYt9DkVMyj9/ W8W7AQCK5WvU2jpUoGocWrPvLHkBF+Rgi80zakhoO543IXMZChYhBDP2xrOKZb6U QyaBrHh/A36E12jLAADwwwEAqM9EW+KQ/01eymT90Rc4yoN5PnhAVjSURL6gNhGa lYYA/3gNZjK2TeqVdQ4yy227TbRtmSio9gpAi4e/vW40kBIJzjMEZfrWDhYJKwYB BAHaRw8BAQdA+4R/PWyZEvTlMDvVRXbDW94gJA9TyqClnOujs0Y9o/nCwL8EGBYK ATEFgmX61g4JEHh/A36E12jLRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVv aWEtcGdwLm9yZwc5cTO3bvpV6ZTUQKUBe+knMcrpq4lP44YeQ9XBWX6PApsCvqAE GRYKAG8FgmX61g4JEJEwcMe36q5ZRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZy7scon/U/GbizIxCpEUN6wJflur7kGSMHdvA9HXZDGIFiEE /D3w5qlQFN4t+9LSkTBwx7fqrlkAAIZ9AQDILKY+f6lU6oGozNnTgRbXngDe8Boc yAjOSNT7WRMGhwEA8NeqDqGJnjX5t0jfMetoR2OquanGmAoA3Q4HdIVF2gEWIQQz 9sazimW+lEMmgax4fwN+hNdoywAAGEYA/0YtksE7nC84Aj6ukfgQ45sggsXL8sa9 uveWSKUr/QnKAQDag7d5/YHfwwMJcGoC8Fwwmlp3/cePrHerH9yofzi8D844BGX6 1g4SCisGAQQBl1UBBQEBB0AN7ZCAJ1bfmFaWZk+TnESolMRiKi/NyjupKX0Hnq5r dQMBCAfCwAAEGBYKAHIFgmX61g4JEHh/A36E12jLRxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZ8sB5qahPylhw/zxT+t0w+ZpQ5kGKvSltwma SMwYdaPpApsMFiEEM/bGs4plvpRDJoGseH8DfoTXaMsAANnxAQDFrrAkyaYHOThH 5ESHk64+MsDoU1azH+EuOV7m6LGgIQD9E4BVOt8YCpUN5XgBEyXTsH7A5vji6GEU pqiIv2uECgI= =ZnlB -----END PGP PUBLIC KEY BLOCK----- sequoia-gpg-agent-0.4.2/tests/data/keys/password-xyzzy-private.pgp000064400000000000000000000054241046102023000234340ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 5EDB C398 0B2A A227 18C8 8B51 48F8 7139 8EB8 57C6 Comment: Password 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 =g6L2 -----END PGP PRIVATE KEY BLOCK----- sequoia-gpg-agent-0.4.2/tests/data/keys/password-xyzzy.pgp000064400000000000000000000045171046102023000217660ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 5EDB C398 0B2A A227 18C8 8B51 48F8 7139 8EB8 57C6 Comment: Password xjMEZfrV+RYJKwYBBAHaRw8BAQdATaEvo66kP5B6UwI1zrV3a1GpPGdjvAKrx1Ta 9MNQYxzCwAsEHxYKAH0FgmX61fkDCwkHCRBI+HE5jrhXxkcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeBl1c1k6GYZUkVw514KQwgxJKlkz5V 3VCwcgBoaE8gZQMVCggCmwECHgEWIQRe28OYCyqiJxjIi1FI+HE5jrhXxgAAR1kB AMfbWmQ8yiLCpoqFxtBL7hXC0alBgXtpxM+qZc+pSvT6AQCO5g2N6hCDvEYSDhXZ fPiv6oX6tDnZcYYV4G3ujHifA80cUGFzc3dvcmQgPHh5enp5QGV4YW1wbGUub3Jn PsLADgQTFgoAgAWCZfrV+QMLCQcJEEj4cTmOuFfGRxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZ4ZVEcPj/577Rj+frRVAXPzNMtaMmiHEiFUx OoUA5yXYAxUKCAKZAQKbAQIeARYhBF7bw5gLKqInGMiLUUj4cTmOuFfGAAAUtwD/ dGO9zKzk5cheFFZjMEKfvtpLVzW0gzgKAPTp6KCCKrEBAOnTVbPzgBxEDbhE30no h4ZvW67C+TUPdsWUgs627X8NzjMEZfrV+RYJKwYBBAHaRw8BAQdABqiB0ywVFtvp JskQD1oL+8XZFT7Az9GoO/Tdgtbiwe7CwL8EGBYKATEFgmX61fkJEEj4cTmOuFfG RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ0pL5sjr8ERf 4tlHddqMvOPwA5HN95Q0rvS3oSoMiIfjApsCvqAEGRYKAG8FgmX61fkJEIJmakxU JF2tRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6mck7BU DnjyoCWrDK6NU87O2JYig8t9hox7d0kUXk4TFiEEO0vzsaCDdNY2Pb8UgmZqTFQk Xa0AAHssAQD47Cg1lDD7pTWEzuziiCPjkLmVsAPAH5wehax9qOeoEgEAmo0XovUt czpbUjoLg4hQdhvxY/p/HCYH7N5XgLfiFgQWIQRe28OYCyqiJxjIi1FI+HE5jrhX xgAAIkYBAK2anKuTKqwhUeTgLhmkwRMf+H/yQWL9A+E6c1ZcYLzFAP9ZMs0SqI7r +jU7RAKCeO8RF+dcOQgxjkuVEPMPgN3JD84zBGX61fkWCSsGAQQB2kcPAQEHQKta 3r6gD7foePlRWO7TtKoT9xjQKeWsgXvVedZrKXePwsC/BBgWCgExBYJl+tX5CRBI +HE5jrhXxkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcme4 COPQp0+8r8CPpXChsV9aCRmxD8Ls3gnnwDKrvUH3PgKbIL6gBBkWCgBvBYJl+tX5 CRCnYB6cpJVSKEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v cmeCHzYNeru4v0T6halZP1MsfUvc7RVPLDrPGlIofCTxAxYhBBwyLl4lWwp2UANW VadgHpyklVIoAADGdQEA/HknGgMFCPQC5a3L3V+uUJebHomB8qB7g4K3rQrhj+UB AP7/WcbHMIIsf9bOGPw18WTscstnMnbAPYP7VdwbGM4KFiEEXtvDmAsqoicYyItR SPhxOY64V8YAAL5dAP9YOvXFvYRfJHiVFdU0rzws5D94S0c86Xur8Jls/TfnyQD/ RDqFaeFr3CCVaOoHrQLqckfN0GCE+prrPu0sHYe2xwfOOARl+tX5EgorBgEEAZdV AQUBAQdALBfqy7h3LgdPdeqSjqac8cUeRzYN9SmfYgV6ASApHnMDAQgHwsAABBgW CgByBYJl+tX5CRBI+HE5jrhXxkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1 b2lhLXBncC5vcme0FDB5GQR1iwo64MaH1P4HVMa1nm+9D8RCoWXLLt9IhwKbDBYh BF7bw5gLKqInGMiLUUj4cTmOuFfGAAAxngD/SWu4t6d01zw2h79Fy5yl+O2VAVo+ +PpbE4/GiYNa2nUA/AuPayI9zDHhZY/qCCD2wG3I/8b7F+Faqv8jUOHWdT0K =W3qw -----END PGP PUBLIC KEY BLOCK----- sequoia-gpg-agent-0.4.2/tests/data/keys/testy-new-private.pgp000064400000000000000000000007701046102023000223150ustar 00000000000000XZSo +G@@F<XU`U| n^+`,B2,Ԁ7>a7~sP(C&}1Testy McTestface (my new key) 8!9gս7QX}ZSo   7QX}$t?LYG ?g鈅+$Ƀ ֐QGR$X;p~`~:hx !9gս7QX}ZSo 7QX}2X_u)x?~4[hkq|e;'¹A&>oxsI3} sequoia-gpg-agent-0.4.2/tests/data/keys/testy-new.pgp000064400000000000000000000006561046102023000206500ustar 000000000000003ZSo +G@@F<XU`U| n^+`,1Testy McTestface (my new key) 8!9gս7QX}ZSo   7QX}$t?LYG ?g鈅+$Ƀ ֐QGR$X;p~oxsI3} sequoia-gpg-agent-0.4.2/tests/gpg-agent.rs000064400000000000000000000347321046102023000165360ustar 00000000000000//! Tests gpg-agent interaction. use std::io::{self, Write}; use anyhow::Context as _; use futures::StreamExt; use sequoia_openpgp as openpgp; use crate::openpgp::{ packet::{ Any, PKESK, }, PacketPile, types::{ HashAlgorithm, SymmetricAlgorithm, }, }; use crate::openpgp::crypto::{SessionKey, Decryptor}; use crate::openpgp::parse::{Parse, stream::*}; use crate::openpgp::serialize::{Serialize, stream::*}; use crate::openpgp::cert::prelude::*; use crate::openpgp::policy::Policy; use sequoia_gpg_agent as gpg_agent; use gpg_agent::gnupg::{Context, Agent, KeyPair}; macro_rules! make_context { () => {{ let ctx = match Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("SKIP: Failed to create GnuPG context: {}\n\ SKIP: Is GnuPG installed?", e); return Ok(()); }, }; std::fs::write(ctx.homedir().unwrap().join("gpg-agent.conf"), "allow-loopback-pinentry\n").unwrap(); match ctx.start("gpg-agent") { Ok(_) => (), Err(e) => { eprintln!("SKIP: Failed to create GnuPG context: {}\n\ SKIP: Is the GnuPG agent installed?", e); return Ok(()); }, } ctx }}; } #[tokio::test] async fn nop() -> openpgp::Result<()> { let ctx = make_context!(); let mut agent = Agent::connect(&ctx).await.unwrap(); agent.send("NOP").unwrap(); let response = agent.collect::>().await; assert_eq!(response.len(), 1); response.into_iter().next().unwrap().unwrap(); Ok(()) } #[tokio::test] async fn help() -> openpgp::Result<()> { let ctx = make_context!(); let mut agent = Agent::connect(&ctx).await.unwrap(); agent.send("HELP").unwrap(); let response = agent.collect::>().await; assert!(response.len() > 3); response.into_iter().last().unwrap().unwrap(); Ok(()) } const MESSAGE: &str = "дружба"; const PASSWORD: &str = "streng geheim"; fn gpg_import(ctx: &Context, what: &[u8]) -> openpgp::Result<()> { use std::process::{Command, Stdio}; let mut gpg = Command::new("gpg") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .arg("--homedir").arg(ctx.homedir().unwrap()) .arg("--batch") .arg("--import") .spawn() .context("failed to start gpg")?; gpg.stdin.as_mut().unwrap().write_all(what)?; let output = gpg.wait_with_output()?; // We capture stdout and stderr, and use eprintln! so that the // output will be captured by Rust's test harness. This way, the // output will be at the right position, instead of out-of-order // and garbled by the concurrent tests. if ! output.stdout.is_empty() { eprintln!("stdout:\n{}", String::from_utf8_lossy(&output.stdout)); } if ! output.stderr.is_empty() { eprintln!("stderr:\n{}", String::from_utf8_lossy(&output.stderr)); } let status = output.status; if status.success() { Ok(()) } else { Err(anyhow::anyhow!("gpg --import failed")) } } #[test] fn sync_sign() -> openpgp::Result<()> { sign() } #[test] fn async_sign() -> openpgp::Result<()> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async { sign() }) } fn sign() -> openpgp::Result<()> { use self::CipherSuite::*; use openpgp::policy::StandardPolicy as P; let p = &P::new(); let ctx = make_context!(); for cs in &[RSA2k, Cv25519, P521] { if let Err(_) = cs.is_supported() { continue; } for password in vec![None, Some(PASSWORD.into())] { let (cert, _) = CertBuilder::new() .set_cipher_suite(*cs) .add_userid("someone@example.org") .add_signing_subkey() .set_password(password.clone()) .generate().unwrap(); let mut buf = Vec::new(); cert.as_tsk().serialize(&mut buf).unwrap(); gpg_import(&ctx, &buf)?; let mut keypair = KeyPair::new( &ctx, cert.keys().with_policy(p, None).alive().revoked(false) .for_signing().take(1).next().unwrap().key()) .unwrap(); if let Some(p) = password.clone() { keypair = keypair.with_password(p); } let mut message = Vec::new(); { // Start streaming an OpenPGP message. let message = Message::new(&mut message); // We want to sign a literal data packet. let signer = Signer::new(message, keypair) // XXX: Is this necessary? If so, it shouldn't. .hash_algo(HashAlgorithm::SHA512).unwrap() .build().unwrap(); // Emit a literal data packet. let mut literal_writer = LiteralWriter::new( signer).build().unwrap(); // Sign the data. literal_writer.write_all(MESSAGE.as_bytes()).unwrap(); // Finalize the OpenPGP message to make sure that all data is // written. literal_writer.finalize().unwrap(); } // Make a helper that that feeds the sender's public key to the // verifier. let helper = Helper { cert: &cert }; // Now, create a verifier with a helper using the given Certs. let mut verifier = VerifierBuilder::from_bytes(&message)? .with_policy(p, None, helper)?; // Verify the data. let mut sink = Vec::new(); io::copy(&mut verifier, &mut sink).unwrap(); assert_eq!(MESSAGE.as_bytes(), &sink[..]); } struct Helper<'a> { cert: &'a openpgp::Cert, } impl<'a> VerificationHelper for Helper<'a> { fn get_certs(&mut self, _ids: &[openpgp::KeyHandle]) -> openpgp::Result> { // Return public keys for signature verification here. Ok(vec![self.cert.clone()]) } fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> { // In this function, we implement our signature verification // policy. let mut good = false; for (i, layer) in structure.into_iter().enumerate() { match (i, layer) { // First, we are interested in signatures over the // data, i.e. level 0 signatures. (0, MessageLayer::SignatureGroup { results }) => { // Finally, given a VerificationResult, which only says // whether the signature checks out mathematically, we apply // our policy. match results.into_iter().next() { Some(Ok(_)) => good = true, Some(Err(e)) => return Err(openpgp::Error::from(e).into()), None => (), } }, _ => return Err(anyhow::anyhow!( "Unexpected message structure")), } } if good { Ok(()) // Good signature. } else { Err(anyhow::anyhow!("Signature verification failed")) } } } } Ok(()) } #[test] fn sync_decrypt() -> openpgp::Result<()> { decrypt(true) } #[test] fn async_decrypt() -> openpgp::Result<()> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async { decrypt(false) }) } fn decrypt(also_try_explicit_async: bool) -> openpgp::Result<()> { use self::CipherSuite::*; use openpgp::policy::StandardPolicy as P; let p = &P::new(); // Make a cert for a second recipient. let (other, _) = CertBuilder::new() .add_userid("other-recipient@example.org") .add_transport_encryption_subkey() .generate()?; for cs in &[RSA2k, Cv25519, P521] { if let Err(_) = cs.is_supported() { continue; } for password in vec![None, Some(PASSWORD.into())] { let ctx = make_context!(); let (cert, _) = CertBuilder::new() .set_cipher_suite(*cs) .add_userid("someone@example.org") .add_transport_encryption_subkey() .set_password(password.clone()) .generate().unwrap(); let mut buf = Vec::new(); cert.as_tsk().serialize(&mut buf).unwrap(); gpg_import(&ctx, &buf)?; // Import the second recipient. let mut buf = Vec::new(); other.serialize(&mut buf).unwrap(); gpg_import(&ctx, &buf)?; let mut message = Vec::new(); { let recipients = [&cert, &other].iter().flat_map( |c| c.keys().with_policy(p, None).alive().revoked(false) .for_transport_encryption() .map(|ka| ka.key())) .collect::>(); // Start streaming an OpenPGP message. let message = Message::new(&mut message); // We want to encrypt a literal data packet. let encryptor = Encryptor2::for_recipients(message, recipients) .build().unwrap(); // Emit a literal data packet. let mut literal_writer = LiteralWriter::new( encryptor).build().unwrap(); // Encrypt the data. literal_writer.write_all(MESSAGE.as_bytes()).unwrap(); // Finalize the OpenPGP message to make sure that all data is // written. literal_writer.finalize().unwrap(); } if also_try_explicit_async { // First, test Agent::decrypt. Using this function we can try // multiple decryption requests on the same connection. let rt = tokio::runtime::Runtime::new()?; let mut agent = rt.block_on(Agent::connect(&ctx))?; let pp = PacketPile::from_bytes(&message)?; let pkesk_0: &PKESK = pp.path_ref(&[0]).unwrap().downcast_ref().unwrap(); let pkesk_1: &PKESK = pp.path_ref(&[1]).unwrap().downcast_ref().unwrap(); // We only gave the cert to GnuPG, the agent doesn't have the // secret. let keypair = KeyPair::new( &ctx, other.keys().with_policy(p, None) .for_storage_encryption().for_transport_encryption() .take(1).next().unwrap().key())?; rt.block_on(agent.decrypt(&keypair, pkesk_1.esk(), None)).unwrap_err(); // Now try "our" key. let mut keypair = KeyPair::new( &ctx, cert.keys().with_policy(p, None) .for_storage_encryption().for_transport_encryption() .take(1).next().unwrap().key())?; if let Some(p) = password.clone() { keypair = keypair.with_password(p); } rt.block_on(agent.decrypt(&keypair, pkesk_0.esk(), None)).unwrap(); // Close connection. drop(agent); } // Make a helper that that feeds the recipient's secret key to the // decryptor. let helper = Helper { policy: p, ctx: &ctx, cert: &cert, other: &other, password: &password, }; // Now, create a decryptor with a helper using the given Certs. let mut decryptor = DecryptorBuilder::from_bytes(&message).unwrap() .with_policy(p, None, helper).unwrap(); // Decrypt the data. let mut sink = Vec::new(); io::copy(&mut decryptor, &mut sink).unwrap(); assert_eq!(MESSAGE.as_bytes(), &sink[..]); struct Helper<'a> { policy: &'a dyn Policy, ctx: &'a Context, cert: &'a openpgp::Cert, other: &'a openpgp::Cert, password: &'a Option, } impl<'a> VerificationHelper for Helper<'a> { fn get_certs(&mut self, _ids: &[openpgp::KeyHandle]) -> openpgp::Result> { // Return public keys for signature verification here. Ok(Vec::new()) } fn check(&mut self, _structure: MessageStructure) -> openpgp::Result<()> { // Implement your signature verification policy here. Ok(()) } } impl<'a> DecryptionHelper for Helper<'a> { fn decrypt(&mut self, pkesks: &[openpgp::packet::PKESK], _skesks: &[openpgp::packet::SKESK], sym_algo: Option, mut decrypt: D) -> openpgp::Result> where D: FnMut(SymmetricAlgorithm, &SessionKey) -> bool { // We only gave the cert to GnuPG, the agent doesn't // have the secret. let mut keypair = KeyPair::new( self.ctx, self.other.keys().with_policy(self.policy, None) .for_storage_encryption().for_transport_encryption() .take(1).next().unwrap().key()) .unwrap(); for pkesk in pkesks { assert!(pkesk.decrypt(&mut keypair, sym_algo).is_none()); } // Now use "our" key. let mut keypair = KeyPair::new( self.ctx, self.cert.keys().with_policy(self.policy, None) .for_storage_encryption().for_transport_encryption() .take(1).next().unwrap().key()) .unwrap(); if let Some(p) = self.password.clone() { keypair = keypair.with_password(p); } for pkesk in pkesks { if *pkesk.recipient() != keypair.public().keyid() { continue; } let (algo, session_key) = pkesk.decrypt(&mut keypair, sym_algo) .expect("decryption must succeed"); assert!(decrypt(algo, &session_key)); } // XXX: In production code, return the Fingerprint of the // recipient's Cert here Ok(None) } } } } Ok(()) }