Compare commits

..

1 Commits

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

1
.gitattributes vendored
View File

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

View File

@ -1,134 +1,3 @@
# 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 # Keyfork v0.2.1
This release contains an emergency bugfix for Keyfork Shard, which previously This release contains an emergency bugfix for Keyfork Shard, which previously

1721
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,62 +19,12 @@ members = [
"crates/util/keyfork-crossterm", "crates/util/keyfork-crossterm",
"crates/util/keyfork-entropy", "crates/util/keyfork-entropy",
"crates/util/keyfork-frame", "crates/util/keyfork-frame",
"crates/util/keyfork-mnemonic", "crates/util/keyfork-mnemonic-util",
"crates/util/keyfork-prompt", "crates/util/keyfork-prompt",
"crates/util/keyfork-slip10-test-data", "crates/util/keyfork-slip10-test-data",
"crates/util/smex", "crates/util/smex",
] ]
[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.2.2", 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.1.1", 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"
[profile.dev.package.keyfork-qrcode] [profile.dev.package.keyfork-qrcode]
opt-level = 3 opt-level = 3
debug = true debug = true

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

View File

@ -11,7 +11,7 @@ fn secp256k1_test_suite() {
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove("secp256k1") .remove(&"secp256k1".to_string())
.unwrap(); .unwrap();
for seed_test in tests { for seed_test in tests {
@ -70,7 +70,7 @@ fn secp256k1_test_suite() {
fn ed25519_test_suite() { fn ed25519_test_suite() {
use ed25519_dalek::SigningKey; 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 { for seed_test in tests {
let seed = seed_test.seed; 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
keyfork-derive-util = { workspace = true, default-features = false } keyfork-derive-util = { version = "0.2.0", path = "../../derive/keyfork-derive-util", default-features = false, registry = "distrust" }
serde = { workspace = true } serde = { version = "1.0.190", features = ["derive"] }
thiserror = { workspace = true } thiserror = "1.0.50"

View File

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

View File

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

View File

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

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

@ -7,7 +7,7 @@ license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
keyfork-derive-util = { workspace = true } keyfork-derive-util = { version = "0.2.0", path = "../keyfork-derive-util", registry = "distrust" }
keyforkd-client = { workspace = true } keyforkd-client = { version = "0.2.0", path = "../../daemon/keyforkd-client", registry = "distrust" }
smex = { workspace = true } smex = { version = "0.1.0", path = "../../util/smex", registry = "distrust" }
thiserror = { workspace = true } 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}; use std::{env, process::ExitCode, str::FromStr};

View File

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

View File

@ -19,14 +19,8 @@ use sequoia_openpgp::{
Cert, Packet, 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; pub type XPrvKey = SigningKey;
pub type XPrv = ExtendedPrivateKey<SigningKey>;
/// The extended private key type used with OpenPGP.
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
/// An error occurred while creating an OpenPGP key. /// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)] #[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 std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::DerivationPath; use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_derive_path_data::paths;
use keyforkd_client::Client; use keyforkd_client::Client;
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
@ -79,14 +78,16 @@ fn validate(
subkey_format: &str, subkey_format: &str,
default_userid: &str, default_userid: &str,
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> { ) -> 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)?; let path = DerivationPath::from_str(path)?;
assert!(path.len() >= 2, "Expected path of at least m/{index}/account_id'"); assert!(path.len() >= 2, "Expected path of at least m/{index}/account_id'");
let given_index = path.iter().next().expect("checked .len() above"); let given_index = path.iter().next().expect("checked .len() above");
assert_eq!( assert_eq!(
index, given_index, &index, given_index,
"Expected derivation path starting with m/{index}, got: {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)?; let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
for packet in cert.into_packets2() { for packet in cert.into_packets() {
packet.serialize(&mut w)?; packet.serialize(&mut w)?;
} }

View File

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

View File

@ -2,129 +2,32 @@
#![allow(clippy::unreadable_literal)] #![allow(clippy::unreadable_literal)]
use once_cell::sync::Lazy;
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
/// All common paths for key derivation.
pub mod paths {
use super::*;
/// The default derivation path for OpenPGP. /// The default derivation path for OpenPGP.
pub static OPENPGP: Lazy<DerivationPath> = Lazy::new(|| { pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true);
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
}
}
/// A derivation target. /// A derivation target.
#[derive(Debug)]
#[non_exhaustive]
pub enum Target { pub enum Target {
/// An OpenPGP key, whose account is the given index. /// An OpenPGP key, whose account is the given index.
OpenPGP(DerivationIndex), 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 { impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Target::OpenPGP(account) => { Self::OpenPGP(account) => {
write!(f, "OpenPGP key (account {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 /// 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. /// `keyforkd` to provide an optional textual prompt to what a client is attempting to derive.
pub fn guess_target(path: &DerivationPath) -> Option<Target> { pub fn guess_target(path: &DerivationPath) -> Option<Target> {
test_match!(path, paths::OPENPGP_SHARD, Target::OpenPGPShard); Some(match path.iter().collect::<Vec<_>>()[..] {
test_match!( [t, index] if t == &OPENPGP => Target::OpenPGP(index.clone()),
path, _ => return None,
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:?}"),
}
}
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork-derive-util" name = "keyfork-derive-util"
version = "0.2.1" version = "0.2.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -12,25 +12,25 @@ secp256k1 = ["k256"]
ed25519 = ["ed25519-dalek"] ed25519 = ["ed25519-dalek"]
[dependencies] [dependencies]
keyfork-mnemonic = { workspace = true } keyfork-mnemonic-util = { version = "0.3.0", path = "../../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-bug = { workspace = true } keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
# Included in Rust # Included in Rust
digest = "0.10.7" digest = "0.10.7"
sha2 = { workspace = true } sha2 = "0.10.7"
# Rust-Crypto ecosystem, not personally audited # Rust-Crypto ecosystem, not personally audited
ripemd = "0.1.3" ripemd = "0.1.3"
hmac = { workspace = true, features = ["std"] } hmac = { version = "0.12.1", features = ["std"] }
# Personally audited # Personally audited
serde = { workspace = true } serde = { version = "1.0.186", features = ["derive"] }
thiserror = { workspace = true } thiserror = "1.0.47"
# Optional, not personally audited # Optional, not personally audited
k256 = { workspace = true, default-features = false, features = ["std", "arithmetic"], optional = true } k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true }
ed25519-dalek = { workspace = true, optional = true } ed25519-dalek = { version = "2.0.0", optional = true }
[dev-dependencies] [dev-dependencies]
hex-literal = { workspace = true } hex-literal = "0.4.1"
keyfork-slip10-test-data = { workspace = true } 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 ```rust
use std::str::FromStr; use std::str::FromStr;
use keyfork_mnemonic::Mnemonic; use keyfork_mnemonic_util::Mnemonic;
use keyfork_derive_util::{*, request::*}; use keyfork_derive_util::{*, request::*};
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
//! Decrypt a single OpenPGP shard and encapsulate it for remote transport. //!
use std::{ use std::{
env, env,

View File

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

View File

@ -1,4 +1,4 @@
//! Split a hex-encoded secret into OpenPGP shards //!
use std::{env, path::PathBuf, process::ExitCode, str::FromStr}; use std::{env, path::PathBuf, process::ExitCode, str::FromStr};

View File

@ -13,7 +13,7 @@ use aes_gcm::{
use base64::prelude::{Engine, BASE64_STANDARD}; use base64::prelude::{Engine, BASE64_STANDARD};
use hkdf::Hkdf; use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX}; use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_mnemonic::{English, Mnemonic}; use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{ use keyfork_prompt::{
validators::{ validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength}, mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
@ -22,7 +22,7 @@ use keyfork_prompt::{
Message as PromptMessage, PromptHandler, Terminal, Message as PromptMessage, PromptHandler, Terminal,
}; };
use sha2::Sha256; use sha2::Sha256;
use blahaj::{Share, Sharks}; use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
const PLAINTEXT_LENGTH: u8 = 32 // shard const PLAINTEXT_LENGTH: u8 = 32 // shard
@ -249,7 +249,7 @@ pub trait Format {
// create our shared key // create our shared key
let our_key = EphemeralSecret::random(); 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)); let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
assert!( assert!(
shared_secret.was_contributory(), shared_secret.was_contributory(),
@ -302,7 +302,7 @@ pub trait Format {
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize]; let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
mnemonic_bytes.copy_from_slice(&encrypted_bytes); 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")] #[cfg(feature = "qrcode")]
{ {
@ -439,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) { while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1; iter += 1;
let our_key = EphemeralSecret::random(); 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")] #[cfg(feature = "qrcode")]
{ {

View File

@ -25,7 +25,7 @@ use openpgp::{
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper}, stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
Parse, Parse,
}, },
policy::{NullPolicy, StandardPolicy, Policy}, policy::{NullPolicy, Policy, StandardPolicy},
serialize::{ serialize::{
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer}, stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal, Marshal,
@ -34,7 +34,7 @@ use openpgp::{
KeyID, PacketPile, KeyID, PacketPile,
}; };
pub use sequoia_openpgp as openpgp; pub use sequoia_openpgp as openpgp;
use blahaj::Share; use sharks::Share;
mod keyring; mod keyring;
use keyring::Keyring; use keyring::Keyring;
@ -77,10 +77,6 @@ pub enum Error {
/// An IO error occurred. /// An IO error occurred.
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[source] std::io::Error), 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)] #[allow(missing_docs)]
@ -185,7 +181,7 @@ impl EncryptedMessage {
} }
} }
/// Encoding and decoding shards using OpenPGP. ///
pub struct OpenPGP<P: PromptHandler> { pub struct OpenPGP<P: PromptHandler> {
p: PhantomData<P>, p: PhantomData<P>,
} }
@ -243,13 +239,6 @@ impl<P: PromptHandler> OpenPGP<P> {
certs.insert(certfp, cert); 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()) Ok(certs.into_values().collect())
} }
} }
@ -588,8 +577,7 @@ fn get_encryption_keys<'a>(
openpgp::packet::key::UnspecifiedRole, openpgp::packet::key::UnspecifiedRole,
> { > {
cert.keys() cert.keys()
// NOTE: this causes complications on Airgap systems .alive()
// .alive()
.revoked(false) .revoked(false)
.supported() .supported()
.for_storage_encryption() .for_storage_encryption()

View File

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

View File

@ -1,5 +1,5 @@
use super::Keyfork; use super::Keyfork;
use clap::{Args, Parser, Subcommand}; use clap::{Parser, Subcommand};
use keyfork_derive_openpgp::{ use keyfork_derive_openpgp::{
openpgp::{ openpgp::{
@ -10,8 +10,7 @@ use keyfork_derive_openpgp::{
}, },
XPrvKey, XPrvKey,
}; };
use keyfork_derive_util::DerivationIndex; use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_derive_path_data::paths;
use keyforkd_client::Client; use keyforkd_client::Client;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -28,26 +27,22 @@ pub enum DeriveSubcommands {
/// It is recommended to use the default expiration of one day and to change the expiration /// 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. /// using an external utility, to ensure the Certify key is usable.
#[command(name = "openpgp")] #[command(name = "openpgp")]
OpenPGP(OpenPGP) OpenPGP {
}
#[derive(Args, Clone, Debug)]
pub struct OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format. /// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String, user_id: String,
},
} }
impl DeriveSubcommands { impl DeriveSubcommands {
fn handle(&self, account: DerivationIndex) -> Result<()> { fn handle(&self, account: DerivationIndex) -> Result<()> {
match self { match self {
DeriveSubcommands::OpenPGP(opgp) => opgp.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()
impl OpenPGP { .chain_push(chain)
pub fn handle(&self, account: DerivationIndex) -> Result<()> { .chain_push(account);
let path = paths::OPENPGP.clone().chain_push(account);
// TODO: should this be customizable? // TODO: should this be customizable?
let subkeys = vec![ let subkeys = vec![
KeyFlags::empty().set_certification(), KeyFlags::empty().set_certification(),
@ -58,16 +53,19 @@ impl OpenPGP {
KeyFlags::empty().set_authentication(), KeyFlags::empty().set_authentication(),
]; ];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?; let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(self.user_id.as_str()); let default_userid = UserID::from(user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?; 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_packets2() { for packet in cert.into_packets() {
packet.serialize(&mut w)?; packet.serialize(&mut w)?;
} }
w.finalize()?; w.finalize()?;
}
}
Ok(()) Ok(())
} }
} }

View File

@ -109,7 +109,7 @@ impl MnemonicSeedSource {
MnemonicSeedSource::Tarot => todo!(), MnemonicSeedSource::Tarot => todo!(),
MnemonicSeedSource::Dice => todo!(), MnemonicSeedSource::Dice => todo!(),
}; };
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?; let mnemonic = keyfork_mnemonic_util::Mnemonic::from_bytes(&seed)?;
Ok(mnemonic.to_string()) Ok(mnemonic.to_string())
} }
} }

View File

@ -2,7 +2,7 @@ use super::Keyfork;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::path::PathBuf; use std::path::PathBuf;
use keyfork_mnemonic::{English, Mnemonic}; use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{default_terminal, DefaultTerminal}; use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_shard::{remote_decrypt, Format}; use keyfork_shard::{remote_decrypt, Format};
@ -85,7 +85,7 @@ pub struct Recover {
impl Recover { impl Recover {
pub fn handle(&self, _k: &Keyfork) -> Result<()> { pub fn handle(&self, _k: &Keyfork) -> Result<()> {
let seed = self.command.handle()?; let seed = self.command.handle()?;
let mnemonic = Mnemonic::try_from_slice(&seed)?; let mnemonic = Mnemonic::from_bytes(&seed)?;
tokio::runtime::Builder::new_multi_thread() tokio::runtime::Builder::new_multi_thread()
.enable_all() .enable_all()
.build() .build()

View File

@ -1,29 +1,26 @@
use super::Keyfork; use super::Keyfork;
use clap::{Args, Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf}; use std::{
collections::HashSet,
fs::File,
io::IsTerminal,
path::{Path, PathBuf},
};
use card_backend_pcsc::PcscBackend; use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card}; use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use keyfork_derive_openpgp::{ use keyfork_derive_openpgp::{
openpgp::{ openpgp::{self, packet::UserID, types::KeyFlags, Cert, serialize::Marshal, armor::{Writer, Kind}},
self,
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
Cert,
},
XPrv, XPrv,
}; };
use keyfork_derive_path_data::paths; use keyfork_derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed};
use keyfork_derive_util::DerivationIndex;
use keyfork_mnemonic::Mnemonic;
use keyfork_prompt::{ use keyfork_prompt::{
default_terminal, default_terminal,
validators::{SecurePinValidator, Validator}, validators::{SecurePinValidator, Validator},
DefaultTerminal, Message, PromptHandler, DefaultTerminal, Message, PromptHandler,
}; };
use keyfork_mnemonic_util::Mnemonic;
use keyfork_shard::{openpgp::OpenPGP, Format}; use keyfork_shard::{openpgp::OpenPGP, Format};
@ -45,8 +42,17 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
KeyFlags::empty().set_authentication(), 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 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) let xprv = XPrv::new(seed)
.expect("could not construct master key from seed") .expect("could not construct master key from seed")
.derive_path(&path)?; .derive_path(&path)?;
@ -103,82 +109,18 @@ fn factory_reset_current_card(
Ok(()) Ok(())
} }
#[derive(Subcommand, Clone, Debug)] fn generate_shard_secret(
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, threshold: u8,
/// The maximum amount of shards.
#[arg(long)]
max: u8, max: u8,
/// The amount of smart cards to provision per-shard.
#[arg(long, default_value = "1")]
keys_per_shard: u8, keys_per_shard: u8,
output_file: &Option<PathBuf>,
/// The file to write the generated shard file to. ) -> Result<()> {
#[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.
#[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(),
}
}
}
impl GenerateShardSecret {
fn handle(&self) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mut pm = default_terminal()?; let mut pm = default_terminal()?;
let mut certs = vec![]; let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new(); let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout(); let stdout = std::io::stdout();
if self.output.is_none() { if output_file.is_none() {
assert!( assert!(
!stdout.is_terminal(), !stdout.is_terminal(),
"not printing shard to terminal, redirect output" "not printing shard to terminal, redirect output"
@ -196,9 +138,9 @@ impl GenerateShardSecret {
} }
.to_fn(); .to_fn();
for index in 0..self.max { for index in 0..max {
let cert = derive_key(seed, index)?; let cert = derive_key(seed, index)?;
for i in 0..self.keys_per_shard { for i in 0..keys_per_shard {
pm.prompt_message(Message::Text(format!( pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}", "Please remove all keys and insert key #{} for user #{}",
(i as u16) + 1, (i as u16) + 1,
@ -236,12 +178,12 @@ impl GenerateShardSecret {
let opgp = OpenPGP::<DefaultTerminal>::new(); let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = self.output.as_ref() { if let Some(output_file) = output_file {
let output = File::create(output_file)?; let output = File::create(output_file)?;
opgp.shard_and_encrypt(self.threshold, certs.len() as u8, &seed, &certs[..], output)?; opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else { } else {
opgp.shard_and_encrypt( opgp.shard_and_encrypt(
self.threshold, threshold,
certs.len() as u8, certs.len() as u8,
&seed, &seed,
&certs[..], &certs[..],
@ -250,18 +192,18 @@ impl GenerateShardSecret {
} }
Ok(()) Ok(())
} }
}
impl BottomsUp { fn bottoms_up(key_discovery: &Path, threshold: u8, output_shardfile: &Path, output_cert: &Path, user_id: &str,) -> Result<()> {
fn handle(&self) -> Result<()> {
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mnemonic = Mnemonic::from_array(entropy); let mnemonic = Mnemonic::from_nonstandard_bytes(entropy);
// TODO: make this return const size, since is hash based
let seed = mnemonic.generate_seed(None); let seed = mnemonic.generate_seed(None);
// TODO: should this allow for customizing the account index from 0? Potential for key reuse // TODO: should this allow for customizing the account index from 0? Potential for key reuse
// errors. // errors.
let path = paths::OPENPGP_DISASTER_RECOVERY let path = DerivationPath::default()
.clone() .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)?); .chain_push(DerivationIndex::new(0, true)?);
let subkeys = [ let subkeys = [
KeyFlags::empty().set_certification(), KeyFlags::empty().set_certification(),
@ -271,30 +213,96 @@ impl BottomsUp {
.set_storage_encryption(), .set_storage_encryption(),
KeyFlags::empty().set_authentication(), KeyFlags::empty().set_authentication(),
]; ];
let xprv = XPrv::new(seed) let xprv = XPrv::new(VariableLengthSeed::new(&seed))
.expect("could not construct master key from seed") .expect("could not construct master key from seed")
.derive_path(&path)?; .derive_path(&path)?;
let userid = UserID::from(self.user_id.as_str()); let userid = UserID::from(user_id);
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?; let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
let certfile = File::create(&self.output_cert)?; let certfile = File::create(output_cert)?;
let mut w = Writer::new(certfile, Kind::PublicKey)?; let mut w = Writer::new(certfile, Kind::PublicKey)?;
cert.serialize(&mut w)?; cert.serialize(&mut w)?;
w.finalize()?; w.finalize()?;
let opgp = OpenPGP::<DefaultTerminal>::new(); let opgp = OpenPGP::<DefaultTerminal>::new();
let certs = OpenPGP::<DefaultTerminal>::discover_certs(&self.key_discovery)?; let certs = OpenPGP::<DefaultTerminal>::discover_certs(key_discovery)?;
let shardfile = File::create(output_shardfile)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &entropy, &certs[..], shardfile)?;
let shardfile = File::create(&self.output_shardfile)?;
opgp.shard_and_encrypt(
self.threshold,
certs.len() as u8,
&entropy,
&certs[..],
shardfile,
)?;
Ok(()) Ok(())
} }
#[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<()> {
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),
}
}
} }
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]

View File

@ -14,9 +14,9 @@ decode-backend-rqrr = ["dep:rqrr"]
decode-backend-zbar = ["dep:keyfork-zbar"] decode-backend-zbar = ["dep:keyfork-zbar"]
[dependencies] [dependencies]
keyfork-bug = { workspace = true } keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
keyfork-zbar = { workspace = true, optional = true } keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true, registry = "distrust" }
image = { workspace = true, default-features = false, features = ["jpeg"] } image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
rqrr = { version = "0.7.0", optional = true } rqrr = { version = "0.6.0", optional = true }
thiserror = { workspace = true } thiserror = "1.0.56"
v4l = { workspace = true } v4l = "0.14.0"

View File

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

View File

@ -2,7 +2,7 @@
use keyfork_bug as bug; use keyfork_bug as bug;
use image::ImageReader; use image::io::Reader as ImageReader;
use std::{ use std::{
io::{Cursor, Write}, io::{Cursor, Write},
time::{Duration, Instant}, time::{Duration, Instant},
@ -103,11 +103,6 @@ pub fn qrencode(
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format"; const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
/// Continuously scan the `index`-th camera for a QR code. /// 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")] #[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; 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. /// 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")] #[cfg(feature = "decode-backend-zbar")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;

View File

@ -13,9 +13,9 @@ bin = ["image"]
image = ["dep:image"] image = ["dep:image"]
[dependencies] [dependencies]
keyfork-zbar-sys = { workspace = true } keyfork-zbar-sys = { version = "0.1.0", path = "../keyfork-zbar-sys", registry = "distrust" }
image = { workspace = true, default-features = false, optional = true } image = { version = "0.24.7", default-features = false, optional = true }
thiserror = { workspace = true } thiserror = "1.0.56"
[dev-dependencies] [dev-dependencies]
v4l = { workspace = true } v4l = "0.14.0"

View File

@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.decode()?, .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())); println!("{}", String::from_utf8_lossy(symbol.data()));
return Ok(()); return Ok(());
} }

View File

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

View File

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

View File

@ -55,16 +55,16 @@ crossterm_winapi = { version = "0.9.1", optional = true }
libc = "0.2" libc = "0.2"
signal-hook = { version = "0.3.17", optional = true } signal-hook = { version = "0.3.17", optional = true }
filedescriptor = { version = "0.8", optional = true } filedescriptor = { version = "0.8", optional = true }
mio = { version = "1.0", features = ["os-poll"], optional = true } mio = { version = "0.8", features = ["os-poll"], optional = true }
signal-hook-mio = { version = "0.2.3", features = ["support-v1_0"], optional = true } signal-hook-mio = { version = "0.2.3", features = ["support-v0_8"], optional = true }
# Dev dependencies (examples, ...) # Dev dependencies (examples, ...)
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["full"] } tokio = { version = "1.25", features = ["full"] }
futures = "0.3" futures = "0.3"
futures-timer = "3.0" futures-timer = "3.0"
async-std = "1.12" async-std = "1.12"
serde_json = { workspace = true } serde_json = "1.0"
serial_test = "2.0.0" serial_test = "2.0.0"
# Examples # Examples

View File

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

View File

@ -1,7 +1,7 @@
use std::{collections::VecDeque, io, time::Duration}; use std::{collections::VecDeque, io, time::Duration};
use mio::{unix::SourceFd, Events, Interest, Poll, Token}; 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")] #[cfg(feature = "event-stream")]
use crate::event::sys::Waker; use crate::event::sys::Waker;

View File

@ -11,5 +11,5 @@ default = ["bin"]
bin = ["smex"] bin = ["smex"]
[dependencies] [dependencies]
keyfork-bug = { workspace = true } keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
smex = { workspace = true, optional = true } 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>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let bit_size: usize = std::env::args() let bit_size: usize = std::env::args()

View File

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

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork-mnemonic" name = "keyfork-mnemonic-util"
version = "0.4.0" version = "0.3.0"
description = "Utilities to generate and manage seeds based on BIP-0039 mnemonics." description = "Utilities to generate and manage seeds based on BIP-0039 mnemonics."
repository = "https://git.distrust.co/public/keyfork" repository = "https://git.distrust.co/public/keyfork"
edition = "2021" edition = "2021"
@ -11,14 +11,14 @@ default = ["bin"]
bin = ["smex"] bin = ["smex"]
[dependencies] [dependencies]
smex = { workspace = true, optional = true } smex = { version = "0.1.0", path = "../smex", optional = true, registry = "distrust" }
keyfork-bug = { workspace = true } keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
sha2 = { workspace = true } sha2 = "0.10.7"
hmac = { workspace = true } hmac = "0.12.1"
pbkdf2 = "0.12.2" pbkdf2 = "0.12.2"
[dev-dependencies] [dev-dependencies]
bip39 = "2.0.0" bip39 = "2.0.0"
hex = "0.4.3" 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>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let input = std::io::stdin(); 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: //! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase:
//! //!
//! ```rust //! ```rust
//! use keyfork_mnemonic::Mnemonic; //! use keyfork_mnemonic_util::Mnemonic;
//! let data = b"Hello, world! I am a mnemonic :)"; //! let data = b"Hello, world! I am a mnemonic :)";
//! assert_eq!(data.len(), 32); //! 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}"); //! println!("Our mnemonic is: {mnemonic}");
//! ``` //! ```
//! //!
//! A mnemonic can also be parsed from a string: //! A mnemonic can also be parsed from a string:
//! //!
//! ```rust //! ```rust
//! use keyfork_mnemonic::Mnemonic; //! use keyfork_mnemonic_util::Mnemonic;
//! use std::str::FromStr; //! use std::str::FromStr;
//! //!
//! let data = b"Hello, world! I am a mnemonic :)"; //! let data = b"Hello, world! I am a mnemonic :)";
@ -28,7 +28,7 @@
//! verified to be safe: //! verified to be safe:
//! //!
//! ```rust //! ```rust
//! use keyfork_mnemonic::Mnemonic; //! use keyfork_mnemonic_util::Mnemonic;
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; //! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) }; //! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
//! let mnemonic_text = mnemonic.to_string(); //! let mnemonic_text = mnemonic.to_string();
@ -37,7 +37,7 @@
//! If given an invalid length, undefined behavior may follow, or code may panic. //! If given an invalid length, undefined behavior may follow, or code may panic.
//! //!
//! ```rust,should_panic //! ```rust,should_panic
//! use keyfork_mnemonic::Mnemonic; //! use keyfork_mnemonic_util::Mnemonic;
//! use std::str::FromStr; //! use std::str::FromStr;
//! //!
//! // NOTE: Data is of invalid length, 31 //! // NOTE: Data is of invalid length, 31
@ -268,11 +268,11 @@ where
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// 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; let bit_count = bytes.len() * 8;
if bit_count % 32 != 0 { if bit_count % 32 != 0 {
@ -290,23 +290,23 @@ where
/// of a factor of 4, up to 1024 bytes. /// of a factor of 4, up to 1024 bytes.
/// ///
/// ```rust /// ```rust
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"hello world!"; /// 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: /// If an invalid size is requested, the code will fail to compile:
/// ///
/// ```rust,compile_fail /// ```rust,compile_fail
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// let mnemonic = Mnemonic::from_array([0u8; 53]); /// let mnemonic = Mnemonic::from_nonstandard_bytes([0u8; 53]);
/// ``` /// ```
/// ///
/// ```rust,compile_fail /// ```rust,compile_fail
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// let mnemonic = Mnemonic::from_array([0u8; 1024 + 4]); /// 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)] #[allow(clippy::let_unit_value)]
{ {
let () = AssertValidMnemonicSize::<N>::OK_CHUNKS; let () = AssertValidMnemonicSize::<N>::OK_CHUNKS;
@ -315,6 +315,16 @@ where
Self::from_raw_bytes(&bytes) 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 /// 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. /// 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 /// 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 /// 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 /// == 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 /// [`MnemonicBase::from_nonstandard_bytes`] function may be used to generate entropy if the
/// data is known at compile-time. /// length of the data is known at compile-time.
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) }; /// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string(); /// let mnemonic_text = mnemonic.to_string();
@ -336,7 +346,7 @@ where
/// If given an invalid length, undefined behavior may follow, or code may panic. /// If given an invalid length, undefined behavior may follow, or code may panic.
/// ///
/// ```rust,should_panic /// ```rust,should_panic
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// use std::str::FromStr; /// use std::str::FromStr;
/// ///
/// // NOTE: Data is of invalid length, 31 /// // NOTE: Data is of invalid length, 31
@ -373,31 +383,16 @@ where
&self.data &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. /// A clone of the internal representation of the decoded data.
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
self.data.to_vec() 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. /// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_bytes(self) -> Vec<u8> { pub fn into_bytes(self) -> Vec<u8> {
self.data 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. /// Clone the existing data.
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"] #[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> { pub fn entropy(&self) -> Vec<u8> {
@ -413,7 +408,7 @@ where
&self, &self,
passphrase: impl Into<Option<&'a str>>, passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> { ) -> 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. /// Create a BIP-0032 seed from the provided data and an optional passphrase.
@ -421,7 +416,8 @@ where
/// # Panics /// # Panics
/// The function may panic if the HmacSha512 function returns an error. The only error the /// 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. /// 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 passphrase = passphrase.into();
let mut seed = [0u8; 64]; let mut seed = [0u8; 64];
@ -429,7 +425,7 @@ where
let salt = ["mnemonic", passphrase.unwrap_or("")].join(""); let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed) pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.expect(bug!("HmacSha512 InvalidLength should be infallible")); .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 /// 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)] #[cfg(test)]
mod tests { mod tests {
use std::{collections::HashSet, fs::File, io::Read}; use std::{collections::HashSet, fs::File, io::Read};
@ -515,7 +478,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap(); let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8]; let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap(); 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(); let new_entropy = mnemonic.as_bytes();
assert_eq!(new_entropy, entropy); assert_eq!(new_entropy, entropy);
} }
@ -531,7 +494,7 @@ mod tests {
}; };
let hex = hex::decode(hex_.as_str().unwrap()).unwrap(); 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()); 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 mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8]; let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap(); 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(); 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.to_string(), their_mnemonic.to_string());
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed("")); assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
@ -566,7 +529,7 @@ mod tests {
for _ in 0..tests { for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap(); 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(); let words = mnemonic.words();
hs.clear(); hs.clear();
hs.extend(words); hs.extend(words);
@ -597,7 +560,7 @@ mod tests {
let mut entropy = [0u8; 1024]; let mut entropy = [0u8; 1024];
let mut random = std::fs::File::open("/dev/urandom").unwrap(); let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap(); random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_array(entropy); let mnemonic = Mnemonic::from_nonstandard_bytes(entropy);
let words = mnemonic.words(); let words = mnemonic.words();
assert_eq!(words.len(), 768); assert_eq!(words.len(), 768);
} }

View File

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

View File

@ -1,38 +1,36 @@
#![allow(missing_docs)] //!
use std::io::{stdin, stdout}; use std::io::{stdin, stdout};
use keyfork_prompt::{ use keyfork_prompt::{
validators::{mnemonic, Validator}, MaybeIdentifier, PromptHandler, Terminal,
Terminal, PromptHandler,
}; };
use keyfork_mnemonic::English; #[derive(PartialEq, Eq, Debug)]
pub enum Example {
RetryQR,
UseMnemonic,
}
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>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut mgr = Terminal::new(stdin(), stdout())?; let mut mgr = Terminal::new(stdin(), stdout())?;
let transport_validator = mnemonic::MnemonicSetValidator {
word_lengths: [9, 24],
};
let combine_validator = mnemonic::MnemonicSetValidator {
word_lengths: [24, 48],
};
let mnemonics = mgr.prompt_validated_wordlist::<English, _>( let choice = mgr.prompt_choice(
"Enter a 9-word and 24-word mnemonic: ", "Unable to detect QR code.",
3, &[Example::RetryQR, Example::UseMnemonic],
transport_validator.to_fn(),
)?; )?;
assert_eq!(mnemonics[0].as_bytes().len(), 12); dbg!(choice);
assert_eq!(mnemonics[1].as_bytes().len(), 32);
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
"Enter a 24 and 48-word mnemonic: ",
3,
combine_validator.to_fn(),
)?;
assert_eq!(mnemonics[0].as_bytes().len(), 32);
assert_eq!(mnemonics[1].as_bytes().len(), 64);
Ok(()) Ok(())
} }

View File

@ -3,12 +3,12 @@
use std::borrow::Borrow; use std::borrow::Borrow;
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
use keyfork_mnemonic::Wordlist; use keyfork_mnemonic_util::Wordlist;
/// ///
pub mod terminal; pub mod terminal;
pub mod validators; pub mod validators;
pub use terminal::{Terminal, DefaultTerminal, default_terminal}; pub use terminal::{default_terminal, DefaultTerminal, Terminal};
/// An error occurred while displaying a prompt. /// An error occurred while displaying a prompt.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -42,6 +42,12 @@ pub enum Message {
Data(String), Data(String),
} }
pub trait MaybeIdentifier {
fn identifier(&self) -> Option<char> {
None
}
}
/// A trait to allow displaying prompts and accepting input. /// A trait to allow displaying prompts and accepting input.
pub trait PromptHandler { pub trait PromptHandler {
/// Prompt the user for input. /// Prompt the user for input.
@ -58,7 +64,9 @@ pub trait PromptHandler {
/// The method may return an error if the message was not able to be displayed or if the input /// The method may return an error if the message was not able to be displayed or if the input
/// could not be read. /// could not be read.
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist; 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 /// 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 /// provided parser function, returning the type from the parser. A language must be specified
@ -97,6 +105,19 @@ pub trait PromptHandler {
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, 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`]. /// Prompt the user with a [`Message`].
/// ///
/// # Errors /// # Errors

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::{ use std::{
borrow::Borrow, borrow::Borrow,
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write}, io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
@ -21,7 +15,7 @@ use keyfork_crossterm::{
use keyfork_bug::bug; use keyfork_bug::bug;
use crate::{Error, Message, PromptHandler, Wordlist}; use crate::{Error, MaybeIdentifier, Message, PromptHandler, Wordlist};
#[allow(missing_docs)] #[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -128,6 +122,9 @@ where
W: Write + AsRawFd, W: Write + AsRawFd,
{ {
fn drop(&mut self) { fn drop(&mut self) {
self.write
.execute(cursor::Show)
.expect(bug!("can't enable cursor blinking"));
self.write self.write
.execute(DisableBracketedPaste) .execute(DisableBracketedPaste)
.expect(bug!("can't restore bracketed paste")); .expect(bug!("can't restore bracketed paste"));
@ -461,6 +458,77 @@ where
Ok(passphrase) Ok(passphrase)
} }
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<()> { fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?; let mut terminal = self.lock().alternate_screen()?.raw_mode()?;

View File

@ -158,7 +158,7 @@ pub mod mnemonic {
use super::Validator; use super::Validator;
use keyfork_bug::bug; 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. /// A mnemonic could not be validated from the given input.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]

View File

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

101
deny.toml
View File

@ -11,9 +11,6 @@
# Root options # 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, # If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`. # 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 # This means, if a particular package is only ever used as a target specific
@ -25,7 +22,7 @@
targets = [ targets = [
# The triple can be any string, but only the target triples built in to # 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 # 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 # You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against # particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture. # 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` # 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. # is specified on the cmd line they will take precedence over this option.
#features = [] #features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this # 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. # 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 # 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: # More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories] [advisories]
# The path where the advisory databases are cloned/fetched into # The path where the advisory database is cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs" db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use # 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 # A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered. # output a note when they are encountered.
ignore = [ ignore = [
#"RUSTSEC-0000-0000", #"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, # Not applicable, RSA is not used for crypto operations in the dep it's
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish # used for, openpgp-card
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, "RUSTSEC-2023-0071",
{ id = "RUSTSEC-2023-0071", reason = "Not applicable, vulnerable path is not used" },
] ]
# 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 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. # 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. # 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: # More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses] [licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses # List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses # See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. # [possible values: any SPDX 3.11 short identifier (+ optional exception)].
@ -99,9 +113,30 @@ allow = [
"Unicode-DFS-2016", "Unicode-DFS-2016",
"LGPL-2.0", "LGPL-2.0",
"LGPL-3.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 confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the # The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file. # canonical license text of a valid SPDX license file.
@ -112,17 +147,18 @@ confidence-threshold = 0.8
exceptions = [ exceptions = [
# Each entry is the crate and version constraint, and its specific allow # Each entry is the crate and version constraint, and its specific allow
# list # list
#{ allow = ["Zlib"], crate = "adler32" }, #{ allow = ["Zlib"], name = "adler32", version = "*" },
{ allow = ["BSL-1.0"], name = "xxhash-rust", version = "*" }, { allow = ["BSL-1.0"], name = "xxhash-rust", version = "*" },
{ allow = ["Zlib"], name = "foldhash", version = "*" },
] ]
# Some crates don't have (easily) machine readable licensing information, # Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the # adding a clarification entry for it allows you to manually specify the
# licensing information # licensing information
#[[licenses.clarify]] #[[licenses.clarify]]
# The package spec the clarification applies to # The name of the crate the clarification applies to
#crate = "ring" #name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate # The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL" #expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for # One or more files in the crate's source used as the "source of truth" for
@ -172,24 +208,25 @@ workspace-default-features = "allow"
external-default-features = "allow" external-default-features = "allow"
# List of crates that are allowed. Use with care! # List of crates that are allowed. Use with care!
allow = [ allow = [
#"ansi_term@0.11.0", #{ name = "ansi_term", version = "=0.11.0" },
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
] ]
# List of crates to deny # List of crates to deny
deny = [ deny = [
#"ansi_term@0.11.0", # Each entry the name of a crate and a version range. If version is
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, # 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 # Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate # is a direct dependency of the otherwise banned crate
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, #{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
{ name = "serde", version = ">1.0.171, <1.0.184", reason = "ships with prebuilt binaries" } { name = "serde", version = ">1.0.171, <1.0.184" }
] ]
# List of features to allow/deny # List of features to allow/deny
# Each entry the name of a crate and a version range. If version is # Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched. # not specified, all versions will be matched.
#[[bans.features]] #[[bans.features]]
#crate = "reqwest" #name = "reqwest"
# Features to not allow # Features to not allow
#deny = ["json"] #deny = ["json"]
# Features to allow # Features to allow
@ -210,18 +247,14 @@ deny = [
# Certain crates/versions that will be skipped when doing duplicate detection. # Certain crates/versions that will be skipped when doing duplicate detection.
skip = [ skip = [
#"ansi_term@0.11.0", #{ name = "ansi_term", version = "=0.11.0" },
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
] ]
# Similarly to `skip` allows you to skip certain crates during duplicate # Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive # detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is # dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite. # by default infinite.
skip-tree = [ skip-tree = [
{ name = "windows-sys" }, #{ name = "ansi_term", version = "=0.11.0", depth = 20 },
{ 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 },
] ]
# This section is considered when running `cargo deny check sources`. # 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-openpgp`]
* [`keyfork-derive-util`] * [`keyfork-derive-util`]
* [`keyfork-entropy`] * [`keyfork-entropy`]
* [`keyfork-mnemonic`] * [`keyfork-mnemonic-util`]
* [`keyfork-prompt`] * [`keyfork-prompt`]
* [`keyfork-qrcode`] * [`keyfork-qrcode`]
* [`keyfork-shard`] * [`keyfork-shard`]
@ -68,7 +68,7 @@ seed or close-to-root derivations.
* [`keyfork-derive-path-data`] * [`keyfork-derive-path-data`]
* [`keyfork-derive-util`] * [`keyfork-derive-util`]
* [`keyfork-frame`] * [`keyfork-frame`]
* [`keyfork-mnemonic`] * [`keyfork-mnemonic-util`]
* [`keyforkd-models`] * [`keyforkd-models`]
* [`serde`] * [`serde`]
* [`thiserror`] * [`thiserror`]
@ -129,7 +129,7 @@ BIP-0032 derivation.
* [`ed25519-dalek`]: Ed25519 key parsing and arithmetic. * [`ed25519-dalek`]: Ed25519 key parsing and arithmetic.
* [`hmac`]: Derivation of keys using HMAC. * [`hmac`]: Derivation of keys using HMAC.
* [`k256`]: secp256k1 (K-256) key parsing and arithmetic. * [`k256`]: secp256k1 (K-256) key parsing and arithmetic.
* [`keyfork-mnemonic`] * [`keyfork-mnemonic-util`]
* [`ripemd`]: Generating hash for fingerprinting of BIP-0032 derived data. * [`ripemd`]: Generating hash for fingerprinting of BIP-0032 derived data.
* [`serde`] * [`serde`]
* [`sha2`]: Generating hashes for fingerprinting and derivation of data. * [`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. * [`card-backend-pcsc`]: PCSC support for OpenPGP-card.
* [`hkdf`]: Key derivation for transport encryption keys. * [`hkdf`]: Key derivation for transport encryption keys.
* [`keyfork-derive-openpgp`] * [`keyfork-derive-openpgp`]
* [`keyfork-mnemonic`]: Encoding encrypted shards using mnemonics. * [`keyfork-mnemonic-util`]: Encoding encrypted shards using mnemonics.
* [`keyfork-prompt`] * [`keyfork-prompt`]
* [`keyfork-qrcode`]: Encoding and decoding of encrypted shards using QR codes. * [`keyfork-qrcode`]: Encoding and decoding of encrypted shards using QR codes.
* [`openpgp-card`]: OpenPGP card support. * [`openpgp-card`]: OpenPGP card support.
@ -193,7 +193,7 @@ Frame data in a length-storing checksum-verified format.
* [`thiserror`] * [`thiserror`]
* [`tokio`]: Read and write from AsyncRead and AsyncWrite sources. * [`tokio`]: Read and write from AsyncRead and AsyncWrite sources.
## `keyfork-mnemonic` ## `keyfork-mnemonic-util`
* [`hmac`]: Hash utilities. * [`hmac`]: Hash utilities.
* [`sha2`]: Checksum of mnemonic data and hash for pbkdf2 * [`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-prompt`
* [`keyfork-crossterm`]: Interacting with the terminal. * [`keyfork-crossterm`]: Interacting with the terminal.
* [`keyfork-mnemonic`] * [`keyfork-mnemonic-util`]
* [`thiserror`] * [`thiserror`]
## `keyfork-plumbing` ## `keyfork-plumbing`
@ -210,7 +210,7 @@ Frame data in a length-storing checksum-verified format.
Binaries for `keyfork-entropy` and `keyfork-mnemonic-from-seed`. Binaries for `keyfork-entropy` and `keyfork-mnemonic-from-seed`.
* [`keyfork-entropy`] * [`keyfork-entropy`]
* [`keyfork-mnemonic`] * [`keyfork-mnemonic-util`]
* [`smex`] * [`smex`]
## `keyfork-slip10-test-data` ## `keyfork-slip10-test-data`
@ -229,7 +229,7 @@ Zero-dependency hex encoding and decoding.
[`keyfork-derive-util`]: #keyfork-derive-util [`keyfork-derive-util`]: #keyfork-derive-util
[`keyfork-entropy`]: #keyfork-entropy [`keyfork-entropy`]: #keyfork-entropy
[`keyfork-frame`]: #keyfork-frame [`keyfork-frame`]: #keyfork-frame
[`keyfork-mnemonic`]: #keyfork-mnemonic [`keyfork-mnemonic-util`]: #keyfork-mnemonic-util
[`keyfork-prompt`]: #keyfork-prompt [`keyfork-prompt`]: #keyfork-prompt
[`keyfork-qrcode`]: #keyfork-qrcode [`keyfork-qrcode`]: #keyfork-qrcode
[`keyfork-shard`]: #keyfork-shard [`keyfork-shard`]: #keyfork-shard

View File

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

View File

@ -10,7 +10,7 @@ cargo metadata --format-version=1 | jq -r '.packages[] | select(.source == null)
while read crate manifest_path version <&3; do while read crate manifest_path version <&3; do
crate_path="$(dirname $manifest_path)" crate_path="$(dirname $manifest_path)"
git_log="$(git log --format='%h %s' "$LAST_REF".."$CURRENT_REF" "$crate_path")" git_log="$(git log --format='%h %s' "$LAST_REF"..HEAD "$crate_path")"
git_tag="$(git tag --list "$crate-v${version}")" git_tag="$(git tag --list "$crate-v${version}")"
if test ! -z "$git_log" -a -z "$git_tag"; then if test ! -z "$git_log" -a -z "$git_tag"; then
{ {