sequoia-keystore-0.6.2/.cargo_vcs_info.json0000644000000001460000000000100143530ustar { "git": { "sha1": "b17c68ed41fe2a923554f9b2de99bdf4a83c0115" }, "path_in_vcs": "keystore" }sequoia-keystore-0.6.2/Cargo.toml0000644000000062050000000000100123530ustar # 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-keystore" version = "0.6.2" authors = ["Neal H. Walfield "] build = "build.rs" autobins = false autoexamples = false autotests = false autobenches = false description = "Sequoia's private key store server." homepage = "https://sequoia-pgp.org/" readme = "README.md" keywords = [ "cryptography", "openpgp", ] categories = ["cryptography"] license = "LGPL-2.0-or-later" repository = "https://gitlab.com/sequoia-pgp/sequoia-keystore" [package.metadata.docs.rs] features = ["sequoia-openpgp/default"] [lib] name = "sequoia_keystore" path = "src/lib.rs" [dependencies.anyhow] version = "1.0.18" [dependencies.async-generic] version = "1.1.0" [dependencies.capnp] version = "0.19" [dependencies.dirs] version = "5" [dependencies.env_logger] version = ">=0.10, <0.12" [dependencies.lazy_static] version = "1.4.0" [dependencies.log] version = "0.4.17" [dependencies.paste] version = "1" [dependencies.sequoia-directories] version = "0.1" [dependencies.sequoia-ipc] version = "0.35.1" [dependencies.sequoia-keystore-backend] version = "0.6" [dependencies.sequoia-keystore-gpg-agent] version = "0.4.1" optional = true [dependencies.sequoia-keystore-openpgp-card] version = "0.1" optional = true [dependencies.sequoia-keystore-softkeys] version = "0.6" optional = true [dependencies.sequoia-keystore-tpm] version = "0.1" optional = true [dependencies.sequoia-openpgp] version = "1.17" default-features = false [dependencies.thiserror] version = "1.0.2" [dependencies.tokio] version = "1.21" features = [ "rt", "net", "io-std", "macros", ] [dependencies.tokio-util] version = "0.7" features = ["compat"] [dev-dependencies.dircpy] version = "0.3" [dev-dependencies.env_logger] version = ">=0.10, <0.12" [dev-dependencies.test-log] version = "0.2.10" [dev-dependencies.tracing] version = "0.1" default-features = false [dev-dependencies.tracing-subscriber] version = "0.3" features = ["env-filter"] default-features = false [build-dependencies.capnpc] version = "0.19" [features] default = [ "softkeys", "gpg-agent", ] gpg-agent = ["dep:sequoia-keystore-gpg-agent"] openpgp-card = ["dep:sequoia-keystore-openpgp-card"] softkeys = ["dep:sequoia-keystore-softkeys"] tpm = ["dep:sequoia-keystore-tpm"] [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.maintenance] status = "actively-developed" sequoia-keystore-0.6.2/Cargo.toml.orig000064400000000000000000000051521046102023000160340ustar 00000000000000[package] name = "sequoia-keystore" description = "Sequoia's private key store server." version = "0.6.2" authors = ["Neal H. Walfield "] homepage = "https://sequoia-pgp.org/" repository = "https://gitlab.com/sequoia-pgp/sequoia-keystore" readme = "README.md" keywords = ["cryptography", "openpgp"] categories = ["cryptography"] license = "LGPL-2.0-or-later" edition = "2021" build = "build.rs" rust-version = "1.70" [badges] maintenance = { status = "actively-developed" } [features] default = ["softkeys", "gpg-agent"] softkeys = ["dep:sequoia-keystore-softkeys"] # Note: openpgp-card is currently opt-in. It will become the default # when it has seen more testing. openpgp-card = ["dep:sequoia-keystore-openpgp-card"] # Note: tpm is currently opt-in. It will become the default when it # has seen more testing. tpm = ["dep:sequoia-keystore-tpm"] gpg-agent = ["dep:sequoia-keystore-gpg-agent"] [dependencies] anyhow = "1.0.18" async-generic = "1.1.0" capnp = "0.19" dirs = "5" # Our MSRV is only compatible with 0.10, but we're compatible with # 0.11. env_logger = ">=0.10, <0.12" lazy_static = "1.4.0" log = "0.4.17" paste = "1" sequoia-directories = "0.1" sequoia-ipc = "0.35.1" sequoia-keystore-backend = { path = "../backend", version = "0.6" } sequoia-keystore-softkeys = { path = "../softkeys", version = "0.6", optional = true } sequoia-keystore-gpg-agent = { path = "../gpg-agent", version = "0.4.1", optional = true } sequoia-keystore-openpgp-card = { path = "../openpgp-card", version = "0.1", optional = true } sequoia-keystore-tpm = { path = "../tpm", version = "0.1", optional = true } sequoia-openpgp = { version = "1.17", default-features = false } thiserror = "1.0.2" tokio = { version = "1.21", features = ["rt", "net", "io-std", "macros"] } tokio-util = { version = "0.7", features = ["compat"] } [build-dependencies] capnpc = "0.19" [dev-dependencies] env_logger = ">=0.10, <0.12" test-log = "0.2.10" tracing = {version = "0.1", default-features = false} tracing-subscriber = {version = "0.3", default-features = false, features = ["env-filter"]} dircpy = "0.3" # Enables a crypto backend for the tests: [target.'cfg(not(windows))'.dev-dependencies] sequoia-openpgp = { version = "1", default-features = false, features = ["crypto-nettle", "__implicit-crypto-backend-for-tests"] } # Enables a crypto backend for the tests: [target.'cfg(windows)'.dev-dependencies] sequoia-openpgp = { version = "1", default-features = false, features = ["crypto-cng", "__implicit-crypto-backend-for-tests"] } # Enables a crypto backend for the docs.rs generation: [package.metadata.docs.rs] features = ["sequoia-openpgp/default"] sequoia-keystore-0.6.2/LICENSE.txt000064400000000000000000000627341046102023000150010ustar 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-keystore-0.6.2/README.md000064400000000000000000000041461046102023000144260ustar 00000000000000Sequoia's private key store. This project implements a private key store for Sequoia. A private key store mediates applications' access to private keys, and offers three major advantages relative to every application accessing the keys or HSMs directly: - A private key store is in a separate address space. This means that private keys that are in memory are in a different address space from the application. This was underlying cause of the [Heartbleed vulnerability]. [Heartbleed vulnerability]: https://de.wikipedia.org/wiki/Heartbleed - A private key store can provide a uniform interface for accessing keys stored on different backends, e.g., an in-memory key, a key on a smart card, or a key on a remote computer, which is accessed via ssh. This simplifies applications. - This architecture simplifies sharing private key material among multiple applications. Only the private key store needs to worry about managing the private key material, which improves security. And, when a user unlocks a key in one application, it is potentially unlocked in all applications, which improves usability. Although the key store can run as a separate server, sometimes it is useful to co-locate it. This is useful to increase robustness, e.g., the key store is not running, and can't be started for some reason. And, it allows the key store to be used in places where starting processes is not easy or not desirable, like in an initrd. The private key store uses a device-driver style architecture. The [sequoia-keystore-backend crate] defines a trait that different backends implement. Currently, [backends are added at compile time]. [sequoia-keystore-backend crate]: https://gitlab.com/sequoia-pgp/sequoia-keystore/-/tree/main/backend [backends are added at compile time]: https://gitlab.com/sequoia-pgp/sequoia-keystore/-/blob/main/keystore/src/server/backend.rs The [sequoia-keystore-softkeys] backend is an example of a backend. It supports soft keys, i.e., in-memory keys. [sequoia-keystore-softkeys]: https://gitlab.com/sequoia-pgp/sequoia-keystore/-/tree/main/softkeys sequoia-keystore-0.6.2/build.rs000064400000000000000000000025731046102023000146160ustar 00000000000000use std::env; use std::fs; use std::io; use std::io::Write; use std::path::PathBuf; fn capnp(src: &str) { println!("rerun-if-changed={}", src); ::capnpc::CompilerCommand::new().file(src).run().unwrap(); } /// Builds the index of the test data for use with the `::testdata` /// 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(()) } fn main() { capnp("src/keystore_protocol.capnp"); include_test_data().unwrap(); } sequoia-keystore-0.6.2/src/capnp_relay.rs000064400000000000000000000334771046102023000166120ustar 00000000000000use std::any::Any; use std::collections::HashMap; use std::sync::Arc; use std::thread; use tokio::sync::mpsc; use tokio::sync::mpsc::error::TryRecvError; use capnp::any_pointer; use capnp::capability; use capnp::message; use crate::keystore_protocol_capnp::keystore; use sequoia_ipc as ipc; use ipc::capnp_rpc::rpc_twoparty_capnp::Side; use ipc::Descriptor; use crate::Result; // A capability reference. // // When this goes out of scope, we add the capability to the dead // capability list so that it can be deallocated. #[derive(Clone, Debug)] struct CapRef { index: u64, dead_caps: Option>>, } impl Drop for CapRef { fn drop(&mut self) { log::trace!("Dropping cap {}", self.index); if let Some(dead_caps) = self.dead_caps.take() { let _ = dead_caps.send(self.index); } } } /// A local capability. /// /// We wrap capnproto capabilities, because they are not Send + Sync. #[derive(Clone, Debug)] pub struct Cap { // The index into CapTable::caps. index: u64, // We don't read from this, but we rely on its drop method to // clean up the actual capability. #[allow(unused)] cap_ref: Option>, } /// A capability table. pub struct CapTable { next_index: u64, caps: HashMap, dead_caps: Arc>, } impl CapTable { /// Returns a new capability table. /// /// Normally, there is one capability table per root capability. pub fn new() -> (Self, mpsc::UnboundedReceiver) { let (sender, receiver) = mpsc::unbounded_channel(); ( CapTable { next_index: 1, caps: HashMap::new(), dead_caps: Arc::new(sender), }, receiver ) } /// Adds the capnproto capability to the capability table. Returns /// the local capability. pub fn insert(&mut self, cap: capability::Client) -> Cap { let i = self.next_index; self.next_index += 1; self.caps.insert(i, cap); Cap { index: i, cap_ref: Some(Arc::new(CapRef { index: i, dead_caps: Some(Arc::clone(&self.dead_caps)), })), } } /// Returns the capnproto capability. fn lookup(&self, cap: C) -> Option<&capability::Client> where C: std::borrow::Borrow { self.caps.get(&cap.borrow().index) } /// Sets the root capability. /// /// This may only be called once. fn set_root(&mut self, cap: capability::Client) { if let Some(_) = self.caps.insert(0, cap) { panic!("root capability already set"); } } /// Returns the root capability. pub fn root() -> Cap { Cap { index: 0, // The root is always live. cap_ref: None, } } } type CapnProtoRelayResponse = Result>; type CapnProtoRelayRequest = ( Cap, // capability in CapnProtoRelay::caps u64, // Interface ID. u16, // Method ID. message::Reader>, // Response from the server. mpsc::Sender, // Function to extract the result. Box, &mut CapTable, capability::Response) -> Result> + Send + Sync> ); /// A capnproto relay. /// /// We want capabilities to be Send + Sync, but capnproto capabilities /// aren't. This relay makes that possible by wrapping capabilities /// and forwarding messages to a dedicated thread that is responsible /// for dispatching RPCs. pub struct CapnProtoRelay { // The end point used to send a message to the worker thread. sender: mpsc::Sender, } impl CapnProtoRelay { /// Instantiates a new relay. pub fn new(descriptor: Descriptor) -> Result> { log::trace!("CapnProtoRelay::new"); // To get any errors regarding the initialization of the // worker thread. let (init_sender, init_receiver): (std::sync::mpsc::Sender>, std::sync::mpsc::Receiver>) = std::sync::mpsc::channel(); // We don't need much capacity: the sender will always // immediately follow up by waiting for a reply. So if the // sender's message is queued, and then the sender waits for a // reply, or the sender blocks until there is space, and then // waits for a reply, it doesn't matter. let (rpc_sender, rpc_receiver): (mpsc::Sender, mpsc::Receiver) = mpsc::channel(8); let relay = Arc::new(CapnProtoRelay { sender: rpc_sender, }); let relay_ref = Arc::downgrade(&relay); thread::spawn(move || { CapnProtoRelay::worker( relay_ref, init_sender, descriptor, rpc_receiver); }); // Wait for the worker thread to signal us that startup went // ok. let () = init_receiver.recv()??; Ok(relay) } /// Returns the root capability. pub fn root(&self) -> Cap { CapTable::root() } fn worker(relay: std::sync::Weak, init_sender: std::sync::mpsc::Sender>, descriptor: ipc::Descriptor, mut rpc_receiver: mpsc::Receiver) { log::trace!("CapnProtoRelay::worker"); let rt = tokio::runtime::Builder::new_current_thread() .enable_io() .build(); let rt = match rt { Ok(rt) => rt, Err(err) => { let _ = init_sender.send(Err(err.into())); return; } }; // Need to enter the Tokio context due to Tokio TcpStream // creation binding eagerly to an I/O reactor. let rpc_system = rt.block_on(async { descriptor.connect() }); let mut rpc_system = match rpc_system { Ok(rpc_system) => rpc_system, Err(err) => { let _ = init_sender.send(Err(err)); return; } }; // Start up went well. init_sender.send(Ok(())).expect("CapnProtoRelay::new waiting"); drop(init_sender); let root: keystore::Client = rpc_system.bootstrap(Side::Server); let (mut caps, dead_caps_receiver) = CapTable::new(); caps.set_root(root.client); // Since RpcSystem is explicitly `!Send`, we need to spawn it // on a `LocalSet`. let rpc_task = tokio::task::LocalSet::new(); rpc_task.spawn_local(rpc_system); rpc_task.block_on(&rt, async move { let mut dead_caps_receiver = Some(dead_caps_receiver); 'message: loop { // Clean up any dead capabilities. if let Some(recv) = dead_caps_receiver.as_mut() { 'dead_caps: loop { match recv.try_recv() { Ok(i) => { log::trace!("CapnProtoRelay::worker: \ Removing dead cap {}", i); caps.caps.remove(&i).expect("valid capability"); } Err(TryRecvError::Empty) => { // No pending messages. break 'dead_caps; } Err(err) => { log::trace!("CapnProtoRelay::worker: \ dead caps receiver is... dead ({})", err); dead_caps_receiver = None; break 'dead_caps; } } } } // Process a message. log::trace!("CapnProtoRelay::worker: waiting for a message"); let (cap, interface_id, method_id, params, reply, f) = match rpc_receiver.recv().await { None => { // This only happens if there are no // senders. In that case, CapnProtoRelay // has been dropped and we should exit. log::debug!("CapnProtoRelay::worker: \ no senders, exiting"); break 'message; } Some(r) => r, }; log::trace!("CapnProtoRelay::worker: processing message"); let relay = if let Some(relay) = relay.upgrade() { relay } else { log::debug!("CapnProtoRelay::worker: \ Aborting. Capability relay dropped."); break 'message; }; let r = Self::relay( relay, &mut caps, cap, interface_id, method_id, params, f).await; if let Err(ref err) = r { log::debug!("CapnProtoRelay::worker: \ error relaying rpc {:x}/{}: {}", interface_id, method_id, err); } if let Err(err) = reply.send(r).await { log::debug!("CapnProtoRelay::worker: \ error forwarding reply {:x}/{}: {}", interface_id, method_id, err); } else { log::trace!("CapnProtoRelay::worker: \ forwarded reply: {:x}/{}", interface_id, method_id); } } log::trace!("CapnProtoRelay::worker: exited message processing loop"); }); log::trace!("CapnProtoRelay::worker: exiting function"); } async fn relay( relay: std::sync::Arc, caps: &mut CapTable, cap: Cap, interface_id: u64, method_id: u16, params: message::Reader>, f: F) -> Result> where F: FnOnce(std::sync::Arc, &mut CapTable, capability::Response) -> Result> { log::trace!("CapnProtoRelay::relay(interface: {}, method: {})", interface_id, method_id); let cap = caps.lookup(&cap) // XXX: Use an Error variant. .ok_or(anyhow::anyhow!("Invalid capability ({})", cap.index))?; let mut request: capability::Request = cap.new_call(interface_id, method_id, None); request.set(params.get_root()?)?; log::trace!("CapnProtoRelay::relay: sending RPC"); let response: Result, _> = request.send().promise.await; log::trace!("CapnProtoRelay::relay: got response"); f(relay, caps, response?) } /// Sends an RPC and returns a handle that can be waited on. /// /// Use `await_reply` to wait on the returned handle. #[async_generic::async_generic] pub fn send_rpc(&self, cap: Cap, interface_id: u64, method_id: u16, message: message::Reader< message::Builder>, f: F) -> Result> where F: 'static + FnOnce(std::sync::Arc, &mut CapTable, capability::Response) -> Result> + Send + Sync { log::trace!("CapnProtoRelay::send_rpc"); let (sender, receiver) = mpsc::channel(1); log::trace!("CapnProtoRelay::send_rpc: forwarding to relay thread"); let result = if _sync { self.sender.blocking_send((cap, interface_id, method_id, message, sender, Box::new(f))) } else { self.sender.send((cap, interface_id, method_id, message, sender, Box::new(f))).await }; if let Err(_) = result { // sending can only fail if the receiver disconnected. We // can't get here if the relay thread is not running. panic!("capnproto worker threader died"); } Ok(receiver) } /// Returns the response to an RPC. /// /// The handle is as returned by `send_rpc`. #[async_generic::async_generic] pub fn await_reply(mut handle: mpsc::Receiver) -> Result> { log::trace!("CapnProtoRelay::await_reply: waiting for relay thread"); let response = if _sync { handle.blocking_recv().expect("worker is alive")? } else { handle.recv().await.expect("worker is alive")? }; log::trace!("CapnProtoRelay::await_reply: relay thread responded"); Ok(response) } } sequoia-keystore-0.6.2/src/error.rs000064400000000000000000000340361046102023000154360ustar 00000000000000use std::sync::Arc; use sequoia_openpgp as openpgp; use openpgp::Result; use openpgp::packet; use openpgp::parse::Parse; use crate::capnp_relay; use crate::capnp_relay::CapnProtoRelay; use crate::keystore; use crate::InaccessibleDecryptionKey; use crate::Key; use crate::server; #[derive(thiserror::Error, Debug)] /// Errors returned from the keystore. // There errors are also defined in keystore_protocol.capnp. If you // add a variant here, then you'll probably need to add it there as // well. pub enum Error { // keystore_protocol.capnp errors: /// A generic error occurred. /// /// The other error variants aren't appropriate for representing /// this error. The text describes what went wrong. #[error("Error: {}", .0.as_deref().unwrap_or("an unspecified error occurred"))] GenericError(Option), /// An unspecified protocol error occurred. #[error("Unspecified protocol error: something went wrong")] ProtocolError, /// An end of file condition was reached. /// /// This is also used for iterations to indicate that there are no /// more items. #[error("EOF")] EOF, /// An operation couldn't be completed, because a required key is /// inaccessible (unavailable, or locked). /// /// This is returned by `Keystore::decrypt`. Repeat the operation /// after ensuring that one of the keys is available by prompting /// the user to connect it, or unlocking the key. #[error("Can't decrypt a PKESK, the candidate keys are inaccessible")] InaccessibleDecryptionKey(Vec), /// The key cannot be used for decryption. #[error("Key {0} cannot be used for decryption")] NotDecryptionCapable(String), /// The key cannot be used for signing. #[error("Key {0} cannot be used for signing")] NotSigningCapable(String), /// An internal server error occurred. #[error("Internal server error")] InternalError(String), /// The backend doesn't support importing keys using this interface. /// /// Some backends require backend-specific information in order to /// import a key. For instance, when importing a key to a /// smartcard, the user needs to specify the smartcard, and the /// slot to use on the smartcard. Rather than try and model these /// parameters using this generic interface, backends should just /// have their own tools. The text should be a hint that is /// displayed to the user describing how to find the tool. #[error("Can't import keys into the backend: {}", .0.as_deref().unwrap_or("use another tool to import keys \ into this backend"))] ExternalImportRequired(Option), /// The secret key material cannot be exported. /// /// This error is returned by [`Key::export`] if the secret key /// material cannot be exported. Many backends do not allow /// exporting secret key material. Some may only allow exporting /// secret key material in admin mode. The text is a more /// detailed description of why this is not possible, or what the /// user could do to export the secret key material. #[error("Can't export secret key material key: {}", .0.as_deref().unwrap_or("use another tool to import keys \ into this backend"))] SecretKeyMaterialSealed(Option), /// The key doesn't support inline passwords. /// /// Password entry is taken care of by the device managing the /// key. For instance, the password may be obtained using an /// external PIN pad. #[error("Can't unlock using an inline password{}{}", .0.as_ref().map(|_| ": ").unwrap_or(""), .0.as_deref().map(|msg| msg).unwrap_or(""))] NoInlinePassword(Option), /// The key doesn't support getting passwords. /// /// The password must be provided inline. The password cannot be /// obtained using something like an external PIN pad. #[error("External password entry is not supported{}{}", .0.as_ref().map(|_| ": ").unwrap_or(""), .0.as_deref().map(|msg| msg).unwrap_or(""))] NoExternalPassword(Option), // Other errors: /// A `capnp::Error` occurred. #[error("Internal RPC error")] RpcError(#[from] capnp::Error), // !!! If you add an error here, make sure you update the // following function. } /// Errors returned from the server, which first need to be converted /// to the public form, `Error`. #[derive(thiserror::Error, Debug)] pub(crate) enum ServerError { /// An operation couldn't be completed, because a required key is /// inaccessible (unavailable, or locked). /// /// This is returned by `Keystore::decrypt`. Repeat the operation /// after ensuring that one of the keys is available by prompting /// the user to connect it, or unlocking the key. #[error("Can't decrypt a PKESK, the candidate keys are inaccessible")] InaccessibleDecryptionKey(Vec), } impl Error { /// Converts an error stored in a capnp buffer to our local error /// type. pub(crate) fn from_capnp(relay: Arc, captable: &mut capnp_relay::CapTable, err: keystore::error::Reader<'_>) -> anyhow::Error { let mut try_from = || { match err.which() { Ok(keystore::error::GenericError(msg)) => { let msg = msg?; if msg.is_empty() { Ok(Error::GenericError(None).into()) } else if let Ok(msg) = msg.to_string() { Ok(Error::GenericError(Some(msg)).into()) } else { // Yowzers: we couldn't extract the error // message. Ok(Error::GenericError( Some("Error parsing error message".into())).into()) } }, Ok(keystore::error::Protocol(())) => Ok(Error::ProtocolError.into()), Ok(keystore::error::Eof(())) => Ok(Error::EOF.into()), Ok(keystore::error::NotDecryptionCapable(s)) => { let s = s?.to_string()?; Ok(Error::NotDecryptionCapable(s).into()) } Ok(keystore::error::NotSigningCapable(s)) => { let s = s?.to_string()?; Ok(Error::NotSigningCapable(s).into()) } Ok(keystore::error::InaccessibleDecryptionKey(keys)) => { let keys = keys?.into_iter() .map(|inaccessible_key| { let key = inaccessible_key.get_key_descriptor()?; let cap = key.get_handle()?; let pk = key.get_public_key()?; let pk = packet::Key:: ::from_bytes(pk)?; let pk = pk.parts_into_public(); let pkesk = inaccessible_key.get_pkesk()?; let pkesk = packet::PKESK::from_bytes(pkesk)?; Ok(InaccessibleDecryptionKey { key: Key { relay: Arc::clone(&relay), cap: captable.insert(cap.client), key: pk, }, pkesk: pkesk, }) }) .collect::>>()?; Ok(Error::InaccessibleDecryptionKey(keys).into()) } Ok(keystore::error::InternalError(s)) => { let s = s?.to_string()?; Ok(Error::InternalError(s).into()) } Ok(keystore::error::ExternalImportRequired(msg)) => { let msg = msg?; if msg.is_empty() { Ok(Error::ExternalImportRequired(None).into()) } else if let Ok(msg) = msg.to_string() { Ok(Error::ExternalImportRequired(Some(msg)).into()) } else { // Yowzers: we couldn't extract the error // message. Ok(Error::ExternalImportRequired(None).into()) } } Ok(keystore::error::SecretKeyMaterialSealed(msg)) => { let msg = msg?; if msg.is_empty() { Ok(Error::SecretKeyMaterialSealed(None).into()) } else if let Ok(msg) = msg.to_string() { Ok(Error::SecretKeyMaterialSealed(Some(msg)).into()) } else { // Yowzers: we couldn't extract the error // message. Ok(Error::SecretKeyMaterialSealed(None).into()) } } Ok(keystore::error::NoInlinePassword(msg)) => { let msg = msg?; if msg.is_empty() { Ok(Error::NoInlinePassword(None).into()) } else if let Ok(msg) = msg.to_string() { Ok(Error::NoInlinePassword(Some(msg)).into()) } else { // Yowzers: we couldn't extract the error // message. Ok(Error::NoInlinePassword(None).into()) } } Ok(keystore::error::NoExternalPassword(msg)) => { let msg = msg?; if msg.is_empty() { Ok(Error::NoExternalPassword(None).into()) } else if let Ok(msg) = msg.to_string() { Ok(Error::NoExternalPassword(Some(msg)).into()) } else { // Yowzers: we couldn't extract the error // message. Ok(Error::NoExternalPassword(None).into()) } } Err(err) => { // The error is incorrectly formatted. Turn that into // a protocol error. log::debug!("Protocol violation while parsing error: {}", err); Ok(Error::ProtocolError.into()) } } }; match try_from() { Ok(err) => err, Err(err) => err, } } } impl keystore::error::Builder<'_> { /// Sets the error on the wire from an `anyhow::Error`. /// /// This converts an error to the wire format. This only handles /// the local error type, [`keystore::error::Error]`; everything /// else is mapped to [`Error::UnspecifiedError`]. /// /// Note: client code never needs this. pub(crate) fn from_anyhow(&mut self, err: &anyhow::Error) { match err.downcast_ref::() { Some(ServerError::InaccessibleDecryptionKey(keys)) => { let mut keys_wire = self .reborrow() .init_inaccessible_decryption_key(keys.len() as u32); for (i, key) in keys.into_iter().enumerate() { key.serialize(keys_wire.reborrow().get(i as u32)); } return; } None => (), } match err.downcast_ref::() { Some(Error::GenericError(msg)) => self.set_generic_error(msg.as_deref().unwrap_or("")), Some(Error::ProtocolError) => self.set_protocol(()), Some(Error::EOF) => self.set_eof(()), Some(Error::NotDecryptionCapable(fpr)) => { let mut builder = self.reborrow() .init_not_decryption_capable(fpr.len() as u32); builder.push_str(&fpr); } Some(Error::NotSigningCapable(fpr)) => { let mut builder = self.reborrow() .init_not_signing_capable(fpr.len() as u32); builder.push_str(&fpr); } Some(Error::InaccessibleDecryptionKey(_keys)) => { // We never have to serialize an Error::Inaccessible: // that's a client variant. On the server, we use // ServerError::Inaccessible. log::debug!("Invalid attempt to serialize Error::InaccessibleDecryptionKey"); self.set_internal_error("InaccessibleDecryptionKey shouldn't \ be returned in this context"); } Some(Error::InternalError(err)) => { let mut builder = self.reborrow() .init_internal_error(err.len() as u32); builder.push_str(&err); } Some(Error::ExternalImportRequired(msg)) => { self.set_external_import_required(msg.as_deref().unwrap_or("")); } Some(Error::SecretKeyMaterialSealed(msg)) => { self.set_secret_key_material_sealed(msg.as_deref().unwrap_or("")); } Some(Error::NoInlinePassword(msg)) => { self.set_no_inline_password(msg.as_deref().unwrap_or("")); } Some(Error::NoExternalPassword(msg)) => { self.set_no_external_password(msg.as_deref().unwrap_or("")); } Some(Error::RpcError(_err)) => self.set_protocol(()), None => { // This is the best we can do. self.set_generic_error(err.to_string()); } } } } impl From for Error { fn from(_: capnp::NotInSchema) -> Self { Error::ProtocolError } } sequoia-keystore-0.6.2/src/import_status.rs000064400000000000000000000017111046102023000172140ustar 00000000000000pub use sequoia_keystore_backend::ImportStatus; use crate::Result; use crate::keystore_protocol_capnp::keystore; impl TryFrom> for ImportStatus { type Error = anyhow::Error; fn try_from(reader: keystore::import_status::Reader<'_>) -> Result { use keystore::import_status::Which; match reader.which()? { Which::New(()) => Ok(ImportStatus::New), Which::Updated(()) => Ok(ImportStatus::Updated), Which::Unchanged(()) => Ok(ImportStatus::Unchanged), } } } impl keystore::import_status::Builder<'_> { /// Serialize ImportStatus. pub fn set_import_status(&mut self, import_status: ImportStatus) -> Result<()> { match import_status { ImportStatus::New => self.set_new(()), ImportStatus::Updated => self.set_updated(()), ImportStatus::Unchanged => self.set_unchanged(()), } Ok(()) } } sequoia-keystore-0.6.2/src/keystore_protocol.capnp000064400000000000000000000325531046102023000205520ustar 00000000000000@0xc996c4872a6820ae; # This file describes the interface between a client of the keystore # (e.g., sq) and the keystore. Keystore backends implement the traits # defined in the sequoia-keystore-backend crate. interface Keystore { backends @0 () -> (result: Result(List(Backend))); # Returns the backends. decrypt @1 (pkesks: List(Data)) -> (result: Result(DecryptedPkesk)); # Decrypts a PKESK. # # If multiple PKESKs are provided, this tries to use the most # convenient key from the user's perspective. In particular, it # tries to use an unlocked key before trying a locked key. # # If no keys could be used, but there is at least one registered key # that could be used, this returns `Error::Inaccessible` and the # list of keys that could be used, and why they can't be used. struct InaccessibleDecryptionKey { # Returned by Keystore::decrypt. keyDescriptor @0: KeyDescriptor; # The inaccessible key. pkesk @1: Data; # The serialized PKESK that triggered this error. } struct DecryptedPkesk { index @0: UInt32; # The index of the PKESK that was decrypted. fingerprint @1: Data; # The fingerprint of the key used to decrypt the data. algo @2: UInt8; # The decrypted symmetric algorithm identifier. # # See https://datatracker.ietf.org/doc/html/rfc4880#section-5.1 sessionKey @3: Data; # The decrypted session key. # # See https://datatracker.ietf.org/doc/html/rfc4880#section-5.1 } interface Backend { id @0 () -> (result: Result(Text)); # Returns the backend's unique identifier. # # It should be a well-formed UTF-8 string, which should give a # curious user a pretty good idea of what backend this is. list @1 () -> (result: Result(List(Device))); # List known devices. import @2 (cert: Data) -> (result: Result(List(ImportResult))); # Imports secret key material. # # cert is a serialized TSK. Keys without secret key material are # silently ignored. # # If a key already exists, it is overwritten. # # An ImportResult is returned for each secret key. If the TSK # doesn't include any secret keys, then an empty list is returned. # # Some backends require additional information to import a key. # These backends should `Error::externalImportRequired`, and # indicate how a user might import a key to this backend. # scan @2 () -> (result: VoidResult); # # Search for devices. # # register @3 (description: Text) -> (result: Result(Device)); # # Register a device. } struct ImportResult { key @0: KeyDescriptor; status @1: ImportStatus; } struct ImportStatus { union { new @0 :Void; # The imported object is new. updated @1: Void; # The imported object updated an existing object. unchanged @2: Void; # The object already existed, and is unchanged. } } interface Device { id @0 () -> (result: Result(Text)); # Returns a unique device identifier. # # It should be a well-formed UTF-8 string, which should give a # curious user a pretty good idea of what device this is. list @1 () -> (result: Result(List(KeyDescriptor))); # List known keys associated with this device. } struct KeyDescriptor { handle @0: Key; publicKey @1: Data; # The serialized PublicKey or PublicSubkey packet without OpenPGP framing. # # If the creation time is not known, the backend should set the # creation time to 0. } interface Key { id @0 () -> (result: Result(Text)); # Returns a unique key identifier. # # It should be a well-formed UTF-8 string, which should give a # curious user a pretty good idea of what key this is. decryptCiphertext @1 (algo: UInt8, ciphertext: Data, plaintextLen: UInt32) -> (result: Result(Data)); # Decrypts the ciphertext. # # The semantics are identical to # `sequoia_openpgp::crypto::Decryptor::decrypt`. # # https://docs.sequoia-pgp.org/sequoia_openpgp/crypto/trait.Decryptor.html#tymethod.decrypt signMessage @2 (hash_algo: UInt8, digest: Data) -> (result: Result(SignedData)); # Signs the digest. unlock @3 (password: Data) -> (result: VoidResult); # Unlocks the key. # # If the key is not available, this first attempts to connect to # the device (e.g., bring up an ssh tunnel). # # If the key is already unlocked, this returns an error. # # If the password is wrong, thus returns an error. # # If the key can be unlocked, the key remains unlocked until # the cache flushes unlocked key. available @4 () -> (result: BoolResult); # Whether the key is available. # # If false, this usually means the device needs to be connected. locked @5 () -> (result: Result(Protection)); # Whether the key is locked, and the type of protection. decryptionCapable @6 () -> (result: BoolResult); # Whether the key can be used for decryption. signingCapable @7 () -> (result: BoolResult); # Whether the key can be used for signing. export @8 () -> (result: Result(Data)); # Exports the secret key material. # # The secret key material is exported as a serialized # SecretKey or SecretSubkey packet without OpenPGP framing. passwordSource @9 () -> (result: Result(PasswordSource)); # How the password is obtained. changePassword @10 (password: ChangePasswordAction) -> (result: VoidResult); # Changes the key's password. deleteSecretKeyMaterial @11 () -> (result: VoidResult); # Deletes the key's secret key material. } struct Protection { union { unlocked @0 :Void; # The secret key material is unlocked. unknownProtection @1 :Text; # The backend is not able to determine if the secret key # material is protected. # # It is, however, safe to try a secret key operation (e.g., the # retry counter will not be decremented). Trying an operation # may trigger an external event, like a system pin entry dialog. # # The string is an optional hint for the user. password @2 :Text; # The secret key material is protected by a password. It can # be unlocked using the unlock interface. # # The string is an optional hint for the user. externalPassword @3 :Text; # The secret key material is protected, and can only be unlocked # using an external terminal. # # The string is an optional hint for the user. # # Note: some devices don't provide a mechanism to determine if # the secret key material is currently locked. For instance, # some smart cards can be configured to require the user to # enter a pin on an external keypad before their first use, but # not require it as long as the smart card remains attached to # the host, and also not provide a mechanism for the host to # determine the current policy. Such devices should still # report `Protection::ExternalPassword`, and should phrase the # hint appropriately. externalTouch @4 :Text; # The secret key material is protected, and can only be unlocked # if the user touches the device. # # The string is an optional hint for the user. externalOther @5 :Text; # The secret key material is protected, and can only be unlocked # externally. # # The string is an optional hint for the user, e.g., "Please connect # to the VPN." } } struct PasswordSource { union { inline @0 :Void; # The application must provide the password to unlock the key. # # This means that if a key is locked, then before executing a # private key operation, the key must be unlocked using # [`KeyHandle::unlock`]. inlineWithConfirmation @1 :Void; # The application must provide the password to unlock the key, # but operations must be confirmed externally. # # This means that if a key is locked, then before executing a # private key operation, the key must be unlocked using # [`KeyHandle::unlock`]. These operations may require the user # externally confirm the operation. externalOnDemand @2 :Void; # The user must provide the password out of band. # # The user provides the password using an external PIN pad, or # via a trusted user interface so as to not reveal the password # to the application or system. # # This means if the key is locked, the user will be prompted to # unlock it as a side effect of a private key operation. # # The application can use [`KeyHandle::unlock`] to proactively # cause the user to be prompted to unlock the key. externalSideEffect @3 :Void; # The user must provide the password out of band as a side # effect of an operation. # # The user provides the password using an external PIN pad, or # via a trusted user interface so as to not reveal the password # to the application or system. # # This means if the key is locked, the user will be prompted to # unlock it as a side effect of a private key operation. # # The application cannot proactively cause the user to be # prompted to unlock the key; the prompt is only a side effect # of a private key operation. That is, the key cannot be # unlocked using [`KeyHandle::unlock`]. } } struct ChangePasswordAction { union { password @0 :Data; # Change the password to the specified password. # # If the empty string is provided, this removes any password # protection. ask @1 :Void; # Ask the user for a password, and change the password # accordingly. # # This method only works for keys that have an external password # source. } } struct SignedData { pkAlgo @0 : UInt8; mpis @1 : Data; # The serialized mpi::Signature, without OpenPGP framing. } struct Unit {} # Unit struct. Useful with Result. struct Error { union { genericError @0 :Text; # A generic error occurred. # # Text is a description of the error. protocol @1: Void; eof @2: Void; inaccessibleDecryptionKey @3: List(InaccessibleDecryptionKey); # Returned by `KeyStore::decrypt`. # # The operation failed, because no keys that could be used to # complete the operation are accessible. This could be because # they are locked, or a device that contains a key is # registered, but not connected. notDecryptionCapable @4: Text; # The key cannot be used for decryption. notSigningCapable @5: Text; # The key cannot be used for signing. internalError @6: Text; # An internal server error occurred. externalImportRequired @7: Text; # The backend doesn't support importing keys using this interface. # # This error is returned by [`Backend::import`] if the backend # requires backend-specific information in order to import a # key, which is not provided via this interface. For instance, # when importing a key to a smartcard, the user needs to specify # the smartcard, and the slot to use on the smartcard. Rather # than try and model these parameters using this generic # interface, backends should just have their own tools. The # text should be a hint that is displayed to the user describing # how to find the tool. secretKeyMaterialSealed @8: Text; # The secret key material cannot be exported. # # This error is returned by [`Key::export`] if the secret key # material cannot be exported. Many backends do not allow # exporting secret key material. Some may only allow exporting # secret key material in admin mode. In particular in the # latter case, the text should describe what the user has to do # to export the secret key material. noInlinePassword @9: Text; # The key doesn't support inline passwords. # # Password entry is taken care of by the device managing the # key. For instance, the password may be obtained using an # external PIN pad. noExternalPassword @10: Text; # The key doesn't support getting passwords. # # The password must be provided inline. The password cannot be # obtained using something like an external PIN pad. } } struct Result(T) { union { ok @0 :T; err @1 :Error; } } struct VoidResult { # Generic types can only be pointers. So to return Result<()>, we # need a different type. See: # # 'Kenton Varda' via Cap'n Proto Thu, 30 Apr 2020 15:19:13 -0700 # Subject: Re: [capnproto] Why "Sorry, only pointer types can be used as generic parameters."? # # https://www.mail-archive.com/capnproto@googlegroups.com/msg01286.html union { ok @0 :Void; err @1 :Error; } } struct BoolResult { # Generic types can only be pointers. So to return Result, we # need a different type. See: # # 'Kenton Varda' via Cap'n Proto Thu, 30 Apr 2020 15:19:13 -0700 # Subject: Re: [capnproto] Why "Sorry, only pointer types can be used as generic parameters."? # # https://www.mail-archive.com/capnproto@googlegroups.com/msg01286.html union { ok @0 :Bool; err @1 :Error; } } } sequoia-keystore-0.6.2/src/keystore_protocol_capnp.rs000064400000000000000000000002051046102023000212430ustar 00000000000000// Include the capnp-generated code. #![allow(unused_parens)] include!(concat!(env!("OUT_DIR"), "/src/keystore_protocol_capnp.rs")); sequoia-keystore-0.6.2/src/lib.rs000064400000000000000000002267511046102023000150620ustar 00000000000000//! Sequoia's key store. //! //! Sequoia's key store is a service, which manages and multiplexes //! access to secret key material. Conceptually, keys live on //! devices, and devices are managed by backends. A device may be as //! simple as an on-disk file (a soft key), it may be a smartcard, or //! it could be another key store server that is accessed over the //! network. The key store manages all of these devices, and provides //! a common, higher-level interface. //! //! The key store is a server. It normally lives in a separate //! process, but it may be co-located with the application. Using a //! separate process improves security, because secret key material //! isn't exposed to the application, which can help prevent //! [Heartbleed-style bugs]. It also means that the state can be //! shared, which improves usability. For instance, the server can //! cache passwords. Sometimes, a separate process is not desirable, //! or awkward, e.g., when using the key store from an initrd. In //! these cases, the co-located key store is better. //! //! [Heartbleed-style bugs]: https://heartbleed.com/ use std::any::Any; use std::sync::Arc; use capnp::any_pointer; use capnp::capability; use capnp::capability_list; use capnp::message; use capnp::traits::HasTypeId; use sequoia_openpgp as openpgp; use openpgp::Cert; use openpgp::Fingerprint; use openpgp::KeyID; use openpgp::KeyHandle; use openpgp::crypto::mpi; use openpgp::crypto::SessionKey; use openpgp::crypto::Password; use openpgp::packet; use openpgp::packet::PKESK; use openpgp::packet::key; use openpgp::parse::Parse; use openpgp::types::HashAlgorithm; use openpgp::types::PublicKeyAlgorithm; use openpgp::types::SymmetricAlgorithm; pub use sequoia_ipc; use sequoia_ipc as ipc; pub use ipc::Context; /// Re-export, since this is part of our API. pub use sequoia_directories; mod import_status; pub use import_status::ImportStatus; mod password_source; pub use password_source::PasswordSource; mod protection; pub use protection::Protection; #[allow(dead_code)] mod keystore_protocol_capnp; use crate::keystore_protocol_capnp::keystore; /// Macros managing requests and responses. #[macro_use] mod macros; mod error; pub use error::Error; mod server; mod capnp_relay; use capnp_relay::CapnProtoRelay; use capnp_relay::Cap; use capnp_relay::CapTable; #[cfg(test)] mod testdata; /// Result type. pub type Result = ::std::result::Result; #[doc(hidden)] pub fn descriptor(c: &Context) -> ipc::Descriptor { ipc::Descriptor::new( c, c.home().join("keystore.cookie"), c.lib().join("sequoia-keystore"), server::Keystore::new_descriptor, ) } /// A handle to the key store. pub struct Keystore { relay: Arc, cap: Cap, } impl Keystore { /// Connects to the keystore. /// /// To set the context's home directory, you should usually /// instantiate a `sequoia_dirs::Home` object, and use the value /// returned by /// `home.data_dir(sequoia_dirs::Component::Keystore)`. pub fn connect(c: &Context) -> Result { let descriptor = descriptor(&c); let relay = CapnProtoRelay::new(descriptor)?; let root = relay.root(); Ok(Self { relay: relay, cap: root, }) } crpc!( /// Lists all backends. fn [keystore] backends/0(&mut self) -> Result> -> Result> // Marshal. |_root: keystore::backends_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::backends_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { let r: capability_list::Reader = r; let backends = r.iter() .map(|backend| { let cap: keystore::backend::Client = backend?; Ok(captable.insert(cap.client)) }).collect::>>()?; Ok(backends) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }; |caps: Vec| { let backends: Vec = caps .into_iter() .map(|cap| { Backend { relay: Arc::clone(&self.relay), cap: cap, } }) .collect(); Ok(backends) }); /// Finds the specified keys. /// /// As a key may reside on multiple devices, this may return /// multiple handles for a given key. /// /// The second return value is the list of keys that were not /// found on the keystore. #[async_generic::async_generic] pub fn find_keys(&mut self, ids: &[KeyHandle]) -> Result<(Vec, Vec)> { let mut found: Vec = Vec::new(); let backends = if _sync { self.backends() } else { self.backends_async().await }; for mut b in backends.unwrap_or_else(|_| Vec::new()).into_iter() { let devices = if _sync { b.list() } else { b.list_async().await }; for mut d in devices.unwrap_or_else(|_| Vec::new()).into_iter() { let keys = if _sync { d.list() } else { d.list_async().await }; for mut k in keys.unwrap_or_else(|_| Vec::new()).into_iter() { let id = if _sync { k.id() } else { k.id_async().await }; if let Ok(fpr) = id.and_then(|f| f.parse::()) { if ids.iter().any(|id| id.aliases(KeyHandle::from(&fpr))) { found.push(k); } } } } } let have: Vec = found.iter().map(|k| { KeyHandle::from(k.fingerprint()) }).collect(); let missing = ids .iter() .filter(|id| ! have.iter().any(|fpr| id.aliases(fpr))) .cloned() .collect(); Ok((found, missing)) } /// Finds the specified key. /// /// As a key may reside on multiple devices, this may return /// multiple keys for a given id. #[async_generic::async_generic] pub fn find_key(&mut self, id: KeyHandle) -> Result> { let result = if _sync { self.find_keys(&[id]) } else { self.find_keys_async(&[id]).await }; result.map(|r| r.0) } crpc!( /// Decrypts a PKESK. /// /// The keystore tries to decrypt the PKESKs in an arbitrary /// order. When it succeeds in decrypting a PKESK, it stops /// and returns the decrypted session key. By not enforcing /// an order, the keystore is able to first try keys that are /// immediately available, and only try keys that need to be /// unlocked or connected to if that fails. /// /// On success, this function returns the index of the PKESK /// that was decrypted, the fingerprint of the key that /// decrypted the PKESK, and the plaintext (the symmetric /// algorithm and the session key). fn [keystore]decrypt/1(&mut self, pkesks: &[PKESK]) -> Result<(usize, Fingerprint, SymmetricAlgorithm, SessionKey)> // Marshal. |root: keystore::decrypt_params::Builder| -> Result<()> { // Convert the arguments into the form expected by the RPC. let mut pkesks_param = root.init_pkesks(pkesks.len() as u32); pkesks .iter() .enumerate() .for_each(|(i, pkesk)| { use openpgp::serialize::MarshalInto; let pkesk = pkesk.to_vec() .expect("serializing to a vec is infallible"); pkesks_param.set(i as u32, &pkesk[..]); }); Ok(()) }; // Extract. |relay, captable, response: keystore::decrypt_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { let i = r.get_index() as usize; let fpr = Fingerprint::from_bytes(r.get_fingerprint()?); let algo = SymmetricAlgorithm::from(r.get_algo() as u8); let session_key = SessionKey::from(r.get_session_key()?); let r: (usize, Fingerprint, SymmetricAlgorithm, SessionKey) = (i, fpr, algo, session_key); Ok(r) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }); } /// A handle to a backend. /// /// The key store may have multiple backends. These include a backend /// for soft keys (keys stored on disk), and a `gpg-agent` backend. /// /// Use [`Keystore::backends`] to get a list of the backends that are /// enabled. pub struct Backend { relay: Arc, cap: Cap, } impl Backend { crpc!( /// Returns the backend's ID. fn [keystore::backend]id/0(&mut self) -> Result // Marshal. |_root: keystore::backend::id_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::backend::id_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { Ok(r.to_string()?) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Lists all devices. /// /// Lists the devices managed by a backend. fn [keystore::backend] list/1(&mut self) -> Result> -> Result> // Marshal. |_root: keystore::backend::list_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::backend::list_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { let r: capability_list::Reader = r; let devices = r.iter() .map(|device| { let cap: keystore::device::Client = device?; Ok(captable.insert(cap.client)) }).collect::>>()?; Ok(devices) }, Which::Err(Ok(e)) => Err(Error::from_capnp( Arc::clone(&relay), captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }; |caps: Vec| { let devices: Vec = caps .into_iter() .map(|cap| { Device { relay: Arc::clone(&self.relay), cap: cap, } }) .collect(); Ok(devices) }); crpc!( /// Imports secret key material. /// /// cert is a TSK. Any keys without secret key material are /// silent ignored. /// /// If a key already exists, it is overwritten. /// /// An [`ImportStatus`] is returned for each secret key. If the /// TSK doesn't include any secret keys, then an empty list is /// returned. /// /// Some backends require additional information to import a /// key. These backends should /// [`Error::ExternalImportRequired`], and indicate how a user /// might import a key to this backend. fn [keystore::backend] import/2(&mut self, cert: &Cert) -> Result)>> -> Result> // Marshal. |root: keystore::backend::import_params::Builder| -> Result<()> { use openpgp::serialize::MarshalInto; let bytes = cert.as_tsk().to_vec()?; let cert_param = root.init_cert(bytes.len() as u32); cert_param.copy_from_slice(&bytes); Ok(()) }; // Extract. |relay, captable, response: keystore::backend::import_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { let r: capnp::struct_list::Reader = r; let r = r.iter() .map(|x| { use openpgp::parse::Parse; let status = ImportStatus::try_from(x.get_status()?)?; let key = x.get_key()?; let cap = key.get_handle()?; let pk = key.get_public_key()?; let pk = packet::Key:: ::from_bytes(pk)?; let pk = pk.parts_into_public(); Ok((status, captable.insert(cap.client), pk)) }).collect::)>>>(); Ok(r?) }, Which::Err(Ok(e)) => Err(Error::from_capnp( Arc::clone(&relay), captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }; |caps: Vec<(ImportStatus, Cap, packet::Key<_, _>)>| { let keys: Vec<(ImportStatus, Key)> = caps .into_iter() .map(|(import_status, cap, key)| { ( import_status, Key { relay: Arc::clone(&self.relay), cap: cap, key: key, } ) }) .collect(); Ok(keys) }); // crpc!(fn [keystore::backend]scan/2(&mut self) -> Result<()> // }); // crpc!(fn register(&mut self, description: &str) -> Result // |slf: &mut Self, device| { // Ok(Device { // relay: Arc::clone(&slf.relay), // cap: Arc::new(Mutex::new(device)), // }) // }); } /// A handle to a Device. /// /// A device contains zero or more keys. /// /// Some backends manage multiple physical devices. For instance, the /// smartcard backend exposes each smartcard as a separate device. /// Other backends use the device abstraction to logically group /// related keys together. For instance, the soft keys backend /// exposes each certificate as a separate device. pub struct Device { relay: Arc, cap: Cap, } impl Device { crpc!( /// Returns the device's ID. fn [keystore::device]id/0(&mut self) -> Result // Marshal. |_root: keystore::device::id_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::device::id_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { Ok(r.to_string()?) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// List keys on a device. /// /// Lists the keys on the device. /// /// Some of the returned keys may be known, but not currently /// available. For instance, if a smartcard is not plugged /// in, or an ssh connection is not established. fn [keystore::device] list/1(&mut self) -> Result)>> -> Result> // Marshal. |_root: keystore::device::list_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::device::list_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { let r: capnp::struct_list::Reader = r; let r = r.iter() .map(|x| { use openpgp::parse::Parse; let cap = x.get_handle()?; let pk = x.get_public_key()?; let pk = packet::Key:: ::from_bytes(pk)?; let pk = pk.parts_into_public(); Ok((captable.insert(cap.client), pk)) }).collect::)>>>(); Ok(r?) }, Which::Err(Ok(e)) => Err(Error::from_capnp( Arc::clone(&relay), captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }; |caps: Vec<(Cap, packet::Key<_, _>)>| { let keys: Vec = caps .into_iter() .map(|(cap, key)| { Key { relay: Arc::clone(&self.relay), cap: cap, key: key, } }) .collect(); Ok(keys) }); // /// Forget a device. // /// // /// Unregister the device from the backend. This should not // /// destroy any secret key material stored on the device. // pub fn unregister(&mut self) -> Result<()> { // todo!() // } // /// Unlock a device. // /// // /// Connects to and unlocks a device. // /// // /// Some devices need to be initialized. For instance, to access a // /// remote key, it may be necessary to create an ssh tunnel. Some // /// devices need to be unlocked before the keys can be enumerated. // /// For instance, if soft keys are stored in a database and the // /// database is encrypted, it may be necessary to supply a password to // /// decrypt the database. In this case, the parameter might be // /// "password='1234'". // pub fn unlock(&mut self, _password: &[u8]) -> Result<()> { // todo!(); // } // /// Lock a device. // /// // /// Locks the device if it has been previously unlock. If the device // /// is locked or can't be locked, this is a noop. If the device needs // /// to be deinitialized, it MAY be deinitialized lazily if doing so // /// cannot result in a user-visible error. For instance, if the // /// device uses an ssh tunnel, the ssh tunnel be closed later. A // /// smartcard, however, should be released immediately. // pub fn lock(&mut self) -> Result<()> { // todo!() // } } /// A handle to a key. /// /// A key encapsulates secret key material, and exposes some secret /// key operations like decrypting a message, signing a message, /// changing the key's password, and deleting the secret key material. /// Not all keys implement all operations. In particular, a key will /// typically either implement [`Key::decrypt_ciphertext`] or /// [`Key::sign_message`]. #[derive(Clone)] pub struct Key { relay: Arc, cap: Cap, key: packet::Key, } impl Key { /// Returns the key's fingerprint. pub fn fingerprint(&self) -> Fingerprint { self.key.fingerprint() } /// Returns the key's Key ID. pub fn keyid(&self) -> KeyID { self.key.keyid() } /// Returns the key's public key. pub fn public_key(&self) -> &packet::Key { &self.key } crpc!( /// Returns a unique key identifier. /// /// It should be a well-formed UTF-8 string, which should give a /// curious user a pretty good idea of what key this is. fn [keystore::key]id/0(&mut self) -> Result // Marshal. |_root: keystore::key::id_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::key::id_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { Ok(r.to_string()?) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Unlocks a key. /// /// A key is typically unlocked by providing a password or pin. Not /// all keys are locked. If the key is not available, this should /// attempt to connect to the device. If the device is not available /// or cannot be initialized, then this should fail. fn [keystore::key]unlock/3(&mut self, password: Password) -> Result<()> // Marshal. |mut root: keystore::key::unlock_params::Builder| -> Result<()> { password.map(|password| { root.set_password(&password[..]); }); Ok(()) }; // Extract. |relay, captable, response: keystore::key::unlock_results::Reader| { use keystore::void_result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(()) => Ok(()), Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), } }); // /// Lock a key (optional) // /// // /// Relocks the key. This usually causes the backend to forget the // /// key's password. If the key can't be locked or is already locked, // /// this is a noop. // pub fn lock(&mut self) -> Result<()> { // todo!() // } crpc!( /// Decrypts a ciphertext. /// /// This function corresponds to [`Decryptor::decrypt`]. /// /// When decrypting a message you normally don't want to manually /// try to decrypt each PKESK using this function, but use /// [`Keystore::decrypt`], which first tries to use keys that /// don't require user interaction. /// /// [`Decryptor::decrypt`]: https://docs.sequoia-pgp.org/sequoia_openpgp/crypto/trait.Decryptor.html#tymethod.decrypt /// /// If you want to decrypt a `PKESK`, then you should pass the /// `Key` to `PKESK::decrypt`. fn [keystore::key]decrypt_ciphertext/1(&mut self, ciphertext: &Ciphertext, plaintext_len: Option) -> Result // Marshal. |mut root: keystore::key::decrypt_ciphertext_params::Builder| -> Result<()> { // Convert the arguments into the form expected by the RPC. let algo = ciphertext.pk_algo().map(u8::from).unwrap_or(0); let ciphertext = { use sequoia_openpgp::serialize::MarshalInto; ciphertext.to_vec().expect("serializing to a vec is infallible") }; root.set_algo(algo); root.set_ciphertext(&ciphertext); root.set_plaintext_len(plaintext_len.unwrap_or(0) as u32); Ok(()) }; // Extract. |relay, captable, response: keystore::key::decrypt_ciphertext_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { let r: capnp::data::Reader = r; Ok(SessionKey::from(r)) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Signs a message. /// /// `digest` is the message to sign. fn [keystore::key]sign_message/2(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> Result // Marshal. |mut root: keystore::key::sign_message_params::Builder| -> Result<()> { // Convert the arguments into the form expected by the RPC. root.set_hash_algo(u8::from(hash_algo)); root.set_digest(digest); Ok(()) }; // Extract. |relay, captable, response: keystore::key::sign_message_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(r)) => { let pk_algo = PublicKeyAlgorithm::from(r.get_pk_algo()); let mpis = r.get_mpis()?; let sig = mpi::Signature::parse(pk_algo, mpis)?; Ok(sig) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Whether the key is available. /// /// If false, this usually means the device needs to be /// connected, e.g., a smartcard needs to be plugged in. fn [keystore::key]available/4(&mut self) -> Result // Marshal. |_root: keystore::key::available_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::key::available_results::Reader| { use keystore::bool_result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(b) => { Ok(b) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Whether the key is locked, and the type of protection. fn [keystore::key]locked/5(&mut self) -> Result // Marshal. |_root: keystore::key::locked_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::key::locked_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(protection) => { Protection::try_from(protection?) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// How the password is obtained to unlock the key. /// /// This is independent of whether the key is currently /// protected. fn [keystore::key]password_source/9(&mut self) -> Result // Marshal. |_root: keystore::key::password_source_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::key::password_source_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(password_source) => { PasswordSource::try_from(password_source?) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Whether the key can be used for decryption. fn [keystore::key]decryption_capable/6(&mut self) -> Result // Marshal. |_root: keystore::key::decryption_capable_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::key::decryption_capable_results::Reader| { use keystore::bool_result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(b) => { Ok(b) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Whether the key can be used for signing. fn [keystore::key]signing_capable/7(&mut self) -> Result // Marshal. |_root: keystore::key::signing_capable_params::Builder| -> Result<()> { Ok(()) }; // Extract. |relay, captable, response: keystore::key::signing_capable_results::Reader| { use keystore::bool_result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(b) => { Ok(b) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Exports the secret key material. fn [keystore::key]export/8(&mut self) -> Result> // Marshal. |mut _root: keystore::key::export_params::Builder| -> Result<()> { // Convert the arguments into the form expected by the RPC. Ok(()) }; // Extract. |relay, captable, response: keystore::key::export_results::Reader| { use keystore::result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(Ok(bytes)) => { let key = openpgp::packet::Key::from_bytes(bytes)?; let secret_key = key.parts_into_secret()?; Ok(secret_key) }, Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), // Error reading the result from the response: Which::Ok(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Changes the key's password. fn [keystore::key]change_password/10(&mut self, password: Option<&Password>) -> Result<()> // Marshal. |root: keystore::key::change_password_params::Builder| -> Result<()> { // Convert the arguments into the form expected by the RPC. let mut change_password_action = root.init_password(); if let Some(password) = password { password.map(|password| { change_password_action.set_password(&password[..]); }); } else { change_password_action.set_ask(()); } Ok(()) }; // Extract. |relay, captable, response: keystore::key::change_password_results::Reader| { use keystore::void_result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(()) => Ok(()), Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), } }); crpc!( /// Deletes the specified key's secret key material. /// /// On success, the key is no registered with the device, and /// future operations on the current key handle will fail. fn [keystore::key]delete_secret_key_material/11(&mut self) -> Result<()> // Marshal. |_root: keystore::key::delete_secret_key_material_params::Builder| -> Result<()> { // Convert the arguments into the form expected by the RPC. Ok(()) }; // Extract. |relay, captable, response: keystore::key::delete_secret_key_material_results::Reader| { use keystore::void_result::Which; match response.get_result()?.which()? { // The RPC's result: Which::Ok(()) => Ok(()), Which::Err(Ok(e)) => Err(Error::from_capnp(relay, captable, e)), // Protocol violations: // Protocol error: Which::Err(Err(e)) => Err(anyhow::Error::from(e)), } }); } /// Information about key that could not be used for decryption. /// /// This is returned by [`Keystore::decrypt`] when a key appears to be /// able to decrypt a message, but the key isn't accessible, e.g., /// because it is locked. #[derive(Clone)] pub struct InaccessibleDecryptionKey { key: Key, pkesk: PKESK, } impl std::fmt::Debug for InaccessibleDecryptionKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "InaccessibleDecryptionKey {{ {} }}", self.key.fingerprint()) } } impl InaccessibleDecryptionKey { /// Returns the key handle. pub fn key(&self) -> &Key { &self.key } /// Returns the key handle. pub fn key_mut(&mut self) -> &mut Key { &mut self.key } /// Returns the key handle. pub fn into_key(self) -> Key { self.key } /// Returns the PKESK that could not be decrypted. /// /// Normally, you'd follow up the failed generic decryption by /// retrying with the unlocked key, and the relevant PKESK. pub fn pkesk(&self) -> &PKESK { &self.pkesk } } use openpgp::crypto::Decryptor; use openpgp::crypto::Signer; use openpgp::crypto::mpi::Ciphertext; use openpgp::crypto::mpi::Signature; impl Decryptor for &mut Key { fn public(&self) -> &packet::Key { self.public_key() } fn decrypt( &mut self, ciphertext: &Ciphertext, plaintext_len: Option ) -> Result { ::decrypt(self, ciphertext, plaintext_len) } } impl Decryptor for Key { fn public(&self) -> &packet::Key { self.public_key() } fn decrypt( &mut self, ciphertext: &Ciphertext, plaintext_len: Option ) -> Result { self.decrypt_ciphertext(ciphertext, plaintext_len) } } impl Signer for &mut Key { fn public(&self) -> &packet::Key { self.public_key() } fn sign( &mut self, hash_algo: HashAlgorithm, digest: &[u8] ) -> Result { ::sign(self, hash_algo, digest) } } impl Signer for Key { fn public(&self) -> &packet::Key { self.public_key() } fn sign( &mut self, hash_algo: HashAlgorithm, digest: &[u8] ) -> Result { self.sign_message(hash_algo, digest) } } #[cfg(test)] mod tests { use test_log::test; use super::*; use anyhow::Context as _; use openpgp::Cert; use openpgp::cert::CertBuilder; use openpgp::PacketPile; use openpgp::packet::PKESK; use openpgp::packet::Packet; use openpgp::packet::SEIP; use openpgp::packet::signature::SignatureBuilder; use openpgp::parse::PacketParserBuilder; use openpgp::parse::PacketParserResult; use openpgp::parse::Parse; use openpgp::types::SignatureType; use sequoia_directories::Home; use sequoia_directories::Component; use testdata::password; use testdata::simple; /// Tries to decrypt the message using the key store. /// /// Iterates over each of the PKESKs and tries to decrypt them /// using a key on the keystore. If the key is in recipients and /// has a password, the key is also unlocked, if needed. /// /// If assert_exact is true, asserts that exactly the recipients /// in `recipients` decrypt the message. That is, if there are /// two recipients, then both must decrypt the message, and no /// others. /// /// Returns the keys that decrypted the message and whether they /// were in `recipients`. fn try_decrypt(ks: &mut Keystore, msg: &[u8], recipients: &[(Fingerprint, Option<&str>)], assert_exact: bool) -> Result> { let mut results: Vec<(Fingerprint, bool)> = Vec::new(); let mut saw_pkesk = false; let mut ppr = PacketParserBuilder::from_bytes(msg) .expect("PacketParserBuilder") .buffer_unread_content() .build() .context("Parsing message")?; while let PacketParserResult::Some(pp) = ppr { // Get the packet out of the parser and start parsing // the next packet, recursing. let (packet, next_ppr) = pp.recurse()?; ppr = next_ppr; if let Packet::PKESK(pkesk) = packet { saw_pkesk = true; let recipient = KeyHandle::from(pkesk.recipient()); let should_decrypt = recipients.iter() .find(|(r, _password)| { recipient.aliases(KeyHandle::from(r)) }); let kh = match ks.find_key(recipient.clone()) { Ok(kh) => kh, Err(err) => { if should_decrypt.is_some() { eprintln!("{} should decrypt message, \ but isn't on the key store: {}", recipient, err); } continue; } }; let mut kh = if let Some(kh) = kh.into_iter().next() { kh } else { continue; }; let fpr = kh.fingerprint(); assert!(recipient.aliases(KeyHandle::from(&fpr))); let recipient: Fingerprint = fpr; let decrypted = pkesk.decrypt(&mut kh, None); match (decrypted, should_decrypt) { (Some(_), Some((_, _))) => { eprintln!("PKESK ({}) decrypted as expected.", recipient); results.push((recipient.clone(), true)); } (Some(_), None) => { eprintln!("PKESK ({}) unexpectedly decrypted!", recipient); results.push((recipient.clone(), false)); } (None, Some((_, password))) => { if let Some(password) = password { if let Err(err) = kh.unlock(Password::from(*password)) { eprintln!("Failed to unlock {}: {}", recipient, err); } else { if pkesk.decrypt(&mut kh, None).is_some() { results.push((recipient.clone(), true)); } else { eprintln!("Failed to decrypt PKESK for {} \ after unlocking it", recipient); } } } else { eprintln!("Failed to unlock PKESK for {}", recipient); } } (None, None) => { eprintln!("Failed to decrypt PKESK ({}), \ and shouldn't have.", recipient); } } } } // All messages have at least one PKESK. assert!(saw_pkesk); if assert_exact { let mut decryptors = results.iter() .map(|(decryptor, expected)| { assert!(expected, "Unexpected decrypted PKESK for {}", decryptor); decryptor }) .collect::>(); decryptors.sort(); let mut recipients: Vec<&Fingerprint> = recipients.iter().map(|(r, _)| r).collect(); recipients.sort(); assert_eq!(decryptors, recipients); } Ok(results) } // A simple test that lists all of the keys. #[test] fn simple_ping() -> Result<()> { let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); let source = simple::dir().join("keystore"); dircpy::copy_dir(&source, &keystore_home) .with_context(|| format!("Copying {:?} to {:?}", source, keystore_home))?; // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; let mut backends = ks.backends()?; eprintln!("Got {} backends", backends.len()); for backend in &mut backends { eprintln!(" - {}", backend.id()?); } eprintln!("Listing devices"); let mut saw_softkeys = false; for backend in &mut backends { let is_softkeys = backend.id().map(|id| id == "softkeys") .unwrap_or(false); if is_softkeys { saw_softkeys = true; } eprintln!(" - {}", backend.id()?); let devices = backend.list()?; if is_softkeys { assert_eq!(devices.len(), 3); } for mut device in devices { eprintln!(" - {}", device.id()?); let keys = device.list()?; if is_softkeys { assert_eq!(keys.len(), 4); } for mut key in keys.into_iter() { eprintln!(" - {}", key.id()?); } } } assert!(saw_softkeys); Ok(()) } // Bash on the relay from multiple threads. #[test] fn simple_bash() -> Result<()> { use std::thread; use std::sync::Mutex; let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); let source = simple::dir().join("keystore"); dircpy::copy_dir(&source, &keystore_home) .with_context(|| format!("Copying {:?} to {:?}", source, keystore_home))?; // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let ks = Arc::new(Mutex::new(Box::new(Keystore::connect(&c)?))); let handles: Vec<_> = (0..10u32).map(|t| { let ks = Arc::clone(&ks); thread::spawn(move || -> Result<()> { for _ in 0..10 { thread::yield_now(); let mut ks = ks.lock().unwrap(); let backends: Vec = ks.backends()?; // Drop ks to increase concurrency. drop(ks); let mut saw_softkeys = false; for (b, mut backend) in &mut backends.into_iter().enumerate() { let is_softkeys = backend.id().map(|id| id == "softkeys") .unwrap_or(false); if is_softkeys { saw_softkeys = true; } // Don't just use a capability once and then throw it // away. for _ in 1..3 { eprintln!(" {}.{}. {}", t, b, backend.id()?); let devices = backend.list()?; if is_softkeys { assert_eq!(devices.len(), 3); } for (d, mut device) in devices.into_iter().enumerate() { for _ in 1..3 { eprintln!(" {}.{}.{}. {}", t, b, d, device.id()?); let keys = device.list()?; if is_softkeys { assert_eq!(keys.len(), 4); } for (k, mut key) in keys.into_iter().enumerate() { eprintln!(" {}.{}.{}.{}. {}", t, b, d, k, key.id()?); } } } } } assert!(saw_softkeys); } Ok(()) }) }).collect(); for h in handles.into_iter() { h.join().unwrap().expect("worked"); } Ok(()) } // Test Keystore::decrypt using the simple keystore. #[test] fn simple_keystore_decrypt() -> Result<()> { let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); let source = simple::dir().join("keystore"); dircpy::copy_dir(&source, &keystore_home) .with_context(|| format!("Copying {:?} to {:?}", source, keystore_home))?; // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; // Run the tests. let mut bad = 0; for msg in simple::MSGS.iter() { eprintln!("Decrypting {}", msg.filename); let mut pkesks = Vec::new(); let mut decrypted = false; let mut ppr = PacketParserBuilder::from_bytes( testdata::file(msg.filename))? .buffer_unread_content() .build()?; while let PacketParserResult::Some(mut pp) = ppr { if let Packet::SEIP(_) = pp.packet { match ks.decrypt(&pkesks[..]) { // Decrypted, but shouldn't have. Ok(_) if msg.recipients.is_empty() => { eprintln!("Decrypted, but should have failed"); bad += 1; } // Decrypted, and should have. Ok((i, fpr, algo, sk)) => { let fpr = KeyHandle::from(fpr); // Make sure we used an expected PKESK. if ! msg.recipients.iter().any(|&r| { KeyHandle::from(r).aliases(&fpr) }) { eprintln!("fpr ({}) is not an expected \ recipient ({})", fpr, msg.recipients .iter() .map(|r| r.to_string()) .collect::>() .join(", ")); bad += 1; break; } // Make sure the fpr and the pkesk match. if ! fpr.aliases( KeyHandle::from(pkesks[i].recipient())) { eprintln!("fpr ({}) and pkesk ({}) \ don't match", fpr, pkesks[i].recipient()); bad += 1; break; } // Decrypt the SEIP. match pp.decrypt(algo, &sk) { Ok(()) => { decrypted = true; } Err(err) => { eprintln!("Failed to decrypt SEIP: {}", err); bad += 1; break; } } } // Can't decrypt, as expected. Err(_) if msg.recipients.is_empty() => break, // Can't decrypt, and that's a problem. Err(err) => { eprintln!("Failed to decrypt: {}", err); bad += 1; break; } } } // Get the packet out of the parser and start parsing // the next packet, recursing. let (packet, next_ppr) = pp.recurse()?; ppr = next_ppr; match packet { Packet::PKESK(pkesk) if ! decrypted => pkesks.push(pkesk), Packet::SEIP(_) if decrypted => (), Packet::MDC(_) if decrypted => (), Packet::CompressedData(_) if decrypted => (), Packet::Literal(lit) if decrypted => { if msg.content.as_bytes() != lit.body() { eprintln!("Decrypted plaintext does not match \ expected plaintext:\n got: {:?}\n\ expected: {:?}", String::from_utf8_lossy(lit.body()), msg.content); bad += 1; break; } } p => unreachable!("Unexpected packet: {}.", p.tag()), } } } if bad > 0 { panic!("{} tests failed", bad); } Ok(()) } // Test Key::decrypt_pkesk using the simple keystore. #[test] fn simple_key_decrypt_pkesk() -> Result<()> { let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); let source = simple::dir().join("keystore"); dircpy::copy_dir(&source, &keystore_home) .with_context(|| format!("Copying {:?} to {:?}", source, keystore_home))?; // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; // We also try to decrypt with Alice's and Carol's primary // keys to make sure that that fails. let alice_pri = ks.find_key(KeyHandle::from(&*simple::alice_pri)) .expect("keystore is up"); // In the simple keystore, keys are not repeated. assert!(alice_pri.len() <= 1); let mut alice_pri = if let Some(kh) = alice_pri.into_iter().next() { kh } else { panic!("Alice's primary key is not present on the key store."); }; let carol_pri = ks.find_key(KeyHandle::from(&*simple::carol_pri)) .expect("keystore is up"); // In the simple keystore, keys are not repeated. assert!(carol_pri.len() <= 1); let mut carol_pri = if let Some(kh) = carol_pri.into_iter().next() { kh } else { panic!("Carol's primary key is not present on the key store."); }; // Run the tests. let mut bad = 0; for msg in simple::MSGS.iter() { eprintln!("Decrypting {}", msg.filename); let mut saw_pkesk = false; let mut ppr = PacketParserBuilder::from_bytes( testdata::file(msg.filename))? .buffer_unread_content() .build()?; while let PacketParserResult::Some(pp) = ppr { // Get the packet out of the parser and start parsing // the next packet, recursing. let (packet, next_ppr) = pp.recurse()?; ppr = next_ppr; if let Packet::PKESK(pkesk) = packet { saw_pkesk = true; let recip = KeyHandle::from(pkesk.recipient()); let should_decrypt = msg.recipients .iter() .any(|&r| { KeyHandle::from(r).aliases(recip.clone()) }); let kh = ks.find_key(recip.clone())?; // In the simple keystore, keys are not repeated. assert!(kh.len() <= 1); let mut kh = if let Some(kh) = kh.into_iter().next() { kh } else { // No key corresponding to recip in the key // store. if should_decrypt { eprintln!("Should decrypt PKESK ({}), \ but there is no key for it on the \ key store.", recip); bad += 1; } continue; }; let decrypted = pkesk.decrypt(&mut kh, None); match (decrypted, should_decrypt) { (Some(_), true) => { eprintln!("PKESK ({}) decrypted as expected.", recip); } (Some(_), false) => { eprintln!("PKESK ({}) unexpectedly decrypted!", recip); bad += 1; } (None, true) => { eprintln!("Failed to decrypt PKESK ({}), \ but should have!", recip); bad += 1; } (None, false) => { eprintln!("Failed to decrypt PKESK ({}), \ and shouldn't have.", recip); } } // Make sure we can't decrypt using alice's or // carol's primary keys. if pkesk.decrypt(&mut alice_pri, None).is_some() { eprintln!("Unexpected decrypted message using \ alice's primary key"); bad += 1; } if pkesk.decrypt(&mut carol_pri, None).is_some() { eprintln!("Unexpected decrypted message using \ carol's primary key"); bad += 1; } } } // All messages have at least one PKESK. assert!(saw_pkesk); } if bad > 0 { panic!("{} tests failed", bad); } Ok(()) } #[test] fn simple_key_sign() -> Result<()> { let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); let source = simple::dir().join("keystore"); dircpy::copy_dir(&source, &keystore_home) .with_context(|| format!("Copying {:?} to {:?}", source, keystore_home))?; let certs = [ testdata::file("simple/keystore/softkeys/alice.pgp"), testdata::file("simple/keystore/softkeys/bob.pgp"), testdata::file("simple/keystore/softkeys/carol.pgp") ]; let certs: Vec = certs.iter() .map(|cert| { Cert::from_bytes(cert) }) .collect::>>()?; let lookup = |fpr: Fingerprint| { certs .iter() .flat_map(|cert| cert.keys()) .find(|k| { k.fingerprint() == fpr }) }; // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; let msg = b"hi?!"; let mut bad = 0; let mut backends = ks.backends()?; let mut saw_softkeys = false; for backend in &mut backends { let is_softkeys = backend.id().map(|id| id == "softkeys") .unwrap_or(false); if is_softkeys { saw_softkeys = true; } else { continue; } let devices = backend.list()?; assert_eq!(devices.len(), 3); for mut device in devices { let keys = device.list()?; assert_eq!(keys.len(), 4); for mut key in keys.into_iter() { let ka = lookup(key.fingerprint()) .expect("have corresponding certificate"); eprintln!(" - {} ({}, {})", ka.fingerprint(), ka .cert() .userids() .next() .map(|ua| { String::from_utf8_lossy(ua.value()) .into_owned() }) .unwrap_or_else(|| { String::from("") }), ka.pk_algo()); // Note: we don't consider keyflags, because the // key store can't either (even if a backend has // the whole cert, it may not be up to date). let bad_key = ! ka.pk_algo().for_signing(); let sig = SignatureBuilder::new(SignatureType::Binary); match sig.sign_message(&mut key, msg) { Ok(sig) => { if bad_key { eprintln!("Key created signature, \ but shouldn't have."); bad += 1; continue; } // Make sure the signature checks out. if let Err(err) = sig.clone() .verify_message(ka.key(), msg) { eprintln!("Failed to verify signature, \ but should have: {}", err); bad += 1; continue; } } Err(err) => { if ! bad_key { eprintln!("Key failed to create signature, \ but should have: {}", err); bad += 1; continue; } } } } } } assert!(saw_softkeys); if bad > 0 { panic!("{} tests failed", bad); } Ok(()) } // Test Key::decrypt_pkesk using the password keystore. #[test] fn password_key_decrypt_pkesk() -> Result<()> { let _ = env_logger::Builder::from_default_env().try_init(); let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); let source = password::dir().join("keystore"); dircpy::copy_dir(&source, &keystore_home) .with_context(|| format!("Copying {:?} to {:?}", source, keystore_home))?; // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; // Run the tests. for (msg, pw) in password::MSGS.iter() { eprintln!("Decrypting {}", msg.filename); let content = testdata::file(msg.filename); let recipients = msg.recipients.iter().map(|&r| (r.clone(), *pw)) .collect::)>>(); try_decrypt( &mut ks, content, &recipients[..], true) .unwrap(); } Ok(()) } // Test Key::decrypt_pkesk using the password keystore. #[test] fn password_key_decrypt_pkesk2() -> Result<()> { let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); let source = password::dir().join("keystore"); dircpy::copy_dir(&source, &keystore_home) .with_context(|| format!("Copying {:?} to {:?}", source, keystore_home))?; // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; for (msg, password) in testdata::password::MSGS.iter() { eprintln!("{}", msg.filename); let pp = PacketPile::from_bytes(testdata::file(msg.filename)) .expect("valid OpenPGP message"); pp.children().enumerate().for_each(|(i, p)| { eprintln!(" {}: {}", i, p.tag()); }); let pkesks: Vec = pp.children() .filter_map(|p| { match p { Packet::PKESK(p) => Some(p.clone()), _ => None, } }) .collect(); assert!(pkesks.len() > 0); let seip: Vec<&SEIP> = pp.children() .filter_map(|p| { match p { Packet::SEIP(p) => Some(p), _ => None, } }) .collect(); assert!(seip.len() == 1); match (ks.decrypt(&pkesks), password) { (Ok(_), None) => { // Decrypted and no password is required. Success. } (Ok(_), Some(_password)) => { panic!("Decrypted, but a password should be required."); } (Err(err), None) => { panic!("Failed to decrypt, but a password isn't required: {}.", err); } (Err(mut err), Some(password)) => { let keys = if let Some(Error::InaccessibleDecryptionKey(keys)) = err.downcast_mut() { keys } else { panic!("Unexpected error decrypting message: {}", err); }; // There's only one. assert_eq!(keys.len(), 1); let key = &mut keys[0]; let pkesk = key.pkesk().clone(); // Try to decryt it; it should still be locked. let r = pkesk.decrypt(key.key_mut(), None); assert!(r.is_none()); // Unlock it and try again. key.key_mut().unlock(Password::from(*password)) .expect("correct password"); let r = pkesk.decrypt(key.key_mut(), None); assert!(r.is_some()); } } } Ok(()) } // Test Key::import and Key::export. #[test] fn import_export() -> Result<()> { let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; let mut softkeys = None; for mut backend in ks.backends()?.into_iter() { if backend.id().expect("ok") == "softkeys" { softkeys = Some(backend); break; } } let mut softkeys = softkeys.expect("have softkeys backend"); use openpgp::cert::CipherSuite::*; for cs in [ Cv25519, RSA3k, P256, P384, P521, RSA2k, RSA4k ] { if cs.is_supported().is_err() { continue; } eprintln!("Testing a {:?} cert", cs); let (cert, _rev) = CertBuilder::general_purpose( cs, Some("alice")) .generate()?; softkeys.import(&cert).expect("can import"); for key in cert.keys() { eprintln!("Exporting {}", key.fingerprint()); let key = key.parts_into_secret().expect("has secret"); let remote = ks.find_key(key.key_handle()).expect("have key"); assert_eq!(remote.len(), 1, "only one key under this name"); let mut remote = remote.into_iter().next().unwrap(); assert_eq!(remote.fingerprint(), key.fingerprint()); let export = remote.export().expect("can export"); assert_eq!(&export, key.key()); } } Ok(()) } // Test Key::password_source. #[test] fn password_source() -> Result<()> { let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; let mut softkeys = None; for mut backend in ks.backends()?.into_iter() { if backend.id().expect("ok") == "softkeys" { softkeys = Some(backend); break; } } let mut softkeys = softkeys.expect("have softkeys backend"); // This is a pretty naive test: we just make sure the backend // returns success. for password in [false, true] { let mut builder = CertBuilder::general_purpose(None, Some("alice")); if password { builder = builder.set_password(Some("foo".into())); } let (cert, _rev) = builder.generate()?; let result = softkeys.import(&cert).expect("can import"); assert_eq!(result.len(), cert.keys().count()); for (_import_status, mut key) in result.into_iter() { match key.locked() { Ok(Protection::Unlocked) => { if password { panic!("Key is password protected and should be locked, \ but is unlocked?"); } } Ok(other) => { if ! password { panic!("Key is not password protected and shouldn't \ be locked, but got: {:?}", other); } } Err(err) => { panic!("Getting locked status: {}", err); } } assert!(key.password_source().is_ok()); } } Ok(()) } // Test Key::change_password #[test] fn change_password() -> Result<()> { let _ = env_logger::Builder::from_default_env().try_init(); let home = Home::ephemeral().unwrap(); eprintln!("Using {}", home); let keystore_home = home.data_dir(Component::Keystore); let source = password::dir().join("keystore"); dircpy::copy_dir(&source, &keystore_home) .with_context(|| format!("Copying {:?} to {:?}", source, keystore_home))?; // Start and connect to the keystore. let c = Context::configure() .ephemeral() .home(keystore_home) .build()?; let mut ks = Keystore::connect(&c)?; // Run the tests. for (msg, pw) in password::MSGS.iter() { eprintln!("Decrypting {}", msg.filename); let content = testdata::file(msg.filename); let new_password = "a NEW pa$$word"; let recipients = msg.recipients.iter().map(|&r| (r.clone(), *pw)) .collect::)>>(); try_decrypt( &mut ks, content, &recipients[..], true) .unwrap(); // Change the passwords. let recipients = msg.recipients.iter() .map(|&r| { let kh = ks.find_key(KeyHandle::from(r)) .expect(&format!("Have key {}", r)); let mut kh = if let Some(kh) = kh.into_iter().next() { kh } else { panic!("Should have a key for {}", r); }; if let Some(pw) = *pw { if let Protection::Password(_hint) = kh.locked().expect("locked") { kh.unlock(Password::from(pw)) .expect("Can unlock locked key"); } } kh.change_password(Some(&Password::from(new_password))) .expect("can change password"); (r.clone(), Some(new_password)) }) .collect::)>>(); try_decrypt( &mut ks, content, &recipients[..], true) .unwrap(); } Ok(()) } } sequoia-keystore-0.6.2/src/macros.rs000064400000000000000000000152311046102023000155650ustar 00000000000000//! Macros to simplify calling and handling RPCs. //! //! These macros are specialized for our protocol, which: //! //! - Use the Result(T) type to communicate rich errors to the //! client. //! //! Cap'n Proto's generic variables must be pointers. Thus to //! model non-pointer types like Result<()>, we need a separate //! type. See this [message]. //! //! [message]: https://www.mail-archive.com/capnproto@googlegroups.com/msg01286.html //! //! - Call RPCs synchronously. //! //! Since Cap'n Proto is asynchronous, we use Tokio in the //! background. //! //! For wrapping an RPC, the [`crpc!`] macro is most convenient. The //! [`send_rpc!`] macro is useful when more control is needed. //! //! For dispatching an RPC, the [`srpc!`] macro is most convenient. /// Wraps an RPC call. /// /// This macro synthesizes a method, which marshals an RPC request, /// synchronously calls the RPC, and massages the result. /// /// `self.relay` must be a reference to a `CapnProtoRelay` executor, /// and `self.cap` must be a capability (as wrapped by /// `CapnProtoRelay`) to invoke the method on. /// /// `$f` is simultaneously the method name and the name of the RPC /// that should be invoked. `$method_id` is its method identifier as /// set in the capnproto IDL file. (Unfortunately, the method ID is /// not given a symbolic name by the capnproto IDL compiler so it /// needs to be provided explicitly.) /// /// `$marshal` is a closure, which is called to set the method's /// parameters. It is passed the root of the parameters data /// structure, i.e., the `METHOD_params::Builder` data structure, /// which is generated by the IDL compiler. /// /// `$extract1` is a closure, which is called by the relay to allow /// the caller to extract the results from the server's response. It /// is called directly by the relay thread. This means that we can /// pass a reference to the response, which avoids unnecessarily /// copying data. But, more importantly for the problem that this /// module is trying to solve, executing it in the same thread as the /// relay gets around the problem that capnproto capabilities are not /// `Send` and `Sync`, and consequently a message that contains /// capabilities can't be sent to another thread. A result of this /// is that the closure must be `Send` and `Sync`, since it is /// executed by a different thread. The closure is passed the relay's /// capability table (`CapTable`), and the root of the response /// (`METHOD_results::Builder`). If a returned capability needs to be /// used later, then this closure should wrap it using /// `captable.insert`, and save the result. /// /// Note: you can sometimes avoid the cost of copying the data by /// massaging it directly in this closure, but be careful as this /// function blocks the relay's main thread. /// /// `$extract2` is a closure, which is called on the result of /// `$extract1`. It is executed in the context of the caller. It can /// be used to incorporate data from `self`. (If data from self flows /// into the result of `$extract1`, then `self` would have to have a /// `'static` lifetime, which is doesn't have.) macro_rules! crpc { ($(#[$outer:meta])* fn [$mod:path] $f:ident/$method_id:literal(&mut $self:ident $(, $v:ident: $t:ty)*) -> Result<$rt:ty> $marshal:expr; // A close called as f(root) -> Result<()> $extract1:expr // A closure called as f(response) -> Result<$rt> ) => { crpc!( $(#[$outer])* fn [$mod] $f/$method_id(&mut self $(, $v: $t)*) -> Result<$rt> -> Result<$rt> $marshal; $extract1; |r| Ok(r)); }; ($(#[$outer:meta])* fn [$mod:path] $f:ident/$method_id:literal(&mut $self:ident $(, $v:ident: $t:ty)*) -> Result<$irt:ty> -> Result<$rt:ty> $marshal:expr; // A closure called as f(root) -> Result<()> $extract1:expr; // A closure called as f(response) -> Result<$irt> $extract2:expr // A closure called as f($irt) -> Result<$rt> ) => { $(#[$outer])* #[async_generic::async_generic] pub fn $f(&mut $self $(, $v: $t)*) -> Result<$rt> { log::trace!("{}::{}", stringify!($mod), stringify!($f)); use $mod as m; paste::paste! { use m::[< $f _params >] as params; use m::[< $f _results >] as results; }; let mut message = message::TypedBuilder::::new_default(); // Set the parameters. // e.g., keystore::key::sign_message_params::Builder let root: params::Builder = message.init_root(); let result: Result<()> = $marshal(root); result?; let message = message::TypedReader::from(message); let extract1: Box, &mut crate::CapTable, results::Reader) -> Result<$irt> + Send + Sync> = Box::new($extract1); let f = |relay: std::sync::Arc, cap_table: &mut crate::CapTable, response: capability::Response| -> Result> { // e.g., keystore::key::sign_message_results::Reader let response: results::Reader = response.get()?.get_as()?; Ok(extract1(relay, cap_table, response).map(|r| { Box::new(r) as Box })?) }; let handle = if _sync { $self.relay.send_rpc($self.cap.clone(), ::TYPE_ID, $method_id, message.into_inner(), f)? } else { $self.relay.send_rpc_async($self.cap.clone(), ::TYPE_ID, $method_id, message.into_inner(), f).await? }; let response = if _sync { CapnProtoRelay::await_reply(handle)? } else { CapnProtoRelay::await_reply_async(handle).await? }; let r: $irt = *response.downcast().unwrap(); let extract2: Box Result<$rt>> = Box::new($extract2); let r: Result<$rt> = extract2(r); Ok(r?) } }; } sequoia-keystore-0.6.2/src/password_source.rs000064400000000000000000000025511046102023000175240ustar 00000000000000pub use sequoia_keystore_backend::PasswordSource; use crate::Result; use crate::keystore_protocol_capnp::keystore; impl TryFrom> for PasswordSource { type Error = anyhow::Error; fn try_from(reader: keystore::password_source::Reader<'_>) -> Result { use keystore::password_source::Which; match reader.which()? { Which::Inline(()) => Ok(PasswordSource::Inline), Which::InlineWithConfirmation(()) => Ok(PasswordSource::InlineWithConfirmation), Which::ExternalSideEffect(()) => Ok(PasswordSource::ExternalSideEffect), Which::ExternalOnDemand(()) => Ok(PasswordSource::ExternalOnDemand), } } } impl keystore::password_source::Builder<'_> { /// Serialize PasswordSource. pub fn set_password_source(&mut self, password_source: PasswordSource) -> Result<()> { match password_source { PasswordSource::Inline => self.set_inline(()), PasswordSource::InlineWithConfirmation => self.set_inline_with_confirmation(()), PasswordSource::ExternalSideEffect => self.set_external_side_effect(()), PasswordSource::ExternalOnDemand => self.set_external_on_demand(()), } Ok(()) } } sequoia-keystore-0.6.2/src/protection.rs000064400000000000000000000044151046102023000164710ustar 00000000000000pub use sequoia_keystore_backend::Protection; use crate::Result; use crate::keystore_protocol_capnp::keystore; impl TryFrom> for Protection { type Error = anyhow::Error; fn try_from(reader: keystore::protection::Reader<'_>) -> Result { use keystore::protection::Which; let extract = |msg: capnp::text::Reader<'_>| -> Result<_> { let msg = msg.to_string()?; if msg.is_empty() { Ok(None) } else { Ok(Some(msg)) } }; match reader.which()? { Which::Unlocked(()) => Ok(Protection::Unlocked), Which::UnknownProtection(msg) => { Ok(Protection::UnknownProtection(extract(msg?)?)) } Which::Password(msg) => { Ok(Protection::Password(extract(msg?)?)) } Which::ExternalPassword(msg) => { Ok(Protection::ExternalPassword(extract(msg?)?)) } Which::ExternalTouch(msg) => { Ok(Protection::ExternalTouch(extract(msg?)?)) } Which::ExternalOther(msg) => { Ok(Protection::ExternalOther(extract(msg?)?)) } } } } impl keystore::protection::Builder<'_> { /// Serialize Protection. pub fn set_protection(&mut self, protection: Protection) -> Result<()> { match protection { Protection::Unlocked => self.set_unlocked(()), Protection::UnknownProtection(msg) => { self.set_unknown_protection( msg.as_deref().unwrap_or("")) } Protection::Password(msg) => { self.set_password( msg.as_deref().unwrap_or("")) } Protection::ExternalPassword(msg) => { self.set_external_password( msg.as_deref().unwrap_or("")) } Protection::ExternalTouch(msg) => { self.set_external_touch( msg.as_deref().unwrap_or("")) } Protection::ExternalOther(msg) => { self.set_external_other( msg.as_deref().unwrap_or("")) } } Ok(()) } } sequoia-keystore-0.6.2/src/server/backend.rs000064400000000000000000000141141046102023000171750ustar 00000000000000use std::collections::BTreeMap; use std::collections::btree_map::Entry as BTreeEntry; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; use sequoia_openpgp as openpgp; use openpgp::Result; use sequoia_directories::Home; use sequoia_directories::Component; #[cfg(feature = "softkeys")] use sequoia_keystore_softkeys as softkeys; #[cfg(feature = "openpgp-card")] use sequoia_keystore_openpgp_card as openpgp_card; #[cfg(feature = "tpm")] use sequoia_keystore_tpm as tpm; #[cfg(feature = "gpg-agent")] use sequoia_keystore_gpg_agent as gpg_agent; use sequoia_keystore_backend::Backend; /// The keystore servers. /// /// We have one root for each "home" directory. pub struct Servers { servers: BTreeMap>>, } impl Servers { const fn new() -> Self { Servers { servers: BTreeMap::new(), } } /// Returns the server for the specified "home" directory. /// /// This is normally derived from an instance of /// `sequoia_dirs::Home`. Specifically, its the value returned by /// `home.data_dir(sequoia_dirs::Component::Keystore)`. pub(crate) async fn get(home: &Path) -> Result>> { let mut servers = SERVERS.lock().await; let home = home.to_path_buf(); match servers.servers.entry(home.clone()) { BTreeEntry::Occupied(oe) => Ok(Arc::clone(oe.get())), BTreeEntry::Vacant(ve) => { let server = Arc::new(Mutex::new(Server::new(home).await?)); ve.insert(Arc::clone(&server)); Ok(server) } } } } lazy_static::lazy_static! { /// A thread-safe reference to the SERVERS singleton. static ref SERVERS: Mutex = { Mutex::new(Servers::new()) }; } /// A Server. pub struct Server { pub backends: Vec>, } impl Server { /// Initializes the server. /// /// This may be called at most once. /// /// `home` is the home directory to use. You should usually /// instantiate a `sequoia_dirs::Home` object, and use the value /// returned by /// `home.data_dir(sequoia_dirs::Component::Keystore)`. /// /// Each backend is passed a backend-specific home directory of /// the form `HOME/$BACKEND`. async fn new(home: PathBuf) -> Result { log::trace!("Server::new({})", home.display()); let mut backends = Vec::new(); // Whether this is the default keystore server, or an // alternative keystore server. The `default` keystore uses // shared resources by default. An alternate keystore should // only uses local resources by default, and shared resources // that the user explicitly enabled. let is_default_keystore = Home::default() .and_then(|default| { let default_keystore = default.data_dir(Component::Keystore); Home::aliases(&home, &default_keystore).ok() }) .unwrap_or(false); let mut add = |backend: Result>| { match backend { Ok(backend) => backends.push(backend), Err(err) => { #[allow(unused_mut)] let mut shown = false; #[cfg(feature = "gpg-agent")] use gpg_agent::sequoia_gpg_agent; #[cfg(feature = "gpg-agent")] if let Some(sequoia_gpg_agent::Error::GnuPG( sequoia_gpg_agent::gnupg::Error::GPGConfMissing)) = err.downcast_ref() { log::info!("Failed to initialize backend: {:#}", err); shown = true; } #[cfg(feature = "gpg-agent")] if let Some(sequoia_gpg_agent::Error::GnuPG( sequoia_gpg_agent::gnupg::Error::ComponentMissing(_))) = err.downcast_ref() { log::info!("Failed to initialize backend: {:#}", err); shown = true; } #[cfg(feature = "gpg-agent")] if let Some(sequoia_gpg_agent::Error::GnuPGHomeMissing(_)) = err.downcast_ref() { log::info!("Failed to initialize backend: {:#}", err); shown = true; } if ! shown { log::error!("Failed to initialize backend: {:#}", err); } } } }; #[cfg(feature = "softkeys")] add(softkeys::Backend::init(Some(home.clone().join("softkeys"))).await .map(|b| { b as Box })); #[cfg(feature = "openpgp-card")] add(openpgp_card::Backend::init(home.clone().join("openpgp-card"), is_default_keystore).await .map(|b| { Box::new(b) as Box })); #[cfg(feature = "tpm")] add(tpm::Backend::init(home.clone().join("tpm"), is_default_keystore).await .map(|b| { Box::new(b) as Box })); #[cfg(feature = "gpg-agent")] add(gpg_agent::Backend::init(home.join("gpg-agent"), is_default_keystore).await .map(|b| { Box::new(b) as Box })); log::trace!("Server::new({}) -> {} backends", home.display(), backends.len()); Ok(Server { backends, }) } } impl Server { pub fn iter(&self) -> impl Iterator> + ExactSizeIterator { self.backends.iter() } } sequoia-keystore-0.6.2/src/server.rs000064400000000000000000001360511046102023000156130ustar 00000000000000use std::path::PathBuf; use std::sync::Arc; use capnp::capability::Promise; use sequoia_ipc::capnp_rpc as capnp_rpc; use capnp_rpc::pry; use capnp_rpc::rpc_twoparty_capnp::Side; use capnp_rpc::{RpcSystem, twoparty}; use sequoia_openpgp as openpgp; use openpgp::Cert; use openpgp::crypto::mpi::Ciphertext; use openpgp::crypto::Password; use openpgp::packet::Key; use openpgp::packet::PKESK; use openpgp::packet::key; use openpgp::parse::Parse; use openpgp::types::HashAlgorithm; use openpgp::types::PublicKeyAlgorithm; use sequoia_ipc as ipc; use crate::keystore_protocol_capnp::keystore; use crate::Error; use crate::ImportStatus; use crate::PasswordSource; use crate::Protection; use crate::Result; use crate::error::ServerError; mod backend; use backend::Servers; type JoinResult = std::result::Result; pub struct Keystore { #[allow(unused)] descriptor: ipc::Descriptor, ks: keystore::Client, } impl Keystore { /// Instantiates a new `Keystore` server. pub fn new(descriptor: ipc::Descriptor, _local: &tokio::task::LocalSet) -> Result { // The application may not use the rust logging framework. // Try to initialize it so that the user can get some output. let _ = env_logger::Builder::from_default_env().try_init(); let home = descriptor.context().home().to_path_buf(); log::debug!("KeystoreServer::new: keystore directory: {}", home.display()); Ok(Keystore { descriptor, ks: capnp_rpc::new_client(KeystoreServer::new(home)?), }) } /// A generic variant of [`Keystore::new`]. /// /// This is a variant of [`Keystore::new`], which returns a /// generic type instead of a [`Keystore`]. This is useful in /// conjunction with [`sequoia_ipc::Descriptor::new`]. pub fn new_descriptor(descriptor: ipc::Descriptor, local: &tokio::task::LocalSet) -> Result> { Self::new(descriptor, local).map(|d| Box::new(d) as Box) } } impl ipc::Handler for Keystore { fn handle(&self, network: twoparty::VatNetwork>) -> RpcSystem { RpcSystem::new(Box::new(network), Some(self.ks.clone().client)) } } #[derive(Clone, Debug)] struct KeystoreServer { inner: Arc, } struct KeystoreServerInner { home: PathBuf, rt: tokio::runtime::Runtime, } impl std::fmt::Debug for KeystoreServerInner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("KeystoreServer") .field("home", &self.home) .finish() } } impl KeystoreServer { fn new(home: PathBuf) -> Result { Ok(Self { inner: Arc::new(KeystoreServerInner { home, rt: tokio::runtime::Runtime::new()?, }), }) } } // Like try!, but stores the error in `$results`. macro_rules! ary { ($results:expr, $r:expr) => { match $r { Ok(r) => r, Err(err) => { pry!($results.get().get_result()) .init_err() .from_anyhow(&anyhow::Error::from(err)); return Promise::ok(()); } } } } // Server stubs run in three phases: // // - Phase 1 extracts the parameters, and copies any required data from // self. // // - Phase 2 executes the method's body in a separate async task so // that tasks that are bound to this thread are not blocked. // // - Phase 3 serializes the result in the main thread. // // This macro avoids some boilerplate in phase 2 and phase 3. // // `phase2` is an async block, which runs the server's body. It is // spawned as a separate async task, which, unlike the current task, // can run on other threads. // // `phase3` is a closure, which is fed phase 2's return value, and // serializes the results. macro_rules! run { ($ks: expr, $results: expr, $phase2: expr, $phase3: expr) => { Promise::from_future(async move { // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. let ks: KeystoreServer = $ks; let r: JoinResult> = ks.inner.rt.spawn($phase2).await; // Convert a join error to an internal server error. let r = match r { Ok(r) => r, Err(err) => { let err = err.to_string(); let mut builder = $results.get().get_result()?.init_err() .init_internal_error(err.len() as u32); builder.push_str(&err); return Ok(()); } }; // Phase 3: Serialize the result in the main thread. let r: Result<_> = r.and_then($phase3); if let Err(err) = r { $results.get().get_result()? .init_err() .from_anyhow(&anyhow::Error::from(err)); } Ok(()) }) }; } impl keystore::Server for KeystoreServer { fn backends(&mut self, _params: keystore::BackendsParams, mut results: keystore::BackendsResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.clone(); let ks = self.clone(); let ks2 = self.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let server = server.lock().await; let ids = server.iter() .map(|backend| backend.id()) .collect::>(); Ok(ids) }, // Phase 3: Serialize the result in the main thread. |ids: Vec| { let mut ok = results.get().get_result()? .initn_ok(ids.len() as u32); for (i, id) in ids.into_iter().enumerate() { let server: BackendServer = BackendServer::new( ks2.clone(), id); let cap: keystore::backend::Client = capnp_rpc::new_client(server); ok.set(i as u32, cap.client.hook); } Ok(()) }) } fn decrypt(&mut self, params: keystore::DecryptParams, mut results: keystore::DecryptResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let mut pkesks = Vec::new(); for (i, pkesk) in pry!(pry!(params.get()).get_pkesks()).iter().enumerate() { pkesks.push((i, ary!(results, PKESK::from_bytes(pry!(pkesk))))); } let ks0 = self.clone(); let ks = self.clone(); log::trace!("KeystoreServer::decrypt({})", pkesks.iter() .map(|(_i, pkesk)| { pkesk.recipient().to_string() }) .collect::>() .join(", ")); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let (wildcard, known): (Vec<&(usize, PKESK)>, Vec<&(usize, PKESK)>) = pkesks .iter() .partition(|(_i, pkesk)| pkesk.recipient().is_wildcard()); // Keys that are inaccessible, but that could become available // with a little help from the user (e.g., unlock a password, // insert a device, etc.) let mut inaccessible: Vec = Vec::new(); let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if ! known.is_empty() { for backend in server.backends.iter_mut() { for device in backend.list().await { for mut key in device.list().await { for (i, pkesk) in known.iter() { let keyid = key.keyid(); if pkesk.recipient() != &keyid { log::trace!("PKESK for {}, key: {}, skipping", pkesk.recipient(), keyid); continue; } if ! key.decryption_capable().await { log::trace!("{} cannot be used for decryption", key.fingerprint()); continue; } let mut abort = false; let protection = key.locked().await; log::trace!("{}'s protection: {:?}", keyid, protection); match protection { // An unlocked key should work. Protection::Unlocked => (), // External protection may // succeed, but it may block. Protection::UnknownProtection(_) => (), Protection::ExternalPassword(_) => (), Protection::ExternalTouch(_) => (), Protection::ExternalOther(_) => (), protection @ Protection::Password(_) => { log::trace!("{} is locked: {:?}", keyid, protection); abort = true; } } if ! key.available().await { log::trace!("{} is unavailable", key.fingerprint()); abort = true; } if abort { log::trace!("{} is inaccessible", keyid); inaccessible.push(InaccessibleDecryptionKey { key: KeyDescriptor { server: KeyServer::new( ks.clone(), backend.id(), device.id(), key.id()), key: key.public_key().await.clone(), }, pkesk: pkesk.clone(), }); continue; } if let Some((sym_algo, sk)) = key.decrypt_pkesk(pkesk).await { let fpr = key.fingerprint().as_bytes().to_vec(); return Ok((*i, fpr, sym_algo, sk)); } else { log::trace!("PKESK for {} matches key {}, \ but failed to decrypt it!", pkesk.recipient(), keyid); } } } } } } if ! wildcard.is_empty() { unimplemented!(); } if ! inaccessible.is_empty() { log::debug!("Failed to decrypt, but have {} candidate keys \ that are inaccessible", inaccessible.len()); Err(ServerError::InaccessibleDecryptionKey(inaccessible).into()) } else { // XXX: The right error is: no known secret key can decrypt // it. Err(Error::EOF.into()) } }, // Phase 3: Serialize the result. |(i, fingerprint, sym_algo, sk)| { let mut ok = results.get().get_result()?.init_ok(); ok.set_index(i as u32); ok.set_fingerprint(&fingerprint); ok.set_algo(u8::from(sym_algo)); ok.set_session_key(&sk); Ok::<_, anyhow::Error>(()) }) } } #[derive(Clone, Debug)] struct BackendServer { ks: KeystoreServer, id: String, } impl BackendServer { fn new>(ks: KeystoreServer, id: S) -> Self { let server = Self { ks, id: id.as_ref().into(), }; server } } impl keystore::backend::Server for BackendServer { fn id(&mut self, _params: keystore::backend::IdParams, mut results: keystore::backend::IdResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { let mut ok = pry!(results.get().get_result()); pry!(ok.set_ok(&self.id)); Promise::ok(()) } fn list(&mut self, _params: keystore::backend::ListParams, mut results: keystore::backend::ListResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let ks2 = self.ks.clone(); let backend_id = self.id.clone(); let backend_id2 = self.id.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { let devices: Vec<_> = backend.list().await.collect(); let ids = devices.into_iter() .map(|d| d.id()) .collect::>(); Ok(ids) } else { Err(Error::EOF.into()) } }, // Phase 3: Serialize the result. |ids: Vec| { let mut ok = results.get().get_result()? .initn_ok(ids.len() as u32); for (i, device_id) in ids.into_iter().enumerate() { let server = DeviceServer::new( ks2.clone(), &backend_id2, device_id); let cap: keystore::device::Client = capnp_rpc::new_client(server); ok.set(i as u32, cap.client.hook); } Ok::<(), anyhow::Error>(()) }) } fn import<'a>(&mut self, params: keystore::backend::ImportParams, mut results: keystore::backend::ImportResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let bytes = pry!(pry!(params.get()).get_cert()); let cert = ary!(results, Cert::from_bytes(bytes)); let ks0 = self.ks.clone(); let ks = self.ks.clone(); let ks2 = self.ks.clone(); let backend_id = self.id.clone(); let backend_id2 = self.id.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { let mut results = Vec::new(); for (import_status, key) in backend.import(cert).await? { use openpgp::serialize::MarshalInto; let key_bytes = key.public_key().await.to_vec() .expect("serializing to a vec is infallible"); results.push( (import_status, key.device().await.id(), key.id(), key_bytes)) } Ok(results) } else { Err(Error::EOF.into()) } }, // Phase 3: Serialize the result. |imports: Vec<(ImportStatus, String, String, Vec)>| { let mut ok = results.get().get_result()? .initn_ok(imports.len() as u32); for (i, (import_status, device_id, key_id, key_bytes)) in imports.into_iter().enumerate() { let mut r = ok.reborrow().get(i as u32); r.reborrow().init_status().set_import_status(import_status)?; let mut key_descriptor = r.init_key(); let server = KeyServer::new( ks2.clone(), &backend_id2, device_id, key_id); let cap: keystore::key::Client = capnp_rpc::new_client(server); key_descriptor.set_handle(cap); key_descriptor.set_public_key(&key_bytes[..]); } Ok::<(), anyhow::Error>(()) }) } } #[derive(Clone, Debug)] struct DeviceServer { ks: KeystoreServer, backend: String, id: String, } impl DeviceServer { fn new(ks: KeystoreServer, backend: B, id: I) -> Self where B: AsRef, I: AsRef { let device = Self { ks, backend: backend.as_ref().into(), id: id.as_ref().into(), }; device } } impl keystore::device::Server for DeviceServer { fn id(&mut self, _params: keystore::device::IdParams, mut results: keystore::device::IdResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { let mut ok = pry!(results.get().get_result()); pry!(ok.set_ok(&self.id)); Promise::ok(()) } fn list(&mut self, _params: keystore::device::ListParams, mut results: keystore::device::ListResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let ks2 = self.ks.clone(); let backend_id = self.backend.clone(); let backend_id2 = self.backend.clone(); let device_id = self.id.clone(); let device_id2 = self.id.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { let mut info = Vec::new(); let keys: Vec<_> = device.list().await.collect(); for key in keys.into_iter() { let key_id = key.id(); use openpgp::serialize::MarshalInto; let key_bytes = key.public_key().await.to_vec() .expect("serializing to a vec is infallible"); info.push((key_id, key_bytes)); } Ok(info) } else { Err(Error::EOF.into()) } } else { Err(Error::EOF.into()) } }, // Phase 3: Serialize the result. |info: Vec<(String, Vec)>| { let mut ok = results.get().get_result()? .initn_ok(info.len() as u32); for (i, (key_id, key_bytes)) in info.into_iter().enumerate() { let server = KeyServer::new( ks2.clone(), &backend_id2, &device_id2, key_id); let cap: keystore::key::Client = capnp_rpc::new_client(server); let mut r = ok.reborrow().get(i as u32); r.set_handle(cap); r.set_public_key(&key_bytes[..]); } Ok::<(), anyhow::Error>(()) }) } } #[derive(Clone, Debug)] struct KeyServer { ks: KeystoreServer, backend: String, device: String, key: String, } impl KeyServer { fn new(ks: KeystoreServer, backend: B, device: D, key: K) -> Self where B: AsRef, D: AsRef, K: AsRef { let key = Self { ks, backend: backend.as_ref().into(), device: device.as_ref().into(), key: key.as_ref().into(), }; key } } impl keystore::key::Server for KeyServer { fn id(&mut self, _params: keystore::key::IdParams, mut results: keystore::key::IdResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { let mut ok = pry!(results.get().get_result()); pry!(ok.set_ok(&self.key)); Promise::ok(()) } fn unlock(&mut self, params: keystore::key::UnlockParams, mut results: keystore::key::UnlockResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let p = pry!(params.get()); let password: Vec = pry!(p.get_password()).to_vec(); let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(mut key) = device.list().await .find(|k| k.id() == key_id) { match key.unlock(Some(&password.into())).await { Ok(()) => return Ok(()), Err(err) => return Err(err.into()), } } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |()| { let _ok = results.get().get_result()?.set_ok(()); Ok(()) }) } fn decrypt_ciphertext(&mut self, params: keystore::key::DecryptCiphertextParams, mut results: keystore::key::DecryptCiphertextResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let algo = PublicKeyAlgorithm::from(pry!(params.get()).get_algo()); let ciphertext = pry!(pry!(params.get()).get_ciphertext()); let ciphertext = ary!(results, Ciphertext::parse(algo, ciphertext)); let plaintext_len = match pry!(params.get()).get_plaintext_len() { 0 => None, n => Some(n as usize), }; let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(mut key) = device.list().await .find(|k| k.id() == key_id) { if ! key.decryption_capable().await { log::trace!("{} is not for decryption", key.fingerprint()); return Err(Error::NotDecryptionCapable( key.fingerprint().to_string()).into()); } match key.decrypt_ciphertext( &ciphertext, plaintext_len).await { Ok(sk) => return Ok(sk), Err(err) => return Err(err.into()), } } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |sk| { let ok = results.get().get_result()? .initn_ok(sk.len() as u32); ok.copy_from_slice(&sk); Ok(()) }) } fn sign_message(&mut self, params: keystore::key::SignMessageParams, mut results: keystore::key::SignMessageResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let p = pry!(params.get()); let hash_algo = HashAlgorithm::from(p.get_hash_algo()); let digest: Vec = pry!(p.get_digest()).to_vec(); let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(mut key) = device.list().await .find(|k| k.id() == key_id) { log::trace!("Signing digest with {} (hash: {})", key.keyid(), hash_algo); if ! key.signing_capable().await { log::trace!("{} is not for signing", key.fingerprint()); return Err(Error::NotSigningCapable( key.fingerprint().to_string()).into()); } match key.sign(hash_algo, &digest).await { Ok((pk_algo, sig)) => { use openpgp::serialize::MarshalInto; let bytes = sig.to_vec() .expect("serializing to a vec is infallible"); return Ok((pk_algo, bytes)); } Err(err) => { log::info!("Failed to sign {} digest with {}: {}", hash_algo, key.keyid(), err); return Err(err.into()); } } } } } log::debug!("Invalid key capability: {}/{}/{} not found", backend_id, device_id, key_id); return Err(Error::EOF.into()); }, // Phase 3: Serialize the result in the main thread. |(pk_algo, bytes)| { let mut ok = results.get().get_result()?.init_ok(); ok.set_pk_algo(u8::from(pk_algo)); let mpis = ok.init_mpis(bytes.len() as u32); mpis.copy_from_slice(&bytes); Ok(()) }) } fn available(&mut self, _params: keystore::key::AvailableParams, mut results: keystore::key::AvailableResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(key) = device.list().await .find(|k| k.id() == key_id) { return Ok(key.available().await); } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |available: bool| { let _ok = results.get().get_result()?.set_ok(available); Ok(()) }) } fn locked(&mut self, _params: keystore::key::LockedParams, mut results: keystore::key::LockedResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(key) = device.list().await .find(|k| k.id() == key_id) { return Ok(key.locked().await); } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |protection: Protection| { let mut result = results.get().get_result()?.init_ok(); result.set_protection(protection)?; Ok(()) }) } fn password_source(&mut self, _params: keystore::key::PasswordSourceParams, mut results: keystore::key::PasswordSourceResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(key) = device.list().await .find(|k| k.id() == key_id) { return Ok(key.password_source().await); } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |password_source: PasswordSource| { let mut result = results.get().get_result()?.init_ok(); result.set_password_source(password_source)?; Ok(()) }) } fn decryption_capable(&mut self, _params: keystore::key::DecryptionCapableParams, mut results: keystore::key::DecryptionCapableResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(key) = device.list().await .find(|k| k.id() == key_id) { return Ok(key.decryption_capable().await); } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |decryption_capable: bool| { let _ok = results.get().get_result()?.set_ok(decryption_capable); Ok(()) }) } fn signing_capable(&mut self, _params: keystore::key::SigningCapableParams, mut results: keystore::key::SigningCapableResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(key) = device.list().await .find(|k| k.id() == key_id) { return Ok(key.signing_capable().await); } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |signing_capable: bool| { let _ok = results.get().get_result()?.set_ok(signing_capable); Ok(()) }) } fn export(&mut self, _params: keystore::key::ExportParams, mut results: keystore::key::ExportResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(mut key) = device.list().await .find(|k| k.id() == key_id) { use openpgp::serialize::MarshalInto; let key = key.export().await?; return Ok(key.to_vec()?); } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |sk: Vec| { let ok = results.get().get_result()? .initn_ok(sk.len() as u32); ok.copy_from_slice(&sk); Ok(()) }) } fn change_password(&mut self, params: keystore::key::ChangePasswordParams, mut results: keystore::key::ChangePasswordResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let p = pry!(params.get()); use keystore::change_password_action::Which; let new_password = match pry!(pry!(p.get_password()).which()) { Which::Ask(()) => None, Which::Password(password) => { Some(Password::from(pry!(password))) } }; let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(mut key) = device.list().await .find(|k| k.id() == key_id) { key.change_password(new_password.as_ref()).await?; return Ok(()); } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |()| { let _ok = results.get().get_result()?.set_ok(()); Ok(()) }) } fn delete_secret_key_material(&mut self, _params: keystore::key::DeleteSecretKeyMaterialParams, mut results: keystore::key::DeleteSecretKeyMaterialResults) -> ::capnp::capability::Promise<(), ::capnp::Error> { // Phase 1: Extract the parameters, and copy any required data // from self. let ks0 = self.ks.clone(); let ks = self.ks.clone(); let backend_id = self.backend.clone(); let device_id = self.device.clone(); let key_id = self.key.clone(); run!( ks0, results, // Phase 2: Execute the method's body in a separate task // so that tasks that are bound to this thread are not // blocked. async move { let server = Servers::get(ks.inner.home.as_path()).await?; let mut server = server.lock().await; if let Some(backend) = server.backends.iter_mut() .find(|b| b.id() == backend_id) { if let Some(device) = backend.list().await .find(|d| d.id() == device_id) { if let Some(mut key) = device.list().await .find(|k| k.id() == key_id) { key.delete_secret_key_material().await?; return Ok(()); } } } Err(Error::EOF.into()) }, // Phase 3: Serialize the result in the main thread. |()| { let _ok = results.get().get_result()?.set_ok(()); Ok(()) }) } } // Basically, a wrapper around a KeyHandle that we want to return // later. This isn't just a wrapper around a `dyn // sequoia_keystore_backend::KeyHandle` due to lifetimes. #[derive(Clone)] pub(crate) struct KeyDescriptor { server: KeyServer, key: Key, } impl std::fmt::Debug for KeyDescriptor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "server::KeyDescriptor {{ {} }}", self.key.fingerprint()) } } impl KeyDescriptor { /// Returns a capability. pub fn cap(&self) -> keystore::key::Client { capnp_rpc::new_client(self.server.clone()) } /// Returns the key handle. pub fn public_key(&self) -> &Key { &self.key } pub fn serialize(&self, mut builder: keystore::key_descriptor::Builder) { use openpgp::serialize::MarshalInto; builder.set_handle(self.cap()); let bytes = self.public_key().to_vec() .expect("serializing to a vec is infallible"); builder.set_public_key(&bytes[..]); } } #[derive(Clone)] pub(crate) struct InaccessibleDecryptionKey { key: KeyDescriptor, pkesk: PKESK, } impl std::fmt::Debug for InaccessibleDecryptionKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "server::InaccessibleDecryptionKey {{ {} }}", self.key.key.fingerprint()) } } impl InaccessibleDecryptionKey { pub fn serialize(&self, mut builder: keystore::inaccessible_decryption_key::Builder) { use openpgp::serialize::MarshalInto; self.key.serialize(builder.reborrow().init_key_descriptor()); let bytes = self.pkesk.to_vec() .expect("serializing to a vec is infallible"); builder.set_pkesk(&bytes[..]); } } sequoia-keystore-0.6.2/src/testdata.rs000064400000000000000000000114401046102023000161100ustar 00000000000000//! Test data. //! //! This module includes the test data from `keystore/tests/data` in a //! structured way. use std::fmt; use std::collections::BTreeMap; use std::path::PathBuf; use lazy_static::lazy_static; use sequoia_openpgp as openpgp; use openpgp::Fingerprint; 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, "keystore/tests/data/{}", self.path) } } pub fn dir() -> PathBuf { PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data")) } /// Returns the content of the given file below `keystore/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("simple/dave.pgp")); m }; } FILES.get(name).unwrap_or_else(|| panic!("No such file {:?}", name)) } pub struct EncryptedMessage { pub filename: &'static str, pub recipients: Vec<&'static Fingerprint>, pub content: &'static str, } macro_rules! em { ( $dir:expr, $name:expr $(, $recip:ident)* ) => { EncryptedMessage { filename: concat!($dir, "/", $name, ".pgp"), recipients: vec![ $(&*$recip,)* ], content: concat!($name, "\n"), } } } #[allow(non_upper_case_globals)] pub mod simple { use super::*; // The encryption subkeys. lazy_static! { pub static ref alice: Fingerprint = "EBB7CD5ED3AF234A0AD15BDA1B19948D7A8424A4".parse().unwrap(); pub static ref alice_pri: Fingerprint = "AE81192975C08CA56C257BD3A29F4223DDD881B2".parse().unwrap(); pub static ref bob: Fingerprint = "92D7E5A2005676E7F2B350B11DF3BDD1D6072B44".parse().unwrap(); pub static ref carol: Fingerprint = "AF2620370975120E25A0469FAC13CAA7E282324B".parse().unwrap(); pub static ref carol_pri: Fingerprint = "CD6C4799E67CC4A551B7F2F400431FF5B8A682CD".parse().unwrap(); pub static ref MSGS: [EncryptedMessage; 13] = [ em!("simple", "for-alice", alice), em!("simple", "for-bob", bob), em!("simple", "for-carol", carol), em!("simple", "for-dave"), em!("simple", "for-alice-bob", alice, bob), em!("simple", "for-alice-carol", alice, carol), em!("simple", "for-alice-dave", alice), em!("simple", "for-bob-alice", bob, alice), em!("simple", "for-carol-alice", carol, alice), em!("simple", "for-carol-dave", carol), em!("simple", "for-dave-alice", alice), em!("simple", "for-dave-carol", carol), em!("simple", "for-dave-alice-bob-carol", alice, bob, carol), ]; } pub fn dir() -> PathBuf { super::dir().join("simple") } } #[allow(non_upper_case_globals)] pub mod password { use super::*; // The encryption subkeys. lazy_static! { pub static ref alice_pri: Fingerprint = "B9D905221577435EC69E6C10F9DF67187D2BF8B2".parse().unwrap(); pub static ref alice: Fingerprint = "9B980721629F4A98332438635AE7278072A2D0D2".parse().unwrap(); pub static ref bob_pri: Fingerprint = "7E0622ECF641D2FFE9A47C854A04BA480CE3E102".parse().unwrap(); pub static ref bob: Fingerprint = "568078BB9A65285B8A7B4498E3B63B9AE4D10DD5".parse().unwrap(); pub static ref frank_pri: Fingerprint = "D7347AEF97CCAF209FCC4DBBFEB42E3C9D46ED92".parse().unwrap(); pub static ref frank: Fingerprint = "9FB98AB6ADEBCCD0D11943258738B7C8950072AE".parse().unwrap(); // The string is the password needed to decrypt the message. // // - Alice: unencrypted. // - Bob: two variants: unencrypted and encrypted. // - Frank: encrypted. pub static ref MSGS: [(EncryptedMessage, Option<&'static str>); 6] = [ (em!("password", "for-alice-bob", alice, bob), None), (em!("password", "for-bob-alice", bob, alice), None), (em!("password", "for-bob", bob), None), (em!("password", "for-frank", frank), Some("foo")), (em!("password", "for-frank-bob", frank, bob), None), (em!("password", "for-frank-bob", frank, bob), None), ]; } pub fn dir() -> PathBuf { super::dir().join("password") } } sequoia-keystore-0.6.2/tests/data/password/README.md000064400000000000000000000115321046102023000203400ustar 00000000000000A keystore with some password-protected keys. - Alice's key is unlocked. - Bob's key is provided in two version: unlocked and locked with the password "bar". - Frank's key is lock with the password "foo". ``` sq key generate --userid '' -o alice.pgp --expiry never sq key generate --userid '' -o bob.pgp --expiry never --with-password sq key password < bob.pgp > bob-unlocked.pgp sq key generate --userid '' -o frank.pgp --expiry never --with-password ``` Files are named after the recipients (in the order of the PKESKs), e.g., `for-alice.pgp`. The content is the filename without the extension plus a newline: ```text $ echo 123 | sq encrypt --recipient-file alice.pgp --recipient-file bob.pgp > for-alice-bob.pgp $ echo 123 | sq encrypt --recipient-file bob.pgp --recipient-file alice.pgp > for-bob-alice.pgp $ echo 123 | sq encrypt --recipient-file bob.pgp > for-bob.pgp $ echo 123 | sq encrypt --recipient-file frank.pgp > for-frank.pgp $ echo 123 | sq encrypt --recipient-file frank.pgp --recipient-file bob.pgp > for-frank-bob.pgp ``` ```text sq inspect alice.pgp alice.pgp: Transferable Secret Key. Fingerprint: B9D905221577435EC69E6C10F9DF67187D2BF8B2 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Unencrypted Creation time: 2024-01-15 08:55:28 UTC Key flags: certification Subkey: B3F8F25FC629E058009F282928EBE800BE51B38D Public-key algo: EdDSA Public-key size: 256 bits Secret key: Unencrypted Creation time: 2024-01-15 08:55:28 UTC Key flags: authentication Subkey: 90D35FC8143312947489EDB6EB0F6B45C6FFB0D4 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Unencrypted Creation time: 2024-01-15 08:55:28 UTC Key flags: signing Subkey: 9B980721629F4A98332438635AE7278072A2D0D2 Public-key algo: ECDH Public-key size: 256 bits Secret key: Unencrypted Creation time: 2024-01-15 08:55:28 UTC Key flags: transport encryption, data-at-rest encryption UserID: $ sq inspect bob.pgp bob.pgp: Transferable Secret Key. Fingerprint: 7E0622ECF641D2FFE9A47C854A04BA480CE3E102 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Encrypted Creation time: 2024-01-15 08:59:03 UTC Key flags: certification Subkey: 58F4A44FC0F5AF61113F56F538E06AC6F5941919 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Encrypted Creation time: 2024-01-15 08:59:03 UTC Key flags: signing Subkey: 73628A0446F6F7F235893DDE674AE5654256C0A6 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Encrypted Creation time: 2024-01-15 08:59:03 UTC Key flags: authentication Subkey: 568078BB9A65285B8A7B4498E3B63B9AE4D10DD5 Public-key algo: ECDH Public-key size: 256 bits Secret key: Encrypted Creation time: 2024-01-15 08:59:03 UTC Key flags: transport encryption, data-at-rest encryption UserID: $ sq inspect bob-unlocked.pgp bob-unlocked.pgp: Transferable Secret Key. Fingerprint: 7E0622ECF641D2FFE9A47C854A04BA480CE3E102 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Unencrypted Creation time: 2024-01-15 08:59:03 UTC Key flags: certification Subkey: 58F4A44FC0F5AF61113F56F538E06AC6F5941919 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Unencrypted Creation time: 2024-01-15 08:59:03 UTC Key flags: signing Subkey: 73628A0446F6F7F235893DDE674AE5654256C0A6 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Unencrypted Creation time: 2024-01-15 08:59:03 UTC Key flags: authentication Subkey: 568078BB9A65285B8A7B4498E3B63B9AE4D10DD5 Public-key algo: ECDH Public-key size: 256 bits Secret key: Unencrypted Creation time: 2024-01-15 08:59:03 UTC Key flags: transport encryption, data-at-rest encryption UserID: $ sq inspect frank.pgp frank.pgp: Transferable Secret Key. Fingerprint: D7347AEF97CCAF209FCC4DBBFEB42E3C9D46ED92 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Encrypted Creation time: 2024-01-15 09:00:22 UTC Key flags: certification Subkey: 5D05E0A24D11DD97C2C08588BC5F84DB378ED544 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Encrypted Creation time: 2024-01-15 09:00:22 UTC Key flags: authentication Subkey: 6428BE0964CF04D8A3972B45C695C3375D14D868 Public-key algo: EdDSA Public-key size: 256 bits Secret key: Encrypted Creation time: 2024-01-15 09:00:22 UTC Key flags: signing Subkey: 9FB98AB6ADEBCCD0D11943258738B7C8950072AE Public-key algo: ECDH Public-key size: 256 bits Secret key: Encrypted Creation time: 2024-01-15 09:00:22 UTC Key flags: transport encryption, data-at-rest encryption UserID: ``` sequoia-keystore-0.6.2/tests/data/password/alice.pgp000064400000000000000000000050031046102023000206420ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: B9D9 0522 1577 435E C69E 6C10 F9DF 6718 7D2B F8B2 Comment: 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 =nzfL -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/password/bob-unlocked.pgp000064400000000000000000000050011046102023000221270ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 7E06 22EC F641 D2FF E9A4 7C85 4A04 BA48 0CE3 E102 Comment: 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 =hguM -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/password/bob.pgp000064400000000000000000000053711046102023000203370ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 7E06 22EC F641 D2FF E9A4 7C85 4A04 BA48 0CE3 E102 Comment: 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 =7J0a -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/password/for-alice-bob.pgp000064400000000000000000000006171046102023000221740ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4DWucngHKi0NISAQdARE4iErOGIuY/I8e7nyW/aR1wt3iFxDqW4P/f8fw2+1Mw O2UXLsnsiryKZJ0WFjj3CDnD42UqFJAF4SBpaXdqoGFLmZCOgWIcouTqyVlDA1q7 wV4D47Y7muTRDdUSAQdAzeDs+oOyn8QaTA4b+zsW4JfYZCNH1yRtA0t3zmmXiCUw CuTj5jJXnAhos0WcmaXIbddGQ0xqL+JFHwqDZBimH7njHlJcSEjRI/IvgCUKchrT 0jUB40UKUR/NTF4rZi0SyNMX96a5xd7NXuioZsfbL67NhoVQowTZYYa+yzvB1nCc YCfV/tg5Pg== =SscF -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/password/for-bob-alice.pgp000064400000000000000000000006171046102023000221740ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4D47Y7muTRDdUSAQdAaO6NuunJnCIb1b9b9PDmFxmmxf2I/hid3J3nznsIOhYw UgbtRPxYIjxSoWWROd6cb87lvtTFQVswERnukQgq5GgjIg1ORidedfWbOulRLhbI wV4DWucngHKi0NISAQdAXB4u94WwTd6EnSzAUbIJohozeHmGVjQATYwl7GI//0Yw /yREJ8cDzz1Tpt2bKz7JOrl4Hg64bTo+VTnB435M5QSx1M4aagGgl9wXYTjGhuhQ 0jUB5ldM2IR3XFPofZ+6wkVjLpwj7bKYRAJE7m4Lbzbss3qZwgJjI+Wbf4imLIr4 Izfqz5rxVQ== =ud7M -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/password/for-bob.pgp000064400000000000000000000004151046102023000211150ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4D47Y7muTRDdUSAQdAKJmifoBOYXsnZqbC4C9iQIzUXYvAh0kpdu+dA+o1qlcw xZ39SmBXPn4M68r1Ci0nAPDbVQvIqFOlQrvOqh2opq+itstVXFXZCQ6YFqAWeajd 0jUBwOc7ATFW2v+otyzAWdJ2o9vG7ST8RHBx2HhjsEvMX/evcE8HDGLAI3G1vCZu W2uc+MDLxQ== =RyPY -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/password/for-frank-bob.pgp000064400000000000000000000006171046102023000222200ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4Dhzi3yJUAcq4SAQdATHGoKsvqMT8q4vC5pWwsS3p/x5G0r1WHRZaNLjtUPh4w kA5nLzbhGVEEZ1/+6PJY+hHLNbXTI+R5SUlIAw419qkMLh3rn1AXoksJU+4NKv/8 wV4D47Y7muTRDdUSAQdA03dNETJIsNgJuyNomvBHjZXM8DZRV1UQuK43/kLCAkEw EcNbDZBsPxZEG8XrKWxREfKoEOvoH6yi7MfwVR05B1O1frqlV1pQmpyGtip3VnI6 0jUByoWDhmbu8ewnVc9X8pKJKBm76IOfWWRsJUiO/wfyWs7p9U/AdN6td3i3EbwZ H9aMj3kHdg== =4Mc9 -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/password/for-frank.pgp000064400000000000000000000004151046102023000214540ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4Dhzi3yJUAcq4SAQdAKk3DcDlW18l+q1Ev4XDOb2sl/tTU6lINdbVUq98d2X0w IzyM+T/EsmkmkKuTkSMKbIB5GhmRAiPUD0CIegB9X2zGCIrfFI6fsN69yLsiV7RZ 0jUBvta/7h1x2J2a5Sy+h+flLTs5GpMrBTvLm++PiOJMKME7K13+RQemrsPN4ZaN PVf38fEjiQ== =/HHW -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/password/frank.pgp000064400000000000000000000053771046102023000207040ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: D734 7AEF 97CC AF20 9FCC 4DBB FEB4 2E3C 9D46 ED92 Comment: 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 =r2tp -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/password/keystore/softkeys/alice.pgp000064400000000000000000000050031046102023000243560ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: B9D9 0522 1577 435E C69E 6C10 F9DF 6718 7D2B F8B2 Comment: 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 =nzfL -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/password/keystore/softkeys/bob-unlocked.pgp000064400000000000000000000050011046102023000256430ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 7E06 22EC F641 D2FF E9A4 7C85 4A04 BA48 0CE3 E102 Comment: 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 =hguM -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/password/keystore/softkeys/bob.pgp000064400000000000000000000053711046102023000240530ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 7E06 22EC F641 D2FF E9A4 7C85 4A04 BA48 0CE3 E102 Comment: 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 =7J0a -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/password/keystore/softkeys/frank.pgp000064400000000000000000000053771046102023000244200ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: D734 7AEF 97CC AF20 9FCC 4DBB FEB4 2E3C 9D46 ED92 Comment: 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 =r2tp -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/simple/README.md000064400000000000000000000042151046102023000177670ustar 00000000000000A simple keystore with three keys (Alice, Bob, and Carol), which are all unlocked. Bob's certificate is expired (which shouldn't matter for the purpose of decrypting). Alice's and Bob's keys use ECC whereas Carol's key uses RSA. Some messages are also encrypted for a different key, Dave. (It is stored next to this file, but not imported into the `keystore` directory.) One message is encrypt just to dave. Some of the messages also include dave as a recipient. Files are named after the recipients (in the order of the PKESKs), e.g., `for-dave-alice.pgp`. The content is the filename without the extension plus a newline. ```text $ sq inspect alice.pgp Fingerprint: AE81192975C08CA56C257BD3A29F4223DDD881B2 Public-key algo: EdDSA Subkey: E424FDC35BE93C90686A99E8D4D08F3C919C9ED7 Key flags: authentication Subkey: 48DFA35094CE7D9EE0483EDB61779B413826EB98 Key flags: signing Subkey: EBB7CD5ED3AF234A0AD15BDA1B19948D7A8424A4 Key flags: transport encryption, data-at-rest encryption UserID: Alice $ sq inspect bob.pgp bob.pgp: Transferable Secret Key. Fingerprint: 9BD6F501FCB436970C1FE23F51B9EA96699DDBA5 Creation time: 2022-09-26 21:06:22 UTC Expiration time: 2022-09-26 23:06:22 UTC (creation time + PT7200S) Key flags: certification Subkey: 48DAD7FE5E50183B818C009B42B7FCE2D4D9B4B8 Key flags: authentication Subkey: 54EFD9DB3DCFEDF69C3DC45FA08D6FA15CDB3736 Key flags: signing Subkey: 92D7E5A2005676E7F2B350B11DF3BDD1D6072B44 Key flags: transport encryption, data-at-rest encryption UserID: Bob $ sq inspect carol.pgp carol.pgp: Transferable Secret Key. Fingerprint: CD6C4799E67CC4A551B7F2F400431FF5B8A682CD Public-key algo: RSA Public-key size: 4096 bits Subkey: AF2620370975120E25A0469FAC13CAA7E282324B Key flags: transport encryption, data-at-rest encryption Subkey: 9CA852189BDB571874AEA5BC971033045BE0B34D Key flags: signing Subkey: D786023D2D8D4B302BA8896FAF8A2B1CD1B9561D Key flags: authentication UserID: Carol ``` sequoia-keystore-0.6.2/tests/data/simple/dave.pgp000064400000000000000000000357031046102023000201450ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 7FF8 FAA8 5884 DEB2 5E12 63A0 0B34 FA7E 1462 DCE5 Comment: Dave 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 =ZzuW -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/simple/for-alice-bob.pgp000064400000000000000000000006471046102023000216260ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4DGxmUjXqEJKQSAQdAr/mmoRHHuF2OJilO008E2vqTruhV2EjcUfS1Wgeiey0w /18Xei8VkbGRn4i8y45MgfQa2K25m+UzbH/yk9sWmM8Q7S2VQ5TztJrpYPhMWFTi wV4DHfO90dYHK0QSAQdACDIRqA5sfc3owTzr2XBJtC87CdGaXwltLYWLK+pqzGUw OZZQRxkomBt86vI7Bc0s54tdayPhdEMf6VbAn1yGhAk/V+O84sSrzZ0Jiw5M4WD6 0kcBakYMtQbpTk/M+PZRb+aa10RdsRMBLLPbNBv6qX5jgGEVl+YeNCbeVr4p24c6 72sKP4gJtio0XWQ4q3KYwKxcE6JINw1Gog== =3Nr/ -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-alice-carol.pgp000064400000000000000000000017601046102023000221610ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4DGxmUjXqEJKQSAQdAS3vofOf6kOlsff3TysLieZDs/26H85vCNlKoNoCcvEsw AN+nC+wzkJlmOIgDBdFUovt1d7rTYJgestL3y/zydKv+2RAPFT9XP2TLFkWdWVFx wcFMA6wTyqfigjJLAQ/+Kug51ONzDbGD/wCus0XaT+nCmIsWvz1OWYwe9i39PeRT OF+JQwTwA7pQ83rvJ3IVFHKweEzsWLs4Q6Nq8r6viC2LoAXtKHWwpbBv37J0+VFz QGPWgBlybVCPWwY3UaharixkBPrsnVIzNDmVMUo48wY1FX8xBcFQkKBJoC7m0A/w SFHGtMy7bhHTSwlvurtAXiagcXvPMNTilIBVtKBsdwZoafv6dGMQgia3bBKCZZRh G+wM/eiyShOkMOEhh5W+POk7RHUxnFeMYWQfpZWsi1d1k0TwqeluP9ZeWmzUNbtG whbzszv8oHg8KyU5qFZw5ePEc7JZOK/hvNbQnSAp0LBoCqTMfDDjV/7ejD4xe3lY oyTijZMH9+kpttonrMHihsrcWrF9nOM1xj0YRvnYfBCO3hDLtiIojB0zU9LoAYf9 8EQlgb7lEi42qyOKISCm7Ns/VBbSRm/c/BwJny4v6s8tC9W1Vzo9mBAA7UP1ojyf QMBJHmwSrmAyDgM1udmPNrEN/k0443qW5zxi6JXk/JQ1C1CmriKMm+93fYwLTh5m E8kNLju3DjetylDKVKQ3vs26xQIFHCKQbn19Jim2C2BXr+CEdFBRJUcB0EvSIJ1K nJ9IYCOzrJCIxNLOoL/SslcP6qMbrOJVG6WWhFHYngTSMwSVsq/8ZXl8k9kufoDS SQGgLD3VZdNC+vd6uCdNisFoPMuIxwjLGu415rbCj8vmsfqqhwhs1pmtdvn3fcTq cTjTnDpiv7255lan1AGbkpcdoMP3hFXEHIc= =KpDo -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-alice-dave.pgp000064400000000000000000000017601046102023000220000ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4DGxmUjXqEJKQSAQdASWY8TwPuvs5XiXw+RN6MsbQYZI/IkIQC4NRvombce3Mw KK3LHB22NpKFizEMfzBGoAO86QFnWDjOKF6mMve7dVVZXjb/hkMQC26i66hICC2i wcFMAxR5cuEvhCpwAQ/9Hrl2a0lebcLn+pB/H0ZYE4yOef35tzJ8frn9RrNq7ph8 Zxo2BKTwKm5C/TzyXS0IqrPxLWJWSAAyJvgUUp2IhDF5XdWOp0cTiIdzyOg2Un6v y17K+C/9LG4tBB888hHk3MVYV2McVsq0FNgUQCz831cqP/wcNXoPs3St9jJNFL1V 1bBc49+4VAEQ6PIk9HzVYe6OHbKKm5uUrkUi7qEVcf2NdeJfsskWedY7uEy7aLYu /Q0r0KITkT95AEYfu7sQm1CU8L/ltqyJlX8KVCDzYxdmSVWWOUh5Zju825KHmGCH cPJo5IEYUMkZFw6Kvza/P5n5o1MpEktH9rA75KCGUQ2ZgQyedioFs4N9LQUXPkSp W5WGD5hwJObUaUdyu8DhzvL0+J0Ph4UZJZ4WjTe/8WQH8lIkAztjCVf7EnU5CnLw UXR3juMxRbrApUTsvvFx2fRNNZ+zfDZdxUc5cqiNMoK4tlGiYjOYwHfjbvHTx5J6 Xecb2FXXa8AKMCDFP9tCVzJwsXhDEp9AlP6x5Pek+/t3BE5FkIDxhRt1anEwHYzP n5SiLDCaAJWqldoUnLADBVfc90mFC+M56fmDk1O42aldqeLQHfFdwn7G70bgYsdI 8MDPldOUOimaNGN2lqoXv85UUHf96sah2j9PrywHAHKG1ZcGicdGluMhHmh01XjS SQGjS3ZaMB1k956bKe8JU6OEkwp9MdJGNIBKHVXsK+Pq2fruB41i1yH1ZSwTxisy lRPr2vF9oeQXGPiHb2ai1WdXA0l0BGGG9Pg= =t1r5 -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-alice.pgp000064400000000000000000000004351046102023000210610ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4DGxmUjXqEJKQSAQdAGjAYr/1C8XxWq3+0haXaonCUL5O90hfIzZrVaC4AxiYw lLO/5VaUiiEDzp1axYtecyvu77oZeE75c76HAaqILl9FKXdWKUpJY9VzgZENHT7Q 0kMBQYhii/KBwWVfyApzrVkQ5nypfmT5lROnoGdTMMElHrkNugAmyA1zLhmJjSPg SbNoty69Yvo1sx9/gcv9VweplLF/ =5TT9 -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-bob-alice.pgp000064400000000000000000000006471046102023000216260ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4DHfO90dYHK0QSAQdAwD8wWTAYOUTfKwxWRSdoPf/afUQFs8lcdFtuVAjzbhgw MjnAxYNeobPPEVeBJm4ZgKdNR+OcOj4DbOTmgnsQIBWlkOQTDNwD6fzb9vOKiitD wV4DGxmUjXqEJKQSAQdAL5ekFu4p3Cb2aItYtFeH9/sqZ1ImrdhMoSqPiyG7d1ow jVLc4ChMKPKH3xhY/xMGqC9LdSur3/0hWPp1uzM+ucnb82pqD3Q5pKwlAc+8+FCv 0kcBplAH8d+YWjQSjqUSJV7drBfF5ued3jXNNZxmjJAqsGU261tyf6OXbEYJl4Wf VE2TTC+erP+dubXpMHJzw3k/fUL16+GmIQ== =TgC7 -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-bob.pgp000064400000000000000000000004351046102023000205460ustar 00000000000000-----BEGIN PGP MESSAGE----- wV4DHfO90dYHK0QSAQdA4cIC1rWfbkyuuUl0drOJMtNIC2V7WvxU9EIVb3YN0FAw Ub2YfljWnzVh0sn8P6d6p62LEdTeKXvwV3FVOf7jVfX5fMmb1om4P2ilEa8aJpsc 0kEBs8FXu8sb3Znv2w3oAjyaqSKJDcYWhVLpnx6V+Y1L/2NB2If3QCmy+sDO6Ggp 31seUsTPyZ9nLClGcuz3QJIraQ== =s3I+ -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-carol-alice.pgp000064400000000000000000000017601046102023000221610ustar 00000000000000-----BEGIN PGP MESSAGE----- wcFMA6wTyqfigjJLARAAmHJ03KrPUEpnATU1PFsrTiK3qJabb01/llfuwXyrPbGt voXxb6hq5wHh04dINxAKFlMke7xtIdzXtJxxuwnz9AHixV8U8dugneRVwQkTOO58 ykg77wACM6c4kbolQmmReClRZbb4fDACxHn51F3y1qlgB0s9LfNS3klqcXuLEvaK 6d9uZ3KISEYGcxXOFzDj7+nr1OCqcl0F6SXLGDdysZ62dlhjGLANidh9fnNaLYRn 2Yd+MJevPV2yGgPiGBY/TUmpd23J+tCZsEakvxCFNUrijod7xbRixda4WHSDQiS6 +f+K7YebSgeX6ldCHpN9r2ww6e54GoTWX87XddZysh9XtwQ67DhpTJNewfnykD+z hKOD6tEMtbs1UvDJVOu8qYQTf1Nx6tVyH4+osfNVHOdTGPxVydrPwPaiNWxlCdkN BWWeuJuk+yZOWD6K1VNL+dYdxgEoZL0q5JqX/I5VGsR1YcKOUV8sudoyVx/u/VGt f5yWp9JlLsViwT7jfaCTjGVAhIQAadTVqGxXOXcWHTSXk20ZiCVOwtVe1Jl3qT3a Wa8wBCg/aPJ87fbO0MpyiODu/rw1O/Ladvk3b0s7YfUKnpO0JpGgYJa9TgSHDUPq JEAd9MhwKHI7skiS3xvaed94korU8OVWLzt64goK1/DJQp72VqFZp2T4indJBQ7B XgMbGZSNeoQkpBIBB0A0wAQpAAPKgKahXFi6Ba8cSctnbRSCqGalARZhGuzXODDW Ek5J3MtbyoA7xQdzvqyOnmWWbDzwvPX+/IPgjLwv1ywKU7h4K60M9AmrsFMD3n7S SQGBBMJ3xYYAJP76y7HbGFq0D+5G5SLV6ND72W88JFAtiprvQ4stpIcXipehJdfl VNsVospJknpyO/uTYD1ksDtX9CpkOh3jHVc= =90/j -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-carol-dave.pgp000064400000000000000000000030711046102023000220200ustar 00000000000000-----BEGIN PGP MESSAGE----- wcFMA6wTyqfigjJLARAArh9yagXPVcC50MSjGLpg3o2I6L0RBCGaTpxj6KgTk2vy FwkOyqf8NDzjBTNoHiUg3Aa+Cz8YNoICLKwgAyxV0Nw7hLty6kFpsXpaLFPOtMDO Pbcu5zds7p3ryjygxPRnB3N/9yNWQbmY6WEhMGn4iIXTYmlU+irfQ1NjJEbko/Ph f476jyLaN2bahPt3hVylrSM5NFNHdrzv6A1RAxvmvAXZyKqlqixAVycBhcU2WHgZ jXZ0zNORndLkAcJB6x1LCPAS9CzhF5Hsi8EOEvFbEjRdYsYHZdtZmJWXJ+HoaLjs bwMQEJ2Xn3U5eeVTAr1p979joUPoQTtfSGlmGcWxav3byIDQN1L5eWU4GaN/lxQf 2x/62fb8V5Ckl7OpbaM5sErcpqQEGIjT5JTfg7vJUrIOmoPZP4wu0gV1obtzyweC W3A1w/aAzneZJ47HwcWDwoEhR8rb81SlelnHHXDc/QyOSqBLAOh+BoJa8tDF1s6i kF6BjT+g2CFJeplwfKzRxNkb3xJHGEihso6HRotz1b2SktOhtAjX9SthhAhlKqrt Ag3HKcVHeHRTIN2E5gC0FcV3NwYyLO6woyoVdhOBFDSMSkK12lxpS1zIWwC4nmdY sCRzUr/xE1xd2suOs9/Hwh7SnOMjbjM6BxTqCHMMBu1VoFiPMy1DqR/I4AMKCtjB wUwDFHly4S+EKnABD/9cp/r0es/NHezrgaXSSR0nxny38d5SstmwHGAWFoi6ObXu 3h4yCBEaO0ptraUIoaFsIZSme9tLL0iPYTRooB+AT226WjoxbQlfCTNoOj+vbO9/ 8cJ14gBWHb3g1BzAyAOIuRa8C+1qkg0EI0Mgk6GZ/VSaaKQaQewqwYsLml+zVtv6 ermFk1tbtsCSWV6BuNpfeOKqv4NytRBPfWAea5MOhtLWqGVpKUm/aeuqTgVskfAA BdzhdL8lQXMUlghiM0ZNQvfM6zPLzVIKmC/QEuOeNvv0n3I+0ICRKl+dgeJbxoQk wlONVidqRZlFH7KOoHYbd4UhRf59hImXzPLUU+sI1r0zmUZ2j42RpJ7+qN3kwnHl p8Zh15JuOE6rd+FIOCjkX2J9lmW0YQ3CxW7GcsidFnzLxumbf6k8gGAAMsMquA96 JxN7sAwg81r8isaNw5llvsz+l6CDhkv7RQ+DYIYsWLySV1zuuxL+ijb6P7bRAlik oLNBOhQds3pujCbBxEZn5ZjQRSaxhsqCtKvB7HeMUy8hYmJt6SWAdVilWDwGOIVq u59iLkZ3ojSGe+fJuD6YEW9Qn4hafDJ013Dspn5F1zGAdVR+PuYR2u8t8cht2FwS 9ca7yCYDQk4CWVm4/dnDUvb/I79kpemhO/hU21+ekGnlTcC6zng4vZ9y5EVh/tJJ AVjWyEtR2kLZsz3f5yENSR4e56qWHP2ID7CE4Yiby6uhQ4iVHYoQTuJCiJsxkRlY Ho2yPq8w3+gbwLXDTNRGgWOF7maa4hvbaw== =Hl31 -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-carol.pgp000064400000000000000000000015461046102023000211100ustar 00000000000000-----BEGIN PGP MESSAGE----- wcFMA6wTyqfigjJLARAAnVLZrXI5x8ogY/ot+RFqDU7w5tb/MCTCM9lFjkny+dS3 hLUsc4OefbH5EPnixksRG354IlxlkiYRRRA3uneTJai7C82t46+Pc/XdLmiSpKJc V5tbcMi+XiJ/mKnpXd64+VkB4Aos0kQeXHRcTk03rtrjpQ4ZWFblWiCeL6u3NsJT 2m3ddChVUkMUKGoCIHcEgaLrKbwoT0fFKv+9kFkDeYXscHX3leTvUmjNBGo9oRnO 4RAJg4KRrHjCSkn1CDZlLv2T2kx2Aj/POZKzOD8I9nvul/RBW3/W+WpVA1uLcCIA YqhgOTE0ntZV82p7KNJM8EjhqZdpElcW9j6NWrY7dci+4UC5GkPegrRLNUL/nVIu heJlcv+TRBxk4K8zthJ1iR4LXCKL8wWOyYAxg9lbS6tuTDNH2hX3pQD7rpS33284 XDAz5fdgbTFkMNHux9I9xcTO4vZ3qR3kZDCF5Zg3dZczk/At6Rkd3u5oY60FQA4f Ds1ctFlig+/fK0SDl8jUzBU1yx09SiCrEqlCz9qLRfAo6wNJzGZuFVt9rXiAUtRY 32aTsVJYzX3TeJ7+RTGSZO9EvpGIiUFjPU6Rawwm/g+tKrb3fOg10VvQCIuuN5Zk 0Q1+gk0w2Kb38OyHqqBWBz7sjFp+1iajAlTbj3W4s2g2KfhB7WGyHU8dwrQubALS QwGZfbtnbjVhPpOdftqbySiy0+YO9jDjAU0QtCOQCuFrZlWDjgYMlutAwB9XELYJ JAxBuk9ib6waYRuIf9+5dXzUctE= =2NWj -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-dave-alice-bob-carol.pgp000064400000000000000000000035111046102023000236320ustar 00000000000000-----BEGIN PGP MESSAGE----- wcFMAxR5cuEvhCpwARAAry+JdnibUXqc7Orv54vuJ4VYUdBLzadlGQTbdL1xEGDd KY86eTKgvaihFSUs4yGragGYum72LRReiquMcLztmcUwhtr9h6rirfGhVOGDYIgX kZLpIh2DHhKAeWVvhHRx2RK2a/baJ4FuBNu3AJZem4uzC1lLt0nZS6oq6jk9rcLE Mc79+OkMP2K62MsV0kYaWHQyHcPEOTjkH61ILbawKb9jJMxLI8UoiNma0eJYs/rF d4sER4EOQh1M3dLIVPlAIaHi1GuRcOV/yEpqipYCSYubat9z7/QhbQXVMl/YYoMi lKFo4gpuN3X3I5XISPOIzUf28WJvmhroWxjok7kerJ10IKoQi2Kc7u6pGTWBK/RW gI5nushkua2LhPT1R9EKltP3mPN17w2Z5tQLl3p1lgKVrDIJMYVa352uSkzFvC0S 5Oh+ianuNDD/J3CGMGDmnmoq/ib3FteTGcfYLo5SNxUv0JOZeWnUm5pW4/sTYMPj S4t0dB0koZbGs/NPSxp2+5mLGR+nK7vyP8nPd++S4fgvv5t1J6m+9D/uZx3Anm29 U+BbBYWFa0yqM+mJ8ZRLEmselwC/qMYvP7nup9HE9KP91yLVWTHGugxoCx/52qt6 uL5N7KcNOzM8sikCHa0Zmek/N52IJ4VHj1+gvivO+r2VgVWNGlQyRqzpxrNs1mfB XgMbGZSNeoQkpBIBB0CAWZ3zvgD1FpWD+UPZvOxExpqXPQbiIjCez6xpjWvpbzBo oLXX/cAHFYLLrAcCNl4rfEA+R01QRucyagd2zNv+BjbqptCnzZQPdlJUDxILh+/B XgMd873R1gcrRBIBB0An13d5+gN4N2Lg8pEfslZ+QMkAis0NURpX/8kJoBBfFzBh PIbO73sln7PkqUFBuakP/BEDC/0YvCfiHDan0Nevq0bWXyBnizCZp/FiDf0LdhLB wUwDrBPKp+KCMksBEACzHnQuLP1K5+LYbuH9r/C5BcnFS6/qare5t+yU+E2SfxCu vemmefc6QMlcYb79D57deTLRx4rhCKpOBd2WT50S06+PNgdt9to86YUNg7B3MUTG VoS6j2JNe0VEaNx0UMdwDuly1qOqxe+UkQd7MvR7sPX17SVRfz+3dwFfwnmtTAhf v3zWPD3FsVNXg9VA8+5t5fE6Sg7fC5SBETdejQpMUW5vw+741vvNYnheEMODQMVv 2JapP4cz9KmOAPglc9t7w+LXOQp7v4JUBtp5stu6L8ynnL8zUgj0yArXlEiizJHD H/u36OaF4pqoZRY9mD2JAB+AA54UxB9J5FsPc7cBNYEZxw4RBtn26kOCTlFoj9se 1ySgTZKr7CvFN1Yu8qo69KhJIEBks+XiMhitYF4YBccM/yYaG9iihW3g6oje2Om/ kpUKYBMmnckUunRA1vSfRdc3zaHOwA2qc51k5zgqiRjfMY/nDeaxHG8VObaPpDOy Q30vtWnPv31TB2QHn6IynJW8iygtCmWf4KKEjRfr0qcmCPCqF03PVnlybeCVe4KT TZFrYjs4WcKIKBkLEf+xgqFEHxSuFdsivEe49YqhnxkWaapKeUujQQObEPZwxRkJ odgCUMlG/DmjniD4l9fhyPAkqhdRKcWKAoDeryCQIe/Ihk0fn+pbCb0/2ljsb9JT AXH0f1MJTwEMy0btKCqWLM1YKIq62JI3U6phye8OC3bp9KajOz8e7nQzGxvuk04h LUpjnsvPCrpzQfnWgHSjwFO7g15SmJ7vRnlQ7umLXVVs5p0= =B3K4 -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-dave-alice.pgp000064400000000000000000000017601046102023000220000ustar 00000000000000-----BEGIN PGP MESSAGE----- wcFMAxR5cuEvhCpwAQ/+Mu3Cal1Xg8QAQTX50LCzdx26odzgSrhyzFkCjHt75CRm sUvOQOB14T6r4QsuDF8Kats3EN31/RdjRr+Xfi4hiGQQqfqTlzktaCRntZnTrGSz jnF4821Rp3Nk6u0K846hsqsI2T//G1XvrKVv8fgxYVlAEdOOFsunT67kkdRKQXMa bR2uLvB1NjiBVt++3Ul4TCE2ZsaCnklJJoPMo999Kt8ZcEaMdULLeAIA+tYeLe9e stPm0RBFkjZec78afelWH60VxgBMVrMQN8yYl5VAppQxbztZLW+xcTnkCieSL+es PToEJiALdf2nbjOSuYTfSFhNW6RocDSICrJszP0gvpcCF0DDGeor/8eMypW/qSZP yMwmsfcrcf7+zO0A8RylzgGwwwXKMb2AaSO9FaEx7PP0RaMYN+DjDtrOYLDgk3Ad lcj6GtqI9kHELadQ6m9yoktjBMDjBYdpXeK9VsxFbbay/X7CGlDjJ+Y6FIk+Alce n6mH5xEyTMgaUWAa8A5g1M1zcqTxMhUDLqum4KXN/FVGVOYnia+sugcQ94KU1gck 4YkYQOMth7oOLkc8jRK7VnLE5uos1pIZBKxqsfnDIPMXqqyC4kp9yCV6UlVnKgP5 ZJhRdXfiy12jSxWRzPHHaZd8hkADF9plOlR3o4mv5FIazMjm/Bf45xLJ3nb0+ErB XgMbGZSNeoQkpBIBB0C4MQgBwreT9oGWKKZKTcTlnQyEoRi+2psB/QBGGeXOADA9 pr5xn41zQTMX+1g9zRG46hyWupoiWi0N7wlY51CTlBfqI+AzwhC4pm2OzTM8VqTS SQFwAqWnNJe0O54PV3xNpJWYDRXp52pcIefEaIbxSYwHCdJP8c+ZGsUDDox4V8W/ /R/+qwtRczbDoGlCpdDd/vBnzr0J7CIGWqg= =RgHc -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-dave-carol.pgp000064400000000000000000000030711046102023000220200ustar 00000000000000-----BEGIN PGP MESSAGE----- wcFMAxR5cuEvhCpwARAAjbDiUbP2qQPPJGCMqcgtr02SzUMrx4GVVGWCU/C9tu1M hnZbDdUSRFlnjAziWfC03ke7n509ZRIUecY5Sl6hxMjFgpU7CGFsd3jic1Bxv6gV TEebOBLWxQNQYh+y0xMNu3buU2C//8q6ZSjLFlnYIAuhD22zSicU4oacLescwOiI WUEMWDalSVNfxvhKdqWSnXSWvbFAdFJgpCmJOIUAovLxTGMCudl8jsoFYGQ1xMp/ HedTl1kBywAx/FiRQOlaEG3p0Sv7v5Wua3wcxo387hBKLRvt0wdTNSBONUjn8gp0 BnxaK8yEJzCyydRBGJsjqHtEEIIgeFnI1KRpoDYgOnrpxtFgf+jjSpVCsQVuhpbL 1QvN69hrQf3fUkWObE7h0UU9ZhMLSrHJigBrO/vP+ntMxfYPtJnMX/tiAeThYZDA x+AM2lvPb5bl1QSGrirhsljM5Vslc3uiFPUWtSTNhPaPVnsrQoH96nW5MzcFitXB 5dZ4uhDOs9gupbh4TydNrdeIZqeY5efegxclPc2zf/vHakMTufg5SJVDQtYWp9oL zLxzBE8/bfNF6hCiOd42cRvJaqSPohbfpLfQLcA+es0l0rwHfI1U1BdacwUh8lad z56Fsl4u/4hdXnrmTiSjs8u7b0cLxrGxivHK1fXEOT2KTeR3iLB2aIymz7Fa+dnB wUwDrBPKp+KCMksBEACMsQrQdtZhkKYI7L0k5f8fVuH/RLqg5elbt4TcpCZlfIjD 4jpyS55tll1MOuMl9yDJSHrODcOLt3WKMQgQObYHYquAlrg/FlPx2WrRdHhdR49j aAd130mBKOpEuT59Ib8AnQ1TGhCk4kNNO5CYPfHfFwt/s6xi8Tl8FT8UT3gTEPE7 x0pLD+UGdwzPygt1tZIfcRTVslGrvWzXAjL+/UQVCFoD5EgZFU9zT+EwZ8Fc7cDe 5s3rVZb/gSvUk2y1XkkOs51wO8h+upbbzxVF4/NFBeFpGZm3JN/AZ1wGcNoYCbi3 IXUsaQ+Em2TSiPTKKb6+H2q9ISFPm5r+D6AXZZrWWSzkXHUC7tEoUnf6F3So4Jls CuODgoHhi9LDkap7j5Xn1B++E36dkZbh9v08qc7pmh1wRQwivk3Y/5XdiAlh6NtS DyGLT/l78xya7nEyxm9p6gIR9QsbXzbRpRWsawPU9wGVaLG0yptTuXSTFyWuzGa0 gCAacn5k4SOqvsypxhj85fR0YmCqu7gjZSxquj4Y7Gt1bCyNy4Zx6cZ4tbCxniCK IcWvTWRndFm+ArQ+XZ0rDb7ZFZMID2Zye4saQM8rp2XSQHc86ktI9xXoejMilvJw VZ3fii21Li3qUhKf8M2zTC/UNUq8WoJwDMzVUWqrgo8JgT/FEjnHAgXL0x6Q+dJJ AfcwUiVVSXZl/Y7/HnxjOgdkgqEBboHMJNcnylgEDYXgCYR4oTtIR//i3xN2odkr ijLU+Rl4RqtcGKypDnfGJSEiLUjJT3LodQ== =2rlg -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/for-dave.pgp000064400000000000000000000015461046102023000207270ustar 00000000000000-----BEGIN PGP MESSAGE----- wcFMAxR5cuEvhCpwARAArB646fNh2eydNTwQDkVB/CIhJd63ofmnxBwQt6avZysb wLx2JS+Wi5HXYlcgTv7Wqx3gLojdLBnnMFwTG+fNtSXsvyboJfT1JIKS2LaAsGgs /iKAlc+PzuE76WEuyQyzC0eKwgewEIRKrnWJ8FIBkKhlGuHyFjwcgbEcRbLtQMJs KyQLIW16xjoxZ9hO4KQq9VJ8VcIDjtLfq8ajcm8IuYQ4TjvJOXql/6hsuJ76GVO5 NLPj4o8fSNyY3KgsdClA7cYtTuESFztDrqpHnV96h+N/CWGx/cfH6RJrzARbkY1d XCnOjBh7DtW/wQxae95GuE/3p21N+/tAF41K9DJsLsJAarnKF1MxLq+7R8So+E6f U/YfgQHLSRIwiAwCF8kYiZUGgJv5IuJSlSROZMAKXBfNikIiGaVT3ENso9Dfhdjo TeVu8ta1vr+Ik5gvUkAKx8ROyKhggEbo/tWcUCHoiH+SI7GCuKBB2Zi0BnV58rHV ywkYxfogsBtwzEq9DIXkJIJkw6KZspJGKtsCEuNjwHSH/FxqckkVxyp27V+JSjrX DYrirlFJylnOSeoV32yfNq5ITEak90ssQjtGAV9afKghZsV4g6iaKh38Lq7ofua/ nqv09nBn8pTjNmooiVaXv6f+xxxPSNPF0pRzty+jayH0IxXg9TjU5OD0VBTEb1XS QwEL46EH/ZK952thphJdx6KsBa3MQw3k1QqmKIMlcR6Mn7CKEqUEgdEcs2p1hExZ wCuPnYZF3/LGUKeZdKhL51ukc2A= =6bAe -----END PGP MESSAGE----- sequoia-keystore-0.6.2/tests/data/simple/keystore/softkeys/alice.pgp000064400000000000000000000044211046102023000240100ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: AE81 1929 75C0 8CA5 6C25 7BD3 A29F 4223 DDD8 81B2 Comment: Alice xVgEYzGHxRYJKwYBBAHaRw8BAQdAJ9rAuJ4gDSuf/rfaAEqtDKsHN/Mo/ZtAPGcs Difsf4EAAQCWciaJOnLN9j5n20uLVbTbvMoZ81S8W7m37Snrof9roRJ7wsALBB8W CgB9BYJjMYfFAwsJBwkQop9CI93YgbJHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JnjDHAhiAiqVy16X8eANAAzEnZqtyIs4KgzUtMcULGxq4D FQoIApsBAh4BFiEEroEZKXXAjKVsJXvTop9CI93YgbIAAPBhAP428z80XKxoVVgp JorYh7jOUPZOrkgr5omch+Kkxel6gQEAxb1lgKeZTnfNEfwcM81c8um57bQgLqqq 8eZhNU/P1wrNGUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CwA4EExYKAIAFgmMx h8UDCwkHCRCin0Ij3diBskcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lh LXBncC5vcmfS2MDX2okMOzhZv9LofGYWvlMcU6qwCexdrprl2A6e5wMVCggCmQEC mwECHgEWIQSugRkpdcCMpWwle9Oin0Ij3diBsgAAUFcA/jXQN07PTcieW1EtbCNe ibXtkKtwgYXy3LZ7WjJNDzWTAQCdnDbtxcDUWL21JujeNcF9Jc11gcbEwcNVJsDr TQBWBcdYBGMxh8UWCSsGAQQB2kcPAQEHQCodfl/lfXU1p0ElihQeVLsS3GrmFqlw TF4uy+XprzspAAD+Lbeyqce8NBfMMFkfYBjQdmizekUdIrsMRmbHwKkhFOoPGMLA AAQYFgoAcgWCYzGHxQkQop9CI93YgbJHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JnZBFRcsVvodyVplXdfQyETKGmx7HOFiF7NW2LvVSWvxUC myAWIQSugRkpdcCMpWwle9Oin0Ij3diBsgAAvhUA/0vyKMLyfv/Efrh96q+zeZQc HoeG2K8B2peXGshh1JZBAQDnSyf+wPDYc6iUJftwxoEWWIyDdHwNaUxDPfuSgJMn CMdYBGMxh8UWCSsGAQQB2kcPAQEHQGR01QOqFou1Cp2J5kO964PUwG+tF8YuRyQw B+HaAV0oAAEAm/coALRVOfJtF6zW/Ock0buFSoRUqtnj1pKKlcffA10SIsLAvwQY FgoBMQWCYzGHxQkQop9CI93YgbJHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2Vx dW9pYS1wZ3Aub3JnOac1Ycbo6jfMPTL5cQfkEfFBrNEO/sDHaoLDO963i3gCmwK+ oAQZFgoAbwWCYzGHxQkQYXebQTgm65hHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3Jnh3Pwozw0imEoOo8NFbEVbNZ3kNpOXB5pQCnu8xVQLzoW IQRI36NQlM59nuBIPtthd5tBOCbrmAAAv3sA/0fcgBjWZ9mcSqA1deRYqJ4gdXXc d4Ld1sSC1jiqJXnBAQDr64QWtrSvXxTAwrdL20EcaqJkR0fetDFkfCMpz4vUChYh BK6BGSl1wIylbCV706KfQiPd2IGyAACX6wD6AzF/6n5IQhBcRtgM/ke5aqdy2ewk tzBovrhNH3nwrQMBALYbTPtMhJ51kRZlN8wS3xh3o6xeKCWSN0EIyTOSN9kMx10E YzGHxRIKKwYBBAGXVQEFAQEHQL8+4ip/AxDAdbRfGM/el+1ABtLpF3ZgjFB44f2x dIQ9AwEIBwAA/3Yryd1AvHJWQrj5bK+M4mxTSclsUE/OAA0EEu5RQAVADxfCwAAE GBYKAHIFgmMxh8UJEKKfQiPd2IGyRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZ36Gmx/S0sCNA61L4N8kGbOQHGE0o5h+y/XX6zFsf2GcApsM FiEEroEZKXXAjKVsJXvTop9CI93YgbIAAFROAQCJjNSiQNmM5My0a03iXPZrgQ0I Vb2zDWqHpxpk3gIfWwD/f3inBpONItaATeo2KqBxiuAixqVyr1FFWPxF8RYE4A8= =v1ce -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/simple/keystore/softkeys/bob.pgp000064400000000000000000000044621046102023000235020ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 9BD6 F501 FCB4 3697 0C1F E23F 51B9 EA96 699D DBA5 Comment: Bob xVgEYzIUThYJKwYBBAHaRw8BAQdAFbSyDhRdv5tXOvVTsyPL6FlzaAatQ5EZnKDF XjBF9moAAP9xeQ7niLilOkDcqfDE3DZQExeri0LWuMYBQk80rRxMbxAYwsARBB8W CgCDBYJjMhROBYkAABwgAwsJBwkQUbnqlmmd26VHFAAAAAAAHgAgc2FsdEBub3Rh dGlvbnMuc2VxdW9pYS1wZ3Aub3JnhN+7Buz9U15gu3n/qeMOmGE4VLAcSbbhxKSZ XZzfmGADFQoIApsBAh4BFiEEm9b1Afy0NpcMH+I/Ubnqlmmd26UAAOVzAQCOaQl9 t4m9GeHfIFnCQ7ZFpP2c78mteJOH01FuFWTCqQD/bjkmB1tmYqjOUNPbJH1JqX/P 3hEcO+TgIpoFfhfb2g/NFUJvYiA8Ym9iQGV4YW1wbGUub3JnPsLAFAQTFgoAhgWC YzIUTgWJAAAcIAMLCQcJEFG56pZpndulRxQAAAAAAB4AIHNhbHRAbm90YXRpb25z LnNlcXVvaWEtcGdwLm9yZxWZvA9PFFo4neeQd7QqCchDvPbWVjpstXeIe2qugpwf AxUKCAKZAQKbAQIeARYhBJvW9QH8tDaXDB/iP1G56pZpndulAABGnwD8CZOKcbTd o5S+XoNdQEpx6oDYsU+JIeZNCDvCLKJc8ywA/1qe7SBXGQ/yr/bOLmdzOfEZOghe fzNkrudve0w2hqoJx1gEYzIUThYJKwYBBAHaRw8BAQdABLhk17JyX0pj4YxLAIeg c8MJktEuA/kMEsAKpxsxlOAAAQCRw07WTG/7yeG+Z8LsF5RVDdhe0Rmgp9d7cv8c qynctRJkwsAGBBgWCgB4BYJjMhROBYkAABwgCRBRueqWaZ3bpUcUAAAAAAAeACBz YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeahBFVbGlUsQAu4JGEY6r6vUu0 lhYK8lTD9DTfeJjCogKbIBYhBJvW9QH8tDaXDB/iP1G56pZpndulAAB+3AEA0Bcx HbOpr8LXufMjoGNNKEBfnv7mzI2gY8yBETrN6/cA/0UtxR2mis3Aw0UJ7Peog05s kGdrOmjD7Tkn2X9oVn8Gx1gEYzIUThYJKwYBBAHaRw8BAQdAiTZO7702BrujkmnZ 1CXE0KdEATyC85WZT1aJ5ezSGuMAAQCfueqxw5btl2oWR6Y3pRjqR9r/uwhh+YPG niMiGBL/7xGdwsDFBBgWCgE3BYJjMhROBYkAABwgCRBRueqWaZ3bpUcUAAAAAAAe ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmc7Bbvps3NqU7dMKsOqypKy Qd1KuysEhJbWGLKHqdVi7AKbAr6gBBkWCgBvBYJjMhROCRCgjW+hXNs3NkcUAAAA AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeqxA2HMTTnRgaR1QyP IXpkKg0P5X3SC27eMpGG2bPo2RYhBFTv2ds9z+32nD3EX6CNb6Fc2zc2AACfqwEA mG7YFjZQdXFshPBXVWVbUxvSRVDobFjDcrnuuXk8PjEBAOduMihaizIpUmUbUb4e 3+qCnq924Q9rSzJMHSsYwoEPFiEEm9b1Afy0NpcMH+I/Ubnqlmmd26UAALw/AP0Y 1OLO5SO6OW65WOaVkONtpaTOAwcJF6szD/ddN7rLswEAjarLg+MqZYAnzo7mgabA d2DZizRLT7fZPqQ8SB5F1gzHXQRjMhROEgorBgEEAZdVAQUBAQdAiCm8M10guqXv nTz9kD4TSvVWa6vHx6boNhkW9kbVmAsDAQgHAAD/QGmzblOd8ysWPShqKUnnF7sS 9ZjpZPiQjt8uS6BHu1gQO8LABgQYFgoAeAWCYzIUTgWJAAAcIAkQUbnqlmmd26VH FAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn9pEtKAzSeNSV IYqxTd6Myr39tsvXCpJ+hLHEbizf9uUCmwwWIQSb1vUB/LQ2lwwf4j9RueqWaZ3b pQAAq/oBAKWgfDB7di2fp/bMFPlrcT8J5k3RtEccNgkftUOBXoD5AQCwp2KuZJqH WqGjRgBeHypaRze1cxdZ4pdh0imQ679hDA== =a2zQ -----END PGP PRIVATE KEY BLOCK----- sequoia-keystore-0.6.2/tests/data/simple/keystore/softkeys/carol.pgp000064400000000000000000000357111046102023000240410ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: CD6C 4799 E67C C4A5 51B7 F2F4 0043 1FF5 B8A6 82CD Comment: Carol xcZYBGMxiL8BEACg4Ox9mAqWUVUvXdNuq3uOK+qTsIkbOMasGSJS2r4+JK/OXa6I cNs7oM2+ENqIF41SlYRj7t96C8R11WEDU9OvvL8MqUbYrut4R1Aja3NDIGR/EQNm QhmOv6H+9VtEz9H+VzewDdsLztMbqPDtuSeTd0j6RVvO+G9Cmm+icH9+6rs416tA HWVaU5OrhYvBs5j/0Uz7fbGVmxT/FA4gQ3FvVfvGz3EkJ7oDuzn+mejDXtCN6A1G NMIXg1T5B9zRxmPM1veXexKHHOOAqtoSbhXuYPql2I6O32a9YxLOWO8Qp4ohVdGF LG1K5Bisl6zQ3CT0iXzntCWgQp/H12rQ3bTPBFy1iOpNEwCU5O8tqLTdJqeLHrcL SGJaayCuyPTmatvMQD/W4xp3fKOfug7VGEmNzOvqYO9lE8lXTCAaGB2pz6fj+mpi nLGJCZAmDc628Sj7hwsSvhRwLguQac4ADARUmowhLQr6mGrObZfHM3kJk9JlucZZ LK9RQPYxxQvjSXWpUeSaHVH5X5Giyjh1ElaDSfhw4oiEfskHYYJhw3aYa5ULdAmS 8GBLLnXZd6WzvbOiZIGdmbXcqtt6HYn4WusePOKw8J3T7JDMeH77bYzvVvU6vHYS ZbTmJ0+hMRsqkzzx6Khy2MJQ5geXjp8tflBSlenh6/rg56cHojTDAMGlSQARAQAB AA//ak52tz9Cr7Wq3pPXZOf3yyiF53XeA5ju30X0gl99Gr9PVMRe0lYQ6EQH8DJe 1VMK1alrREXScmlYNqz7YCGCXjwQ2qL457IecupcsCsOjSrYfYaWgnnnPYav2zPy TXQuRS0I5VMijZjrsvldyxhwaGeIV6iBVDCiBJbccbB47zrY4GTTCpjiIl/b0pl6 klLFJTX+8qhKMpu5shxkgTlPucxNhUAKeWOu7UxItE6rmn4Kvdq6+std/f+QpMZn Sk8YH4FlNjnjMlkKlDlRxJ/t6gk3V2PUP+CrmTXgdW4a+xxakFCPFLsYtUYob9QC JDl0SgqX+Nv3iQC9aGaWdNIr6RtedeLYu6TMp181iN/SgfY3onBqptKiq4oDid3/ RONM8NuQy6mdPsRG9iRWY10adYcFmhn3qzutvKtjAOgjQGd3b4qDAPxqCpssMnYx hNgKw7nFWnGzjOhsFmpgRRTKFJs2LOXBLdTCSW0BjdBrjtCs7U0LRAI8ghQHNskd R8es4VO3aC1kH6zy09B+qHehzO52s7KulQC7fWK6U6O56LW/zA5Wg/PMAawQnQQp 6yHJmGg2A5W4Zj15zqCnFKkIe8WrJKMvsTylJNJqVk0MkAG8gMWXnEhOpZzkA6Wo 4X+q6Ua0I8EXtuLEEVs+OmiY12wD+vj0fKij1AJJf8jpjAEIAMC2vsXe0su8i889 TPxJhzbkNaMO23yEr9HMCuLVAa6R5eOBk71YI0/JrZNuxe4+sKJsGn8IXtcu8aDK wRLCfbDlIH2xsv/1Y0/qUopSq/OBAjB29tg+Be1bzBQeXdTCvYvZcEOiAg1KIGdg lG7Inrv1bV7v8w0zbG36IqVTv1w6+Wa8Npv8fQjIJKJgQR6sKP7t99Sm29+lkS9J YlIOx/CDbcgj7fzmzy4oM1csoHK/PeClJSePkkKJP3ImnB79hqWhx/1UW7uV86BP DYiuwRyMA7b0NT71sF7Fy/PQjYYWb0YRZ0ompDD/ltUFytzpTQLN9u9JgpSWePF6 5p6olYkIANW10p+huzNLk9OckodAfbFIHMTATEgjhsubV254gB6n3erzTdtIGh21 OL/MgMjlK0JKvpaRktZ7xiAM6E6ZA7GQ45AYiWd9mTchcrGtLzHqWUoKOs7sx791 AKDjGgi5+UwvGii7lFto86Qvx9GsoHJUgtL6VYnZ3CSdGuK8l56OYAo5Tsx3YB+V UGe+hJ4K9zROyMd/mhVmRZpfK+6ZqzGKIJXC5THOCFyOJeFALRqSQATkqji86fUe gO4PEUbPZ1dGVbfyOesdz3KrYRBe79lduFncam9PXdF3hCYCWG2oagKqd6VhzUzF lNV0y67xqwrFhccsjYzQ/nNzfasAYcEIAJPJw5BVNHKHYhuKMNjEaL7sVSL7d4Op 1lr/4PNTueDypUWsqdGrJG53hgGGHMgmDpyhShIngQ5t3Yf9Kh1vwfA0Qgn5yOMT rRT3hczuyT61tS5XrP/uLnRJdqw8GIKcrO6MlC77zpe1MJcmtP8tXjd7WqT00l3f GI1UELJ2Rz7hFKngRHWTRKzFehelSAZCgEUQoO/iYauYTz9FCPqbmFrfiTTTOfMj oCwuZ0GZ7f56Emo9guQ1pOYWldhM2hjj+tG5GfsR303tP1W7nUez9gw0pkect+v7 ua9nxV6ecjz616Vb/wsYu7cyPUusWyX+cz4zQvoOysi7OWUmfkN7nQyRH8LByQQf AQoAfQWCYzGIvwMLCQcJEABDH/W4poLNRxQAAAAAAB4AIHNhbHRAbm90YXRpb25z LnNlcXVvaWEtcGdwLm9yZyMcBo0EkA3jTQrv5WEBZ3T3Txj2Xw6EjRLm+hkr/7mE AxUKCAKbAQIeARYhBM1sR5nmfMSlUbfy9ABDH/W4poLNAAD87w/+JuHoLBfoGzWV PW6Jr83FDAgsmgt0wV7vLN82Ux4f3Ju56NciiKpMY3J7kXP3l1h9oimN1N3+Z25y U1UpRBNVpURDLH+wRGQFwqGAH5aT4FcQru5WLFxZW1xhZhLmiNsD/MSMrCTLifCj 6PnZE9Ki+HUdtUpIg74/yF3PtpQ7t2cxqg0KZiRc8KXsh7/EL+YqVaYQK7rKQ796 RWYl2YeQr6IsBN10vqp28Q87m6MH40eIpG93Aa0F8W7ZAnnSTwIqjG/l7IEB/0AE Fd6sM/0f4F8UpSLX6DhU925Q7xbIQddhT0E9XEC7XjYdP0goNcKFzpXJkOwxWAT8 SRxTUjLV9I1O5inKDOTzCOrZTSRKDbYkZWj43i9MrUMwqlNQpHknBFHkvZ5Tfk5E 3DGxqJM98V5/3F5B/NnOcR63TetaDri1EY0DHr8c9Eez6+utnXUCpyWQvWE6K4AB 9uorV9vP1FZ405x/QN7a4vNfjZNFIfr7iWD3gO5ve1c0wOgX9Dfoti0YLzlc3gTD icy1GKGVfPjizaRHkuN/ZnKiUg1h46GRM2C0gOV/H1zN85Zu7ok7a5yVvQ+ljt8U OwMDwdUX65QtgoiSimpyXlwySSUdMn/xxDXkj22ipNt8WJo7R33acI8A5W/Wrjgs DkcJOv9djHI+8S+kgtn1W5sxklK4SGvNGUNhcm9sIDxjYXJvbEBleGFtcGxlLm9y Zz7CwcwEEwEKAIAFgmMxiL8DCwkHCRAAQx/1uKaCzUcUAAAAAAAeACBzYWx0QG5v dGF0aW9ucy5zZXF1b2lhLXBncC5vcmegNtLllDYKPZcaeIwNVlk5rwIdB0HK7a9K Q+pKFK4wmAMVCggCmQECmwECHgEWIQTNbEeZ5nzEpVG38vQAQx/1uKaCzQAALtIQ AJP/4nc1fMPourRz/Pk6X/sxz6tOJ2mHzBMd154IxlfML7g1SZg33+h/lzcySfyV GcJrQ6gCN6y6OYYSTzl1gfIoArhNai1acktYprF9UnxTRl5wxZjjrl9IOpUA3yt9 tGHDv/MbazJma90r8jOOI6VAam1cRGTFFzjl2hHEvCQXLNfIYmOsjYT5hY7hXlSy Njnkp2+2fNrEgkx3yP+qOPZlbNL74FrCDNsJFP7Ul2w5iUp9y+Wxm99/WyxzO88J yCRgw+jqLRyczbI+a+0J2EjaWduZxDMMNu69FhSt4A0vjt2wsw2Q1z/swdyOQbMh ZWxjeG/LmPB4a+dOZm4eeFwDatrQvklgAtT1o4N0dJ4ZHy2JMj7XRTQJnYGsHM5q cjELk+aHU3oT8R+TZhowIp+ZF4C1imkjWRRr9yKRWyWNDN5dbk0lZoLU5N/IjwzS MOs+g3n4qGCR9Q0cqWct0PU+T3JXUq3JNKanMtJ/hoWZo09qoq/1YXK9tBatHt7J z2AiCzY+U/p4n4cSlRkxbbci5WwTjrNZxERdEGK1yZ5X2KyejONC6qP5GMU42d8G 5yvf2fG9HMj4d5QoHYiYDr1M6URbZXZxgzxT1jeL6BNtG6e84fEp+w5pgISe9WL1 rqK1L+xLAFVSDaXbySAfARLzDMvlqn5TL14yt3qXzjbhx8ZYBGMxiL8BEADO1YC/ cjO9z9CUNvMsHMKjfz0LI93D0tcFNSXcj4ZT65jVnFEoQSYWRBHT2HkElMKMBey+ h+pJFlt1Sh4bAelONWTEkb9C/Uoy4Pr9wf2gK9XHgMGVRh7Tyn7y+gqJGl0Mcncn ENBZXqceAOFYoJlFSEsCEoe+i/p5z2qGu1TziRVhJPUHgDOR+spaqy2RDrvBd1Wk vzaX5+2bsDWSYwyATNeW7BwLrGHS4x1uAFjmn8RtETYLeU4axBsO2JX4mEya+JDl mQMGsU5BDXt/Z1PRvEEmGiDyYMzF6HBGR5vMEz1io85WPCJLEKaHmkQzpAkEM0u7 uYTqaB3ueRkzb5Bh3EQ72GWC7XWzbGSgLIare7kVXGkYhRYHpGb67YCW4qk21IEU jQtvp8n6oKzPBH1l9x17r3a15FfJ017TShUOx1EhW5rq8StN20/WJKdrHWu6IUI8 FLkD10T7InTVenL3WYk+Wt+6pz2fVa/5M8esmEN4YZrT9RDYiFVdQbLsMdgl5zRQ nj+0RpiX22OrReR4xB6qJk4sWhdEN0q49Jjs//Un9us8u1PA1eL4x5NZctOiGq4v aZBea4ARINLTAxOggDcCf0yLqBb+A1uI287Q6A259HdOeFeDoHKJcr+rHA/jzBaO T+cq05vNgDrWV651Wb8zOKCenxSNy0uGg79RCQARAQABABAAyGvIezrdenNyaPyq PDeOP8He0/moPcYDh09N5eDJyG4GRg9X8QrYLkE9iSs9cTv5HJftJV8sni/k5c2x SHhcq+U/RN76F567FHw/mj8tVdTELbLNYql028NfqnCyXtJGEfalERzQ15jJlWBC /I96ldEMpfBwyyp2xyw+zFmgYvCV3/Fg4ijPUAhVpvAyklpVg43wVljXPPGHpBJm hvglt/k9+0fFkWYz0Jc1AE+j1a5BN3KzDQE5tuQKWCN1h1OE/7IXLfRMI2MSMiUv Y3Arn5OtCY/OFy/HnYqzqNZg2KpYFEooehWBIX378HCC4At2lTRjkX+elUnYnymH SvkzTHIxh39QynTZlelLrxe94JM1k/GDLU10F3KouDQxG/T7ciphpAib54xt+26B yVsExx+s5QMMicLde1MUSH0itcmsGEoZ338Hgan2urxKv2eai0iiuBxQ7FN2IKv2 GXz4tSzA3WQ00TF+cOOE1LyzCIlvikv3ESweq4hQutvE/tcByChZ8rNcbvzk+cAM LTDEf6bJ5CorpQS1YAjVGmZQb7gsEPN8qecE84sN/VnDeu/glKVOOedqB8sHGEzp h9Pn5fKQ6i4sInOdLvSNPlZ5apwrXP4CpU7KNYqvqSgvqWGMDPZNBdue5J/8rQ6m 4d2TEU6t2JfScIiIiHEo+2rz54UIANwlR1UDjAv3/rq9dBy4MnA5fX7upDFWLqDs 8Kk2jaaYdAmUKIBVeVdO/bVV91xHZM5lfDsm8RzASXsZcuA/6KQZxzZ1LRTcqf9f S4Z/zlEv1C8TOKfehrk2/OtvzT6ogECk2XG2SxuNXxXxXdTFuKMzML0RZ5BHhCwA pItRRnIlh4EoZ66PzXJUbllSTfXtQQuHKS4GcWSsqYLVHxVba5+bz3y0ZaxERmN+ Tp3S+UzkddI8yqWK6W6ojTzX+FfIV3F86J7AQFWP/+quIM6e80xHbzZeAjDk3K8h AythCw/8uPVt8svdLlFfoWkJQ1OAQvya6FHrp6NzlgZkw0Cc1KsIAPCFNhS5xLXR LB9ENIJmpgdLFBTeyosfxicXbQn/QdHmEvxQ9k/g59pqyo9RkfeDAvWMALp54AZX FAyI6J3/0b5Itu4Rm4cE/+7y9h39UY723FfRVXatZLUCJRVLtYFjbX48X8M/bqhU 28vVSxmj2SHF6ONI4CkLhdwMul8K4cbvapPIqmU0GD796o+mL8Sxmf76YG8XhlN/ nrDgBvt1i/OkHYwiZXpFLvV/Qbp0kQI3R9Ufn9T9IsNzTozKS697zBO31ipCyYYB xqP6XLJiuxEmiBuSO2cfbl6YRsCHBfuU4nQIKGo7fw4OJejTyGU2EOoEzMdv2UoU S8KOvmBwqRsH/1Jeaw/a2jnd162qSIFXIGQ6p1NodjVLuATh7atmTlKsA3D/u28Q O7kVEwPTaHXAjfGnt3ZZeATUQYI2460vwEP7EvKECI+JyvT4ydYzitvpLAMpALmn qFJJIHMJVkKjRK/R/JzKZWQkIBFuETdRFlQoa9MgU449UJ817EkPUBtgSqQ2JCcH QFAuzFNJMz6SCzOmogg1FvEm+c4scnrbwwiGmuiJuqTKhuAEPpIAVxvH0WgtmZI8 SHPiVhZRymcAdByYzbU7YTXsBdSMeeAp8WVsBYZhr67FAa3qQPqkEGrBvk1d/Nwg VN8CJcSE9a3FdN/JOLtFz1ICpLLzgi3Bg2Z6v8LBvgQYAQoAcgWCYzGIvwkQAEMf 9bimgs1HFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnpWnB PKxD0r20HHMNcw3E7FFKZuCgc1sK689MV6teKyQCmwwWIQTNbEeZ5nzEpVG38vQA Qx/1uKaCzQAAauUP+wVQUNJqpNjru3hbhMj5PaoqnorDrDOt+GEPQYAYg6xU3pmd 7dA3PxhoCHmuLkXMc4myU0HP2H15T3LoRV5e+OmLXeF67kUdtVqlhF1fFxdnIUmR VWTQvaZYEWzHPWHIJDxCbz2mqYKxa3yGLAVaMR+iBTxb0enJ4JjO4oQ4QuiaSpEc 3CSSeJ1PA0i98u89Fb7tsz2lR6z0/toprcfl/ydO7XiW1Inw/ETlBsLPbbLlhneK y8YbJCRkp9Bhn8U9jY3Iy1cCBPHpRR7K0Y6MaKu1sYRCR82NzR3+gvD1DB2XpZrE xHk2Be5mPV6APb4ECq8LfIqy+BRaDFiEG3kxVtW54Jz12LTO6HJSQqOxVuVB1+Fe 4Q6qJUtMVU1LNVhLidG/VtMZOOMkVWLldfvN/F2tho5HGBGnGUeKyHF4o7takkuJ VEVev35kgCiVPlRdmEtuqtUKyU1O8pwEnD0yWIIL5qsyweCeoHSq1QBZYSmLHfkP DH4FkyC7qZ45zBQVYfVHiE1izVH6dKdoMa8KjqzPufJzZrU4X/UUWI2wUfHpSCYU iLC8WUfbdQ1lc0e1IDW4HO5FqIrbAzlspKAkxGPtolS0NLOSsKhEEvi7Z4WwlLWN ayudw0y+KRWYDhJTmPF1yegZjNtmV55GewMCDQr+1Wpq9TR9XALy1XZaUzO6x8ZY BGMxiL8BEADgctWtGvuLdc68CrxzT0TJkSGkvcSNCxwcP5nJj1nx0y8qazlHkIfW BmHCYMccxZWwd/IA/EviIv/xskK02PaPQgLnHEX3Qm7e26bMDWSh9x0sYcXvzJ+k y2AaNVX3z9zuEiR/9MEus2DpnIs6YDeQDMlmo4wYgXN6oY55Fv53h3bNe0YOl/8D WpS18wc4tB7vxR5Yu9eOTnhpkC7xEDdevhYZ/oiWS5bHnXecVv8mlCamgIo1IE0I 7BZUNIub8klipn29YqXXmfGyqEYL9CMGcGustIppdRv4BT+0tsPSWu4makqYYgYo rNHtMeKmlPzn3GtDxbivNMR0H3UQWeDVahchEzorngMXbMd01tDomUJhnVBQ9pvg xiyQnw+3nav3xZ7jNv6SJrTGqpjgJdzFXGiv7a/zWDzePhpjtmWjlNICm0cCLhyw r2CrpBvMqItU9vVO/eOtmhd+vqX7z50Qv43HEyLa0hMmhgCpEHhCoyJBWrxjK4/Y K+pJIz0TqXqXX03Jhg3EKsbolbzsaaFnoKw6Rm/EUJyaLX1ajBJcsKQkPX548aNa LBf/sEVEy2oIObHHDVKHCG7T6VysGfBpDHLJXnbyPhnBXPBfdFB4+FQNZzOkvnTI 5JNrgcdyBCX2DPgeMTu4NDmF6sYpH1lyXbLvGEz7LkFDHga9+3jOnwARAQABAA// WUiVA1VWdvWYFWlX6a7AuEYJUhVDGLAwsHQEYw+pQe82NfONR5CQ2GyVetMwoZtf nmE4XXf/X6d4lNbKflJIUlh5+yFeG6vLms9ZnJY/T8aTTlJegLOvVcipJAPPfNKt Ge8Roezk5ATrKkLhh4k60QylGTU5x43HOCMIH9crxrSWZ8r/5VJFWToxKefRGZdd LxgEqFugtbU1ZRi785z1ybRn6lM8Vj0fb6yOJhzdRqVscpNzZwrAWio//6MfXhuV Ya7FGg8h1ZAhe2ZA5wRo4mjaDEuNO8IgLBs6alLBtu4BCjsCDXgA5zFIGMjdVlKy ADHIYZ9HhjrBY0gYMzbtnnjYpGrNaQD27DR5Gxdu8T77nVQobIReb11Y2TZ3xQx3 SlFXRoBtwMSMi7E5euUFnLv00zVJZ4nT2swk0hb6vBGRgl0oZebJf6bkSfGIv/C+ c7SLim5WYOpkPTPxYzI8K7a+grqyjkGyKDyTyoLIi4gRpCznxO53GbJR9Cf1KJ7X OI3oIXbmASYqMLWQ0BZ+MTxsBrlyrmFKDTdE0iOq1nKAUZWWEx07V04YI4KAkEuk DYeVolrQW4lpaQHxe9HWHsSyHxzhAmCxXrfnR1Qd46Ry+dtcLcqKOrYGq4FLY5MR vWD+V/twEuGXZyLKYoYT72mrG5lonO0VWS/eM5Ai9AEIAOi3N45vpG+WsileRenz XbdivFjMuNiNfpyYrpsuZvByR44uefz0qDhJTf1rZeMsKJomptmIYeAZAE46zpKN L+ywUrahXxjr928h/ZSs3OVuTjAdqHJRE8GnQmIM32AIP4wQg0gU1pAzfRybASFo kNRjfcG3l6v8LXMW246Rg7JcqU98p8oNga664Gafhh4c6ai6NyUHCYlemWJiEz9S KjHDtmzBV5Mb+HwJj8Vqz1Ki4RlOiYnsN0loVDQIXjokgZblZ/p6H1BTpnEL3ylM JQNqf+BABiOWtjdAQsEwX/sXYKQOdkoloXGcW8guEfSgGByerIvOxdDLsSBR7teX x58IAPbn3RKg+REVNGL/culjjimvwvb12YxXhfec5FHWywwyxH91/XzZfaf620TI VVVcrQE+9dS7mihP0Fi2YHMLEmtA5RH3pBKfmoLr2iwUmbAAF9puKFfpupOrX8US 9mrp0TEwdc2OVNDYPRzXofArnmXW5gUxeenxlMBK1MRmlT+Rfom7VpLwr2c3Twaf fp2Md+TX63hb1APnd4AzjwSqGg0PA0rDDfjo8ezdlAIlHsL973PI7xGpJBwRVfEM SenpUWzxA13l7oFn65PyffE2pYQT5He2CeNbPg7cPz1j0phzgZsnMj63hZvDq52y 4qqIb4wYvrRHyMgZQyV7baWNmQEH/jj2/g3ErnE57Eqo8E1a7SZ+940M8m2q1fQ6 BgWNPtZ3dAinYDHeTvEk6fbMc1vLD7MZh5o9SI11uuhAypH0r3wOG5KjJO9h39f5 VALKvSHffsU7/oTx7fijbQ0fvGwcoO2brMSehS4Kg7ko+NpUjmG4afzlBB/Y0Xpe uMsYeP++uuIzVvhgiqsW4NZ+/FVJFrADY80gjFmI0y3X2OsXvNwkNHFBY5bpsaZl P2fGqzrdtyIAB7FIJwJ64HogwHSBh8qCU2HbOjy9eQZb+XyPI4GfPM631RrZB28p RYv5wiLaa+5P319/rBzFFEQgvU/LcmnCSzUXU884PN3nprxwlSF47cLEPAQYAQoC 8AWCYzGIvwkQAEMf9bimgs1HFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9p YS1wZ3Aub3JnmfNueFm0fSAmHqiTblobMrGdGhca5ZZdNWX1Du0HH28CmwLBvKAE GQEKAG8FgmMxiL8JEJcQMwRb4LNNRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZ8sVq2HC5RL/v/bL3I7IoDn91q//DmKL+FoaKpfe2SoxFiEE nKhSGJvbVxh0rqW8lxAzBFvgs00AALVQD/wMBfgR63Z2G73PSzS4GzxHeLeQ9osf kQmXIWh7SMyK2REHNtNInGYVG9Z2LyCAxeKbITUnz6ylVYzclMAxpK3OFxTaKuXW A28bwzgNd6ow+EshfqymQwco8S7tcBT/qVKdu79dFKegfNzA2MnHqJMti8cNHtXg B3Qvs6go419TCXHqj1/s9RndJ60LY9E/cCumakj3qyrKr1a3usVAovXKJYn/HMQa 5NHZiP/4uJqwwgaJ1iBmbQ/YkwRB1JSsDBqm4vrppXRu9jOeawWTi88DU0PwlQLY EMiDMPakBBlW3f7I5WYBkSYDowOc6M9WC5TLIrhKlG9+PyPf6EGsB/tZxtiOqLJv uDsXC5lCiIIY8X+288ElrenFvfhdgObIIk1opODQrI9j3VJM/7GN14vAMDsqja4v jrGc2C7K6lkTXH1oLCq1FNEMjr4EB47zA0plpLKd87NREw++Cy2sYIZdLX+yBK1T Y9L0qBvp2Zu0hJqk6fHbowo/JhstPrut6D+YmXJ5GklTnAO1HUnbUpMNYAS7lWe1 aBHgxfk49SAhnOFBp+hDH2qwKSLuZQ1rkLEWh7dpNMlZFVc3yIbO00+bzczWzTXz gUcMErCUZ4PuZQXfIawl2nd72J9mye5vNFW+HS/uO7ADx/eoQYq2br0dweht0sq6 X6QFCQT1a/DDwBYhBM1sR5nmfMSlUbfy9ABDH/W4poLNAACoDA/+KdQqqqOHWtMF fTu94czNX1CPuwFuLQ7XTlLlJ2YEoXKsAexy5xvYNaxmwSD/vYA2BO0R9gp2Q1gS 52UWBS6G+A8a3yanABXYXMrYrtzEhxu7SaXC1nhLLVTFFiPq+uJrmEj62daeXpph veb/9NO2kbtPRtHXgmccRN5IfBcfCTr+jlzKJHab+aUaLdxZUNmEBOknbH7ehBDT Ns/8SUjyx94H8j7R5eqyFadQfFtrMiojxuL2Km1TUnXfXXN3QesQfTyU9UFenUwG mp9Xr5WIHfuyD/2TeomSzM8r8JH2A+a2KsHyxXlCVJ+HJ6hw2fZs8GxJgBcvNP9f 43MYjkvo/LUsen0M5njFYix7FrLE5Ti5865efcEDi/nvpCeNTEPnnWh4lN4gIV3S EUJkfcbkgGfqD6Csh2lqGYwVi7G6qmPYH6X3zmzdshPwdO2baZQjpgaGu9OmkR+Q Xb+mLb+dJ/qMLggzqJZkkmrRRdqqZhmyqJL0DvDg/NFcfsgLsVGp9K0+fTgccWuc QUfkmTl6AGpGMDJzpAaHLo5Y3fs4rT7IKEj/zirFvr+yF/FiMK2hvVbrIoZwPJ/Q /mvq/moC6B8BoOp4SBx61Fhrme9eyQ4XeTpMmR8Bc1A7pHrdYfpbZNMixKnt79rW 0Z7VzUPy4G6apXkuTj0unq6UAnCm72LHxlgEYzGIvwEQAODMjF1qrnI0P/AJB89D kEc6y2V1IiUMm1BO/CtYaK1woIYKMAJ+PQw/XLMRPNRbZwXsZdZrENKEfKtz+Kbc odRBu01QTQ5JrcJ+bivI2+F6yqR5DB/jJFJ/4GfZ8nkWyu2yFlbz3e7t78aMAGY5 S8H58TAB9AUB7RJ7g/6FDMyLEOSpKm+I1MNjhtv4bKiknZV3DqVBJDxb31xSWiOA mux6gIZc2BLFkgpOvSfGsFRyLJt3xPzODilbyUJBRyP7GC42RinJrXAFKaQNHL2r zA6JMtq40B3sAP/ptJAMO5QMiCKT683dAq1c4lBchBVXUpZyKBKMDK9q5C+GcU+r iTRUGQl+HZfp8s0ovXSBHodsbDHgN5NYLmNYvBU9t6WyjzoDQnaqyXNe0Dbv47gV LcSvadvvUi9q4BITC7AKm1Bgs2RgqgiWohkzIWEC9gJF0PLM9KRwWWICwyld4+HJ o7VHXbueYABXW8Sq0fl6NAAyuNE/Xw+u32W5MetYzOdwHjKfDnL8xn0tuBXqW8xx RYSOsvqqZu4t5yGFSHKbxIWo5zn1s2a//Yp7tsnjl2df9DbBEV3p3I+FEEX6WmFP IYqxNHQgxgyygiZtbXacYo+VCgVVlvAauPUxl3cH0pM21LYQ83D3vlEX9IKyfRO2 4PhWk++E4qOX2nklUSeASq7FABEBAAEAD/92HaQHFZ3RbfjGY8hRtBtkAdWLWq91 LhmRad7/23Us5IfDxiiRtsjW3ZgUkpMWtWv9ZcfgkOMG6gwjfk0BR95UC7GhnvbN 8hK1pf63TNOTtxQwtVWUj6GvI3fCuXa4/yvjFva7+2j6ONUKytcO3z4aDv0Wj4Aq /WcWlgW1vy0JcGyQ6P3eGtRIUcwzWmWletE+Do+IbyDeAhvrg23PR6yfYoKTlscF nf/tIJAtDS3rQQ6FJbpAZvhz7MFj6a66jh2XcncNT+WsfVny09ztIA6Y+Kfa2s6n WjDwRljUc0j+slL5cCWvRlg4xRw4w1O3bul7GUJK9IFZzl1VR1vUU7pD4y0fydWr WwRYaACRhxonaLL/draRzNRTTMTRlV4CUBlA6ts8VC0EPal2+VrGjMXqqzeTGZvf xNU1AIOxNgOSBYYreyzV4eWT1MjShkogFUdhnfy9VkhbVsmw2Vt1ismu8QQwidmV KrVuPPekLsbSkRkiI3b0E24/mLes0+h+6RB5B6wECfLOgi2bVAfOCXykMIiKiBXe PcpmtMH0cZ0tVfEQL6MkTdLus/pef1J+jfc06vEoDJk0jijB6Hc188rIlmMm0jsP DPDd7wLX1LQSsT+tFU+qskqQJQv/OmVMoE7clW7B07cPBPkpuyLc5wOy91j6jk3T hB9c0zqn6c4xYQgA5UJG9kHhpRKHFVPuTkP9NCFBGYT6fu6OpYz6OrWil0Pq0he5 recI3/I88K32lvGaQJB8tNBt8lV05BIDdusoPNdB5LY05KJqWnan/h1tKc1u8uI6 pveLHKLwJFAHSmJ+5aKtscyLP0BIaGUEu+nyggEmSDnvBGWsOz4Sc5uN3eoHANZb zqVi36/3WHPmuggk/8AkFuFpc7L7alt5NGYo9GK8t80qp6uoJPdHkIIH2C5QgqoD BTynKy3xiqn5L81FdQGhsNKv65i9tNa/jphdh69+88U7GIywqMIJ+f1ObIOT/LA1 pz4MSJycB7w3hsmUXSD/76Axpdimd6kfHozrmQgA+wUZHkXhOCSyI1+zFuF60S72 zNLBBy1v/KBk5lvurSae9/sv5Y2UhEBwZ4AKP5zLH9QCbqqD4ygpr7PQ6v2dTcub tIwiCFChL9YqnTjS95rCV4Avhh5zSwvVflPf7BsktnGKOasz0aIbJG3avhLZ7QbV ekrzBDhLjcsBsLvlk7mXzuu8K3gO7jICBmKqsz4TErOBQ1b2ZJIZjCcMqWzWSsyD AW8MbTIKBDCPE5/YZMvkbb6p2sPOg6PlZ0XH67MwUFM6AGwV+u4drDg/pcb9B0Me ijn3MFw7BYF7ufZ+D0JLibyhv4Sr/2trE7ihS1Vmb1AOC/PjYroRSRyDIit4DQf9 Epe4KlYGsCsyL8JAikYBGqVyR0QHI7cV+dRHcnVOjs+eV+Eog2B3ogjTwI9fPUe1 AKrUAd0W2s2Isb4NOW2a+18J9KYArF0aWx0fPc8zsV3kfefB524qYPZMUjd/786e UzYn41dHe16s/NCNlzLyQePBjCBDYaPdEvYFTgYzN5Ildp2POSCH77YxJCcRqZ+o OuQ9mEpKnX6amfOedXJ5xt0maz2TEnhuQWvsvtYxfFzv3ACWWM50GCdJombdKOTg URxa3mCU9fGnhLBpVNjP2wdVuoJuDpQHyX4fBjU28Dh/P4aNQtBaK3os7yeopa/9 oGjxKZR1Tt6F0ZY6VyxIV4FMwsG+BBgBCgByBYJjMYi/CRAAQx/1uKaCzUcUAAAA AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmerKK61I4H6667lpYEg ZqeX3C0xL6Hsa4nbzaczYdfN8gKbIBYhBM1sR5nmfMSlUbfy9ABDH/W4poLNAABx 0g/+PZoF8OKfUvdsTH8pCdmvayHK00bboFh4B+8fe56r5uDN8qbpXg2xAXa1PBHm AsXh3SyvgA2VORTfUCUg5Sj/WY1dPSmE0qcSaz4/PtUuLUvx8l+5s+FQVW5jt1vq lfqwxM5zOKqmV4P5DwIyTQdLLHolwMe8zjxKOYJe3jqhSHj6UlYLlygNfsLJ/HV6 djA5I666GSDfoArjHwdZAQeMC2nQSmjAhka+9TfFrbZVwoU7pP95eJQ2hB4SQQLK Yu9uZrDW+YIqdCOlznyRH237PjB0rQdGA6Fy6T16ZU4rh9N+dFhsT3HfvL3co+W+ Cuei7qIZmIJAjOxbOjmOp0g8y7kno+5EVS2A/0FqRBUkx+Z/8e4jv9eTiPvjQBwW 5BgmHzTfjrPTmdWZ/Ka2mpvwVMFGB8TDg4MMHeOqcWQHL0OmbYGW13HPA/wIli+k Y2XOwXV0LyapxRJ+/TxrZwm3BE8TF0yWMfkzU1Vqs8Y80X6m3ok3XOipOOoR3zlp stynHfhaY5cCf8vXIO+tm8mqOEdUNz897LDPxLkKXw1HqkYGGeR2uIvD0qGQEhBS tk0ezsx3tqfV4yi24OwVxcQFEhY/fB0sUmt13zDEmgIHTtvzEX0/FqAVVcYevsEp wQpO+8t0BhXbthutxQovw2n08AMv4EyVOq5b5mwW0dH9wy8= =76yN -----END PGP PRIVATE KEY BLOCK-----