Compare commits

..

No commits in common. "main" and "ryan/keyfork-shard/reuse-prompt-handler" have entirely different histories.

104 changed files with 2067 additions and 6115 deletions

View File

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

1
.gitattributes vendored
View File

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

View File

@ -1,517 +0,0 @@
# Keyfork v0.3.0
The Wizard is Dead. Long Live the Mnemonic Generator.
The `keyfork wizard` subcommand was previously used to perform complex
operations that couldn't be performed with just `keyfork mnemonic generate`.
Since we've introduced complexity into `keyfork mnemonic generate`, it only
makes sense to consolidate all mnemonic generation complexity into one
location. Therefore, `keyfork mnemonic generate` should be a one-stop shop from
going to zero entropy to 256 bits of entropy. :)
The following operations are added:
* `keyfork mnemonic generate --derive=<derivation>`: Allow for the immediate
derivation of a key. The value passed will be parsed directly as though
`keyfork derive` were run. For example,
`keyfork mnemonic generate --derive='openpgp "Ryan Heywood"'` generates an
OpenPGP Transferable Secret Key that is nearly-identical to one generated by
`keyfork derive openpgp "Ryan Heywood"`, with the only exception being the
time the signatures were created.
* `keyfork mnemonic generate --encrypt-to <keyring>`: Encrypt the mnemonic to
an existing OpenPGP keyring or certificate.
* `keyfork mnemonic generate --shard-to <shardfile>`: Shard the mnemonic to
an existing Keyfork Shardfile.
* `keyfork mnemonic generate --shard <config>`: Shard the mnemonic to an
existing set of OpenPGP certificates.
* `keyfork mnemonic generate --encrypt-to-self <file>`: Encrypt the mnemonic to
an OpenPGP certificate generated in `--derive` or `--provision`
* `keyfork mnemonic generate --shard-to-self <file>,<config>`: Shard the
mnemonic to freshly generated certificates, provisioned to OpenPGP
smartcards. This option replaces the traditional Keyfork Wizard, which has
been removed.
* `keyfork mnemonic generate --provision`: Provision a key derived from the new
mnemonic, which can be used for `--encrypt-to-self`, or to just bypass
needing to load the mnemonic to provision with it.
Along with these changes, some other minor additions were added:
* QR code retries in the Shard mechanism are now implemented.
* `keyfork-qrcode` now prefers libzbar and can compile with both.
* `keyfork-prompt` should now work better on AirgapOS and Linux terminals.
### Changes in keyfork:
```
53665ca keyfork: the wizard is dead! long live the mnemonic generator!
a1c3d52 keyfork: restructure wizard shard key generation
674e2e9 keyfork: restructure CLI commands to act more like the other commands
723194f keyfork mnemonic generate: userid equivalency, rename provisioner cert_output to output
db19b30 keyfork mnemonic generate: feedback improvements
```
### Changes in keyfork-bug:
Add `keyfork_bug::assert!()` for asserting with Keyfork Bug printing.
```
88a05f2 keyfork-prompt: add choice mechanism, & add to keyfork-shard
```
### Changes in keyfork-prompt:
```
88a05f2 keyfork-prompt: add choice mechanism, & add to keyfork-shard
0243212 keyfork-prompt: clear terminal before leaving alt screen; fixes linux terminal
```
### Changes in keyfork-qrcode:
```
98b9dbb keyfork-qrcode: restructure to prefer libzbar and compile with both enabled
```
### Changes in keyfork-shard:
```
88a05f2 keyfork-prompt: add choice mechanism, & add to keyfork-shard
aa8526c Release keyfork-shard v0.3.1
```
### Changes in keyfork-zbar:
```
98b9dbb keyfork-qrcode: restructure to prefer libzbar and compile with both enabled
```
### Changes in keyforkd:
```
674e2e9 keyfork: restructure CLI commands to act more like the other commands
```
# Keyfork v0.2.6
* The `--daemon` flag has been added for `keyfork recover` subcommands.
* `keyfork mnemonic generate` now has a bunch more options, to improve the out-of-the-box experience.
* `keyfork shard metadata` can be used to get the threshold and OpenPGP certificates.
* `keyfork derive openpgp` now correctly provides private keys, instead of public keys.
### Changes in keyfork:
```
4e342ac keyfork: add `--daemon`
c232828 superpower `keyfork mnemonic generate`
8756c3d keyfork wizard generate-shard-secret: allow exporting certificates and cross-sign generated keys
c95ed0b keyfork shard metadata: initial commit
adb5293 keyfork derive openpgp: export secret keys instead of public certs
```
### Changes in keyfork-derive-openpgp:
```
adb5293 keyfork derive openpgp: export secret keys instead of public certs
```
### Changes in keyfork-prompt:
```
35e0eb5 keyfork-prompt: use raw mode for input
```
### Changes in keyfork-shard:
```
c95ed0b keyfork shard metadata: initial commit
```
### Changes in keyfork-tests:
```
19fbb51 keyfork-tests: initial commit. also, fixup test_util's Panicable to not be generic. it's always unit type
```
### Changes in keyforkd:
```
19fbb51 keyfork-tests: initial commit. also, fixup test_util's Panicable to not be generic. it's always unit type
```
# Keyfork v0.2.5
### Changes in keyfork:
```
503c6fa keyfork derive key: initial commit
c46f9e4 move things to use default handler mechanism
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
```
### Changes in keyfork-crossterm:
```
6317cc9 Cargo.lock: bump deps, dupe generic-array :(
a8b2814 make clippy happy
```
### Changes in keyfork-derive-key:
```
a8b2814 make clippy happy
```
### Changes in keyfork-derive-openpgp:
```
4ab1e8a add docs to make clippy extra happy
a8b2814 make clippy happy
```
### Changes in keyfork-derive-path-data:
```
4ab1e8a add docs to make clippy extra happy
```
### Changes in keyfork-derive-util:
```
a8b2814 make clippy happy
```
### Changes in keyfork-entropy:
```
a8b2814 make clippy happy
```
### Changes in keyfork-mnemonic:
```
a8b2814 make clippy happy
```
### Changes in keyfork-prompt:
```
f8db870 keyfork-prompt: add Headless
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
a8b2814 make clippy happy
```
### Changes in keyfork-qrcode:
```
be6d562 keyfork-qrcode: use image::ImageReader over image::io::Reader (deprecated)
305e070 Cargo.lock: bump multiple deps to deduplicate
4ab1e8a add docs to make clippy extra happy
a8b2814 make clippy happy
```
### Changes in keyfork-shard:
```
c46f9e4 move things to use default handler mechanism
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
d7bf3d1 keyfork-shard: move to blahaj
a8b2814 make clippy happy
c36fe0a keyfork-shard: re-enable standard policy, alive check still disabled, add check for encryption keys when discovering certs
```
### Changes in keyfork-zbar:
```
a8b2814 make clippy happy
```
### Changes in keyforkd:
```
c46f9e4 move things to use default handler mechanism
a8b2814 make clippy happy
```
### Changes in keyforkd-client:
```
a8b2814 make clippy happy
```
# Keyfork v0.2.4
This release includes a lot of "maintenance" changes, without any changes in
end-user functionality.
### Changes in keyfork:
The most significant change in this release is the reorganization of some of
the subcommands, where they would be better as enum-traits, such as `keyfork
derive` and `keyfork wizard`.
```
b254ba7 cleanup post-merge
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
5219c5a keyfork: enum-trait-ify choose-your-own commands
b26f296 keyfork-derive-path-data: move all pathcrafting here
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
```
### Changes in keyfork-derive-openpgp:
```
b254ba7 cleanup post-merge
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
b26f296 keyfork-derive-path-data: move all pathcrafting here
```
### Changes in keyfork-derive-path-data:
This change now centralizes all special Keyfork paths. This means crates should
no longer be required to implement their own path parsing logic.
```
b26f296 keyfork-derive-path-data: move all pathcrafting here
```
### Changes in keyfork-derive-util:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
```
### Changes in keyfork-mnemonic:
`keyfork-mnemonic-util` has finally been renamed to `keyfork-mnemonic`. The
method names `as_bytes() => as_slice()`, `to_bytes() => to_vec()`, and
`into_bytes() => into_vec()`, and the function names
`from_bytes() => try_from_slice()` and
`from_nonstandard_bytes() => from_array()`, have been implemented to more
closely represent the native types they are representing. Additionally,
`Mnemonic::generate_seed()` has been modified to return a constant size array;
this is a breaking change, but should have minimal impact.
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
3ee81b6 keyfork-mnemonic-util: impl as_slice to_vec into_vec
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
```
### Changes in keyfork-prompt:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
```
### Changes in keyfork-shard:
```
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
```
### Changes in keyforkd:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
536e6da keyforkd{,-client}: lots of documentationings
```
### Changes in keyforkd-client:
```
536e6da keyforkd{,-client}: lots of documentationings
```
# Keyfork v0.2.3
This release includes a bugfix for the wizard where the wizard was too strict
about when keys were "alive".
### Changes in keyfork:
```
dd4354f keyfork: bump keyfork-shard
```
### Changes in keyfork-shard:
```
ba64db8 update Cargo.toml and Cargo.lock
fa84a2a keyfork-shard: Be less strict about keys
```
# Keyfork v0.2.2
This release adds a new wizard, intended to be used at DEFCON 32.
### Changes in keyfork:
```
8d40d26 keyfork: add `bottoms-up` wizard
```
### Changes in keyfork-derive-openpgp:
This change also includes a minor change, allowing the derivation path for
`keyfork-derive-openpg` to derive further than two paths, which was useful in
the testing of the wizard.
```
8d40d26 keyfork: add `bottoms-up` wizard
```
# Keyfork v0.2.1
This release contains an emergency bugfix for Keyfork Shard, which previously
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`

1886
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,6 +1,3 @@
.PHONY: default
default: docs/book
BASE_REF ?= main BASE_REF ?= main
HEAD_REF ?= HEAD HEAD_REF ?= HEAD
@ -16,7 +13,6 @@ endef
docs/book: docs/src/links.md $(shell find docs/src -type f -name '*.md') docs/book: docs/src/links.md $(shell find docs/src -type f -name '*.md')
mdbook build docs mdbook build docs
mkdir -p docs/book/rustdoc mkdir -p docs/book/rustdoc
cargo test --doc
cargo doc --no-deps cargo doc --no-deps
cp -r ${CARGO_TARGET_DIR}/doc/* docs/book/rustdoc/ cp -r ${CARGO_TARGET_DIR}/doc/* docs/book/rustdoc/

View File

@ -75,7 +75,7 @@ Note: The following features are proposed, and may not yet be implemented.
* Offline * Offline
* Will exit if network access is detected to force you to keep keys offline * Will exit if network access is detected to force you to keep keys offline
* Helps limit the risk of supply chain attacks * 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 * Private keys are installed to HSMs/TEEs for use by online machines
## Install ## Install
@ -178,8 +178,7 @@ keyfork recover mnemonic
This guide assumes you are sharding to an `N`-of-`M` system with `I` smart 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 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 `$N`, `$M`, and `$I`. The smart cards will be factory reset during the process.
the process.
On an airgapped system, run the following command to generate a file containing On an airgapped system, run the following command to generate a file containing
encrypted shards of a generated seed: encrypted shards of a generated seed:

Binary file not shown.

View File

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

View File

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

View File

@ -1,10 +1,8 @@
//! # The Keyforkd Client //! # The Keyforkd Client
//! //!
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation //! 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 //! requests performed against a server, "Keyforkd" or the "Keyfork Server". The server is operated
//! enforcement of policies, such as requiring at least two leves of a derivation path (for //! on a UNIX socket with messages sent using the Keyfork Frame format.
//! 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 //! 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., //! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
@ -12,58 +10,9 @@
//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but //! 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`. //! `1.x.y` will not be compatible with `2.0.0`.
//! //!
//! The Keyfork Client documentation makes extensive use of the `keyforkd::test_util` module. //! Presently, the Keyfork server only supports the following requests:
//! 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 //! * Derive Key
//!
//! 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 //! ## Extended Private Keys
//! //!
@ -72,101 +21,42 @@
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two //! 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. //! will be derived similarly between the server and the client.
//! //!
//! # Examples
//! ```rust //! ```rust
//! use std::str::FromStr; //! use std::str::FromStr;
//! //!
//! use keyforkd_client::Client; //! use keyforkd_client::Client;
//! use keyfork_derive_util::{DerivationIndex, DerivationPath}; //! use keyfork_derive_util::DerivationPath;
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey; //! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//! // use k256::SecretKey as PrivateKey; //! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey 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"; //! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| { //! # keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'/0'/0")?; //! # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
//! 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 derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap(); //! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap(); //! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! keyforkd::test_util::Panicable::Ok(()) //! # keyforkd::test_util::Infallible::Ok(())
//! # }).unwrap();
//! ```
//!
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used.
//!
//! ```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| {
//! std::env::set_var("KEYFORKD_SOCKET_PATH", 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::Infallible::Ok(())
//! }).unwrap(); //! }).unwrap();
//! ``` //! ```
@ -218,10 +108,6 @@ pub enum Error {
/// An error encountered in Keyforkd. /// An error encountered in Keyforkd.
#[error("Error in Keyforkd: {0}")] #[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError), Keyforkd(#[from] KeyforkdError),
/// An invalid key was returned.
#[error("Invalid key returned")]
InvalidKey,
} }
#[allow(missing_docs)] #[allow(missing_docs)]
@ -275,9 +161,10 @@ impl Client {
/// ///
/// # let seed = b"funky accordion noises"; /// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| { /// # keyforkd::test_util::run_test(seed, |socket_path| {
/// let mut socket = get_socket()?; /// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let mut socket = get_socket().unwrap();
/// let mut client = Client::new(socket); /// let mut client = Client::new(socket);
/// # Ok::<_, keyforkd_client::Error>(()) /// # keyforkd::test_util::Infallible::Ok(())
/// # }).unwrap(); /// # }).unwrap();
/// ``` /// ```
pub fn new(socket: UnixStream) -> Self { pub fn new(socket: UnixStream) -> Self {
@ -296,8 +183,9 @@ impl Client {
/// ///
/// # let seed = b"funky accordion noises"; /// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| { /// # keyforkd::test_util::run_test(seed, |socket_path| {
/// let mut client = Client::discover_socket()?; /// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// # Ok::<_, keyforkd_client::Error>(()) /// let mut client = Client::discover_socket().unwrap();
/// # keyforkd::test_util::Infallible::Ok(())
/// # }).unwrap(); /// # }).unwrap();
/// ``` /// ```
pub fn discover_socket() -> Result<Self> { pub fn discover_socket() -> Result<Self> {
@ -325,10 +213,11 @@ impl Client {
/// ///
/// # let seed = b"funky accordion noises"; /// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| { /// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap(); /// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
/// let mut client = Client::discover_socket().unwrap(); /// let mut client = Client::discover_socket().unwrap();
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap(); /// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
/// # keyforkd::test_util::Panicable::Ok(()) /// # keyforkd::test_util::Infallible::Ok(())
/// # }).unwrap(); /// # }).unwrap();
/// ``` /// ```
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>> pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
@ -345,26 +234,24 @@ impl Client {
} }
let depth = path.len() as u8; let depth = path.len() as u8;
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code) Ok(ExtendedPrivateKey::new_from_parts(
.map_err(|_| Error::InvalidKey) &d.data,
depth,
d.chain_code,
))
} }
_ => Err(Error::InvalidResponse), _ => Err(Error::InvalidResponse),
} }
} }
}
impl Client {
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`]. /// 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 /// # Errors
/// An error may be returned if: /// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error. /// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response. /// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd. /// * An error occurred in Keyforkd.
#[doc(hidden)]
pub fn request(&mut self, req: &Request) -> Result<Response> { pub fn request(&mut self, req: &Request) -> Result<Response> {
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?; try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
let resp = try_decode_from(&mut self.socket)?; let resp = try_decode_from(&mut self.socket)?;

View File

@ -1,7 +1,7 @@
use crate::Client; use crate::Client;
use keyfork_derive_util::{request::*, DerivationPath}; use keyfork_derive_util::{request::*, DerivationPath};
use keyfork_slip10_test_data::test_data; use keyfork_slip10_test_data::test_data;
use keyforkd::test_util::{run_test, Panicable}; use keyforkd::test_util::{run_test, Infallible};
use std::{os::unix::net::UnixStream, str::FromStr}; use std::{os::unix::net::UnixStream, str::FromStr};
#[test] #[test]
@ -11,7 +11,7 @@ fn secp256k1_test_suite() {
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove("secp256k1") .remove(&"secp256k1".to_string())
.unwrap(); .unwrap();
for seed_test in tests { for seed_test in tests {
@ -25,9 +25,6 @@ fn secp256k1_test_suite() {
if chain_len < 2 { if chain_len < 2 {
continue; 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 // 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 // key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len { for i in 2..chain_len {
@ -70,7 +67,7 @@ fn secp256k1_test_suite() {
fn ed25519_test_suite() { fn ed25519_test_suite() {
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove("ed25519").unwrap(); let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
for seed_test in tests { for seed_test in tests {
let seed = seed_test.seed; let seed = seed_test.seed;
@ -109,7 +106,7 @@ fn ed25519_test_suite() {
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap(); DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(&response.data, test.private_key.as_slice()); assert_eq!(&response.data, test.private_key.as_slice());
} }
Panicable::Ok(()) Infallible::Ok(())
}) })
.unwrap(); .unwrap();
} }

View File

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

View File

@ -43,10 +43,6 @@ pub enum DerivationError {
#[error("Invalid derivation length: Expected at least 2, actual: {0}")] #[error("Invalid derivation length: Expected at least 2, actual: {0}")]
InvalidDerivationLength(usize), 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. /// An error occurred while deriving data.
#[error("Derivation error: {0}")] #[error("Derivation error: {0}")]
Derivation(String), Derivation(String),

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ use keyfork_derive_path_data::guess_target;
// use keyfork_derive_util::request::{DerivationError, DerivationRequest, DerivationResponse}; // use keyfork_derive_util::request::{DerivationError, DerivationRequest, DerivationResponse};
use keyforkd_models::{DerivationError, Error, Request, Response}; use keyforkd_models::{DerivationError, Error, Request, Response};
use tower::Service; use tower::Service;
use tracing::{info, warn}; use tracing::info;
// NOTE: All values implemented in Keyforkd must implement Clone with low overhead, either by // 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. // 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 { impl Keyforkd {
/// Create a new instance of Keyfork from a given seed. /// Create a new instance of Keyfork from a given seed.
pub fn new(seed: Vec<u8>) -> Self { pub fn new(seed: Vec<u8>) -> Self {
if seed.len() < 16 {
warn!("Entropy size is lower than 128 bits: {} bits.", seed.len() * 8);
}
Self { Self {
seed: Arc::new(seed), seed: Arc::new(seed),
} }
@ -72,18 +69,6 @@ impl Service<Request> for Keyforkd {
return Err(DerivationError::InvalidDerivationLength(len).into()); 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")] #[cfg(feature = "tracing")]
if let Some(target) = guess_target(req.path()) { if let Some(target) = guess_target(req.path()) {
info!("Deriving path: {target}"); info!("Deriving path: {target}");
@ -113,7 +98,7 @@ mod tests {
async fn properly_derives_secp256k1() { async fn properly_derives_secp256k1() {
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove("secp256k1") .remove(&"secp256k1".to_string())
.unwrap(); .unwrap();
for per_seed in tests { for per_seed in tests {
@ -125,9 +110,6 @@ mod tests {
if chain.len() < 2 { if chain.len() < 2 {
continue; continue;
} }
if chain.iter().take(2).any(|index| !index.is_hardened()) {
continue;
}
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain); let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response: DerivationResponse = keyforkd let response: DerivationResponse = keyforkd
.ready() .ready()
@ -146,7 +128,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn properly_derives_ed25519() { async fn properly_derives_ed25519() {
let tests = test_data().unwrap().remove("ed25519").unwrap(); let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
for per_seed in tests { for per_seed in tests {
let seed = &per_seed.seed; let seed = &per_seed.seed;

View File

@ -7,26 +7,23 @@ use crate::{middleware, Keyforkd, ServiceBuilder, UnixServer};
use tokio::runtime::Builder; use tokio::runtime::Builder;
use keyfork_bug::bug;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[error("This error can never be instantiated")] #[error("This error can never be instantiated")]
#[doc(hidden)] #[doc(hidden)]
pub enum UninstantiableError {} pub struct InfallibleError {
protected: (),
}
/// A panicable result. This type can be used when a closure chooses to panic instead of /// An infallible result. This type can be used to represent a function that should never error.
/// 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 /// ```rust
/// use keyforkd::test_util::Panicable; /// use keyforkd::test_util::Infallible;
/// let closure = || { /// let closure = || {
/// Panicable::Ok(()) /// Infallible::Ok(())
/// }; /// };
/// assert!(closure().is_ok()); /// assert!(closure().is_ok());
/// ``` /// ```
pub type Panicable = std::result::Result<(), UninstantiableError>; pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a /// 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 /// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
@ -40,8 +37,6 @@ pub type Panicable = std::result::Result<(), UninstantiableError>;
/// runtime. /// runtime.
/// ///
/// # Examples /// # Examples
/// The test utility provides a socket that can be connected to for deriving keys.
///
/// ```rust /// ```rust
/// use std::os::unix::net::UnixStream; /// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
@ -49,67 +44,44 @@ pub type Panicable = std::result::Result<(), UninstantiableError>;
/// UnixStream::connect(&path).map(|_| ()) /// UnixStream::connect(&path).map(|_| ())
/// }).unwrap(); /// }).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)] #[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> std::result::Result<(), E> pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where where
F: FnOnce(&std::path::Path) -> std::result::Result<(), E> + Send + 'static, F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
E: Send + 'static, E: Send + 'static,
{ {
let rt = Builder::new_multi_thread() let rt = Builder::new_multi_thread()
.worker_threads(2) .worker_threads(2)
.enable_io() .enable_io()
.build() .build()
.expect(bug!( .expect("tokio threaded IO runtime");
"can't make tokio threaded IO runtime, should be enabled via feature flags" let socket_dir = tempfile::tempdir().expect("can't create tempdir");
));
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
let socket_path = socket_dir.path().join("keyforkd.sock"); let socket_path = socket_dir.path().join("keyforkd.sock");
let result = rt.block_on(async move { rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(1); let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let server_handle = tokio::spawn({ let server_handle = tokio::spawn({
let socket_path = socket_path.clone(); let socket_path = socket_path.clone();
let seed = seed.to_vec(); let seed = seed.to_vec();
async move { async move {
let mut server = let mut server = UnixServer::bind(&socket_path).expect("can't bind unix socket");
UnixServer::bind(&socket_path).expect(bug!("can't bind unix socket")); tx.send(()).await.expect("couldn't send server start signal");
tx.send(())
.await
.expect(bug!("couldn't send server start signal"));
let service = ServiceBuilder::new() let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new()) .layer(middleware::BincodeLayer::new())
.service(Keyforkd::new(seed.to_vec())); .service(Keyforkd::new(seed.to_vec()));
server.run(service).await.expect(bug!("Unable to start service")); server.run(service).await.unwrap();
} }
}); });
rx.recv() rx.recv()
.await .await
.expect(bug!("can't receive server start signal from channel")); .expect("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 test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
let result = test_handle.await; let result = test_handle.await;
server_handle.abort(); server_handle.abort();
result result
}); })
if let Err(e) = result { .expect("runtime could not join all threads")
if let Ok(reason) = e.try_into_panic() {
std::panic::resume_unwind(reason);
}
}
Ok(())
} }
#[cfg(test)] #[cfg(test)]
@ -119,6 +91,6 @@ mod tests {
#[test] #[test]
fn test_run_test() { fn test_run_test() {
let seed = b"beefbeef"; let seed = b"beefbeef";
run_test(seed, |_path| Panicable::Ok(())).expect("infallible"); run_test(seed, |_path| Infallible::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] [package]
name = "keyfork-derive-key" name = "keyfork-derive-key"
version = "0.1.2" version = "0.1.0"
edition = "2021" edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
keyfork-derive-util = { workspace = true } keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" }
keyforkd-client = { workspace = true } keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client" }
smex = { workspace = true } smex = { version = "0.1.0", path = "../../util/smex" }
thiserror = { workspace = true } thiserror = "1.0.48"

View File

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

View File

@ -1,19 +1,18 @@
[package] [package]
name = "keyfork-derive-openpgp" name = "keyfork-derive-openpgp"
version = "0.1.5" version = "0.1.0"
edition = "2021" edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["bin"] default = []
bin = ["sequoia-openpgp/crypto-nettle"] bin = ["sequoia-openpgp/crypto-nettle"]
[dependencies] [dependencies]
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] } keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] }
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] } keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", default-features = false, features = ["ed25519"] }
ed25519-dalek = { workspace = true } ed25519-dalek = "2.0.0"
sequoia-openpgp = { workspace = true } sequoia-openpgp = { version = "1.17.0", default-features = false }
anyhow = { workspace = true } anyhow = "1.0.75"
thiserror = { workspace = true } thiserror = "1.0.49"
keyfork-derive-path-data = { workspace = true }

View File

@ -19,14 +19,8 @@ use sequoia_openpgp::{
Cert, Packet, Cert, Packet,
}; };
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
// something like Prv or PrvKey.
/// The private key type used with OpenPGP.
pub type XPrvKey = SigningKey; pub type XPrvKey = SigningKey;
pub type XPrv = ExtendedPrivateKey<SigningKey>;
/// The extended private key type used with OpenPGP.
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
/// An error occurred while creating an OpenPGP key. /// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -65,17 +59,13 @@ pub enum Error {
#[allow(missing_docs)] #[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Create an OpenPGP Cert with private key data, with derived keys from the given derivation /// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
/// response, keys, and User ID. /// 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).
/// ///
/// # Errors /// # Errors
/// The function may error for any condition mentioned in [`Error`]. /// The function may error for any condition mentioned in [`Error`].
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> { pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.first() { let primary_key_flags = match keys.get(0) {
Some(kf) if kf.for_certification() => kf, Some(kf) if kf.for_certification() => kf,
_ => return Err(Error::NotCert), _ => return Err(Error::NotCert),
}; };
@ -119,7 +109,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?; let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
let policy = sequoia_openpgp::policy::StandardPolicy::new(); 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 mut keypair = primary_key.clone().into_keypair()?;
let signatures = let signatures =
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?; cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
use crate::{DerivationIndex, DerivationPath, ExtendedPublicKey, PrivateKey, PublicKey}; use crate::{DerivationIndex, DerivationPath, ExtendedPublicKey, PrivateKey, PublicKey};
use keyfork_bug::bug;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::Sha512; use sha2::Sha512;
@ -27,10 +25,6 @@ pub enum Error {
/// The given slice was of an inappropriate size to create a Private Key. /// 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")] #[error("The given slice was of an inappropriate size to create a Private Key")]
InvalidSliceError(#[from] std::array::TryFromSliceError), 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 Result<T, E = Error> = std::result::Result<T, E>;
@ -128,10 +122,10 @@ mod serde_with {
K: PrivateKey + Clone, K: PrivateKey + Clone,
{ {
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?; let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
let bytes: [u8; 32] = variable_len_bytes.try_into().expect(bug!( let bytes: [u8; 32] = variable_len_bytes
"unable to parse serialized private key; no support for static len" .try_into()
)); .expect("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"))) Ok(K::from_bytes(&bytes))
} }
} }
@ -152,9 +146,13 @@ where
/// Generate a new [`ExtendedPrivateKey`] from a seed, ideally from a 12-word or 24-word /// Generate a new [`ExtendedPrivateKey`] from a seed, ideally from a 12-word or 24-word
/// mnemonic, but may take 16-byte seeds. /// mnemonic, but may take 16-byte seeds.
/// ///
/// # Panics
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
///
/// # Errors /// # Errors
/// The function may return an error if the derived master key could not be parsed as a key for /// An error may be returned if:
/// the given algorithm (such as exceeding values on the secp256k1 curve). /// * The given seed had an incorrect length.
/// * A `HmacSha512` can't be constructed.
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
@ -167,39 +165,25 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed); /// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// ``` /// ```
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Result<Self> { pub fn new(seed: impl as_private_key::AsPrivateKey) -> Self {
Self::new_internal(seed.as_private_key()) Self::new_internal(seed.as_private_key())
} }
fn new_internal(seed: &[u8]) -> Result<Self> { fn new_internal(seed: &[u8]) -> Self {
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>()) let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
.expect(bug!("HmacSha512 InvalidLength should be infallible")) .expect("HmacSha512 InvalidLength should be infallible")
.chain_update(seed) .chain_update(seed)
.finalize() .finalize()
.into_bytes(); .into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8); let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
// Verify the master key is nonzero, hopefully avoiding side-channel attacks. Self::new_from_parts(
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 private_key
.try_into() .try_into()
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")), .expect("KEY_SIZE / 8 did not give a 32 byte slice"),
0, 0,
// Checked: chain_code is always the same length, hash is static size // Checked: chain_code is always the same length, hash is static size
chain_code chain_code.try_into().expect("Invalid chain code length"),
.try_into()
.expect(bug!("Invalid chain code length")),
) )
} }
@ -219,18 +203,13 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code); /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// ``` /// ```
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Result<Self> { pub fn new_from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self {
match K::from_bytes(key) { Self {
Ok(key) => { private_key: K::from_bytes(key),
Ok(Self {
private_key: key,
depth, depth,
chain_code, chain_code,
})
}
Err(_) => Err(Error::InvalidKey),
} }
} }
@ -244,15 +223,12 @@ where
/// # public_key::TestPublicKey as PublicKey, /// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = // /// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key)?); /// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key));
/// # Ok(())
/// # }
/// ``` /// ```
pub fn private_key(&self) -> &K { pub fn private_key(&self) -> &K {
&self.private_key &self.private_key
@ -277,14 +253,14 @@ where
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27, /// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251 /// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
/// # ]; /// # ];
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let xpub = xprv.extended_public_key(); /// let xpub = xprv.extended_public_key();
/// assert_eq!(known_key, xpub.public_key().to_bytes()); /// assert_eq!(known_key, xpub.public_key().to_bytes());
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> { pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
ExtendedPublicKey::from_parts(self.public_key(), self.depth, self.chain_code) ExtendedPublicKey::new_from_parts(self.public_key(), self.depth, self.chain_code)
} }
/// Return a public key for the current [`PrivateKey`]. /// Return a public key for the current [`PrivateKey`].
@ -301,7 +277,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let pubkey = xprv.public_key(); /// let pubkey = xprv.public_key();
/// # Ok(()) /// # Ok(())
/// # } /// # }
@ -319,15 +295,12 @@ where
/// # public_key::TestPublicKey as PublicKey, /// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = // /// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.depth(), 4); /// assert_eq!(xprv.depth(), 4);
/// # Ok(())
/// # }
/// ``` /// ```
pub fn depth(&self) -> u8 { pub fn depth(&self) -> u8 {
self.depth self.depth
@ -342,15 +315,12 @@ where
/// # public_key::TestPublicKey as PublicKey, /// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = // /// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(chain_code, &xprv.chain_code()); /// assert_eq!(chain_code, &xprv.chain_code());
/// # Ok(())
/// # }
/// ``` /// ```
pub fn chain_code(&self) -> [u8; 32] { pub fn chain_code(&self) -> [u8; 32] {
self.chain_code self.chain_code
@ -372,7 +342,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?; /// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let path = DerivationPath::default() /// let path = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?) /// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?) /// .chain_push(DerivationIndex::new(0, true)?)
@ -418,7 +388,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?; /// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let bip44_wallet = DerivationPath::default() /// let bip44_wallet = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?) /// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?) /// .chain_push(DerivationIndex::new(0, true)?)
@ -435,7 +405,7 @@ where
let depth = self.depth.checked_add(1).ok_or(Error::Depth)?; let depth = self.depth.checked_add(1).ok_or(Error::Depth)?;
let mut hmac = HmacSha512::new_from_slice(&self.chain_code) let mut hmac = HmacSha512::new_from_slice(&self.chain_code)
.expect(bug!("HmacSha512 InvalidLength should be infallible")); .expect("HmacSha512 InvalidLength should be infallible");
if index.is_hardened() { if index.is_hardened() {
hmac.update(&[0]); hmac.update(&[0]);
hmac.update(&self.private_key.to_bytes()); hmac.update(&self.private_key.to_bytes());
@ -453,7 +423,7 @@ where
.derive_child( .derive_child(
&private_key &private_key
.try_into() .try_into()
.expect(bug!("Invalid length for private key")), .expect("Invalid length for private key"),
) )
.map_err(|_| Error::Derivation)?; .map_err(|_| Error::Derivation)?;
@ -462,7 +432,7 @@ where
depth, depth,
chain_code: chain_code chain_code: chain_code
.try_into() .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 sha2::Sha512;
use thiserror::Error; use thiserror::Error;
use keyfork_bug::bug;
const KEY_SIZE: usize = 256; const KEY_SIZE: usize = 256;
/// Errors associated with creating or deriving Extended Public Keys. /// Errors associated with creating or deriving Extended Public Keys.
#[derive(Error, Clone, Debug)] #[derive(Error, Clone, Debug)]
pub enum Error { pub enum Error {
/// BIP-0032 does not support hardened public key derivation from parent public keys. /// BIP-0032 does not support deriving public keys from hardened private keys.
#[error("Hardened child public keys may not be derived from parent public keys")] #[error("Public keys may not be derived when hardened")]
HardenedIndex, HardenedIndex,
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255. /// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
@ -60,11 +58,11 @@ where
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let pubkey = PublicKey::from_bytes(key); /// let pubkey = PublicKey::from_bytes(key);
/// let xpub = ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code); /// let xpub = ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self { pub fn new_from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
Self { Self {
public_key, public_key,
depth, depth,
@ -86,7 +84,7 @@ where
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key); /// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = // /// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code); /// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// let pubkey = xpub.public_key(); /// let pubkey = xpub.public_key();
/// # Ok(()) /// # Ok(())
/// # } /// # }
@ -121,7 +119,7 @@ where
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key); /// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = // /// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code); /// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// let index = DerivationIndex::new(0, false)?; /// let index = DerivationIndex::new(0, false)?;
/// let child = xpub.derive_child(&index)?; /// let child = xpub.derive_child(&index)?;
/// # Ok(()) /// # Ok(())
@ -144,11 +142,9 @@ where
let (child_key, chain_code) = hmac.split_at(KEY_SIZE / 8); let (child_key, chain_code) = hmac.split_at(KEY_SIZE / 8);
let derived_key = self let derived_key = self
.public_key .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)?; .map_err(|_| Error::Derivation)?;
let chain_code = chain_code let chain_code = chain_code.try_into().expect("Invalid chain code length");
.try_into()
.expect(bug!("Invalid chain code length"));
Ok(Self { Ok(Self {
public_key: derived_key, public_key: derived_key,

View File

@ -24,7 +24,7 @@ pub trait PrivateKey: Sized {
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data); /// 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. /// Convert a &Self to bytes.
/// ///
@ -36,7 +36,7 @@ pub trait PrivateKey: Sized {
/// # }; /// # };
/// let key_data: &[u8; 32] = // /// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap(); /// let private_key = OurPrivateKey::from_bytes(key_data);
/// assert_eq!(key_data, &private_key.to_bytes()); /// assert_eq!(key_data, &private_key.to_bytes());
/// ``` /// ```
fn to_bytes(&self) -> PrivateKeyBytes; fn to_bytes(&self) -> PrivateKeyBytes;
@ -71,7 +71,7 @@ pub trait PrivateKey: Sized {
/// # }; /// # };
/// let key_data: &[u8; 32] = // /// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap(); /// let private_key = OurPrivateKey::from_bytes(key_data);
/// let public_key = private_key.public_key(); /// let public_key = private_key.public_key();
/// ``` /// ```
fn public_key(&self) -> Self::PublicKey; fn public_key(&self) -> Self::PublicKey;
@ -83,7 +83,7 @@ pub trait PrivateKey: Sized {
/// # Errors /// # Errors
/// ///
/// An error may be returned if: /// 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. /// * An error specific to the given algorithm was encountered.
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>; fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
@ -100,10 +100,6 @@ pub enum PrivateKeyError {
/// For the given algorithm, the private key must be nonzero. /// For the given algorithm, the private key must be nonzero.
#[error("The provided private key must be nonzero, but is not")] #[error("The provided private key must be nonzero, but is not")]
NonZero, 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")] #[cfg(feature = "secp256k1")]
@ -118,8 +114,8 @@ impl PrivateKey for k256::SecretKey {
"Bitcoin seed" "Bitcoin seed"
} }
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> { fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self::from_slice(b).map_err(|_| PrivateKeyError::InvalidScalar) Self::from_slice(b).expect("Invalid private key bytes")
} }
fn to_bytes(&self) -> PrivateKeyBytes { fn to_bytes(&self) -> PrivateKeyBytes {
@ -132,19 +128,20 @@ impl PrivateKey for k256::SecretKey {
} }
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> { fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
use k256::elliptic_curve::ScalarPrimitive; if other.iter().all(|n| n == &0) {
use k256::{Scalar, Secp256k1}; return Err(PrivateKeyError::NonZero);
}
// Construct a scalar from bytes let other = *other;
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(other.into()); // Checked: See above nonzero check
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar); let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
let scalar = scalar.ok_or(PrivateKeyError::InvalidScalar)?; .expect("Should have been able to get a NonZeroScalar");
let scalar = Scalar::from(scalar);
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref(); let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
let nonzero_scalar = Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar)) Ok(
.ok_or(PrivateKeyError::NonZero)?; Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
Ok(Self::from(nonzero_scalar)) .map(Into::into)
.expect("Should be able to make Key"),
)
} }
} }
@ -157,8 +154,8 @@ impl PrivateKey for ed25519_dalek::SigningKey {
"ed25519 seed" "ed25519 seed"
} }
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> { fn from_bytes(b: &PrivateKeyBytes) -> Self {
Ok(Self::from_bytes(b)) Self::from_bytes(b)
} }
fn to_bytes(&self) -> PrivateKeyBytes { fn to_bytes(&self) -> PrivateKeyBytes {
@ -181,8 +178,7 @@ impl PrivateKey for ed25519_dalek::SigningKey {
use crate::public_key::TestPublicKey; use crate::public_key::TestPublicKey;
/// A private key that can be used for testing purposes. Does not utilize any significant #[doc(hidden)]
/// cryptographic operations.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct TestPrivateKey { pub struct TestPrivateKey {
key: [u8; 32], key: [u8; 32],
@ -202,8 +198,10 @@ impl PrivateKey for TestPrivateKey {
type PublicKey = TestPublicKey; type PublicKey = TestPublicKey;
type Err = PrivateKeyError; type Err = PrivateKeyError;
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> { fn from_bytes(b: &PrivateKeyBytes) -> Self {
Ok(Self { key: *b }) Self {
key: *b
}
} }
fn to_bytes(&self) -> PrivateKeyBytes { fn to_bytes(&self) -> PrivateKeyBytes {

View File

@ -5,8 +5,6 @@ use ripemd::Ripemd160;
use sha2::Sha256; use sha2::Sha256;
use thiserror::Error; use thiserror::Error;
use keyfork_bug::bug;
pub(crate) type PublicKeyBytes = [u8; 33]; pub(crate) type PublicKeyBytes = [u8; 33];
/// Functions required to use an `ExtendedPublicKey`. /// Functions required to use an `ExtendedPublicKey`.
@ -30,7 +28,7 @@ pub trait PublicKey: Sized {
/// # }; /// # };
/// let key_data: &[u8; 32] = // /// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap(); /// let private_key = OurPrivateKey::from_bytes(key_data);
/// let public_key_bytes = private_key.public_key().to_bytes(); /// let public_key_bytes = private_key.public_key().to_bytes();
/// ``` /// ```
fn to_bytes(&self) -> PublicKeyBytes; fn to_bytes(&self) -> PublicKeyBytes;
@ -42,7 +40,7 @@ pub trait PublicKey: Sized {
/// # Errors /// # Errors
/// ///
/// An error may be returned if: /// 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. /// * An error specific to the given algorithm was encountered.
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>; fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
@ -56,7 +54,7 @@ pub trait PublicKey: Sized {
/// # }; /// # };
/// let key_data: &[u8; 32] = // /// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap(); /// let private_key = OurPrivateKey::from_bytes(key_data);
/// let fingerprint = private_key.public_key().fingerprint(); /// let fingerprint = private_key.public_key().fingerprint();
/// ``` /// ```
fn fingerprint(&self) -> [u8; 4] { fn fingerprint(&self) -> [u8; 4] {
@ -65,7 +63,7 @@ pub trait PublicKey: Sized {
// Note: Safety assured by type returned from Ripemd160 // Note: Safety assured by type returned from Ripemd160
hash[..4] hash[..4]
.try_into() .try_into()
.expect(bug!("Ripemd160 returned too little data")) .expect("Ripemd160 returned too little data")
} }
} }
@ -77,10 +75,6 @@ pub enum PublicKeyError {
#[error("The provided public key must be nonzero, but is not")] #[error("The provided public key must be nonzero, but is not")]
NonZero, 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. /// Public key derivation is unsupported for this algorithm.
#[error("Public key derivation is unsupported for this algorithm")] #[error("Public key derivation is unsupported for this algorithm")]
DerivationUnsupported, DerivationUnsupported,
@ -89,7 +83,7 @@ pub enum PublicKeyError {
#[cfg(feature = "secp256k1")] #[cfg(feature = "secp256k1")]
use k256::{ use k256::{
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint}, elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
AffinePoint, AffinePoint, NonZeroScalar,
}; };
#[cfg(feature = "secp256k1")] #[cfg(feature = "secp256k1")]
@ -109,18 +103,15 @@ impl PublicKey for k256::PublicKey {
} }
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> { fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
use k256::elliptic_curve::ScalarPrimitive; if other.iter().all(|n| n == &0) {
use k256::{Secp256k1, Scalar}; 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 point = self.to_projective() + (AffinePoint::generator() * *scalar);
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into()); Ok(Self::from_affine(point.into()).expect("Could not from_affine after scalar arithmetic"))
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")))
} }
} }
@ -148,23 +139,21 @@ impl PublicKey for VerifyingKey {
} }
} }
/// A public key that can be used for testing purposes. Does not utilize any significant #[doc(hidden)]
/// cryptographic operations.
#[derive(Clone)] #[derive(Clone)]
pub struct TestPublicKey { pub struct TestPublicKey {
pub(crate) key: [u8; 33], pub(crate) key: [u8; 33],
} }
impl TestPublicKey { impl TestPublicKey {
/// Create a new TestPublicKey from the given bytes. #[doc(hidden)]
#[allow(dead_code)] #[allow(dead_code)]
pub fn from_bytes(b: &[u8]) -> Self { pub fn from_bytes(b: &[u8]) -> Self {
Self { Self {
key: b key: b.try_into().unwrap(),
.try_into()
.expect(bug!("invalid size when constructing TestPublicKey")),
} }
} }
} }
impl PublicKey for TestPublicKey { impl PublicKey for TestPublicKey {

View File

@ -24,7 +24,7 @@ use crate::{
DerivationPath, ExtendedPrivateKey, DerivationPath, ExtendedPrivateKey,
}; };
use keyfork_mnemonic::{Mnemonic, MnemonicGenerationError}; use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// An error encountered while deriving a key. /// An error encountered while deriving a key.
@ -57,7 +57,7 @@ pub enum DerivationAlgorithm {
#[allow(missing_docs)] #[allow(missing_docs)]
Secp256k1, Secp256k1,
#[doc(hidden)] #[doc(hidden)]
TestAlgorithm, Internal,
} }
impl DerivationAlgorithm { impl DerivationAlgorithm {
@ -70,7 +70,7 @@ impl DerivationAlgorithm {
match self { match self {
#[cfg(feature = "ed25519")] #[cfg(feature = "ed25519")]
Self::Ed25519 => { Self::Ed25519 => {
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed)?; let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed);
let derived_key = key.derive_path(path)?; let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv( Ok(DerivationResponse::with_algo_and_xprv(
self.clone(), self.clone(),
@ -79,15 +79,15 @@ impl DerivationAlgorithm {
} }
#[cfg(feature = "secp256k1")] #[cfg(feature = "secp256k1")]
Self::Secp256k1 => { Self::Secp256k1 => {
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed)?; let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed);
let derived_key = key.derive_path(path)?; let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv( Ok(DerivationResponse::with_algo_and_xprv(
self.clone(), self.clone(),
&derived_key, &derived_key,
)) ))
} }
Self::TestAlgorithm => { Self::Internal => {
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed)?; let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed);
let derived_key = key.derive_path(path)?; let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv( Ok(DerivationResponse::with_algo_and_xprv(
self.clone(), self.clone(),
@ -120,7 +120,7 @@ pub trait AsAlgorithm: PrivateKey {
impl AsAlgorithm for TestPrivateKey { impl AsAlgorithm for TestPrivateKey {
fn as_algorithm() -> DerivationAlgorithm { fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::TestAlgorithm DerivationAlgorithm::Internal
} }
} }
@ -144,7 +144,7 @@ impl DerivationRequest {
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm; /// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
@ -169,7 +169,7 @@ impl DerivationRequest {
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm; /// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
@ -194,12 +194,13 @@ impl DerivationRequest {
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mnemonic: keyfork_mnemonic::Mnemonic = // /// let mnemonic: keyfork_mnemonic_util::Mnemonic = //
/// # keyfork_mnemonic::Mnemonic::from_entropy( /// # keyfork_mnemonic_util::Mnemonic::from_entropy(
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
/// # Default::default(),
/// # )?; /// # )?;
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm; /// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
@ -228,7 +229,7 @@ impl DerivationRequest {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm; /// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
@ -300,9 +301,11 @@ mod secp256k1 {
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> { fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm { match value.algorithm {
DerivationAlgorithm::Secp256k1 => { DerivationAlgorithm::Secp256k1 => Ok(Self::new_from_parts(
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into) &value.data,
} value.depth,
value.chain_code,
)),
_ => Err(Self::Error::Algorithm), _ => Err(Self::Error::Algorithm),
} }
} }
@ -333,9 +336,11 @@ mod ed25519 {
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> { fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm { match value.algorithm {
DerivationAlgorithm::Ed25519 => { DerivationAlgorithm::Ed25519 => Ok(Self::new_from_parts(
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into) &value.data,
} value.depth,
value.chain_code,
)),
_ => Err(Self::Error::Algorithm), _ => Err(Self::Error::Algorithm),
} }
} }

View File

@ -15,7 +15,7 @@ fn secp256k1() {
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove("secp256k1") .remove(&"secp256k1".to_string())
.unwrap(); .unwrap();
for per_seed in tests { for per_seed in tests {
@ -31,7 +31,7 @@ fn secp256k1() {
// Tests for ExtendedPrivateKey // Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(seed); let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed).unwrap(); let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap(); let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!( assert_eq!(
derived_key.chain_code().as_slice(), derived_key.chain_code().as_slice(),
@ -62,7 +62,7 @@ fn secp256k1() {
fn ed25519() { fn ed25519() {
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove("ed25519").unwrap(); let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
for per_seed in tests { for per_seed in tests {
let seed = &per_seed.seed; let seed = &per_seed.seed;
@ -77,7 +77,7 @@ fn ed25519() {
// Tests for ExtendedPrivateKey // Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(seed); let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed).unwrap(); let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap(); let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!( assert_eq!(
derived_key.chain_code().as_slice(), derived_key.chain_code().as_slice(),
@ -110,7 +110,7 @@ fn panics_with_unhardened_derivation() {
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
let seed = hex!("000102030405060708090a0b0c0d0e0f"); let seed = hex!("000102030405060708090a0b0c0d0e0f");
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap(); let xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
xkey.derive_path(&DerivationPath::from_str("m/0").unwrap()) xkey.derive_path(&DerivationPath::from_str("m/0").unwrap())
.unwrap(); .unwrap();
} }
@ -122,7 +122,7 @@ fn panics_at_depth() {
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
let seed = hex!("000102030405060708090a0b0c0d0e0f"); let seed = hex!("000102030405060708090a0b0c0d0e0f");
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap(); let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
for i in 0..=u32::from(u8::MAX) { for i in 0..=u32::from(u8::MAX) {
xkey = xkey xkey = xkey
.derive_child(&DerivationIndex::new(i, true).unwrap()) .derive_child(&DerivationIndex::new(i, true).unwrap())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,63 +1,26 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![allow(clippy::expect_fun_call)]
use std::{ use std::{
io::{Read, Write}, io::{stdin, stdout, Read, Write},
rc::Rc, sync::{Arc, Mutex},
str::FromStr,
sync::{LazyLock, Mutex},
}; };
use aes_gcm::{ use aes_gcm::{
aead::{consts::U12, Aead}, aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce, Aes256Gcm, KeyInit, Nonce,
}; };
use base64::prelude::{Engine, BASE64_STANDARD};
use blahaj::{Share, Sharks};
use hkdf::Hkdf; use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX}; use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{ use keyfork_prompt::{
prompt_validated_wordlist, validators::{mnemonic::MnemonicSetValidator, Validator},
validators::{ Message as PromptMessage, PromptHandler, Terminal,
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator,
},
Message as PromptMessage, PromptHandler,
}; };
use sha2::Sha256; use sha2::Sha256;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
const PLAINTEXT_LENGTH: u8 = 32 // shard // 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
+ 1 // index const ENC_LEN: u8 = 4 * 16;
+ 1 // threshold
+ 1 // version
+ 1; // length;
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
#[derive(PartialEq, Eq, Clone, Copy)]
enum RetryScanMnemonic {
Retry,
Continue,
}
impl keyfork_prompt::Choice for RetryScanMnemonic {
fn identifier(&self) -> Option<char> {
Some(match self {
RetryScanMnemonic::Retry => 'r',
RetryScanMnemonic::Continue => 'c',
})
}
}
impl std::fmt::Display for RetryScanMnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RetryScanMnemonic::Retry => write!(f, "Retry scanning mnemonic."),
RetryScanMnemonic::Continue => write!(f, "Continue to manual mnemonic entry."),
}
}
}
#[cfg(feature = "openpgp")] #[cfg(feature = "openpgp")]
pub mod openpgp; pub mod openpgp;
@ -162,10 +125,10 @@ pub trait Format {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData], encrypted_messages: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>, prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Vec<Share>, u8), Self::Error>; ) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a readable input. For the current /// Decrypt a single share and associated metadata from a reaable input. For the current
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to /// version of Keyfork, the only associated metadata is a u8 representing the threshold to
/// combine secrets. /// combine secrets.
/// ///
@ -176,43 +139,9 @@ pub trait Format {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>, prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Share, u8), Self::Error>; ) -> Result<(Share, u8), Self::Error>;
/// Decrypt the public keys and metadata from encrypted data.
///
/// # Errors
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
/// could neither be encrypted nor parsed.
fn decrypt_metadata(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error>;
/// Decrypt the public keys and metadata from a Shardfile.
///
/// # Errors
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
/// could neither be encrypted nor parsed.
fn decrypt_metadata_from_file(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>,
) -> Result<(u8, Vec<Self::PublicKey>), Self::Error> {
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
self.decrypt_metadata(
private_keys,
&encrypted_messages,
Rc::new(Mutex::new(prompt)),
)
}
/// Decrypt multiple shares and combine them to recreate a secret. /// Decrypt multiple shares and combine them to recreate a secret.
/// ///
/// # Errors /// # Errors
@ -222,7 +151,7 @@ pub trait Format {
&self, &self,
private_key_discovery: Option<impl KeyDiscovery<Self>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>, prompt: impl PromptHandler,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> { ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_discovery let private_keys = private_key_discovery
.map(|p| p.discover_private_keys()) .map(|p| p.discover_private_keys())
@ -231,7 +160,7 @@ pub trait Format {
let (shares, threshold) = self.decrypt_all_shards( let (shares, threshold) = self.decrypt_all_shards(
private_keys, private_keys,
&encrypted_messages, &encrypted_messages,
Rc::new(Mutex::new(prompt)), Arc::new(Mutex::new(prompt)),
)?; )?;
let secret = Sharks(threshold) let secret = Sharks(threshold)
@ -252,9 +181,9 @@ pub trait Format {
&self, &self,
private_key_discovery: Option<impl KeyDiscovery<Self>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
prompt: Box<dyn PromptHandler>, prompt: impl PromptHandler,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let prompt = Rc::new(Mutex::new(prompt)); let prompt = Arc::new(Mutex::new(prompt));
// parse input // parse input
let private_keys = private_key_discovery let private_keys = private_key_discovery
@ -263,6 +192,7 @@ pub trait Format {
let encrypted_messages = self.parse_shard_file(reader)?; let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH // establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None; let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera // receive remote data via scanning QR code from camera
@ -270,82 +200,60 @@ pub trait Format {
{ {
prompt prompt
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .unwrap()
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?; .prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
loop { if let Ok(Some(hex)) =
if let Ok(Some(qrcode_content)) = keyfork_qrcode::scan_camera( keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
std::time::Duration::from_secs(*QRCODE_TIMEOUT), {
0, let decoded_data = smex::decode(&hex)?;
) { nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
let decoded_data = BASE64_STANDARD pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?);
break;
} else { } else {
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX)); prompt
let choice = keyfork_prompt::prompt_choice( .lock()
&mut **prompt, .unwrap()
"A QR code could not be scanned. Retry or continue?", .prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
)?;
if choice == RetryScanMnemonic::Continue {
break;
}
}; };
} }
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics // if QR code scanning failed or was unavailable, read from a set of mnemonics
let their_pubkey = match pubkey_data { let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
Some(pubkey) => pubkey, (Some(nonce), Some(pubkey)) => (nonce, pubkey),
None => { _ => {
let validator = MnemonicValidator { let validator = MnemonicSetValidator {
word_length: Some(WordLength::Count(24)), word_lengths: [9, 24],
}; };
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX)); let [nonce_mnemonic, pubkey_mnemonic] = prompt
prompt_validated_wordlist::<English, _>(
&mut **prompt,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
/*
prompt
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .unwrap()
.prompt_validated_wordlist::<English, _>( .prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ, QRCODE_COULDNT_READ,
3, 3,
validator.to_fn(), validator.to_fn(),
)? )?;
let nonce = nonce_mnemonic
.as_bytes() .as_bytes()
.try_into() .try_into()
.map_err(|_| InvalidData)? .map_err(|_| InvalidData)?;
*/ let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
(nonce, pubkey)
} }
}; };
// create our shared key // create our shared key
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?; let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey)); let shared_secret = our_key
assert!( .diffie_hellman(&PublicKey::from(their_pubkey))
shared_secret.was_contributory(), .to_bytes();
bug!("shared secret might be insecure") let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
); let mut hkdf_output = [0u8; 256 / 8];
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes()); hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
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 // decrypt a single shard and create the payload
let (share, threshold) = let (share, threshold) =
@ -354,51 +262,50 @@ pub trait Format {
payload.insert(0, HUNK_VERSION); payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold); payload.insert(1, threshold);
assert!( assert!(
payload.len() < PLAINTEXT_LENGTH as usize, payload.len() <= ENC_LEN as usize,
"invalid share length (too long, must be less than {PLAINTEXT_LENGTH} bytes)" "invalid share length (too long, max {ENC_LEN} 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 // encrypt data
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?; let nonce = Nonce::<U12>::from_slice(&nonce);
assert_eq!( let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
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); // convert data to a static-size payload
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
#[allow(clippy::assertions_on_constants)]
{
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
}
#[allow(clippy::cast_possible_truncation)]
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
assert!(
payload_bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit"
);
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
// difficulty when entering in prompts manually, as one's place could be lost due to
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
// but not including the last byte.
#[allow(clippy::cast_possible_truncation)]
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
.iter_mut()
.enumerate()
{
*byte = (i % u8::MAX as usize) as u8;
}
// safety: size of out_bytes is constant and always % 4 == 0
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
use keyfork_qrcode::{qrencode, ErrorCorrection}; use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes(); let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes()); qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode( if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
&BASE64_STANDARD.encode(qrcode_data), prompt.lock().unwrap().prompt_message(PromptMessage::Text(
ErrorCorrection::Highest,
) {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(
concat!( concat!(
"A QR code will be displayed after this prompt. ", "A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ", "Send the QR code back to the operator combining the shards. ",
@ -408,14 +315,14 @@ pub trait Format {
))?; ))?;
prompt prompt
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .unwrap()
.prompt_message(PromptMessage::Data(qrcode))?; .prompt_message(PromptMessage::Data(qrcode))?;
} }
} }
prompt prompt
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .unwrap()
.prompt_message(PromptMessage::Text(format!( .prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}" "Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?; )))?;
@ -489,17 +396,13 @@ pub struct InvalidData;
/// 1 byte: Version /// 1 byte: Version
/// 1 byte: Threshold /// 1 byte: Threshold
/// Data: &[u8] /// Data: &[u8]
pub(crate) const HUNK_VERSION: u8 = 2; pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_OFFSET: usize = 2; pub(crate) const HUNK_OFFSET: usize = 2;
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera."; const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
const QRCODE_TIMEOUT: u64 = 60; // One minute
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: "; const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| { const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry.";
std::env::var("KEYFORK_QRCODE_TIMEOUT")
.ok()
.and_then(|t| u64::from_str(&t).ok())
.unwrap_or(60)
});
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the /// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
/// shares, and combine them. /// shares, and combine them.
@ -514,7 +417,7 @@ static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
/// The function may panic if it is given payloads generated using a version of Keyfork that is /// The function may panic if it is given payloads generated using a version of Keyfork that is
/// incompatible with the currently running version. /// incompatible with the currently running version.
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> { pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = keyfork_prompt::default_handler()?; let mut pm = Terminal::new(stdin(), stdout())?;
let mut iter_count = None; let mut iter_count = None;
let mut shares = vec![]; let mut shares = vec![];
@ -524,22 +427,22 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) { while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1; iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?; let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
use keyfork_qrcode::{qrencode, ErrorCorrection}; use keyfork_qrcode::{qrencode, ErrorCorrection};
let qrcode_data = key_mnemonic.to_bytes(); let mut qrcode_data = nonce_mnemonic.to_bytes();
if let Ok(qrcode) = qrencode( qrcode_data.extend(key_mnemonic.as_bytes());
&BASE64_STANDARD.encode(qrcode_data), if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
ErrorCorrection::Highest,
) {
pm.prompt_message(PromptMessage::Text(format!( pm.prompt_message(PromptMessage::Text(format!(
concat!( concat!(
"QR code #{iter} will be displayed after this prompt. ", "A QR code will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ", "Send the QR code to only shardholder {iter}. ",
"Only the next shardholder should scan the QR code." "Nobody else should scan this QR code."
), ),
iter = iter iter = iter
)))?; )))?;
@ -549,9 +452,11 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
pm.prompt_message(PromptMessage::Text(format!( pm.prompt_message(PromptMessage::Text(format!(
concat!( concat!(
"Upon request, these words should be sent to the shardholder: ", "Upon request, these words should be sent to shardholder {iter}: ",
"{key_mnemonic}" "{nonce_mnemonic} {key_mnemonic}"
), ),
iter = iter,
nonce_mnemonic = nonce_mnemonic,
key_mnemonic = key_mnemonic, key_mnemonic = key_mnemonic,
)))?; )))?;
@ -561,48 +466,29 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?; pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
loop { if let Ok(Some(hex)) =
if let Ok(Some(qrcode_content)) = keyfork_qrcode::scan_camera( keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
std::time::Duration::from_secs(*QRCODE_TIMEOUT), {
0, let decoded_data = smex::decode(&hex)?;
) { let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
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 _ =
pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec()); let _ = payload_data.insert(decoded_data[32..].to_vec());
} else { } else {
let choice = keyfork_prompt::prompt_choice( pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
&mut *pm,
"A QR code could not be scanned. Retry or continue?",
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
)?;
if choice == RetryScanMnemonic::Continue {
break;
}
}; };
} }
}
let (pubkey, payload) = match (pubkey_data, payload_data) { let (pubkey, payload) = match (pubkey_data, payload_data) {
(Some(pubkey), Some(payload)) => (pubkey, payload), (Some(pubkey), Some(payload)) => (pubkey, payload),
_ => { _ => {
let validator = MnemonicSetValidator { let validator = MnemonicSetValidator {
word_lengths: [24, 39], word_lengths: [24, 48],
}; };
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>( let [pubkey_mnemonic, payload_mnemonic] = pm
&mut *pm, .prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ, QRCODE_COULDNT_READ,
3, 3,
&*validator.to_fn(), validator.to_fn(),
)?; )?;
let pubkey = pubkey_mnemonic let pubkey = pubkey_mnemonic
.as_bytes() .as_bytes()
@ -613,28 +499,14 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
} }
}; };
assert_eq!( let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
payload.len(), let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
ENCRYPTED_LENGTH as usize, let mut hkdf_output = [0u8; 256 / 8];
bug!("invalid payload data") hkdf.expand(&[], &mut hkdf_output)?;
); let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)); let payload =
assert!( shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
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())?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version"); assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
match &mut iter_count { match &mut iter_count {
@ -649,8 +521,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..].to_vec());
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
} }
let shares = shares let shares = shares

View File

@ -1,17 +1,14 @@
//! OpenPGP Shard functionality. //! OpenPGP Shard functionality.
#![allow(clippy::expect_fun_call)]
use std::{ use std::{
collections::HashMap, collections::HashMap,
io::{Read, Write}, io::{Read, Write},
path::Path, path::Path,
str::FromStr, str::FromStr,
sync::Mutex, sync::{Arc, Mutex},
rc::Rc, marker::PhantomData,
}; };
use keyfork_bug::bug;
use keyfork_derive_openpgp::{ use keyfork_derive_openpgp::{
derive_util::{DerivationPath, VariableLengthSeed}, derive_util::{DerivationPath, VariableLengthSeed},
XPrv, XPrv,
@ -25,7 +22,7 @@ use openpgp::{
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper}, stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
Parse, Parse,
}, },
policy::{NullPolicy, StandardPolicy, Policy}, policy::{NullPolicy, Policy, StandardPolicy},
serialize::{ serialize::{
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer}, stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal, Marshal,
@ -34,7 +31,7 @@ use openpgp::{
KeyID, PacketPile, KeyID, PacketPile,
}; };
pub use sequoia_openpgp as openpgp; pub use sequoia_openpgp as openpgp;
use blahaj::Share; use sharks::Share;
mod keyring; mod keyring;
use keyring::Keyring; use keyring::Keyring;
@ -77,10 +74,6 @@ pub enum Error {
/// An IO error occurred. /// An IO error occurred.
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[source] std::io::Error), Io(#[source] std::io::Error),
/// No valid keys were found for the given recipient.
#[error("No valid keys were found for the recipient {0}")]
NoValidKeys(KeyID),
} }
#[allow(missing_docs)] #[allow(missing_docs)]
@ -185,69 +178,52 @@ impl EncryptedMessage {
} }
} }
/// Encoding and decoding shards using OpenPGP.
pub struct OpenPGP;
impl OpenPGP {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them.
/// ///
/// Certificates are read from a file, or from files one level deep in a directory. pub struct OpenPGP<P: PromptHandler> {
/// Certificates with duplicated fingerprints will be discarded. p: PhantomData<P>,
}
impl<P: PromptHandler> OpenPGP<P> {
#[allow(clippy::new_without_default, missing_docs)]
pub fn new() -> Self {
Self {
p: PhantomData,
}
}
}
impl<P: PromptHandler> OpenPGP<P> {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// from a file, or from files one level deep in a directory.
/// ///
/// # Errors /// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is /// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// unable to load certificates from the file. /// to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> { pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref(); let path = path.as_ref();
let mut pubkeys = std::collections::HashSet::new();
let mut certs = HashMap::new();
if path.is_file() { if path.is_file() {
for maybe_cert in CertParser::from_file(path).map_err(Error::Sequoia)? { let mut vec = vec![];
let cert = maybe_cert.map_err(Error::Sequoia)?; for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
let certfp = cert.fingerprint(); vec.push(cert.map_err(Error::Sequoia)?);
for key in cert.keys() {
let fp = key.fingerprint();
if pubkeys.contains(&fp) {
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
}
pubkeys.insert(fp);
}
certs.insert(certfp, cert);
} }
Ok(vec)
} else { } else {
let mut vec = vec![];
for entry in path for entry in path
.read_dir() .read_dir()
.map_err(Error::Io)? .map_err(Error::Io)?
.filter_map(Result::ok) .filter_map(Result::ok)
.filter(|p| p.path().is_file()) .filter(|p| p.path().is_file())
{ {
let cert = Cert::from_file(entry.path()).map_err(Error::Sequoia)?; vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
let certfp = cert.fingerprint();
for key in cert.keys() {
let fp = key.fingerprint();
if pubkeys.contains(&fp) {
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
} }
pubkeys.insert(fp); Ok(vec)
} }
certs.insert(certfp, cert);
}
}
for cert in certs.values() {
let policy = StandardPolicy::new();
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
if get_encryption_keys(&valid_cert).next().is_none() {
return Err(Error::NoValidKeys(valid_cert.keyid()))
}
}
Ok(certs.into_values().collect())
} }
} }
const METADATA_MESSAGE_MISSING: &str = "Metadata message was not found in parsed packets"; impl<P: PromptHandler> Format for OpenPGP<P> {
impl Format for OpenPGP {
type Error = Error; type Error = Error;
type PublicKey = Cert; type PublicKey = Cert;
type PrivateKeyData = Vec<Cert>; type PrivateKeyData = Vec<Cert>;
@ -259,17 +235,16 @@ impl Format for OpenPGP {
let seed = VariableLengthSeed::new(seed); let seed = VariableLengthSeed::new(seed);
// build cert to sign encrypted shares // build cert to sign encrypted shares
let userid = UserID::from("keyfork-sss"); let userid = UserID::from("keyfork-sss");
let path = DerivationPath::from_str("m/7366512'/0'").expect(bug!("valid derivation path")); let path = DerivationPath::from_str("m/7366512'/0'").expect("valid derivation path");
let xprv = XPrv::new(seed) let xprv = XPrv::new(seed)
.expect(bug!("could not create XPrv from key"))
.derive_path(&path) .derive_path(&path)
.expect(bug!("valid derivation")); .expect("valid derivation");
keyfork_derive_openpgp::derive( keyfork_derive_openpgp::derive(
xprv, xprv,
&[KeyFlags::empty().set_certification().set_signing()], &[KeyFlags::empty().set_certification().set_signing()],
&userid, &userid,
) )
.expect(bug!("valid cert creation")) .expect("valid cert creation")
} }
fn format_encrypted_header( fn format_encrypted_header(
@ -283,26 +258,21 @@ impl Format for OpenPGP {
// Note: Sequoia does not export private keys on a Cert, only on a TSK // Note: Sequoia does not export private keys on a Cert, only on a TSK
signing_key signing_key
.serialize(&mut pp) .serialize(&mut pp)
.expect(bug!("serialize cert into bytes")); .expect("serialize cert into bytes");
for cert in key_data { for cert in key_data {
cert.serialize(&mut pp) cert.serialize(&mut pp)
.expect(bug!("serialize pubkey into bytes")); .expect("serialize pubkey into bytes");
} }
// verify packet pile // verify packet pile
let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..]) let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..])
.expect(bug!("should have certs")); .expect("should have certs");
let first_cert = iter let first_cert = iter.next().transpose().ok().flatten().expect("first cert");
.next()
.transpose()
.ok()
.flatten()
.expect(bug!("first cert"));
assert_eq!(signing_key, &first_cert); assert_eq!(signing_key, &first_cert);
for (packet_cert, cert) in iter.zip(key_data) { for (packet_cert, cert) in iter.zip(key_data) {
assert_eq!( assert_eq!(
&packet_cert.expect(bug!("parsed packet cert")), &packet_cert.expect("parsed packet cert"),
cert, cert,
"packet pile could not recreate cert: {}", "packet pile could not recreate cert: {}",
cert.fingerprint(), cert.fingerprint(),
@ -415,7 +385,7 @@ impl Format for OpenPGP {
.map_err(Error::Sequoia)? .map_err(Error::Sequoia)?
.into_iter() .into_iter()
.next() .next()
.expect(bug!("serialized message should be parseable")); .expect("serialized message should be parseable");
Ok(message) Ok(message)
} }
@ -444,7 +414,7 @@ impl Format for OpenPGP {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>, prompt: Arc<Mutex<impl PromptHandler>>,
) -> std::result::Result<(Vec<Share>, u8), Self::Error> { ) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting. // Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired. // We don't want to invalidate someone's keys just because the old sig expired.
@ -455,9 +425,7 @@ impl Format for OpenPGP {
let mut encrypted_messages = encrypted_data.iter(); let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages let metadata = encrypted_messages.next().expect("metdata");
.next()
.expect(bug!(METADATA_MESSAGE_MISSING));
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?; let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?; let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
@ -505,7 +473,7 @@ impl Format for OpenPGP {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Rc<Mutex<Box<dyn PromptHandler>>>, prompt: Arc<Mutex<impl PromptHandler>>,
) -> std::result::Result<(Share, u8), Self::Error> { ) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new(); let policy = NullPolicy::new();
@ -514,9 +482,7 @@ impl Format for OpenPGP {
let mut encrypted_messages = encrypted_data.iter(); let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages let metadata = encrypted_messages.next().expect("metadata");
.next()
.expect(bug!(METADATA_MESSAGE_MISSING));
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?; let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?; let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
@ -549,44 +515,24 @@ impl Format for OpenPGP {
panic!("unable to decrypt shard"); panic!("unable to decrypt shard");
} }
}
fn decrypt_metadata( impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &Path {
&self, fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
private_keys: Option<Self::PrivateKeyData>, OpenPGP::<P>::discover_certs(self)
encrypted_data: &[Self::EncryptedData], }
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
let mut manager = SmartcardManager::new(prompt.clone())?;
let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
.next() OpenPGP::<P>::discover_certs(self)
.expect(bug!(METADATA_MESSAGE_MISSING));
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, _root_cert, certs) = decode_metadata_v1(&metadata_content)?;
Ok((threshold, certs))
} }
} }
impl KeyDiscovery<OpenPGP> for &Path { impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> { fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
OpenPGP::discover_certs(self)
}
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
OpenPGP::discover_certs(self)
}
}
impl KeyDiscovery<OpenPGP> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
Ok(self.to_vec()) Ok(self.to_vec())
} }
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> { fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
Ok(self.to_vec()) Ok(self.to_vec())
} }
} }
@ -599,8 +545,7 @@ fn get_encryption_keys<'a>(
openpgp::packet::key::UnspecifiedRole, openpgp::packet::key::UnspecifiedRole,
> { > {
cert.keys() cert.keys()
// NOTE: this causes complications on Airgap systems .alive()
// .alive()
.revoked(false) .revoked(false)
.supported() .supported()
.for_storage_encryption() .for_storage_encryption()
@ -648,12 +593,12 @@ fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a // NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
// single message. // single message.
fn decrypt_with_manager( fn decrypt_with_manager<P: PromptHandler>(
threshold: u8, threshold: u8,
messages: &mut HashMap<KeyID, EncryptedMessage>, messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert], certs: &[Cert],
policy: &dyn Policy, policy: &dyn Policy,
manager: &mut SmartcardManager, manager: &mut SmartcardManager<P>,
) -> Result<HashMap<KeyID, Vec<u8>>> { ) -> Result<HashMap<KeyID, Vec<u8>>> {
let mut decrypted_messages = HashMap::new(); let mut decrypted_messages = HashMap::new();
@ -680,12 +625,7 @@ fn decrypt_with_manager(
// Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message // Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message
if let Some(fp) = manager.load_any_fingerprint(unused_fingerprints)? { if let Some(fp) = manager.load_any_fingerprint(unused_fingerprints)? {
let cert_keyid = cert_by_fingerprint let cert_keyid = cert_by_fingerprint.get(&fp).unwrap().clone();
.get(&fp)
.expect(bug!(
"manager loaded fingerprint not from unused_fingerprints"
))
.clone();
if let Some(message) = messages.remove(&cert_keyid) { if let Some(message) = messages.remove(&cert_keyid) {
let message = message.decrypt_with(policy, &mut *manager)?; let message = message.decrypt_with(policy, &mut *manager)?;
decrypted_messages.insert(cert_keyid, message); decrypted_messages.insert(cert_keyid, message);
@ -698,11 +638,11 @@ fn decrypt_with_manager(
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to // NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
// decrypt messages with. // decrypt messages with.
fn decrypt_with_keyring( fn decrypt_with_keyring<P: PromptHandler>(
messages: &mut HashMap<KeyID, EncryptedMessage>, messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert], certs: &[Cert],
policy: &NullPolicy, policy: &NullPolicy,
keyring: &mut Keyring, keyring: &mut Keyring<P>,
) -> Result<HashMap<KeyID, Vec<u8>>, Error> { ) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
let mut decrypted_messages = HashMap::new(); let mut decrypted_messages = HashMap::new();
@ -732,11 +672,11 @@ fn decrypt_with_keyring(
Ok(decrypted_messages) Ok(decrypted_messages)
} }
fn decrypt_metadata( fn decrypt_metadata<P: PromptHandler>(
message: &EncryptedMessage, message: &EncryptedMessage,
policy: &NullPolicy, policy: &NullPolicy,
keyring: &mut Keyring, keyring: &mut Keyring<P>,
manager: &mut SmartcardManager, manager: &mut SmartcardManager<P>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
Ok(if keyring.is_empty() { Ok(if keyring.is_empty() {
manager.load_any_card()?; manager.load_any_card()?;

View File

@ -1,8 +1,5 @@
#![allow(clippy::expect_fun_call)] use std::sync::{Arc, Mutex};
use std::{rc::Rc, sync::Mutex};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{Error as PromptError, PromptHandler}; use keyfork_prompt::{Error as PromptError, PromptHandler};
use super::openpgp::{ use super::openpgp::{
@ -27,14 +24,14 @@ pub enum Error {
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring { pub struct Keyring<P: PromptHandler> {
full_certs: Vec<Cert>, full_certs: Vec<Cert>,
root: Option<Cert>, root: Option<Cert>,
pm: Rc<Mutex<Box<dyn PromptHandler>>>, pm: Arc<Mutex<P>>,
} }
impl Keyring { impl<P: PromptHandler> Keyring<P> {
pub fn new(certs: impl AsRef<[Cert]>, p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Result<Self> { pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self { Ok(Self {
full_certs: certs.as_ref().to_vec(), full_certs: certs.as_ref().to_vec(),
root: Default::default(), root: Default::default(),
@ -62,7 +59,7 @@ impl Keyring {
} }
} }
impl VerificationHelper for &mut Keyring { impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> { fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
Ok(ids Ok(ids
.iter() .iter()
@ -84,23 +81,13 @@ impl VerificationHelper for &mut Keyring {
aead_algo, aead_algo,
} => {} } => {}
MessageLayer::SignatureGroup { results } => { 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 { for result in results {
if let Err(e) = result { 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}")); return Err(anyhow::anyhow!("Invalid signature: {e}"));
} }
} }
*/
} }
} }
} }
@ -108,7 +95,7 @@ impl VerificationHelper for &mut Keyring {
} }
} }
impl DecryptionHelper for &mut Keyring { impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
fn decrypt<D>( fn decrypt<D>(
&mut self, &mut self,
pkesks: &[PKESK], pkesks: &[PKESK],
@ -153,7 +140,7 @@ impl DecryptionHelper for &mut Keyring {
let passphrase = self let passphrase = self
.pm .pm
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .unwrap()
.prompt_passphrase(&message) .prompt_passphrase(&message)
.context("Decryption passphrase")?; .context("Decryption passphrase")?;
secret_key secret_key

View File

@ -1,14 +1,9 @@
#![allow(clippy::expect_fun_call)]
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
rc::Rc, sync::{Arc, Mutex},
sync::Mutex,
}; };
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{ use keyfork_prompt::{
prompt_validated_passphrase,
validators::{PinValidator, Validator}, validators::{PinValidator, Validator},
Error as PromptError, Message, PromptHandler, Error as PromptError, Message, PromptHandler,
}; };
@ -73,15 +68,15 @@ fn format_name(input: impl AsRef<str>) -> String {
} }
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub struct SmartcardManager { pub struct SmartcardManager<P: PromptHandler> {
current_card: Option<Card<Open>>, current_card: Option<Card<Open>>,
root: Option<Cert>, root: Option<Cert>,
pm: Rc<Mutex<Box<dyn PromptHandler>>>, pm: Arc<Mutex<P>>,
pin_cache: HashMap<Fingerprint, String>, pin_cache: HashMap<Fingerprint, String>,
} }
impl SmartcardManager { impl<P: PromptHandler> SmartcardManager<P> {
pub fn new(p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Result<Self> { pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self { Ok(Self {
current_card: None, current_card: None,
root: None, root: None,
@ -105,7 +100,7 @@ impl SmartcardManager {
} }
self.pm self.pm
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .unwrap()
.prompt_message(Message::Text( .prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter" "No smart card was found. Please plug in a smart card and press enter"
.to_string(), .to_string(),
@ -165,7 +160,7 @@ impl SmartcardManager {
self.pm self.pm
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .unwrap()
.prompt_message(Message::Text( .prompt_message(Message::Text(
"Please plug in a smart card and press enter".to_string(), "Please plug in a smart card and press enter".to_string(),
))?; ))?;
@ -175,7 +170,7 @@ impl SmartcardManager {
} }
} }
impl VerificationHelper for &mut SmartcardManager { impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> { fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
#[allow(clippy::flat_map_option)] #[allow(clippy::flat_map_option)]
Ok(ids Ok(ids
@ -195,23 +190,12 @@ impl VerificationHelper for &mut SmartcardManager {
aead_algo, aead_algo,
} => {} } => {}
MessageLayer::SignatureGroup { results } => { 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 { for result in results {
if let Err(e) = result { if let Err(e) = result {
return Err(anyhow::anyhow!("Invalid signature: {e}")); // FIXME: anyhow leak
return Err(anyhow::anyhow!("Verification error: {}", e.to_string()));
} }
} }
*/
} }
} }
} }
@ -219,7 +203,7 @@ impl VerificationHelper for &mut SmartcardManager {
} }
} }
impl DecryptionHelper for &mut SmartcardManager { impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
fn decrypt<D>( fn decrypt<D>(
&mut self, &mut self,
pkesks: &[PKESK], pkesks: &[PKESK],
@ -277,8 +261,11 @@ impl DecryptionHelper for &mut SmartcardManager {
} else { } else {
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ") format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
}; };
let mut prompt = self.pm.lock().expect(bug!(POISONED_MUTEX)); let temp_pin = self
let temp_pin = prompt_validated_passphrase(&mut **prompt, &message, 3, &pin_validator)?; .pm
.lock()
.unwrap()
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim()); let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
match verification_status { match verification_status {
#[allow(clippy::ignored_unit_patterns)] #[allow(clippy::ignored_unit_patterns)]
@ -290,7 +277,7 @@ impl DecryptionHelper for &mut SmartcardManager {
Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => { Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => {
self.pm self.pm
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .unwrap()
.prompt_message(Message::Text("Invalid PIN length entered.".to_string()))?; .prompt_message(Message::Text("Invalid PIN length entered.".to_string()))?;
} }
Err(_) => {} Err(_) => {}

View File

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

View File

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

View File

@ -1,191 +1,45 @@
use super::{Keyfork, create}; use super::Keyfork;
use clap::{Args, Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand};
use std::{fmt::Display, io::Write, path::PathBuf};
use keyfork_derive_openpgp::openpgp::{ use keyfork_derive_openpgp::{
openpgp::{
armor::{Kind, Writer}, armor::{Kind, Writer},
packet::UserID, packet::UserID,
serialize::Marshal, serialize::Marshal,
types::KeyFlags, types::KeyFlags,
Cert, },
}; XPrvKey,
use keyfork_derive_path_data::paths;
use keyfork_derive_util::{
request::DerivationAlgorithm, DerivationIndex, DerivationPath, ExtendedPrivateKey as XPrv,
IndexError, PrivateKey,
}; };
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyforkd_client::Client; use keyforkd_client::Client;
type OptWrite = Option<Box<dyn Write>>;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
pub trait Deriver {
type Prv: PrivateKey + Clone;
const DERIVATION_ALGORITHM: DerivationAlgorithm;
fn derivation_path(&self) -> DerivationPath;
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()>;
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()>;
}
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
pub enum DeriveSubcommands { pub enum DeriveSubcommands {
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP /// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
/// ASCII Armor, a format usable by most programs 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 /// The key is generated with a 24-hour expiration time. The operation to set the expiration
/// expire later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as /// time to a higher value is left to the user to ensure the key is usable by the user.
/// "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")] #[command(name = "openpgp")]
OpenPGP(OpenPGP), OpenPGP {
/// Derive an Ed25519 key for a specific algorithm, in a given format.
Key(Key),
}
/// Derivation path to use when deriving OpenPGP keys.
#[derive(ValueEnum, Clone, Debug, Default)]
pub enum Path {
/// The default derivation path; no additional index is used.
#[default]
Default,
/// The Disaster Recovery index.
DisasterRecovery,
}
impl std::fmt::Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Path {
fn as_str(&self) -> &'static str {
match self {
Path::Default => "default",
Path::DisasterRecovery => "disaster-recovery",
}
}
fn derivation_path(&self) -> DerivationPath {
match self {
Self::Default => paths::OPENPGP.clone(),
Self::DisasterRecovery => paths::OPENPGP_DISASTER_RECOVERY.clone(),
}
}
}
#[derive(Args, Clone, Debug)]
pub struct OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format. /// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String, user_id: String,
},
/// Derivation path to use when deriving OpenPGP keys.
#[arg(long, required = false, default_value = "default")]
derivation_path: Path,
}
/// A format for exporting a key.
#[derive(ValueEnum, Clone, Debug)]
pub enum KeyFormat {
Hex,
Base64,
}
/// An invalid slug was provided.
#[derive(thiserror::Error, Debug)]
pub enum InvalidSlug {
/// The value provided was longer than four bytes.
#[error("The value provided was longer than four bytes: {0}")]
InvalidSize(usize),
/// The value provided was higher than the maximum derivation index.
#[error("The value provided was higher than the maximum derivation index: {0}")]
InvalidValue(#[from] IndexError),
}
#[derive(Clone, Debug)]
pub struct Slug(DerivationIndex);
impl std::str::FromStr for Slug {
type Err = InvalidSlug;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let bytes = s.as_bytes();
let mut parseable_bytes = [0u8; 4];
if bytes.len() <= 4 && !bytes.is_empty() {
parseable_bytes[(4 - bytes.len())..4].copy_from_slice(bytes);
} else {
return Err(InvalidSlug::InvalidSize(bytes.len()));
}
let slug = u32::from_be_bytes(parseable_bytes);
let index = DerivationIndex::new(slug, true)?;
Ok(Slug(index))
}
}
impl Display for Slug {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (self.0.inner() & (0b1 << 31)).to_be_bytes().as_slice() {
[0, 0, 0, 0] => Ok(()),
[0, 0, 0, bytes @ ..] | [0, 0, bytes @ ..] | [0, bytes @ ..] | [bytes @ ..] => f
.write_str(
std::str::from_utf8(&bytes[..]).expect("slug constructed from non-utf8"),
),
}
}
}
#[derive(Args, Clone, Debug)]
pub struct Key {
/// The derivation algorithm to derive a key for.
derivation_algorithm: DerivationAlgorithm,
/// The output format.
#[arg(value_enum)]
format: KeyFormat,
/// A maximum of four bytes, used for creating the derivation path.
#[arg(value_parser = clap::value_parser!(Slug))]
slug: Slug,
} }
impl DeriveSubcommands { impl DeriveSubcommands {
fn handle(&self, account: DerivationIndex, is_public: bool, writer: OptWrite) -> Result<()> { fn handle(&self, account: DerivationIndex) -> Result<()> {
match self { match self {
DeriveSubcommands::OpenPGP(opgp) => { DeriveSubcommands::OpenPGP { user_id } => {
let path = opgp.derivation_path(); let mut pgp_u32 = [0u8; 4];
let xprv = Client::discover_socket()? pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
.request_xprv::<<OpenPGP as Deriver>::Prv>(&path.chain_push(account))?; let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
if is_public { let path = DerivationPath::default()
opgp.derive_public_with_xprv(writer, xprv) .chain_push(chain)
} else { .chain_push(account);
opgp.derive_with_xprv(writer, xprv) // TODO: should this be customizable?
}
}
DeriveSubcommands::Key(key) => {
let path = key.derivation_path();
let xprv = Client::discover_socket()?
.request_xprv::<<Key as Deriver>::Prv>(&path.chain_push(account))?;
if is_public {
key.derive_public_with_xprv(writer, xprv)
} else {
key.derive_with_xprv(writer, xprv)
}
}
}
}
}
impl OpenPGP {
fn cert_from_xprv(&self, xprv: keyfork_derive_openpgp::XPrv) -> Result<Cert> {
let subkeys = vec![ let subkeys = vec![
KeyFlags::empty().set_certification(), KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(), KeyFlags::empty().set_signing(),
@ -194,100 +48,20 @@ impl OpenPGP {
.set_storage_encryption(), .set_storage_encryption(),
KeyFlags::empty().set_authentication(), KeyFlags::empty().set_authentication(),
]; ];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
let userid = UserID::from(&*self.user_id); let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
keyfork_derive_openpgp::derive(xprv, &subkeys, &userid).map_err(Into::into)
for packet in cert.into_packets() {
packet.serialize(&mut w)?;
}
w.finalize()?;
} }
} }
impl Deriver for OpenPGP {
type Prv = keyfork_derive_openpgp::XPrvKey;
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
fn derivation_path(&self) -> DerivationPath {
self.derivation_path.derivation_path()
}
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
let cert = self.cert_from_xprv(xprv)?;
let writer = match writer {
Some(w) => w,
None => {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
let file = create(&path)?;
Box::new(file)
}
};
let mut writer = Writer::new(writer, Kind::SecretKey)?;
for packet in cert.as_tsk().into_packets() {
packet.serialize(&mut writer)?;
}
writer.finalize()?;
Ok(())
}
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
let cert = self.cert_from_xprv(xprv)?;
let writer = match writer {
Some(w) => w,
None => {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
let file = create(&path)?;
Box::new(file)
}
};
let mut writer = Writer::new(writer, Kind::PublicKey)?;
for packet in cert.into_packets2() {
packet.serialize(&mut writer)?;
}
writer.finalize()?;
Ok(())
}
}
impl Deriver for Key {
// HACK: We're abusing that we use the same key as OpenPGP. Maybe we should use ed25519_dalek.
type Prv = keyfork_derive_openpgp::XPrvKey;
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
fn derivation_path(&self) -> DerivationPath {
DerivationPath::default().chain_push(self.slug.0.clone())
}
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
let (formatted, ext) = match self.format {
KeyFormat::Hex => (smex::encode(xprv.private_key().to_bytes()), "hex"),
KeyFormat::Base64 => {
use base64::prelude::*;
(BASE64_STANDARD.encode(xprv.private_key().to_bytes()), "b64")
}
};
let filename =
PathBuf::from(smex::encode(xprv.public_key().to_bytes())).with_extension(ext);
if let Some(mut writer) = writer {
writeln!(writer, "{formatted}")?;
} else {
std::fs::write(&filename, formatted)?;
}
Ok(())
}
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
let (formatted, ext) = match self.format {
KeyFormat::Hex => (smex::encode(xprv.public_key().to_bytes()), "hex"),
KeyFormat::Base64 => {
use base64::prelude::*;
(BASE64_STANDARD.encode(xprv.public_key().to_bytes()), "b64")
}
};
let filename =
PathBuf::from(smex::encode(xprv.public_key().to_bytes())).with_extension(ext);
if let Some(mut writer) = writer {
writeln!(writer, "{formatted}")?;
} else {
std::fs::write(&filename, formatted)?;
}
Ok(()) Ok(())
} }
} }
@ -295,7 +69,7 @@ impl Deriver for Key {
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
pub struct Derive { pub struct Derive {
#[command(subcommand)] #[command(subcommand)]
pub(crate) command: DeriveSubcommands, command: DeriveSubcommands,
/// Account ID. Required for all derivations. /// Account ID. Required for all derivations.
/// ///
@ -303,45 +77,12 @@ pub struct Derive {
/// account ID can often come as a hindrance in the future. As such, it is always required. If /// 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`. /// the account ID is not relevant, it is assumed to be `0`.
#[arg(long, global = true, default_value = "0")] #[arg(long, global = true, default_value = "0")]
pub(crate) account_id: u32, account_id: u32,
/// Whether derivation should return the public key or a private key.
#[arg(long, global = true)]
pub(crate) public: bool,
/// Whether the file should be written to standard output, or to a filename generated by the
/// derivation system.
#[arg(long, global = true, default_value = "false")]
pub to_stdout: bool,
/// The file to write the derived public key to, if not standard output. If omitted, a filename
/// will be generated by the relevant deriver.
#[arg(long, global = true, conflicts_with = "to_stdout")]
pub output: Option<PathBuf>,
} }
impl Derive { impl Derive {
pub fn handle(&self, _k: &Keyfork) -> Result<()> { pub fn handle(&self, _k: &Keyfork) -> Result<()> {
let account = DerivationIndex::new(self.account_id, true)?; let account = DerivationIndex::new(self.account_id, true)?;
let writer = if let Some(output) = self.output.as_deref() { self.command.handle(account)
Some(Box::new(std::fs::File::create(output)?) as Box<dyn Write>)
} else if self.to_stdout {
Some(Box::new(std::io::stdout()) as Box<dyn Write>)
} else {
None
};
self.command.handle(account, self.public, writer)
}
}
impl std::str::FromStr for Derive {
type Err = clap::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Derive::try_parse_from(
[String::from("derive")]
.into_iter()
.chain(shlex::Shlex::new(s)),
)
} }
} }

View File

@ -1,43 +1,6 @@
use super::{ use super::Keyfork;
create,
derive::{self, Deriver},
provision,
Keyfork,
};
use crate::{clap_ext::*, config, openpgp_card::factory_reset_current_card};
use card_backend_pcsc::PcscBackend;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use std::{ use std::fmt::Display;
collections::HashMap,
fmt::Display,
fs::File,
io::{IsTerminal, Write},
path::{Path, PathBuf},
str::FromStr,
};
use keyfork_derive_openpgp::{
openpgp::{
self,
armor::{Kind, Writer},
packet::{UserID, signature::SignatureBuilder},
policy::StandardPolicy,
serialize::{
stream::{Encryptor2, LiteralWriter, Message, Recipient},
Serialize,
},
types::{KeyFlags, SignatureType},
},
XPrv,
};
use keyfork_derive_util::DerivationIndex;
use keyfork_prompt::{
default_handler, prompt_validated_passphrase,
validators::{SecurePinValidator, Validator},
};
use keyfork_shard::{openpgp::OpenPGP, Format};
type StringMap = HashMap<String, String>;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub enum SeedSize { pub enum SeedSize {
@ -96,7 +59,6 @@ impl From<&SeedSize> for usize {
} }
} }
} }
#[derive(Clone, Debug, thiserror::Error)] #[derive(Clone, Debug, thiserror::Error)]
pub enum MnemonicSeedSourceParseError { pub enum MnemonicSeedSourceParseError {
#[error("Expected one of system, playing, tarot, dice")] #[error("Expected one of system, playing, tarot, dice")]
@ -134,41 +96,24 @@ impl std::str::FromStr for MnemonicSeedSource {
} }
impl MnemonicSeedSource { impl MnemonicSeedSource {
pub fn handle( pub fn handle(&self, size: &SeedSize) -> Result<String, Box<dyn std::error::Error>> {
&self,
size: &SeedSize,
) -> Result<keyfork_mnemonic::Mnemonic, Box<dyn std::error::Error>> {
let size = match size { let size = match size {
SeedSize::Bits128 => 128, SeedSize::Bits128 => 128,
SeedSize::Bits256 => 256, SeedSize::Bits256 => 256,
}; };
let seed = match self { let seed = match self {
MnemonicSeedSource::System => keyfork_entropy::generate_entropy_of_size(size / 8)?, MnemonicSeedSource::System => {
keyfork_entropy::generate_entropy_of_size(size / 8)?
}
MnemonicSeedSource::Playing => todo!(), MnemonicSeedSource::Playing => todo!(),
MnemonicSeedSource::Tarot => todo!(), MnemonicSeedSource::Tarot => todo!(),
MnemonicSeedSource::Dice => todo!(), MnemonicSeedSource::Dice => todo!(),
}; };
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?; let mnemonic = keyfork_mnemonic_util::Mnemonic::from_bytes(&seed)?;
Ok(mnemonic) Ok(mnemonic.to_string())
} }
} }
/// An error occurred while performing an operation.
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// An error occurred when interacting iwth a file.
#[error("Error while performing IO operation on: {1}")]
IOContext(#[source] std::io::Error, PathBuf),
/// A required option was not provided.
#[error("The required option {0} was not provided")]
MissingOption(&'static str),
}
fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a {
|e| Error::IOContext(e, path.to_path_buf())
}
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
pub enum MnemonicSubcommands { pub enum MnemonicSubcommands {
/// Generate a mnemonic using a given entropy source. /// Generate a mnemonic using a given entropy source.
@ -179,10 +124,6 @@ pub enum MnemonicSubcommands {
/// method of generating a seed using system entropy, as well as various forms of loading /// method of generating a seed using system entropy, as well as various forms of loading
/// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location /// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location
/// (such as a Trezor "recovery seed card") and never persisted digitally. /// (such as a Trezor "recovery seed card") and never persisted digitally.
///
/// When using the `--shard`, `--shard-to`, `--encrypt-to`, and `--encrypt-to-self` +
/// `--provision` arguments, the mnemonic is _not_ sent to output. The data for the mnemonic is
/// then either split using Keyfork Shard or encrypted using OpenPGP.
Generate { Generate {
/// The source from where a seed is created. /// The source from where a seed is created.
#[arg(long, value_enum, default_value_t = Default::default())] #[arg(long, value_enum, default_value_t = Default::default())]
@ -191,757 +132,17 @@ pub enum MnemonicSubcommands {
/// The size of the mnemonic, in bits. /// The size of the mnemonic, in bits.
#[arg(long, default_value_t = Default::default())] #[arg(long, default_value_t = Default::default())]
size: SeedSize, size: SeedSize,
/// Derive a key. By default, a private key is derived. Unlike other arguments in this
/// file, arguments must be passed using the format similar to the CLI. For example:
/// `--derive='openpgp --public "Ryan Heywood <ryan@distrust.co>"'` would be synonymous
/// with starting the Keyfork daemon with the provided mnemonic, then running
/// `keyfork derive openpgp --public "Ryan Heywood <ryan@distrust.co>"`.
///
/// The output of the derived key is written to a filename based on the content of the key;
/// for instance, OpenPGP keys are written to a file identifiable by the certificate's
/// fingerprint. This behavior can be changed by using the `--to-stdout` or `--output`
/// modifiers to the `--derive` command.
#[arg(long)]
derive: Option<derive::Derive>,
/// Encrypt the mnemonic to an OpenPGP certificate in the provided path.
///
/// When given arguments in the format `--encrypt-to input.asc,output=output.asc`, the
/// output of the encryption will be written to `output.asc`. Otherwise, the default
/// behavior is to write the output to `input.enc.asc`. If the output file already exists,
/// it will not be overwritten, and the command will exit unsuccessfully.
#[arg(long)]
encrypt_to: Option<Vec<ValueWithOptions<PathBuf>>>,
/// Shard the mnemonic to the certificates in the given Shardfile. Requires a decrypt
/// operation on the Shardfile to access the metadata and certificates.
///
/// When given arguments in the format `--shard-to input.asc,output=output.asc`, the
/// output of the encryption will be written to `output.asc`. Otherwise, the default
/// behavior is to write the output to `input.new.asc`. If the output file already exists,
/// it will not be overwritten, and the command will exit unsuccessfully.
#[arg(long)]
shard_to: Option<Vec<ValueWithOptions<PathBuf>>>,
/// Shard the mnemonic to the provided certificates.
///
/// The following additional arguments are available:
///
/// * threshold, m: the minimum amount of shares required to reconstitute the shard. By
/// default, this is the amount of certificates provided.
///
/// * max, n: the maximum amount of shares. When provided, this is used to ensure the
/// certificate count is correct. This is required when using `threshold` or `m`.
///
/// * output: the file to write the generated Shardfile to. By default, assuming the
/// certificate input is `input.asc`, the generated Shardfile would be written to
/// `input.shard.asc`.
#[arg(long)]
shard: Option<Vec<ValueWithOptions<PathBuf>>>,
/// Encrypt the mnemonic to an OpenPGP certificate derived from the mnemonic, writing the
/// output to the provided path. This command must be run in combination with
/// `--provision openpgp-card`, `--derive openpgp`, or another OpenPGP key derivation
/// mechanism, to ensure the generated mnemonic would be decryptable.
///
/// When used in combination with `--derive` or `--provision` with OpenPGP configurations,
/// the default behavior is to encrypt the mnemonic to all derived and provisioned
/// accounts. By default, the account `0` is used.
#[arg(long)]
encrypt_to_self: Option<PathBuf>,
/// Shard the mnemonic to freshly-generated OpenPGP certificates derived from the mnemonic,
/// writing the output to the provided path, and provisioning OpenPGP smartcards with the
/// new certificates.
///
/// The following additional arguments are required:
///
/// * threshold, m: the minimum amount of shares required to reconstitute the shard.
///
/// * max, n: the maximum amount of shares.
///
/// * cards_per_shard: the amount of OpenPGP smartcards to provision per shardholder.
///
/// * cert_output: the file to write all generated OpenPGP certificates to; if not
/// provided, files will be automatically generated for each certificate.
#[arg(long)]
shard_to_self: Option<ValueWithOptions<PathBuf>>,
/// Provision a key derived from the mnemonic to a piece of hardware such as an OpenPGP
/// smartcard. This argument is required when used with `--encrypt-to-self`.
///
/// Provisioners may choose to output a public key to the current directory by default, but
/// this functionality may be altered on a by-provisioner basis by providing the `output=`
/// option to `--provisioner-config`. Additionally, Keyfork may choose to disable
/// provisioner output if a matching public key has been derived using `--derive`, which
/// may allow for controlling additional metadata that is not relevant to the provisioned
/// keys, such as an OpenPGP User ID.
#[arg(long)]
provision: Option<provision::Provision>,
/// The amount of times the provisioner should be run. If provisioning multiple devices at
/// once, this number should be specified to the number of devices, and all devices should
/// be plugged into the system at the same time.
#[arg(long, requires = "provision", default_value = "1")]
provision_count: usize,
/// The configuration to pass to the provisioner. These values are specific to each
/// provisioner, and should be provided in a `key=value,key=value` format. Most
/// provisioners only expect an `output=` option, to be used in place of the default output
/// path, if the provisioner needs to write data to a file, such as an OpenPGP certificate.
#[arg(long, requires = "provision", default_value_t = Options::default())]
provision_config: Options,
}, },
} }
// NOTE: This function defaults to `.asc` in the event no extension is found.
// This is specific to OpenPGP. If you want to use this function elsewhere (why?),
// be sure to use a relevant extension for your context.
fn determine_valid_output_path<T: AsRef<Path>>(
path: &Path,
mid_ext: &str,
optional_path: Option<T>,
) -> PathBuf {
match optional_path {
Some(p) => p.as_ref().to_path_buf(),
None => {
let extension = match path.extension() {
Some(ext) => format!("{mid_ext}.{ext}", ext = ext.to_string_lossy()),
None => format!("{mid_ext}.asc"),
};
path.with_extension(extension)
}
}
}
fn is_extension_armored(path: &Path) -> bool {
match path.extension().and_then(|s| s.to_str()) {
Some("pgp") | Some("gpg") => false,
Some("asc") => true,
_ => {
eprintln!("unable to determine whether to armor file: {path:?}");
eprintln!("use .gpg, .pgp, or .asc extension, or `armor=true`");
eprintln!("defaulting to armored");
true
}
}
}
fn do_encrypt_to(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let policy = StandardPolicy::new();
let output_file = determine_valid_output_path(path, "enc", options.get("output"));
let is_armored =
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
let certs = OpenPGP::discover_certs(path)?;
let valid_certs = certs
.iter()
.map(|c| c.with_policy(&policy, None))
.collect::<openpgp::Result<Vec<_>>>()?;
let recipients = valid_certs.iter().flat_map(|valid_cert| {
let keys = valid_cert.keys().alive().for_storage_encryption();
keys.map(|key| Recipient::new(key.keyid(), key.key()))
});
let mut output = vec![];
let message = Message::new(&mut output);
let encrypted_message = Encryptor2::for_recipients(message, recipients).build()?;
let mut literal_message = LiteralWriter::new(encrypted_message).build()?;
literal_message.write_all(mnemonic.to_string().as_bytes())?;
literal_message.write_all(b"\n")?;
literal_message.finalize()?;
let mut file = File::create(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
let mut writer = Writer::new(file, Kind::Message)?;
writer.write_all(&output)?;
writer.finalize()?;
} else {
file.write_all(&output)?;
}
Ok(())
}
fn do_encrypt_to_self(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
accounts: &[keyfork_derive_util::DerivationIndex],
) -> Result<(), Box<dyn std::error::Error>> {
let mut certs = vec![];
for account in accounts.iter().cloned() {
let userid = UserID::from("Keyfork Temporary Key");
let subkeys = [
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let seed = mnemonic.generate_seed(None);
let xprv = XPrv::new(seed)?;
let derivation_path = keyfork_derive_path_data::paths::OPENPGP
.clone()
.chain_push(account);
let cert =
keyfork_derive_openpgp::derive(xprv.derive_path(&derivation_path)?, &subkeys, &userid)?;
certs.push(cert);
}
let mut file = tempfile::NamedTempFile::new()?;
let mut writer = Writer::new(&mut file, Kind::PublicKey)?;
for cert in certs {
cert.serialize(&mut writer)?;
}
writer.finalize()?;
let temp_path = file.into_temp_path();
// a sneaky bit of DRY
do_encrypt_to(
mnemonic,
&temp_path,
&StringMap::from([(String::from("output"), path.to_string_lossy().to_string())]),
)?;
temp_path.close()?;
Ok(())
}
#[derive(thiserror::Error, Debug)]
#[error("Either the threshold(m) or the max(n) values are missing")]
struct MissingThresholdOrMax;
fn do_shard(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let output_file = determine_valid_output_path(path, "shard", options.get("output"));
let is_armored =
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
let threshold = options
.get("threshold")
.or_else(|| options.get("m"))
.map(|s| u8::from_str(s))
.transpose()?;
let max = options
.get("max")
.or_else(|| options.get("n"))
.map(|s| u8::from_str(s))
.transpose()?;
let certs = OpenPGP::discover_certs(path)?;
// if neither are set: false
// if both are set: false
// if only one is set: true
if threshold.is_some() ^ max.is_some() {
return Err(MissingThresholdOrMax)?;
}
let (threshold, max) = match threshold.zip(max) {
Some(t) => t,
None => {
let len = u8::try_from(certs.len())?;
(len, len)
}
};
let openpgp = keyfork_shard::openpgp::OpenPGP;
let mut output = vec![];
openpgp.shard_and_encrypt(threshold, max, mnemonic.as_bytes(), &certs[..], &mut output)?;
let mut file = File::create(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
file.write_all(&output)?;
} else {
todo!("keyfork does not handle binary shardfiles");
/*
* NOTE: this code works, but can't be recombined by Keyfork.
* therefore, we'll error, before someone tries to use it.
let mut dearmor = Reader::from_bytes(&output, ReaderMode::Tolerant(None));
std::io::copy(&mut dearmor, &mut file)?;
*/
}
Ok(())
}
fn do_shard_to(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let output_file = determine_valid_output_path(path, "new", options.get("output"));
let is_armored =
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?;
let input = File::open(path)?;
let (threshold, certs) = openpgp.decrypt_metadata_from_file(
Some(&[][..]), // the things i must do to avoid qualifying types.
input,
prompt,
)?;
let mut output = vec![];
openpgp.shard_and_encrypt(
threshold,
u8::try_from(certs.len())?,
mnemonic.as_bytes(),
&certs[..],
&mut output,
)?;
let mut file = File::create(&output_file).map_err(context_stub(&output_file))?;
if is_armored {
file.write_all(&output)?;
} else {
todo!("keyfork does not handle binary shardfiles");
/*
* NOTE: this code works, but can't be recombined by Keyfork.
* therefore, we'll error, before someone tries to use it.
let mut dearmor = Reader::from_bytes(&output, ReaderMode::Tolerant(None));
std::io::copy(&mut dearmor, &mut file)?;
*/
}
Ok(())
}
fn derive_key(seed: [u8; 64], index: u8) -> Result<openpgp::Cert, Box<dyn std::error::Error>> {
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let subkey = DerivationIndex::new(u32::from(index), true)?;
let path = keyfork_derive_path_data::paths::OPENPGP_SHARD.clone().chain_push(subkey);
let xprv = XPrv::new(seed)
.expect("could not construct master key from seed")
.derive_path(&path)?;
let userid = UserID::from(format!("Keyfork Shard {index}"));
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
Ok(cert)
}
fn cross_sign_certs(certs: &mut [openpgp::Cert]) -> Result<(), Box<dyn std::error::Error>> {
let policy = StandardPolicy::new();
#[allow(clippy::unnecessary_to_owned)]
for signing_cert in certs.to_vec() {
let mut certify_key = signing_cert
.with_policy(&policy, None)?
.keys()
.unencrypted_secret()
.for_certification()
.next()
.expect("certify key unusable/not found")
.key()
.clone()
.into_keypair()?;
for signable_cert in certs.iter_mut() {
let sb = SignatureBuilder::new(SignatureType::GenericCertification);
let userid = signable_cert
.userids()
.next()
.expect("a signable user ID is necessary to create web of trust");
let signature = sb.sign_userid_binding(
&mut certify_key,
signable_cert.primary_key().key(),
&userid,
)?;
let changed;
(*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?;
assert!(
changed,
"OpenPGP certificate was unchanged after inserting packets"
);
}
}
Ok(())
}
fn do_shard_to_self(
mnemonic: &keyfork_mnemonic::Mnemonic,
path: &Path,
options: &StringMap,
) -> Result<(), Box<dyn std::error::Error>> {
let seed = mnemonic.generate_seed(None);
let mut pm = default_handler()?;
let mut certs = vec![];
let mut seen_cards = std::collections::HashSet::new();
let threshold: u8 = options
.get("threshold")
.or(options.get("m"))
.ok_or(Error::MissingOption("threshold"))?
.parse()?;
let max: u8 = options
.get("max")
.or(options.get("n"))
.ok_or(Error::MissingOption("max"))?
.parse()?;
let cards_per_shard = options
.get("cards_per_shard")
.as_deref()
.map(|cps| u8::from_str(cps))
.transpose()?;
let pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
for index in 0..max {
let cert = derive_key(seed, index)?;
for i in 0..cards_per_shard.unwrap_or(1) {
pm.prompt_message(keyfork_prompt::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(keyfork_prompt::Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
};
let pin = prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard PIN: ",
3,
&pin_validator,
)?;
factory_reset_current_card(
&mut |application_identifier| {
if seen_cards.contains(&application_identifier) {
// we were given a previously-seen card, error
// we're gonna panic because this is a significant error
panic!("Previously used card {application_identifier} was reused");
} else {
seen_cards.insert(application_identifier);
true
}
},
pin.trim(),
pin.trim(),
&cert,
&openpgp::policy::NullPolicy::new(),
card_backend,
)?;
}
certs.push(cert);
}
cross_sign_certs(&mut certs)?;
let opgp = OpenPGP;
let output = File::create(path)?;
opgp.shard_and_encrypt(
threshold,
certs.len() as u8,
mnemonic.as_bytes(),
&certs[..],
output,
)?;
match options.get("cert_output") {
Some(path) => {
let cert_file = std::fs::File::create(path)?;
let mut writer = Writer::new(cert_file, Kind::PublicKey)?;
for cert in &certs {
cert.serialize(&mut writer)?;
}
writer.finalize()?;
}
None => {
for cert in &certs {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
let file = create(&path)?;
let mut writer = Writer::new(file, Kind::PublicKey)?;
cert.serialize(&mut writer)?;
writer.finalize()?;
}
}
}
Ok(())
}
fn do_provision(
mnemonic: &keyfork_mnemonic::Mnemonic,
provision: &provision::Provision,
count: usize,
config: &HashMap<String, String>,
) -> Result<(), Box<dyn std::error::Error>> {
assert!(
provision.subcommand.is_none(),
"provisioner was given a subcommand; this functionality is not supported"
);
let identifiers = match &provision.identifier {
Some(identifier) => {
vec![identifier.clone()]
}
None => provision
.provisioner_name
.discover()?
.into_iter()
.map(|(name, _ctx)| name)
.collect(),
};
assert_eq!(
identifiers.len(),
count,
"amount of provisionable devices discovered did not match provisioner count"
);
for identifier in identifiers {
let provisioner_with_identifier = provision::Provision {
identifier: Some(identifier),
..provision.clone()
};
let mut provisioner = config::Provisioner::try_from(provisioner_with_identifier)?;
match &mut provisioner.metadata {
Some(metadata) => {
metadata.extend(config.clone().into_iter());
}
metadata @ None => {
*metadata = Some(config.clone());
}
};
provision
.provisioner_name
.provision_with_mnemonic(mnemonic, provisioner)?;
}
Ok(())
}
fn do_derive(
mnemonic: &keyfork_mnemonic::MnemonicBase<keyfork_mnemonic::English>,
deriver: &derive::Derive,
) -> Result<(), Box<dyn std::error::Error>> {
let writer = if let Some(output) = deriver.output.as_deref() {
Some(Box::new(std::fs::File::create(output)?) as Box<dyn Write>)
} else if deriver.to_stdout {
Some(Box::new(std::io::stdout()) as Box<dyn Write>)
} else {
None
};
match deriver {
derive::Derive {
command: derive::DeriveSubcommands::OpenPGP(opgp),
account_id,
public,
..
} => {
use keyfork_derive_openpgp::XPrv;
let root_xprv = XPrv::new(mnemonic.generate_seed(None))?;
let account = DerivationIndex::new(*account_id, true)?;
let derived = root_xprv.derive_path(&opgp.derivation_path().chain_push(account))?;
if *public {
opgp.derive_public_with_xprv(writer, derived)?;
} else {
opgp.derive_with_xprv(writer, derived)?;
}
}
derive::Derive {
command: derive::DeriveSubcommands::Key(key),
account_id,
public,
..
} => {
// HACK: We're abusing that we use the same key as OpenPGP. Maybe
// we should use ed25519_dalek.
use keyfork_derive_openpgp::XPrv;
let root_xprv = XPrv::new(mnemonic.generate_seed(None))?;
let account = DerivationIndex::new(*account_id, true)?;
let derived = root_xprv.derive_path(&key.derivation_path().chain_push(account))?;
if *public {
key.derive_public_with_xprv(writer, derived)?;
} else {
key.derive_with_xprv(writer, derived)?;
}
}
}
Ok(())
}
impl MnemonicSubcommands { impl MnemonicSubcommands {
pub fn handle( pub fn handle(
&self, &self,
_m: &Mnemonic, _m: &Mnemonic,
_keyfork: &Keyfork, _keyfork: &Keyfork,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
match self { match self {
MnemonicSubcommands::Generate { MnemonicSubcommands::Generate { source, size } => source.handle(size),
source,
size,
derive,
encrypt_to,
shard_to,
shard,
encrypt_to_self,
shard_to_self,
provision,
provision_count,
provision_config,
} => {
// NOTE: We should never have a case where there's Some() of empty vec, but
// we will make sure to check it just in case.
//
// We do not print the mnemonic if we are:
// * Encrypting to an existing, usable key
// * Encrypting to a newly provisioned key
// * Sharding to an existing Shardfile with usable keys
// * Sharding to existing, usable keys
// * Sharding to newly provisioned keys
let mut will_print_mnemonic =
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(|e| e.is_empty());
will_print_mnemonic = will_print_mnemonic
&& (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none());
will_print_mnemonic = will_print_mnemonic && shard_to.is_none()
|| shard_to.as_ref().is_some_and(|s| s.is_empty());
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|| shard.as_ref().is_some_and(|s| s.is_empty());
will_print_mnemonic = will_print_mnemonic && shard_to_self.is_none();
let mnemonic = source.handle(size)?;
if let Some(derive) = derive {
let stdout = std::io::stdout();
if will_print_mnemonic && !stdout.is_terminal() {
eprintln!(
"Writing plaintext mnemonic and derivation output to standard output"
);
}
do_derive(&mnemonic, derive)?;
}
if let Some(encrypt_to) = encrypt_to {
for entry in encrypt_to {
do_encrypt_to(&mnemonic, &entry.inner, &entry.values)?;
}
}
if let Some(encrypt_to_self) = encrypt_to_self {
let mut accounts: std::collections::HashSet<u32> = Default::default();
if let Some(provision::Provision {
provisioner_name: provision::Provisioner::OpenPGPCard(_),
account_id,
..
}) = provision
{
accounts.insert(*account_id);
}
if let Some(derive::Derive {
command: derive::DeriveSubcommands::OpenPGP(_),
account_id,
..
}) = derive
{
accounts.insert(*account_id);
}
let indices = accounts
.into_iter()
.map(|i| DerivationIndex::new(i, true))
.collect::<Result<Vec<_>, _>>()?;
assert!(
!indices.is_empty(),
"neither derived nor provisioned accounts were found"
);
do_encrypt_to_self(&mnemonic, &encrypt_to_self, &indices)?;
}
if let Some(shard_to_self) = shard_to_self {
do_shard_to_self(&mnemonic, &shard_to_self.inner, &shard_to_self.values)?;
}
if let Some(provisioner) = provision {
// determine if we should write to standard output based on whether we have a
// matching pair of provisioner and public derivation output.
let mut will_output_public_key = true;
if let Some(derive) = derive {
let matches = match (provisioner, derive) {
(
provision::Provision {
provisioner_name: provision::Provisioner::OpenPGPCard(_),
account_id: p_id,
..
},
derive::Derive {
command: derive::DeriveSubcommands::OpenPGP(_),
account_id: d_id,
..
},
) => p_id == d_id,
_ => false,
};
if matches && derive.public {
will_output_public_key = false;
}
}
let mut values = provision_config.values.clone();
if !will_output_public_key && !values.contains_key("output") {
values.insert(String::from("_skip_cert_output"), String::from("1"));
}
do_provision(&mnemonic, provisioner, *provision_count, &values)?;
}
if let Some(shard_to) = shard_to {
for entry in shard_to {
do_shard_to(&mnemonic, &entry.inner, &entry.values)?;
}
}
if let Some(shard) = shard {
for entry in shard {
do_shard(&mnemonic, &entry.inner, &entry.values)?;
}
}
if will_print_mnemonic {
println!("{}", mnemonic);
}
Ok(())
}
} }
} }
} }

View File

@ -5,11 +5,7 @@ mod mnemonic;
mod provision; mod provision;
mod recover; mod recover;
mod shard; mod shard;
mod wizard;
pub fn create(path: &std::path::Path) -> std::io::Result<std::fs::File> {
eprintln!("Writing derived key to: {path}", path=path.display());
std::fs::File::create(path)
}
/// The Kitchen Sink of Entropy. /// The Kitchen Sink of Entropy.
#[derive(Parser, Clone, Debug)] #[derive(Parser, Clone, Debug)]
@ -61,6 +57,9 @@ pub enum KeyforkCommands {
/// leaked by any individual deriver. /// leaked by any individual deriver.
Recover(recover::Recover), Recover(recover::Recover),
/// Utilities to automatically manage the setup of Keyfork.
Wizard(wizard::Wizard),
/// Print an autocompletion file to standard output. /// Print an autocompletion file to standard output.
/// ///
/// Keyfork does not manage the installation of completion files. Consult the documentation for /// Keyfork does not manage the installation of completion files. Consult the documentation for
@ -80,7 +79,8 @@ impl KeyforkCommands {
d.handle(keyfork)?; d.handle(keyfork)?;
} }
KeyforkCommands::Mnemonic(m) => { KeyforkCommands::Mnemonic(m) => {
m.command.handle(m, keyfork)?; let response = m.command.handle(m, keyfork)?;
println!("{response}");
} }
KeyforkCommands::Shard(s) => { KeyforkCommands::Shard(s) => {
s.command.handle(s, keyfork)?; s.command.handle(s, keyfork)?;
@ -91,6 +91,9 @@ impl KeyforkCommands {
KeyforkCommands::Recover(r) => { KeyforkCommands::Recover(r) => {
r.handle(keyfork)?; r.handle(keyfork)?;
} }
KeyforkCommands::Wizard(w) => {
w.handle(keyfork)?;
}
#[cfg(feature = "completion")] #[cfg(feature = "completion")]
KeyforkCommands::Completion { shell } => { KeyforkCommands::Completion { shell } => {
let mut command = Keyfork::command(); let mut command = Keyfork::command();

View File

@ -3,135 +3,81 @@ use crate::config;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_derive_util::{DerivationIndex, ExtendedPrivateKey};
mod openpgp;
type Identifier = (String, Option<String>);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Provisioner { pub enum Provisioner {
OpenPGPCard(openpgp::OpenPGPCard), OpenPGPCard(OpenPGPCard),
Shard(openpgp::Shard),
} }
impl std::fmt::Display for Provisioner { impl std::fmt::Display for Provisioner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.identifier()) match self {
Provisioner::OpenPGPCard(_) => f.write_str("openpgp-card"),
}
} }
} }
impl Provisioner { impl Provisioner {
pub fn identifier(&self) -> &'static str { fn discover(&self) -> Vec<(String, Option<String>)> {
match self {
Provisioner::OpenPGPCard(_) => "openpgp-card",
Provisioner::Shard(_) => "shard",
}
}
pub fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
match self { match self {
Provisioner::OpenPGPCard(o) => o.discover(), Provisioner::OpenPGPCard(o) => o.discover(),
Provisioner::Shard(s) => s.discover(),
} }
} }
pub fn provision( fn provision(
&self, &self,
provisioner: config::Provisioner, provisioner: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
match self { match self {
Provisioner::OpenPGPCard(o) => { Provisioner::OpenPGPCard(o) => o.provision(provisioner),
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
type XPrv = ExtendedPrivateKey<Prv>;
let account_index = DerivationIndex::new(provisioner.account, true)?;
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let mut client = keyforkd_client::Client::discover_socket()?;
let xprv: XPrv = client.request_xprv(&path)?;
o.provision(xprv, provisioner)
}
Provisioner::Shard(s) => {
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
type XPrv = ExtendedPrivateKey<Prv>;
let account_index = DerivationIndex::new(provisioner.account, true)?;
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let mut client = keyforkd_client::Client::discover_socket()?;
let xprv: XPrv = client.request_xprv(&path)?;
s.provision(xprv, provisioner)
}
}
}
pub fn provision_with_mnemonic(
&self,
mnemonic: &keyfork_mnemonic::Mnemonic,
provisioner: config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
match self {
Provisioner::OpenPGPCard(o) => {
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
type XPrv = ExtendedPrivateKey<Prv>;
let account_index = DerivationIndex::new(provisioner.account, true)?;
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
o.provision(xprv, provisioner)
}
Provisioner::Shard(s) => {
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
type XPrv = ExtendedPrivateKey<Prv>;
let account_index = DerivationIndex::new(provisioner.account, true)?;
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
s.provision(xprv, provisioner)
}
} }
} }
} }
impl ValueEnum for Provisioner { impl ValueEnum for Provisioner {
fn value_variants<'a>() -> &'a [Self] { fn value_variants<'a>() -> &'a [Self] {
&[Self::OpenPGPCard(openpgp::OpenPGPCard), Self::Shard(openpgp::Shard)] &[Self::OpenPGPCard(OpenPGPCard)]
} }
fn to_possible_value(&self) -> Option<PossibleValue> { fn to_possible_value(&self) -> Option<PossibleValue> {
Some(PossibleValue::new(self.identifier())) Some(PossibleValue::new(match self {
} Self::OpenPGPCard(_) => "openpgp-card",
} }))
#[derive(Debug, thiserror::Error)]
#[error("The given value could not be matched as a provisioner: {0} ({1})")]
pub struct ProvisionerFromStrError(String, String);
impl std::str::FromStr for Provisioner {
type Err = ProvisionerFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<Provisioner as ValueEnum>::from_str(s, false)
.map_err(|e| ProvisionerFromStrError(s.to_string(), e))
} }
} }
trait ProvisionExec { trait ProvisionExec {
type PrivateKey: keyfork_derive_util::PrivateKey + Clone;
/// Discover all known places the formatted key can be deployed to. /// Discover all known places the formatted key can be deployed to.
fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> { fn discover(&self) -> Vec<(String, Option<String>)> {
Ok(vec![]) vec![]
} }
/// Return the derivation path for deriving keys.
fn derivation_prefix() -> keyfork_derive_util::DerivationPath;
/// Derive a key and deploy it to a target. /// Derive a key and deploy it to a target.
fn provision( fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
&self, }
xprv: keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
p: config::Provisioner, #[derive(Clone, Debug)]
) -> Result<(), Box<dyn std::error::Error>>; pub struct OpenPGPCard;
impl ProvisionExec for OpenPGPCard {
fn discover(&self) -> Vec<(String, Option<String>)> {
/*
vec![
(
"0006:26144195".to_string(),
Some("Yubicats Heywood".to_string()),
),
(
"0006:2614419y".to_string(),
Some("Yubicats Heywood".to_string()),
),
]
*/
vec![]
}
fn provision(&self, _p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>> {
todo!()
}
} }
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
@ -148,27 +94,15 @@ pub struct Provision {
#[command(subcommand)] #[command(subcommand)]
pub subcommand: Option<ProvisionSubcommands>, pub subcommand: Option<ProvisionSubcommands>,
pub provisioner_name: Provisioner, provisioner_name: Provisioner,
/// Account ID. /// Account ID.
#[arg(long, default_value = "0")] #[arg(long, required(true))]
pub account_id: u32, account_id: Option<u32>,
/// Identifier of the hardware to deploy to, listable by running the `discover` subcommand. /// Identifier of the hardware to deploy to, listable by running the `discover` subcommand.
#[arg(long)] #[arg(long, required(true))]
pub identifier: Option<String>, identifier: Option<String>,
}
impl std::str::FromStr for Provision {
type Err = clap::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Provision::try_parse_from(
[String::from("provision")]
.into_iter()
.chain(shlex::Shlex::new(s)),
)
}
} }
// NOTE: In the future, this impl will be used by `keyfork recover` to reprovision hardware from // NOTE: In the future, this impl will be used by `keyfork recover` to reprovision hardware from
@ -184,7 +118,8 @@ impl TryFrom<Provision> for config::Provisioner {
fn try_from(value: Provision) -> Result<Self, Self::Error> { fn try_from(value: Provision) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
account: value.account_id, name: value.provisioner_name.to_string(),
account: value.account_id.ok_or(MissingField("account_id"))?,
identifier: value.identifier.ok_or(MissingField("identifier"))?, identifier: value.identifier.ok_or(MissingField("identifier"))?,
metadata: Default::default(), metadata: Default::default(),
}) })
@ -195,7 +130,7 @@ impl Provision {
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> { pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
match self.subcommand { match self.subcommand {
Some(ProvisionSubcommands::Discover) => { Some(ProvisionSubcommands::Discover) => {
let mut iter = self.provisioner_name.discover()?.into_iter().peekable(); let mut iter = self.provisioner_name.discover().into_iter().peekable();
while let Some((identifier, context)) = iter.next() { while let Some((identifier, context)) = iter.next() {
println!("Identifier: {identifier}"); println!("Identifier: {identifier}");
if let Some(context) = context { if let Some(context) = context {
@ -207,21 +142,7 @@ impl Provision {
} }
} }
None => { None => {
let provisioner_with_identifier = match self.identifier { self.provisioner_name.provision(self.clone().try_into()?)?;
Some(_) => self.clone(),
None => {
let identifiers = self.provisioner_name.discover()?;
let [id] = &identifiers[..] else {
panic!("invalid amount of identifiers; pass --identifier");
};
Self {
identifier: Some(id.0.clone()),
..self.clone()
}
}
};
let config = config::Provisioner::try_from(provisioner_with_identifier)?;
self.provisioner_name.provision(config)?;
} }
} }
Ok(()) Ok(())

View File

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

View File

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

View File

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

View File

@ -0,0 +1,229 @@
use super::Keyfork;
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, packet::UserID, types::KeyFlags, Cert},
XPrv,
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_prompt::{
validators::{PinValidator, Validator},
Message, PromptHandler, DefaultTerminal, default_terminal
};
use keyfork_shard::{Format, openpgp::OpenPGP};
#[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>;
fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
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 = DerivationPath::default()
.chain_push(chain)
.chain_push(account)
.chain_push(subkey);
let xprv = XPrv::new(seed).derive_path(&path)?;
let userid = UserID::from(format!("Keyfork Shard {index}"));
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
Ok(cert)
}
// TODO: extract into crate
/// Factory reset the current card so long as it does not match the last-used backend.
fn factory_reset_current_card(
seen_cards: &mut HashSet<String>,
user_pin: &str,
admin_pin: &str,
cert: &Cert,
card_backend: PcscBackend,
) -> Result<()> {
let policy = openpgp::policy::NullPolicy::new();
let valid_cert = cert.with_policy(&policy, None)?;
let signing_key = valid_cert
.keys()
.for_signing()
.secret()
.next()
.expect("no signing key found");
let decryption_key = valid_cert
.keys()
.for_storage_encryption()
.secret()
.next()
.expect("no decryption key found");
let authentication_key = valid_cert
.keys()
.for_authentication()
.secret()
.next()
.expect("no authentication key found");
let mut card = Card::<Open>::new(card_backend)?;
let mut transaction = card.transaction()?;
let application_identifier = transaction.application_identifier()?.ident();
if seen_cards.contains(&application_identifier) {
// we were given the same card, error
panic!("Previously used card {application_identifier} was reused");
} else {
seen_cards.insert(application_identifier);
}
transaction.factory_reset()?;
let mut admin = transaction.to_admin_card("12345678")?;
admin.upload_key(signing_key, KeyType::Signing, None)?;
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
transaction.change_user_pin("123456", user_pin)?;
transaction.change_admin_pin("12345678", admin_pin)?;
Ok(())
}
fn generate_shard_secret(
threshold: u8,
max: u8,
keys_per_shard: u8,
output_file: &Option<PathBuf>,
) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{256 / 8}>()?;
let mut pm = default_terminal()?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout();
if output_file.is_none() {
assert!(
!stdout.is_terminal(),
"not printing shard to terminal, redirect output"
);
}
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);
}
let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = output_file {
let output = File::create(output_file)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?;
}
Ok(())
}
#[derive(Subcommand, Clone, Debug)]
pub enum WizardSubcommands {
/// Create a 256 bit secret and shard the secret to smart cards.
///
/// Smart cards will need to be plugged in periodically during the wizard, where they will be factory reset and
/// provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered with `keyfork recover shard` or
/// `keyfork recover remote-shard`. The share file will be printed to standard output.
GenerateShardSecret {
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// The maximum amount of shards.
#[arg(long)]
max: u8,
/// The amount of smart cards to provision per-shard.
#[arg(long, default_value = "1")]
keys_per_shard: u8,
/// The file to write the generated shard file to.
#[arg(long)]
output: Option<PathBuf>,
},
}
impl WizardSubcommands {
fn handle(&self) -> Result<()> {
match self {
WizardSubcommands::GenerateShardSecret {
threshold,
max,
keys_per_shard,
output,
} => generate_shard_secret(*threshold, *max, *keys_per_shard, output),
}
}
}
#[derive(Parser, Debug, Clone)]
pub struct Wizard {
#[command(subcommand)]
command: WizardSubcommands,
}
impl Wizard {
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
self.command.handle()?;
Ok(())
}
}

View File

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

View File

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

View File

@ -1,115 +0,0 @@
use card_backend_pcsc::PcscBackend;
use keyfork_derive_openpgp::openpgp::{policy::Policy, Cert};
use keyfork_prompt::{
prompt_validated_passphrase,
validators::{SecurePinValidator, Validator},
Message, PromptHandler,
};
use openpgp_card_sequoia::{state::Open, types::KeyType, types::TouchPolicy, Card};
pub fn get_new_pins(
pm: &mut dyn PromptHandler,
) -> Result<(String, String), Box<dyn std::error::Error>> {
let user_pin_validator = SecurePinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = SecurePinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
let user_pin = loop {
let user_pin = prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let validated_user_pin = prompt_validated_passphrase(
&mut *pm,
"Please verify the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
if user_pin != validated_user_pin {
pm.prompt_message(Message::Text("User PINs did not match. Retrying.".into()))?;
} else {
break user_pin;
}
};
let admin_pin = loop {
let admin_pin = prompt_validated_passphrase(
&mut *pm,
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
let validated_admin_pin = prompt_validated_passphrase(
&mut *pm,
"Please verify the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
if admin_pin != validated_admin_pin {
pm.prompt_message(Message::Text("Admin PINs did not match. Retrying.".into()))?;
} else {
break admin_pin;
}
};
Ok((user_pin, admin_pin))
}
/// Factory reset the current card so long as it does not match the last-used backend.
///
/// The return value of `false` means the filter was matched, whereas `true` means it was
/// successfully provisioned.
pub fn factory_reset_current_card(
card_filter: &mut dyn FnMut(String) -> bool,
user_pin: &str,
admin_pin: &str,
cert: &Cert,
policy: &dyn Policy,
card_backend: PcscBackend,
) -> Result<bool, Box<dyn std::error::Error>> {
let valid_cert = cert.with_policy(policy, None)?;
let signing_key = valid_cert
.keys()
.for_signing()
.secret()
.next()
.expect("no signing key found");
let decryption_key = valid_cert
.keys()
.for_storage_encryption()
.secret()
.next()
.expect("no decryption key found");
let authentication_key = valid_cert
.keys()
.for_authentication()
.secret()
.next()
.expect("no authentication key found");
let mut card = Card::<Open>::new(card_backend)?;
let mut transaction = card.transaction()?;
let application_identifier = transaction.application_identifier()?.ident();
if !card_filter(application_identifier) {
return Ok(false);
}
transaction.factory_reset()?;
let mut admin = transaction.to_admin_card("12345678")?;
admin.upload_key(signing_key, KeyType::Signing, None)?;
admin.set_touch_policy(KeyType::Signing, TouchPolicy::On)?;
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
admin.set_touch_policy(KeyType::Decryption, TouchPolicy::On)?;
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
admin.set_touch_policy(KeyType::Authentication, TouchPolicy::On)?;
transaction.change_user_pin("123456", user_pin)?;
transaction.change_admin_pin("12345678", admin_pin)?;
Ok(true)
}

View File

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

View File

@ -1,11 +1,11 @@
#![allow(missing_docs)] //!
use std::time::Duration; use std::time::Duration;
use keyfork_qrcode::scan_camera; use keyfork_qrcode::scan_camera;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let output = scan_camera(Duration::from_secs(15), 0)?; let output = scan_camera(Duration::from_secs(60 * 10), 0)?;
if let Some(scanned_text) = output { if let Some(scanned_text) = output {
println!("{scanned_text}"); println!("{scanned_text}");
} }

View File

@ -1,18 +1,17 @@
//! Encoding and decoding QR codes. //! Encoding and decoding QR codes.
use keyfork_bug as bug; use image::io::Reader as ImageReader;
use image::{ImageBuffer, ImageReader, Luma};
use std::{ use std::{
io::{Cursor, Write}, io::{Cursor, Write},
time::{Duration, SystemTime},
process::{Command, Stdio}, process::{Command, Stdio},
time::{Duration, Instant},
}; };
use v4l::{ use v4l::{
buffer::Type, buffer::Type,
io::{traits::CaptureStream, userptr::Stream}, io::{userptr::Stream, traits::CaptureStream},
video::Capture, video::Capture,
Device, FourCC, FourCC,
Device,
}; };
/// A QR code could not be generated. /// A QR code could not be generated.
@ -99,119 +98,62 @@ pub fn qrencode(
Ok(result) Ok(result)
} }
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
trait Scanner {
fn scan_image(&mut self, image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Option<String>;
}
#[cfg(feature = "decode-backend-zbar")]
mod zbar {
use super::{ImageBuffer, Luma, Scanner};
pub struct Zbar {
scanner: keyfork_zbar::image_scanner::ImageScanner,
}
impl Zbar {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
}
impl Default for Zbar {
fn default() -> Self {
Self {
scanner: keyfork_zbar::image_scanner::ImageScanner::new(),
}
}
}
impl Scanner for Zbar {
fn scan_image(
&mut self,
image: ImageBuffer<Luma<u8>, Vec<u8>>,
) -> Option<String> {
let image = keyfork_zbar::image::Image::from(image);
self.scanner.scan_image(&image).into_iter().next().map(|symbol| {
String::from_utf8_lossy(symbol.data()).into()
})
}
}
}
#[cfg(feature = "decode-backend-rqrr")]
mod rqrr {
use super::{ImageBuffer, Luma, Scanner};
pub struct Rqrr;
impl Scanner for Rqrr {
fn scan_image(
&mut self,
image: ImageBuffer<Luma<u8>, Vec<u8>>,
) -> Option<String> {
let mut image = rqrr::PreparedImage::prepare(image);
for grid in image.detect_grids() {
if let Ok((_, content)) = grid.decode() {
return Some(content);
}
}
None
}
}
}
#[allow(dead_code)]
fn dbg_elapsed(count: u64, instant: Instant) {
let elapsed = instant.elapsed().as_secs();
let framerate = count as f64 / elapsed as f64;
eprintln!("framerate: {count}/{elapsed} = {framerate}");
}
/// Continuously scan the `index`-th camera for a QR code. /// Continuously scan the `index`-th camera for a QR code.
/// #[cfg(feature = "decode-backend-rqrr")]
/// # Errors
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;
let mut fmt = device let mut fmt = device.format().expect("Failed to read format");
.format()
.unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
fmt.fourcc = FourCC::new(b"MPG1"); fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?; device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?; let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = Instant::now(); let start = SystemTime::now();
cfg_if::cfg_if! {
if #[cfg(feature = "decode-backend-zbar")] {
let mut scanner = zbar::Zbar::default();
} else if #[cfg(feature = "decode-backend-rqrr")] {
let mut scanner = rqrr::Rqrr;
} else {
unimplemented!("neither decode-backend-zbar nor decode-backend-rqrr were selected")
}
};
#[allow(unused)] while SystemTime::now()
let mut count = 0; .duration_since(start)
.unwrap_or(Duration::from_secs(0))
while Instant::now().duration_since(start) < timeout { < timeout
count += 1; {
let (buffer, _) = stream.next()?; let (buffer, _) = stream.next()?;
let image = ImageReader::new(Cursor::new(buffer)) let image = ImageReader::new(Cursor::new(buffer))
.with_guessed_format()? .with_guessed_format()?
.decode()? .decode()?
.to_luma8(); .to_luma8();
if let Some(content) = scanner.scan_image(image) { let mut image = rqrr::PreparedImage::prepare(image);
// dbg_elapsed(count, start); for grid in image.detect_grids() {
return Ok(Some(content)); if let Ok((_, content)) = grid.decode() {
return Ok(Some(content))
}
}
}
Ok(None)
}
/// Continuously scan the `index`-th camera for a QR code.
#[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().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 = SystemTime::now();
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout
{
let (buffer, _) = stream.next()?;
let image = ImageReader::new(Cursor::new(buffer))
.with_guessed_format()?
.decode()?;
let image = keyfork_zbar::image::Image::from(image);
for symbol in scanner.scan_image(&image) {
return Ok(Some(String::from_utf8_lossy(symbol.data()).to_string()));
} }
} }
// dbg_elapsed(count, start);
Ok(None) Ok(None)
} }

View File

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

View File

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

View File

@ -58,7 +58,7 @@ impl Image {
#[cfg(feature = "image")] #[cfg(feature = "image")]
mod impls { mod impls {
use super::*; use super::*;
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma}; use image::{DynamicImage, GenericImageView};
impl From<DynamicImage> for Image { impl From<DynamicImage> for Image {
fn from(value: DynamicImage) -> Self { fn from(value: DynamicImage) -> Self {
@ -70,17 +70,6 @@ mod impls {
image image
} }
} }
impl From<ImageBuffer<Luma<u8>, Vec<u8>>> for Image {
fn from(value: ImageBuffer<Luma<u8>, Vec<u8>>) -> Self {
let mut image = Self::alloc();
let (width, height) = value.dimensions();
image.set_size(width, height);
image.set_format(b"Y800");
image.set_data(value.into_raw());
image
}
}
} }
impl Drop for Image { impl Drop for Image {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,137 +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
//! let rows = 24;
//! let input_lines_len = 25;
//! assert!(input_lines_len < rows, "{input_lines_len} can't fit in {rows} lines!");
//! ```
//!
//! ```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)
}};
}
/// Assert a condition is true, otherwise throwing an error using Keyfork Bug.
///
/// # Examples
/// ```rust
/// let expectations = "conceivable!";
/// let circumstances = "otherwise";
/// assert!(circumstances != expectations, "you keep using that word...");
/// ```
///
/// Variables can be used in the error message, without having to pass them manually.
///
/// ```rust,should_panic
/// let rows = 24;
/// let input_lines_len = 25;
/// assert!(input_lines_len < rows, "{input_lines_len} can't fit in {rows} lines!");
/// ```
#[macro_export]
macro_rules! assert {
($cond:expr, $($input:tt)*) => {
std::assert!($cond, "{}", keyfork_bug::bug!($($input)*));
}
}
/// 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

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
//! Utilities for reading entropy from secure sources. //! Utilities for reading entropy from secure sources.
use keyfork_bug::bug;
use std::{ use std::{
fs::{read_dir, read_to_string, File}, fs::{read_dir, read_to_string, File},
io::Read, io::Read,
@ -11,16 +9,15 @@ static WARNING_LINKS: [&str; 1] =
["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"]; ["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"];
fn ensure_safe_kernel_version() { fn ensure_safe_kernel_version() {
let kernel_version = let kernel_version = read_to_string("/proc/version").expect("/proc/version");
read_to_string("/proc/version").expect(bug!("Unable to open file: /proc/version"));
let v = kernel_version let v = kernel_version
.split(' ') .split(' ')
.nth(2) .nth(2)
.expect(bug!("Unable to parse kernel version")) .expect("Unable to parse kernel version")
.split('.') .split('.')
.take(2) .take(2)
.map(str::parse) .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>>(); .collect::<Vec<u32>>();
let [major, minor, ..] = v.as_slice() else { let [major, minor, ..] = v.as_slice() else {
panic!("Unable to determine major and minor: {kernel_version}"); panic!("Unable to determine major and minor: {kernel_version}");
@ -33,23 +30,22 @@ fn ensure_safe_kernel_version() {
} }
fn ensure_offline() { 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 { 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 if path
.as_os_str() .as_os_str()
.to_str() .to_str()
.expect(bug!("Unable to decode UTF-8 filepath")) .expect("Unable to decode UTF-8 filepath")
.split('/') .split('/')
.last() .last()
.expect(bug!("No data in file path")) .expect("No data in file path")
== "lo" == "lo"
{ {
continue; continue;
} }
path.push("operstate"); path.push("operstate");
let isup = let isup = read_to_string(&path).expect("Unable to read operstate of network interfaces");
read_to_string(&path).expect(bug!("Unable to read operstate of network interfaces"));
assert_ne!(isup.trim(), "up", "No network interfaces should be up"); assert_ne!(isup.trim(), "up", "No network interfaces should be up");
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -3,17 +3,17 @@
//! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase: //! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase:
//! //!
//! ```rust //! ```rust
//! use keyfork_mnemonic::Mnemonic; //! use keyfork_mnemonic_util::Mnemonic;
//! let data = b"Hello, world! I am a mnemonic :)"; //! let data = b"Hello, world! I am a mnemonic :)";
//! assert_eq!(data.len(), 32); //! assert_eq!(data.len(), 32);
//! let mnemonic = Mnemonic::try_from_slice(data).unwrap(); //! let mnemonic = Mnemonic::from_bytes(data).unwrap();
//! println!("Our mnemonic is: {mnemonic}"); //! println!("Our mnemonic is: {mnemonic}");
//! ``` //! ```
//! //!
//! A mnemonic can also be parsed from a string: //! A mnemonic can also be parsed from a string:
//! //!
//! ```rust //! ```rust
//! use keyfork_mnemonic::Mnemonic; //! use keyfork_mnemonic_util::Mnemonic;
//! use std::str::FromStr; //! use std::str::FromStr;
//! //!
//! let data = b"Hello, world! I am a mnemonic :)"; //! let data = b"Hello, world! I am a mnemonic :)";
@ -28,7 +28,7 @@
//! verified to be safe: //! verified to be safe:
//! //!
//! ```rust //! ```rust
//! use keyfork_mnemonic::Mnemonic; //! use keyfork_mnemonic_util::Mnemonic;
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; //! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) }; //! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
//! let mnemonic_text = mnemonic.to_string(); //! let mnemonic_text = mnemonic.to_string();
@ -37,7 +37,7 @@
//! If given an invalid length, undefined behavior may follow, or code may panic. //! If given an invalid length, undefined behavior may follow, or code may panic.
//! //!
//! ```rust,should_panic //! ```rust,should_panic
//! use keyfork_mnemonic::Mnemonic; //! use keyfork_mnemonic_util::Mnemonic;
//! use std::str::FromStr; //! use std::str::FromStr;
//! //!
//! // NOTE: Data is of invalid length, 31 //! // NOTE: Data is of invalid length, 31
@ -48,9 +48,13 @@
//! let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap(); //! let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
//! ``` //! ```
use std::{error::Error, fmt::Display, marker::PhantomData, str::FromStr, sync::OnceLock}; use std::{
error::Error,
use keyfork_bug::bug; fmt::Display,
str::FromStr,
sync::OnceLock,
marker::PhantomData,
};
use hmac::Hmac; use hmac::Hmac;
use pbkdf2::pbkdf2; use pbkdf2::pbkdf2;
@ -111,11 +115,12 @@ impl Wordlist for English {
fn get_singleton<'a>() -> &'a Self { fn get_singleton<'a>() -> &'a Self {
ENGLISH.get_or_init(|| { ENGLISH.get_or_init(|| {
let wordlist_file = include_str!("data/wordlist.txt"); let wordlist_file = include_str!("data/wordlist.txt");
let mut words = wordlist_file.lines().skip(1).map(|x| x.trim().to_string()); let mut words = wordlist_file
.lines()
.skip(1)
.map(|x| x.trim().to_string());
English { English {
words: std::array::from_fn(|_| { words: std::array::from_fn(|_| words.next().expect("wordlist has 2048 words")),
words.next().expect(bug!("wordlist {} should have 2048 words"))
}),
} }
}) })
} }
@ -125,13 +130,6 @@ impl Wordlist for English {
} }
} }
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`]. /// A BIP-0039 mnemonic with reference to a [`Wordlist`].
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct MnemonicBase<W: Wordlist> { pub struct MnemonicBase<W: Wordlist> {
@ -249,10 +247,7 @@ where
} }
} }
Ok(MnemonicBase { Ok(MnemonicBase { data, marker: PhantomData })
data,
marker: PhantomData,
})
} }
} }
@ -268,11 +263,11 @@ where
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = Mnemonic::try_from_slice(data.as_slice()).unwrap(); /// let mnemonic = Mnemonic::from_bytes(data.as_slice()).unwrap();
/// ``` /// ```
pub fn try_from_slice(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> { pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
let bit_count = bytes.len() * 8; let bit_count = bytes.len() * 8;
if bit_count % 32 != 0 { if bit_count % 32 != 0 {
@ -283,51 +278,31 @@ where
return Err(MnemonicGenerationError::InvalidByteLength(bit_count)); return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
} }
Ok( Self::from_raw_bytes(bytes) ) Ok(unsafe { Self::from_raw_bytes(bytes) })
} }
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data may be of a size /// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of a factor of 4, up to 1024 bytes. /// of 128, 192, or 256 bits, as per BIP-0039.
/// ///
/// ```rust /// # Errors
/// use keyfork_mnemonic::Mnemonic; /// An error may be returned if the data is not within the expected lengths.
/// let data = b"hello world!"; #[deprecated = "use Mnemonic::from_bytes"]
/// let mnemonic = Mnemonic::from_array(*data); pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
/// ``` MnemonicBase::from_bytes(bytes)
///
/// 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 /// Create a Mnemonic using an arbitrary length of given data. The length does not need to
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes. /// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
/// ///
/// # Panics /// # Safety
///
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't /// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4 /// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`. If the assumption is incorrect, code may panic. The /// == 0`. If the assumption is incorrect, code may panic.
/// [`MnemonicBase::from_array`] function may be used to generate entropy if the length of the
/// data is known at compile-time.
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) }; /// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string(); /// let mnemonic_text = mnemonic.to_string();
@ -336,16 +311,17 @@ where
/// If given an invalid length, undefined behavior may follow, or code may panic. /// If given an invalid length, undefined behavior may follow, or code may panic.
/// ///
/// ```rust,should_panic /// ```rust,should_panic
/// use keyfork_mnemonic::Mnemonic; /// use keyfork_mnemonic_util::Mnemonic;
/// use std::str::FromStr; /// use std::str::FromStr;
/// ///
/// // NOTE: Data is of invalid length, 31 /// // NOTE: Data is of invalid length, 31
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) }; /// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string();
/// // NOTE: panic happens here
/// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
/// ``` /// ```
pub fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> { pub unsafe fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
assert!(bytes.len() % 4 == 0);
assert!(bytes.len() <= 1024);
MnemonicBase { MnemonicBase {
data: bytes.to_vec(), data: bytes.to_vec(),
marker: PhantomData, marker: PhantomData,
@ -373,31 +349,16 @@ where
&self.data &self.data
} }
/// A view to internal representation of the decoded data.
pub fn as_slice(&self) -> &[u8] {
&self.data
}
/// A clone of the internal representation of the decoded data. /// A clone of the internal representation of the decoded data.
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
self.data.to_vec() self.data.to_vec()
} }
/// A clone of the internal representation of the decoded data.
pub fn to_vec(&self) -> Vec<u8> {
self.data.to_vec()
}
/// Conver the Mnemonic into the internal representation of the decoded data. /// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_bytes(self) -> Vec<u8> { pub fn into_bytes(self) -> Vec<u8> {
self.data self.data
} }
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_vec(self) -> Vec<u8> {
self.data
}
/// Clone the existing data. /// Clone the existing data.
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"] #[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> { pub fn entropy(&self) -> Vec<u8> {
@ -413,7 +374,7 @@ where
&self, &self,
passphrase: impl Into<Option<&'a str>>, passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> { ) -> Result<Vec<u8>, MnemonicGenerationError> {
Ok(self.generate_seed(passphrase).to_vec()) Ok(self.generate_seed(passphrase))
} }
/// Create a BIP-0032 seed from the provided data and an optional passphrase. /// Create a BIP-0032 seed from the provided data and an optional passphrase.
@ -421,15 +382,16 @@ where
/// # Panics /// # Panics
/// The function may panic if the HmacSha512 function returns an error. The only error the /// The function may panic if the HmacSha512 function returns an error. The only error the
/// HmacSha512 function should return is an invalid length, which should not be possible. /// HmacSha512 function should return is an invalid length, which should not be possible.
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> [u8; 64] { ///
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> Vec<u8> {
let passphrase = passphrase.into(); let passphrase = passphrase.into();
let mut seed = [0u8; 64]; let mut seed = [0u8; 64];
let mnemonic = self.to_string(); let mnemonic = self.to_string();
let salt = ["mnemonic", passphrase.unwrap_or("")].join(""); let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed) pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.expect(bug!("HmacSha512 InvalidLength should be infallible")); .expect("HmacSha512 InvalidLength should be infallible");
seed seed.to_vec()
} }
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a /// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
@ -453,49 +415,13 @@ where
} }
// TODO: find a way to not have to collect to vec // TODO: find a way to not have to collect to vec
bits.chunks_exact(11) bits.chunks_exact(11).peekable().map(|chunk| {
.peekable()
.map(|chunk| {
let mut num = 0usize; let mut num = 0usize;
for i in 0..11 { for i in 0..11 {
num += usize::from(chunk[10 - i]) << i; num += usize::from(chunk[10 - i]) << i;
} }
num num
}) }).collect()
.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)
} }
} }
@ -515,7 +441,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap(); let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8]; let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap(); random_handle.read_exact(&mut entropy[..]).unwrap();
let mnemonic = super::Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap(); let mnemonic = super::Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let new_entropy = mnemonic.as_bytes(); let new_entropy = mnemonic.as_bytes();
assert_eq!(new_entropy, entropy); assert_eq!(new_entropy, entropy);
} }
@ -531,7 +457,7 @@ mod tests {
}; };
let hex = hex::decode(hex_.as_str().unwrap()).unwrap(); let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
let mnemonic = Mnemonic::try_from_slice(&hex).unwrap(); let mnemonic = Mnemonic::from_bytes(&hex).unwrap();
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap()); assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
} }
@ -542,7 +468,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap(); let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8]; let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap(); random_handle.read_exact(&mut entropy[..]).unwrap();
let my_mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap(); let my_mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap(); let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string()); assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed("")); assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
@ -566,7 +492,7 @@ mod tests {
for _ in 0..tests { for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap(); random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap(); let mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let words = mnemonic.words(); let words = mnemonic.words();
hs.clear(); hs.clear();
hs.extend(words); hs.extend(words);
@ -593,30 +519,12 @@ mod tests {
} }
#[test] #[test]
fn can_do_up_to_8192_bits() { fn can_do_up_to_1024_bits() {
let mut entropy = [0u8; 1024]; let entropy = &mut [0u8; 128];
let mut random = std::fs::File::open("/dev/urandom").unwrap(); let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap(); random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_array(entropy); let mnemonic = unsafe { Mnemonic::from_raw_bytes(&entropy[..]) };
let words = mnemonic.words(); let words = mnemonic.words();
assert_eq!(words.len(), 768); assert!(words.len() == 96);
}
#[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] [package]
name = "keyfork-prompt" name = "keyfork-prompt"
version = "0.2.2" version = "0.1.0"
description = "Prompt management utilities for Keyfork" description = "Prompt management utilities for Keyfork"
repository = "https://git.distrust.co/public/keyfork" repository = "https://git.distrust.co/public/keyfork"
edition = "2021" edition = "2021"
@ -10,10 +10,9 @@ license = "MIT"
[features] [features]
default = ["mnemonic"] default = ["mnemonic"]
mnemonic = ["keyfork-mnemonic"] mnemonic = ["keyfork-mnemonic-util"]
[dependencies] [dependencies]
keyfork-bug = { workspace = true } keyfork-crossterm = { version = "0.27.1", path = "../keyfork-crossterm", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] }
keyfork-crossterm = { workspace = true, default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] } keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", optional = true }
keyfork-mnemonic = { workspace = true, optional = true } thiserror = "1.0.51"
thiserror = { workspace = true }

View File

@ -1,47 +1,38 @@
#![allow(missing_docs)] //!
use keyfork_prompt::default_handler; use std::io::{stdin, stdout};
#[derive(PartialEq, Eq, Debug, Copy, Clone)] use keyfork_prompt::{
pub enum Choices { validators::{mnemonic, Validator},
Retry, Terminal, PromptHandler,
Continue, };
}
impl std::fmt::Display for Choices { use keyfork_mnemonic_util::English;
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Choices::Retry => write!(
f,
"Retry with some really long text that I want to cause issues with."
),
Choices::Continue => write!(
f,
"Continue with some really long text that I want to cause issues with."
),
}
}
}
impl keyfork_prompt::Choice for Choices {
fn identifier(&self) -> Option<char> {
Some(match self {
Choices::Retry => 'r',
Choices::Continue => 'c',
})
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut handler = default_handler()?; let mut mgr = Terminal::new(stdin(), stdout())?;
let transport_validator = mnemonic::MnemonicSetValidator {
word_lengths: [9, 24],
};
let combine_validator = mnemonic::MnemonicSetValidator {
word_lengths: [24, 48],
};
let choice = keyfork_prompt::prompt_choice( let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
&mut *handler, "Enter a 9-word and 24-word mnemonic: ",
"Here are some options!", 3,
&[Choices::Retry, Choices::Continue], transport_validator.to_fn(),
); )?;
assert_eq!(mnemonics[0].as_bytes().len(), 12);
assert_eq!(mnemonics[1].as_bytes().len(), 32);
dbg!(&choice); let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
"Enter a 24 and 48-word mnemonic: ",
3,
combine_validator.to_fn(),
)?;
assert_eq!(mnemonics[0].as_bytes().len(), 32);
assert_eq!(mnemonics[1].as_bytes().len(), 64);
Ok(()) Ok(())
} }

View File

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

View File

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

View File

@ -1,9 +1,3 @@
//! A terminal prompt handler.
//!
//! This prompt handler uses a raw terminal device to read inputs and uses ANSI escape codes to
//! provide formatting for prompts. Because of these reasons, it is not intended to be
//! machine-readable.
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write}, io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
@ -19,9 +13,7 @@ use keyfork_crossterm::{
ExecutableCommand, QueueableCommand, ExecutableCommand, QueueableCommand,
}; };
use keyfork_bug::bug; use crate::{Error, Message, PromptHandler, Wordlist};
use crate::{BoxResult, Choice, Error, Message, PromptHandler};
#[allow(missing_docs)] #[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -128,27 +120,9 @@ where
W: Write + AsRawFd, W: Write + AsRawFd,
{ {
fn drop(&mut self) { fn drop(&mut self) {
self.write self.write.execute(DisableBracketedPaste).unwrap();
.execute(cursor::EnableBlinking) self.write.execute(LeaveAlternateScreen).unwrap();
.expect(bug!("can't enable blinking")) self.terminal.disable_raw_mode().unwrap();
.execute(cursor::Show)
.expect(bug!("can't show cursor"))
.execute(DisableBracketedPaste)
.expect(bug!("can't restore bracketed paste"));
self.terminal
.disable_raw_mode()
.expect(bug!("can't disable raw mode"));
// we don't want to clear error messages
if !std::thread::panicking() {
self.write
.queue(LeaveAlternateScreen)
.expect(bug!("can't leave alternate screen"))
.queue(terminal::Clear(terminal::ClearType::All))
.expect(bug!("can't clear screen"))
.queue(cursor::MoveTo(0, 0))
.expect(bug!("can't move to origin"));
}
self.write.flush().expect(bug!("can't execute terminal reset commands"));
} }
} }
@ -190,194 +164,43 @@ where
W: Write + AsRawFd + Sized, W: Write + AsRawFd + Sized,
{ {
fn prompt_input(&mut self, prompt: &str) -> Result<String> { fn prompt_input(&mut self, prompt: &str) -> Result<String> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?; let mut terminal = self.lock().alternate_screen()?;
terminal terminal
.queue(terminal::Clear(terminal::ClearType::All))? .queue(terminal::Clear(terminal::ClearType::All))?
.queue(cursor::MoveTo(0, 0))?; .queue(cursor::MoveTo(0, 0))?;
let mut lines = prompt.lines().peekable(); let mut lines = prompt.lines().peekable();
let mut prefix_length = 0;
while let Some(line) = lines.next() { while let Some(line) = lines.next() {
prefix_length = line.len();
terminal.queue(Print(line))?; terminal.queue(Print(line))?;
if lines.peek().is_some() { if lines.peek().is_some() {
terminal.queue(cursor::MoveToNextLine(1))?;
}
}
terminal.flush()?;
let (mut cols, mut _rows) = terminal.size()?;
let mut input = String::new();
loop {
let input_len = input.len();
match read()? {
Event::Resize(new_cols, new_rows) => {
cols = new_cols;
_rows = new_rows;
}
Event::Key(k) => match k.code {
KeyCode::Enter => {
break;
}
KeyCode::Backspace => {
if input.pop().is_some() && prefix_length + input_len < cols as usize {
terminal terminal
.queue(cursor::MoveLeft(1))? .queue(cursor::MoveDown(1))?
.queue(Print(" "))?
.queue(cursor::MoveLeft(1))?
.flush()?;
}
}
KeyCode::Char('w') if k.modifiers.contains(KeyModifiers::CONTROL) => {
let mut has_deleted_text = true;
while input.pop().is_some_and(char::is_whitespace) {
has_deleted_text = false;
}
while input.pop().is_some_and(|c| !c.is_whitespace()) {
has_deleted_text = true;
}
if !input.is_empty() && has_deleted_text {
input.push(' ');
}
}
KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => {
return Err(Error::CtrlC);
}
KeyCode::Char(c) => {
input.push(c);
}
_ => (),
},
_ => (),
}
let printable_start = if prefix_length + input.len() < cols as usize {
0
} else {
let printable_space = (cols as usize) - prefix_length;
input.len() - (printable_space - 1)
};
terminal
.queue(cursor::MoveToColumn(prefix_length as u16))?
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
.queue(Print(&input[printable_start..]))?
.flush()?;
}
Ok(input)
}
fn prompt_choice_num(&mut self, prompt: &str, choices: &[Box<dyn Choice>]) -> Result<usize> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
terminal
.queue(terminal::Clear(terminal::ClearType::All))?
.queue(cursor::MoveTo(0, 0))?
.queue(cursor::Hide)?;
for line in prompt.lines() {
terminal
.queue(Print(line))?
.queue(cursor::MoveToNextLine(1))?;
terminal.flush()?;
}
let mut active_choice = 0;
let mut drawn = false;
loop {
let (cols, rows) = terminal.size()?;
// all choices, plus their padding, plus the spacing between, minus whitespace at end.
let max_size = choices
.iter()
.fold(0usize, |agg, choice| agg + choice.to_string().len() + 2)
+ std::cmp::max(choices.len(), 1)
- 1;
let horizontal = max_size < cols.into();
keyfork_bug::assert!(
horizontal || usize::from(rows) > prompt.lines().count() + choices.len(),
"screen too small, can't fit choices on {rows}x{cols}",
);
if horizontal {
terminal.queue(cursor::MoveToColumn(0))?;
} else if drawn {
terminal
.queue(cursor::MoveUp(
choices
.len()
.saturating_sub(1)
.try_into()
.expect(keyfork_bug::bug!("more than {} choices provided", u16::MAX)),
))?
.queue(cursor::MoveToColumn(0))?; .queue(cursor::MoveToColumn(0))?;
} else {
drawn = true;
}
let mut iter = choices.iter().enumerate().peekable();
while let Some((i, choice)) = iter.next() {
// if active choice, flip foreground and background
// if active choice, wrap in []
// if not, wrap in spaces, to preserve spacing and prevent redraws
if i == active_choice {
terminal.queue(PrintStyledContent(Stylize::reverse(format!("[{choice}]"))))?;
} else {
terminal.queue(Print(format!(" {choice} ")))?;
}
if iter.peek().is_some() {
if horizontal {
terminal.queue(Print(" "))?;
} else {
terminal.queue(cursor::MoveToNextLine(1))?;
}
} }
} }
terminal.flush()?; terminal.flush()?;
if let Event::Key(k) = read()? { let mut line = String::new();
match k.code { terminal.read.read_line(&mut line)?;
KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => { Ok(line)
return Err(Error::CtrlC);
}
KeyCode::Char(c) => {
for (i, choice) in choices.iter().enumerate() {
if choice.identifier().is_some_and(|id| id == c) {
active_choice = i;
}
}
}
KeyCode::Left | KeyCode::Up => {
active_choice = active_choice.saturating_sub(1);
}
KeyCode::Right | KeyCode::Down => match choices.len().saturating_sub(active_choice) {
0 | 1 => {}
_ => {
active_choice += 1;
}
},
KeyCode::Enter => {
return Ok(active_choice);
}
_ => {}
}
}
}
} }
fn prompt_validated_wordlist( #[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
wordlist: &[&str], validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
validator_fn: &mut dyn FnMut(String) -> BoxResult, ) -> Result<V, Error>
) -> Result<(), Error> { where
X: Wordlist,
{
let mut last_error = None; let mut last_error = None;
for _ in 0..retries { for _ in 0..retries {
let s = self.prompt_wordlist(prompt, wordlist)?; let s = self.prompt_wordlist::<X>(prompt)?;
match validator_fn(s) { match validator_fn(s) {
Ok(v) => return Ok(v), Ok(v) => return Ok(v),
Err(e) => { Err(e) => {
self.prompt_message(Message::Text(format!("Error validating wordlist: {e}")))?; self.prompt_message(&Message::Text(format!("Error validating wordlist: {e}")))?;
let _ = last_error.insert(e); let _ = last_error.insert(e);
} }
} }
@ -390,8 +213,15 @@ where
)) ))
} }
#[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &[&str]) -> Result<String> { fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
where
X: Wordlist,
{
let wordlist = X::get_singleton();
let words = wordlist.to_str_array();
let mut terminal = self let mut terminal = self
.lock() .lock()
.alternate_screen()? .alternate_screen()?
@ -407,7 +237,9 @@ where
prefix_length = line.len(); prefix_length = line.len();
terminal.queue(Print(line))?; terminal.queue(Print(line))?;
if lines.peek().is_some() { if lines.peek().is_some() {
terminal.queue(cursor::MoveToNextLine(1))?; terminal
.queue(cursor::MoveDown(1))?
.queue(cursor::MoveToColumn(0))?;
} }
} }
terminal.flush()?; terminal.flush()?;
@ -454,20 +286,6 @@ where
} }
KeyCode::Char(c) => { KeyCode::Char(c) => {
input.push(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 wordlist.iter().filter(|word| word.len() >= 4) {
if word[..4] == steel_word {
input.push_str(&word[4..]);
input.push(' ');
}
}
}
}
}
} }
_ => (), _ => (),
}, },
@ -505,7 +323,7 @@ where
let mut iter = printable_input.split_whitespace().peekable(); let mut iter = printable_input.split_whitespace().peekable();
while let Some(word) = iter.next() { while let Some(word) = iter.next() {
if wordlist.contains(&word) { if words.contains(&word) {
terminal.queue(PrintStyledContent(word.green()))?; terminal.queue(PrintStyledContent(word.green()))?;
} else { } else {
terminal.queue(PrintStyledContent(word.red()))?; terminal.queue(PrintStyledContent(word.red()))?;
@ -526,19 +344,19 @@ where
Ok(input) Ok(input)
} }
fn prompt_validated_passphrase( fn prompt_validated_passphrase<V>(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: &mut dyn FnMut(String) -> BoxResult, validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<(), Error> { ) -> Result<V, Error> {
let mut last_error = None; let mut last_error = None;
for _ in 0..retries { for _ in 0..retries {
let s = self.prompt_passphrase(prompt)?; let s = self.prompt_passphrase(prompt)?;
match validator_fn(s) { match validator_fn(s) {
Ok(v) => return Ok(v), Ok(v) => return Ok(v),
Err(e) => { Err(e) => {
self.prompt_message(Message::Text(format!( self.prompt_message(&Message::Text(format!(
"Error validating passphrase: {e}" "Error validating passphrase: {e}"
)))?; )))?;
let _ = last_error.insert(e); let _ = last_error.insert(e);
@ -566,7 +384,9 @@ where
prefix_length = line.len(); prefix_length = line.len();
terminal.queue(Print(line))?; terminal.queue(Print(line))?;
if lines.peek().is_some() { if lines.peek().is_some() {
terminal.queue(cursor::MoveToNextLine(1))?; terminal
.queue(cursor::MoveDown(1))?
.queue(cursor::MoveToColumn(0))?;
} }
} }
terminal.flush()?; terminal.flush()?;
@ -613,7 +433,7 @@ where
Ok(passphrase) Ok(passphrase)
} }
fn prompt_message(&mut self, prompt: Message) -> Result<()> { fn prompt_message(&mut self, prompt: impl Borrow<Message>) -> Result<()> {
let mut terminal = self.lock().alternate_screen()?.raw_mode()?; let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
loop { loop {
@ -632,17 +452,21 @@ where
let len = std::cmp::min(u16::MAX as usize, word.len()) as u16; let len = std::cmp::min(u16::MAX as usize, word.len()) as u16;
written_chars += len + 1; written_chars += len + 1;
if written_chars > cols { if written_chars > cols {
terminal.queue(cursor::MoveToNextLine(1))?; terminal
.queue(cursor::MoveDown(1))?
.queue(cursor::MoveToColumn(0))?;
written_chars = len + 1; written_chars = len + 1;
} }
terminal.queue(Print(word))?.queue(Print(" "))?; terminal.queue(Print(word))?.queue(Print(" "))?;
} }
terminal.queue(cursor::MoveToNextLine(1))?; terminal
.queue(cursor::MoveDown(1))?
.queue(cursor::MoveToColumn(0))?;
} }
} }
Message::Data(data) => { Message::Data(data) => {
let count = data.lines().count(); let count = data.lines().count();
// NOTE: GE to allow a MoveToNextLine(1) // NOTE: GE to allow a MoveDown(1)
if count >= rows as usize { if count >= rows as usize {
let msg = format!( let msg = format!(
"{} {count} {} {rows} {}", "{} {count} {} {rows} {}",
@ -650,12 +474,14 @@ where
); );
terminal terminal
.queue(Print(msg))? .queue(Print(msg))?
.queue(cursor::MoveToNextLine(1))?; .queue(cursor::MoveDown(1))?
.queue(cursor::MoveToColumn(0))?;
} else { } else {
for line in data.lines() { for line in data.lines() {
terminal terminal
.queue(Print(line))? .queue(Print(line))?
.queue(cursor::MoveToNextLine(1))?; .queue(cursor::MoveDown(1))?
.queue(cursor::MoveToColumn(0))?;
} }
} }
} }
@ -677,6 +503,7 @@ where
_ => (), _ => (),
} }
} }
terminal.queue(cursor::EnableBlinking)?.flush()?;
Ok(()) Ok(())
} }
} }

View File

@ -29,84 +29,6 @@ pub enum PinError {
/// The PIN contained invalid characters. /// The PIN contained invalid characters.
#[error("PIN contained invalid characters (found {0} at position {1})")] #[error("PIN contained invalid characters (found {0} at position {1})")]
InvalidCharacters(char, usize), 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. /// Validate that a PIN is of a certain length and matches a range of characters.
@ -157,8 +79,7 @@ pub mod mnemonic {
use super::Validator; use super::Validator;
use keyfork_bug::bug; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
use keyfork_mnemonic::{Mnemonic, MnemonicFromStrError};
/// A mnemonic could not be validated from the given input. /// A mnemonic could not be validated from the given input.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -316,7 +237,7 @@ pub mod mnemonic {
Ok(output Ok(output
.try_into() .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" license = "MIT"
[dependencies] [dependencies]
smex = { workspace = true } smex = { version = "0.1.0", path = "../smex" }

101
deny.toml
View File

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

View File

@ -33,4 +33,3 @@
- [Provisioners](./dev-guide/provisioners.md) - [Provisioners](./dev-guide/provisioners.md)
- [Auditing Dependencies](./dev-guide/auditing.md) - [Auditing Dependencies](./dev-guide/auditing.md)
- [Entropy Guide](./dev-guide/entropy.md) - [Entropy Guide](./dev-guide/entropy.md)
- [The Shard Protocol](./dev-guide/shard-protocol.md)

View File

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

View File

@ -1,39 +0,0 @@
# The Shard Protocol
Keyfork Shard uses a single-handshake protocol to transfer encrypted shards.
The initial payload is generated by the program combining the shards, while the
response is generated by the program transport-encrypting the shards.
## Combiner Payload
The combiner payload consists of a 12-byte nonce and a 32-byte x25519 public
key. The payload is then either encoded to hex and displayed as a QR code, and
encoded as a mnemonic and printed on-screen.
```
[12-byte nonce | 32-byte public key]
```
The transporter receives the 12-byte nonce and 32-byte x25519 key and generates
their own x25519 key. Using HKDF-Sha256 with no salt on the resulting key
generates the AES-256-GCM key used to encrypt the now-decrypted shard, along
with the received nonce.
## Transporter Payload
The transporter payload consists of a 32-byte x25519 public key and a
64-byte-padded encrypted "hunk". The hunk contains a version byte, a threshold
byte, and the encrypted shard. The last byte of the 64-byte sequence is the
total length of the encrypted hunk.
```
Handshake:
[32-byte public key | 63-byte-padded encrypted hunk | 1-byte hunk length ]
Hunk:
[1-byte version | 1-byte threshold | variable-length shard ]
```
The combiner receives the 32-byte x25519 key and the 64-byte hunk, and uses the
same key derivation scheme as above to generate the decryption key. The
threshold byte is used to determine how many shares (in total) are needed.

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