Compare commits

..

1 Commits

129 changed files with 2681 additions and 6307 deletions

View File

@ -1,2 +0,0 @@
[registries.distrust]
index = "https://git.distrust.co/public/_cargo-index.git"

View File

@ -1,277 +0,0 @@
# Keyfork v0.2.4
This release includes a lot of "maintenance" changes, without any changes in
end-user functionality.
### Changes in keyfork:
The most significant change in this release is the reorganization of some of
the subcommands, where they would be better as enum-traits, such as `keyfork
derive` and `keyfork wizard`.
```
b254ba7 cleanup post-merge
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
5219c5a keyfork: enum-trait-ify choose-your-own commands
b26f296 keyfork-derive-path-data: move all pathcrafting here
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
```
### Changes in keyfork-derive-openpgp:
```
b254ba7 cleanup post-merge
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
b26f296 keyfork-derive-path-data: move all pathcrafting here
```
### Changes in keyfork-derive-path-data:
This change now centralizes all special Keyfork paths. This means crates should
no longer be required to implement their own path parsing logic.
```
b26f296 keyfork-derive-path-data: move all pathcrafting here
```
### Changes in keyfork-derive-util:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
```
### Changes in keyfork-mnemonic:
`keyfork-mnemonic-util` has finally been renamed to `keyfork-mnemonic`. The
method names `as_bytes() => as_slice()`, `to_bytes() => to_vec()`, and
`into_bytes() => into_vec()`, and the function names
`from_bytes() => try_from_slice()` and
`from_nonstandard_bytes() => from_array()`, have been implemented to more
closely represent the native types they are representing. Additionally,
`Mnemonic::generate_seed()` has been modified to return a constant size array;
this is a breaking change, but should have minimal impact.
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
3ee81b6 keyfork-mnemonic-util: impl as_slice to_vec into_vec
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
```
### Changes in keyfork-prompt:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
```
### Changes in keyfork-shard:
```
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
```
### Changes in keyforkd:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
536e6da keyforkd{,-client}: lots of documentationings
```
### Changes in keyforkd-client:
```
536e6da keyforkd{,-client}: lots of documentationings
```
# Keyfork v0.2.3
This release includes a bugfix for the wizard where the wizard was too strict
about when keys were "alive".
### Changes in keyfork:
```
dd4354f keyfork: bump keyfork-shard
```
### Changes in keyfork-shard:
```
ba64db8 update Cargo.toml and Cargo.lock
fa84a2a keyfork-shard: Be less strict about keys
```
# Keyfork v0.2.2
This release adds a new wizard, intended to be used at DEFCON 32.
### Changes in keyfork:
```
8d40d26 keyfork: add `bottoms-up` wizard
```
### Changes in keyfork-derive-openpgp:
This change also includes a minor change, allowing the derivation path for
`keyfork-derive-openpg` to derive further than two paths, which was useful in
the testing of the wizard.
```
8d40d26 keyfork: add `bottoms-up` wizard
```
# Keyfork v0.2.1
This release contains an emergency bugfix for Keyfork Shard, which previously
would not be able to properly verify the length of remote shard QR codes.
# Keyfork v0.2.0
Some of the changes in this release are based on feedback from audits
(publications coming soon!). The previous version of Keyfork, in almost every
configuration, is safe to use. The most significant change in this version
affects Keyfork Shard, which has an incompatible difference between this
version and the previous version. Information about shards, such as the length
of the shard, could be leaked and discovered by an attacker when using the
Remote Shard recovery mechanism.
An additional change is the requirement of hardened indices on the first two
levels of key derivation. This is due to Keyfork potentially leaking private
keys when hardened derivation is not used. To be completely honest, I don't
entirely understand the math behind it.
There is no reason to upgrade if Keyfork has been used as-is, as all supported
provisioners at this point in time require hardened derivation at all steps.
### Changes in keyfork:
```
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
5d2309e keyfork-prompt: add SecurePinValidator for making new, secure, PINs
cdf4015 keyfork wizard: use correct derivation path for re-deriving shard decryption keys
f0e5ae9 keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE
289cec3 keyfork wizard: upcast i and index to avoid wrapping add
9394500 keyfork-shard: generate nonce using hkdf
```
### Changes in keyfork-derive-openpgp:
```
f0e5ae9 keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE
9f089e7 keyfork-derive-openpgp: use .first() in place of .get(0)
```
### Changes in keyfork-derive-util:
```
de4e98a keyfork-derive-util: black-box checking all zeroes
48ccd7c keyfork-derive-util: add note about potential side-channel when verifying keys
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
1de466c keyfork-derive-util: allow zeroable input for non-master-key derivation
61871a7 keyfork-derive-util: make private and public test keys more visible
2bca0a1 keyfork-derive-util: make Test{Public,Private}Key public, rename Internal algorithm
```
### Changes in keyfork-entropy:
```
5438f4e keyfork-entropy: downgrade entropy size limit to warning
```
### Changes in keyfork-mnemonic-util:
```
001fc0b remove trailing hitespace :(
6a265ad keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes
```
### Changes in keyfork-prompt:
```
5d2309e keyfork-prompt: add SecurePinValidator for making new, secure, PINs
```
### Changes in keyfork-qrcode:
```
fa125e7 keyfork-qrcode: prefer Instant over SystemTime for infallible time comparison
```
### Changes in keyfork-shard:
```
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
1a036a0 keyfork-shard: clean up documentation for encrypted shard padding
e068743 keyfork-shard: display error message on duplicate key fingerprints found
23db509 keyfork-shard: improve wording for counting shardholders
9461772 keyfork-shard: ignore duplicate certificate entries
6a265ad keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes
c0b19e2 keyfork-shard: assert shared secrets are contributory
0fe5301 keyfork-shard: add in bug messages
08a66e2 keyfork-shard: base64 encode content instead of base16
6fa434e keyfork-shard: shorten length and pad inside encrypted block
9394500 keyfork-shard: generate nonce using hkdf
194d475 keyfork-shard: validate signatures using shard-specific validation requirements
```
### Changes in keyfork-zbar:
```
0c76869 .cargo/config.toml: add registry configuration :)
```
### Changes in keyforkd:
```
bcfcc87 keyforkd: add warning when loading seed with less than 128 bits
40551a5 keyforkd: require hardened derivation on two highest indexes
```
### Changes in keyforkd-client:
```
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
1de466c keyfork-derive-util: allow zeroable input for non-master-key derivation
40551a5 keyforkd: require hardened derivation on two highest indexes
```
### Changes in keyforkd-models:
```
40551a5 keyforkd: require hardened derivation on two highest indexes
```
# Keyfork v0.1.0
### Tagged releases:
* `keyfork-bin 0.1.0`
* `keyfork-bug 0.1.0`
* `keyfork-crossterm 0.27.1`
* `keyfork-derive-key 0.1.0`
* `keyfork-derive-openpgp 0.1.0`
* `keyfork-derive-path-data 0.1.0`
* `keyfork-derive-util 0.1.0`
* `keyfork-entropy 0.1.0`
* `keyfork-frame 0.1.0`
* `keyfork-mnemonic-util 0.2.0`
* `keyfork-prompt 0.1.0`
* `keyfork-qrcode 0.1.0`
* `keyfork-shard 0.1.0`
* `keyfork-slip10-test-data 0.1.0`
* `keyfork 0.1.0`
* `keyfork-zbar-sys 0.1.0`
* `keyfork-zbar 0.1.0`
* `keyforkd-client 0.1.0`
* `keyforkd-models 0.1.0`
* `keyforkd 0.1.0`
* `smex 0.1.0`

1387
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,68 +14,15 @@ members = [
"crates/qrcode/keyfork-qrcode",
"crates/qrcode/keyfork-zbar",
"crates/qrcode/keyfork-zbar-sys",
"crates/util/keyfork-bin",
"crates/util/keyfork-bug",
"crates/util/keyfork-crossterm",
"crates/util/keyfork-entropy",
"crates/util/keyfork-frame",
"crates/util/keyfork-mnemonic",
"crates/util/keyfork-mnemonic-util",
"crates/util/keyfork-prompt",
"crates/util/keyfork-slip10-test-data",
"crates/util/smex",
]
[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-key = { version = "0.1.1", path = "crates/derive/keyfork-derive-key", 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]
opt-level = 3
debug = true

View File

@ -1,6 +1,3 @@
.PHONY: default
default: docs/book
BASE_REF ?= main
HEAD_REF ?= HEAD
@ -13,21 +10,6 @@ define clone-repo
test `git -C $(1) rev-parse HEAD` = $(3)
endef
docs/book: docs/src/links.md $(shell find docs/src -type f -name '*.md')
mdbook build docs
mkdir -p docs/book/rustdoc
cargo test --doc
cargo doc --no-deps
cp -r ${CARGO_TARGET_DIR}/doc/* docs/book/rustdoc/
docs/src/links.md: docs/src/links.md.template
echo "<!-- DO NOT EDIT THIS FILE MANUALLY, edit links.md.template -->" > $@
envsubst < $< >> $@
.PHONY: touch
touch:
touch docs/src/links.md.template
.PHONY: review
review:
$(eval BASE_REF_PARSED := $(shell git rev-parse $(BASE_REF)))

View File

@ -75,7 +75,7 @@ Note: The following features are proposed, and may not yet be implemented.
* Offline
* Will exit if network access is detected to force you to keep keys offline
* Helps limit the risk of supply chain attacks
* Intended for use with QubesOS Vault VM, [AirgapOS](https://git.distrust.co/public/airgap), etc
* Intended for use with QubesOS Vault VM, AirgapOS, etc
* Private keys are installed to HSMs/TEEs for use by online machines
## Install
@ -178,8 +178,7 @@ keyfork recover mnemonic
This guide assumes you are sharding to an `N`-of-`M` system with `I` smart
cards per shardholder. The variables will be used in the following commands as
`$N`, `$M`, and `$I`. The smart card OpenPGP slots will be factory reset during
the process.
`$N`, `$M`, and `$I`. The smart cards will be factory reset during the process.
On an airgapped system, run the following command to generate a file containing
encrypted shards of a generated seed:

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "keyforkd-client"
version = "0.2.1"
version = "0.1.0"
edition = "2021"
license = "MIT"
@ -8,18 +8,18 @@ license = "MIT"
[features]
default = ["ed25519", "secp256k1"]
ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"]
secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
ed25519 = ["keyfork-derive-util/ed25519"]
secp256k1 = ["keyfork-derive-util/secp256k1"]
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false }
keyfork-frame = { workspace = true }
keyforkd-models = { workspace = true }
bincode = { workspace = true }
thiserror = { workspace = true }
k256 = { workspace = true, default-features = false, features = ["std"], optional = true }
ed25519-dalek = { workspace = true, optional = true }
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false }
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame" }
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" }
bincode = "1.3.3"
thiserror = "1.0.49"
[dev-dependencies]
keyfork-slip10-test-data = { workspace = true }
keyforkd = { workspace = true }
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" }
keyforkd = { path = "../keyforkd" }
tempfile = "3.9.0"
tokio = { version = "1.32.0", features = ["rt", "sync", "rt-multi-thread"] }

View File

@ -1,185 +1,9 @@
//! # The Keyforkd Client
//!
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". This allows
//! enforcement of policies, such as requiring at least two leves of a derivation path (for
//! instance, `m/0'` would not be allowed, but `m/0'/0'` would). The server is operated on a UNIX
//! socket with messages sent using the Keyfork Frame format.
//!
//! Programs using the Keyfork Client should ensure they are built against a compatible version of
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
//! `0.5.x`) will be compatible, but `0.5.x` will not be compatible with `0.6.x`. For versions
//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but
//! `1.x.y` will not be compatible with `2.0.0`.
//!
//! The Keyfork Client documentation makes extensive use of the `keyforkd::test_util` module.
//! This provides testing infrastructure to set up a temporary Keyfork Daemon. In
//! your code, you should assume the daemon has already been initialized, whether by another
//! process, on another terminal, or some other instance. At no point should a program deriving an
//! "endpoint" key have control over a mnemonic or a seed.
//!
//! ## Server Requests
//!
//! Keyfork is designed as a client-request/server-response model. The client sends a request, such
//! as a derivation request, and the server sends its response. Presently, the Keyfork server
//! supports the following requests:
//!
//! ### Request: Derive Key
//!
//! The client creates a derivation path of at least two indices and requests a derived XPrv
//! (Extended Private Key) from the server.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey as PrivateKey;
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! # Ok::<_, Error>(())
//! # })?;
//! Ok(())
//! }
//! ```
//!
//! ---
//!
//! Request objects are typically handled by the Keyfork Client library (such as with
//! [`Client::request_xprv`]). While unadvised, clients can also attempt to handle their own
//! requests, using [`Client::request`].
//!
//! ## Extended Private Keys
//!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
//! will be derived similarly between the server and the client.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::{DerivationIndex, DerivationPath};
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey as PrivateKey;
//! # fn check_wallet<T>(_: T) {}
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Index(#[from] keyfork_derive_util::IndexError),
//!
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! PrivateKey(#[from] keyfork_derive_util::PrivateKeyError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'/0'/0")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! // scan first 20 wallets
//! for index in 0..20 {
//! // use non-hardened derivation
//! let new_xprv = xprv.derive_child(&DerivationIndex::new(index, false)?);
//! check_wallet(new_xprv)
//! }
//! # Ok::<_, Error>(())
//! # })?;
//! Ok(())
//! }
//! ```
//!
//! ## Testing Infrastructure
//!
//! In tests, the `keyforkd::test_util` module and TestPrivateKeys can be used. These provide
//! useful utilities for writing tests that interact with the Keyfork Server without needing to
//! manually create the server for the purpose of the test. The `run_test` method can be used to
//! run a test, which can handle both returning errors and correctly translating panics (though,
//! the panics definitely won't look tidy).
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! Ok::<_, Error>(())
//! })?;
//! Ok(())
//! }
//! ```
//!
//! If you would rather write tests to panic rather than error, or would rather not deal with error
//! types, the Panicable type should be used, which will handle the Error type for the closure.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//!
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! keyforkd::test_util::Panicable::Ok(())
//! }).unwrap();
//! ```
//! A client for Keyforkd.
pub use std::os::unix::net::UnixStream;
use std::{collections::HashMap, path::PathBuf};
use keyfork_derive_util::{
request::{AsAlgorithm, DerivationRequest},
DerivationPath, ExtendedPrivateKey, PrivateKey,
};
use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf};
use keyfork_frame::{try_decode_from, try_encode_to, DecodeError, EncodeError};
use keyforkd_models::{Error as KeyforkdError, Request, Response};
use keyforkd_models::{Request, Response, Error as KeyforkdError};
#[cfg(test)]
mod tests;
@ -187,10 +11,6 @@ mod tests;
/// An error occurred while interacting with Keyforkd.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The response from the server did not match the request.
#[error("The response from the server did not match the request")]
InvalidResponse,
/// The environment variables used for determining a Keyforkd socket path were not set.
#[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")]
EnvVarsNotFound,
@ -217,11 +37,7 @@ pub enum Error {
/// An error encountered in Keyforkd.
#[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError),
/// An invalid key was returned.
#[error("Invalid key returned")]
InvalidKey,
Keyforkd(#[from] KeyforkdError)
}
#[allow(missing_docs)]
@ -265,21 +81,7 @@ pub struct Client {
}
impl Client {
/// Create a new client from a given already-connected [`UnixStream`]. This function is
/// provided in case a specific UnixStream has to be used; otherwise,
/// [`Client::discover_socket`] should be preferred.
///
/// # Examples
/// ```rust
/// use keyforkd_client::{Client, get_socket};
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// let mut socket = get_socket()?;
/// let mut client = Client::new(socket);
/// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap();
/// ```
/// Create a new client from a given already-connected [`UnixStream`].
pub fn new(socket: UnixStream) -> Self {
Self { socket }
}
@ -289,77 +91,12 @@ impl Client {
/// # Errors
/// An error may be returned if the required environment variables were not set or if the
/// socket could not be connected to.
///
/// # Examples
/// ```rust
/// use keyforkd_client::Client;
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// let mut client = Client::discover_socket()?;
/// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap();
/// ```
pub fn discover_socket() -> Result<Self> {
get_socket().map(|socket| Self { socket })
}
/// Request an [`ExtendedPrivateKey`] for a given [`DerivationPath`].
///
/// # Errors
/// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd.
/// * Keyforkd returned invalid data.
///
/// # Examples
/// ```rust
/// use std::str::FromStr;
///
/// use keyforkd_client::Client;
/// use keyfork_derive_util::DerivationPath;
/// # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
/// // use k256::SecretKey as PrivateKey;
/// // use ed25519_dalek::SigningKey as PrivateKey;
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
/// let mut client = Client::discover_socket().unwrap();
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
/// # keyforkd::test_util::Panicable::Ok(())
/// # }).unwrap();
/// ```
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
where
K: PrivateKey + Clone + AsAlgorithm,
{
let algo = K::as_algorithm();
let request = Request::Derivation(DerivationRequest::new(algo.clone(), path));
let response = self.request(&request)?;
match response {
Response::Derivation(d) => {
if d.algorithm != algo {
return Err(Error::InvalidResponse);
}
let depth = path.len() as u8;
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code)
.map_err(|_| Error::InvalidKey)
}
_ => Err(Error::InvalidResponse),
}
}
}
impl Client {
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
///
/// This function does not properly assert the association between a request type and a
/// response type, and does not perform any serialization of native objects into Request or
/// Response types, and should only be used when absolutely necessary.
///
/// # Errors
/// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error.

View File

@ -1,116 +1,100 @@
use crate::Client;
use keyfork_derive_util::{request::*, DerivationPath};
use keyfork_slip10_test_data::test_data;
use keyforkd::test_util::{run_test, Panicable};
use std::sync::mpsc::channel;
use std::{os::unix::net::UnixStream, str::FromStr};
use tokio::runtime::Builder;
#[test]
#[cfg(feature = "secp256k1")]
fn secp256k1_test_suite() {
use k256::SecretKey;
fn secp256k1() {
let tests = test_data()
.unwrap()
.remove("secp256k1")
.remove(&"secp256k1".to_string())
.unwrap();
for seed_test in tests {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests {
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len();
if chain_len < 2 {
continue;
}
if chain.iter().take(2).any(|index| !index.is_hardened()) {
continue;
}
// Consistency check: ensure the server and the client can each derive the same
// key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len {
// FIXME: Keyfork will only allow one request per session
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let right_path = path.inner()[i..]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
assert_eq!(
derived_xprv, keyforkd_xprv,
"{left_path} + {right_path} != {path}"
);
}
let req = DerivationRequest::new(
DerivationAlgorithm::Secp256k1,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(&response.data, test.private_key.as_slice());
// note: since client is non async, can't be single threaded
let rt = Builder::new_multi_thread().enable_io().build().unwrap();
let tempdir = tempfile::tempdir().unwrap();
for (i, per_seed) in tests.into_iter().enumerate() {
let mut socket_name = i.to_string();
socket_name.push_str("-keyforkd.sock");
let socket_path = tempdir.path().join(socket_name);
let (tx, rx) = channel();
let handle = rt.spawn({
let socket_path = socket_path.clone();
async move {
let seed = per_seed.seed.clone();
let mut server = keyforkd::UnixServer::bind(&socket_path).unwrap();
tx.send(()).unwrap();
let service = keyforkd::ServiceBuilder::new()
.layer(keyforkd::middleware::BincodeLayer::new())
.service(keyforkd::Keyforkd::new(seed));
server.run(service).await.unwrap();
}
Ok(())
})
.unwrap();
});
rx.recv().unwrap();
for test in &per_seed.tests {
let socket = UnixStream::connect(&socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
if chain.len() < 2 {
continue;
}
let req = DerivationRequest::new(
DerivationAlgorithm::Secp256k1,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(response.data, test.private_key);
}
handle.abort();
}
}
#[test]
#[cfg(feature = "ed25519")]
fn ed25519_test_suite() {
use ed25519_dalek::SigningKey;
fn ed25519() {
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
let tests = test_data().unwrap().remove("ed25519").unwrap();
for seed_test in tests {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| {
for test in seed_test.tests {
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len();
if chain_len < 2 {
continue;
}
for i in 2..chain_len {
// Consistency check: ensure the server and the client can each derive the same
// key using an XPrv, for all but the last XPrv, which is verified after this
let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let right_path = path.inner()[i..]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SigningKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_path).unwrap();
let keyforkd_xprv = client.request_xprv::<SigningKey>(&path).unwrap();
assert_eq!(
derived_xprv, keyforkd_xprv,
"{left_path} + {right_path} != {path}"
);
}
let req = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(&response.data, test.private_key.as_slice());
let rt = Builder::new_multi_thread().enable_io().build().unwrap();
let tempdir = tempfile::tempdir().unwrap();
for (i, per_seed) in tests.into_iter().enumerate() {
let mut socket_name = i.to_string();
socket_name.push_str("-keyforkd.sock");
let socket_path = tempdir.path().join(socket_name);
let (tx, rx) = channel();
let handle = rt.spawn({
let socket_path = socket_path.clone();
async move {
let seed = per_seed.seed.clone();
let mut server = keyforkd::UnixServer::bind(&socket_path).unwrap();
tx.send(()).unwrap();
let service = keyforkd::ServiceBuilder::new()
.layer(keyforkd::middleware::BincodeLayer::new())
.service(keyforkd::Keyforkd::new(seed));
server.run(service).await.unwrap();
}
Panicable::Ok(())
})
.unwrap();
});
rx.recv().unwrap();
for test in &per_seed.tests {
let socket = UnixStream::connect(&socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
if chain.len() < 2 {
continue;
}
let req = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(response.data, test.private_key);
}
handle.abort();
}
}

View File

@ -1,12 +1,12 @@
[package]
name = "keyforkd-models"
version = "0.2.0"
version = "0.1.0"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false }
serde = { workspace = true }
thiserror = { workspace = true }
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false }
serde = { version = "1.0.190", features = ["derive"] }
thiserror = "1.0.50"

View File

@ -43,10 +43,6 @@ pub enum DerivationError {
#[error("Invalid derivation length: Expected at least 2, actual: {0}")]
InvalidDerivationLength(usize),
/// The derivation request did not use hardened derivation on the 2 highest indexes.
#[error("Invalid derivation paths: expected index #{0} (1) to be hardened")]
InvalidDerivationPath(usize, u32),
/// An error occurred while deriving data.
#[error("Derivation error: {0}")]
Derivation(String),

View File

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

View File

@ -34,15 +34,3 @@ request, as well as its best-effort guess on what path is being derived (using
the `keyfork-derive-path-data` crate), to inform the user of what keys are
requested. Once the server sends the client the new extended private key, the
client can then choose to use the key as-is, or derive further keys.
## Testing
A Keyfork server can be automatically started by using [`test_util::run_test`].
The function accepts a closure, starting the server before the closure is run,
and closing the server after the closure has completed. This may be useful for
people writing software that interacts with the Keyfork server, such as a
deriver or a provisioner. A test seed must be provided, but can be any content.
The closure accepts one argument, the path of the UNIX socket from which the
server can be accessed.
Examples of the test utility can be seen in the `keyforkd-client` crate.

View File

@ -5,7 +5,7 @@ use std::{
path::{Path, PathBuf},
};
pub use keyfork_mnemonic::Mnemonic;
pub use keyfork_mnemonic_util::Mnemonic;
pub use tower::ServiceBuilder;
#[cfg(feature = "tracing")]
@ -30,8 +30,6 @@ pub use error::Keyforkd as KeyforkdError;
pub use server::UnixServer;
pub use service::Keyforkd;
pub mod test_util;
/// Set up a Tracing subscriber, defaulting to debug mode.
#[cfg(feature = "tracing")]
pub fn setup_registry() {
@ -57,7 +55,7 @@ pub async fn start_and_run_server_on(
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
// TODO: passphrase support and/or store passphrase with mnemonic
.service(Keyforkd::new(mnemonic.generate_seed(None).to_vec()));
.service(Keyforkd::new(mnemonic.seed(None)?));
let mut server = match UnixServer::bind(socket_path) {
Ok(s) => s,

View File

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

View File

@ -1,9 +1,6 @@
//! A UNIX socket server to run a Tower Service.
use keyfork_frame::{
asyncext::{try_decode_from, try_encode_to},
DecodeError, EncodeError,
};
use keyfork_frame::asyncext::{try_decode_from, try_encode_to};
use std::{
io::Error,
path::{Path, PathBuf},
@ -20,34 +17,6 @@ pub struct UnixServer {
listener: UnixListener,
}
/// This feels like a hack, but this is a convenient way to use the same method to quickly verify
/// something across two different error types.
trait IsDisconnect {
fn is_disconnect(&self) -> bool;
}
impl IsDisconnect for DecodeError {
fn is_disconnect(&self) -> bool {
if let Self::Io(e) = self {
if let std::io::ErrorKind::UnexpectedEof = e.kind() {
return true;
}
}
false
}
}
impl IsDisconnect for EncodeError {
fn is_disconnect(&self) -> bool {
if let Self::Io(e) = self {
if let std::io::ErrorKind::UnexpectedEof = e.kind() {
return true;
}
}
false
}
}
impl UnixServer {
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also creates a ctrl_c handler to automatically clean up the socket file.
///
@ -99,85 +68,55 @@ impl UnixServer {
#[cfg(feature = "tracing")]
debug!("new socket connected");
tokio::spawn(async move {
let mut has_processed_request = false;
// Process requests until an error occurs or a client disconnects
loop {
let bytes = match try_decode_from(&mut socket).await {
Ok(bytes) => bytes,
Err(e) => {
if e.is_disconnect() {
#[cfg(feature = "tracing")]
if !has_processed_request {
debug!("client disconnected before sending any response");
}
return;
}
#[cfg(feature = "tracing")]
debug!(%e, "Error reading DerivationPath from socket");
let content = e.to_string().bytes().collect::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")]
if let Err(error) = result {
debug!(%error, "Error sending error to client");
}
return;
}
};
let app = match app.ready().await {
Ok(app) => app,
Err(e) => {
#[cfg(feature = "tracing")]
debug!(%e, "Could not poll ready");
let content = e.to_string().bytes().collect::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")]
if let Err(error) = result {
debug!(%error, "Error sending error to client");
}
return;
}
};
let response = match app.call(bytes.into()).await {
Ok(response) => response,
Err(e) => {
#[cfg(feature = "tracing")]
debug!(%e, "Error reading DerivationPath from socket");
let content = e.to_string().bytes().collect::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")]
if let Err(error) = result {
if error.is_disconnect() {
#[cfg(feature = "tracing")]
if has_processed_request {
debug!("client disconnected while sending error frame");
}
return;
}
debug!(%error, "Error sending error to client");
}
has_processed_request = true;
// The error has been successfully sent, the client may perform
// another request.
continue;
}
}
.into();
if let Err(e) = try_encode_to(&response[..], &mut socket).await {
if e.is_disconnect() {
#[cfg(feature = "tracing")]
if has_processed_request {
debug!("client disconnected while sending success frame");
}
return;
}
let bytes = match try_decode_from(&mut socket).await {
Ok(bytes) => bytes,
Err(e) => {
#[cfg(feature = "tracing")]
debug!(%e, "Error sending response to client");
debug!(%e, "Error reading DerivationPath from socket");
let content = e.to_string().bytes().collect::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")]
if let Err(error) = result {
debug!(%error, "Error sending error to client");
}
return;
}
};
has_processed_request = true;
let app = match app.ready().await {
Ok(app) => app,
Err(e) => {
#[cfg(feature = "tracing")]
debug!(%e, "Could not poll ready");
let content = e.to_string().bytes().collect::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")]
if let Err(error) = result {
debug!(%error, "Error sending error to client");
}
return;
}
};
let response = match app.call(bytes.into()).await {
Ok(response) => response,
Err(e) => {
#[cfg(feature = "tracing")]
debug!(%e, "Error reading DerivationPath from socket");
let content = e.to_string().bytes().collect::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")]
if let Err(error) = result {
debug!(%error, "Error sending error to client");
}
return;
}
}
.into();
if let Err(e) = try_encode_to(&response[..], &mut socket).await {
#[cfg(feature = "tracing")]
debug!(%e, "Error sending response to client");
}
});
}

View File

@ -12,7 +12,7 @@ use keyfork_derive_path_data::guess_target;
// use keyfork_derive_util::request::{DerivationError, DerivationRequest, DerivationResponse};
use keyforkd_models::{DerivationError, Error, Request, Response};
use tower::Service;
use tracing::{info, warn};
use tracing::info;
// NOTE: All values implemented in Keyforkd must implement Clone with low overhead, either by
// using an Arc or by having a small signature. This is because Service<T> takes &mut self.
@ -38,9 +38,6 @@ impl std::fmt::Debug for Keyforkd {
impl Keyforkd {
/// Create a new instance of Keyfork from a given seed.
pub fn new(seed: Vec<u8>) -> Self {
if seed.len() < 16 {
warn!("Entropy size is lower than 128 bits: {} bits.", seed.len() * 8);
}
Self {
seed: Arc::new(seed),
}
@ -72,18 +69,6 @@ impl Service<Request> for Keyforkd {
return Err(DerivationError::InvalidDerivationLength(len).into());
}
if let Some((i, unhardened_index)) = req
.path()
.iter()
.take(2)
.enumerate()
.find(|(_, index)| {
!index.is_hardened()
})
{
return Err(DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into())
}
#[cfg(feature = "tracing")]
if let Some(target) = guess_target(req.path()) {
info!("Deriving path: {target}");
@ -91,7 +76,7 @@ impl Service<Request> for Keyforkd {
info!("Deriving path: {}", req.path());
}
req.derive_with_master_seed(seed.as_ref())
req.derive_with_master_seed((*seed).clone())
.map(Response::Derivation)
.map_err(|e| DerivationError::Derivation(e.to_string()).into())
}),
@ -113,7 +98,7 @@ mod tests {
async fn properly_derives_secp256k1() {
let tests = test_data()
.unwrap()
.remove("secp256k1")
.remove(&"secp256k1".to_string())
.unwrap();
for per_seed in tests {
@ -125,9 +110,6 @@ mod tests {
if chain.len() < 2 {
continue;
}
if chain.iter().take(2).any(|index| !index.is_hardened()) {
continue;
}
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response: DerivationResponse = keyforkd
.ready()
@ -138,7 +120,7 @@ mod tests {
.unwrap()
.try_into()
.unwrap();
assert_eq!(&response.data, test.private_key.as_slice());
assert_eq!(response.data, test.private_key);
assert_eq!(response.chain_code.as_slice(), test.chain_code);
}
}
@ -146,7 +128,7 @@ mod tests {
#[tokio::test]
async fn properly_derives_ed25519() {
let tests = test_data().unwrap().remove("ed25519").unwrap();
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -168,7 +150,7 @@ mod tests {
.unwrap()
.try_into()
.unwrap();
assert_eq!(&response.data, test.private_key.as_slice());
assert_eq!(response.data, test.private_key);
assert_eq!(response.chain_code.as_slice(), test.chain_code);
}
}

View File

@ -1,124 +0,0 @@
//! # Keyforkd Test Utilities
//!
//! This module adds a helper to set up a Tokio runtime, start a Tokio runtime with a given seed,
//! start a Keyfork server on that runtime, and run a given test closure.
use crate::{middleware, Keyforkd, ServiceBuilder, UnixServer};
use tokio::runtime::Builder;
use keyfork_bug::bug;
#[derive(Debug, thiserror::Error)]
#[error("This error can never be instantiated")]
#[doc(hidden)]
pub enum UninstantiableError {}
/// A panicable result. This type can be used when a closure chooses to panic instead of
/// returning an error. This doesn't necessarily mean a closure _has_ to panic, and its absence
/// doesn't imply a closure _can't_ panic, but this is a useful utility function for writing tests,
/// to avoid the necessity of making custom error types.
///
/// ```rust
/// use keyforkd::test_util::Panicable;
/// let closure = || {
/// Panicable::Ok(())
/// };
/// assert!(closure().is_ok());
/// ```
pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
/// the server is capable of being interacted with. The test is in the form of a closure, expected
/// to return a [`Result`] where success is a unit type (test passed) and the error is any error
/// that happened during the test (alternatively, a panic may be used, and will be returned as an
/// error).
///
/// # Panics
/// The function may panic if any errors arise while configuring and using the Tokio multithreaded
/// runtime.
///
/// # Examples
/// The test utility provides a socket that can be connected to for deriving keys.
///
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
///
/// The `keyforkd-client` crate uses the `KEYFORKD_SOCKET_PATH` variable to determine the default
/// socket path. The test will export the environment variable so it may be used by default.
///
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
/// assert_eq!(std::env::var_os("KEYFORKD_SOCKET_PATH").unwrap(), path.as_os_str());
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
#[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
E: Send + 'static,
{
let rt = Builder::new_multi_thread()
.worker_threads(2)
.enable_io()
.build()
.expect(bug!(
"can't make tokio threaded IO runtime, should be enabled via feature flags"
));
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
let socket_path = socket_dir.path().join("keyforkd.sock");
let result = rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let server_handle = tokio::spawn({
let socket_path = socket_path.clone();
let seed = seed.to_vec();
async move {
let mut server =
UnixServer::bind(&socket_path).expect(bug!("can't bind unix socket"));
tx.send(())
.await
.expect(bug!("couldn't send server start signal"));
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
.service(Keyforkd::new(seed.to_vec()));
server.run(service).await.expect(bug!("Unable to start service"));
}
});
rx.recv()
.await
.expect(bug!("can't receive server start signal from channel"));
std::env::set_var("KEYFORKD_SOCKET_PATH", &socket_path);
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
let result = test_handle.await;
server_handle.abort();
result
});
if let Err(e) = result {
if let Ok(reason) = e.try_into_panic() {
std::panic::resume_unwind(reason);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_test() {
let seed = b"beefbeef";
run_test(seed, |_path| Panicable::Ok(())).expect("infallible");
}
}

View File

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

View File

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

View File

@ -1,13 +1,13 @@
[package]
name = "keyfork-derive-key"
version = "0.1.1"
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 }
keyforkd-client = { workspace = true }
smex = { workspace = true }
thiserror = { workspace = true }
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" }
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client" }
smex = { version = "0.1.0", path = "../../util/smex" }
thiserror = "1.0.48"

View File

@ -1,10 +1,10 @@
//! Query the Keyfork Server to generate a hex-encoded key for a given algorithm.
//!
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationError, DerivationRequest, DerivationResponse},
DerivationPath, PathError,
DerivationPath,
};
use keyforkd_client::Client;
@ -17,7 +17,7 @@ pub enum Error {
/// The given path could not be parsed.
#[error("Could not parse the given path: {0}")]
PathFormat(#[from] PathError),
PathFormat(#[from] keyfork_derive_util::path::Error),
/// The request to derive data failed.
#[error("Unable to perform key derivation request: {0}")]
@ -46,7 +46,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::discover_socket()?;
let request = DerivationRequest::new(algo, &path);
let response = client.request(&request.into())?;
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
println!("{}", smex::encode(&DerivationResponse::try_from(response)?.data));
Ok(())
}

View File

@ -1,19 +1,18 @@
[package]
name = "keyfork-derive-openpgp"
version = "0.1.3"
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
[features]
default = ["bin"]
default = []
bin = ["sequoia-openpgp/crypto-nettle"]
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
ed25519-dalek = { workspace = true }
sequoia-openpgp = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
keyfork-derive-path-data = { workspace = true }
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] }
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", default-features = false, features = ["ed25519"] }
ed25519-dalek = "2.0.0"
sequoia-openpgp = { version = "1.17.0", default-features = false }
anyhow = "1.0.75"
thiserror = "1.0.49"

View File

@ -1,14 +1,13 @@
//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data.
//! Creation of OpenPGP certificates from BIP-0032 derived data.
use std::{
str::FromStr,
time::{Duration, SystemTime, SystemTimeError},
use std::time::{Duration, SystemTime, SystemTimeError};
use derive_util::{
request::{DerivationResponse, TryFromDerivationResponseError},
DerivationIndex, ExtendedPrivateKey, PrivateKey,
};
use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey};
use ed25519_dalek::SigningKey;
pub use keyfork_derive_util as derive_util;
pub use sequoia_openpgp as openpgp;
use sequoia_openpgp::{
packet::{
key::{Key4, PrimaryRole, SubordinateRole},
@ -18,15 +17,7 @@ use sequoia_openpgp::{
types::{KeyFlags, SignatureType},
Cert, Packet,
};
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
// something like Prv or PrvKey.
/// The private key type used with OpenPGP.
pub type XPrvKey = SigningKey;
/// The extended private key type used with OpenPGP.
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
pub use sequoia_openpgp as openpgp;
/// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)]
@ -40,9 +31,13 @@ pub enum Error {
#[error("Key configured with both encryption and non-encryption key flags: {0:?}")]
InvalidKeyFlags(KeyFlags),
/// The derivation response contained incorrect data.
#[error("Incorrect derived data: {0}")]
IncorrectDerivedData(#[from] TryFromDerivationResponseError),
/// A derivation index could not be created from the given index.
#[error("Could not create derivation index: {0}")]
Index(#[from] IndexError),
Index(#[from] keyfork_derive_util::index::Error),
/// A derivation operation could not be performed against the private key.
#[error("Could not perform operation against private key: {0}")]
@ -65,64 +60,41 @@ pub enum Error {
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Create an OpenPGP Cert with private key data, with derived keys from the given derivation
/// response, keys, and User ID.
///
/// Certificates are created with a default expiration of one day, but may be configured to expire
/// later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as "15d" (15
/// days), "1m" (one month), or "2y" (two years).
/// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
/// ID.
///
/// # Errors
/// The function may error for any condition mentioned in [`Error`].
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.first() {
pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.get(0) {
Some(kf) if kf.for_certification() => kf,
_ => return Err(Error::NotCert),
};
let epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
let now = SystemTime::now();
let expiration_date = match std::env::var("KEYFORK_OPENPGP_EXPIRE").as_mut() {
Ok(var) => {
let ch = var.pop();
match (ch, u64::from_str(var)) {
(Some(ch @ ('d' | 'm' | 'y')), Ok(expire)) => {
let multiplier = match ch {
'd' => 1,
'm' => 30,
'y' => 365,
_ => unreachable!(),
};
now + Duration::from_secs(60 * 60 * 24 * expire * multiplier)
}
_ => now + Duration::from_secs(60 * 60 * 24),
}
}
Err(_) => now + Duration::from_secs(60 * 60 * 24),
};
let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
// Create certificate with initial key and signature
let xprv = ExtendedPrivateKey::<SigningKey>::try_from(data)?;
let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?;
let mut primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519(
let primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519(
&PrivateKey::to_bytes(derived_primary_key.private_key()),
epoch,
)?);
primary_key.set_creation_time(epoch)?;
let cert = Cert::from_packets(vec![Packet::SecretKey(primary_key.clone())].into_iter())?;
// Sign and attach primary key and primary userid
let builder = SignatureBuilder::new(SignatureType::PositiveCertification)
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
// .set_signature_creation_time(now)?
.set_key_validity_period(one_day.duration_since(epoch)?)?
.set_signature_creation_time(epoch)?
.set_key_flags(primary_key_flags.clone())?;
let binding = userid.bind(&mut primary_key.clone().into_keypair()?, &cert, builder)?;
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
let policy = sequoia_openpgp::policy::StandardPolicy::new();
// Set certificate expiration to configured expiration or (default) one day
// Set certificate expiration to one day
let mut keypair = primary_key.clone().into_keypair()?;
let signatures =
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;
let signatures = cert.set_expiration_time(&policy, None, &mut keypair, Some(one_day))?;
let cert = cert.insert_packets(signatures)?;
let mut cert = cert;
@ -135,7 +107,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let is_non_enc = subkey_flags.for_certification()
|| subkey_flags.for_signing()
|| subkey_flags.for_authentication();
let mut subkey = if is_enc && is_non_enc {
let subkey = if is_enc && is_non_enc {
return Err(Error::InvalidKeyFlags(subkey_flags.clone()));
} else if is_enc {
// Clamp key before exporting as OpenPGP. Reference:
@ -145,27 +117,32 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
bytes[0] &= 0b1111_1000;
bytes[31] &= !0b1000_0000;
bytes[31] |= 0b0100_0000;
Key::from(Key4::<_, SubordinateRole>::import_secret_cv25519(
&bytes, None, None, epoch,
)?)
Key::from(
Key4::<_, SubordinateRole>::import_secret_cv25519(
&bytes,
None,
None,
epoch,
)?
)
} else {
Key::from(Key4::<_, SubordinateRole>::import_secret_ed25519(
&PrivateKey::to_bytes(derived_key.private_key()),
epoch,
)?)
Key::from(
Key4::<_, SubordinateRole>::import_secret_ed25519(
&PrivateKey::to_bytes(derived_key.private_key()),
epoch,
)?
)
};
subkey.set_creation_time(epoch)?;
// As per OpenPGP spec, signing keys must backsig the primary key
let builder = if subkey_flags.for_signing() {
SignatureBuilder::new(SignatureType::SubkeyBinding)
.set_key_flags(subkey_flags.clone())?
// .set_signature_creation_time(epoch)?
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
.set_signature_creation_time(epoch)?
.set_key_validity_period(one_day.duration_since(epoch)?)?
.set_embedded_signature(
SignatureBuilder::new(SignatureType::PrimaryKeyBinding)
//.set_signature_creation_time(epoch)?
.set_signature_creation_time(epoch)?
.sign_primary_key_binding(
&mut subkey.clone().into_keypair()?,
&primary_key,
@ -175,8 +152,8 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
} else {
SignatureBuilder::new(SignatureType::SubkeyBinding)
.set_key_flags(subkey_flags.clone())?
// .set_signature_creation_time(epoch)?
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
.set_signature_creation_time(epoch)?
.set_key_validity_period(one_day.duration_since(epoch)?)?
};
// Sign subkey with primary key and attach to cert

View File

@ -1,18 +1,13 @@
//! Query the Keyfork Servre to derive an OpenPGP Secret Key.
//!
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::DerivationPath;
use keyfork_derive_path_data::paths;
use keyforkd_client::Client;
use ed25519_dalek::SigningKey;
use sequoia_openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath,
};
use keyforkd_client::Client;
use sequoia_openpgp::{packet::UserID, types::KeyFlags, armor::{Kind, Writer}, serialize::Marshal};
#[derive(Debug, thiserror::Error)]
enum Error {
@ -79,14 +74,16 @@ fn validate(
subkey_format: &str,
default_userid: &str,
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
let index = paths::OPENPGP.inner().first().unwrap();
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let path = DerivationPath::from_str(path)?;
assert!(path.len() >= 2, "Expected path of at least m/{index}/account_id'");
assert_eq!(2, path.len(), "Expected path of m/{index}/account_id'");
let given_index = path.iter().next().expect("checked .len() above");
assert_eq!(
index, given_index,
&index, given_index,
"Expected derivation path starting with m/{index}, got: {given_index}",
);
@ -111,17 +108,20 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
_ => panic!("Usage: {program_name} path subkey_format default_userid"),
};
let derived_xprv = Client::discover_socket()?.request_xprv::<SigningKey>(&path)?;
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let derived_data: DerivationResponse = Client::discover_socket()?
.request(&request.into())?
.try_into()?;
let subkeys = subkey_format
.iter()
.map(|kt| kt.inner().clone())
.collect::<Vec<_>>();
let cert = keyfork_derive_openpgp::derive(derived_xprv, subkeys.as_slice(), &default_userid)?;
let cert = keyfork_derive_openpgp::derive(derived_data, subkeys.as_slice(), &default_userid)?;
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)?;
}

View File

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

View File

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

View File

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

View File

@ -1,51 +0,0 @@
# Keyfork Derive: BIP-0032 Key Derivation
Keyfork offers a [BIP-0032] based hierarchial key derivation system enabling
the ability to create keys based on a [BIP-0032] seed, a value between 128 to
512 bits. The keys can be made using any algorithm supported by Keyfork Derive.
Newtypes can be added to wrap around foreign key types that aren't supported by
Keyfork.
Keys derived with the same parameters, from the same seed, will _always_ return
the same value. This makes Keyfork a reliable backend for generating encryption
or signature keys, as every key can be recovered using the previously used
derivation algorithm. However, this may be seen as a concern, as all an
attacker may need to recreate all previously-used seeds would be the original
derivation seed. For this reason, it is recommended to use the Keyfork server
for derivation from the root seed. The Keyfork server will ensure the root seed
and any highest-level keys (such as BIP-44, BIP-85, etc.) keys are not leaked.
The primary use case of Keyfork Derive will be the creation of Derivation
Requests, to be used by Keyforkd Client. In the included example, derivation is
performed directly on a master seed. This is how Keyforkd works internally.
## Example
```rust
use std::str::FromStr;
use keyfork_mnemonic::Mnemonic;
use keyfork_derive_util::{*, request::*};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mnemonic = Mnemonic::from_str(
"enter settle kiwi high shift absorb protect sword talent museum lazy okay"
)?;
let path = DerivationPath::from_str("m/44'/0'/0'/0/0")?;
let request = DerivationRequest::new(
DerivationAlgorithm::Secp256k1,
&path
);
let key1 = request.derive_with_mnemonic(&mnemonic)?;
let seed = mnemonic.seed(None)?;
let key2 = request.derive_with_master_seed(&seed)?;
assert_eq!(key1, key2);
Ok(())
}
```
[BIP-0032]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

View File

@ -1,49 +1,4 @@
//! # Extended Key Derivation
//!
//! Oftentimes, a client will want to create multiple keys. Some examples may include deriving
//! non-hardened public keys to see how many wallets have been used, deriving multiple OpenPGP
//! keys, or generally avoiding key reuse. While Keyforkd locks the root mnemonic and the
//! first-level derivation, any second-level derivations acquired from Keyforkd (for example,
//! `"m/44'/0'"`) can be used to derive further keys by converting the key to an Extended Public
//! Key or Extended Private Key and calling [`ExtendedPublicKey::derive_child`] or
//! [`ExtendedPrivateKey::derive_child`].
//!
//! # Examples
//! ```rust
//! use std::str::FromStr;
//! use keyfork_mnemonic::Mnemonic;
//! use keyfork_derive_util::{*, request::*};
//! use k256::SecretKey;
//!
//! # fn check_wallet<T: PublicKey>(_: ExtendedPublicKey<T>) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let mnemonic = Mnemonic::from_str(
//! # "enter settle kiwi high shift absorb protect sword talent museum lazy okay"
//! # )?;
//! let path = DerivationPath::from_str("m/44'/0'/0'/0")?;
//! let request = DerivationRequest::new(
//! DerivationAlgorithm::Secp256k1, // The algorithm of k256::SecretKey
//! &path,
//! );
//!
//! let response = // perform a Keyforkd Client request...
//! # request.derive_with_mnemonic(&mnemonic)?;
//! let key: ExtendedPrivateKey<SecretKey> = response.try_into()?;
//! let pubkey = key.extended_public_key();
//! drop(key);
//!
//! for account in (0..20).map(|i| DerivationIndex::new(i, false).unwrap()) {
//! let derived_key = pubkey.derive_child(&account)?;
//! check_wallet(derived_key);
//! }
//!
//! Ok(())
//! }
//! ```
#[allow(missing_docs)]
///
pub mod private_key;
#[allow(missing_docs)]
///
pub mod public_key;
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};

View File

@ -1,7 +1,5 @@
use crate::{DerivationIndex, DerivationPath, ExtendedPublicKey, PrivateKey, PublicKey};
use keyfork_bug::bug;
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha512;
@ -12,10 +10,18 @@ const KEY_SIZE: usize = 256;
/// Errors associated with creating or deriving Extended Private Keys.
#[derive(Error, Clone, Debug)]
pub enum Error {
/// The seed has an unsuitable length; supported lengths are 16 bytes, 32 bytes, or 64 bytes.
#[error("Seed had an unsuitable length: {0}")]
BadSeedLength(usize),
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
#[error("Reached maximum depth for key derivation")]
Depth,
/// This should never happen. HMAC keys should be able to take any size input.
#[error("Invalid length for HMAC key while generating master key (report me!)")]
HmacInvalidLength(#[from] hmac::digest::InvalidLength),
/// An unknown error occurred while deriving a child key.
#[error("Unknown error while deriving child key")]
Derivation,
@ -27,114 +33,23 @@ pub enum Error {
/// The given slice was of an inappropriate size to create a Private Key.
#[error("The given slice was of an inappropriate size to create a Private Key")]
InvalidSliceError(#[from] std::array::TryFromSliceError),
/// The given data was not a valid key for the chosen key type.
#[error("The given data was not a valid key for the chosen key type")]
InvalidKey,
}
type Result<T, E = Error> = std::result::Result<T, E>;
type ChainCode = [u8; 32];
type HmacSha512 = Hmac<Sha512>;
/// A reference to a variable-length seed. Keyfork automatically supports a seed of 128 bits,
/// 256 bits, or 512 bits, but because the master key is derived from a hashed seed, in theory
/// any amount of bytes could be used. It is not advised to use a variable-length seed longer
/// than 256 bits, as a brute-force attack on the master key could be performed in 2^256
/// attempts.
///
/// Mnemonics use a 512 bit seed, as knowledge of the mnemonics' words (such as through a side
/// channel attack) could leak which individual word is used, but not the order the words are
/// used in. Using a 512 bit hash to generate the seed results in a more computationally
/// expensive brute-force requirement.
pub struct VariableLengthSeed<'a> {
seed: &'a [u8],
}
impl<'a> VariableLengthSeed<'a> {
/// Create a new VariableLengthSeed.
///
/// # Examples
/// ```rust
/// use sha2::{Sha256, Digest};
/// use keyfork_derive_util::VariableLengthSeed;
///
/// let data = b"the missile is very eepy and wants to take a small sleeb";
/// let seed = VariableLengthSeed::new(data);
/// ```
pub fn new(seed: &'a [u8]) -> Self {
Self { seed }
}
}
mod as_private_key {
use super::VariableLengthSeed;
pub trait AsPrivateKey {
fn as_private_key(&self) -> &[u8];
}
impl AsPrivateKey for [u8; 16] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for [u8; 32] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for [u8; 64] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for VariableLengthSeed<'_> {
fn as_private_key(&self) -> &[u8] {
self.seed
}
}
}
/// Extended private keys derived using BIP-0032.
///
/// Generic over types implementing [`PrivateKey`].
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Clone, Serialize, Deserialize)]
pub struct ExtendedPrivateKey<K: PrivateKey + Clone> {
/// The internal private key data.
#[serde(with = "serde_with")]
private_key: K,
depth: u8,
chain_code: ChainCode,
}
mod serde_with {
use super::*;
pub(crate) fn serialize<S, K>(value: &K, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
K: PrivateKey + Clone,
{
serializer.serialize_bytes(&value.to_bytes())
}
pub(crate) fn deserialize<'de, D, K>(deserializer: D) -> Result<K, D::Error>
where
D: serde::Deserializer<'de>,
K: PrivateKey + Clone,
{
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
let bytes: [u8; 32] = variable_len_bytes.try_into().expect(bug!(
"unable to parse serialized private key; no support for static len"
));
Ok(K::from_bytes(&bytes).expect(bug!("could not deserialize key with invalid scalar")))
}
}
impl<K: PrivateKey + Clone> std::fmt::Debug for ExtendedPrivateKey<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtendedPrivateKey")
@ -152,54 +67,36 @@ where
/// Generate a new [`ExtendedPrivateKey`] from a seed, ideally from a 12-word or 24-word
/// mnemonic, but may take 16-byte seeds.
///
/// # Errors
/// The function may return an error if the derived master key could not be parsed as a key for
/// the given algorithm (such as exceeding values on the secp256k1 curve).
/// # Panics
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// ```
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Result<Self> {
Self::new_internal(seed.as_private_key())
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
///
/// # Errors
///
/// An error may be returned if:
/// * The given seed had an incorrect length.
/// * A `HmacSha512` can't be constructed - this should be impossible.
pub fn new(seed: impl AsRef<[u8]>) -> Result<Self> {
Self::new_internal(seed.as_ref())
}
fn new_internal(seed: &[u8]) -> Result<Self> {
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
.expect(bug!("HmacSha512 InvalidLength should be infallible"))
let len = seed.len();
if ![16, 32, 64].contains(&len) {
return Err(Error::BadSeedLength(len));
}
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())?
.chain_update(seed)
.finalize()
.into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
// Verify the master key is nonzero, hopefully avoiding side-channel attacks.
let mut has_any_nonzero = false;
// deoptimize arithmetic smartness
for byte in private_key.iter().map(std::hint::black_box) {
if *byte != 0 {
// deoptimize break
has_any_nonzero = std::hint::black_box(true);
}
}
assert!(has_any_nonzero, bug!("hmac function returned all-zero master key"));
Self::from_parts(
private_key
.try_into()
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
Self::new_from_parts(
private_key,
0,
// Checked: chain_code is always the same length, hash is static size
chain_code
.try_into()
.expect(bug!("Invalid chain code length")),
chain_code.try_into().expect("Invalid chain code length"),
)
}
@ -207,151 +104,35 @@ where
///
/// # Errors
/// The function may error if a private key can't be created from the seed.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
/// ```
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
match K::from_bytes(key) {
Ok(key) => {
Ok(Self {
private_key: key,
depth,
chain_code,
})
}
Err(_) => Err(Error::InvalidKey),
}
pub fn new_from_parts(seed: &[u8], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
Ok(Self {
private_key: K::from_bytes(seed.try_into()?),
depth,
chain_code,
})
}
/// Returns a reference to the [`PrivateKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::PrivateKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key)?);
/// # Ok(())
/// # }
/// ```
pub fn private_key(&self) -> &K {
&self.private_key
}
/// Create an [`ExtendedPublicKey`] for the current [`PrivateKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::PrivateKey as _,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// # let known_key: [u8; 33] = [
/// # 0, 242, 26, 9, 159, 68, 199, 0, 206, 71, 248,
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
/// # ];
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let xpub = xprv.extended_public_key();
/// assert_eq!(known_key, xpub.public_key().to_bytes());
/// # Ok(())
/// # }
/// ```
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
ExtendedPublicKey::from_parts(self.public_key(), self.depth, self.chain_code)
ExtendedPublicKey::new(self.public_key(), self.chain_code)
}
/// Return a public key for the current [`PrivateKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::PrivateKey as _,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let pubkey = xprv.public_key();
/// # Ok(())
/// # }
/// ```
pub fn public_key(&self) -> K::PublicKey {
self.private_key.public_key()
}
/// Returns the current depth.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(xprv.depth(), 4);
/// # Ok(())
/// # }
/// ```
pub fn depth(&self) -> u8 {
self.depth
}
/// Returns a copy of the current chain code.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(chain_code, &xprv.chain_code());
/// # Ok(())
/// # }
/// ```
pub fn chain_code(&self) -> [u8; 32] {
self.chain_code
}
@ -359,29 +140,9 @@ where
/// Derive a child using the given [`DerivationPath`].
///
/// # Errors
///
/// An error may be returned under the same circumstances as
/// [`ExtendedPrivateKey::derive_child`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let path = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(0, false)?);
/// let derived_xprv = root_xprv.derive_path(&path)?;
/// # Ok(())
/// # }
/// ```
pub fn derive_path(&self, path: &DerivationPath) -> Result<Self> {
if path.path.is_empty() {
Ok(self.clone())
@ -404,38 +165,11 @@ where
/// * The depth exceeds the maximum depth [`u8::MAX`].
/// * A `HmacSha512` can't be constructed - this should be impossible.
/// * Deriving a child key fails. Check the documentation for your [`PrivateKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn check_empty(p: &ExtendedPrivateKey<PrivateKey>) -> Result<(), std::io::Error> {
/// # Ok(())
/// # }
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let bip44_wallet = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(0, false)?);
/// let change_xprv = root_xprv.derive_path(&bip44_wallet)?;
/// for account in (0..20).map(|i| DerivationIndex::new(i, false).unwrap()) {
/// let account_xprv = change_xprv.derive_child(&account)?;
/// check_empty(&account_xprv)?;
/// }
/// # Ok(())
/// # }
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
let depth = self.depth.checked_add(1).ok_or(Error::Depth)?;
let mut hmac = HmacSha512::new_from_slice(&self.chain_code)
.expect(bug!("HmacSha512 InvalidLength should be infallible"));
let mut hmac =
HmacSha512::new_from_slice(&self.chain_code).map_err(Error::HmacInvalidLength)?;
if index.is_hardened() {
hmac.update(&[0]);
hmac.update(&self.private_key.to_bytes());
@ -453,7 +187,7 @@ where
.derive_child(
&private_key
.try_into()
.expect(bug!("Invalid length for private key")),
.expect("Invalid length for private key"),
)
.map_err(|_| Error::Derivation)?;
@ -462,7 +196,7 @@ where
depth,
chain_code: chain_code
.try_into()
.expect(bug!("Invalid length for chain code")),
.expect("Invalid length for chain code"),
})
}
}

View File

@ -4,15 +4,13 @@ use hmac::{Hmac, Mac};
use sha2::Sha512;
use thiserror::Error;
use keyfork_bug::bug;
const KEY_SIZE: usize = 256;
/// Errors associated with creating or deriving Extended Public Keys.
#[derive(Error, Clone, Debug)]
pub enum Error {
/// BIP-0032 does not support hardened public key derivation from parent public keys.
#[error("Hardened child public keys may not be derived from parent public keys")]
/// BIP-0032 does not support deriving public keys from hardened private keys.
#[error("Public keys may not be derived when hardened")]
HardenedIndex,
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
@ -46,51 +44,15 @@ where
K: PublicKey,
{
/// Create a new [`ExtendedPublicKey`] from previously known values.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 33] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let pubkey = PublicKey::from_bytes(key);
/// let xpub = ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// # Ok(())
/// # }
/// ```
pub fn from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
pub fn new(public_key: K, chain_code: ChainCode) -> Self {
Self {
public_key,
depth,
depth: 0,
chain_code,
}
}
/// Return the internal [`PublicKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let key: &[u8; 33] = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// let pubkey = xpub.public_key();
/// # Ok(())
/// # }
/// ```
pub fn public_key(&self) -> &K {
&self.public_key
}
@ -108,25 +70,6 @@ where
/// * The depth exceeds the maximum depth [`u8::MAX`].
/// * A `HmacSha512` can't be constructed - this should be impossible.
/// * Deriving a child key fails. Check the documentation for your [`PublicKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let key: &[u8; 33] = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// let index = DerivationIndex::new(0, false)?;
/// let child = xpub.derive_child(&index)?;
/// # Ok(())
/// # }
/// ```
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
if index.is_hardened() {
return Err(Error::HardenedIndex);
@ -144,11 +87,9 @@ where
let (child_key, chain_code) = hmac.split_at(KEY_SIZE / 8);
let derived_key = self
.public_key
.derive_child(child_key.try_into().expect(bug!("Invalid key length")))
.derive_child(child_key.try_into().expect("Invalid key length"))
.map_err(|_| Error::Derivation)?;
let chain_code = chain_code
.try_into()
.expect(bug!("Invalid chain code length"));
let chain_code = chain_code.try_into().expect("Invalid chain code length");
Ok(Self {
public_key: derived_key,

View File

@ -23,20 +23,8 @@ impl DerivationIndex {
/// Creates a new [`DerivationIndex`].
///
/// # Errors
///
/// Returns an error if the index is larger than the hardened flag.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::*;
/// let bip44 = DerivationIndex::new(44, true).unwrap();
/// ```
///
/// Using a derivation index that is higher than 2^31 returns an error:
///
/// ```rust,should_panic
/// # use keyfork_derive_util::*;
/// let too_high = DerivationIndex::new(u32::MAX, true).unwrap();
/// ```
pub const fn new(index: u32, hardened: bool) -> Result<Self> {
if index & (0b1 << 31) > 0 {
return Err(Error::IndexTooLarge(index));
@ -58,13 +46,6 @@ impl DerivationIndex {
/// Return the internal derivation index. Note that if the derivation index is hardened, the
/// highest bit will be set, and the value can't be used to create a new derivation index.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::*;
/// assert_eq!(DerivationIndex::new(44, true).unwrap().inner(), 2147483692);
/// assert_eq!(DerivationIndex::new(200, false).unwrap().inner(), 200);
/// ```
pub fn inner(&self) -> u32 {
self.0
}
@ -73,15 +54,7 @@ impl DerivationIndex {
self.0.to_be_bytes()
}
/// Whether or not the index is hardened, allowing deriving the key from a known parent public
/// key.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::*;
/// assert_eq!(DerivationIndex::new(0, true).unwrap().is_hardened(), true);
/// assert_eq!(DerivationIndex::new(0, false).unwrap().is_hardened(), false);
/// ```
/// Whether or not the index is hardened, allowing deriving the key from a known parent key.
pub fn is_hardened(&self) -> bool {
self.0 & (0b1 << 31) != 0
}

View File

@ -1,27 +1,27 @@
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
#![doc = include_str!("../README.md")]
//! BIP-0032 derivation utilities.
///
pub mod extended_key;
pub mod request;
mod index;
mod path;
#[doc(hidden)]
///
pub mod index;
///
pub mod path;
///
pub mod private_key;
#[doc(hidden)]
///
pub mod public_key;
///
pub mod request;
#[cfg(test)]
mod tests;
#[doc(inline)]
pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}};
pub use crate::{
index::{DerivationIndex, Error as IndexError},
path::{DerivationPath, Error as PathError},
private_key::{PrivateKey, PrivateKeyError},
public_key::{PublicKey, PublicKeyError},
extended_key::{private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey},
index::DerivationIndex,
path::DerivationPath,
private_key::PrivateKey,
public_key::PublicKey,
};

View File

@ -51,30 +51,7 @@ impl DerivationPath {
self.path.push(index);
}
/// Return the inner path.
pub fn inner(&self) -> &Vec<DerivationIndex> {
&self.path
}
/// Append an index to the path, returning self to allow chaining method calls.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::*;
/// # fn discover_wallet(_p: DerivationPath) -> Result<bool, std::io::Error> { Ok(true) }
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let account = 0;
/// let path = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(account, true)?);
/// let mut has_wallet = false;
/// for index in (0..20).map(|i| DerivationIndex::new(i, true).unwrap()) {
/// has_wallet = has_wallet || discover_wallet(path.clone().chain_push(index))?;
/// }
/// # Ok(())
/// # }
/// ```
pub fn chain_push(mut self, index: DerivationIndex) -> Self {
self.path.push(index);
self

View File

@ -13,32 +13,9 @@ pub trait PrivateKey: Sized {
type Err: std::error::Error;
/// Create a Self from bytes.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// ```
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err>;
fn from_bytes(b: &PrivateKeyBytes) -> Self;
/// Convert a &Self to bytes.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// assert_eq!(key_data, &private_key.to_bytes());
/// ```
fn to_bytes(&self) -> PrivateKeyBytes;
/*
@ -50,40 +27,17 @@ pub trait PrivateKey: Sized {
*/
/// The initial key for BIP-0032 and SLIP-0010 derivation, such as secp256k1's "Bitcoin seed".
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// assert_eq!(OurPrivateKey::key(), "testing seed");
/// ```
fn key() -> &'static str;
/// Generate a [`Self::PublicKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// let public_key = private_key.public_key();
/// ```
fn public_key(&self) -> Self::PublicKey;
/// Derive a child [`PrivateKey`] with given `PrivateKeyBytes`. The implementation of
/// derivation is algorithm-specific and a specification should be consulted when implementing
/// this method.
/// Derive a child [`PrivateKey`] with given `PrivateKeyBytes`.
///
/// # Errors
///
/// An error may be returned if:
/// * An all-zero `other` is provided.
/// * A nonzero `other` is provided.
/// * An error specific to the given algorithm was encountered.
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
@ -100,10 +54,6 @@ pub enum PrivateKeyError {
/// For the given algorithm, the private key must be nonzero.
#[error("The provided private key must be nonzero, but is not")]
NonZero,
/// A scalar could not be constructed for the given algorithm.
#[error("A scalar could not be constructed for the given algorithm")]
InvalidScalar,
}
#[cfg(feature = "secp256k1")]
@ -118,8 +68,8 @@ impl PrivateKey for k256::SecretKey {
"Bitcoin seed"
}
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
Self::from_slice(b).map_err(|_| PrivateKeyError::InvalidScalar)
fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self::from_slice(b).expect("Invalid private key bytes")
}
fn to_bytes(&self) -> PrivateKeyBytes {
@ -132,19 +82,20 @@ impl PrivateKey for k256::SecretKey {
}
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
use k256::elliptic_curve::ScalarPrimitive;
use k256::{Scalar, Secp256k1};
// Construct a scalar from bytes
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(other.into());
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
let scalar = scalar.ok_or(PrivateKeyError::InvalidScalar)?;
let scalar = Scalar::from(scalar);
if other.iter().all(|n| n == &0) {
return Err(PrivateKeyError::NonZero);
}
let other = *other;
// Checked: See above nonzero check
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
.expect("Should have been able to get a NonZeroScalar");
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
let nonzero_scalar = Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
.ok_or(PrivateKeyError::NonZero)?;
Ok(Self::from(nonzero_scalar))
Ok(
Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
.map(Into::into)
.expect("Should be able to make Key"),
)
}
}
@ -157,8 +108,8 @@ impl PrivateKey for ed25519_dalek::SigningKey {
"ed25519 seed"
}
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
Ok(Self::from_bytes(b))
fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self::from_bytes(b)
}
fn to_bytes(&self) -> PrivateKeyBytes {
@ -178,47 +129,3 @@ impl PrivateKey for ed25519_dalek::SigningKey {
true
}
}
use crate::public_key::TestPublicKey;
/// A private key that can be used for testing purposes. Does not utilize any significant
/// cryptographic operations.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TestPrivateKey {
key: [u8; 32],
}
impl TestPrivateKey {
pub(crate) fn public_key(&self) -> TestPublicKey {
let mut bytes = [0u8; 33];
for (i, byte) in self.key.iter().enumerate() {
bytes[i + 1] = byte ^ 0xFF;
}
TestPublicKey { key: bytes }
}
}
impl PrivateKey for TestPrivateKey {
type PublicKey = TestPublicKey;
type Err = PrivateKeyError;
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
Ok(Self { key: *b })
}
fn to_bytes(&self) -> PrivateKeyBytes {
self.key
}
fn key() -> &'static str {
"testing seed"
}
fn public_key(&self) -> Self::PublicKey {
self.public_key()
}
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
Ok(Self { key: *other })
}
}

View File

@ -5,8 +5,6 @@ use ripemd::Ripemd160;
use sha2::Sha256;
use thiserror::Error;
use keyfork_bug::bug;
pub(crate) type PublicKeyBytes = [u8; 33];
/// Functions required to use an `ExtendedPublicKey`.
@ -21,51 +19,25 @@ pub trait PublicKey: Sized {
*/
/// Convert a &Self to bytes.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// let public_key_bytes = private_key.public_key().to_bytes();
/// ```
fn to_bytes(&self) -> PublicKeyBytes;
/// Derive a child [`PublicKey`] with given `PrivateKeyBytes`. The implementation of
/// derivation is algorithm-specific and a specification should be consulted when implementing
/// this method.
/// Derive a child [`PublicKey`] with given `PrivateKeyBytes`.
///
/// # Errors
///
/// An error may be returned if:
/// * An all-zero `other` is provided.
/// * A nonzero `other` is provided.
/// * An error specific to the given algorithm was encountered.
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
/// Create a BIP-0032/SLIP-0010 fingerprint from the public key.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// let fingerprint = private_key.public_key().fingerprint();
/// ```
fn fingerprint(&self) -> [u8; 4] {
let hash = Sha256::new().chain_update(self.to_bytes()).finalize();
let hash = Ripemd160::new().chain_update(hash).finalize();
// Note: Safety assured by type returned from Ripemd160
hash[..4]
.try_into()
.expect(bug!("Ripemd160 returned too little data"))
.expect("Ripemd160 returned too little data")
}
}
@ -77,10 +49,6 @@ pub enum PublicKeyError {
#[error("The provided public key must be nonzero, but is not")]
NonZero,
/// A scalar could not be constructed for the given algorithm.
#[error("A scalar could not be constructed for the given algorithm")]
InvalidScalar,
/// Public key derivation is unsupported for this algorithm.
#[error("Public key derivation is unsupported for this algorithm")]
DerivationUnsupported,
@ -89,7 +57,7 @@ pub enum PublicKeyError {
#[cfg(feature = "secp256k1")]
use k256::{
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
AffinePoint,
AffinePoint, NonZeroScalar,
};
#[cfg(feature = "secp256k1")]
@ -109,18 +77,15 @@ impl PublicKey for k256::PublicKey {
}
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
use k256::elliptic_curve::ScalarPrimitive;
use k256::{Secp256k1, Scalar};
if other.iter().all(|n| n == &0) {
return Err(PublicKeyError::NonZero);
}
// Checked: See above
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
.expect("Should have been able to get a NonZeroScalar");
// Construct a scalar from bytes
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into());
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
let scalar = scalar.ok_or(PublicKeyError::InvalidScalar)?;
let scalar = Scalar::from(scalar);
let point = self.to_projective() + (AffinePoint::generator() * scalar);
Ok(Self::from_affine(point.into())
.expect(bug!("Could not from_affine after scalar arithmetic")))
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
Ok(Self::from_affine(point.into()).expect("Could not from_affine after scalar arithmetic"))
}
}
@ -147,35 +112,3 @@ impl PublicKey for VerifyingKey {
Err(Self::Err::DerivationUnsupported)
}
}
/// A public key that can be used for testing purposes. Does not utilize any significant
/// cryptographic operations.
#[derive(Clone)]
pub struct TestPublicKey {
pub(crate) key: [u8; 33],
}
impl TestPublicKey {
/// Create a new TestPublicKey from the given bytes.
#[allow(dead_code)]
pub fn from_bytes(b: &[u8]) -> Self {
Self {
key: b
.try_into()
.expect(bug!("invalid size when constructing TestPublicKey")),
}
}
}
impl PublicKey for TestPublicKey {
type Err = PublicKeyError;
fn to_bytes(&self) -> PublicKeyBytes {
self.key
}
fn derive_child(&self, _other: PrivateKeyBytes) -> Result<Self, Self::Err> {
// whatever it takes for tests to pass...
Ok(self.clone())
}
}

View File

@ -1,30 +1,10 @@
// Because all algorithms make use of wildcard matching
#![allow(clippy::match_wildcard_for_single_variants)]
//! # Derivation Requests
//!
//! Derivation requests can be sent to Keyforkd using Keyforkd Client to request derivation from a
//! mnemonic or seed that has been loaded into Keyforkd.
//!
//! # Examples
//! ```rust
//! use std::str::FromStr;
//! use keyfork_derive_util::{DerivationPath, request::{DerivationRequest, DerivationAlgorithm}};
//!
//! let path = DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap();
//! let request = DerivationRequest::new(
//! DerivationAlgorithm::Secp256k1,
//! &path
//! );
//! ```
use crate::{
extended_key::private_key::{Error as XPrvError, VariableLengthSeed},
private_key::{PrivateKey, TestPrivateKey},
DerivationPath, ExtendedPrivateKey,
extended_key::private_key::Error as XPrvError, DerivationPath, ExtendedPrivateKey, PrivateKey,
};
use keyfork_mnemonic::{Mnemonic, MnemonicGenerationError};
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
use serde::{Deserialize, Serialize};
/// An error encountered while deriving a key.
@ -49,15 +29,12 @@ pub type Result<T, E = DerivationError> = std::result::Result<T, E>;
/// The algorithm to derive a key for. The choice of algorithm will result in a different resulting
/// derivation.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum DerivationAlgorithm {
#[allow(missing_docs)]
Ed25519,
#[allow(missing_docs)]
Secp256k1,
#[doc(hidden)]
TestAlgorithm,
}
impl DerivationAlgorithm {
@ -65,8 +42,7 @@ impl DerivationAlgorithm {
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
fn derive(&self, seed: &[u8], path: &DerivationPath) -> Result<DerivationResponse> {
let seed = VariableLengthSeed::new(seed);
pub fn derive(&self, seed: Vec<u8>, path: &DerivationPath) -> Result<DerivationResponse> {
match self {
#[cfg(feature = "ed25519")]
Self::Ed25519 => {
@ -86,14 +62,6 @@ impl DerivationAlgorithm {
&derived_key,
))
}
Self::TestAlgorithm => {
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed)?;
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
&derived_key,
))
}
#[allow(unreachable_patterns)]
_ => Err(DerivationError::Algorithm),
}
@ -112,20 +80,8 @@ impl std::str::FromStr for DerivationAlgorithm {
}
}
/// Acquire the associated [`DerivationAlgorithm`] for a [`PrivateKey`].
pub trait AsAlgorithm: PrivateKey {
/// Return the appropriate [`DerivationAlgorithm`].
fn as_algorithm() -> DerivationAlgorithm;
}
impl AsAlgorithm for TestPrivateKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::TestAlgorithm
}
}
/// A derivation request.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DerivationRequest {
algorithm: DerivationAlgorithm,
path: DerivationPath,
@ -133,23 +89,6 @@ pub struct DerivationRequest {
impl DerivationRequest {
/// Create a new derivation request.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # request::*,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
/// # Ok(())
/// # }
pub fn new(algorithm: DerivationAlgorithm, path: &DerivationPath) -> Self {
Self {
algorithm,
@ -158,24 +97,6 @@ impl DerivationRequest {
}
/// Return the path of the derivation request.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # request::*,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
/// assert_eq!(&path, request.path());
/// # Ok(())
/// # }
pub fn path(&self) -> &DerivationPath {
&self.path
}
@ -184,70 +105,28 @@ impl DerivationRequest {
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # request::*,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mnemonic: keyfork_mnemonic::Mnemonic = //
/// # keyfork_mnemonic::Mnemonic::from_entropy(
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
/// # )?;
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
/// let response = request.derive_with_mnemonic(&mnemonic)?;
/// # Ok(())
/// # }
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
// TODO: passphrase support and/or store passphrase within mnemonic
self.derive_with_master_seed(&mnemonic.generate_seed(None))
self.derive_with_master_seed(mnemonic.seed(None)?)
}
/// Derive an [`ExtendedPrivateKey`] using the given seed.
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # request::*,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
/// let response = request.derive_with_master_seed(seed)?;
/// # Ok(())
/// # }
pub fn derive_with_master_seed(&self, seed: &[u8]) -> Result<DerivationResponse> {
pub fn derive_with_master_seed(&self, seed: Vec<u8>) -> Result<DerivationResponse> {
self.algorithm.derive(seed, &self.path)
}
}
/// A response to a [`DerivationRequest`]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DerivationResponse {
/// The algorithm used to derive the data.
pub algorithm: DerivationAlgorithm,
/// The derived private key.
pub data: [u8; 32],
pub data: Vec<u8>,
/// The chain code, used for further derivation.
pub chain_code: [u8; 32],
@ -258,13 +137,13 @@ pub struct DerivationResponse {
impl DerivationResponse {
/// Create a [`DerivationResponse`] with the given values.
fn with_algo_and_xprv<T: PrivateKey + Clone>(
pub fn with_algo_and_xprv<T: PrivateKey + Clone>(
algorithm: DerivationAlgorithm,
xprv: &ExtendedPrivateKey<T>,
) -> Self {
Self {
algorithm,
data: PrivateKey::to_bytes(xprv.private_key()),
data: PrivateKey::to_bytes(xprv.private_key()).to_vec(),
chain_code: xprv.chain_code(),
depth: xprv.depth(),
}
@ -285,67 +164,47 @@ pub enum TryFromDerivationResponseError {
}
#[cfg(feature = "secp256k1")]
mod secp256k1 {
use super::*;
use k256::SecretKey;
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
type Error = TryFromDerivationResponseError;
impl AsAlgorithm for SecretKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::Secp256k1
}
}
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<SecretKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Secp256k1 => {
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into)
}
_ => Err(Self::Error::Algorithm),
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Secp256k1 => {
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
}
_ => Err(Self::Error::Algorithm),
}
}
}
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<SecretKey> {
type Error = TryFromDerivationResponseError;
#[cfg(feature = "secp256k1")]
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: DerivationResponse) -> Result<Self, Self::Error> {
ExtendedPrivateKey::<SecretKey>::try_from(&value)
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> {
ExtendedPrivateKey::<k256::SecretKey>::try_from(&value)
}
}
#[cfg(feature = "ed25519")]
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Ed25519 => {
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
}
_ => Err(Self::Error::Algorithm),
}
}
}
#[cfg(feature = "ed25519")]
mod ed25519 {
use super::*;
use ed25519_dalek::SigningKey;
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
type Error = TryFromDerivationResponseError;
impl AsAlgorithm for SigningKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::Ed25519
}
}
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<SigningKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Ed25519 => {
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into)
}
_ => Err(Self::Error::Algorithm),
}
}
}
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<SigningKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: DerivationResponse) -> Result<Self, Self::Error> {
ExtendedPrivateKey::<SigningKey>::try_from(&value)
}
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> {
ExtendedPrivateKey::<ed25519_dalek::SigningKey>::try_from(&value)
}
}

View File

@ -15,7 +15,7 @@ fn secp256k1() {
let tests = test_data()
.unwrap()
.remove("secp256k1")
.remove(&"secp256k1".to_string())
.unwrap();
for per_seed in tests {
@ -30,8 +30,7 @@ fn secp256k1() {
} = test;
// Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed).unwrap();
let xkey = ExtendedPrivateKey::<SecretKey>::new(seed).unwrap();
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
derived_key.chain_code().as_slice(),
@ -51,8 +50,8 @@ fn secp256k1() {
// Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response = request.derive_with_master_seed(seed).unwrap();
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
let response = request.derive_with_master_seed(seed.clone()).unwrap();
assert_eq!(&response.data, private_key, "test: {chain}");
}
}
}
@ -62,7 +61,7 @@ fn secp256k1() {
fn ed25519() {
use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove("ed25519").unwrap();
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -76,8 +75,7 @@ fn ed25519() {
} = test;
// Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed).unwrap();
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
derived_key.chain_code().as_slice(),
@ -97,8 +95,8 @@ fn ed25519() {
// Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain);
let response = request.derive_with_master_seed(seed).unwrap();
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
let response = request.derive_with_master_seed(seed.to_vec()).unwrap();
assert_eq!(&response.data, private_key, "test: {chain}");
}
}
}

View File

@ -1,40 +1,38 @@
[package]
name = "keyfork-shard"
version = "0.2.3"
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
[features]
default = ["openpgp", "openpgp-card", "qrcode", "bin"]
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
default = ["openpgp", "openpgp-card", "qrcode"]
openpgp = ["sequoia-openpgp", "anyhow"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
qrcode = ["keyfork-qrcode"]
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
[dependencies]
keyfork-bug = { workspace = true }
keyfork-prompt = { workspace = true, default-features = false, features = ["mnemonic"] }
keyfork-qrcode = { workspace = true, optional = true, default-features = false }
smex = { workspace = true }
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", optional = true }
smex = { version = "0.1.0", path = "../util/smex" }
sharks = "0.5.0"
thiserror = { workspace = true }
thiserror = "1.0.50"
# Remote operator mode
keyfork-mnemonic = { workspace = true }
keyfork-mnemonic-util = { version = "0.1.0", path = "../util/keyfork-mnemonic-util" }
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
aes-gcm = { version = "0.10.3", features = ["std"] }
hkdf = { version = "0.12.4", features = ["std"] }
sha2 = { workspace = true }
sha2 = "0.10.8"
# OpenPGP
keyfork-derive-openpgp = { workspace = true, default-features = false }
anyhow = { workspace = true, optional = true }
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp" }
anyhow = { version = "1.0.79", optional = true }
card-backend = { version = "0.2.0", optional = true }
card-backend-pcsc = { workspace = true, optional = true }
openpgp-card-sequoia = { workspace = true, optional = true }
openpgp-card = { workspace = true, optional = true }
sequoia-openpgp = { workspace = true, optional = true }
base64 = "0.22.0"
card-backend-pcsc = { version = "0.5.0", optional = true }
openpgp-card-sequoia = { version = "0.2.0", optional = true, default-features = false }
openpgp-card = { version = "0.4.0", optional = true }
sequoia-openpgp = { version = "1.17.0", optional = true, default-features = false }

View File

@ -1,4 +1,4 @@
//! Combine OpenPGP shards and output the hex-encoded secret.
//!
use std::{
env,
@ -7,36 +7,52 @@ use std::{
process::ExitCode,
};
use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{openpgp::OpenPGP, Format};
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(
shard: impl AsRef<Path>,
key_discovery: Option<&str>,
) -> Result<(File, Option<PathBuf>)> {
) -> Result<(File, Vec<Cert>)> {
let key_discovery = key_discovery.map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
Ok((File::open(shard)?, key_discovery))
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(shard)?, certs))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, key_discovery) = match args.as_slice() {
let (messages_file, cert_list) = match args.as_slice() {
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
[shard] => validate(shard, None)?,
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
let mut encrypted_messages = parse_messages(messages_file)?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?;
print!("{}", smex::encode(bytes));
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
let mut bytes = vec![];
combine(
cert_list,
&encrypted_metadata,
encrypted_messages.into(),
&mut bytes,
)?;
print!("{}", smex::encode(&bytes));
Ok(())
}

View File

@ -1,4 +1,4 @@
//! Decrypt a single OpenPGP shard and encapsulate it for remote transport.
//!
use std::{
env,
@ -7,35 +7,47 @@ use std::{
process::ExitCode,
};
use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{Format, openpgp::OpenPGP};
use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(
shard: impl AsRef<Path>,
key_discovery: Option<&str>,
) -> Result<(File, Option<PathBuf>)> {
let key_discovery = key_discovery.map(PathBuf::from);
fn validate<'a>(
messages_file: impl AsRef<Path>,
key_discovery: impl Into<Option<&'a str>>,
) -> Result<(File, Vec<Cert>)> {
let key_discovery = key_discovery.into().map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
Ok((File::open(shard)?, key_discovery))
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(messages_file)?, certs))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, key_discovery) = match args.as_slice() {
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
[shard] => validate(shard, None)?,
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
let (messages_file, cert_list) = match args.as_slice() {
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
[messages_file] => validate(messages_file, None)?,
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
};
let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
let mut encrypted_messages = parse_messages(messages_file)?;
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
decrypt(
&cert_list,
&encrypted_metadata,
encrypted_messages.make_contiguous(),
)?;
Ok(())
}

View File

@ -1,4 +1,4 @@
//! Combine OpenPGP shards using remote transport and output the hex-encoded secret.
//!
use std::{
env,
@ -20,7 +20,7 @@ fn run() -> Result<()> {
let mut bytes = vec![];
remote_decrypt(&mut bytes)?;
print!("{}", smex::encode(bytes));
print!("{}", smex::encode(&bytes));
Ok(())
}

View File

@ -1,14 +1,15 @@
//! Split a hex-encoded secret into OpenPGP shards
//!
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_prompt::terminal::DefaultTerminal;
use keyfork_shard::{Format, openpgp::OpenPGP};
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
#[derive(Clone, Debug)]
enum Error {
Usage(String),
Input,
Threshold(u8, u8),
InvalidCertCount(usize, u8),
}
impl std::fmt::Display for Error {
@ -18,6 +19,15 @@ impl std::fmt::Display for Error {
write!(f, "Usage: {program_name} threshold max key_discovery")
}
Error::Input => f.write_str("Expected hex encoded input"),
Error::Threshold(threshold, max) => {
write!(
f,
"Invalid threshold: 0 < threshold {threshold} <= max {max} < 256"
)
}
Error::InvalidCertCount(count, max) => {
write!(f, "Invalid cert count: count {count} != max {max}")
}
}
}
}
@ -26,20 +36,31 @@ impl std::error::Error for Error {}
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, u8, PathBuf)> {
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
let threshold = u8::from_str(threshold)?;
let max = u8::from_str(max)?;
let key_discovery = PathBuf::from(key_discovery);
if threshold > max {
return Err(Error::Threshold(threshold, max).into());
}
// Verify path exists
std::fs::metadata(&key_discovery)?;
Ok((threshold, max, key_discovery))
// Load certs from path
let certs = discover_certs(key_discovery)?;
if certs.len() != max.into() {
return Err(Error::InvalidCertCount(certs.len(), max).into());
}
Ok((threshold, certs))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (threshold, max, key_discovery) = match args.as_slice() {
let (threshold, cert_list) = match args.as_slice() {
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
_ => return Err(Error::Usage(program_name).into()),
};
@ -48,12 +69,11 @@ fn run() -> Result<()> {
let Some(line) = stdin().lines().next() else {
return Err(Error::Input.into());
};
smex::decode(line?)?
smex::decode(&line?)?
};
let openpgp = OpenPGP::<DefaultTerminal>::new();
split(threshold, cert_list, &input, std::io::stdout())?;
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
Ok(())
}

View File

@ -1,390 +1,24 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::expect_fun_call)]
use std::{
io::{stdin, stdout, Read, Write},
sync::{Arc, Mutex},
};
use std::io::{stdin, stdout, Write};
use aes_gcm::{
aead::{consts::U12, Aead},
Aes256Gcm, KeyInit, Nonce,
aead::{Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit,
};
use base64::prelude::{Engine, BASE64_STANDARD};
use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use keyfork_prompt::{
validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator,
},
validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, PromptHandler, Terminal,
};
use sha2::Sha256;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
const PLAINTEXT_LENGTH: u8 = 32 // shard
+ 1 // index
+ 1 // threshold
+ 1 // version
+ 1; // length;
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
#[cfg(feature = "openpgp")]
pub mod openpgp;
/// A trait to specify where keys can be discovered from, such as a Rust-native type or a path on
/// the filesystem that keys may be read from.
pub trait KeyDiscovery<F: Format + ?Sized> {
/// Discover public keys for the associated format.
///
/// # Errors
/// The method may return an error if public keys could not be loaded from the given discovery
/// mechanism. A discovery mechanism _must_ be able to detect public keys.
fn discover_public_keys(&self) -> Result<Vec<F::PublicKey>, F::Error>;
/// Discover private keys for the associated format.
///
/// # Errors
/// The method may return an error if private keys could not be loaded from the given
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
}
/// A format to use for splitting and combining secrets.
pub trait Format {
/// The error type returned from any failed operations.
type Error: std::error::Error + 'static;
/// A type encapsulating a single public key recipient.
type PublicKey;
/// A type encapsulating the private key recipients of shards.
type PrivateKeyData;
/// A type representing a Signer derived from the secret.
type SigningKey;
/// A type representing the parsed, but encrypted, Shard data.
type EncryptedData;
/// Derive a signer
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
/// will use the same key_data for both, ensuring an iteration of this method will match with
/// iterations in methods called later.
///
/// # Errors
/// The method may return an error if encryption to any of the public keys fails.
fn format_encrypted_header(
&self,
signing_key: &Self::SigningKey,
key_data: &[Self::PublicKey],
threshold: u8,
) -> Result<Self::EncryptedData, Self::Error>;
/// Format a shard encrypted to the given public key, signing with the private key.
///
/// # Errors
/// The method may return an error if the public key used to encrypt the shard is unsuitable
/// for encryption, or if an error occurs while encrypting.
fn encrypt_shard(
&self,
shard: &[u8],
public_key: &Self::PublicKey,
signing_key: &mut Self::SigningKey,
) -> Result<Self::EncryptedData, Self::Error>;
/// Parse the Shard file into a processable type.
///
/// # Errors
/// The method may return an error if the Shard file could not be read from or if the Shard
/// file could not be properly parsed.
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Vec<Self::EncryptedData>, Self::Error>;
/// Write the Shard data to a Shard file.
///
/// # Errors
/// The method may return an error if the Shard data could not be properly serialized or if the
/// Shard file could not be written to.
fn format_shard_file(
&self,
encrypted_data: &[Self::EncryptedData],
shard_file: impl Write + Send + Sync,
) -> Result<(), Self::Error>;
/// Decrypt shares and associated metadata from a readable input. For the current version of
/// Keyfork, the only associated metadata is a u8 representing the threshold to combine
/// secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if all shards
/// could not be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a reaable input. For the current
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
/// combine secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if a shard could not
/// be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Share, u8), Self::Error>;
/// Decrypt multiple shares and combine them to recreate a secret.
///
/// # Errors
/// The method may return an error if the shares can't be decrypted or if the shares can't
/// be combined into a secret.
fn decrypt_all_shards_to_secret(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: impl PromptHandler,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
let (shares, threshold) = self.decrypt_all_shards(
private_keys,
&encrypted_messages,
Arc::new(Mutex::new(prompt)),
)?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
Ok(secret)
}
/// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the
/// shard to the AES key.
///
/// # Errors
/// The method may return an error if a share can't be decrypted. The method will not return an
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
/// QR code; instead, a mnemonic prompt will be used.
fn decrypt_one_shard_for_transport(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: impl PromptHandler,
) -> Result<(), Box<dyn std::error::Error>> {
let prompt = Arc::new(Mutex::new(prompt));
// parse input
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera
#[cfg(feature = "qrcode")]
{
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
} else {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let their_pubkey = match pubkey_data {
Some(pubkey) => pubkey,
None => {
let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)),
};
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
}
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
assert!(
shared_secret.was_contributory(),
bug!("shared secret might be insecure")
);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
// decrypt a single shard and create the payload
let (share, threshold) =
self.decrypt_one_shard(private_keys, &encrypted_messages, prompt.clone())?;
let mut payload = Vec::from(&share);
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
assert!(
payload.len() < PLAINTEXT_LENGTH as usize,
"invalid share length (too long, must be less than {PLAINTEXT_LENGTH} bytes)"
);
// convert plaintext to static-size payload
#[allow(clippy::assertions_on_constants)]
{
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
}
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
// duplicate mnemonic words. This version does not include that, and instead uses a
// repeated length byte.
#[allow(clippy::cast_possible_truncation)]
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
"previously asserted length must be < {PLAINTEXT_LENGTH}",
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
)); PLAINTEXT_LENGTH as usize];
plaintext_bytes[..payload.len()].clone_from_slice(&payload);
// encrypt data
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?;
assert_eq!(
encrypted_bytes.len(),
ENCRYPTED_LENGTH as usize,
bug!("encrypted bytes size != expected len"),
);
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes);
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ",
"Nobody else should scan this QR code."
)
.to_string(),
))?;
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Data(qrcode))?;
}
}
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?;
Ok(())
}
/// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing
/// threshold.
///
/// # Errors
/// The method may return an error if the shares can't be encrypted.
fn shard_and_encrypt(
&self,
threshold: u8,
max: u8,
secret: &[u8],
public_key_discovery: impl KeyDiscovery<Self>,
writer: impl Write + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut signing_key = self.derive_signing_key(secret);
let sharks = Sharks(threshold);
let dealer = sharks.dealer(secret);
let public_keys = public_key_discovery.discover_public_keys()?;
assert!(
public_keys.len() < u8::MAX as usize,
"must have less than u8::MAX public keys"
);
assert_eq!(
max,
public_keys.len() as u8,
"max must be equal to amount of public keys"
);
let max = public_keys.len() as u8;
assert!(max >= threshold, "threshold must not exceed max keys");
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
let mut messages = vec![header];
for (pk, share) in public_keys.iter().zip(dealer) {
let shard = Vec::from(&share);
messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?);
}
self.format_shard_file(&messages, writer)?;
Ok(())
}
}
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
#[derive(thiserror::Error, Debug)]
pub enum SharksError {
@ -429,6 +63,7 @@ const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry
/// incompatible with the currently running version.
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut iter_count = None;
let mut shares = vec![];
@ -438,22 +73,24 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic =
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
let our_key = EphemeralSecret::random();
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let qrcode_data = key_mnemonic.to_bytes();
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
let mut qrcode_data = nonce_mnemonic.to_bytes();
qrcode_data.extend(key_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"QR code #{iter} will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ",
"Only the next shardholder should scan the QR code."
"A QR code will be displayed after this prompt. ",
"Send the QR code to only shardholder {iter}. ",
"Nobody else should scan this QR code."
),
iter = iter
)))?;
@ -463,9 +100,11 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"Upon request, these words should be sent to the shardholder: ",
"{key_mnemonic}"
"Upon request, these words should be sent to shardholder {iter}: ",
"{nonce_mnemonic} {key_mnemonic}"
),
iter = iter,
nonce_mnemonic = nonce_mnemonic,
key_mnemonic = key_mnemonic,
)))?;
@ -475,18 +114,10 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(qrcode_content)) =
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
assert_eq!(
decoded_data.len(),
// Include length of public key
ENCRYPTED_LENGTH as usize + 32,
bug!("invalid payload data")
);
let decoded_data = smex::decode(&hex)?;
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec());
} else {
@ -501,12 +132,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
word_lengths: [24, 39],
};
let [pubkey_mnemonic, payload_mnemonic] = pm
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let [pubkey_mnemonic, payload_mnemonic] = pm.prompt_validated_wordlist(
QRCODE_COULDNT_READ,
&wordlist,
3,
validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
@ -516,28 +147,14 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
}
};
assert_eq!(
payload.len(),
ENCRYPTED_LENGTH as usize,
bug!("invalid payload data")
);
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
assert!(
shared_secret.was_contributory(),
bug!("shared secret might be insecure")
);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
let payload =
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
match &mut iter_count {
@ -552,8 +169,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
}
}
let payload_len = payload.last().expect(bug!("payload should not be empty"));
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
shares.push(payload[HUNK_OFFSET..].to_vec());
}
let shares = shares

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,4 @@
#![allow(clippy::expect_fun_call)]
use std::sync::{Arc, Mutex};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{Error as PromptError, PromptHandler};
use keyfork_prompt::{Error as PromptError, DefaultTerminal, default_terminal, PromptHandler};
use super::openpgp::{
self,
@ -27,18 +22,18 @@ pub enum Error {
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring<P: PromptHandler> {
pub struct Keyring {
full_certs: Vec<Cert>,
root: Option<Cert>,
pm: Arc<Mutex<P>>,
pm: DefaultTerminal,
}
impl<P: PromptHandler> Keyring<P> {
pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
impl Keyring {
pub fn new(certs: impl AsRef<[Cert]>) -> Result<Self> {
Ok(Self {
full_certs: certs.as_ref().to_vec(),
root: Default::default(),
pm: p,
pm: default_terminal()?,
})
}
@ -62,7 +57,7 @@ impl<P: PromptHandler> Keyring<P> {
}
}
impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
impl VerificationHelper for &mut Keyring {
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
Ok(ids
.iter()
@ -84,23 +79,13 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
aead_algo,
} => {}
MessageLayer::SignatureGroup { results } => {
match &results[..] {
[Ok(_)] => {
return Ok(());
}
_ => {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
}
}
/*
for result in results {
if let Err(e) = result {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Invalid signature: {e}"));
}
}
*/
}
}
}
@ -108,7 +93,7 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
}
}
impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
impl DecryptionHelper for &mut Keyring {
fn decrypt<D>(
&mut self,
pkesks: &[PKESK],
@ -126,10 +111,12 @@ impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
pkesk.recipient().is_wildcard()
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
}) {
#[allow(deprecated, clippy::map_flatten)]
let name = cert
.userids()
.next()
.and_then(|userid| userid.userid().name2().transpose())
.map(|userid| userid.userid().name().transpose())
.flatten()
.transpose()
.ok()
.flatten();
@ -152,8 +139,6 @@ impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
};
let passphrase = self
.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_passphrase(&message)
.context("Decryption passphrase")?;
secret_key

View File

@ -1,14 +1,9 @@
#![allow(clippy::expect_fun_call)]
use std::collections::{HashMap, HashSet};
use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{
default_terminal,
validators::{PinValidator, Validator},
Error as PromptError, Message, PromptHandler,
DefaultTerminal, Error as PromptError, Message, PromptHandler,
};
use super::openpgp::{
@ -71,19 +66,19 @@ fn format_name(input: impl AsRef<str>) -> String {
}
#[allow(clippy::module_name_repetitions)]
pub struct SmartcardManager<P: PromptHandler> {
pub struct SmartcardManager {
current_card: Option<Card<Open>>,
root: Option<Cert>,
pm: Arc<Mutex<P>>,
pm: DefaultTerminal,
pin_cache: HashMap<Fingerprint, String>,
}
impl<P: PromptHandler> SmartcardManager<P> {
pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
impl SmartcardManager {
pub fn new() -> Result<Self> {
Ok(Self {
current_card: None,
root: None,
pm: p,
pm: default_terminal()?,
pin_cache: Default::default(),
})
}
@ -101,13 +96,9 @@ impl<P: PromptHandler> SmartcardManager<P> {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c;
}
self.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
self.pm.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter".to_string(),
))?;
};
let mut card = Card::<Open>::new(card_backend).map_err(Error::OpenSmartCard)?;
let transaction = card.transaction().map_err(Error::Transaction)?;
@ -161,19 +152,16 @@ impl<P: PromptHandler> SmartcardManager<P> {
}
}
self.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(Message::Text(
"Please plug in a smart card and press enter".to_string(),
))?;
self.pm.prompt_message(Message::Text(
"Please plug in a smart card and press enter".to_string(),
))?;
}
Ok(None)
}
}
impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
impl VerificationHelper for &mut SmartcardManager {
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
#[allow(clippy::flat_map_option)]
Ok(ids
@ -193,23 +181,12 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
aead_algo,
} => {}
MessageLayer::SignatureGroup { results } => {
match &results[..] {
[Ok(_)] => {
return Ok(());
}
_ => {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
}
}
/*
for result in results {
if let Err(e) = result {
return Err(anyhow::anyhow!("Invalid signature: {e}"));
// FIXME: anyhow leak
return Err(anyhow::anyhow!("Verification error: {}", e.to_string()));
}
}
*/
}
}
}
@ -217,7 +194,7 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
}
}
impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
impl DecryptionHelper for &mut SmartcardManager {
fn decrypt<D>(
&mut self,
pkesks: &[PKESK],
@ -277,8 +254,6 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
};
let temp_pin = self
.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
match verification_status {
@ -290,8 +265,6 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
// NOTE: This should not be hit, because of the above validator.
Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => {
self.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(Message::Text("Invalid PIN length entered.".to_string()))?;
}
Err(_) => {}

View File

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

View File

@ -1,55 +0,0 @@
# Keyfork: The Kitchen Sink of Entropy
**Note:** Keyfork operations are meant to be run on an airgapped machine and
Keyfork will error if either any network interfaces are detected or if Keyfork
is running on a system with a kernel using an insecure random number generator.
An all-inclusive crate encapsulating end-user functionality of the Keyfork
ecosystem, the Keyfork binary includes all mechanisms that should be exposed to
the user when running Keyfork. Information about what operations Keyfork
performs are available in detail by running `keyfork help` (each subcommand has
thorough documentation) or in the [`docs`] mdBook, but here's a quick overview:
## Getting Started with Keyfork
Keyfork offers two options for getting started. For multi-user setups, it is
best to look at the detailed documentation for Keyfork Shard. For single-user
setups, `keyfork mnemonic generate` will (by default) create a 256-bit mnemonic
phrase that can be used to start Keyfork. *Store this phrase*, as it's the only
way you'll be able to start Keyfork in the future. It is recommended to use a
mnemonic recovery sheet or a printed-steel solution such as the [Billfodl] or
[Cryptosteel Capsule].
```sh
keyfork mnemonic generate
```
Once a mnemonic has been generated and stored in a secure manner, Keyfork can
be started by "recovering" the server from the mnemonic backup mechanism:
```sh
keyfork recover mnemonic
```
## Deriving Keys
Keyfork's primary goal is to derive keys. These keys can later be used for
things such as signing documents and artifacts or decrypting payloads.
Keyfork's first derivation target is OpenPGP, a protocol supporting many
cryptographic operations. OpenPGP keys require a User ID, which can be used to
identify the owner of the key, either by name or by email. To get an OpenPGP
public key (more accurately known as a "cert"), the [`sq`][sq] tool is used to
convert a key to a certificate:
```sh
keyfork derive openpgp "John Doe <jdoe@example.com>" | sq key extract-cert
```
All Keyfork derivations are intended to be reproducible. Because of this,
Keyfork derived keys can be recreated at any time, only requiring the knowledge
of how the key was made.
[`docs`]: /public/keyfork/src/branch/main/docs/src/SUMMARY.md
[Billfodl]: https://privacypros.io/products/the-billfodl/
[Cryptosteel Capsule]: https://cryptosteel.com/product/cryptosteel-capsule-solo/
[sq]: https://gitlab.com/sequoia-pgp/sequoia-sq/

View File

@ -1,17 +1,16 @@
use super::Keyfork;
use clap::{Args, Parser, Subcommand};
use clap::{Parser, Subcommand};
use keyfork_derive_openpgp::{
openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
},
XPrvKey,
use keyfork_derive_openpgp::openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath,
};
use keyfork_derive_util::DerivationIndex;
use keyfork_derive_path_data::paths;
use keyforkd_client::Client;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -20,54 +19,49 @@ type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
pub enum DeriveSubcommands {
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
/// ASCII Armor, a format usable by most programs using OpenPGP.
///
/// Certificates are created with a default expiration of one day, but may be configured to
/// expire later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as
/// "15d" (15 days), "1m" (one month), or "2y" (two years).
///
/// It is recommended to use the default expiration of one day and to change the expiration
/// using an external utility, to ensure the Certify key is usable.
#[command(name = "openpgp")]
OpenPGP(OpenPGP)
}
#[derive(Args, Clone, Debug)]
pub struct OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String,
OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String,
},
}
impl DeriveSubcommands {
fn handle(&self, account: DerivationIndex) -> Result<()> {
match self {
DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account),
}
}
}
DeriveSubcommands::OpenPGP { user_id } => {
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let path = DerivationPath::default()
.chain_push(chain)
.chain_push(account);
// TODO: should this be customizable?
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let derived_data: DerivationResponse = Client::discover_socket()?
.request(&request.into())?
.try_into()?;
let default_userid = UserID::from(user_id.as_str());
let cert = keyfork_derive_openpgp::derive(derived_data, &subkeys, &default_userid)?;
impl OpenPGP {
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
let path = paths::OPENPGP.clone().chain_push(account);
// TODO: should this be customizable?
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(self.user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
for packet in cert.into_packets() {
packet.serialize(&mut w)?;
}
for packet in cert.into_packets2() {
packet.serialize(&mut w)?;
w.finalize()?;
}
}
w.finalize()?;
Ok(())
}
}
@ -78,10 +72,6 @@ pub struct Derive {
command: DeriveSubcommands,
/// Account ID. Required for all derivations.
///
/// An account ID may not be relevant for the derivation being performed, but the lack of an
/// account ID can often come as a hindrance in the future. As such, it is always required. If
/// the account ID is not relevant, it is assumed to be `0`.
#[arg(long, global = true, default_value = "0")]
account_id: u32,
}

View File

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

View File

@ -9,7 +9,7 @@ mod wizard;
/// The Kitchen Sink of Entropy.
#[derive(Parser, Clone, Debug)]
#[command(author, version, about, long_about)]
#[command(author, version, about, long_about = None)]
pub struct Keyfork {
// Global options
#[command(subcommand)]
@ -20,51 +20,25 @@ pub struct Keyfork {
pub enum KeyforkCommands {
/// Derive keys of various formats. These commands require that the Keyfork server is running,
/// which can be started by running a `keyfork recover` command.
///
/// Derived keys are reproducible: assuming the same arguments are used when deriving a key for
/// a second time, the key will be _functionally_ equivalent. This means keys don't need to be
/// persisted to cold storage or left hot in a running program. They can be derived when
/// they're needed and forgotten when they're not.
Derive(derive::Derive),
/// Mnemonic generation and persistence utilities.
Mnemonic(mnemonic::Mnemonic),
/// Splitting and combining secrets, using Shamir's Secret Sharing.
///
/// Keys can be split such that a certain amount of users, from a potentially even-larger
/// amount of users, can be used to recreate a key. This creates resilience for a key, as in a
/// "seven of nine" scenario, nine people in total are capable of recreating a key, but only
/// seven may be required.
Shard(shard::Shard),
/// Derive and deploy keys to hardware.
///
/// Keys existing in hardware creates a situation where it is unlikely (but not impossible) for
/// a key to be extracted. While a key in memory could be captured by a rootkit or some other
/// privilege escalation mechanism, a key in hardware would require a hardware exploit to
/// extract the key.
///
/// It is recommended to provision keys whenever possible, as opposed to deriving them.
#[command(subcommand_negates_reqs(true))]
Provision(provision::Provision),
/// Recover a seed using the requested recovery mechanism and start the Keyfork server.
///
/// Once the Keyfork server is started, derivation requests can be performed. The Keyfork seed
/// is kept solely in the Keyfork server. Derivations with less than two indices are not
/// permitted, to ensure a seed often used to derive keys for multiple different paths is not
/// leaked by any individual deriver.
Recover(recover::Recover),
/// Utilities to automatically manage the setup of Keyfork.
Wizard(wizard::Wizard),
/// Print an autocompletion file to standard output.
///
/// Keyfork does not manage the installation of completion files. Consult the documentation for
/// the shell for which documentation has been generated on the appropriate location to store
/// completion files.
#[cfg(feature = "completion")]
Completion {
#[arg(value_enum)]

View File

@ -2,9 +2,11 @@ use super::Keyfork;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_shard::{remote_decrypt, Format};
use keyfork_mnemonic_util::Mnemonic;
use keyfork_shard::{
openpgp::{combine, discover_certs, parse_messages},
remote_decrypt,
};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -35,14 +37,15 @@ impl RecoverSubcommands {
} => {
let content = std::fs::read_to_string(shard_file)?;
if content.contains("BEGIN PGP MESSAGE") {
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
// TODO: remove .clone() by making handle() consume self
let seed = openpgp.decrypt_all_shards_to_secret(
key_discovery.as_deref(),
content.as_bytes(),
prompt_handler,
)?;
let certs = key_discovery
.as_ref()
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
let mut messages = parse_messages(content.as_bytes())?;
let metadata = messages.pop_front().expect("any pgp encrypted message");
let mut seed = vec![];
combine(certs, &metadata, messages.into(), &mut seed)?;
Ok(seed)
} else {
panic!("unknown format of shard file");
@ -55,6 +58,7 @@ impl RecoverSubcommands {
}
RecoverSubcommands::Mnemonic {} => {
use keyfork_prompt::{
default_terminal,
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
@ -65,8 +69,9 @@ impl RecoverSubcommands {
let validator = MnemonicChoiceValidator {
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
};
let mnemonic = term.prompt_validated_wordlist::<English, _>(
let mnemonic = term.prompt_validated_wordlist(
"Mnemonic: ",
&Default::default(),
3,
validator.to_fn(),
)?;
@ -85,7 +90,7 @@ pub struct Recover {
impl Recover {
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
let seed = self.command.handle()?;
let mnemonic = Mnemonic::try_from_slice(&seed)?;
let mnemonic = Mnemonic::from_entropy(&seed, Default::default())?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()

View File

@ -1,7 +1,5 @@
use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_shard::Format as _;
use std::{
io::{stdin, stdout, Read, Write},
path::{Path, PathBuf},
@ -33,23 +31,27 @@ trait ShardExec {
&self,
threshold: u8,
max: u8,
key_discovery: &Path,
key_discovery: impl AsRef<Path>,
secret: &[u8],
output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>>;
fn combine(
&self,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>;
fn decrypt(
fn combine<T>(
&self,
key_discovery: Option<&Path>,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>>;
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>;
fn decrypt<T>(
&self,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>;
}
#[derive(Clone, Debug)]
@ -60,36 +62,77 @@ impl ShardExec for OpenPGP {
&self,
threshold: u8,
max: u8,
key_discovery: &Path,
key_discovery: impl AsRef<Path>,
secret: &[u8],
output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>> {
let opgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
}
fn combine(
&self,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt = default_terminal()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
write!(output, "{}", smex::encode(bytes))?;
// Get certs and input
let certs = keyfork_shard::openpgp::discover_certs(key_discovery.as_ref())?;
assert_eq!(
certs.len(),
max.into(),
"cert count {} != max {max}",
certs.len()
);
keyfork_shard::openpgp::split(threshold, certs, secret, output).map_err(Into::into)
}
fn combine<T>(
&self,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>,
{
let certs = key_discovery
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
.transpose()?
.unwrap_or(vec![]);
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
let mut bytes = vec![];
keyfork_shard::openpgp::combine(
certs,
&encrypted_metadata,
encrypted_messages.into(),
&mut bytes,
)?;
write!(output, "{}", smex::encode(&bytes))?;
Ok(())
}
fn decrypt(
fn decrypt<T>(
&self,
key_discovery: Option<&Path>,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt = default_terminal()?;
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>,
{
let certs = key_discovery
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
.transpose()?
.unwrap_or(vec![]);
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
keyfork_shard::openpgp::decrypt(
&certs,
&encrypted_metadata,
encrypted_messages.make_contiguous(),
)?;
Ok(())
}
}
@ -182,7 +225,7 @@ impl ShardSubcommands {
match format {
Some(Format::OpenPGP(o)) => {
o.decrypt(key_discovery.as_deref(), shard_content.as_bytes())
o.decrypt(key_discovery.as_ref(), shard_content.as_bytes())
}
Some(Format::P256(_p)) => todo!(),
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
@ -199,7 +242,7 @@ impl ShardSubcommands {
match format {
Some(Format::OpenPGP(o)) => o.combine(
key_discovery.as_deref(),
key_discovery.as_ref(),
shard_content.as_bytes(),
&mut stdout,
),

View File

@ -1,41 +1,27 @@
use super::Keyfork;
use clap::{Args, Parser, Subcommand};
use clap::{Parser, Subcommand};
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use keyfork_derive_openpgp::{
openpgp::{
self,
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
Cert,
},
XPrv,
use keyfork_derive_openpgp::openpgp::{self, packet::UserID, types::KeyFlags, Cert};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest},
DerivationIndex, DerivationPath,
};
use keyfork_derive_path_data::paths;
use keyfork_derive_util::DerivationIndex;
use keyfork_mnemonic::Mnemonic;
use keyfork_prompt::{
default_terminal,
validators::{SecurePinValidator, Validator},
DefaultTerminal, Message, PromptHandler,
validators::{PinValidator, Validator},
Message, PromptHandler, Terminal,
};
use keyfork_shard::{openpgp::OpenPGP, Format};
#[derive(thiserror::Error, Debug)]
#[error("Invalid PIN length: {0}")]
pub struct PinLength(usize);
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
// TODO: refactor to use mnemonic derived seed instead of 256 bit entropy to allow for possible
// recovery in the future.
fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
fn derive_key(seed: &[u8], index: u8) -> Result<Cert> {
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
@ -45,13 +31,21 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
KeyFlags::empty().set_authentication(),
];
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let mut shrd_u32 = [0u8; 4];
shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::<Vec<u8>>());
let account = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let subkey = DerivationIndex::new(u32::from(index), true)?;
let path = paths::OPENPGP_SHARD.clone().chain_push(subkey);
let xprv = XPrv::new(seed)
.expect("could not construct master key from seed")
.derive_path(&path)?;
let path = DerivationPath::default()
.chain_push(chain)
.chain_push(account)
.chain_push(subkey);
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let response = request.derive_with_master_seed(seed.to_vec())?;
let userid = UserID::from(format!("Keyfork Shard {index}"));
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
let cert = keyfork_derive_openpgp::derive(response, &subkeys, &userid)?;
Ok(cert)
}
@ -103,200 +97,121 @@ fn factory_reset_current_card(
Ok(())
}
fn generate_shard_secret(
threshold: u8,
max: u8,
keys_per_shard: u8,
output_file: &Option<PathBuf>,
) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_size(256 / 8)?;
let mut pm = Terminal::new(std::io::stdin(), std::io::stderr())?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout();
if output_file.is_none() {
assert!(
!stdout.is_terminal(),
"not printing shard to terminal, redirect output"
);
}
let user_pin_validator = PinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = PinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
for index in 0..max {
let cert = derive_key(&seed, index)?;
for i in 0..keys_per_shard {
pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
i + 1,
index + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c;
}
pm.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
};
let user_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
factory_reset_current_card(
&mut seen_cards,
user_pin.trim(),
admin_pin.trim(),
&cert,
card_backend,
)?;
}
certs.push(cert);
}
if let Some(output_file) = output_file {
let output = File::create(output_file)?;
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
} else {
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
}
Ok(())
}
#[derive(Subcommand, Clone, Debug)]
pub enum WizardSubcommands {
GenerateShardSecret(GenerateShardSecret),
BottomsUp(BottomsUp),
}
/// Create a 256 bit secret and shard the secret to smart cards.
///
/// Smart cards will need to be plugged in periodically during the wizard, where they will be factory reset and
/// provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered with `keyfork recover shard` or
/// `keyfork recover remote-shard`. The share file will be printed to standard output.
GenerateShardSecret {
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// Create a 256 bit secret and shard the secret to smart cards.
///
/// Smart cards will need to be plugged in periodically during the wizard, where they will be
/// factory reset and provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered
/// with `keyfork recover shard` or `keyfork recover remote-shard`. The share file will be printed
/// to standard output.
#[derive(Args, Clone, Debug)]
pub struct GenerateShardSecret {
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// The maximum amount of shards.
#[arg(long)]
max: u8,
/// The 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 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.
#[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,
/// The file to write the generated shard file to.
#[arg(long)]
output: Option<PathBuf>,
},
}
impl WizardSubcommands {
// dispatch
fn handle(&self) -> Result<()> {
match self {
WizardSubcommands::GenerateShardSecret(gss) => gss.handle(),
WizardSubcommands::BottomsUp(bu) => bu.handle(),
WizardSubcommands::GenerateShardSecret {
threshold,
max,
keys_per_shard,
output,
} => generate_shard_secret(*threshold, *max, *keys_per_shard, output),
}
}
}
impl GenerateShardSecret {
fn handle(&self) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mut pm = default_terminal()?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout();
if self.output.is_none() {
assert!(
!stdout.is_terminal(),
"not printing shard to terminal, redirect output"
);
}
let user_pin_validator = SecurePinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
for index in 0..self.max {
let cert = derive_key(seed, index)?;
for i in 0..self.keys_per_shard {
pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
(i as u16) + 1,
(index as u16) + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c;
}
pm.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
};
let user_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
factory_reset_current_card(
&mut seen_cards,
user_pin.trim(),
admin_pin.trim(),
&cert,
card_backend,
)?;
}
certs.push(cert);
}
let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = self.output.as_ref() {
let output = File::create(output_file)?;
opgp.shard_and_encrypt(self.threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
opgp.shard_and_encrypt(
self.threshold,
certs.len() as u8,
&seed,
&certs[..],
std::io::stdout(),
)?;
}
Ok(())
}
}
impl BottomsUp {
fn handle(&self) -> Result<()> {
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mnemonic = Mnemonic::from_array(entropy);
let seed = mnemonic.generate_seed(None);
// TODO: should this allow for customizing the account index from 0? Potential for key reuse
// errors.
let path = paths::OPENPGP_DISASTER_RECOVERY
.clone()
.chain_push(DerivationIndex::new(0, true)?);
let subkeys = [
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = XPrv::new(seed)
.expect("could not construct master key from seed")
.derive_path(&path)?;
let userid = UserID::from(self.user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
let certfile = File::create(&self.output_cert)?;
let mut w = Writer::new(certfile, Kind::PublicKey)?;
cert.serialize(&mut w)?;
w.finalize()?;
let opgp = OpenPGP::<DefaultTerminal>::new();
let certs = OpenPGP::<DefaultTerminal>::discover_certs(&self.key_discovery)?;
let shardfile = File::create(&self.output_shardfile)?;
opgp.shard_and_encrypt(
self.threshold,
certs.len() as u8,
&entropy,
&certs[..],
shardfile,
)?;
Ok(())
}
}
#[derive(Parser, Debug, Clone)]
pub struct Wizard {
#[command(subcommand)]

View File

@ -1,4 +1,4 @@
#![doc = include_str!("../README.md")]
#![doc = include_str!("../../../README.md")]
#![allow(clippy::module_name_repetitions)]
@ -6,16 +6,21 @@ use std::process::ExitCode;
use clap::Parser;
use keyfork_bin::{Bin, ClosureBin};
mod cli;
mod config;
fn main() -> ExitCode {
let bin = ClosureBin::new(|| {
let opts = cli::Keyfork::parse();
opts.command.handle(&opts)
});
let opts = cli::Keyfork::parse();
bin.main()
if let Err(e) = opts.command.handle(&opts) {
eprintln!("Unable to run command: {e}");
let mut source = e.source();
while let Some(new_error) = source.take() {
eprintln!("Source: {new_error}");
source = new_error.source();
}
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-qrcode"
version = "0.1.1"
version = "0.1.0"
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
@ -8,15 +8,14 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["bin"]
bin = ["decode-backend-rqrr"]
default = []
bin = ["decode-backend-zbar"]
decode-backend-rqrr = ["dep:rqrr"]
decode-backend-zbar = ["dep:keyfork-zbar"]
[dependencies]
keyfork-bug = { workspace = true }
keyfork-zbar = { workspace = true, optional = true }
image = { workspace = true, default-features = false, features = ["jpeg"] }
rqrr = { version = "0.7.0", optional = true }
thiserror = { workspace = true }
v4l = { workspace = true }
image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true }
rqrr = { version = "0.6.0", optional = true }
thiserror = "1.0.56"
v4l = "0.14.0"

View File

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

View File

@ -1,11 +1,9 @@
//! Encoding and decoding QR codes.
use keyfork_bug as bug;
use image::ImageReader;
use image::io::Reader as ImageReader;
use std::{
io::{Cursor, Write},
time::{Duration, Instant},
time::{Duration, SystemTime},
process::{Command, Stdio},
};
use v4l::{
@ -100,25 +98,19 @@ pub fn qrencode(
Ok(result)
}
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
/// Continuously scan the `index`-th camera for a QR code.
///
/// # Errors
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
#[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
let mut fmt = device.format().expect("Failed to read format");
fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = Instant::now();
let start = SystemTime::now();
while Instant::now()
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout
{
let (buffer, _) = stream.next()?;
@ -138,23 +130,19 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
}
/// Continuously scan the `index`-th camera for a QR code.
///
/// # Errors
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
#[cfg(feature = "decode-backend-zbar")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
let mut fmt = device.format().expect("Failed to read format");
fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = Instant::now();
let start = SystemTime::now();
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
while Instant::now()
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout
{
let (buffer, _) = stream.next()?;

View File

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

View File

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

View File

@ -22,9 +22,9 @@ impl Image {
///
/// A FourCC code can be given in the format:
///
/// ```rust,ignore
/// ```no_run
/// self.set_format(b"Y800")
/// ```
/// ````
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
let fourcc: u64 = fourcc[0] as u64
| ((fourcc[1] as u64) << 8)

View File

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

View File

@ -1,12 +0,0 @@
[package]
name = "keyfork-bin"
version = "0.1.0"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dev-dependencies]
anyhow = { workspace = true }

View File

@ -1,140 +0,0 @@
#![allow(clippy::needless_doctest_main)]
//! A convenient trait for quickly writing binaries in a consistent pattern.
//!
//! # Examples
//! ```rust
//! use anyhow::anyhow;
//! use keyfork_bin::Bin;
//!
//! struct Main;
//!
//! impl Bin for Main {
//! type Args = (String, String);
//!
//! fn usage_hint(&self) -> Option<String> {
//! Some(String::from("<param1> <param2>"))
//! }
//!
//! fn validate_args(&self, mut args: impl Iterator<Item = String>) -> keyfork_bin::ProcessResult<Self::Args> {
//! let arg1 = args.next().ok_or(anyhow!("missing argument 1"))?;
//! let arg2 = args.next().ok_or(anyhow!("missing argument 2"))?;
//! Ok((arg1, arg2))
//! }
//!
//! fn run(&self, (arg1, arg2): Self::Args) -> keyfork_bin::ProcessResult {
//! println!("First argument: {arg1}");
//! println!("Second argument: {arg2}");
//! Ok(())
//! }
//!#
//!# fn main(&self) -> std::process::ExitCode {
//!# self.main_inner([String::from("hello"), String::from("world")].into_iter())
//!# }
//! }
//!
//! fn main() {
//! // Assume the program was called with something like "hello world"...
//! let bin = Main;
//! bin.main();
//! }
//! ```
use std::process::ExitCode;
/// A result that may contain any error.
pub type ProcessResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
fn report_err(e: Box<dyn std::error::Error>) {
eprintln!("Unable to run command: {e}");
let mut source = e.source();
while let Some(new_error) = source.take() {
eprintln!("- Caused by: {new_error}");
source = new_error.source();
}
}
/// A trait for implementing the flow of a binary's execution.
pub trait Bin {
/// The type for command-line arguments required by the function.
type Args;
/// A usage hint for how the arguments should be provided to the program.
fn usage_hint(&self) -> Option<String> {
None
}
/// Validate the arguments provided by the user into types required by the binary.
#[allow(clippy::missing_errors_doc)]
fn validate_args(&self, args: impl Iterator<Item = String>) -> ProcessResult<Self::Args>;
/// Run the binary
#[allow(clippy::missing_errors_doc)]
fn run(&self, args: Self::Args) -> ProcessResult;
/// The default handler for running the binary and reporting any errors.
fn main(&self) -> ExitCode {
self.main_inner(std::env::args())
}
#[doc(hidden)]
fn main_inner(&self, mut args: impl Iterator<Item = String>) -> ExitCode {
let command = args.next();
let args = match self.validate_args(args) {
Ok(args) => args,
Err(e) => {
if let (Some(command), Some(hint)) = (command, self.usage_hint()) {
eprintln!("Usage: {command} {hint}");
}
report_err(e);
return ExitCode::FAILURE;
}
};
if let Err(e) = self.run(args) {
report_err(e);
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
}
/// A Bin that doesn't take any arguments.
pub struct ClosureBin<F: Fn() -> ProcessResult> {
closure: F
}
impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
/// Create a new Bin from a closure.
///
/// # Examples
/// ```rust
/// use keyfork_bin::{Bin, ClosureBin};
///
/// let bin = ClosureBin::new(|| {
/// println!("Hello, world!");
/// Ok(())
/// });
///
/// bin.main();
/// ```
pub fn new(closure: F) -> Self {
Self {
closure
}
}
}
impl<F> Bin for ClosureBin<F> where F: Fn() -> ProcessResult {
type Args = ();
fn validate_args(&self, _args: impl Iterator<Item = String>) -> ProcessResult<Self::Args> {
Ok(())
}
fn run(&self, _args: Self::Args) -> ProcessResult {
let c = &self.closure;
c()
}
}

View File

@ -1,9 +0,0 @@
[package]
name = "keyfork-bug"
version = "0.1.0"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -1,108 +0,0 @@
//! Keyfork Bug Reporting Utilities.
//!
//! # Examples
//!
//! ```rust
//! use std::{fs::File, io::Write};
//! use keyfork_bug as bug;
//!
//! let option = Some("hello world!");
//! let value = option.expect(bug::bug!("missing str value!"));
//!
//! let mut output_file = File::create("/dev/null").expect(bug::bug!("can't open /dev/null"));
//! output_file
//! .write_all(value.as_bytes())
//! .unwrap_or_else(bug::panic!("Can't write to file: {}", value));
//! ```
//!
//! ```rust,should_panic
//! use std::fs::File;
//! use keyfork_bug as bug;
//!
//! let mut output_file = File::open("/dev/nukk").expect(bug::bug!("can't open /dev/null"));
//! ```
/// The mutex was poisoned and is unusable.
pub const POISONED_MUTEX: &str = "The mutex was poisoned and is unusable";
/// Automatically generate a bug report message for Keyfork. This macro is intended to use when
/// using `Result::expect()` or `Option::expect()` to retrieve information about the callsite where
/// the bug was located.
///
/// # Examples
/// ```rust
/// use keyfork_bug::bug;
///
/// let option = Some(0u32);
/// let value = option.expect(bug!("missing u32 value!"));
/// ```
///
/// ```rust
/// use keyfork_bug::bug;
///
/// let error_message = "This is a really long error message that should not be in the macro.";
/// let option = Some(0u32);
/// let value = option.expect(bug!(error_message));
/// ```
///
/// ```rust,should_panic
/// use keyfork_bug::bug;
///
/// let option: Option<u32> = None;
/// let value = option.expect(bug!("missing u32 value!"));
/// ```
#[macro_export]
macro_rules! bug {
($input:literal) => {
concat!(
"Keyfork encountered a BUG at: [",
file!(),
":",
line!(),
":",
column!(),
"]: ",
$input,
"\n\nReport this bug to <team@distrust.co>, this behavior is unexpected!"
)
};
($input:ident) => {
format!(
concat!("Keyfork encountered a BUG at: [{file}:{line}:{column}]: {input}\n\n",
"Report this bug to <team@distrust.co>, this behavior is unexpected!"
),
file=file!(),
line=line!(),
column=column!(),
input=$input,
).as_str()
};
($($arg:tt)*) => {{
let message = format!($($arg)*);
$crate::bug!(message)
}};
}
/// Return a closure that, when called, panics with a bug report message for Keyfork. Returning a
/// closure can help handle the `clippy::expect_fun_call` lint. The closure accepts an error
/// argument, so it is suitable for being used with [`Result`] types instead of [`Option`] types.
///
/// # Examples
/// ```rust
/// use std::fs::File;
/// use keyfork_bug as bug;
///
/// let file = File::open("/dev/null").unwrap_or_else(bug::panic!("couldn't open /dev/null"));
/// ```
#[macro_export]
macro_rules! panic {
($input:literal) => { |e| {
std::panic!("{}\n{}", $crate::bug!($input), e)
}};
($input:ident) => { |e| {
std::panic!("{}\n{}", $crate::bug!($input), e)
}};
($($arg:tt)*) => { |e| {
std::panic!("{}\n{}", $crate::bug!($($arg)*), e)
}};
}

View File

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

View File

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

View File

@ -13,7 +13,7 @@ use crate::{
/// The top left cell is represented as `(0, 0)`.
///
/// On unix systems, this function will block and possibly time out while
/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called.
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
pub fn position() -> io::Result<(u16, u16)> {
if is_raw_mode_enabled() {
read_position_raw()

View File

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

View File

@ -170,7 +170,7 @@ pub fn available_color_count() -> u16 {
///
/// # Notes
///
/// crossterm supports NO_COLOR (<https://no-color.org/>) to disabled colored output.
/// crossterm supports NO_COLOR (https://no-color.org/) to disabled colored output.
///
/// This API allows applications to override that behavior and force colorized output
/// even if NO_COLOR is set.

View File

@ -71,7 +71,7 @@ impl Colored {
}
/// Checks whether ansi color sequences are disabled by setting of NO_COLOR
/// in environment as per <https://no-color.org/>
/// in environment as per https://no-color.org/
pub fn ansi_color_disabled() -> bool {
!std::env::var("NO_COLOR")
.unwrap_or("".to_string())

View File

@ -199,7 +199,7 @@ pub struct WindowSize {
/// Returns the terminal size `[WindowSize]`.
///
/// The width and height in pixels may not be reliably implemented or default to 0.
/// For unix, <https://man7.org/linux/man-pages/man4/tty_ioctl.4.html> documents them as "unused".
/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused".
/// For windows it is not implemented.
pub fn window_size() -> io::Result<WindowSize> {
sys::window_size()

View File

@ -144,7 +144,7 @@ pub(crate) fn disable_raw_mode() -> io::Result<()> {
/// Queries the terminal's support for progressive keyboard enhancement.
///
/// On unix systems, this function will block and possibly time out while
/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called.
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
#[cfg(feature = "events")]
pub fn supports_keyboard_enhancement() -> io::Result<bool> {
if is_raw_mode_enabled() {

View File

@ -1,15 +1,14 @@
[package]
name = "keyfork-entropy"
version = "0.1.1"
version = "0.1.0"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["bin"]
default = []
bin = ["smex"]
[dependencies]
keyfork-bug = { workspace = true }
smex = { workspace = true, optional = true }
smex = { version = "0.1.0", path = "../smex", optional = true }

View File

@ -1,26 +1,20 @@
//! Utilities for reading entropy from secure sources.
use keyfork_bug::bug;
use std::{
fs::{read_dir, read_to_string, File},
io::Read,
};
use std::{fs::{read_dir, read_to_string, File}, io::Read};
static WARNING_LINKS: [&str; 1] =
["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"];
fn ensure_safe_kernel_version() {
let kernel_version =
read_to_string("/proc/version").expect(bug!("Unable to open file: /proc/version"));
let kernel_version = read_to_string("/proc/version").expect("/proc/version");
let v = kernel_version
.split(' ')
.nth(2)
.expect(bug!("Unable to parse kernel version"))
.expect("Unable to parse kernel version")
.split('.')
.take(2)
.map(str::parse)
.map(|x| x.expect(bug!("Unable to parse kernel version number")))
.map(|x| x.expect("Unable to parse kernel version number"))
.collect::<Vec<u32>>();
let [major, minor, ..] = v.as_slice() else {
panic!("Unable to determine major and minor: {kernel_version}");
@ -33,49 +27,31 @@ fn ensure_safe_kernel_version() {
}
fn ensure_offline() {
let paths = read_dir("/sys/class/net").expect(bug!("Unable to read network interfaces"));
let paths = read_dir("/sys/class/net").expect("Unable to read network interfaces");
for entry in paths {
let mut path = entry.expect(bug!("Unable to read directory entry")).path();
let mut path = entry.expect("Unable to read directory entry").path();
if path
.as_os_str()
.to_str()
.expect(bug!("Unable to decode UTF-8 filepath"))
.expect("Unable to decode UTF-8 filepath")
.split('/')
.last()
.expect(bug!("No data in file path"))
.expect("No data in file path")
== "lo"
{
continue;
}
path.push("operstate");
let isup =
read_to_string(&path).expect(bug!("Unable to read operstate of network interfaces"));
let isup = read_to_string(&path).expect("Unable to read operstate of network interfaces");
assert_ne!(isup.trim(), "up", "No network interfaces should be up");
}
}
/// Ensure the system is safe.
///
/// # Examples
/// ```rust
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// keyfork_entropy::ensure_safe();
/// ```
///
/// When running on a system that's online, or running an outdated kernel:
///
/// ```rust,should_panic
/// # // NOTE: sometimes, the environment variable is set, for testing purposes. I'm not sure how
/// # // to un-set it. Set it to a sentinel value.
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "test-must-fail");
/// # std::env::set_var("INSECURE_HARDWARE_ALLOWED", "test-must-fail");
/// keyfork_entropy::ensure_safe();
/// ```
pub fn ensure_safe() {
if !std::env::vars().any(|(name, value)| {
(name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
&& value != "test-must-fail"
}) {
if !std::env::vars()
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
{
ensure_safe_kernel_version();
ensure_offline();
}
@ -85,16 +61,6 @@ pub fn ensure_safe() {
///
/// # Errors
/// An error may be returned if an error occurred while reading from the random source.
///
/// # Examples
/// ```rust,no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// let entropy = keyfork_entropy::generate_entropy_of_size(64)?;
/// assert_eq!(entropy.len(), 64);
/// # Ok(())
/// # }
/// ```
pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::Error> {
ensure_safe();
let mut vec = vec![0u8; byte_count];
@ -102,24 +68,3 @@ pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::E
entropy_file.read_exact(&mut vec[..])?;
Ok(vec)
}
/// Read system entropy of a constant size.
///
/// # Errors
/// An error may be returned if an error occurred while reading from the random source.
///
/// # Examples
/// ```rust,no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// let entropy = keyfork_entropy::generate_entropy_of_const_size::<64>()?;
/// assert_eq!(entropy.len(), 64);
/// # Ok(())
/// # }
/// ```
pub fn generate_entropy_of_const_size<const N: usize>() -> Result<[u8; N], std::io::Error> {
let mut output = [0u8; N];
let mut entropy_file = File::open("/dev/urandom")?;
entropy_file.read_exact(&mut output[..])?;
Ok(output)
}

View File

@ -1,4 +1,4 @@
//! Generate entropy of a given size, encoded as hex.
//!
fn main() -> Result<(), Box<dyn std::error::Error>> {
let bit_size: usize = std::env::args()
@ -10,15 +10,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
bit_size % 8 == 0,
"Bit size must be divisible by 8, got: {bit_size}"
);
match bit_size {
128 | 256 | 512 => {}
_ => {
eprintln!("reading entropy of uncommon size: {bit_size}");
}
}
assert!(
bit_size <= 256,
"Maximum supported bit size is 256, got: {bit_size}"
);
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
println!("{}", smex::encode(entropy));
println!("{}", smex::encode(&entropy));
Ok(())
}

View File

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

View File

@ -66,11 +66,6 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
/// # Errors
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
/// constraint on a protocol level.
///
/// # Examples
/// ```rust
/// let data = keyfork_frame::try_encode(b"hello world!".as_slice()).unwrap();
/// ```
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
let mut output = vec![];
try_encode_to(data, &mut output)?;
@ -82,12 +77,6 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
/// # Errors
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
/// is unable to write data.
///
/// # Examples
/// ```rust
/// let mut output = vec![];
/// keyfork_frame::try_encode_to(b"hello world!".as_slice(), &mut output).unwrap();
/// ```
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
let hash = hash(data);
let len = hash.len() + data.len();
@ -118,40 +107,18 @@ pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
/// * The given `data` does not contain enough data to parse a length,
/// * The given `data` does not contain the given length's worth of data,
/// * The given `data` has a checksum that does not match what we build locally.
///
/// # Examples
/// ```rust
/// let input = b"hello world!";
/// let encoded = keyfork_frame::try_encode(input.as_slice()).unwrap();
/// let decoded = keyfork_frame::try_decode(&encoded).unwrap();
/// assert_eq!(input.as_slice(), decoded.as_slice());
/// ```
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
try_decode_from(&mut &data[..])
}
/// Read and decode a framed message into a `Vec<u8>`.
///
/// Note that unlike [`try_encode_to`], this method does not allow writing to an object
/// implementing Write. This is because the data must be stored entirely in memory to allow
/// verifying the data. The data is then returned using the same in-memory representation as is
/// used in memory, and a caller may then choose to use `writable.write_all()`.
///
/// # Errors
/// An error may be returned if:
/// * The given `data` does not contain enough data to parse a length,
/// * The given `data` does not contain the given length's worth of data,
/// * The given `data` has a checksum that does not match what we build locally.
/// * The source for the data returned an error.
///
/// # Examples
/// ```rust
/// let input = b"hello world!";
/// let mut encoded = vec![];
/// keyfork_frame::try_encode_to(input.as_slice(), &mut encoded).unwrap();
/// let decoded = keyfork_frame::try_decode_from(&mut &encoded[..]).unwrap();
/// assert_eq!(input.as_slice(), decoded.as_slice());
/// ```
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
let mut bytes = 0u32.to_be_bytes();
readable.read_exact(&mut bytes)?;

View File

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

View File

@ -1,6 +1,6 @@
//! Generate a mnemonic from hex-encoded input.
//!
use keyfork_mnemonic::Mnemonic;
use keyfork_mnemonic_util::Mnemonic;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input = std::io::stdin();
@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
input.read_line(&mut line)?;
let decoded = smex::decode(line.trim())?;
let mnemonic = Mnemonic::from_raw_bytes(&decoded) ;
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&decoded, Default::default()) };
println!("{mnemonic}");

View File

@ -0,0 +1,445 @@
//! Zero-dependency Mnemonic encoding and decoding.
use std::{error::Error, fmt::Display, str::FromStr, sync::Arc};
use hmac::Hmac;
use pbkdf2::pbkdf2;
use sha2::{Digest, Sha256, Sha512};
/// The error type representing a failure to create a [`Mnemonic`]. These errors only occur during
/// [`Mnemonic`] creation.
#[derive(Debug, Clone)]
pub enum MnemonicGenerationError {
/// The amount of bits passed to a mnemonic must be divisible by 32.
InvalidByteCount(usize),
/// The length of a mnemonic in bits must be within the BIP-0039 range, and supported by the
/// library. Currently, only 128, 192 (for testing purposes), and 256 are supported.
InvalidByteLength(usize),
/// Invalid length resulting from PBKDF2.
InvalidPbkdf2Length,
}
impl Display for MnemonicGenerationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MnemonicGenerationError::InvalidByteCount(count) => {
write!(f, "Invalid byte count: {count}, must be divisible by 8")
}
MnemonicGenerationError::InvalidByteLength(count) => {
write!(f, "Invalid byte length: {count}, must be 128 or 256")
}
MnemonicGenerationError::InvalidPbkdf2Length => {
f.write_str("Invalid length from PBKDF2")
}
}
}
}
impl Error for MnemonicGenerationError {}
/// A BIP-0039 compatible list of words.
#[derive(Debug, Clone)]
pub struct Wordlist(Vec<String>);
impl Default for Wordlist {
/// Returns the English wordlist in the Bitcoin BIP-0039 specification.
fn default() -> Self {
// TODO: English is the only supported language.
let wordlist_file = include_str!("data/wordlist.txt");
Wordlist(
wordlist_file
.lines()
// skip 1: comment at top of file to point to BIP-0039 source.
.skip(1)
.map(|x| x.trim().to_string())
.collect(),
)
}
}
impl Wordlist {
/// Return an Arced version of the Wordlist
#[allow(clippy::must_use_candidate)]
pub fn arc(self) -> Arc<Self> {
Arc::new(self)
}
/// Determine whether the Wordlist contains a given word.
pub fn contains(&self, word: &str) -> bool {
self.0.iter().any(|w| w.as_str() == word)
}
/// Given an index, get a word from the wordlist.
pub fn get_word(&self, word: usize) -> Option<&String> {
self.0.get(word)
}
/*
fn inner(&self) -> &Vec<String> {
&self.0
}
*/
#[cfg(test)]
fn into_inner(self) -> Vec<String> {
self.0
}
}
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
#[derive(Debug, Clone)]
pub struct Mnemonic {
entropy: Vec<u8>,
// words: Vec<usize>,
wordlist: Arc<Wordlist>,
}
impl Display for Mnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let bit_count = self.entropy.len() * 8;
let mut bits = vec![false; bit_count + bit_count / 32];
for byte_index in 0..bit_count / 8 {
for bit_index in 0..8 {
bits[byte_index * 8 + bit_index] =
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
}
}
let mut hasher = Sha256::new();
hasher.update(&self.entropy);
let hash = hasher.finalize().to_vec();
for check_bit in 0..bit_count / 32 {
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
}
let mut iter = bits
.chunks_exact(11)
.peekable()
.map(|chunk| {
let mut num = 0usize;
for i in 0..11 {
num += usize::from(chunk[10 - i]) << i;
}
num
})
.filter_map(|word| self.wordlist.get_word(word))
.peekable();
while let Some(word) = iter.next() {
f.write_str(word)?;
if iter.peek().is_some() {
f.write_str(" ")?;
}
}
Ok(())
}
}
/// The error type representing a failure to parse a [`Mnemonic`]. These errors only occur during
/// [`Mnemonic`] creation.
#[derive(Debug, Clone)]
pub enum MnemonicFromStrError {
/// The amount of words used to parse a mnemonic was not correct.
InvalidWordCount(usize),
/// One of the words used to generate the mnemonic was not found in the default wordlist.
InvalidWord(usize),
/// The checksum for the mnemonic did not match the given words.
InvalidChecksum,
}
impl Display for MnemonicFromStrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Mnemonic error: ")?;
match self {
MnemonicFromStrError::InvalidWordCount(count) => {
write!(f, "Incorrect word count: {count}")
}
MnemonicFromStrError::InvalidWord(index) => {
write!(f, "Unknown word at index: {index}")
}
MnemonicFromStrError::InvalidChecksum => {
f.write_str("Checksum of data did not match expected value")
}
}
}
}
impl Error for MnemonicFromStrError {}
impl FromStr for Mnemonic {
type Err = MnemonicFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let words: Vec<_> = s.split_whitespace().collect();
let mut usize_words = vec![];
let wordlist = Wordlist::default().arc();
let mut bits = vec![false; words.len() * 11];
for (index, word) in words.iter().enumerate() {
let word = wordlist
.0
.iter()
.position(|w| w == word)
.ok_or(MnemonicFromStrError::InvalidWord(index))?;
usize_words.push(word);
for bit in 0..11 {
bits[index * 11 + bit] = (word & (1 << (10 - bit))) > 0;
}
}
let mut checksum_bits = vec![false; bits.len() - (bits.len() * 32 / 33)];
checksum_bits.copy_from_slice(&bits[bits.len() * 32 / 33..]);
// remove checksum bits
bits.truncate(bits.len() * 32 / 33);
// bits.truncate(bits.len() - bits.len() % 32);
let entropy: Vec<u8> = bits
.chunks_exact(8)
.map(|chunk| {
let mut num = 0u8;
for i in 0..8 {
num += u8::from(chunk[7 - i]) << i;
}
num
})
.collect();
let mut hasher = Sha256::new();
hasher.update(&entropy);
let hash = hasher.finalize().to_vec();
for (i, bit) in checksum_bits.iter().enumerate() {
if !hash[i / 8] & (1 << (7 - (i % 8))) == u8::from(*bit) {
return Err(MnemonicFromStrError::InvalidChecksum);
}
}
Ok(Mnemonic {
entropy,
// words: usize_words,
wordlist,
})
}
}
impl Mnemonic {
/// Generate a [`Mnemonic`] from the provided entropy and [`Wordlist`].
///
/// # Errors
/// An error may be returned if the entropy is not within the acceptable lengths.
pub fn from_entropy(
bytes: &[u8],
wordlist: Arc<Wordlist>,
) -> Result<Mnemonic, MnemonicGenerationError> {
let bit_count = bytes.len() * 8;
if bit_count % 32 != 0 {
return Err(MnemonicGenerationError::InvalidByteCount(bit_count));
}
// 192 supported for test suite
if ![128, 192, 256].contains(&bit_count) {
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
}
Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) })
}
/// # Safety
///
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`.
pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
Mnemonic {
entropy: bytes.to_vec(),
wordlist,
}
}
/// The internal representation of the decoded data.
pub fn as_bytes(&self) -> &[u8] {
&self.entropy
}
/// The internal representation of the decoded data, as a [`Vec<u8>`].
pub fn to_bytes(&self) -> Vec<u8> {
self.entropy.to_vec()
}
/// Drop self, returning the decoded data.
pub fn into_bytes(self) -> Vec<u8> {
self.entropy
}
/// Clone the existing entropy.
#[deprecated]
pub fn entropy(&self) -> Vec<u8> {
self.entropy.clone()
}
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
///
/// # Errors
/// The method may return an error if the pbkdf2 function returns an invalid length, but this
/// case should not be reached.
pub fn seed<'a>(
&self,
passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> {
let passphrase = passphrase.into();
let mut seed = [0u8; 64];
let mnemonic = self.to_string();
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?;
Ok(seed.to_vec())
}
/// Encode the mnemonic into a list of wordlist indexes.
pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) {
let bit_count = self.entropy.len() * 8;
let mut bits = vec![false; bit_count + bit_count / 32];
for byte_index in 0..bit_count / 8 {
for bit_index in 0..8 {
bits[byte_index * 8 + bit_index] =
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
}
}
let mut hasher = Sha256::new();
hasher.update(&self.entropy);
let hash = hasher.finalize().to_vec();
for check_bit in 0..bit_count / 32 {
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
}
let words = bits.chunks_exact(11).peekable().map(|chunk| {
let mut num = 0usize;
for i in 0..11 {
num += usize::from(chunk[10 - i]) << i;
}
num
});
(words.collect(), self.wordlist.clone())
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, fs::File, io::Read};
use super::*;
#[test]
fn wordlist_word_count_correct() {
let wordlist = Wordlist::default().into_inner();
assert_eq!(
wordlist.len(),
2usize.pow(11),
"Wordlist did not include correct word count"
);
}
#[test]
fn reproduces_its_own_seed() {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let wordlist = Wordlist::default().arc();
let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
let new_entropy = mnemonic.entropy();
assert_eq!(&new_entropy, entropy);
}
#[test]
fn conforms_to_trezor_tests() {
let content = include_str!("data/vectors.json");
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
let wordlist = Wordlist::default().arc();
for test in jsonobj["english"].as_array().unwrap() {
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
panic!("bad test: {test}");
};
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
let mnemonic = Mnemonic::from_entropy(&hex, wordlist.clone()).unwrap();
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
}
}
#[test]
fn matches_bip39_crate() {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let wordlist = Wordlist::default().arc();
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).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.seed(None).unwrap(), their_mnemonic.to_seed(""));
assert_eq!(
my_mnemonic.seed("testing").unwrap(),
their_mnemonic.to_seed("testing")
);
assert_ne!(
my_mnemonic.seed("test1").unwrap(),
their_mnemonic.to_seed("test2")
);
}
#[test]
fn count_rate_of_duplicate_words() {
let tests = 100_000;
let mut count = 0.;
let entropy = &mut [0u8; 256 / 8];
let wordlist = Wordlist::default().arc();
let mut random = std::fs::File::open("/dev/urandom").unwrap();
let mut hs = HashSet::<usize>::with_capacity(24);
for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap();
let (words, _) = mnemonic.words();
hs.clear();
hs.extend(words);
if hs.len() != 24 {
count += 1.;
}
}
// NOTE: Birthday problem math is: 0.126532
// Set values to (about) 1 below, 1 above
// Source: https://en.wikipedia.org/wiki/Birthday_problem
let min = 11.5;
let max = 13.5;
assert!(
count > f64::from(tests) * min / 100.,
"{count} probability should be more than {min}%: {}",
count / f64::from(tests)
);
assert!(
count < f64::from(tests) * max / 100.,
"{count} probability should be more than {max}%: {}",
count / f64::from(tests)
);
}
#[test]
fn can_do_up_to_1024_bits() {
let entropy = &mut [0u8; 128];
let wordlist = Wordlist::default().arc();
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&entropy[..], wordlist.clone()) };
let (words, _) = mnemonic.words();
assert!(words.len() == 96);
}
}

View File

@ -1,622 +0,0 @@
//! Zero-dependency mnemonic encoding and decoding of data.
//!
//! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase:
//!
//! ```rust
//! use keyfork_mnemonic::Mnemonic;
//! let data = b"Hello, world! I am a mnemonic :)";
//! assert_eq!(data.len(), 32);
//! let mnemonic = Mnemonic::try_from_slice(data).unwrap();
//! println!("Our mnemonic is: {mnemonic}");
//! ```
//!
//! A mnemonic can also be parsed from a string:
//!
//! ```rust
//! use keyfork_mnemonic::Mnemonic;
//! use std::str::FromStr;
//!
//! let data = b"Hello, world! I am a mnemonic :)";
//! let words = "embody clock brand tattoo search desert saddle eternal
//! goddess animal banner dolphin bitter mother loyal asset
//! hover clock forward system normal mosquito trim credit";
//! let mnemonic = Mnemonic::from_str(words).unwrap();
//! assert_eq!(&data[..], mnemonic.as_bytes());
//! ```
//!
//! Mnemonics can also be used to store data of other lengths, but such functionality is not
//! verified to be safe:
//!
//! ```rust
//! use keyfork_mnemonic::Mnemonic;
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
//! let mnemonic_text = mnemonic.to_string();
//! ```
//!
//! If given an invalid length, undefined behavior may follow, or code may panic.
//!
//! ```rust,should_panic
//! use keyfork_mnemonic::Mnemonic;
//! use std::str::FromStr;
//!
//! // NOTE: Data is of invalid length, 31
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
//! let mnemonic_text = mnemonic.to_string();
//! // NOTE: panic happens here
//! let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
//! ```
use std::{error::Error, fmt::Display, marker::PhantomData, str::FromStr, sync::OnceLock};
use keyfork_bug::bug;
use hmac::Hmac;
use pbkdf2::pbkdf2;
use sha2::{Digest, Sha256, Sha512};
/// The error type representing a failure to create a [`Mnemonic`]. These errors only occur during
/// [`Mnemonic`] creation.
#[derive(Debug, Clone)]
pub enum MnemonicGenerationError {
/// The amount of bits passed to a mnemonic must be divisible by 32.
InvalidByteCount(usize),
/// The length of a mnemonic in bits must be within the BIP-0039 range, and supported by the
/// library. Currently, only 128, 192 (for testing purposes), and 256 are supported.
InvalidByteLength(usize),
/// Invalid length resulting from PBKDF2.
InvalidPbkdf2Length,
}
impl Display for MnemonicGenerationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MnemonicGenerationError::InvalidByteCount(count) => {
write!(f, "Invalid byte count: {count}, must be divisible by 8")
}
MnemonicGenerationError::InvalidByteLength(count) => {
write!(f, "Invalid byte length: {count}, must be 128 or 256")
}
MnemonicGenerationError::InvalidPbkdf2Length => {
f.write_str("Invalid length from PBKDF2")
}
}
}
}
impl Error for MnemonicGenerationError {}
/// A trait representing a BIP-0039 wordlist, of 2048 words, with each word having a unique first
/// three letters.
pub trait Wordlist: std::fmt::Debug {
/// Get a reference to a [`std::sync::OnceLock`] Self.
fn get_singleton<'a>() -> &'a Self;
/// Return a representation of the words in the wordlist as an array of [`str`].
fn to_str_array(&self) -> [&str; 2048];
}
/// A wordlist for the English language, from the BIP-0039 dataset.
#[derive(Debug)]
pub struct English {
words: [String; 2048],
}
static ENGLISH: OnceLock<English> = OnceLock::new();
impl Wordlist for English {
fn get_singleton<'a>() -> &'a Self {
ENGLISH.get_or_init(|| {
let wordlist_file = include_str!("data/wordlist.txt");
let mut words = wordlist_file.lines().skip(1).map(|x| x.trim().to_string());
English {
words: std::array::from_fn(|_| {
words.next().expect(bug!("wordlist {} should have 2048 words"))
}),
}
})
}
fn to_str_array(&self) -> [&str; 2048] {
std::array::from_fn(|i| self.words[i].as_str())
}
}
struct AssertValidMnemonicSize<const N: usize>;
impl<const N: usize> AssertValidMnemonicSize<N> {
const OK_CHUNKS: () = assert!(N % 4 == 0, "bytes must be a length divisible by 4");
const OK_SIZE: () = assert!(N <= 1024, "bytes must be less-or-equal 1024");
}
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MnemonicBase<W: Wordlist> {
data: Vec<u8>,
marker: PhantomData<W>,
}
/// A default Mnemonic using the English language.
pub type Mnemonic = MnemonicBase<English>;
impl<W> Display for MnemonicBase<W>
where
W: Wordlist,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let wordlist = W::get_singleton();
let words = wordlist.to_str_array();
let mut iter = self
.words()
.into_iter()
.filter_map(|word| words.get(word))
.peekable();
while let Some(word) = iter.next() {
f.write_str(word)?;
if iter.peek().is_some() {
f.write_str(" ")?;
}
}
Ok(())
}
}
/// The error type representing a failure to parse a [`Mnemonic`]. These errors only occur during
/// [`Mnemonic`] creation.
#[derive(Debug, Clone)]
pub enum MnemonicFromStrError {
/// The amount of words used to parse a mnemonic was not correct.
InvalidWordCount(usize),
/// One of the words used to generate the mnemonic was not found in the default wordlist.
InvalidWord(usize),
/// The checksum for the mnemonic did not match the given words.
InvalidChecksum,
}
impl Display for MnemonicFromStrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Mnemonic error: ")?;
match self {
MnemonicFromStrError::InvalidWordCount(count) => {
write!(f, "Incorrect word count: {count}")
}
MnemonicFromStrError::InvalidWord(index) => {
write!(f, "Unknown word at index: {index}")
}
MnemonicFromStrError::InvalidChecksum => {
f.write_str("Checksum of data did not match expected value")
}
}
}
}
impl Error for MnemonicFromStrError {}
impl<W> FromStr for MnemonicBase<W>
where
W: Wordlist,
{
type Err = MnemonicFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let wordlist = W::get_singleton();
let wordlist_words = wordlist.to_str_array();
let words: Vec<_> = s.split_whitespace().collect();
let mut usize_words = vec![];
let mut bits = vec![false; words.len() * 11];
for (index, word) in words.iter().enumerate() {
let word = wordlist_words
.iter()
.position(|w| w == word)
.ok_or(MnemonicFromStrError::InvalidWord(index))?;
usize_words.push(word);
for bit in 0..11 {
bits[index * 11 + bit] = (word & (1 << (10 - bit))) > 0;
}
}
let mut checksum_bits = vec![false; bits.len() - (bits.len() * 32 / 33)];
checksum_bits.copy_from_slice(&bits[bits.len() * 32 / 33..]);
// remove checksum bits
bits.truncate(bits.len() * 32 / 33);
// bits.truncate(bits.len() - bits.len() % 32);
let data: Vec<u8> = bits
.chunks_exact(8)
.map(|chunk| {
let mut num = 0u8;
for i in 0..8 {
num += u8::from(chunk[7 - i]) << i;
}
num
})
.collect();
let mut hasher = Sha256::new();
hasher.update(&data);
let hash = hasher.finalize().to_vec();
for (i, bit) in checksum_bits.iter().enumerate() {
if !hash[i / 8] & (1 << (7 - (i % 8))) == u8::from(*bit) {
return Err(MnemonicFromStrError::InvalidChecksum);
}
}
Ok(MnemonicBase {
data,
marker: PhantomData,
})
}
}
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.
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = Mnemonic::try_from_slice(data.as_slice()).unwrap();
/// ```
pub fn try_from_slice(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
let bit_count = bytes.len() * 8;
if bit_count % 32 != 0 {
return Err(MnemonicGenerationError::InvalidByteCount(bit_count));
}
// 192 supported for test suite
if ![128, 192, 256].contains(&bit_count) {
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
}
Ok( Self::from_raw_bytes(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.
///
/// ```rust
/// use keyfork_mnemonic::Mnemonic;
/// let data = b"hello world!";
/// let mnemonic = Mnemonic::from_array(*data);
/// ```
///
/// If an invalid size is requested, the code will fail to compile:
///
/// ```rust,compile_fail
/// use keyfork_mnemonic::Mnemonic;
/// let mnemonic = Mnemonic::from_array([0u8; 53]);
/// ```
///
/// ```rust,compile_fail
/// use keyfork_mnemonic::Mnemonic;
/// let mnemonic = Mnemonic::from_array([0u8; 1024 + 4]);
/// ```
pub fn from_array<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
#[allow(clippy::let_unit_value)]
{
let () = AssertValidMnemonicSize::<N>::OK_CHUNKS;
let () = AssertValidMnemonicSize::<N>::OK_SIZE;
}
Self::from_raw_bytes(&bytes)
}
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
///
/// # Panics
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`. If the assumption is incorrect, code may panic. The
/// [`MnemonicBase::from_array`] function may be used to generate entropy if the length of the
/// data is known at compile-time.
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string();
/// ```
///
/// If given an invalid length, undefined behavior may follow, or code may panic.
///
/// ```rust,should_panic
/// use keyfork_mnemonic::Mnemonic;
/// use std::str::FromStr;
///
/// // NOTE: Data is of invalid length, 31
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// ```
pub fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
assert!(bytes.len() % 4 == 0);
assert!(bytes.len() <= 1024);
MnemonicBase {
data: bytes.to_vec(),
marker: PhantomData,
}
}
/// 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.
///
/// # Safety
///
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`. If the assumption is incorrect, code may panic.
#[deprecated = "use Mnemonic::from_raw_bytes"]
pub unsafe fn from_raw_entropy(bytes: &[u8]) -> MnemonicBase<W> {
MnemonicBase {
data: bytes.to_vec(),
marker: PhantomData,
}
}
/// A view to internal representation of the decoded data.
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
/// A view to internal representation of the decoded data.
pub fn as_slice(&self) -> &[u8] {
&self.data
}
/// A clone of the internal representation of the decoded data.
pub fn to_bytes(&self) -> Vec<u8> {
self.data.to_vec()
}
/// A clone of the internal representation of the decoded data.
pub fn to_vec(&self) -> Vec<u8> {
self.data.to_vec()
}
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_bytes(self) -> Vec<u8> {
self.data
}
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_vec(self) -> Vec<u8> {
self.data
}
/// Clone the existing data.
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> {
self.data.clone()
}
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
///
/// # Errors
/// The method should not return an error.
#[deprecated = "Use generate_seed() instead"]
pub fn seed<'a>(
&self,
passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> {
Ok(self.generate_seed(passphrase).to_vec())
}
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
///
/// # Panics
/// The function may panic if the HmacSha512 function returns an error. The only error the
/// HmacSha512 function should return is an invalid length, which should not be possible.
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> [u8; 64] {
let passphrase = passphrase.into();
let mut seed = [0u8; 64];
let mnemonic = self.to_string();
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.expect(bug!("HmacSha512 InvalidLength should be infallible"));
seed
}
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
/// BIP-0039 wordlist.
pub fn words(&self) -> Vec<usize> {
let bit_count = self.data.len() * 8;
let mut bits = vec![false; bit_count + bit_count / 32];
for byte_index in 0..bit_count / 8 {
for bit_index in 0..8 {
bits[byte_index * 8 + bit_index] =
(self.data[byte_index] & (1 << (7 - bit_index))) > 0;
}
}
let mut hasher = Sha256::new();
hasher.update(&self.data);
let hash = hasher.finalize().to_vec();
for check_bit in 0..bit_count / 32 {
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
}
// TODO: find a way to not have to collect to vec
bits.chunks_exact(11)
.peekable()
.map(|chunk| {
let mut num = 0usize;
for i in 0..11 {
num += usize::from(chunk[10 - i]) << i;
}
num
})
.collect()
}
}
impl<W> MnemonicBase<W>
where
W: Wordlist,
{
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::try_from_slice"]
pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::try_from_slice(bytes)
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::try_from_slice"]
pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::try_from_slice(bytes)
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data may be of a size
/// of a factor of 4, up to 1024 bytes.
///
#[deprecated = "Use Mnemonic::from_array"]
pub fn from_nonstandard_bytes<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
MnemonicBase::from_array(bytes)
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, fs::File, io::Read};
use super::*;
#[test]
fn can_load_wordlist() {
let _wordlist = English::get_singleton();
}
#[test]
fn reproduces_its_own_seed() {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let mnemonic = super::Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let new_entropy = mnemonic.as_bytes();
assert_eq!(new_entropy, entropy);
}
#[test]
fn conforms_to_trezor_tests() {
let content = include_str!("data/vectors.json");
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
for test in jsonobj["english"].as_array().unwrap() {
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
panic!("bad test: {test}");
};
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
let mnemonic = Mnemonic::try_from_slice(&hex).unwrap();
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
}
}
#[test]
fn matches_bip39_crate() {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let my_mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
assert_eq!(
my_mnemonic.generate_seed("testing"),
their_mnemonic.to_seed("testing")
);
assert_ne!(
my_mnemonic.generate_seed("test1"),
their_mnemonic.to_seed("test2")
);
}
#[test]
fn count_rate_of_duplicate_words() {
let tests = 100_000;
let mut count = 0.;
let entropy = &mut [0u8; 256 / 8];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
let mut hs = HashSet::<usize>::with_capacity(24);
for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let words = mnemonic.words();
hs.clear();
hs.extend(words);
if hs.len() != 24 {
count += 1.;
}
}
// NOTE: Birthday problem math is: 0.126532
// Set values to (about) 1 below, 1 above
// Source: https://en.wikipedia.org/wiki/Birthday_problem
let min = 11.5;
let max = 13.5;
assert!(
count > f64::from(tests) * min / 100.,
"{count} probability should be more than {min}%: {}",
count / f64::from(tests)
);
assert!(
count < f64::from(tests) * max / 100.,
"{count} probability should be more than {max}%: {}",
count / f64::from(tests)
);
}
#[test]
fn can_do_up_to_8192_bits() {
let mut entropy = [0u8; 1024];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_array(entropy);
let words = mnemonic.words();
assert_eq!(words.len(), 768);
}
#[test]
#[should_panic]
fn fails_over_8192_bits() {
let entropy = &mut [0u8; 1024 + 4];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
}
#[test]
#[should_panic]
fn fails_over_invalid_size() {
let entropy = &mut [0u8; 255];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
}
}

View File

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

View File

@ -1,4 +1,4 @@
#![allow(missing_docs)]
//!
use std::io::{stdin, stdout};
@ -7,8 +7,6 @@ use keyfork_prompt::{
Terminal, PromptHandler,
};
use keyfork_mnemonic::English;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut mgr = Terminal::new(stdin(), stdout())?;
let transport_validator = mnemonic::MnemonicSetValidator {
@ -18,21 +16,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
word_lengths: [24, 48],
};
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
let mnemonics = mgr.prompt_validated_wordlist(
"Enter a 9-word and 24-word mnemonic: ",
&Default::default(),
3,
transport_validator.to_fn(),
)?;
assert_eq!(mnemonics[0].as_bytes().len(), 12);
assert_eq!(mnemonics[1].as_bytes().len(), 32);
assert_eq!(mnemonics[0].entropy().len(), 12);
assert_eq!(mnemonics[1].entropy().len(), 32);
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
let mnemonics = mgr.prompt_validated_wordlist(
"Enter a 24 and 48-word mnemonic: ",
&Default::default(),
3,
combine_validator.to_fn(),
)?;
assert_eq!(mnemonics[0].as_bytes().len(), 32);
assert_eq!(mnemonics[1].as_bytes().len(), 64);
assert_eq!(mnemonics[0].entropy().len(), 32);
assert_eq!(mnemonics[1].entropy().len(), 64);
Ok(())
}

View File

@ -3,7 +3,7 @@
use std::borrow::Borrow;
#[cfg(feature = "mnemonic")]
use keyfork_mnemonic::Wordlist;
use keyfork_mnemonic_util::Wordlist;
///
pub mod terminal;
@ -51,31 +51,31 @@ pub trait PromptHandler {
/// could not be read.
fn prompt_input(&mut self, prompt: &str) -> Result<String>;
/// Prompt the user for input based on a wordlist. A language must be specified as the generic
/// parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
/// Prompt the user for input based on a wordlist.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if the input
/// could not be read.
#[cfg(feature = "mnemonic")]
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist;
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String>;
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser. A language must be specified
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
/// provided parser function, returning the type from the parser.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>(
fn prompt_validated_wordlist<V, F, E>(
&mut self,
prompt: &str,
wordlist: &Wordlist,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
validator_fn: F,
) -> Result<V, Error>
where
X: Wordlist;
F: Fn(String) -> Result<V, E>,
E: std::error::Error;
/// Prompt the user for a passphrase, which is hidden while typing.
///
@ -90,12 +90,15 @@ pub trait PromptHandler {
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_passphrase<V>(
fn prompt_validated_passphrase<V, F, E>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>;
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error;
/// Prompt the user with a [`Message`].
///

View File

@ -1,13 +1,6 @@
//! A terminal prompt handler.
//!
//! This prompt handler uses a raw terminal device to read inputs and uses ANSI escape codes to
//! provide formatting for prompts. Because of these reasons, it is not intended to be
//! machine-readable.
use std::{
borrow::Borrow,
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
os::fd::AsRawFd,
os::fd::AsRawFd, borrow::Borrow,
};
use keyfork_crossterm::{
@ -19,9 +12,7 @@ use keyfork_crossterm::{
ExecutableCommand, QueueableCommand,
};
use keyfork_bug::bug;
use crate::{Error, Message, PromptHandler, Wordlist};
use crate::{PromptHandler, Message, Wordlist, Error};
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -128,15 +119,9 @@ where
W: Write + AsRawFd,
{
fn drop(&mut self) {
self.write
.execute(DisableBracketedPaste)
.expect(bug!("can't restore bracketed paste"));
self.write
.execute(LeaveAlternateScreen)
.expect(bug!("can't leave alternate screen"));
self.terminal
.disable_raw_mode()
.expect(bug!("can't disable raw mode"));
self.write.execute(DisableBracketedPaste).unwrap();
self.write.execute(LeaveAlternateScreen).unwrap();
self.terminal.disable_raw_mode().unwrap();
}
}
@ -170,13 +155,11 @@ where
fn lock(&mut self) -> TerminalGuard<'_, R, W> {
TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal)
}
}
impl<R, W> PromptHandler for Terminal<R, W>
where
R: Read + Sized,
W: Write + AsRawFd + Sized,
{
impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + AsRawFd + Sized {
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
let mut terminal = self.lock().alternate_screen()?;
terminal
@ -199,18 +182,20 @@ where
}
#[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>(
fn prompt_validated_wordlist<V, F, E>(
&mut self,
prompt: &str,
wordlist: &Wordlist,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
validator_fn: F,
) -> Result<V, Error>
where
X: Wordlist,
F: Fn(String) -> Result<V, E>,
E: std::error::Error,
{
let mut last_error = None;
for _ in 0..retries {
let s = self.prompt_wordlist::<X>(prompt)?;
let s = self.prompt_wordlist(prompt, wordlist)?;
match validator_fn(s) {
Ok(v) => return Ok(v),
Err(e) => {
@ -229,13 +214,7 @@ where
#[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)]
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
where
X: Wordlist,
{
let wordlist = X::get_singleton();
let words = wordlist.to_str_array();
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> {
let mut terminal = self
.lock()
.alternate_screen()?
@ -300,20 +279,6 @@ where
}
KeyCode::Char(c) => {
input.push(c);
let entry_mode = std::env::var("KEYFORK_PROMPT_MNEMONIC_MODE");
if entry_mode.is_ok_and(|mode| mode.to_ascii_lowercase() == "steel") {
let word = input.split_whitespace().next_back().map(ToOwned::to_owned);
if let Some(steel_word) = word {
if steel_word.len() >= 4 {
for word in words.iter().filter(|word| word.len() >= 4) {
if word[..4] == steel_word {
input.push_str(&word[4..]);
input.push(' ');
}
}
}
}
}
}
_ => (),
},
@ -351,7 +316,7 @@ where
let mut iter = printable_input.split_whitespace().peekable();
while let Some(word) = iter.next() {
if words.contains(&word) {
if wordlist.contains(word) {
terminal.queue(PrintStyledContent(word.green()))?;
} else {
terminal.queue(PrintStyledContent(word.red()))?;
@ -372,12 +337,16 @@ where
Ok(input)
}
fn prompt_validated_passphrase<V>(
fn prompt_validated_passphrase<V, F, E>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error> {
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error,
{
let mut last_error = None;
for _ in 0..retries {
let s = self.prompt_passphrase(prompt)?;

View File

@ -12,7 +12,7 @@ pub trait Validator {
type Error;
/// Create a validator function from the given parameters.
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>>;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>>;
}
/// A PIN could not be validated from the given input.
@ -29,84 +29,6 @@ pub enum PinError {
/// The PIN contained invalid characters.
#[error("PIN contained invalid characters (found {0} at position {1})")]
InvalidCharacters(char, usize),
/// The provided PIN had either too many repeated characters or too many sequential characters.
#[error("PIN contained too many repeated or sequential characters")]
InsecurePIN,
}
/// Validate that a PIN is of a certain length, matches a range of characters, and does not use
/// incrementing or decrementing sequences of characters.
///
/// The validator determines a score for a passphrase and, if the score is high enough, returns an
/// error.
///
/// Score is calculated based on:
/// * how many sequential characters are in the passphrase (ascending or descending)
/// * how many repeated characters are in the passphrase
#[derive(Default, Clone)]
pub struct SecurePinValidator {
/// The minimum length of provided PINs.
pub min_length: Option<usize>,
/// The maximum length of provided PINs.
pub max_length: Option<usize>,
/// The characters allowed by the PIN parser.
pub range: Option<RangeInclusive<char>>,
/// Whether repeated characters count against the PIN.
pub ignore_repeated_characters: bool,
/// Whether sequential characters count against the PIN.
pub ignore_sequential_characters: bool,
}
impl Validator for SecurePinValidator {
type Output = String;
type Error = PinError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
let min_len = self.min_length.unwrap_or(usize::MIN);
let max_len = self.max_length.unwrap_or(usize::MAX);
let range = self.range.clone().unwrap_or('0'..='9');
let ignore_repeated_characters = self.ignore_repeated_characters;
let ignore_sequential_characters = self.ignore_sequential_characters;
Box::new(move |mut s: String| {
s.truncate(s.trim_end().len());
let len = s.len();
if len < min_len {
return Err(Box::new(PinError::TooShort(len, min_len)));
}
if len > max_len {
return Err(Box::new(PinError::TooLong(len, max_len)));
}
let mut last_char = 0;
let mut score = 0;
for (index, ch) in s.chars().enumerate() {
if !range.contains(&ch) {
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
}
if [-1, 1].contains(&(ch as i32 - last_char))
&& !ignore_sequential_characters
{
score += 1;
}
last_char = ch as i32;
}
let mut chars = s.chars().collect::<Vec<_>>();
chars.sort();
chars.dedup();
if !ignore_repeated_characters {
// SAFETY: the amount of characters can't have _increased_ since deduping
score += s.chars().count() - chars.len();
}
if score * 2 > s.chars().count() {
return Err(Box::new(PinError::InsecurePIN))
}
Ok(s)
})
}
}
/// Validate that a PIN is of a certain length and matches a range of characters.
@ -126,7 +48,7 @@ impl Validator for PinValidator {
type Output = String;
type Error = PinError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, PinError>> {
let min_len = self.min_length.unwrap_or(usize::MIN);
let max_len = self.max_length.unwrap_or(usize::MAX);
let range = self.range.clone().unwrap_or('0'..='9');
@ -134,14 +56,14 @@ impl Validator for PinValidator {
s.truncate(s.trim_end().len());
let len = s.len();
if len < min_len {
return Err(Box::new(PinError::TooShort(len, min_len)));
return Err(PinError::TooShort(len, min_len));
}
if len > max_len {
return Err(Box::new(PinError::TooLong(len, max_len)));
return Err(PinError::TooLong(len, max_len));
}
for (index, ch) in s.chars().enumerate() {
if !range.contains(&ch) {
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
return Err(PinError::InvalidCharacters(ch, index));
}
}
Ok(s)
@ -157,8 +79,7 @@ pub mod mnemonic {
use super::Validator;
use keyfork_bug::bug;
use keyfork_mnemonic::{Mnemonic, MnemonicFromStrError};
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
/// A mnemonic could not be validated from the given input.
#[derive(thiserror::Error, Debug)]
@ -202,13 +123,13 @@ pub mod mnemonic {
type Output = Mnemonic;
type Error = MnemonicValidationError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Box<dyn std::error::Error>>> {
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Self::Error>> {
let word_length = self.word_length.clone();
Box::new(move |s: String| match word_length.as_ref() {
Some(wl) => {
let count = s.split_whitespace().count();
if !wl.matches(count) {
return Err(Box::new(Self::Error::InvalidLength(count, wl.clone())));
return Err(Self::Error::InvalidLength(count, wl.clone()));
}
let m = Mnemonic::from_str(&s)?;
Ok(m)
@ -244,7 +165,7 @@ pub mod mnemonic {
type Output = Mnemonic;
type Error = MnemonicChoiceValidationError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>> {
let word_lengths = self.word_lengths.clone();
Box::new(move |s: String| {
let count = s.split_whitespace().count();
@ -254,10 +175,10 @@ pub mod mnemonic {
return Ok(m);
}
}
Err(Box::new(MnemonicChoiceValidationError::InvalidLength(
Err(MnemonicChoiceValidationError::InvalidLength(
count,
word_lengths.to_vec(),
)))
))
})
}
}
@ -286,7 +207,7 @@ pub mod mnemonic {
type Output = [Mnemonic; N];
type Error = MnemonicSetValidationError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>> {
let word_lengths = self.word_lengths;
Box::new(move |s: String| {
let mut counter: usize = 0;
@ -298,17 +219,15 @@ pub mod mnemonic {
.take(word_length)
.collect::<Vec<_>>();
if words.len() != word_length {
return Err(Box::new(MnemonicSetValidationError::InvalidSetLength(
return Err(MnemonicSetValidationError::InvalidSetLength(
word_set,
words.len(),
word_length,
)));
));
}
let mnemonic = match Mnemonic::from_str(&words.join(" ")) {
Ok(m) => m,
Err(e) => {
return Err(Box::new(Self::Error::MnemonicFromStrError(word_set, e)))
}
Err(e) => return Err(Self::Error::MnemonicFromStrError(word_set, e)),
};
output.push(mnemonic);
counter += word_length;
@ -316,7 +235,7 @@ pub mod mnemonic {
Ok(output
.try_into()
.expect(bug!("vec with capacity of const N was not filled")))
.expect("vec with capacity of const N was not filled"))
})
}
}

View File

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

View File

@ -1,5 +1,5 @@
//! SLIP-0010 test data for use by derivation tests.
//! Source: <https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors>
//! Source: https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors
use std::collections::HashMap;

View File

@ -28,15 +28,7 @@ impl std::fmt::Display for DecodeError {
impl std::error::Error for DecodeError {}
/// Encode a given input as a hex string.
///
/// # Examples
/// ```rust
/// let data = b"hello world!";
/// let result = smex::encode(&data);
/// assert_eq!(result, "68656c6c6f20776f726c6421");
/// ```
pub fn encode(input: impl AsRef<[u8]>) -> String {
let input = input.as_ref();
pub fn encode(input: &[u8]) -> String {
let mut s = String::new();
for byte in input {
write!(s, "{byte:02x}").unwrap();
@ -58,26 +50,7 @@ fn val(c: u8) -> Result<u8, DecodeError> {
/// # Errors
/// The function may error if a non-hex character is encountered or if the character count is not
/// evenly divisible by two.
///
/// # Examples
/// ```rust
/// let data = b"hello world!";
/// let encoded = smex::encode(&data);
/// let decoded = smex::decode(&encoded).unwrap();
/// assert_eq!(data.as_slice(), decoded.as_slice());
/// ```
///
/// The function may return an error if the given input is not valid hex.
///
/// ```rust,should_panic
/// let data = b"hello world!";
/// let mut encoded = smex::encode(&data);
/// encoded.push('G');
/// let decoded = smex::decode(&encoded).unwrap();
/// assert_eq!(data.as_slice(), decoded.as_slice());
/// ```
pub fn decode(input: impl AsRef<str>) -> Result<Vec<u8>, DecodeError> {
let input = input.as_ref();
pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
let len = input.len();
if len % 2 != 0 {
return Err(DecodeError::InvalidCharacterCount(len));

104
deny.toml
View File

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

View File

@ -1,5 +1,3 @@
{{#include links.md}}
## Dependencies
Keyfork has different dependencies depending on the feature set used for
@ -68,3 +66,5 @@ cargo install --index https://git.distrust.co/public/_cargo-index keyfork-entrop
# Confirmed to work as of 2024-01-17.
cargo install --locked --path crates/util/keyfork-entropy --bin keyfork-entropy --features bin
```
[SBOM]: https://en.wikipedia.org/wiki/SBOM

View File

@ -1,10 +1,8 @@
<!-- vim:set et sts=0 sw=2 ts=2: -->
{{ #include links.md }}
# Summary
# User Guide
- [Introduction to Keyfork](./introduction.md)
- [Installing Keyfork](./INSTALL.md)
- [Security Considerations](./security.md)
- [Shard Commands](./shard.md)
@ -33,4 +31,3 @@
- [Provisioners](./dev-guide/provisioners.md)
- [Auditing Dependencies](./dev-guide/auditing.md)
- [Entropy Guide](./dev-guide/entropy.md)
- [The Shard Protocol](./dev-guide/shard-protocol.md)

View File

@ -1,5 +1,3 @@
{{#include ../links.md}}
# keyfork-derive-key
Derive a key from a given derivation path.
@ -20,3 +18,5 @@ the shell silently ignoring the single quotes in the derivation path.
Hex-encoded private key. Note that this is not the _extended_ private key, and
can't be used to derive further data.
[`keyforkd`]: ./bin/keyforkd.md

Some files were not shown because too many files have changed in this diff Show More