Compare commits

..

1 Commits

Author SHA1 Message Date
Ryan Heywood 09e6e6de71
keyfork-prompt: add choice mechanism 2024-08-01 18:25:33 -04:00
88 changed files with 1719 additions and 3918 deletions

1
.gitattributes vendored
View File

@ -1 +0,0 @@
audits filter=lfs diff=lfs merge=lfs -text

View File

@ -1,281 +1,3 @@
# Keyfork v0.2.6
* The `--daemon` flag has been added for `keyfork recover` subcommands.
* `keyfork mnemonic generate` now has a bunch more options, to improve the out-of-the-box experience.
* `keyfork shard metadata` can be used to get the threshold and OpenPGP certificates.
* `keyfork derive openpgp` now correctly provides private keys, instead of public keys.
### Changes in keyfork:
```
4e342ac keyfork: add `--daemon`
c232828 superpower `keyfork mnemonic generate`
8756c3d keyfork wizard generate-shard-secret: allow exporting certificates and cross-sign generated keys
c95ed0b keyfork shard metadata: initial commit
adb5293 keyfork derive openpgp: export secret keys instead of public certs
```
### Changes in keyfork-derive-openpgp:
```
adb5293 keyfork derive openpgp: export secret keys instead of public certs
```
### Changes in keyfork-prompt:
```
35e0eb5 keyfork-prompt: use raw mode for input
```
### Changes in keyfork-shard:
```
c95ed0b keyfork shard metadata: initial commit
```
### Changes in keyfork-tests:
```
19fbb51 keyfork-tests: initial commit. also, fixup test_util's Panicable to not be generic. it's always unit type
```
### Changes in keyforkd:
```
19fbb51 keyfork-tests: initial commit. also, fixup test_util's Panicable to not be generic. it's always unit type
```
# Keyfork v0.2.5
### Changes in keyfork:
```
503c6fa keyfork derive key: initial commit
c46f9e4 move things to use default handler mechanism
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
```
### Changes in keyfork-crossterm:
```
6317cc9 Cargo.lock: bump deps, dupe generic-array :(
a8b2814 make clippy happy
```
### Changes in keyfork-derive-key:
```
a8b2814 make clippy happy
```
### Changes in keyfork-derive-openpgp:
```
4ab1e8a add docs to make clippy extra happy
a8b2814 make clippy happy
```
### Changes in keyfork-derive-path-data:
```
4ab1e8a add docs to make clippy extra happy
```
### Changes in keyfork-derive-util:
```
a8b2814 make clippy happy
```
### Changes in keyfork-entropy:
```
a8b2814 make clippy happy
```
### Changes in keyfork-mnemonic:
```
a8b2814 make clippy happy
```
### Changes in keyfork-prompt:
```
f8db870 keyfork-prompt: add Headless
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
a8b2814 make clippy happy
```
### Changes in keyfork-qrcode:
```
be6d562 keyfork-qrcode: use image::ImageReader over image::io::Reader (deprecated)
305e070 Cargo.lock: bump multiple deps to deduplicate
4ab1e8a add docs to make clippy extra happy
a8b2814 make clippy happy
```
### Changes in keyfork-shard:
```
c46f9e4 move things to use default handler mechanism
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
d7bf3d1 keyfork-shard: move to blahaj
a8b2814 make clippy happy
c36fe0a keyfork-shard: re-enable standard policy, alive check still disabled, add check for encryption keys when discovering certs
```
### Changes in keyfork-zbar:
```
a8b2814 make clippy happy
```
### Changes in keyforkd:
```
c46f9e4 move things to use default handler mechanism
a8b2814 make clippy happy
```
### Changes in keyforkd-client:
```
a8b2814 make clippy happy
```
# Keyfork v0.2.4
This release includes a lot of "maintenance" changes, without any changes in
end-user functionality.
### Changes in keyfork:
The most significant change in this release is the reorganization of some of
the subcommands, where they would be better as enum-traits, such as `keyfork
derive` and `keyfork wizard`.
```
b254ba7 cleanup post-merge
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
5219c5a keyfork: enum-trait-ify choose-your-own commands
b26f296 keyfork-derive-path-data: move all pathcrafting here
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
```
### Changes in keyfork-derive-openpgp:
```
b254ba7 cleanup post-merge
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
b26f296 keyfork-derive-path-data: move all pathcrafting here
```
### Changes in keyfork-derive-path-data:
This change now centralizes all special Keyfork paths. This means crates should
no longer be required to implement their own path parsing logic.
```
b26f296 keyfork-derive-path-data: move all pathcrafting here
```
### Changes in keyfork-derive-util:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
```
### Changes in keyfork-mnemonic:
`keyfork-mnemonic-util` has finally been renamed to `keyfork-mnemonic`. The
method names `as_bytes() => as_slice()`, `to_bytes() => to_vec()`, and
`into_bytes() => into_vec()`, and the function names
`from_bytes() => try_from_slice()` and
`from_nonstandard_bytes() => from_array()`, have been implemented to more
closely represent the native types they are representing. Additionally,
`Mnemonic::generate_seed()` has been modified to return a constant size array;
this is a breaking change, but should have minimal impact.
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
3ee81b6 keyfork-mnemonic-util: impl as_slice to_vec into_vec
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
```
### Changes in keyfork-prompt:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
```
### Changes in keyfork-shard:
```
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
```
### Changes in keyforkd:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
536e6da keyforkd{,-client}: lots of documentationings
```
### Changes in keyforkd-client:
```
536e6da keyforkd{,-client}: lots of documentationings
```
# Keyfork v0.2.3
This release includes a bugfix for the wizard where the wizard was too strict
about when keys were "alive".
### Changes in keyfork:
```
dd4354f keyfork: bump keyfork-shard
```
### Changes in keyfork-shard:
```
ba64db8 update Cargo.toml and Cargo.lock
fa84a2a keyfork-shard: Be less strict about keys
```
# Keyfork v0.2.2
This release adds a new wizard, intended to be used at DEFCON 32.
### Changes in keyfork:
```
8d40d26 keyfork: add `bottoms-up` wizard
```
### Changes in keyfork-derive-openpgp:
This change also includes a minor change, allowing the derivation path for
`keyfork-derive-openpg` to derive further than two paths, which was useful in
the testing of the wizard.
```
8d40d26 keyfork: add `bottoms-up` wizard
```
# Keyfork v0.2.1
This release contains an emergency bugfix for Keyfork Shard, which previously

1836
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,67 +19,12 @@ members = [
"crates/util/keyfork-crossterm",
"crates/util/keyfork-entropy",
"crates/util/keyfork-frame",
"crates/util/keyfork-mnemonic",
"crates/util/keyfork-mnemonic-util",
"crates/util/keyfork-prompt",
"crates/util/keyfork-slip10-test-data",
"crates/util/smex",
"crates/tests",
]
[workspace.dependencies]
# Keyfork dependencies
keyforkd = { version = "0.1.1", path = "crates/daemon/keyforkd", registry = "distrust", default-features = false }
keyforkd-client = { version = "0.2.0", path = "crates/daemon/keyforkd-client", registry = "distrust", default-features = false }
keyforkd-models = { version = "0.2.0", path = "crates/daemon/keyforkd-models", registry = "distrust", default-features = false }
keyfork-derive-openpgp = { version = "0.1.2", path = "crates/derive/keyfork-derive-openpgp", registry = "distrust", default-features = false }
keyfork-derive-path-data = { version = "0.1.1", path = "crates/derive/keyfork-derive-path-data", registry = "distrust", default-features = false }
keyfork-derive-util = { version = "0.2.0", path = "crates/derive/keyfork-derive-util", registry = "distrust", default-features = false }
keyfork-shard = { version = "0.3.0", path = "crates/keyfork-shard", registry = "distrust", default-features = false }
keyfork-qrcode = { version = "0.1.1", path = "crates/qrcode/keyfork-qrcode", registry = "distrust", default-features = false }
keyfork-zbar = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar", registry = "distrust", default-features = false }
keyfork-zbar-sys = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar-sys", registry = "distrust", default-features = false }
keyfork-bin = { version = "0.1.0", path = "crates/util/keyfork-bin", registry = "distrust", default-features = false }
keyfork-bug = { version = "0.1.0", path = "crates/util/keyfork-bug", registry = "distrust", default-features = false }
keyfork-crossterm = { version = "0.27.1", path = "crates/util/keyfork-crossterm", registry = "distrust", default-features = false }
keyfork-entropy = { version = "0.1.1", path = "crates/util/keyfork-entropy", registry = "distrust", default-features = false }
keyfork-frame = { version = "0.1.0", path = "crates/util/keyfork-frame", registry = "distrust", default-features = false }
keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false }
keyfork-prompt = { version = "0.2.0", path = "crates/util/keyfork-prompt", registry = "distrust", default-features = false }
keyfork-slip10-test-data = { version = "0.1.0", path = "crates/util/keyfork-slip10-test-data", registry = "distrust", default-features = false }
smex = { version = "0.1.0", path = "crates/util/smex", registry = "distrust", default-features = false }
# External dependencies
# Cryptography
ed25519-dalek = "2.1.1"
hmac = "0.12.1"
k256 = { version = "0.13.3", default-features = false, features = ["std"] }
sha2 = "0.10.8"
# OpenPGP
card-backend-pcsc = "0.5.0"
openpgp-card = { version = "0.4.1" }
openpgp-card-sequoia = { version = "0.2.0", default-features = false }
sequoia-openpgp = { version = "1.21.2", default-features = false, features = ["compression"] }
# Serialization
bincode = "1.3.3"
serde = { version= "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
# Misc.
anyhow = "1.0.79"
hex-literal = "0.4.1"
image = { version = "0.25.2", default-features = false }
thiserror = "1.0.56"
tokio = "1.35.1"
v4l = "0.14.0"
base64 = "0.22.1"
[profile.release]
debug = true
[profile.dev.package.keyfork-qrcode]
opt-level = 3
debug = true

View File

@ -1,11 +0,0 @@
# Releasing new versions
* Add and review a new blurb to the changelog by running the
`make-changelog-blurb.sh` script and appending the result to the top of
the file.
* Make sure to add some human-readable snippets at the top!
* Update all versions of crates listed in the changelog.
* Commit changes.
* Run the `sign-new-versions.sh` script to tag the new versions.
* Run the `publish.sh` script to push the latest packages to the Distrust
Cargo registry.

Binary file not shown.

View File

@ -1,92 +0,0 @@
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--color", "always"]
need_stdout = false
[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--color", "always"]
need_stdout = false
[jobs.clippy]
command = [
"cargo", "clippy",
"--all-targets",
"--color", "always",
]
need_stdout = false
[jobs.clippy-unwrap]
command = [
"cargo", "clippy",
"--lib",
"--color", "always",
"--",
"-W",
"clippy::unwrap_used",
"-W",
"clippy::expect_used",
]
need_stdout = false
# This job lets you run
# - all tests: bacon test
# - a specific test: bacon test -- config::test_default_files
# - the tests of a package: bacon test -- -- -p config
[jobs.test]
command = [
"cargo", "test", "--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"]
need_stdout = false
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change
# You can run your application and have the result displayed in bacon,
# *if* it makes sense for this crate.
# Don't forget the `--color always` part or the errors won't be
# properly parsed.
# If your program never stops (eg a server), you may set `background`
# to false to have the cargo run output immediately displayed instead
# of waiting for program's end.
[jobs.run]
command = [
"cargo", "run",
"--color", "always",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = true
# This parameterized job runs the example of your choice, as soon
# as the code compiles.
# Call it as
# bacon ex -- my-example
[jobs.ex]
command = ["cargo", "run", "--color", "always", "--example"]
need_stdout = true
allow_warnings = true
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"

View File

@ -1,6 +1,6 @@
[package]
name = "keyforkd-client"
version = "0.2.2"
version = "0.2.0"
edition = "2021"
license = "MIT"
@ -12,14 +12,14 @@ ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"]
secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false }
keyfork-frame = { workspace = true }
keyforkd-models = { workspace = true }
bincode = { workspace = true }
thiserror = { workspace = true }
k256 = { workspace = true, default-features = false, features = ["std"], optional = true }
ed25519-dalek = { workspace = true, optional = true }
keyfork-derive-util = { version = "0.2.0", path = "../../derive/keyfork-derive-util", default-features = false, registry = "distrust" }
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame", registry = "distrust" }
keyforkd-models = { version = "0.2.0", path = "../keyforkd-models", registry = "distrust" }
bincode = "1.3.3"
thiserror = "1.0.49"
k256 = { version = "0.13.3", optional = true }
ed25519-dalek = { version = "2.1.1", optional = true }
[dev-dependencies]
keyfork-slip10-test-data = { workspace = true }
keyforkd = { workspace = true }
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data", registry = "distrust" }
keyforkd = { path = "../keyforkd", registry = "distrust" }

View File

@ -11,7 +11,7 @@ fn secp256k1_test_suite() {
let tests = test_data()
.unwrap()
.remove("secp256k1")
.remove(&"secp256k1".to_string())
.unwrap();
for seed_test in tests {
@ -70,7 +70,7 @@ fn secp256k1_test_suite() {
fn ed25519_test_suite() {
use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove("ed25519").unwrap();
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
for seed_test in tests {
let seed = seed_test.seed;

View File

@ -7,6 +7,6 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false }
serde = { workspace = true }
thiserror = { workspace = true }
keyfork-derive-util = { version = "0.2.0", path = "../../derive/keyfork-derive-util", default-features = false, registry = "distrust" }
serde = { version = "1.0.190", features = ["derive"] }
thiserror = "1.0.50"

View File

@ -1,6 +1,6 @@
[package]
name = "keyforkd"
version = "0.1.4"
version = "0.1.1"
edition = "2021"
license = "AGPL-3.0-only"
@ -12,28 +12,28 @@ tracing = ["tower/tracing", "tokio/tracing", "dep:tracing", "dep:tracing-subscri
multithread = ["tokio/rt-multi-thread"]
[dependencies]
keyfork-bug = { workspace = true }
keyfork-derive-util = { workspace = true }
keyfork-frame = { workspace = true, features = ["async"] }
keyfork-mnemonic = { workspace = true }
keyfork-derive-path-data = { workspace = true }
keyforkd-models = { workspace = true }
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
keyfork-derive-util = { version = "0.2.0", path = "../../derive/keyfork-derive-util", registry = "distrust" }
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame", features = ["async"], registry = "distrust" }
keyfork-mnemonic-util = { version = "0.3.0", path = "../../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-derive-path-data = { version = "0.1.0", path = "../../derive/keyfork-derive-path-data", registry = "distrust" }
keyforkd-models = { version = "0.2.0", path = "../keyforkd-models", registry = "distrust" }
# Not personally audited
bincode = { workspace = true }
bincode = "1.3.3"
# Ecosystem trust, not personally audited
tokio = { workspace = true, features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
tokio = { version = "1.32.0", features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
tracing = { version = "0.1.37", optional = true }
tracing-error = { version = "0.2.0", optional = true }
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
tower = { version = "0.4.13", features = ["tokio", "util"] }
# Personally audited
thiserror = { workspace = true }
serde = { workspace = true }
thiserror = "1.0.47"
serde = { version = "1.0.186", features = ["derive"] }
tempfile = { version = "3.10.0", default-features = false }
[dev-dependencies]
hex-literal = { workspace = true }
keyfork-slip10-test-data = { workspace = true }
hex-literal = "0.4.1"
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data", registry = "distrust" }

View File

@ -5,7 +5,7 @@ use std::{
path::{Path, PathBuf},
};
pub use keyfork_mnemonic::Mnemonic;
pub use keyfork_mnemonic_util::Mnemonic;
pub use tower::ServiceBuilder;
#[cfg(feature = "tracing")]
@ -57,7 +57,7 @@ pub async fn start_and_run_server_on(
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
// TODO: passphrase support and/or store passphrase with mnemonic
.service(Keyforkd::new(mnemonic.generate_seed(None).to_vec()));
.service(Keyforkd::new(mnemonic.generate_seed(None)));
let mut server = match UnixServer::bind(socket_path) {
Ok(s) => s,

View File

@ -1,6 +1,6 @@
//! Launch the Keyfork Server from using a mnemonic passed through standard input.
//!
use keyfork_mnemonic::Mnemonic;
use keyfork_mnemonic_util::Mnemonic;
use tokio::io::{self, AsyncBufReadExt, BufReader};

View File

@ -11,7 +11,7 @@ pub struct BincodeLayer<'a, Request> {
phantom_request: PhantomData<&'a Request>,
}
impl<Request> BincodeLayer<'_, Request> {
impl<'a, Request> BincodeLayer<'a, Request> {
/// Create a new [`BincodeLayer`].
pub fn new() -> Self {
Self {
@ -21,7 +21,7 @@ impl<Request> BincodeLayer<'_, Request> {
}
}
impl<Request> Default for BincodeLayer<'_, Request> {
impl<'a, Request> Default for BincodeLayer<'a, Request> {
fn default() -> Self {
Self::new()
}

View File

@ -113,7 +113,7 @@ mod tests {
async fn properly_derives_secp256k1() {
let tests = test_data()
.unwrap()
.remove("secp256k1")
.remove(&"secp256k1".to_string())
.unwrap();
for per_seed in tests {
@ -146,7 +146,7 @@ mod tests {
#[tokio::test]
async fn properly_derives_ed25519() {
let tests = test_data().unwrap().remove("ed25519").unwrap();
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
for per_seed in tests {
let seed = &per_seed.seed;

View File

@ -26,7 +26,7 @@ pub enum UninstantiableError {}
/// };
/// assert!(closure().is_ok());
/// ```
pub type Panicable = std::result::Result<(), UninstantiableError>;
pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
@ -62,9 +62,9 @@ pub type Panicable = std::result::Result<(), UninstantiableError>;
/// }).unwrap();
/// ```
#[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> std::result::Result<(), E>
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where
F: FnOnce(&std::path::Path) -> std::result::Result<(), E> + Send + 'static,
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
E: Send + 'static,
{
let rt = Builder::new_multi_thread()

View File

@ -1,16 +0,0 @@
[package]
name = "keyfork-derive-age"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
keyforkd-client = { workspace = true }
smex = { workspace = true }
thiserror = "1.0.48"
bech32 = "0.11.0"
keyfork-derive-path-data = { workspace = true }
ed25519-dalek = "2.1.1"

View File

@ -1,69 +0,0 @@
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_path_data::paths;
use keyfork_derive_util::{DerivationPath, ExtendedPrivateKey, PathError};
use keyforkd_client::Client;
use ed25519_dalek::SigningKey;
type XPrv = ExtendedPrivateKey<SigningKey>;
/// Any error that can occur while deriving a key.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The given path could not be parsed.
#[error("Could not parse the given path: {0}")]
PathFormat(#[from] PathError),
/// The request to derive data failed.
#[error("Unable to perform key derivation request: {0}")]
KeyforkdClient(#[from] keyforkd_client::Error),
}
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
fn validate(path: &str) -> Result<DerivationPath> {
let index = paths::AGE.inner().first().unwrap();
let path = DerivationPath::from_str(path)?;
assert!(
path.len() >= 2,
"Expected path of at least m/{index}/account_id'"
);
let given_index = path.iter().next().expect("checked .len() above");
assert_eq!(
index, given_index,
"Expected derivation path starting with m/{index}, got: {given_index}",
);
Ok(path)
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let path = match args.as_slice() {
[path] => validate(path)?,
_ => panic!("Usage: {program_name} path"),
};
let mut client = Client::discover_socket()?;
// TODO: should this key be clamped to Curve25519 specs?
let xprv: XPrv = client.request_xprv(&path)?;
let hrp = bech32::Hrp::parse("AGE-SECRET-KEY-")?;
let age_key = bech32::encode::<bech32::Bech32>(hrp, &xprv.private_key().to_bytes())?;
println!("{}", age_key.to_uppercase());
Ok(())
}
fn main() -> ExitCode {
if let Err(e) = run() {
eprintln!("Error: {e}");
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}

View File

@ -1,13 +1,13 @@
[package]
name = "keyfork-derive-key"
version = "0.1.2"
version = "0.1.1"
edition = "2021"
license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { workspace = true }
keyforkd-client = { workspace = true }
smex = { workspace = true }
thiserror = { workspace = true }
keyfork-derive-util = { version = "0.2.0", path = "../keyfork-derive-util", registry = "distrust" }
keyforkd-client = { version = "0.2.0", path = "../../daemon/keyforkd-client", registry = "distrust" }
smex = { version = "0.1.0", path = "../../util/smex", registry = "distrust" }
thiserror = "1.0.48"

View File

@ -1,4 +1,4 @@
//! Query the Keyfork Server to generate a hex-encoded key for a given algorithm.
//!
use std::{env, process::ExitCode, str::FromStr};

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-derive-openpgp"
version = "0.1.5"
version = "0.1.2"
edition = "2021"
license = "AGPL-3.0-only"
@ -10,10 +10,9 @@ default = ["bin"]
bin = ["sequoia-openpgp/crypto-nettle"]
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
ed25519-dalek = { workspace = true }
sequoia-openpgp = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
keyfork-derive-path-data = { workspace = true }
keyfork-derive-util = { version = "0.2.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"], registry = "distrust" }
keyforkd-client = { version = "0.2.0", path = "../../daemon/keyforkd-client", default-features = false, features = ["ed25519"], registry = "distrust" }
ed25519-dalek = "2.0.0"
sequoia-openpgp = { version = "1.17.0", default-features = false }
anyhow = "1.0.75"
thiserror = "1.0.49"

View File

@ -19,14 +19,8 @@ use sequoia_openpgp::{
Cert, Packet,
};
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
// something like Prv or PrvKey.
/// The private key type used with OpenPGP.
pub type XPrvKey = SigningKey;
/// The extended private key type used with OpenPGP.
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
pub type XPrv = ExtendedPrivateKey<SigningKey>;
/// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)]

View File

@ -1,9 +1,8 @@
//! Query the Keyfork Servre to derive an OpenPGP Secret Key.
//!
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::DerivationPath;
use keyfork_derive_path_data::paths;
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyforkd_client::Client;
use ed25519_dalek::SigningKey;
@ -79,14 +78,16 @@ fn validate(
subkey_format: &str,
default_userid: &str,
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
let index = paths::OPENPGP.inner().first().unwrap();
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let path = DerivationPath::from_str(path)?;
assert!(path.len() >= 2, "Expected path of at least m/{index}/account_id'");
let given_index = path.iter().next().expect("checked .len() above");
assert_eq!(
index, given_index,
&index, given_index,
"Expected derivation path starting with m/{index}, got: {given_index}",
);
@ -121,7 +122,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
for packet in cert.as_tsk().into_packets() {
for packet in cert.into_packets() {
packet.serialize(&mut w)?;
}

View File

@ -1,11 +1,10 @@
[package]
name = "keyfork-derive-path-data"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false }
once_cell = "1.19.0"
keyfork-derive-util = { version = "0.2.0", path = "../keyfork-derive-util", default-features = false, registry = "distrust" }

View File

@ -2,129 +2,32 @@
#![allow(clippy::unreadable_literal)]
use once_cell::sync::Lazy;
use keyfork_derive_util::{DerivationIndex, DerivationPath};
/// All common paths for key derivation.
pub mod paths {
use super::*;
/// The default derivation path for OpenPGP.
pub static OPENPGP: Lazy<DerivationPath> = Lazy::new(|| {
DerivationPath::default().chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),
true,
))
});
/// The derivation path for OpenPGP certificates used for sharding.
pub static OPENPGP_SHARD: Lazy<DerivationPath> = Lazy::new(|| {
DerivationPath::default()
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),
true,
))
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"shrd"),
true,
))
});
/// The derivation path for OpenPGP certificates used for disaster recovery.
pub static OPENPGP_DISASTER_RECOVERY: Lazy<DerivationPath> = Lazy::new(|| {
DerivationPath::default()
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),
true,
))
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00\x00dr"),
true,
))
});
}
/// Determine if a prefix matches and whether the next index exists.
fn prefix_matches(given: &DerivationPath, target: &DerivationPath) -> Option<DerivationIndex> {
if given.len() <= target.len() {
return None;
}
if target
.iter()
.zip(given.iter())
.all(|(left, right)| left == right)
{
given.iter().nth(target.len()).cloned()
} else {
None
}
}
/// The default derivation path for OpenPGP.
pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true);
/// A derivation target.
#[derive(Debug)]
#[non_exhaustive]
pub enum Target {
/// An OpenPGP key, whose account is the given index.
OpenPGP(DerivationIndex),
/// An OpenPGP key used for sharding.
OpenPGPShard(DerivationIndex),
/// An OpenPGP key used for disaster recovery.
OpenPGPDisasterRecovery(DerivationIndex),
}
impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Target::OpenPGP(account) => {
Self::OpenPGP(account) => {
write!(f, "OpenPGP key (account {account})")
}
Target::OpenPGPShard(shard_index) => {
write!(f, "OpenPGP Shard key (shard index {shard_index})")
}
Target::OpenPGPDisasterRecovery(account) => {
write!(f, "OpenPGP Disaster Recovery key (account {account})")
}
}
}
}
macro_rules! test_match {
($var:ident, $shard:path, $target:path) => {
if let Some(index) = prefix_matches($var, &$shard) {
return Some($target(index));
}
};
}
/// Determine the closest [`Target`] for the given path. This method is intended to be used by
/// `keyforkd` to provide an optional textual prompt to what a client is attempting to derive.
pub fn guess_target(path: &DerivationPath) -> Option<Target> {
test_match!(path, paths::OPENPGP_SHARD, Target::OpenPGPShard);
test_match!(
path,
paths::OPENPGP_DISASTER_RECOVERY,
Target::OpenPGPDisasterRecovery
);
test_match!(path, paths::OPENPGP, Target::OpenPGP);
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let index = DerivationIndex::new(5312, false).unwrap();
let dr_key = paths::OPENPGP_DISASTER_RECOVERY
.clone()
.chain_push(index.clone());
match guess_target(&dr_key) {
Some(Target::OpenPGPDisasterRecovery(idx)) if idx == index => (),
bad => panic!("invalid value: {bad:?}"),
}
}
Some(match path.iter().collect::<Vec<_>>()[..] {
[t, index] if t == &OPENPGP => Target::OpenPGP(index.clone()),
_ => return None,
})
}

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-derive-util"
version = "0.2.2"
version = "0.2.0"
edition = "2021"
license = "MIT"
@ -12,25 +12,25 @@ secp256k1 = ["k256"]
ed25519 = ["ed25519-dalek"]
[dependencies]
keyfork-mnemonic = { workspace = true }
keyfork-bug = { workspace = true }
keyfork-mnemonic-util = { version = "0.3.0", path = "../../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
# Included in Rust
digest = "0.10.7"
sha2 = { workspace = true }
sha2 = "0.10.7"
# Rust-Crypto ecosystem, not personally audited
ripemd = "0.1.3"
hmac = { workspace = true, features = ["std"] }
hmac = { version = "0.12.1", features = ["std"] }
# Personally audited
serde = { workspace = true }
thiserror = { workspace = true }
serde = { version = "1.0.186", features = ["derive"] }
thiserror = "1.0.47"
# Optional, not personally audited
k256 = { workspace = true, default-features = false, features = ["std", "arithmetic"], optional = true }
ed25519-dalek = { workspace = true, optional = true }
k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true }
ed25519-dalek = { version = "2.0.0", optional = true }
[dev-dependencies]
hex-literal = { workspace = true }
keyfork-slip10-test-data = { workspace = true }
hex-literal = "0.4.1"
keyfork-slip10-test-data = { version = "0.1.0", path = "../../util/keyfork-slip10-test-data", registry = "distrust" }

View File

@ -23,7 +23,7 @@ performed directly on a master seed. This is how Keyforkd works internally.
```rust
use std::str::FromStr;
use keyfork_mnemonic::Mnemonic;
use keyfork_mnemonic_util::Mnemonic;
use keyfork_derive_util::{*, request::*};
fn main() -> Result<(), Box<dyn std::error::Error>> {

View File

@ -11,7 +11,7 @@
//! # Examples
//! ```rust
//! use std::str::FromStr;
//! use keyfork_mnemonic::Mnemonic;
//! use keyfork_mnemonic_util::Mnemonic;
//! use keyfork_derive_util::{*, request::*};
//! use k256::SecretKey;
//!
@ -41,9 +41,9 @@
//! }
//! ```
#[allow(missing_docs)]
///
pub mod private_key;
#[allow(missing_docs)]
///
pub mod public_key;
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};

View File

@ -24,7 +24,7 @@ use crate::{
DerivationPath, ExtendedPrivateKey,
};
use keyfork_mnemonic::{Mnemonic, MnemonicGenerationError};
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
use serde::{Deserialize, Serialize};
/// An error encountered while deriving a key.
@ -194,8 +194,8 @@ impl DerivationRequest {
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mnemonic: keyfork_mnemonic::Mnemonic = //
/// # keyfork_mnemonic::Mnemonic::from_entropy(
/// let mnemonic: keyfork_mnemonic_util::Mnemonic = //
/// # keyfork_mnemonic_util::Mnemonic::from_entropy(
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
/// # )?;
/// let algo: DerivationAlgorithm = //

View File

@ -15,7 +15,7 @@ fn secp256k1() {
let tests = test_data()
.unwrap()
.remove("secp256k1")
.remove(&"secp256k1".to_string())
.unwrap();
for per_seed in tests {
@ -62,7 +62,7 @@ fn secp256k1() {
fn ed25519() {
use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove("ed25519").unwrap();
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
for per_seed in tests {
let seed = &per_seed.seed;

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-shard"
version = "0.3.1"
version = "0.2.1"
edition = "2021"
license = "AGPL-3.0-only"
@ -14,26 +14,27 @@ openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "de
qrcode = ["keyfork-qrcode"]
[dependencies]
keyfork-bug = { workspace = true }
keyfork-prompt = { workspace = true, default-features = false, features = ["mnemonic"] }
keyfork-qrcode = { workspace = true, optional = true, default-features = false }
smex = { workspace = true }
thiserror = { workspace = true }
keyfork-bug = { version = "0.1.0", path = "../util/keyfork-bug", registry = "distrust" }
keyfork-prompt = { version = "0.1.1", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"], registry = "distrust" }
keyfork-qrcode = { version = "0.1.1", path = "../qrcode/keyfork-qrcode", optional = true, default-features = false, registry = "distrust" }
smex = { version = "0.1.0", path = "../util/smex", registry = "distrust" }
sharks = "0.5.0"
thiserror = "1.0.50"
# Remote operator mode
keyfork-mnemonic = { workspace = true }
keyfork-mnemonic-util = { version = "0.3.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
aes-gcm = { version = "0.10.3", features = ["std"] }
hkdf = { version = "0.12.4", features = ["std"] }
sha2 = { workspace = true }
sha2 = "0.10.8"
# OpenPGP
keyfork-derive-openpgp = { workspace = true, default-features = false }
anyhow = { workspace = true, optional = true }
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp", default-features = false, registry = "distrust" }
anyhow = { version = "1.0.79", optional = true }
card-backend = { version = "0.2.0", optional = true }
card-backend-pcsc = { workspace = true, optional = true }
openpgp-card-sequoia = { workspace = true, optional = true }
openpgp-card = { workspace = true, optional = true }
sequoia-openpgp = { workspace = true, optional = true }
base64 = { workspace = true }
blahaj = "0.6.0"
card-backend-pcsc = { version = "0.5.0", optional = true }
openpgp-card-sequoia = { version = "0.2.0", optional = true, default-features = false }
openpgp-card = { version = "0.4.0", optional = true }
sequoia-openpgp = { version = "1.17.0", optional = true, default-features = false }
base64 = "0.22.0"

View File

@ -1,4 +1,4 @@
//! Combine OpenPGP shards and output the hex-encoded secret.
//!
use std::{
env,
@ -7,7 +7,7 @@ use std::{
process::ExitCode,
};
use keyfork_prompt::default_handler;
use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -32,8 +32,8 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let openpgp = OpenPGP;
let prompt_handler = default_handler()?;
let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?;
print!("{}", smex::encode(bytes));

View File

@ -1,4 +1,4 @@
//! Decrypt a single OpenPGP shard and encapsulate it for remote transport.
//!
use std::{
env,
@ -7,7 +7,7 @@ use std::{
process::ExitCode,
};
use keyfork_prompt::default_handler;
use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{Format, openpgp::OpenPGP};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -32,8 +32,8 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let openpgp = OpenPGP;
let prompt_handler = default_handler()?;
let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?;

View File

@ -1,4 +1,4 @@
//! Combine OpenPGP shards using remote transport and output the hex-encoded secret.
//!
use std::{
env,

View File

@ -1,7 +1,8 @@
//! Split a hex-encoded secret into OpenPGP shards
//!
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_prompt::terminal::DefaultTerminal;
use keyfork_shard::{Format, openpgp::OpenPGP};
#[derive(Clone, Debug)]
@ -50,7 +51,7 @@ fn run() -> Result<()> {
smex::decode(line?)?
};
let openpgp = OpenPGP;
let openpgp = OpenPGP::<DefaultTerminal>::new();
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
Ok(())

View File

@ -3,8 +3,7 @@
use std::{
io::{stdin, stdout, Read, Write},
rc::Rc,
sync::Mutex,
sync::{Arc, Mutex},
};
use aes_gcm::{
@ -12,12 +11,10 @@ use aes_gcm::{
Aes256Gcm, KeyInit, Nonce,
};
use base64::prelude::{Engine, BASE64_STANDARD};
use blahaj::{Share, Sharks};
use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{
prompt_validated_wordlist,
validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator,
@ -25,6 +22,7 @@ use keyfork_prompt::{
Message as PromptMessage, PromptHandler, Terminal,
};
use sha2::Sha256;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
const PLAINTEXT_LENGTH: u8 = 32 // shard
@ -137,10 +135,10 @@ pub trait Format {
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a readable input. For the current
/// Decrypt a single share and associated metadata from a reaable input. For the current
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
/// combine secrets.
///
@ -151,43 +149,9 @@ pub trait Format {
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Share, u8), Self::Error>;
/// Decrypt the public keys and metadata from encrypted data.
///
/// # Errors
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
/// could neither be encrypted nor parsed.
fn decrypt_metadata(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error>;
/// Decrypt the public keys and metadata from a Shardfile.
///
/// # Errors
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
/// could neither be encrypted nor parsed.
fn decrypt_metadata_from_file(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>,
) -> Result<(u8, Vec<Self::PublicKey>), Self::Error> {
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
self.decrypt_metadata(
private_keys,
&encrypted_messages,
Rc::new(Mutex::new(prompt)),
)
}
/// Decrypt multiple shares and combine them to recreate a secret.
///
/// # Errors
@ -197,7 +161,7 @@ pub trait Format {
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>,
prompt: impl PromptHandler,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
@ -206,7 +170,7 @@ pub trait Format {
let (shares, threshold) = self.decrypt_all_shards(
private_keys,
&encrypted_messages,
Rc::new(Mutex::new(prompt)),
Arc::new(Mutex::new(prompt)),
)?;
let secret = Sharks(threshold)
@ -227,9 +191,9 @@ pub trait Format {
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>,
prompt: impl PromptHandler,
) -> Result<(), Box<dyn std::error::Error>> {
let prompt = Rc::new(Mutex::new(prompt));
let prompt = Arc::new(Mutex::new(prompt));
// parse input
let private_keys = private_key_discovery
@ -269,17 +233,6 @@ pub trait Format {
let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)),
};
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
prompt_validated_wordlist::<English, _>(
&mut **prompt,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
/*
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
@ -291,13 +244,12 @@ pub trait Format {
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
*/
}
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
assert!(
shared_secret.was_contributory(),
@ -350,7 +302,7 @@ pub trait Format {
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes);
let payload_mnemonic = Mnemonic::from_nonstandard_bytes(mnemonic_bytes);
#[cfg(feature = "qrcode")]
{
@ -487,7 +439,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1;
let our_key = EphemeralSecret::random();
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
#[cfg(feature = "qrcode")]
{
@ -549,12 +501,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
word_lengths: [24, 39],
};
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
&mut pm,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?;
let [pubkey_mnemonic, payload_mnemonic] = pm
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()

View File

@ -5,10 +5,10 @@
use std::{
collections::HashMap,
io::{Read, Write},
marker::PhantomData,
path::Path,
str::FromStr,
sync::Mutex,
rc::Rc,
sync::{Arc, Mutex},
};
use keyfork_bug::bug;
@ -25,7 +25,7 @@ use openpgp::{
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
Parse,
},
policy::{NullPolicy, StandardPolicy, Policy},
policy::{NullPolicy, Policy, StandardPolicy},
serialize::{
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal,
@ -34,7 +34,7 @@ use openpgp::{
KeyID, PacketPile,
};
pub use sequoia_openpgp as openpgp;
use blahaj::Share;
use sharks::Share;
mod keyring;
use keyring::Keyring;
@ -77,10 +77,6 @@ pub enum Error {
/// An IO error occurred.
#[error("IO error: {0}")]
Io(#[source] std::io::Error),
/// No valid keys were found for the given recipient.
#[error("No valid keys were found for the recipient {0}")]
NoValidKeys(KeyID),
}
#[allow(missing_docs)]
@ -185,10 +181,19 @@ impl EncryptedMessage {
}
}
/// Encoding and decoding shards using OpenPGP.
pub struct OpenPGP;
///
pub struct OpenPGP<P: PromptHandler> {
p: PhantomData<P>,
}
impl OpenPGP {
impl<P: PromptHandler> OpenPGP<P> {
#[allow(clippy::new_without_default, missing_docs)]
pub fn new() -> Self {
Self { p: PhantomData }
}
}
impl<P: PromptHandler> OpenPGP<P> {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them.
///
/// Certificates are read from a file, or from files one level deep in a directory.
@ -234,20 +239,13 @@ impl OpenPGP {
certs.insert(certfp, cert);
}
}
for cert in certs.values() {
let policy = StandardPolicy::new();
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
if get_encryption_keys(&valid_cert).next().is_none() {
return Err(Error::NoValidKeys(valid_cert.keyid()))
}
}
Ok(certs.into_values().collect())
}
}
const METADATA_MESSAGE_MISSING: &str = "Metadata message was not found in parsed packets";
impl Format for OpenPGP {
impl<P: PromptHandler> Format for OpenPGP<P> {
type Error = Error;
type PublicKey = Cert;
type PrivateKeyData = Vec<Cert>;
@ -444,7 +442,7 @@ impl Format for OpenPGP {
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
prompt: Arc<Mutex<impl PromptHandler>>,
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired.
@ -505,7 +503,7 @@ impl Format for OpenPGP {
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
prompt: Arc<Mutex<impl PromptHandler>>,
) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new();
@ -549,44 +547,24 @@ impl Format for OpenPGP {
panic!("unable to decrypt shard");
}
}
fn decrypt_metadata(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
let mut manager = SmartcardManager::new(prompt.clone())?;
let mut encrypted_messages = encrypted_data.iter();
impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &Path {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
OpenPGP::<P>::discover_certs(self)
}
let metadata = encrypted_messages
.next()
.expect(bug!(METADATA_MESSAGE_MISSING));
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, _root_cert, certs) = decode_metadata_v1(&metadata_content)?;
Ok((threshold, certs))
fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
OpenPGP::<P>::discover_certs(self)
}
}
impl KeyDiscovery<OpenPGP> for &Path {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
OpenPGP::discover_certs(self)
}
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
OpenPGP::discover_certs(self)
}
}
impl KeyDiscovery<OpenPGP> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
Ok(self.to_vec())
}
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
Ok(self.to_vec())
}
}
@ -599,8 +577,7 @@ fn get_encryption_keys<'a>(
openpgp::packet::key::UnspecifiedRole,
> {
cert.keys()
// NOTE: this causes complications on Airgap systems
// .alive()
.alive()
.revoked(false)
.supported()
.for_storage_encryption()
@ -648,12 +625,12 @@ fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
// single message.
fn decrypt_with_manager(
fn decrypt_with_manager<P: PromptHandler>(
threshold: u8,
messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert],
policy: &dyn Policy,
manager: &mut SmartcardManager,
manager: &mut SmartcardManager<P>,
) -> Result<HashMap<KeyID, Vec<u8>>> {
let mut decrypted_messages = HashMap::new();
@ -698,11 +675,11 @@ fn decrypt_with_manager(
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
// decrypt messages with.
fn decrypt_with_keyring(
fn decrypt_with_keyring<P: PromptHandler>(
messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert],
policy: &NullPolicy,
keyring: &mut Keyring,
keyring: &mut Keyring<P>,
) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
let mut decrypted_messages = HashMap::new();
@ -732,11 +709,11 @@ fn decrypt_with_keyring(
Ok(decrypted_messages)
}
fn decrypt_metadata(
fn decrypt_metadata<P: PromptHandler>(
message: &EncryptedMessage,
policy: &NullPolicy,
keyring: &mut Keyring,
manager: &mut SmartcardManager,
keyring: &mut Keyring<P>,
manager: &mut SmartcardManager<P>,
) -> Result<Vec<u8>> {
Ok(if keyring.is_empty() {
manager.load_any_card()?;

View File

@ -1,6 +1,6 @@
#![allow(clippy::expect_fun_call)]
use std::{rc::Rc, sync::Mutex};
use std::sync::{Arc, Mutex};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{Error as PromptError, PromptHandler};
@ -27,14 +27,14 @@ pub enum Error {
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring {
pub struct Keyring<P: PromptHandler> {
full_certs: Vec<Cert>,
root: Option<Cert>,
pm: Rc<Mutex<Box<dyn PromptHandler>>>,
pm: Arc<Mutex<P>>,
}
impl Keyring {
pub fn new(certs: impl AsRef<[Cert]>, p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Result<Self> {
impl<P: PromptHandler> Keyring<P> {
pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self {
full_certs: certs.as_ref().to_vec(),
root: Default::default(),
@ -62,7 +62,7 @@ impl Keyring {
}
}
impl VerificationHelper for &mut Keyring {
impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
Ok(ids
.iter()
@ -108,7 +108,7 @@ impl VerificationHelper for &mut Keyring {
}
}
impl DecryptionHelper for &mut Keyring {
impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
fn decrypt<D>(
&mut self,
pkesks: &[PKESK],

View File

@ -2,13 +2,11 @@
use std::{
collections::{HashMap, HashSet},
rc::Rc,
sync::Mutex,
sync::{Arc, Mutex},
};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{
prompt_validated_passphrase,
validators::{PinValidator, Validator},
Error as PromptError, Message, PromptHandler,
};
@ -73,15 +71,15 @@ fn format_name(input: impl AsRef<str>) -> String {
}
#[allow(clippy::module_name_repetitions)]
pub struct SmartcardManager {
pub struct SmartcardManager<P: PromptHandler> {
current_card: Option<Card<Open>>,
root: Option<Cert>,
pm: Rc<Mutex<Box<dyn PromptHandler>>>,
pm: Arc<Mutex<P>>,
pin_cache: HashMap<Fingerprint, String>,
}
impl SmartcardManager {
pub fn new(p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Result<Self> {
impl<P: PromptHandler> SmartcardManager<P> {
pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self {
current_card: None,
root: None,
@ -175,7 +173,7 @@ impl SmartcardManager {
}
}
impl VerificationHelper for &mut SmartcardManager {
impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
#[allow(clippy::flat_map_option)]
Ok(ids
@ -219,7 +217,7 @@ impl VerificationHelper for &mut SmartcardManager {
}
}
impl DecryptionHelper for &mut SmartcardManager {
impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
fn decrypt<D>(
&mut self,
pkesks: &[PKESK],
@ -277,8 +275,11 @@ impl DecryptionHelper for &mut SmartcardManager {
} else {
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
};
let mut prompt = self.pm.lock().expect(bug!(POISONED_MUTEX));
let temp_pin = prompt_validated_passphrase(&mut **prompt, &message, 3, &pin_validator)?;
let temp_pin = self
.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
match verification_status {
#[allow(clippy::ignored_unit_patterns)]

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork"
version = "0.2.6"
version = "0.2.2"
edition = "2021"
license = "AGPL-3.0-only"
@ -23,28 +23,24 @@ sequoia-crypto-backend-openssl = ["sequoia-openpgp/crypto-openssl"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-bin = { workspace = true }
keyforkd = { workspace = true, features = ["tracing"] }
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
keyfork-derive-util = { workspace = true, default-features = true }
keyfork-derive-openpgp = { workspace = true }
keyfork-derive-path-data = { workspace = true }
keyfork-entropy = { workspace = true }
keyfork-mnemonic = { workspace = true }
keyfork-prompt = { workspace = true }
keyfork-qrcode = { workspace = true, default-features = false }
keyfork-shard = { workspace = true, default-features = false, features = ["openpgp", "openpgp-card", "qrcode"] }
smex = { workspace = true }
keyfork-bin = { version = "0.1.0", path = "../util/keyfork-bin", registry = "distrust" }
keyforkd = { version = "0.1.0", path = "../daemon/keyforkd", features = ["tracing"], registry = "distrust" }
keyforkd-client = { version = "0.2.0", path = "../daemon/keyforkd-client", default-features = false, features = ["ed25519"], registry = "distrust" }
keyfork-derive-openpgp = { version = "0.1.1", path = "../derive/keyfork-derive-openpgp", registry = "distrust" }
keyfork-derive-util = { version = "0.2.0", path = "../derive/keyfork-derive-util", default-features = false, features = ["ed25519"], registry = "distrust" }
keyfork-entropy = { version = "0.1.0", path = "../util/keyfork-entropy", registry = "distrust" }
keyfork-mnemonic-util = { version = "0.3.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", registry = "distrust" }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false, registry = "distrust" }
keyfork-shard = { version = "0.2.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"], registry = "distrust" }
smex = { version = "0.1.0", path = "../util/smex", registry = "distrust" }
clap = { version = "4.4.2", features = ["derive", "env", "wrap_help"] }
thiserror = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
card-backend-pcsc = { workspace = true }
openpgp-card-sequoia = { workspace = true }
openpgp-card = { workspace = true }
thiserror = "1.0.48"
serde = { version = "1.0.192", features = ["derive"] }
tokio = { version = "1.35.1", default-features = false, features = ["rt-multi-thread"] }
card-backend-pcsc = "0.5.0"
openpgp-card-sequoia = { version = "0.2.0", default-features = false }
openpgp-card = "0.4.1"
clap_complete = { version = "4.4.6", optional = true }
sequoia-openpgp = { workspace = true }
keyforkd-models.workspace = true
base64.workspace = true
nix = { version = "0.29.0", default-features = false, features = ["process"] }
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] }

View File

@ -1,60 +0,0 @@
//! Extensions to clap.
use std::{collections::HashMap, str::FromStr};
/// A helper struct for clap arguments that can contain additional arguments. For example:
/// `keyfork mnemonic generate --encrypt-to cert.asc,output=encrypted.asc`.
#[derive(Clone, Debug)]
pub struct ValueWithOptions<T: FromStr>
where
T::Err: std::error::Error,
{
/// A mapping between keys and values.
pub values: HashMap<String, String>,
/// The first variable for the argument, such as a [`PathBuf`].
pub inner: T,
}
/// An error that occurred while parsing a base value or its
#[derive(Debug, thiserror::Error)]
pub enum ValueParseError {
/// No value was given; the required type could not be parsed.
#[error("No value was given")]
NoValue,
/// The first value could not properly be parsed.
#[error("Could not parse first value: {0}")]
BadParse(String),
/// Additional values were added, but not in a key=value format.
#[error("A key-value pair was not given")]
BadKeyValue,
}
impl<T: std::str::FromStr> FromStr for ValueWithOptions<T>
where
<T as FromStr>::Err: std::error::Error,
{
type Err = ValueParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut values = s.split(',');
let first = values.next().ok_or(ValueParseError::NoValue)?;
let mut others = HashMap::new();
for value in values {
let [lhs, rhs] = value
.splitn(2, '=')
.collect::<Vec<_>>()
.try_into()
.map_err(|_| ValueParseError::BadKeyValue)?;
others.insert(lhs.to_string(), rhs.to_string());
}
Ok(Self {
inner: first
.parse()
.map_err(|e: <T as FromStr>::Err| ValueParseError::BadParse(e.to_string()))?,
values: others,
})
}
}

View File

@ -1,5 +1,5 @@
use super::Keyfork;
use clap::{Args, Parser, Subcommand, ValueEnum};
use clap::{Parser, Subcommand};
use keyfork_derive_openpgp::{
openpgp::{
@ -10,13 +10,8 @@ use keyfork_derive_openpgp::{
},
XPrvKey,
};
use keyfork_derive_path_data::paths;
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath, IndexError,
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyforkd_client::Client;
use keyforkd_models::Request;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -32,126 +27,45 @@ pub enum DeriveSubcommands {
/// It is recommended to use the default expiration of one day and to change the expiration
/// using an external utility, to ensure the Certify key is usable.
#[command(name = "openpgp")]
OpenPGP(OpenPGP),
/// Derive a bare key for a specific algorithm, in a given format.
Key(Key),
}
#[derive(Args, Clone, Debug)]
pub struct OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String,
}
/// A format for exporting a key.
#[derive(ValueEnum, Clone, Debug)]
pub enum KeyFormat {
Hex,
Base64,
}
/// An invalid slug was provided.
#[derive(thiserror::Error, Debug)]
pub enum InvalidSlug {
/// The value provided was longer than four bytes.
#[error("The value provided was longer than four bytes: {0}")]
InvalidSize(usize),
/// The value provided was higher than the maximum derivation index.
#[error("The value provided was higher than the maximum derivation index: {0}")]
InvalidValue(#[from] IndexError),
}
#[derive(Clone, Debug)]
pub struct Slug(DerivationIndex);
impl std::str::FromStr for Slug {
type Err = InvalidSlug;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let bytes = s.as_bytes();
let mut parseable_bytes = [0u8; 4];
if bytes.len() <= 4 && !bytes.is_empty() {
parseable_bytes[(4 - bytes.len())..4].copy_from_slice(bytes);
} else {
return Err(InvalidSlug::InvalidSize(bytes.len()));
}
let slug = u32::from_be_bytes(parseable_bytes);
let index = DerivationIndex::new(slug, true)?;
Ok(Slug(index))
}
}
#[derive(Args, Clone, Debug)]
pub struct Key {
/// The derivation algorithm to derive a key for.
derivation_algorithm: DerivationAlgorithm,
/// The output format.
#[arg(value_enum)]
format: KeyFormat,
/// A maximum of four bytes, used for creating the derivation path.
#[arg(value_parser = clap::value_parser!(Slug))]
slug: Slug,
OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String,
},
}
impl DeriveSubcommands {
fn handle(&self, account: DerivationIndex) -> Result<()> {
match self {
DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account),
DeriveSubcommands::Key(key) => key.handle(account),
}
}
}
DeriveSubcommands::OpenPGP { user_id } => {
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let path = DerivationPath::default()
.chain_push(chain)
.chain_push(account);
// TODO: should this be customizable?
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
impl OpenPGP {
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
let path = paths::OPENPGP.clone().chain_push(account);
// TODO: should this be customizable?
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(self.user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
for packet in cert.into_packets() {
packet.serialize(&mut w)?;
}
for packet in cert.as_tsk().into_packets() {
packet.serialize(&mut w)?;
}
w.finalize()?;
Ok(())
}
}
impl Key {
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
let mut client = keyforkd_client::Client::discover_socket()?;
let path = DerivationPath::default()
.chain_push(self.slug.0.clone())
.chain_push(account);
let request = DerivationRequest::new(self.derivation_algorithm.clone(), &path);
let request = Request::Derivation(request);
let derived_key: DerivationResponse = client.request(&request)?.try_into()?;
let formatted = match self.format {
KeyFormat::Hex => smex::encode(derived_key.data),
KeyFormat::Base64 => {
use base64::prelude::*;
BASE64_STANDARD.encode(derived_key.data)
w.finalize()?;
}
};
}
eprintln!("{formatted}");
Ok(())
}
}

View File

@ -1,34 +1,6 @@
use super::provision;
use super::Keyfork;
use crate::{clap_ext::*, config};
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use std::{
collections::HashMap,
fmt::Display,
fs::File,
io::Write,
path::{Path, PathBuf},
str::FromStr,
};
use keyfork_derive_openpgp::{
openpgp::{
self,
armor::{Kind, Writer},
packet::UserID,
policy::StandardPolicy,
serialize::{
stream::{Encryptor2, LiteralWriter, Message, Recipient},
Serialize,
},
types::KeyFlags,
},
XPrv,
};
use keyfork_prompt::default_handler;
use keyfork_shard::{openpgp::OpenPGP, Format};
type StringMap = HashMap<String, String>;
use std::fmt::Display;
#[derive(Clone, Debug, Default)]
pub enum SeedSize {
@ -87,7 +59,6 @@ impl From<&SeedSize> for usize {
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum MnemonicSeedSourceParseError {
#[error("Expected one of system, playing, tarot, dice")]
@ -125,37 +96,24 @@ impl std::str::FromStr for MnemonicSeedSource {
}
impl MnemonicSeedSource {
pub fn handle(
&self,
size: &SeedSize,
) -> Result<keyfork_mnemonic::Mnemonic, Box<dyn std::error::Error>> {
pub fn handle(&self, size: &SeedSize) -> Result<String, Box<dyn std::error::Error>> {
let size = match size {
SeedSize::Bits128 => 128,
SeedSize::Bits256 => 256,
};
let seed = match self {
MnemonicSeedSource::System => keyfork_entropy::generate_entropy_of_size(size / 8)?,
MnemonicSeedSource::System => {
keyfork_entropy::generate_entropy_of_size(size / 8)?
}
MnemonicSeedSource::Playing => todo!(),
MnemonicSeedSource::Tarot => todo!(),
MnemonicSeedSource::Dice => todo!(),
};
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?;
Ok(mnemonic)
let mnemonic = keyfork_mnemonic_util::Mnemonic::from_bytes(&seed)?;
Ok(mnemonic.to_string())
}
}
/// An error occurred while performing an operation.
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// An error occurred when interacting iwth a file.
#[error("Error while performing IO operation on: {1}")]
IOContext(#[source] std::io::Error, PathBuf),
}
fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a {
|e| Error::IOContext(e, path.to_path_buf())
}
#[derive(Subcommand, Clone, Debug)]
pub enum MnemonicSubcommands {
/// Generate a mnemonic using a given entropy source.
@ -166,10 +124,6 @@ pub enum MnemonicSubcommands {
/// method of generating a seed using system entropy, as well as various forms of loading
/// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location
/// (such as a Trezor "recovery seed card") and never persisted digitally.
///
/// When using the `--shard`, `--shard-to`, `--encrypt-to`, and `--encrypt-to-self` +
/// `--provision` arguments, the mnemonic is _not_ sent to output. The data for the mnemonic is
/// then either split using Keyfork Shard or encrypted using OpenPGP.
Generate {
/// The source from where a seed is created.
#[arg(long, value_enum, default_value_t = Default::default())]
@ -178,480 +132,17 @@ pub enum MnemonicSubcommands {
/// The size of the mnemonic, in bits.
#[arg(long, default_value_t = Default::default())]
size: SeedSize,
/// Encrypt the mnemonic to an OpenPGP certificate in the provided path.
///
/// When given arguments in the format `--encrypt-to input.asc,output=output.asc`, the
/// output of the encryption will be written to `output.asc`. Otherwise, the default
/// behavior is to write the output to `input.enc.asc`. If the output file already exists,
/// it will not be overwritten, and the command will exit unsuccessfully.
#[arg(long)]
encrypt_to: Option<Vec<ValueWithOptions<PathBuf>>>,
/// Shard the mnemonic to the certificates in the given Shardfile. Requires a decrypt
/// operation on the Shardfile to access the metadata and certificates.
///
/// When given arguments in the format `--encrypt-to input.asc,output=output.asc`, the
/// output of the encryption will be written to `output.asc`. Otherwise, the default
/// behavior is to write the output to `input.new.asc`. If the output file already exists,
/// it will not be overwritten, and the command will exit unsuccessfully.
#[arg(long)]
shard_to: Option<Vec<ValueWithOptions<PathBuf>>>,
/// Shard the mnemonic to the provided certificates.
///
/// The following additional arguments are available:
///
/// * threshold, m: the minimum amount of shares required to reconstitute the shard. By
/// default, this is the amount of certificates provided.
///
/// * max, n: the maximum amount of shares. When provided, this is used to ensure the
/// certificate count is correct. This is required when using `threshold` or `m`.
///
/// * output: the file to write the generated Shardfile to. By default, assuming the
/// certificate input is `input.asc`, the generated Shardfile would be written to
/// `input.shard.asc`.
#[arg(long)]
shard: Option<Vec<ValueWithOptions<PathBuf>>>,
/// Encrypt the mnemonic to an OpenPGP certificate derived from the mnemonic, writing the
/// output to the provided path. This command must be run in combination with
/// `--provision openpgp-card` or another relevant provisioner, to ensure the newly
/// generated mnemonic would be decryptable by some form of provisioned hardware.
///
/// When given arguments in the format `--encrypt-to-self encrypted.asc,output=cert.asc`,
/// the output of the OpenPGP certificate will be written to `cert.asc`, while the output
/// of the encryption will be written to `encrypted.asc`. Otherwise, the
/// default behavior is to write the certificate to a file named after the certificate's
/// fingerprint. If either output file already exists, it will not be overwritten, and the
/// command will exit unsuccessfully. This functionality must happen regardless if a
/// provisioner output is specified, as the certificate is then used to encrypt the
/// mnemonic.
///
/// Additionally, when given the `account=` option (which must match the `account=` option
/// of the relevant provisioner), the given account will be used instead of the default
/// account of 0.
///
/// Because a new OpenPGP cert needs to be created, a User ID can also be supplied, using
/// the option `userid=<your User ID>`. It can contain any characters that are not a comma.
/// If any other operation generating an OpenPGP key has a `userid=` field, and this
/// operation doesn't, that User ID will be used instead.
#[arg(long)]
encrypt_to_self: Option<ValueWithOptions<PathBuf>>,
/// Provision a key derived from the mnemonic to a piece of hardware such as an OpenPGP
/// smartcard. This argument is required when used with `--encrypt-to-self`.
///
/// Additional arguments, such as the amount of hardware to provision and the
/// account to use when deriving, can be specified by using (for example)
/// `--provision openpgp-card,count=2,account=1`.
///
/// Provisioners may output their public key, if necessary. The file path may be chosen
/// based on the provided `output` field, or automatically determined based on the content
/// of the key, such as an OpenPGP fingerprint or a public key hash. If automatically
/// generated, the filename will be printed.
///
/// If the OpenPGP Card provisioner is selected, because a new OpenPGP cert needs to be
/// created, a User ID can also be supplied, using the option `userid=<your User ID>`. It
/// can contain any characters that are not a comma. If any other operation generating an
/// OpenPGP key has a `userid=` field, and this operation doesn't, that User ID will be
/// used instead.
#[arg(long)]
provision: Option<ValueWithOptions<provision::Provisioner>>,
},
}
// NOTE: This function defaults to `.asc` in the event no extension is found.
// This is specific to OpenPGP. If you want to use this function elsewhere (why?),
// be sure to use a relevant extension for your context.
fn determine_valid_output_path<T: AsRef<Path>>(
path: &Path,
mid_ext: &str,
optional_path: Option<T>,
) -> PathBuf {
match optional_path {
Some(p) => p.as_ref().to_path_buf(),
None => {
let extension = match path.extension() {
Some(ext) => format!("{mid_ext}.{ext}", ext = ext.to_string_lossy()),
None => format!("{mid_ext}.asc"),
};
path.with_extension(extension)
}
}
}
fn is_extension_armored(path: &Path) -> bool {
match path.extension().and_then(|s| s.to_str()) {
Some("pgp") | Some("gpg") => false,
Some("asc") => true,
_ => {
eprintln!("unable to determine whether to armor file: {path:?}");
eprintln!("use .gpg, .pgp, or .asc extension, or `armor=true`");
eprintln!("defaulting to armored");
true
}
}
}
fn do_encrypt_to(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let policy = StandardPolicy::new();
let output_file = determine_valid_output_path(path, "enc", options.get("output"));
let is_armored =
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
let certs = OpenPGP::discover_certs(path)?;
let valid_certs = certs
.iter()
.map(|c| c.with_policy(&policy, None))
.collect::<openpgp::Result<Vec<_>>>()?;
let recipients = valid_certs.iter().flat_map(|valid_cert| {
let keys = valid_cert.keys().alive().for_storage_encryption();
keys.map(|key| Recipient::new(key.keyid(), key.key()))
});
let mut output = vec![];
let message = Message::new(&mut output);
let encrypted_message = Encryptor2::for_recipients(message, recipients).build()?;
let mut literal_message = LiteralWriter::new(encrypted_message).build()?;
literal_message.write_all(mnemonic.to_string().as_bytes())?;
literal_message.write_all(b"\n")?;
literal_message.finalize()?;
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
let mut writer = Writer::new(file, Kind::Message)?;
writer.write_all(&output)?;
writer.finalize()?;
} else {
file.write_all(&output)?;
}
Ok(())
}
fn do_encrypt_to_self(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let account = options
.get("account")
.map(|account| u32::from_str(account))
.transpose()?
.unwrap_or(0);
let account_index = keyfork_derive_util::DerivationIndex::new(account, true)?;
let userid = options
.get("userid")
.map(|userid| UserID::from(userid.as_str()));
let subkeys = [
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let seed = mnemonic.generate_seed(None);
let xprv = XPrv::new(seed)?;
let derivation_path = keyfork_derive_path_data::paths::OPENPGP
.clone()
.chain_push(account_index);
let cert = keyfork_derive_openpgp::derive(
xprv.derive_path(&derivation_path)?,
&subkeys,
&userid.unwrap_or(UserID::from("Keyfork-Generated Key")),
)?;
let cert_path = match options.get("output") {
Some(path) => PathBuf::from(path),
None => {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
eprintln!(
"Writing OpenPGP certificate to default path: {path}",
path = path.display()
);
path
}
};
let file = File::create_new(&cert_path).map_err(context_stub(&cert_path))?;
let mut writer = Writer::new(file, Kind::PublicKey)?;
cert.serialize(&mut writer)?;
writer.finalize()?;
// a sneaky bit of DRY
do_encrypt_to(
mnemonic,
&cert_path,
&StringMap::from([(String::from("output"), path.to_string_lossy().to_string())]),
)?;
Ok(())
}
#[derive(thiserror::Error, Debug)]
#[error("Either the threshold(m) or the max(n) values are missing")]
struct MissingThresholdOrMax;
fn do_shard(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let output_file = determine_valid_output_path(path, "shard", options.get("output"));
let is_armored =
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
let threshold = options
.get("threshold")
.or_else(|| options.get("m"))
.map(|s| u8::from_str(s))
.transpose()?;
let max = options
.get("max")
.or_else(|| options.get("n"))
.map(|s| u8::from_str(s))
.transpose()?;
let certs = OpenPGP::discover_certs(path)?;
// if neither are set: false
// if both are set: false
// if only one is set: true
if threshold.is_some() ^ max.is_some() {
return Err(MissingThresholdOrMax)?;
}
let (threshold, max) = match threshold.zip(max) {
Some(t) => t,
None => {
let len = u8::try_from(certs.len())?;
(len, len)
}
};
let openpgp = keyfork_shard::openpgp::OpenPGP;
let mut output = vec![];
openpgp.shard_and_encrypt(threshold, max, mnemonic.as_bytes(), &certs[..], &mut output)?;
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
file.write_all(&output)?;
} else {
todo!("keyfork does not handle binary shardfiles");
/*
* NOTE: this code works, but can't be recombined by Keyfork.
* therefore, we'll error, before someone tries to use it.
let mut dearmor = Reader::from_bytes(&output, ReaderMode::Tolerant(None));
std::io::copy(&mut dearmor, &mut file)?;
*/
}
Ok(())
}
fn do_shard_to(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let output_file = determine_valid_output_path(path, "new", options.get("output"));
let is_armored =
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?;
let input = File::open(path)?;
let (threshold, certs) = openpgp.decrypt_metadata_from_file(
Some(&[][..]), // the things i must do to avoid qualifying types.
input,
prompt,
)?;
let mut output = vec![];
openpgp.shard_and_encrypt(
threshold,
u8::try_from(certs.len())?,
mnemonic.as_bytes(),
&certs[..],
&mut output,
)?;
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
file.write_all(&output)?;
} else {
todo!("keyfork does not handle binary shardfiles");
/*
* NOTE: this code works, but can't be recombined by Keyfork.
* therefore, we'll error, before someone tries to use it.
let mut dearmor = Reader::from_bytes(&output, ReaderMode::Tolerant(None));
std::io::copy(&mut dearmor, &mut file)?;
*/
}
Ok(())
}
fn do_provision(
mnemonic: &keyfork_mnemonic::Mnemonic,
provisioner: &provision::Provisioner,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let mut options = options.clone();
let account = options
.remove("account")
.map(|account| u32::from_str(&account))
.transpose()?
.unwrap_or(0);
let identifier = options
.remove("identifier")
.map(|s| s.split('.').map(String::from).collect::<Vec<_>>())
.map(Result::<_, Box<dyn std::error::Error>>::Ok)
.unwrap_or_else(|| {
Ok(provisioner
.discover()?
.into_iter()
.map(|(identifier, _)| identifier)
.collect())
})?;
let count = options
.remove("count")
.map(|count| usize::from_str(&count))
.transpose()?
.unwrap_or(identifier.len());
assert_eq!(
count,
identifier.len(),
"amount of identifiers discovered or provided did not match provisioner count"
);
for (_, identifier) in (0..count).zip(identifier.into_iter()) {
let provisioner_config = config::Provisioner {
account,
identifier,
metadata: Some(options.clone()),
};
provisioner.provision_with_mnemonic(mnemonic, provisioner_config.clone())?;
}
Ok(())
}
impl MnemonicSubcommands {
pub fn handle(
&self,
_m: &Mnemonic,
_keyfork: &Keyfork,
) -> Result<(), Box<dyn std::error::Error>> {
) -> Result<String, Box<dyn std::error::Error>> {
match self {
MnemonicSubcommands::Generate {
source,
size,
encrypt_to,
shard_to,
shard,
encrypt_to_self,
provision,
} => {
// NOTE: We should never have a case where there's Some() of empty vec, but
// we will make sure to check it just in case.
let mut will_print_mnemonic =
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(|e| e.is_empty());
will_print_mnemonic = will_print_mnemonic && shard_to.is_none()
|| shard_to.as_ref().is_some_and(|s| s.is_empty());
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|| shard.as_ref().is_some_and(|s| s.is_empty());
will_print_mnemonic = will_print_mnemonic
&& (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none());
let mnemonic = source.handle(size)?;
if let Some(encrypt_to) = encrypt_to {
for entry in encrypt_to {
do_encrypt_to(&mnemonic, &entry.inner, &entry.values)?;
}
}
if let Some(encrypt_to_self) = encrypt_to_self {
let mut values = encrypt_to_self.values.clone();
// If we have a userid from `provision` but not one here, use that one.
if let Some(provision) = provision {
if matches!(&provision.inner, provision::Provisioner::OpenPGPCard(_))
&& !values.contains_key("userid")
{
if let Some(userid) = provision.values.get("userid") {
values.insert(String::from("userid"), userid.clone());
}
}
}
do_encrypt_to_self(&mnemonic, &encrypt_to_self.inner, &values)?;
}
if let Some(provisioner) = provision {
// NOTE: If we have encrypt_to_self, we likely also have the certificate
// already generated. Therefore, we can skip generating it in the provisioner.
// However, if we don't have encrypt_to_self, we might not have the
// certificate, therefore the provisioner - by default - generates the public
// key output.
//
// We use the atypical `_skip_cert_output` field here to denote an automatic
// marking to skip the cert output. However, the `output` field will take
// priority, since it can only be manually set by the user.
let mut values = provisioner.values.clone();
if let Some(encrypt_to_self) = encrypt_to_self {
if !values.contains_key("output") {
values.insert(String::from("_skip_cert_output"), String::from("1"));
}
// If we have a userid from `encrypt_to_self` but not one here, use that
// one.
if matches!(&provisioner.inner, provision::Provisioner::OpenPGPCard(_))
&& !values.contains_key("userid")
{
if let Some(userid) = encrypt_to_self.values.get("userid") {
values.insert(String::from("userid"), userid.clone());
}
}
}
do_provision(&mnemonic, &provisioner.inner, &values)?;
}
if let Some(shard_to) = shard_to {
for entry in shard_to {
do_shard_to(&mnemonic, &entry.inner, &entry.values)?;
}
}
if let Some(shard) = shard {
for entry in shard {
do_shard(&mnemonic, &entry.inner, &entry.values)?;
}
}
if will_print_mnemonic {
println!("{}", mnemonic);
}
Ok(())
}
MnemonicSubcommands::Generate { source, size } => source.handle(size),
}
}
}

View File

@ -79,7 +79,8 @@ impl KeyforkCommands {
d.handle(keyfork)?;
}
KeyforkCommands::Mnemonic(m) => {
m.command.handle(m, keyfork)?;
let response = m.command.handle(m, keyfork)?;
println!("{response}");
}
KeyforkCommands::Shard(s) => {
s.command.handle(s, keyfork)?;

View File

@ -3,15 +3,9 @@ use crate::config;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_derive_util::{DerivationIndex, ExtendedPrivateKey};
mod openpgp;
type Identifier = (String, Option<String>);
#[derive(Debug, Clone)]
pub enum Provisioner {
OpenPGPCard(openpgp::OpenPGPCard),
OpenPGPCard(OpenPGPCard),
}
impl std::fmt::Display for Provisioner {
@ -23,52 +17,25 @@ impl std::fmt::Display for Provisioner {
}
impl Provisioner {
pub fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
fn discover(&self) -> Vec<(String, Option<String>)> {
match self {
Provisioner::OpenPGPCard(o) => o.discover(),
}
}
pub fn provision(
fn provision(
&self,
provisioner: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
match self {
Provisioner::OpenPGPCard(o) => {
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
type XPrv = ExtendedPrivateKey<Prv>;
let account_index = DerivationIndex::new(provisioner.account, true)?;
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let mut client = keyforkd_client::Client::discover_socket()?;
let xprv: XPrv = client.request_xprv(&path)?;
o.provision(xprv, provisioner)
}
}
}
pub fn provision_with_mnemonic(
&self,
mnemonic: &keyfork_mnemonic::Mnemonic,
provisioner: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
match self {
Provisioner::OpenPGPCard(o) => {
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
type XPrv = ExtendedPrivateKey<Prv>;
let account_index = DerivationIndex::new(provisioner.account, true)?;
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
o.provision(xprv, provisioner)
}
Provisioner::OpenPGPCard(o) => o.provision(provisioner),
}
}
}
impl ValueEnum for Provisioner {
fn value_variants<'a>() -> &'a [Self] {
&[Self::OpenPGPCard(openpgp::OpenPGPCard)]
&[Self::OpenPGPCard(OpenPGPCard)]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
@ -78,36 +45,39 @@ impl ValueEnum for Provisioner {
}
}
#[derive(Debug, thiserror::Error)]
#[error("The given value could not be matched as a provisioner: {0} ({1})")]
pub struct ProvisionerFromStrError(String, String);
impl std::str::FromStr for Provisioner {
type Err = ProvisionerFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<Provisioner as ValueEnum>::from_str(s, false)
.map_err(|e| ProvisionerFromStrError(s.to_string(), e))
}
}
trait ProvisionExec {
type PrivateKey: keyfork_derive_util::PrivateKey + Clone;
/// Discover all known places the formatted key can be deployed to.
fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
Ok(vec![])
fn discover(&self) -> Vec<(String, Option<String>)> {
vec![]
}
/// Return the derivation path for deriving keys.
fn derivation_prefix() -> keyfork_derive_util::DerivationPath;
/// Derive a key and deploy it to a target.
fn provision(
&self,
xprv: keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
p: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>>;
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Clone, Debug)]
pub struct OpenPGPCard;
impl ProvisionExec for OpenPGPCard {
fn discover(&self) -> Vec<(String, Option<String>)> {
/*
vec![
(
"0006:26144195".to_string(),
Some("Yubicats Heywood".to_string()),
),
(
"0006:2614419y".to_string(),
Some("Yubicats Heywood".to_string()),
),
]
*/
vec![]
}
fn provision(&self, _p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>> {
todo!()
}
}
#[derive(Subcommand, Clone, Debug)]
@ -148,6 +118,7 @@ impl TryFrom<Provision> for config::Provisioner {
fn try_from(value: Provision) -> Result<Self, Self::Error> {
Ok(Self {
name: value.provisioner_name.to_string(),
account: value.account_id.ok_or(MissingField("account_id"))?,
identifier: value.identifier.ok_or(MissingField("identifier"))?,
metadata: Default::default(),
@ -159,7 +130,7 @@ impl Provision {
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
match self.subcommand {
Some(ProvisionSubcommands::Discover) => {
let mut iter = self.provisioner_name.discover()?.into_iter().peekable();
let mut iter = self.provisioner_name.discover().into_iter().peekable();
while let Some((identifier, context)) = iter.next() {
println!("Identifier: {identifier}");
if let Some(context) = context {

View File

@ -1,144 +0,0 @@
use super::ProvisionExec;
use crate::{config, openpgp_card::factory_reset_current_card};
use card_backend_pcsc::PcscBackend;
use keyfork_derive_openpgp::{
openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Serialize,
types::KeyFlags,
},
XPrv,
};
use keyfork_prompt::{
default_handler, prompt_validated_passphrase,
validators::{SecurePinValidator, Validator},
};
use openpgp_card_sequoia::{state::Open, Card};
use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct OpenPGPCard;
#[derive(thiserror::Error, Debug)]
#[error("Provisioner was unable to find a matching smartcard")]
struct NoMatchingSmartcard;
impl ProvisionExec for OpenPGPCard {
type PrivateKey = keyfork_derive_openpgp::XPrvKey;
fn discover(&self) -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> {
let mut idents = vec![];
for backend in PcscBackend::cards(None)? {
let backend = backend?;
let mut card = Card::<Open>::new(backend)?;
let mut transaction = card.transaction()?;
let identifier = transaction.application_identifier()?.ident();
let name = transaction.cardholder_name()?;
let name = (!name.is_empty()).then_some(name);
idents.push((identifier, name));
}
Ok(idents)
}
fn derivation_prefix() -> keyfork_derive_util::DerivationPath {
keyfork_derive_path_data::paths::OPENPGP.clone()
}
fn provision(
&self,
xprv: XPrv,
provisioner: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = default_handler()?;
let user_pin_validator = SecurePinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
let user_pin = prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let userid = match provisioner.metadata.as_ref().and_then(|m| m.get("userid")) {
Some(userid) => UserID::from(userid.as_str()),
None => UserID::from("Keyfork-Provisioned Key"),
};
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &subkeys, &userid)?;
if !provisioner
.metadata
.as_ref()
.is_some_and(|m| m.contains_key("_skip_cert_output"))
{
let cert_output = match provisioner
.metadata
.as_ref()
.and_then(|m| m.get("output"))
{
Some(cert_output) => PathBuf::from(cert_output),
None => {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
eprintln!(
"Writing OpenPGP certificate to: {path}",
path = path.display()
);
path
}
};
let cert_output_file = std::fs::File::create_new(cert_output)?;
let mut writer = Writer::new(cert_output_file, Kind::PublicKey)?;
cert.serialize(&mut writer)?;
writer.finalize()?;
}
let mut has_provisioned = false;
for backend in PcscBackend::cards(None)? {
let backend = backend?;
let result = factory_reset_current_card(
&mut |identifier| identifier == provisioner.identifier,
user_pin.trim(),
admin_pin.trim(),
&cert,
&keyfork_derive_openpgp::openpgp::policy::StandardPolicy::new(),
backend,
)?;
has_provisioned = has_provisioned || result;
}
if !has_provisioned {
return Err(NoMatchingSmartcard)?;
}
Ok(())
}
}

View File

@ -1,19 +1,9 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use nix::{
sys::wait::waitpid,
unistd::{fork, ForkResult},
};
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{
default_handler, prompt_validated_wordlist,
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
},
};
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_shard::{remote_decrypt, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -45,8 +35,8 @@ impl RecoverSubcommands {
} => {
let content = std::fs::read_to_string(shard_file)?;
if content.contains("BEGIN PGP MESSAGE") {
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt_handler = default_handler()?;
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
// TODO: remove .clone() by making handle() consume self
let seed = openpgp.decrypt_all_shards_to_secret(
key_discovery.as_deref(),
@ -64,15 +54,21 @@ impl RecoverSubcommands {
Ok(seed)
}
RecoverSubcommands::Mnemonic {} => {
let mut prompt_handler = default_handler()?;
use keyfork_prompt::{
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
},
PromptHandler,
};
let mut term = default_terminal()?;
let validator = MnemonicChoiceValidator {
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
};
let mnemonic = prompt_validated_wordlist::<English, _>(
&mut *prompt_handler,
let mnemonic = term.prompt_validated_wordlist::<English, _>(
"Mnemonic: ",
3,
&*validator.to_fn(),
validator.to_fn(),
)?;
Ok(mnemonic.to_bytes())
}
@ -84,32 +80,12 @@ impl RecoverSubcommands {
pub struct Recover {
#[command(subcommand)]
command: RecoverSubcommands,
/// Daemonize the server once started, restoring control back to the shell.
#[arg(long, global=true)]
daemon: bool,
}
impl Recover {
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
let seed = self.command.handle()?;
let mnemonic = Mnemonic::try_from_slice(&seed)?;
if self.daemon {
// SAFETY: Forking threaded programs is unsafe. We know we don't have multiple
// threads at this point.
match unsafe { fork() }? {
ForkResult::Parent { child } => {
// wait for the child to die, so we don't exit prematurely
waitpid(Some(child), None)?;
return Ok(());
},
ForkResult::Child => {
if let ForkResult::Parent { .. } = unsafe { fork() }? {
return Ok(());
}
},
}
}
let mnemonic = Mnemonic::from_bytes(&seed)?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()

View File

@ -1,6 +1,6 @@
use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_prompt::default_handler;
use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_shard::Format as _;
use std::{
io::{stdin, stdout, Read, Write},
@ -50,14 +50,6 @@ trait ShardExec {
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>>;
fn metadata(
&self,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
output_pubkeys: &mut impl Write,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Clone, Debug)]
@ -72,7 +64,7 @@ impl ShardExec for OpenPGP {
secret: &[u8],
output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>> {
let opgp = keyfork_shard::openpgp::OpenPGP;
let opgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
}
@ -82,8 +74,8 @@ impl ShardExec for OpenPGP {
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?;
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt = default_terminal()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
write!(output, "{}", smex::encode(bytes))?;
@ -95,36 +87,11 @@ impl ShardExec for OpenPGP {
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?;
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt = default_terminal()?;
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
Ok(())
}
fn metadata(
&self,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
output_pubkeys: &mut impl Write,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> {
use keyfork_derive_openpgp::openpgp::{
serialize::Marshal,
armor::{Writer, Kind},
};
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?;
let (threshold, certs) = openpgp.decrypt_metadata_from_file(key_discovery, input, prompt)?;
let mut writer = Writer::new(output_pubkeys, Kind::PublicKey)?;
for cert in certs {
cert.serialize(&mut writer)?;
}
writer.finalize()?;
writeln!(output, "Threshold: {threshold}")?;
Ok(())
}
}
#[derive(Clone, Debug)]
@ -174,20 +141,6 @@ pub enum ShardSubcommands {
/// The path to discover private keys from.
key_discovery: Option<PathBuf>,
},
/// Decrypt metadata for a shardfile, including the threshold and the public keys. Public keys
/// are serialized to a file.
Metadata {
/// The path to load the Shardfile from.
shardfile: PathBuf,
/// The path to write public keys to.
#[arg(long)]
output_pubkeys: PathBuf,
/// The path to discover private keys from.
key_discovery: Option<PathBuf>,
}
}
impl ShardSubcommands {
@ -256,27 +209,6 @@ impl ShardSubcommands {
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
}
}
ShardSubcommands::Metadata { shardfile, output_pubkeys, key_discovery } => {
let shard_content = std::fs::read_to_string(shardfile)?;
if shard_content.contains("BEGIN PGP MESSAGE") {
let _ = format.insert(Format::OpenPGP(OpenPGP));
}
let mut output_pubkeys_file = std::fs::File::create(output_pubkeys)?;
match format {
Some(Format::OpenPGP(o)) => o.metadata(
key_discovery.as_deref(),
shard_content.as_bytes(),
&mut output_pubkeys_file,
&mut stdout,
),
Some(Format::P256(_p)) => {
todo!()
}
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
}
}
}
}
}

View File

@ -1,30 +1,26 @@
use super::Keyfork;
use crate::openpgp_card::factory_reset_current_card;
use clap::{Args, Parser, Subcommand};
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
use clap::{Parser, Subcommand};
use std::{
collections::HashSet,
fs::File,
io::IsTerminal,
path::{Path, PathBuf},
};
use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use keyfork_derive_openpgp::{
openpgp::{
self,
armor::{Kind, Writer},
packet::{signature::SignatureBuilder, UserID},
policy::StandardPolicy,
serialize::Marshal,
types::{KeyFlags, SignatureType},
Cert,
},
openpgp::{self, packet::UserID, types::KeyFlags, Cert, serialize::Marshal, armor::{Writer, Kind}},
XPrv,
};
use keyfork_derive_path_data::paths;
use keyfork_derive_util::DerivationIndex;
use keyfork_mnemonic::Mnemonic;
use keyfork_derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed};
use keyfork_prompt::{
default_handler, prompt_validated_passphrase,
default_terminal,
validators::{SecurePinValidator, Validator},
Message,
DefaultTerminal, Message, PromptHandler,
};
use keyfork_mnemonic_util::Mnemonic;
use keyfork_shard::{openpgp::OpenPGP, Format};
@ -46,8 +42,17 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
KeyFlags::empty().set_authentication(),
];
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let mut shrd_u32 = [0u8; 4];
shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::<Vec<u8>>());
let account = DerivationIndex::new(u32::from_be_bytes(shrd_u32), true)?;
let subkey = DerivationIndex::new(u32::from(index), true)?;
let path = paths::OPENPGP_SHARD.clone().chain_push(subkey);
let path = DerivationPath::default()
.chain_push(chain)
.chain_push(account)
.chain_push(subkey);
let xprv = XPrv::new(seed)
.expect("could not construct master key from seed")
.derive_path(&path)?;
@ -56,262 +61,247 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
Ok(cert)
}
#[derive(Subcommand, Clone, Debug)]
pub enum WizardSubcommands {
GenerateShardSecret(GenerateShardSecret),
BottomsUp(BottomsUp),
}
/// Create a 256 bit secret and shard the secret to smart cards.
///
/// Smart cards will need to be plugged in periodically during the wizard, where they will be
/// factory reset and provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered
/// with `keyfork recover shard` or `keyfork recover remote-shard`. The share file will be printed
/// to standard output.
#[derive(Args, Clone, Debug)]
pub struct GenerateShardSecret {
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// The maximum amount of shards.
#[arg(long)]
max: u8,
/// The amount of smart cards to provision per-shard.
#[arg(long, default_value = "1")]
keys_per_shard: u8,
/// The file to write the generated shard file to.
#[arg(long)]
output: Option<PathBuf>,
/// The file to write generated certificates to.
#[arg(long)]
cert_output: Option<PathBuf>,
}
/// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates,
/// deriving the default OpenPGP certificate for the secret.
///
/// This command was purpose-built for DEFCON and is not intended to be used normally, as it
/// implies keys used for sharding have been generated by a custom source.
#[derive(Args, Clone, Debug)]
pub struct BottomsUp {
/// The location of OpenPGP certificates to use when sharding.
key_discovery: PathBuf,
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// The file to write the generated shard file to.
#[arg(long)]
output_shardfile: PathBuf,
/// The file to write the generated OpenPGP certificate to.
#[arg(long)]
output_cert: PathBuf,
/// The User ID for the generated OpenPGP certificate.
#[arg(long, default_value = "Disaster Recovery")]
user_id: String,
}
impl WizardSubcommands {
// dispatch
fn handle(&self) -> Result<()> {
match self {
WizardSubcommands::GenerateShardSecret(gss) => gss.handle(),
WizardSubcommands::BottomsUp(bu) => bu.handle(),
}
// TODO: extract into crate
/// Factory reset the current card so long as it does not match the last-used backend.
fn factory_reset_current_card(
seen_cards: &mut HashSet<String>,
user_pin: &str,
admin_pin: &str,
cert: &Cert,
card_backend: PcscBackend,
) -> Result<()> {
let policy = openpgp::policy::NullPolicy::new();
let valid_cert = cert.with_policy(&policy, None)?;
let signing_key = valid_cert
.keys()
.for_signing()
.secret()
.next()
.expect("no signing key found");
let decryption_key = valid_cert
.keys()
.for_storage_encryption()
.secret()
.next()
.expect("no decryption key found");
let authentication_key = valid_cert
.keys()
.for_authentication()
.secret()
.next()
.expect("no authentication key found");
let mut card = Card::<Open>::new(card_backend)?;
let mut transaction = card.transaction()?;
let application_identifier = transaction.application_identifier()?.ident();
if seen_cards.contains(&application_identifier) {
// we were given the same card, error
panic!("Previously used card {application_identifier} was reused");
} else {
seen_cards.insert(application_identifier);
}
transaction.factory_reset()?;
let mut admin = transaction.to_admin_card("12345678")?;
admin.upload_key(signing_key, KeyType::Signing, None)?;
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
transaction.change_user_pin("123456", user_pin)?;
transaction.change_admin_pin("12345678", admin_pin)?;
Ok(())
}
fn cross_sign_certs(certs: &mut [Cert]) -> Result<(), Box<dyn std::error::Error>> {
let policy = StandardPolicy::new();
fn generate_shard_secret(
threshold: u8,
max: u8,
keys_per_shard: u8,
output_file: &Option<PathBuf>,
) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mut pm = default_terminal()?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout();
if output_file.is_none() {
assert!(
!stdout.is_terminal(),
"not printing shard to terminal, redirect output"
);
}
#[allow(clippy::unnecessary_to_owned)]
for signing_cert in certs.to_vec() {
let mut certify_key = signing_cert
.with_policy(&policy, None)?
.keys()
.unencrypted_secret()
.for_certification()
.next()
.expect("certify key unusable/not found")
.key()
.clone()
.into_keypair()?;
for signable_cert in certs.iter_mut() {
let sb = SignatureBuilder::new(SignatureType::GenericCertification);
let userid = signable_cert
.userids()
.next()
.expect("a signable user ID is necessary to create web of trust");
let signature = sb.sign_userid_binding(
&mut certify_key,
signable_cert.primary_key().key(),
&userid,
let user_pin_validator = SecurePinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
for index in 0..max {
let cert = derive_key(seed, index)?;
for i in 0..keys_per_shard {
pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
(i as u16) + 1,
(index as u16) + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c;
}
pm.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
};
let user_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
factory_reset_current_card(
&mut seen_cards,
user_pin.trim(),
admin_pin.trim(),
&cert,
card_backend,
)?;
let changed;
(*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?;
assert!(
changed,
"OpenPGP certificate was unchanged after inserting packets"
);
}
certs.push(cert);
}
let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = output_file {
let output = File::create(output_file)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
opgp.shard_and_encrypt(
threshold,
certs.len() as u8,
&seed,
&certs[..],
std::io::stdout(),
)?;
}
Ok(())
}
impl GenerateShardSecret {
fn handle(&self) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mut pm = default_handler()?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout();
if self.output.is_none() {
assert!(
!stdout.is_terminal(),
"not printing shard to terminal, redirect output"
);
}
fn bottoms_up(key_discovery: &Path, threshold: u8, output_shardfile: &Path, output_cert: &Path, user_id: &str,) -> Result<()> {
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mnemonic = Mnemonic::from_nonstandard_bytes(entropy);
// TODO: make this return const size, since is hash based
let seed = mnemonic.generate_seed(None);
let user_pin_validator = SecurePinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
// TODO: should this allow for customizing the account index from 0? Potential for key reuse
// errors.
let path = DerivationPath::default()
.chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true)?)
.chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00\x00dr"), true)?)
.chain_push(DerivationIndex::new(0, true)?);
let subkeys = [
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = XPrv::new(VariableLengthSeed::new(&seed))
.expect("could not construct master key from seed")
.derive_path(&path)?;
let userid = UserID::from(user_id);
for index in 0..self.max {
let cert = derive_key(seed, index)?;
for i in 0..self.keys_per_shard {
pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
(i as u16) + 1,
(index as u16) + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c;
}
pm.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
};
let user_pin = prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
factory_reset_current_card(
&mut |application_identifier| {
if seen_cards.contains(&application_identifier) {
// we were given the same card, error
// we're gonna panic because this is a significant error
panic!("Previously used card {application_identifier} was reused");
} else {
seen_cards.insert(application_identifier);
true
}
},
user_pin.trim(),
admin_pin.trim(),
&cert,
&openpgp::policy::NullPolicy::new(),
card_backend,
)?;
}
certs.push(cert);
}
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
let certfile = File::create(output_cert)?;
let mut w = Writer::new(certfile, Kind::PublicKey)?;
cert.serialize(&mut w)?;
w.finalize()?;
cross_sign_certs(&mut certs)?;
let opgp = OpenPGP::<DefaultTerminal>::new();
let certs = OpenPGP::<DefaultTerminal>::discover_certs(key_discovery)?;
let opgp = OpenPGP;
let shardfile = File::create(output_shardfile)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &entropy, &certs[..], shardfile)?;
if let Some(output_file) = self.output.as_ref() {
let output = File::create(output_file)?;
opgp.shard_and_encrypt(self.threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
opgp.shard_and_encrypt(
self.threshold,
certs.len() as u8,
&seed,
&certs[..],
std::io::stdout(),
)?;
}
if let Some(cert_output_file) = self.cert_output.as_ref() {
let output = File::create(cert_output_file)?;
let mut writer = Writer::new(output, Kind::PublicKey)?;
for cert in certs {
cert.serialize(&mut writer)?;
}
writer.finalize()?;
}
Ok(())
}
Ok(())
}
impl BottomsUp {
#[derive(Subcommand, Clone, Debug)]
pub enum WizardSubcommands {
/// Create a 256 bit secret and shard the secret to smart cards.
///
/// Smart cards will need to be plugged in periodically during the wizard, where they will be factory reset and
/// provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered with `keyfork recover shard` or
/// `keyfork recover remote-shard`. The share file will be printed to standard output.
GenerateShardSecret {
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// The maximum amount of shards.
#[arg(long)]
max: u8,
/// The amount of smart cards to provision per-shard.
#[arg(long, default_value = "1")]
keys_per_shard: u8,
/// The file to write the generated shard file to.
#[arg(long)]
output: Option<PathBuf>,
},
/// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates,
/// deriving the default OpenPGP certificate for the secret.
///
/// This command was purpose-built for DEFCON and is not intended to be used normally, as it
/// implies keys used for sharding have been generated by a custom source.
BottomsUp {
/// The location of OpenPGP certificates to use when sharding.
key_discovery: PathBuf,
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// The file to write the generated shard file to.
#[arg(long)]
output_shardfile: PathBuf,
/// The file to write the generated OpenPGP certificate to.
#[arg(long)]
output_cert: PathBuf,
/// The User ID for the generated OpenPGP certificate.
#[arg(long, default_value = "Disaster Recovery")]
user_id: String,
},
}
impl WizardSubcommands {
fn handle(&self) -> Result<()> {
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mnemonic = Mnemonic::from_array(entropy);
let seed = mnemonic.generate_seed(None);
// TODO: should this allow for customizing the account index from 0? Potential for key reuse
// errors.
let path = paths::OPENPGP_DISASTER_RECOVERY
.clone()
.chain_push(DerivationIndex::new(0, true)?);
let subkeys = [
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = XPrv::new(seed)
.expect("could not construct master key from seed")
.derive_path(&path)?;
let userid = UserID::from(self.user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
let certfile = File::create(&self.output_cert)?;
let mut w = Writer::new(certfile, Kind::PublicKey)?;
cert.serialize(&mut w)?;
w.finalize()?;
let opgp = OpenPGP;
let certs = OpenPGP::discover_certs(&self.key_discovery)?;
let shardfile = File::create(&self.output_shardfile)?;
opgp.shard_and_encrypt(
self.threshold,
certs.len() as u8,
&entropy,
&certs[..],
shardfile,
)?;
Ok(())
match self {
WizardSubcommands::GenerateShardSecret {
threshold,
max,
keys_per_shard,
output,
} => generate_shard_secret(*threshold, *max, *keys_per_shard, output),
WizardSubcommands::BottomsUp {
key_discovery,
threshold,
output_shardfile,
output_cert,
user_id,
} => bottoms_up(key_discovery, *threshold, output_shardfile, output_cert, user_id),
}
}
}

View File

@ -2,19 +2,20 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize)]
pub struct Mnemonic {
pub hash: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize)]
pub struct Provisioner {
pub name: String,
pub account: u32,
pub identifier: String,
pub metadata: Option<HashMap<String, String>>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize)]
pub struct Config {
pub mnemonic: Mnemonic,
pub provisioner: Vec<Provisioner>,

View File

@ -10,8 +10,6 @@ use keyfork_bin::{Bin, ClosureBin};
mod cli;
mod config;
pub mod clap_ext;
mod openpgp_card;
fn main() -> ExitCode {
let bin = ClosureBin::new(|| {

View File

@ -1,54 +0,0 @@
use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card, types::TouchPolicy};
use keyfork_derive_openpgp::openpgp::{Cert, policy::Policy};
/// Factory reset the current card so long as it does not match the last-used backend.
///
/// The return value of `false` means the filter was matched, whereas `true` means it was
/// successfully provisioned.
pub fn factory_reset_current_card(
card_filter: &mut dyn FnMut(String) -> bool,
user_pin: &str,
admin_pin: &str,
cert: &Cert,
policy: &dyn Policy,
card_backend: PcscBackend,
) -> Result<bool, Box<dyn std::error::Error>> {
let valid_cert = cert.with_policy(policy, None)?;
let signing_key = valid_cert
.keys()
.for_signing()
.secret()
.next()
.expect("no signing key found");
let decryption_key = valid_cert
.keys()
.for_storage_encryption()
.secret()
.next()
.expect("no decryption key found");
let authentication_key = valid_cert
.keys()
.for_authentication()
.secret()
.next()
.expect("no authentication key found");
let mut card = Card::<Open>::new(card_backend)?;
let mut transaction = card.transaction()?;
let application_identifier = transaction.application_identifier()?.ident();
if !card_filter(application_identifier) {
return Ok(false);
}
transaction.factory_reset()?;
let mut admin = transaction.to_admin_card("12345678")?;
admin.upload_key(signing_key, KeyType::Signing, None)?;
admin.set_touch_policy(KeyType::Signing, TouchPolicy::On)?;
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
admin.set_touch_policy(KeyType::Decryption, TouchPolicy::On)?;
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
admin.set_touch_policy(KeyType::Authentication, TouchPolicy::On)?;
transaction.change_user_pin("123456", user_pin)?;
transaction.change_admin_pin("12345678", admin_pin)?;
Ok(true)
}

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-qrcode"
version = "0.1.2"
version = "0.1.1"
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
@ -14,9 +14,9 @@ decode-backend-rqrr = ["dep:rqrr"]
decode-backend-zbar = ["dep:keyfork-zbar"]
[dependencies]
keyfork-bug = { workspace = true }
keyfork-zbar = { workspace = true, optional = true }
image = { workspace = true, default-features = false, features = ["jpeg"] }
rqrr = { version = "0.7.0", optional = true }
thiserror = { workspace = true }
v4l = { workspace = true }
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true, registry = "distrust" }
image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
rqrr = { version = "0.6.0", optional = true }
thiserror = "1.0.56"
v4l = "0.14.0"

View File

@ -1,4 +1,4 @@
#![allow(missing_docs)]
//!
use std::time::Duration;

View File

@ -2,7 +2,7 @@
use keyfork_bug as bug;
use image::ImageReader;
use image::io::Reader as ImageReader;
use std::{
io::{Cursor, Write},
time::{Duration, Instant},
@ -103,11 +103,6 @@ pub fn qrencode(
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
/// Continuously scan the `index`-th camera for a QR code.
///
/// # Errors
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
#[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
@ -138,11 +133,6 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
}
/// Continuously scan the `index`-th camera for a QR code.
///
/// # Errors
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
#[cfg(feature = "decode-backend-zbar")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-zbar"
version = "0.1.1"
version = "0.1.0"
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
@ -13,9 +13,9 @@ bin = ["image"]
image = ["dep:image"]
[dependencies]
keyfork-zbar-sys = { workspace = true }
image = { workspace = true, default-features = false, optional = true }
thiserror = { workspace = true }
keyfork-zbar-sys = { version = "0.1.0", path = "../keyfork-zbar-sys", registry = "distrust" }
image = { version = "0.24.7", default-features = false, optional = true }
thiserror = "1.0.56"
[dev-dependencies]
v4l = { workspace = true }
v4l = "0.14.0"

View File

@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.decode()?,
);
if let Some(symbol) = scanner.scan_image(&image).first() {
if let Some(symbol) = scanner.scan_image(&image).get(0) {
println!("{}", String::from_utf8_lossy(symbol.data()));
return Ok(());
}

View File

@ -1,4 +1,4 @@
//! A Symbol represents some form of encoded data.
//!
use super::sys;

View File

@ -1,11 +0,0 @@
[package]
name = "keyfork-tests"
version = "0.1.0"
edition = "2021"
publish = false
license = "MIT"
[dependencies]
assert_cmd = "2.0.16"
keyforkd = { workspace = true, features = ["default"] }
sequoia-openpgp = { workspace = true, features = ["crypto-nettle"] }

View File

@ -1 +0,0 @@
mod openpgp;

View File

@ -1,58 +0,0 @@
use sequoia_openpgp as openpgp;
use assert_cmd::Command;
use openpgp::{
parse::{PacketParser, Parse},
policy::StandardPolicy,
types::KeyFlags,
Cert,
};
const KEYFORK_BIN: &str = "keyfork";
#[test]
fn test() {
let policy = StandardPolicy::new();
let command_output = Command::cargo_bin(KEYFORK_BIN)
.unwrap()
.args([
"derive",
"openpgp",
"Ryan Heywood (RyanSquared) <ryan@distrust.co>",
])
.assert()
.success();
let packets = PacketParser::from_bytes(&command_output.get_output().stdout).unwrap();
let cert = Cert::try_from(packets).unwrap();
// assert the cert contains _any_ secret key data
assert!(
cert.is_tsk(),
"exported key should contain secret key data, indicated by the key being a TSK"
);
// assert the correct keys were added in the correct order
let mut key_formats = std::collections::HashSet::from([
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
]);
let valid_cert = cert.with_policy(&policy, None).unwrap();
for key in valid_cert.keys() {
let flags = key.key_flags().unwrap();
assert!(
key_formats.remove(&flags),
"could not find key flag set: {flags:?}"
);
key.alive().expect("is live after being generated");
key.parts_into_secret().expect("has secret keys");
}
if !key_formats.is_empty() {
panic!("remaining key formats: {key_formats:?}");
}
}

View File

@ -1 +0,0 @@
mod derive;

View File

@ -1,2 +0,0 @@
#[cfg(test)]
mod keyfork;

View File

@ -9,4 +9,4 @@ license = "MIT"
[dependencies]
[dev-dependencies]
anyhow = { workspace = true }
anyhow = "1.0.79"

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-crossterm"
version = "0.27.2"
version = "0.27.1"
# authors = ["T. Post"]
authors = ["Ryan Heywood <ryan@distrust.co>"]
description = "A crossplatform terminal library for manipulating terminals."
@ -55,16 +55,16 @@ crossterm_winapi = { version = "0.9.1", optional = true }
libc = "0.2"
signal-hook = { version = "0.3.17", optional = true }
filedescriptor = { version = "0.8", optional = true }
mio = { version = "1.0", features = ["os-poll"], optional = true }
signal-hook-mio = { version = "0.2.3", features = ["support-v1_0"], optional = true }
mio = { version = "0.8", features = ["os-poll"], optional = true }
signal-hook-mio = { version = "0.2.3", features = ["support-v0_8"], optional = true }
# Dev dependencies (examples, ...)
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
tokio = { version = "1.25", features = ["full"] }
futures = "0.3"
futures-timer = "3.0"
async-std = "1.12"
serde_json = { workspace = true }
serde_json = "1.0"
serial_test = "2.0.0"
# Examples

View File

@ -1,4 +1,4 @@
#![allow(missing_docs)]
//!
use keyfork_crossterm::{
execute,

View File

@ -1,7 +1,7 @@
use std::{collections::VecDeque, io, time::Duration};
use mio::{unix::SourceFd, Events, Interest, Poll, Token};
use signal_hook_mio::v1_0::Signals;
use signal_hook_mio::v0_8::Signals;
#[cfg(feature = "event-stream")]
use crate::event::sys::Waker;

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-entropy"
version = "0.1.2"
version = "0.1.1"
edition = "2021"
license = "MIT"
@ -11,5 +11,5 @@ default = ["bin"]
bin = ["smex"]
[dependencies]
keyfork-bug = { workspace = true }
smex = { workspace = true, optional = true }
keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
smex = { version = "0.1.0", path = "../smex", optional = true, registry = "distrust" }

View File

@ -1,4 +1,4 @@
//! Generate entropy of a given size, encoded as hex.
//!
fn main() -> Result<(), Box<dyn std::error::Error>> {
let bit_size: usize = std::env::args()

View File

@ -12,13 +12,13 @@ async = ["dep:tokio"]
[dependencies]
# Included in Rust
sha2 = { workspace = true }
sha2 = "0.10.7"
# Personally audited
thiserror = { workspace = true }
thiserror = "1.0.47"
# Optional, not personally audited
tokio = { workspace = true, optional = true, features = ["io-util"] }
tokio = { version = "1.32.0", optional = true, features = ["io-util"] }
[dev-dependencies]
insta = "1.31.0"

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-mnemonic"
version = "0.4.1"
name = "keyfork-mnemonic-util"
version = "0.3.0"
description = "Utilities to generate and manage seeds based on BIP-0039 mnemonics."
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
@ -11,14 +11,14 @@ default = ["bin"]
bin = ["smex"]
[dependencies]
smex = { workspace = true, optional = true }
keyfork-bug = { workspace = true }
smex = { version = "0.1.0", path = "../smex", optional = true, registry = "distrust" }
keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
sha2 = { workspace = true }
hmac = { workspace = true }
sha2 = "0.10.7"
hmac = "0.12.1"
pbkdf2 = "0.12.2"
[dev-dependencies]
bip39 = "2.0.0"
hex = "0.4.3"
serde_json = { workspace = true }
serde_json = "1.0.105"

View File

@ -1,6 +1,6 @@
//! Generate a mnemonic from hex-encoded input.
//!
use keyfork_mnemonic::Mnemonic;
use keyfork_mnemonic_util::Mnemonic;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input = std::io::stdin();

View File

@ -3,17 +3,17 @@
//! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase:
//!
//! ```rust
//! use keyfork_mnemonic::Mnemonic;
//! use keyfork_mnemonic_util::Mnemonic;
//! let data = b"Hello, world! I am a mnemonic :)";
//! assert_eq!(data.len(), 32);
//! let mnemonic = Mnemonic::try_from_slice(data).unwrap();
//! let mnemonic = Mnemonic::from_bytes(data).unwrap();
//! println!("Our mnemonic is: {mnemonic}");
//! ```
//!
//! A mnemonic can also be parsed from a string:
//!
//! ```rust
//! use keyfork_mnemonic::Mnemonic;
//! use keyfork_mnemonic_util::Mnemonic;
//! use std::str::FromStr;
//!
//! let data = b"Hello, world! I am a mnemonic :)";
@ -28,7 +28,7 @@
//! verified to be safe:
//!
//! ```rust
//! use keyfork_mnemonic::Mnemonic;
//! use keyfork_mnemonic_util::Mnemonic;
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
//! let mnemonic_text = mnemonic.to_string();
@ -37,7 +37,7 @@
//! If given an invalid length, undefined behavior may follow, or code may panic.
//!
//! ```rust,should_panic
//! use keyfork_mnemonic::Mnemonic;
//! use keyfork_mnemonic_util::Mnemonic;
//! use std::str::FromStr;
//!
//! // NOTE: Data is of invalid length, 31
@ -268,11 +268,11 @@ where
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic::Mnemonic;
/// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = Mnemonic::try_from_slice(data.as_slice()).unwrap();
/// let mnemonic = Mnemonic::from_bytes(data.as_slice()).unwrap();
/// ```
pub fn try_from_slice(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
let bit_count = bytes.len() * 8;
if bit_count % 32 != 0 {
@ -290,23 +290,23 @@ where
/// of a factor of 4, up to 1024 bytes.
///
/// ```rust
/// use keyfork_mnemonic::Mnemonic;
/// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"hello world!";
/// let mnemonic = Mnemonic::from_array(*data);
/// let mnemonic = Mnemonic::from_nonstandard_bytes(*data);
/// ```
///
/// If an invalid size is requested, the code will fail to compile:
///
/// ```rust,compile_fail
/// use keyfork_mnemonic::Mnemonic;
/// let mnemonic = Mnemonic::from_array([0u8; 53]);
/// use keyfork_mnemonic_util::Mnemonic;
/// let mnemonic = Mnemonic::from_nonstandard_bytes([0u8; 53]);
/// ```
///
/// ```rust,compile_fail
/// use keyfork_mnemonic::Mnemonic;
/// let mnemonic = Mnemonic::from_array([0u8; 1024 + 4]);
/// use keyfork_mnemonic_util::Mnemonic;
/// let mnemonic = Mnemonic::from_nonstandard_bytes([0u8; 1024 + 4]);
/// ```
pub fn from_array<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
pub fn from_nonstandard_bytes<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
#[allow(clippy::let_unit_value)]
{
let () = AssertValidMnemonicSize::<N>::OK_CHUNKS;
@ -315,6 +315,16 @@ where
Self::from_raw_bytes(&bytes)
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::from_bytes"]
pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::from_bytes(bytes)
}
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
///
@ -322,12 +332,12 @@ where
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`. If the assumption is incorrect, code may panic. The
/// [`MnemonicBase::from_array`] function may be used to generate entropy if the length of the
/// data is known at compile-time.
/// [`MnemonicBase::from_nonstandard_bytes`] function may be used to generate entropy if the
/// length of the data is known at compile-time.
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic::Mnemonic;
/// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string();
@ -336,7 +346,7 @@ where
/// If given an invalid length, undefined behavior may follow, or code may panic.
///
/// ```rust,should_panic
/// use keyfork_mnemonic::Mnemonic;
/// use keyfork_mnemonic_util::Mnemonic;
/// use std::str::FromStr;
///
/// // NOTE: Data is of invalid length, 31
@ -373,31 +383,16 @@ where
&self.data
}
/// A view to internal representation of the decoded data.
pub fn as_slice(&self) -> &[u8] {
&self.data
}
/// A clone of the internal representation of the decoded data.
pub fn to_bytes(&self) -> Vec<u8> {
self.data.to_vec()
}
/// A clone of the internal representation of the decoded data.
pub fn to_vec(&self) -> Vec<u8> {
self.data.to_vec()
}
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_bytes(self) -> Vec<u8> {
self.data
}
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_vec(self) -> Vec<u8> {
self.data
}
/// Clone the existing data.
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> {
@ -413,7 +408,7 @@ where
&self,
passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> {
Ok(self.generate_seed(passphrase).to_vec())
Ok(self.generate_seed(passphrase))
}
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
@ -421,7 +416,8 @@ where
/// # Panics
/// The function may panic if the HmacSha512 function returns an error. The only error the
/// HmacSha512 function should return is an invalid length, which should not be possible.
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> [u8; 64] {
///
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> Vec<u8> {
let passphrase = passphrase.into();
let mut seed = [0u8; 64];
@ -429,7 +425,7 @@ where
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.expect(bug!("HmacSha512 InvalidLength should be infallible"));
seed
seed.to_vec()
}
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
@ -466,39 +462,6 @@ where
}
}
impl<W> MnemonicBase<W>
where
W: Wordlist,
{
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::try_from_slice"]
pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::try_from_slice(bytes)
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::try_from_slice"]
pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::try_from_slice(bytes)
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data may be of a size
/// of a factor of 4, up to 1024 bytes.
///
#[deprecated = "Use Mnemonic::from_array"]
pub fn from_nonstandard_bytes<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
MnemonicBase::from_array(bytes)
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, fs::File, io::Read};
@ -515,7 +478,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let mnemonic = super::Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let mnemonic = super::Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let new_entropy = mnemonic.as_bytes();
assert_eq!(new_entropy, entropy);
}
@ -531,7 +494,7 @@ mod tests {
};
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
let mnemonic = Mnemonic::try_from_slice(&hex).unwrap();
let mnemonic = Mnemonic::from_bytes(&hex).unwrap();
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
}
@ -542,7 +505,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let my_mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let my_mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
@ -566,7 +529,7 @@ mod tests {
for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let words = mnemonic.words();
hs.clear();
hs.extend(words);
@ -597,7 +560,7 @@ mod tests {
let mut entropy = [0u8; 1024];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_array(entropy);
let mnemonic = Mnemonic::from_nonstandard_bytes(entropy);
let words = mnemonic.words();
assert_eq!(words.len(), 768);
}

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-prompt"
version = "0.2.1"
version = "0.1.1"
description = "Prompt management utilities for Keyfork"
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
@ -10,10 +10,10 @@ license = "MIT"
[features]
default = ["mnemonic"]
mnemonic = ["keyfork-mnemonic"]
mnemonic = ["keyfork-mnemonic-util"]
[dependencies]
keyfork-bug = { workspace = true }
keyfork-crossterm = { workspace = true, default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] }
keyfork-mnemonic = { workspace = true, optional = true }
thiserror = { workspace = true }
keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
keyfork-crossterm = { version = "0.27.1", path = "../keyfork-crossterm", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"], registry = "distrust" }
keyfork-mnemonic-util = { version = "0.3.0", path = "../keyfork-mnemonic-util", optional = true, registry = "distrust" }
thiserror = "1.0.51"

View File

@ -1,15 +1,36 @@
#![allow(missing_docs)]
//!
use std::io::{stdin, stdout};
use keyfork_prompt::{
Message,
default_handler,
MaybeIdentifier, PromptHandler, Terminal,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut handler = default_handler()?;
#[derive(PartialEq, Eq, Debug)]
pub enum Example {
RetryQR,
UseMnemonic,
}
let output = handler.prompt_input("Test message: ")?;
handler.prompt_message(Message::Text(format!("Result: {output}")))?;
impl MaybeIdentifier for Example {}
impl std::fmt::Display for Example {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Example::RetryQR => f.write_str("Retry QR Code"),
Example::UseMnemonic => f.write_str("Use Mnemonic"),
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut mgr = Terminal::new(stdin(), stdout())?;
let choice = mgr.prompt_choice(
"Unable to detect QR code.",
&[Example::RetryQR, Example::UseMnemonic],
)?;
dbg!(choice);
Ok(())
}

View File

@ -1,120 +0,0 @@
//! A headless prompt handler.
//!
//! This prompt handler uses the program's standard input and output to read inputs. It is not
//! directly intended to be machine-readable, but can be used for scriptable automation in a
//! fashion similar to a terminal handler.
use std::io::{IsTerminal, Write};
use crate::{BoxResult, Error, Message, PromptHandler, Result};
/// A headless prompt handler, usable in situations when a terminal might not be available, or for
/// scripting purposes where manual input from a terminal is not desirable.
pub struct Headless {
stdin: std::io::Stdin,
stderr: std::io::Stderr,
}
impl Headless {
/// Create a new [`Headless`] prompt handler.
#[allow(clippy::missing_errors_doc, clippy::new_without_default)]
pub fn new() -> Self {
Self {
stdin: std::io::stdin(),
stderr: std::io::stderr(),
}
}
}
impl PromptHandler for Headless {
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
let mut line = String::new();
self.stdin.read_line(&mut line)?;
Ok(line)
}
fn prompt_wordlist(&mut self, prompt: &str, _wordlist: &[&str]) -> Result<String> {
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
let mut line = String::new();
self.stdin.read_line(&mut line)?;
Ok(line)
}
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
// Temporarily perform an IOCTL to disable printed output.
if self.stdin.is_terminal() {
eprintln!("WARNING: Headless terminal mode may leak passwords!");
}
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
let mut line = String::new();
self.stdin.read_line(&mut line)?;
Ok(line)
}
fn prompt_message(&mut self, prompt: Message) -> Result<()> {
match prompt {
Message::Text(s) => {
self.stderr.write_all(s.as_bytes())?;
self.stderr.flush()?;
}
Message::Data(s) => {
self.stderr.write_all(s.as_bytes())?;
self.stderr.flush()?;
}
}
Ok(())
}
fn prompt_validated_wordlist(
&mut self,
prompt: &str,
retries: u8,
_wordlist: &[&str],
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<()> {
let mut line = String::new();
let mut last_error = String::new();
for _ in 0..retries {
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
self.stderr.flush()?;
self.stdin.read_line(&mut line)?;
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
last_error = e.to_string();
self.stderr.write_all(e.to_string().as_bytes())?;
self.stderr.flush()?;
} else {
return Ok(());
}
}
Err(Error::Validation(retries, last_error))
}
fn prompt_validated_passphrase(
&mut self,
prompt: &str,
retries: u8,
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<()> {
let mut line = String::new();
let mut last_error = String::new();
for _ in 0..retries {
self.stderr.write_all(prompt.as_bytes())?;
self.stderr.flush()?;
self.stdin.read_line(&mut line)?;
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
last_error = e.to_string();
self.stderr.write_all(e.to_string().as_bytes())?;
self.stderr.write_all(b"\n")?;
self.stderr.flush()?;
} else {
return Ok(());
}
}
Err(Error::Validation(retries, last_error))
}
}

View File

@ -1,37 +1,15 @@
//! Prompt display and interaction management.
use std::io::IsTerminal;
use std::borrow::Borrow;
#[cfg(feature = "mnemonic")]
use keyfork_mnemonic::Wordlist;
use keyfork_mnemonic_util::Wordlist;
pub mod headless;
///
pub mod terminal;
pub mod validators;
pub use headless::Headless;
pub use terminal::{default_terminal, DefaultTerminal, Terminal};
/// An error occurred in the process of loading a default handler.
#[derive(thiserror::Error, Debug)]
pub enum DefaultHandlerError {
/// An invalid handler was loaded.
#[error("An invalid handler was loaded: {handler} ({error})")]
InvalidHandler {
/// The handle that caused an error.
handler: String,
/// The error that occurred.
error: String,
},
/// An unknown handler was requested.
#[error("An unknown handler was requested: {handler}")]
UnknownHandler {
/// The requested, but unknown, handler.
handler: String,
},
}
/// An error occurred while displaying a prompt.
#[derive(thiserror::Error, Debug)]
pub enum Error {
@ -64,8 +42,11 @@ pub enum Message {
Data(String),
}
#[doc(hidden)]
pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>;
pub trait MaybeIdentifier {
fn identifier(&self) -> Option<char> {
None
}
}
/// A trait to allow displaying prompts and accepting input.
pub trait PromptHandler {
@ -82,7 +63,27 @@ pub trait PromptHandler {
/// # Errors
/// The method may return an error if the message was not able to be displayed or if the input
/// could not be read.
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result<String>;
#[cfg(feature = "mnemonic")]
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
where
X: Wordlist;
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser. A language must be specified
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>
where
X: Wordlist;
/// Prompt the user for a passphrase, which is hidden while typing.
///
@ -91,140 +92,36 @@ pub trait PromptHandler {
/// could not be read.
fn prompt_passphrase(&mut self, prompt: &str) -> Result<String>;
/// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase
/// using a provided parser function, returning the type from the parser.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_passphrase<V>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>;
/// Prompt the user to select a choice between multiple options.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if a choice
/// could not be received.
fn prompt_choice<'a, T>(
&mut self,
prompt: &str,
choices: &'a [T],
) -> Result<&'a T, Error>
where
T: std::fmt::Display + PartialEq + MaybeIdentifier;
/// Prompt the user with a [`Message`].
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if an error
/// occurred while waiting for the user to dismiss the message.
fn prompt_message(&mut self, prompt: Message) -> Result<()>;
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser. A language must be specified
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
///
/// This method MUST NOT be used directly. Instead, use
/// [`prompt_validated_wordlist`].
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_wordlist(
&mut self,
prompt: &str,
retries: u8,
wordlist: &[&str],
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<(), Error>;
/// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase
/// using a provided parser function, returning the type from the parser.
///
/// This method MUST NOT be used directly. Instead, use
/// [`prompt_validated_wordlist`].
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_passphrase(
&mut self,
prompt: &str,
retries: u8,
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<(), Error>;
}
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser. A language must be specified
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")]
#[allow(clippy::missing_panics_doc)]
pub fn prompt_validated_wordlist<X, V>(
handler: &mut dyn PromptHandler,
prompt: &str,
retries: u8,
validator_fn: &dyn Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>
where
X: Wordlist,
{
let wordlist = X::get_singleton();
let words = wordlist.to_str_array();
let mut opt: Option<V> = None;
handler.prompt_validated_wordlist(prompt, retries, &words, &mut |string| {
opt = Some(validator_fn(string)?);
Ok(())
})?;
Ok(opt.unwrap())
}
/// Prompt the user for a passphrase, which is hidden while typing, and validate the passphrase
/// using a provided parser function, returning the type from the parser.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[allow(clippy::missing_panics_doc)]
pub fn prompt_validated_passphrase<V>(
handler: &mut dyn PromptHandler,
prompt: &str,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error> {
let mut opt: Option<V> = None;
handler.prompt_validated_passphrase(prompt, retries, &mut |string| {
opt = Some(validator_fn(string)?);
Ok(())
})?;
Ok(opt.unwrap())
}
/// Get a Prompt Handler that is most suitable for the given environment.
///
/// The following handlers will be used based on the `KEYFORK_PROMPT_TYPE` variable:
/// * `KEYFORK_PROMPT_TYPE=terminal`: [`DefaultTerminal`]
/// * `KEYFORK_PROMPT_TYPE=headless`: [`Headless`]
///
/// Otherwise, the following heuristics are followed:
/// * [`std::io::IsTerminal::is_terminal`]: [`DefaultTerminal`]
/// * default: [`Headless`]
///
/// # Errors
///
/// The function will return an error if a specific handler was requested but could not be
/// constructed.
pub fn default_handler() -> Result<Box<dyn PromptHandler>, DefaultHandlerError> {
if let Some((_, value)) = std::env::vars().find(|(k, _)| k == "KEYFORK_PROMPT_TYPE") {
match value.as_str() {
"terminal" => match default_terminal() {
Ok(terminal) => return Ok(Box::new(terminal)),
Err(e) => {
return Err(DefaultHandlerError::InvalidHandler {
handler: value,
error: e.to_string(),
})
}
},
"headless" => {
return Ok(Box::new(Headless::new()));
}
_ => {
return Err(DefaultHandlerError::UnknownHandler { handler: value });
}
}
}
// we can revert stdin to a readable input by using raw mode, but we can't do the more
// significant operations if we don't have access to a terminal stderr
if std::io::stderr().is_terminal() {
// because this is a "guessed" handler, let's take the nice route and not error, just skip.
if let Ok(terminal) = default_terminal() {
return Ok(Box::new(terminal));
}
}
Ok(Box::new(Headless::new()))
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()>;
}

View File

@ -1,9 +1,3 @@
//! A terminal prompt handler.
//!
//! This prompt handler uses a raw terminal device to read inputs and uses ANSI escape codes to
//! provide formatting for prompts. Because of these reasons, it is not intended to be
//! machine-readable.
use std::{
borrow::Borrow,
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
@ -21,7 +15,7 @@ use keyfork_crossterm::{
use keyfork_bug::bug;
use crate::{BoxResult, Error, Message, PromptHandler};
use crate::{Error, MaybeIdentifier, Message, PromptHandler, Wordlist};
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -128,16 +122,12 @@ where
W: Write + AsRawFd,
{
fn drop(&mut self) {
self.write
.execute(cursor::Show)
.expect(bug!("can't enable cursor blinking"));
self.write
.execute(DisableBracketedPaste)
.expect(bug!("can't restore bracketed paste"));
self.write
.queue(terminal::Clear(terminal::ClearType::All))
.expect(bug!("can't clear screen"))
.queue(cursor::MoveTo(0, 0))
.expect(bug!("can't move to origin"))
.flush()
.expect(bug!("can't execute clear+move"));
self.write
.execute(LeaveAlternateScreen)
.expect(bug!("can't leave alternate screen"));
@ -185,14 +175,12 @@ where
W: Write + AsRawFd + Sized,
{
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
let mut terminal = self.lock().alternate_screen()?;
terminal
.queue(terminal::Clear(terminal::ClearType::All))?
.queue(cursor::MoveTo(0, 0))?;
let mut lines = prompt.lines().peekable();
let mut prefix_length = 0;
while let Some(line) = lines.next() {
prefix_length = line.len();
terminal.queue(Print(line))?;
if lines.peek().is_some() {
terminal
@ -202,82 +190,28 @@ where
}
terminal.flush()?;
let (mut cols, mut _rows) = terminal.size()?;
let mut input = String::new();
loop {
let input_len = input.len();
match read()? {
Event::Resize(new_cols, new_rows) => {
cols = new_cols;
_rows = new_rows;
}
Event::Key(k) => match k.code {
KeyCode::Enter => {
break;
}
KeyCode::Backspace => {
if input.pop().is_some() && prefix_length + input_len < cols as usize {
terminal
.queue(cursor::MoveLeft(1))?
.queue(Print(" "))?
.queue(cursor::MoveLeft(1))?
.flush()?;
}
}
KeyCode::Char('w') if k.modifiers.contains(KeyModifiers::CONTROL) => {
let mut has_deleted_text = true;
while input.pop().is_some_and(char::is_whitespace) {
has_deleted_text = false;
}
while input.pop().is_some_and(|c| !c.is_whitespace()) {
has_deleted_text = true;
}
if !input.is_empty() && has_deleted_text {
input.push(' ');
}
}
KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => {
return Err(Error::CtrlC);
}
KeyCode::Char(c) => {
input.push(c);
}
_ => (),
},
_ => (),
}
let printable_start = if prefix_length + input.len() < cols as usize {
0
} else {
let printable_space = (cols as usize) - prefix_length;
input.len() - (printable_space - 1)
};
terminal
.queue(cursor::MoveToColumn(prefix_length as u16))?
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
.queue(Print(&input[printable_start..]))?
.flush()?;
}
Ok(input)
let mut line = String::new();
terminal.read.read_line(&mut line)?;
Ok(line)
}
fn prompt_validated_wordlist(
#[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>(
&mut self,
prompt: &str,
retries: u8,
wordlist: &[&str],
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<(), Error> {
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>
where
X: Wordlist,
{
let mut last_error = None;
for _ in 0..retries {
let s = self.prompt_wordlist(prompt, wordlist)?;
let s = self.prompt_wordlist::<X>(prompt)?;
match validator_fn(s) {
Ok(v) => return Ok(v),
Err(e) => {
self.prompt_message(Message::Text(format!("Error validating wordlist: {e}")))?;
self.prompt_message(&Message::Text(format!("Error validating wordlist: {e}")))?;
let _ = last_error.insert(e);
}
}
@ -290,8 +224,15 @@ where
))
}
#[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)]
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result<String> {
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
where
X: Wordlist,
{
let wordlist = X::get_singleton();
let words = wordlist.to_str_array();
let mut terminal = self
.lock()
.alternate_screen()?
@ -361,7 +302,7 @@ where
let word = input.split_whitespace().next_back().map(ToOwned::to_owned);
if let Some(steel_word) = word {
if steel_word.len() >= 4 {
for word in wordlist.iter().filter(|word| word.len() >= 4) {
for word in words.iter().filter(|word| word.len() >= 4) {
if word[..4] == steel_word {
input.push_str(&word[4..]);
input.push(' ');
@ -407,7 +348,7 @@ where
let mut iter = printable_input.split_whitespace().peekable();
while let Some(word) = iter.next() {
if wordlist.contains(&word) {
if words.contains(&word) {
terminal.queue(PrintStyledContent(word.green()))?;
} else {
terminal.queue(PrintStyledContent(word.red()))?;
@ -428,19 +369,19 @@ where
Ok(input)
}
fn prompt_validated_passphrase(
fn prompt_validated_passphrase<V>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: &mut dyn FnMut(String) -> BoxResult,
) -> Result<(), Error> {
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error> {
let mut last_error = None;
for _ in 0..retries {
let s = self.prompt_passphrase(prompt)?;
match validator_fn(s) {
Ok(v) => return Ok(v),
Err(e) => {
self.prompt_message(Message::Text(format!(
self.prompt_message(&Message::Text(format!(
"Error validating passphrase: {e}"
)))?;
let _ = last_error.insert(e);
@ -517,7 +458,78 @@ where
Ok(passphrase)
}
fn prompt_message(&mut self, prompt: Message) -> Result<()> {
fn prompt_choice<'a, T>(&mut self, prompt: &str, choices: &'a [T]) -> Result<&'a T, Error>
where
T: std::fmt::Display + PartialEq + MaybeIdentifier,
{
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
terminal
.queue(terminal::Clear(terminal::ClearType::All))?
.queue(cursor::MoveTo(0, 0))?
.queue(cursor::Hide)?;
for line in prompt.lines() {
terminal.queue(Print(line))?;
terminal
.queue(cursor::MoveDown(1))?
.queue(cursor::MoveToColumn(0))?;
}
terminal.flush()?;
let mut active_choice = 0;
let mut redraw = |active_choice| {
terminal.queue(cursor::MoveToColumn(0))?;
let mut iter = choices.iter().enumerate().peekable();
while let Some((i, choice)) = iter.next() {
// if active choice, flip foreground and background
// if active choice, wrap in []
// if not, wrap in spaces, to preserve spacing
if i == active_choice {
terminal.queue(PrintStyledContent(format!("[{choice}]").to_string().reverse()))?;
} else {
terminal.queue(Print(format!(" {choice} ").to_string()))?;
}
if iter.peek().is_some() {
terminal.queue(Print(" "))?;
}
}
terminal.flush()?;
Ok::<_, Error>(())
};
redraw(active_choice)?;
loop {
if let Event::Key(k) = read()? {
match k.code {
KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => {
return Err(Error::CtrlC);
}
KeyCode::Left => {
// prevent underflow
// if 0, max is 1, -1 is 0, no underflow
// if 1, max is 1, -1 is 0
// if 2 or higher, max is 2 or higher, -1 is fine
active_choice = std::cmp::max(1, active_choice) - 1;
}
KeyCode::Right => {
active_choice = std::cmp::min(choices.len() - 1, active_choice + 1);
}
KeyCode::Enter => {
return Ok(&choices[active_choice]);
}
_ => {}
}
}
redraw(active_choice)?;
}
}
fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
loop {

View File

@ -158,7 +158,7 @@ pub mod mnemonic {
use super::Validator;
use keyfork_bug::bug;
use keyfork_mnemonic::{Mnemonic, MnemonicFromStrError};
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
/// A mnemonic could not be validated from the given input.
#[derive(thiserror::Error, Debug)]

View File

@ -5,4 +5,4 @@ edition = "2021"
license = "MIT"
[dependencies]
smex = { workspace = true }
smex = { version = "0.1.0", path = "../smex", registry = "distrust" }

105
deny.toml
View File

@ -11,9 +11,6 @@
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
@ -25,7 +22,7 @@
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#"x86_64-unknown-linux-musl",
#{ triple = "x86_64-unknown-linux-musl" },
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
@ -49,9 +46,6 @@ no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
@ -63,20 +57,38 @@ feature-depth = 1
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use
#db-urls = ["https://github.com/rustsec/advisory-db"]
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2023-0071", reason = "Not applicable, vulnerable path is not used" },
# Not applicable, RSA is not used for crypto operations in the dep it's
# used for, openpgp-card
"RUSTSEC-2023-0071",
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
@ -87,6 +99,8 @@ ignore = [
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
@ -99,9 +113,30 @@ allow = [
"Unicode-DFS-2016",
"LGPL-2.0",
"LGPL-3.0",
"Unicode-3.0",
#"Apache-2.0 WITH LLVM-exception",
]
# List of explicitly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi - The license will be approved if it is OSI approved
# * fsf - The license will be approved if it is FSF Free
# * osi-only - The license will be approved if it is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if it is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
@ -112,17 +147,18 @@ confidence-threshold = 0.8
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], crate = "adler32" },
#{ allow = ["Zlib"], name = "adler32", version = "*" },
{ allow = ["BSL-1.0"], name = "xxhash-rust", version = "*" },
{ allow = ["Zlib"], name = "foldhash", version = "*" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The package spec the clarification applies to
#crate = "ring"
# The name of the crate the clarification applies to
#name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
@ -131,8 +167,8 @@ exceptions = [
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
@ -172,24 +208,25 @@ workspace-default-features = "allow"
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
#{ name = "ansi_term", version = "=0.11.0" },
]
# List of crates to deny
deny = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
{ name = "serde", version = ">1.0.171, <1.0.184", reason = "ships with prebuilt binaries" }
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
{ name = "serde", version = ">1.0.171, <1.0.184" }
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
#name = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
@ -210,18 +247,14 @@ deny = [
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
#{ name = "ansi_term", version = "=0.11.0" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
{ name = "windows-sys" },
{ name = "windows-targets" },
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.

View File

@ -45,7 +45,7 @@ A command line interface for generating, deriving from, and managing secrets.
* [`keyfork-derive-openpgp`]
* [`keyfork-derive-util`]
* [`keyfork-entropy`]
* [`keyfork-mnemonic`]
* [`keyfork-mnemonic-util`]
* [`keyfork-prompt`]
* [`keyfork-qrcode`]
* [`keyfork-shard`]
@ -68,7 +68,7 @@ seed or close-to-root derivations.
* [`keyfork-derive-path-data`]
* [`keyfork-derive-util`]
* [`keyfork-frame`]
* [`keyfork-mnemonic`]
* [`keyfork-mnemonic-util`]
* [`keyforkd-models`]
* [`serde`]
* [`thiserror`]
@ -129,7 +129,7 @@ BIP-0032 derivation.
* [`ed25519-dalek`]: Ed25519 key parsing and arithmetic.
* [`hmac`]: Derivation of keys using HMAC.
* [`k256`]: secp256k1 (K-256) key parsing and arithmetic.
* [`keyfork-mnemonic`]
* [`keyfork-mnemonic-util`]
* [`ripemd`]: Generating hash for fingerprinting of BIP-0032 derived data.
* [`serde`]
* [`sha2`]: Generating hashes for fingerprinting and derivation of data.
@ -145,7 +145,7 @@ M-of-N recombination of secret data using Shamir's Secret Sharing.
* [`card-backend-pcsc`]: PCSC support for OpenPGP-card.
* [`hkdf`]: Key derivation for transport encryption keys.
* [`keyfork-derive-openpgp`]
* [`keyfork-mnemonic`]: Encoding encrypted shards using mnemonics.
* [`keyfork-mnemonic-util`]: Encoding encrypted shards using mnemonics.
* [`keyfork-prompt`]
* [`keyfork-qrcode`]: Encoding and decoding of encrypted shards using QR codes.
* [`openpgp-card`]: OpenPGP card support.
@ -193,7 +193,7 @@ Frame data in a length-storing checksum-verified format.
* [`thiserror`]
* [`tokio`]: Read and write from AsyncRead and AsyncWrite sources.
## `keyfork-mnemonic`
## `keyfork-mnemonic-util`
* [`hmac`]: Hash utilities.
* [`sha2`]: Checksum of mnemonic data and hash for pbkdf2
@ -202,7 +202,7 @@ Frame data in a length-storing checksum-verified format.
## `keyfork-prompt`
* [`keyfork-crossterm`]: Interacting with the terminal.
* [`keyfork-mnemonic`]
* [`keyfork-mnemonic-util`]
* [`thiserror`]
## `keyfork-plumbing`
@ -210,7 +210,7 @@ Frame data in a length-storing checksum-verified format.
Binaries for `keyfork-entropy` and `keyfork-mnemonic-from-seed`.
* [`keyfork-entropy`]
* [`keyfork-mnemonic`]
* [`keyfork-mnemonic-util`]
* [`smex`]
## `keyfork-slip10-test-data`
@ -229,7 +229,7 @@ Zero-dependency hex encoding and decoding.
[`keyfork-derive-util`]: #keyfork-derive-util
[`keyfork-entropy`]: #keyfork-entropy
[`keyfork-frame`]: #keyfork-frame
[`keyfork-mnemonic`]: #keyfork-mnemonic
[`keyfork-mnemonic-util`]: #keyfork-mnemonic-util
[`keyfork-prompt`]: #keyfork-prompt
[`keyfork-qrcode`]: #keyfork-qrcode
[`keyfork-shard`]: #keyfork-shard

View File

@ -3,13 +3,12 @@ set -o pipefail
LAST_REF="$1"
CURRENT_REF="${2:-HEAD}"
IGNORE="${3:-ABCDEFG}"
cargo metadata --format-version=1 | \
jq -r '.packages[] | select(.source == null) | .name + " " + .manifest_path' | \
while read crate manifest_path; do
crate_path="$(dirname $manifest_path)"
git_log="$(git log --format='%h %s' "$LAST_REF".."$CURRENT_REF" "$crate_path" | { grep -v "$IGNORE" || true; })"
git_log="$(git log --format='%h %s' "$LAST_REF"..HEAD "$crate_path")"
if test ! -z "$git_log"; then
echo "### Changes in $crate:"
echo ""

View File

@ -1,18 +1,13 @@
#!/bin/bash
set -eu
set -o pipefail
scripts_dir="$(dirname "$0")"
scripts_dir="$(dirname $0)"
python_script="$scripts_dir/generate-dependency-queue.py"
registry_url="https://git.distrust.co/api/packages/public/cargo"
search_url="${registry_url}/api/v1/crates"
cargo metadata --format-version=1 | python3 "$python_script" | while read -r crate version; do
cargo metadata --format-version=1 | python3 "$python_script" | while read crate version; do
# Verify the package does not exist
if test "${crate}" = "keyfork-tests"; then
continue
fi
if ! curl "${search_url}?q=${crate}" 2>/dev/null | jq -e "$(printf '.crates | .[] | select(.name == "%s" and .max_version == "%s")' "$crate" "$version")" >/dev/null; then
cargo publish --registry distrust -p "$crate"
fi

View File

@ -1,4 +1,3 @@
#!/bin/bash
set -eu
set -o pipefail
@ -9,9 +8,9 @@ temp_file="$(mktemp)"
cargo metadata --format-version=1 | jq -r '.packages[] | select(.source == null) | .name + " " + .manifest_path + " " + .version' > "$temp_file"
while read -r crate manifest_path version <&3; do
crate_path="$(dirname "$manifest_path")"
git_log="$(git log --format='%h %s' "$LAST_REF".."$CURRENT_REF" "$crate_path")"
while read crate manifest_path version <&3; do
crate_path="$(dirname $manifest_path)"
git_log="$(git log --format='%h %s' "$LAST_REF"..HEAD "$crate_path")"
git_tag="$(git tag --list "$crate-v${version}")"
if test ! -z "$git_log" -a -z "$git_tag"; then
{
@ -23,7 +22,6 @@ while read -r crate manifest_path version <&3; do
echo ""
echo "# Crate: ${crate} ${version}"
} | git tag --sign "${crate}-v${version}" -F - -e
reset
echo "Making new tag: ${crate}-v${version}"
fi
done 3<"$temp_file"