Compare commits
59 Commits
keyforkd-c
...
main
Author | SHA1 | Date |
---|---|---|
|
4714f616ea | |
|
71bd819797 | |
|
fc930399de | |
|
e7a776f59f | |
|
672cc6a699 | |
|
df552250ba | |
|
0cb96782ef | |
|
625e8e490b | |
|
c6e274c4da | |
|
18773d351c | |
|
2083eb216f | |
|
f63b686e70 | |
|
0737ca6907 | |
|
acdf894558 | |
|
9b2a8a5967 | |
|
e7be91bdd4 | |
|
739921d915 | |
|
64c75085f4 | |
|
00e35bcb7d | |
|
d0019a93f0 | |
|
020fa4d25e | |
|
76ca4b0812 | |
|
53665cac2e | |
|
a1c3d52c14 | |
|
674e2e93c5 | |
|
88a05f23ac | |
|
98b9dbb811 | |
|
723194fdd7 | |
|
db19b30bfe | |
|
0243212c80 | |
|
083eb16b39 | |
|
aa8526cda0 | |
|
e1c3e38fc7 | |
|
4e342ac7a9 | |
|
35e0eb57a0 | |
|
c232828290 | |
|
8756c3d233 | |
|
c95ed0b729 | |
|
19fbb51d12 | |
|
adb5293f1d | |
|
a233686996 | |
|
7fab63c1ae | |
|
503c6fa0b4 | |
|
c46f9e48b7 | |
|
f8db8702ce | |
|
92dde3dcee | |
|
d7bf3d16e1 | |
|
9e4d5649d9 | |
|
6a3244df01 | |
|
be6d562b33 | |
|
6317cc964f | |
|
305e070b93 | |
|
7e5c7ea8fb | |
|
63b4677b19 | |
|
1d68dd19d9 | |
|
4ab1e8afa6 | |
|
a8b2814b17 | |
|
c36fe0a1b1 | |
|
c25c11d1a0 |
|
@ -0,0 +1 @@
|
|||
audits filter=lfs diff=lfs merge=lfs -text
|
411
CHANGELOG.md
411
CHANGELOG.md
|
@ -1,3 +1,414 @@
|
|||
# Keyfork v0.3.3
|
||||
|
||||
This release introduces a checksum verification mechanism for Remote Shard.
|
||||
|
||||
### Changes in keyfork-prompt:
|
||||
|
||||
```
|
||||
e7be91b keyfork-{shard,prompt}: add Yes/No prompt for verifying QR codes
|
||||
```
|
||||
|
||||
### Changes in keyfork-shard:
|
||||
|
||||
```
|
||||
e7be91b keyfork-{shard,prompt}: add Yes/No prompt for verifying QR codes
|
||||
739921d WIP: add checksum to shard
|
||||
```
|
||||
|
||||
|
||||
# Keyfork v0.3.2
|
||||
|
||||
This is another bugfix release, allowing the derivation of Shard keys.
|
||||
|
||||
|
||||
### Changes in keyfork:
|
||||
|
||||
```
|
||||
6ffcdc3 add derivation path for Shard keys
|
||||
```
|
||||
|
||||
# Keyfork v0.3.1
|
||||
|
||||
This is a bugfix release, resolving an issue with Keyfork Shard not having a
|
||||
exit condition for when a valid QR code was scanned.
|
||||
|
||||
### Changes in keyfork-shard:
|
||||
|
||||
```
|
||||
d0019a9 keyfork-shard: break loop when receiving valid QR code
|
||||
```
|
||||
|
||||
# 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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
46
Cargo.toml
46
Cargo.toml
|
@ -16,36 +16,58 @@ members = [
|
|||
"crates/qrcode/keyfork-zbar-sys",
|
||||
"crates/util/keyfork-bin",
|
||||
"crates/util/keyfork-bug",
|
||||
"crates/util/keyfork-crossterm",
|
||||
"crates/util/keyfork-crossterm-ioctl-shim",
|
||||
"crates/util/keyfork-entropy",
|
||||
"crates/util/keyfork-frame",
|
||||
"crates/util/keyfork-mnemonic",
|
||||
"crates/util/keyfork-prompt",
|
||||
"crates/util/keyfork-slip10-test-data",
|
||||
"crates/util/smex",
|
||||
"crates/tests",
|
||||
]
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing_docs = { level = "warn" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "deny", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
||||
missing_errors_doc = { level = "warn" }
|
||||
missing_panics_doc = { level = "warn" }
|
||||
|
||||
# used often in tests
|
||||
wildcard_imports = { level = "allow"}
|
||||
|
||||
# annoying
|
||||
must_use_candidate = "allow"
|
||||
return_self_not_must_use = "allow"
|
||||
|
||||
# sometimes i like the logical flow of keeping things in an "else"
|
||||
redundant_else = "allow"
|
||||
|
||||
# i hate using `.unwrap_or_else(|| keyfork_bug::bug!())`
|
||||
expect_fun_call = "allow"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
# Keyfork dependencies
|
||||
keyforkd = { version = "0.1.1", path = "crates/daemon/keyforkd", registry = "distrust", default-features = false }
|
||||
keyforkd-client = { version = "0.2.0", path = "crates/daemon/keyforkd-client", registry = "distrust", default-features = false }
|
||||
keyforkd-models = { version = "0.2.0", path = "crates/daemon/keyforkd-models", registry = "distrust", default-features = false }
|
||||
keyfork-derive-key = { version = "0.1.1", path = "crates/derive/keyfork-derive-key", registry = "distrust", default-features = false }
|
||||
keyfork-derive-openpgp = { version = "0.1.2", path = "crates/derive/keyfork-derive-openpgp", registry = "distrust", default-features = false }
|
||||
keyfork-derive-path-data = { version = "0.1.1", path = "crates/derive/keyfork-derive-path-data", registry = "distrust", default-features = false }
|
||||
keyfork-derive-util = { version = "0.2.0", path = "crates/derive/keyfork-derive-util", registry = "distrust", default-features = false }
|
||||
keyfork-shard = { version = "0.2.2", path = "crates/keyfork-shard", registry = "distrust", default-features = false }
|
||||
keyfork-shard = { version = "0.3.4", 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-bug = { version = "0.1.1", path = "crates/util/keyfork-bug", 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.3.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false }
|
||||
keyfork-prompt = { version = "0.1.1", path = "crates/util/keyfork-prompt", registry = "distrust", default-features = false }
|
||||
keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false }
|
||||
keyfork-prompt = { version = "0.2.3", 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 }
|
||||
|
||||
|
@ -70,13 +92,17 @@ serde_json = "1.0.111"
|
|||
|
||||
# Misc.
|
||||
anyhow = "1.0.79"
|
||||
hex-literal = "0.4.1"
|
||||
image = { version = "0.24.8", default-features = false }
|
||||
hex-literal = "1.0.0"
|
||||
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]
|
||||
opt-level = 3
|
||||
debug = true
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# 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.
|
2
Makefile
2
Makefile
|
@ -34,7 +34,7 @@ review:
|
|||
$(eval HEAD_REF_PARSED := $(shell git rev-parse $(HEAD_REF)))
|
||||
@echo "Ensuring current HEAD_REF is not BASE_REF"
|
||||
test "$(BASE_REF_PARSED)" != "$(HEAD_REF_PARSED)"
|
||||
@echo "Verifying if any changes happened in Cargo.lock that require review; otherwise, use `git difftool` directly"
|
||||
@echo "Verifying if any changes happened in Cargo.lock that require review; otherwise, use: git difftool"
|
||||
test "$(shell git show $(BASE_REF_PARSED):Cargo.lock | sha256sum)" != "$(shell git show $(HEAD_REF_PARSED):Cargo.lock | sha256sum)"
|
||||
$(eval TEMP_REPO := $(shell mktemp -d))
|
||||
$(call clone-repo,$(TEMP_REPO),$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))),$(BASE_REF_PARSED))
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,92 @@
|
|||
# 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"
|
|
@ -0,0 +1 @@
|
|||
doc-valid-idents = ["OpenPGP", ".."]
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "keyforkd-client"
|
||||
version = "0.2.0"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
//!
|
||||
//! ### Request: Derive Key
|
||||
//!
|
||||
//! The client creates a derivation path of at least two indices and requests a derived XPrv
|
||||
//! The client creates a derivation path of at least two indices and requests a derived `XPrv`
|
||||
//! (Extended Private Key) from the server.
|
||||
//!
|
||||
//! ```rust
|
||||
|
@ -68,7 +68,7 @@
|
|||
//! ## Extended Private Keys
|
||||
//!
|
||||
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
|
||||
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
|
||||
//! Private Key (often shortened to `XPrv`) has been created, further derivations can be performed.
|
||||
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
|
||||
//! will be derived similarly between the server and the client.
|
||||
//!
|
||||
|
@ -117,7 +117,7 @@
|
|||
//!
|
||||
//! ## Testing Infrastructure
|
||||
//!
|
||||
//! In tests, the `keyforkd::test_util` module and TestPrivateKeys can be used. These provide
|
||||
//! 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,
|
||||
|
@ -199,6 +199,10 @@ pub enum Error {
|
|||
#[error("Socket was unable to connect to {1}: {0} (make sure keyforkd is running)")]
|
||||
Connect(std::io::Error, PathBuf),
|
||||
|
||||
/// The path of the derived key was of an invalid length.
|
||||
#[error("Derived key path is of invalid length")]
|
||||
InvalidPathLength(#[from] std::num::TryFromIntError),
|
||||
|
||||
/// Data could not be written to, or read from, the socket.
|
||||
#[error("Could not write to or from the socket: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
@ -237,19 +241,15 @@ pub fn get_socket() -> Result<UnixStream, Error> {
|
|||
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
||||
.collect::<HashMap<String, String>>();
|
||||
let mut socket_path: PathBuf;
|
||||
#[allow(clippy::single_match_else)]
|
||||
match socket_vars.get("KEYFORKD_SOCKET_PATH") {
|
||||
Some(occupied) => {
|
||||
socket_path = PathBuf::from(occupied);
|
||||
}
|
||||
None => {
|
||||
socket_path = PathBuf::from(
|
||||
socket_vars
|
||||
.get("XDG_RUNTIME_DIR")
|
||||
.ok_or(Error::EnvVarsNotFound)?,
|
||||
);
|
||||
socket_path.extend(["keyforkd", "keyforkd.sock"]);
|
||||
}
|
||||
if let Some(occupied) = socket_vars.get("KEYFORKD_SOCKET_PATH") {
|
||||
socket_path = PathBuf::from(occupied);
|
||||
} else {
|
||||
socket_path = PathBuf::from(
|
||||
socket_vars
|
||||
.get("XDG_RUNTIME_DIR")
|
||||
.ok_or(Error::EnvVarsNotFound)?,
|
||||
);
|
||||
socket_path.extend(["keyforkd", "keyforkd.sock"]);
|
||||
}
|
||||
UnixStream::connect(&socket_path).map_err(|e| Error::Connect(e, socket_path))
|
||||
}
|
||||
|
@ -266,7 +266,7 @@ pub struct Client {
|
|||
|
||||
impl Client {
|
||||
/// Create a new client from a given already-connected [`UnixStream`]. This function is
|
||||
/// provided in case a specific UnixStream has to be used; otherwise,
|
||||
/// provided in case a specific `UnixStream` has to be used; otherwise,
|
||||
/// [`Client::discover_socket`] should be preferred.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -344,7 +344,7 @@ impl Client {
|
|||
return Err(Error::InvalidResponse);
|
||||
}
|
||||
|
||||
let depth = path.len() as u8;
|
||||
let depth = u8::try_from(path.len())?;
|
||||
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code)
|
||||
.map_err(|_| Error::InvalidKey)
|
||||
}
|
||||
|
|
|
@ -9,58 +9,58 @@ use std::{os::unix::net::UnixStream, str::FromStr};
|
|||
fn secp256k1_test_suite() {
|
||||
use k256::SecretKey;
|
||||
|
||||
let tests = test_data()
|
||||
.unwrap()
|
||||
.remove(&"secp256k1".to_string())
|
||||
.unwrap();
|
||||
let tests = test_data().unwrap().remove("secp256k1").unwrap();
|
||||
|
||||
for seed_test in tests {
|
||||
let seed = seed_test.seed;
|
||||
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
|
||||
for test in seed_test.tests {
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let chain = DerivationPath::from_str(test.chain).unwrap();
|
||||
let chain_len = chain.len();
|
||||
if chain_len < 2 {
|
||||
continue;
|
||||
}
|
||||
if chain.iter().take(2).any(|index| !index.is_hardened()) {
|
||||
continue;
|
||||
}
|
||||
// Consistency check: ensure the server and the client can each derive the same
|
||||
// key using an XPrv, for all but the last XPrv, which is verified after this
|
||||
for i in 2..chain_len {
|
||||
// FIXME: Keyfork will only allow one request per session
|
||||
run_test(
|
||||
&seed,
|
||||
move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
|
||||
for test in seed_test.tests {
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let path = DerivationPath::from_str(test.chain).unwrap();
|
||||
let left_path = path.inner()[..i]
|
||||
.iter()
|
||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||
let right_path = path.inner()[i..]
|
||||
.iter()
|
||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
|
||||
let derived_xprv = xprv.derive_path(&right_path).unwrap();
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
|
||||
assert_eq!(
|
||||
derived_xprv, keyforkd_xprv,
|
||||
"{left_path} + {right_path} != {path}"
|
||||
let chain = DerivationPath::from_str(test.chain).unwrap();
|
||||
let chain_len = chain.len();
|
||||
if chain_len < 2 {
|
||||
continue;
|
||||
}
|
||||
if chain.iter().take(2).any(|index| !index.is_hardened()) {
|
||||
continue;
|
||||
}
|
||||
// Consistency check: ensure the server and the client can each derive the same
|
||||
// key using an XPrv, for all but the last XPrv, which is verified after this
|
||||
for i in 2..chain_len {
|
||||
// FIXME: Keyfork will only allow one request per session
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let path = DerivationPath::from_str(test.chain).unwrap();
|
||||
let left_path = path.inner()[..i]
|
||||
.iter()
|
||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||
let right_path = path.inner()[i..]
|
||||
.iter()
|
||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
|
||||
let derived_xprv = xprv.derive_path(&right_path).unwrap();
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
|
||||
assert_eq!(
|
||||
derived_xprv, keyforkd_xprv,
|
||||
"{left_path} + {right_path} != {path}"
|
||||
);
|
||||
}
|
||||
let req = DerivationRequest::new(
|
||||
DerivationAlgorithm::Secp256k1,
|
||||
&DerivationPath::from_str(test.chain).unwrap(),
|
||||
);
|
||||
let response =
|
||||
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
|
||||
assert_eq!(&response.data, test.private_key.as_slice());
|
||||
}
|
||||
let req = DerivationRequest::new(
|
||||
DerivationAlgorithm::Secp256k1,
|
||||
&DerivationPath::from_str(test.chain).unwrap(),
|
||||
);
|
||||
let response =
|
||||
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
|
||||
assert_eq!(&response.data, test.private_key.as_slice());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ fn secp256k1_test_suite() {
|
|||
fn ed25519_test_suite() {
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
||||
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
||||
|
||||
for seed_test in tests {
|
||||
let seed = seed_test.seed;
|
||||
|
|
|
@ -4,6 +4,9 @@ version = "0.2.0"
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "keyforkd"
|
||||
version = "0.1.1"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
|
@ -27,12 +30,12 @@ tokio = { workspace = true, features = ["io-util", "macros", "rt", "io-std", "ne
|
|||
tracing = { version = "0.1.37", optional = true }
|
||||
tracing-error = { version = "0.2.0", optional = true }
|
||||
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
|
||||
tower = { version = "0.4.13", features = ["tokio", "util"] }
|
||||
tower = { version = "0.5.0", features = ["tokio", "util"], default-features = false }
|
||||
|
||||
# Personally audited
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tempfile = { version = "3.10.0", default-features = false }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = { workspace = true }
|
||||
|
|
|
@ -89,27 +89,23 @@ pub async fn start_and_run_server(mnemonic: Mnemonic) -> Result<(), Box<dyn std:
|
|||
let runtime_vars = std::env::vars()
|
||||
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
||||
.collect::<HashMap<String, String>>();
|
||||
let mut runtime_path: PathBuf;
|
||||
#[allow(clippy::single_match_else)]
|
||||
match runtime_vars.get("KEYFORKD_SOCKET_PATH") {
|
||||
Some(occupied) => {
|
||||
runtime_path = PathBuf::from(occupied);
|
||||
let runtime_path = if let Some(occupied) = runtime_vars.get("KEYFORKD_SOCKET_PATH") {
|
||||
PathBuf::from(occupied)
|
||||
} else {
|
||||
let mut runtime_path = PathBuf::from(
|
||||
runtime_vars
|
||||
.get("XDG_RUNTIME_DIR")
|
||||
.ok_or(KeyforkdError::NoSocketPath)?,
|
||||
);
|
||||
runtime_path.push("keyforkd");
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!("ensuring directory exists: {}", runtime_path.display());
|
||||
if !runtime_path.is_dir() {
|
||||
tokio::fs::create_dir(&runtime_path).await?;
|
||||
}
|
||||
None => {
|
||||
runtime_path = PathBuf::from(
|
||||
runtime_vars
|
||||
.get("XDG_RUNTIME_DIR")
|
||||
.ok_or(KeyforkdError::NoSocketPath)?,
|
||||
);
|
||||
runtime_path.push("keyforkd");
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!("ensuring directory exists: {}", runtime_path.display());
|
||||
if !runtime_path.is_dir() {
|
||||
tokio::fs::create_dir(&runtime_path).await?;
|
||||
}
|
||||
runtime_path.push("keyforkd.sock");
|
||||
}
|
||||
}
|
||||
runtime_path.push("keyforkd.sock");
|
||||
runtime_path
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//!
|
||||
//! Launch the Keyfork Server from using a mnemonic passed through standard input.
|
||||
|
||||
use keyfork_mnemonic::Mnemonic;
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ pub struct BincodeLayer<'a, Request> {
|
|||
phantom_request: PhantomData<&'a Request>,
|
||||
}
|
||||
|
||||
impl<'a, Request> BincodeLayer<'a, Request> {
|
||||
impl<Request> BincodeLayer<'_, Request> {
|
||||
/// Create a new [`BincodeLayer`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -21,7 +21,7 @@ impl<'a, Request> BincodeLayer<'a, Request> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Request> Default for BincodeLayer<'a, Request> {
|
||||
impl<Request> Default for BincodeLayer<'_, Request> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
|
@ -162,6 +162,9 @@ mod tests {
|
|||
.call(content.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result, serialize(&Result::<Test, Infallible>::Ok(test)).unwrap());
|
||||
assert_eq!(
|
||||
result,
|
||||
serialize(&Result::<Test, Infallible>::Ok(test)).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,8 @@ impl IsDisconnect for EncodeError {
|
|||
}
|
||||
|
||||
impl UnixServer {
|
||||
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also creates a ctrl_c handler to automatically clean up the socket file.
|
||||
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also
|
||||
/// creates a `ctrl_c` handler to automatically clean up the socket file.
|
||||
///
|
||||
/// # Errors
|
||||
/// This function may return an error if the socket can't be bound.
|
||||
|
|
|
@ -39,7 +39,10 @@ impl Keyforkd {
|
|||
/// Create a new instance of Keyfork from a given seed.
|
||||
pub fn new(seed: Vec<u8>) -> Self {
|
||||
if seed.len() < 16 {
|
||||
warn!("Entropy size is lower than 128 bits: {} bits.", seed.len() * 8);
|
||||
warn!(
|
||||
"Entropy size is lower than 128 bits: {} bits.",
|
||||
seed.len() * 8
|
||||
);
|
||||
}
|
||||
Self {
|
||||
seed: Arc::new(seed),
|
||||
|
@ -77,11 +80,11 @@ impl Service<Request> for Keyforkd {
|
|||
.iter()
|
||||
.take(2)
|
||||
.enumerate()
|
||||
.find(|(_, index)| {
|
||||
!index.is_hardened()
|
||||
})
|
||||
.find(|(_, index)| !index.is_hardened())
|
||||
{
|
||||
return Err(DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into())
|
||||
return Err(
|
||||
DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
|
@ -111,10 +114,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn properly_derives_secp256k1() {
|
||||
let tests = test_data()
|
||||
.unwrap()
|
||||
.remove(&"secp256k1".to_string())
|
||||
.unwrap();
|
||||
let tests = test_data().unwrap().remove("secp256k1").unwrap();
|
||||
|
||||
for per_seed in tests {
|
||||
let seed = &per_seed.seed;
|
||||
|
@ -146,7 +146,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn properly_derives_ed25519() {
|
||||
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
||||
|
||||
for per_seed in tests {
|
||||
let seed = &per_seed.seed;
|
||||
|
@ -174,7 +174,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[should_panic(expected = "InvalidDerivationLength(0)")]
|
||||
#[tokio::test]
|
||||
async fn errors_on_no_path() {
|
||||
let tests = [(
|
||||
|
@ -200,7 +200,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[should_panic(expected = "InvalidDerivationLength(1)")]
|
||||
#[tokio::test]
|
||||
async fn errors_on_short_path() {
|
||||
let tests = [(
|
||||
|
|
|
@ -26,7 +26,7 @@ pub enum UninstantiableError {}
|
|||
/// };
|
||||
/// assert!(closure().is_ok());
|
||||
/// ```
|
||||
pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
|
||||
pub type Panicable = std::result::Result<(), UninstantiableError>;
|
||||
|
||||
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
|
||||
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
|
||||
|
@ -62,9 +62,9 @@ pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
|
|||
/// }).unwrap();
|
||||
/// ```
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
|
||||
pub fn run_test<F, E>(seed: &[u8], closure: F) -> std::result::Result<(), E>
|
||||
where
|
||||
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
|
||||
F: FnOnce(&std::path::Path) -> std::result::Result<(), E> + Send + 'static,
|
||||
E: Send + 'static,
|
||||
{
|
||||
let rt = Builder::new_multi_thread()
|
||||
|
@ -89,8 +89,11 @@ where
|
|||
.expect(bug!("couldn't send server start signal"));
|
||||
let service = ServiceBuilder::new()
|
||||
.layer(middleware::BincodeLayer::new())
|
||||
.service(Keyforkd::new(seed.to_vec()));
|
||||
server.run(service).await.expect(bug!("Unable to start service"));
|
||||
.service(Keyforkd::new(seed.clone()));
|
||||
server
|
||||
.run(service)
|
||||
.await
|
||||
.expect(bug!("Unable to start service"));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "keyfork-derive-age"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# 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"
|
|
@ -0,0 +1,69 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "keyfork-derive-key"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
@ -46,7 +46,10 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let mut client = Client::discover_socket()?;
|
||||
let request = DerivationRequest::new(algo, &path);
|
||||
let response = client.request(&request.into())?;
|
||||
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
|
||||
println!(
|
||||
"{}",
|
||||
smex::encode(DerivationResponse::try_from(response)?.data)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "keyfork-derive-openpgp"
|
||||
version = "0.1.2"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
default = ["bin"]
|
||||
|
|
|
@ -19,8 +19,14 @@ use sequoia_openpgp::{
|
|||
Cert, Packet,
|
||||
};
|
||||
|
||||
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
|
||||
// something like Prv or PrvKey.
|
||||
|
||||
/// The private key type used with OpenPGP.
|
||||
pub type XPrvKey = SigningKey;
|
||||
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.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -68,7 +74,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|||
///
|
||||
/// # Errors
|
||||
/// 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() {
|
||||
Some(kf) if kf.for_certification() => kf,
|
||||
_ => return Err(Error::NotCert),
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
//!
|
||||
//! Query the Keyfork Servre to derive an OpenPGP Secret Key.
|
||||
|
||||
use std::{env, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_derive_util::DerivationPath;
|
||||
use keyfork_derive_path_data::paths;
|
||||
use keyfork_derive_util::DerivationPath;
|
||||
use keyforkd_client::Client;
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
@ -82,7 +82,10 @@ fn validate(
|
|||
let index = paths::OPENPGP.inner().first().unwrap();
|
||||
|
||||
let path = DerivationPath::from_str(path)?;
|
||||
assert!(path.len() >= 2, "Expected path of at least m/{index}/account_id'");
|
||||
assert!(
|
||||
path.len() >= 2,
|
||||
"Expected path of at least m/{index}/account_id'"
|
||||
);
|
||||
|
||||
let given_index = path.iter().next().expect("checked .len() above");
|
||||
assert_eq!(
|
||||
|
@ -117,11 +120,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.map(|kt| kt.inner().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cert = keyfork_derive_openpgp::derive(derived_xprv, subkeys.as_slice(), &default_userid)?;
|
||||
let cert = keyfork_derive_openpgp::derive(&derived_xprv, subkeys.as_slice(), &default_userid)?;
|
||||
|
||||
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
|
||||
|
||||
for packet in cert.into_packets2() {
|
||||
for packet in cert.as_tsk().into_packets() {
|
||||
packet.serialize(&mut w)?;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "keyfork-derive-path-data"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||
|
||||
/// All common paths for key derivation.
|
||||
pub mod paths {
|
||||
use super::*;
|
||||
|
||||
/// The default derivation path for OpenPGP.
|
||||
pub static OPENPGP: Lazy<DerivationPath> = Lazy::new(|| {
|
||||
pub static OPENPGP: LazyLock<DerivationPath> = LazyLock::new(|| {
|
||||
DerivationPath::default().chain_push(DerivationIndex::new_unchecked(
|
||||
u32::from_be_bytes(*b"\x00pgp"),
|
||||
true,
|
||||
|
@ -18,7 +19,7 @@ pub mod paths {
|
|||
});
|
||||
|
||||
/// The derivation path for OpenPGP certificates used for sharding.
|
||||
pub static OPENPGP_SHARD: Lazy<DerivationPath> = Lazy::new(|| {
|
||||
pub static OPENPGP_SHARD: LazyLock<DerivationPath> = LazyLock::new(|| {
|
||||
DerivationPath::default()
|
||||
.chain_push(DerivationIndex::new_unchecked(
|
||||
u32::from_be_bytes(*b"\x00pgp"),
|
||||
|
@ -31,7 +32,7 @@ pub mod paths {
|
|||
});
|
||||
|
||||
/// The derivation path for OpenPGP certificates used for disaster recovery.
|
||||
pub static OPENPGP_DISASTER_RECOVERY: Lazy<DerivationPath> = Lazy::new(|| {
|
||||
pub static OPENPGP_DISASTER_RECOVERY: LazyLock<DerivationPath> = LazyLock::new(|| {
|
||||
DerivationPath::default()
|
||||
.chain_push(DerivationIndex::new_unchecked(
|
||||
u32::from_be_bytes(*b"\x00pgp"),
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "keyfork-derive-util"
|
||||
version = "0.2.0"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
|
|
|
@ -41,9 +41,9 @@
|
|||
//! }
|
||||
//! ```
|
||||
|
||||
///
|
||||
#[allow(missing_docs)]
|
||||
pub mod private_key;
|
||||
///
|
||||
#[allow(missing_docs)]
|
||||
pub mod public_key;
|
||||
|
||||
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};
|
||||
|
|
|
@ -52,7 +52,7 @@ pub struct VariableLengthSeed<'a> {
|
|||
}
|
||||
|
||||
impl<'a> VariableLengthSeed<'a> {
|
||||
/// Create a new VariableLengthSeed.
|
||||
/// Create a new `VariableLengthSeed`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
|
@ -167,6 +167,7 @@ where
|
|||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Result<Self> {
|
||||
Self::new_internal(seed.as_private_key())
|
||||
}
|
||||
|
@ -189,7 +190,10 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
assert!(has_any_nonzero, bug!("hmac function returned all-zero master key"));
|
||||
assert!(
|
||||
has_any_nonzero,
|
||||
bug!("hmac function returned all-zero master key")
|
||||
);
|
||||
|
||||
Self::from_parts(
|
||||
private_key
|
||||
|
@ -223,13 +227,11 @@ where
|
|||
/// ```
|
||||
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
|
||||
match K::from_bytes(key) {
|
||||
Ok(key) => {
|
||||
Ok(Self {
|
||||
private_key: key,
|
||||
depth,
|
||||
chain_code,
|
||||
})
|
||||
}
|
||||
Ok(key) => Ok(Self {
|
||||
private_key: key,
|
||||
depth,
|
||||
chain_code,
|
||||
}),
|
||||
Err(_) => Err(Error::InvalidKey),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ mod tests {
|
|||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
#[should_panic(expected = "IndexTooLarge")]
|
||||
fn fails_on_high_index() {
|
||||
DerivationIndex::new(0x8000_0001, false).unwrap();
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
#[should_panic(expected = "IndexTooLarge")]
|
||||
fn from_str_fails_on_high_index() {
|
||||
DerivationIndex::from_str(&0x8000_0001u32.to_string()).unwrap();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod extended_key;
|
||||
|
@ -17,7 +17,10 @@ pub mod public_key;
|
|||
mod tests;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}};
|
||||
pub use crate::extended_key::{
|
||||
private_key::{Error as XPrvError, ExtendedPrivateKey, VariableLengthSeed},
|
||||
public_key::{Error as XPubError, ExtendedPublicKey},
|
||||
};
|
||||
|
||||
pub use crate::{
|
||||
index::{DerivationIndex, Error as IndexError},
|
||||
|
|
|
@ -11,7 +11,7 @@ pub enum Error {
|
|||
|
||||
/// The path could not be parsed due to a bad prefix. Paths must be in the format:
|
||||
///
|
||||
/// m [/ index [']]+
|
||||
/// `m [/ index [']]+`
|
||||
///
|
||||
/// The prefix for the path must be `m`, and all indices must be integers between 0 and
|
||||
/// 2^31.
|
||||
|
@ -35,8 +35,8 @@ impl DerivationPath {
|
|||
self.path.iter()
|
||||
}
|
||||
|
||||
/// The amount of segments in the DerivationPath. For consistency, a [`usize`] is returned, but
|
||||
/// BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
|
||||
/// The amount of segments in the [`DerivationPath`]. For consistency, a [`usize`] is returned,
|
||||
/// but BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
|
||||
pub fn len(&self) -> usize {
|
||||
self.path.len()
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ mod tests {
|
|||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
#[should_panic(expected = "UnknownPathPrefix")]
|
||||
fn requires_master_path() {
|
||||
DerivationPath::from_str("1234/5678'").unwrap();
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ impl PublicKey for k256::PublicKey {
|
|||
|
||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
use k256::elliptic_curve::ScalarPrimitive;
|
||||
use k256::{Secp256k1, Scalar};
|
||||
use k256::{Scalar, Secp256k1};
|
||||
|
||||
// Construct a scalar from bytes
|
||||
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into());
|
||||
|
@ -156,7 +156,7 @@ pub struct TestPublicKey {
|
|||
}
|
||||
|
||||
impl TestPublicKey {
|
||||
/// Create a new TestPublicKey from the given bytes.
|
||||
/// Create a new [`TestPublicKey`] from the given bytes.
|
||||
#[allow(dead_code)]
|
||||
pub fn from_bytes(b: &[u8]) -> Self {
|
||||
Self {
|
||||
|
|
|
@ -13,10 +13,7 @@ use keyfork_slip10_test_data::{test_data, Test};
|
|||
fn secp256k1() {
|
||||
use k256::SecretKey;
|
||||
|
||||
let tests = test_data()
|
||||
.unwrap()
|
||||
.remove(&"secp256k1".to_string())
|
||||
.unwrap();
|
||||
let tests = test_data().unwrap().remove("secp256k1").unwrap();
|
||||
|
||||
for per_seed in tests {
|
||||
let seed = &per_seed.seed;
|
||||
|
@ -62,7 +59,7 @@ fn secp256k1() {
|
|||
fn ed25519() {
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
||||
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
||||
|
||||
for per_seed in tests {
|
||||
let seed = &per_seed.seed;
|
||||
|
@ -105,7 +102,7 @@ fn ed25519() {
|
|||
|
||||
#[cfg(feature = "ed25519")]
|
||||
#[test]
|
||||
#[should_panic]
|
||||
#[should_panic(expected = "HardenedDerivationRequired")]
|
||||
fn panics_with_unhardened_derivation() {
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
||||
|
@ -117,7 +114,7 @@ fn panics_with_unhardened_derivation() {
|
|||
|
||||
#[cfg(feature = "ed25519")]
|
||||
#[test]
|
||||
#[should_panic]
|
||||
#[should_panic(expected = "Depth")]
|
||||
fn panics_at_depth() {
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "keyfork-shard"
|
||||
version = "0.2.2"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
|
@ -18,8 +21,6 @@ keyfork-bug = { workspace = true }
|
|||
keyfork-prompt = { workspace = true, default-features = false, features = ["mnemonic"] }
|
||||
keyfork-qrcode = { workspace = true, optional = true, default-features = false }
|
||||
smex = { workspace = true }
|
||||
|
||||
sharks = "0.5.0"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# Remote operator mode
|
||||
|
@ -37,4 +38,5 @@ card-backend-pcsc = { workspace = true, optional = true }
|
|||
openpgp-card-sequoia = { workspace = true, optional = true }
|
||||
openpgp-card = { workspace = true, optional = true }
|
||||
sequoia-openpgp = { workspace = true, optional = true }
|
||||
base64 = "0.22.0"
|
||||
base64 = { workspace = true }
|
||||
blahaj = "0.6.0"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//!
|
||||
//! Combine OpenPGP shards and output the hex-encoded secret.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
|
@ -7,7 +7,7 @@ use std::{
|
|||
process::ExitCode,
|
||||
};
|
||||
|
||||
use keyfork_prompt::{DefaultTerminal, default_terminal};
|
||||
use keyfork_prompt::default_handler;
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
@ -32,10 +32,14 @@ fn run() -> Result<()> {
|
|||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||
};
|
||||
|
||||
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt_handler = default_terminal()?;
|
||||
let openpgp = OpenPGP;
|
||||
let prompt_handler = default_handler()?;
|
||||
|
||||
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));
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//!
|
||||
//! Decrypt a single OpenPGP shard and encapsulate it for remote transport.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
|
@ -7,8 +7,8 @@ use std::{
|
|||
process::ExitCode,
|
||||
};
|
||||
|
||||
use keyfork_prompt::{DefaultTerminal, default_terminal};
|
||||
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||
use keyfork_prompt::default_handler;
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
||||
|
@ -32,10 +32,14 @@ fn run() -> Result<()> {
|
|||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||
};
|
||||
|
||||
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt_handler = default_terminal()?;
|
||||
let openpgp = OpenPGP;
|
||||
let prompt_handler = default_handler()?;
|
||||
|
||||
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,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
//!
|
||||
//! Combine OpenPGP shards using remote transport and output the hex-encoded secret.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
process::ExitCode,
|
||||
};
|
||||
use std::{env, process::ExitCode};
|
||||
|
||||
use keyfork_shard::remote_decrypt;
|
||||
|
||||
|
@ -16,7 +13,7 @@ fn run() -> Result<()> {
|
|||
match args.as_slice() {
|
||||
[] => (),
|
||||
_ => panic!("Usage: {program_name}"),
|
||||
};
|
||||
}
|
||||
|
||||
let mut bytes = vec![];
|
||||
remote_decrypt(&mut bytes)?;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
//!
|
||||
//! Split a hex-encoded secret into OpenPGP shards
|
||||
|
||||
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_prompt::terminal::DefaultTerminal;
|
||||
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Error {
|
||||
|
@ -51,9 +50,15 @@ fn run() -> Result<()> {
|
|||
smex::decode(line?)?
|
||||
};
|
||||
|
||||
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||
let openpgp = OpenPGP;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![allow(clippy::expect_fun_call)]
|
||||
|
||||
use std::{
|
||||
io::{stdin, stdout, Read, Write},
|
||||
sync::{Arc, Mutex},
|
||||
io::{Read, Write},
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
use aes_gcm::{
|
||||
|
@ -11,18 +12,19 @@ use aes_gcm::{
|
|||
Aes256Gcm, KeyInit, Nonce,
|
||||
};
|
||||
use base64::prelude::{Engine, BASE64_STANDARD};
|
||||
use blahaj::{Share, Sharks};
|
||||
use hkdf::Hkdf;
|
||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||
use keyfork_mnemonic::{English, Mnemonic};
|
||||
use keyfork_prompt::{
|
||||
prompt_validated_wordlist,
|
||||
validators::{
|
||||
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
||||
Validator,
|
||||
},
|
||||
Message as PromptMessage, PromptHandler, Terminal,
|
||||
Message as PromptMessage, PromptHandler, YesNo,
|
||||
};
|
||||
use sha2::Sha256;
|
||||
use sharks::{Share, Sharks};
|
||||
use sha2::{Digest, Sha256};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
const PLAINTEXT_LENGTH: u8 = 32 // shard
|
||||
|
@ -32,6 +34,45 @@ const PLAINTEXT_LENGTH: u8 = 32 // shard
|
|||
+ 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."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_checksum(slice: &[u8]) -> Vec<u8> {
|
||||
// generate a verification checksum
|
||||
// this checksum should be expensive to calculate
|
||||
let mut payload = vec![];
|
||||
for _ in 0..1_000_000 {
|
||||
payload.extend(slice);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&payload);
|
||||
let result = hasher.finalize();
|
||||
payload.clear();
|
||||
payload.extend(result);
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
#[cfg(feature = "openpgp")]
|
||||
pub mod openpgp;
|
||||
|
||||
|
@ -50,9 +91,10 @@ pub trait KeyDiscovery<F: Format + ?Sized> {
|
|||
/// # Errors
|
||||
/// The method may return an error if private keys could not be loaded from the given
|
||||
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
|
||||
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
|
||||
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
|
||||
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
|
||||
/// `PrivateKeyData` type of the asssociated format should be either `()` (if the keys may
|
||||
/// never exist on-system) or an empty container (such as an empty Vec); in either case, this
|
||||
/// method _must not_ return an error if keys are accessible but can't be transferred into
|
||||
/// memory.
|
||||
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
|
||||
}
|
||||
|
||||
|
@ -79,8 +121,8 @@ pub trait Format {
|
|||
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
|
||||
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
|
||||
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
|
||||
/// will use the same key_data for both, ensuring an iteration of this method will match with
|
||||
/// iterations in methods called later.
|
||||
/// will use the same `key_data` for both, ensuring an iteration of this method will match
|
||||
/// with iterations in methods called later.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if encryption to any of the public keys fails.
|
||||
|
@ -135,10 +177,10 @@ pub trait Format {
|
|||
&self,
|
||||
private_keys: Option<Self::PrivateKeyData>,
|
||||
encrypted_messages: &[Self::EncryptedData],
|
||||
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||
) -> Result<(Vec<Share>, u8), Self::Error>;
|
||||
|
||||
/// Decrypt a single share and associated metadata from a reaable input. For the current
|
||||
/// Decrypt a single share and associated metadata from a readable input. For the current
|
||||
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
|
||||
/// combine secrets.
|
||||
///
|
||||
|
@ -149,9 +191,43 @@ pub trait Format {
|
|||
&self,
|
||||
private_keys: Option<Self::PrivateKeyData>,
|
||||
encrypted_data: &[Self::EncryptedData],
|
||||
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||
) -> Result<(Share, u8), Self::Error>;
|
||||
|
||||
/// Decrypt the public keys and metadata from encrypted data.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
|
||||
/// could neither be encrypted nor parsed.
|
||||
fn decrypt_metadata(
|
||||
&self,
|
||||
private_keys: Option<Self::PrivateKeyData>,
|
||||
encrypted_data: &[Self::EncryptedData],
|
||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error>;
|
||||
|
||||
/// Decrypt the public keys and metadata from a Shardfile.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
|
||||
/// could neither be encrypted nor parsed.
|
||||
fn decrypt_metadata_from_file(
|
||||
&self,
|
||||
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||
reader: impl Read + Send + Sync,
|
||||
prompt: Box<dyn PromptHandler>,
|
||||
) -> Result<(u8, Vec<Self::PublicKey>), Self::Error> {
|
||||
let private_keys = private_key_discovery
|
||||
.map(|p| p.discover_private_keys())
|
||||
.transpose()?;
|
||||
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||
self.decrypt_metadata(
|
||||
private_keys,
|
||||
&encrypted_messages,
|
||||
Rc::new(Mutex::new(prompt)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Decrypt multiple shares and combine them to recreate a secret.
|
||||
///
|
||||
/// # Errors
|
||||
|
@ -161,7 +237,7 @@ pub trait Format {
|
|||
&self,
|
||||
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||
reader: impl Read + Send + Sync,
|
||||
prompt: impl PromptHandler,
|
||||
prompt: Box<dyn PromptHandler>,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let private_keys = private_key_discovery
|
||||
.map(|p| p.discover_private_keys())
|
||||
|
@ -170,7 +246,7 @@ pub trait Format {
|
|||
let (shares, threshold) = self.decrypt_all_shards(
|
||||
private_keys,
|
||||
&encrypted_messages,
|
||||
Arc::new(Mutex::new(prompt)),
|
||||
Rc::new(Mutex::new(prompt)),
|
||||
)?;
|
||||
|
||||
let secret = Sharks(threshold)
|
||||
|
@ -187,13 +263,14 @@ pub trait Format {
|
|||
/// The method may return an error if a share can't be decrypted. The method will not return an
|
||||
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
|
||||
/// QR code; instead, a mnemonic prompt will be used.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn decrypt_one_shard_for_transport(
|
||||
&self,
|
||||
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||
reader: impl Read + Send + Sync,
|
||||
prompt: impl PromptHandler,
|
||||
prompt: Box<dyn PromptHandler>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let prompt = Arc::new(Mutex::new(prompt));
|
||||
let prompt = Rc::new(Mutex::new(prompt));
|
||||
|
||||
// parse input
|
||||
let private_keys = private_key_discovery
|
||||
|
@ -211,40 +288,66 @@ pub trait Format {
|
|||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||
if let Ok(Some(qrcode_content)) =
|
||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
||||
{
|
||||
let decoded_data = BASE64_STANDARD
|
||||
.decode(qrcode_content)
|
||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
|
||||
} else {
|
||||
prompt
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||
};
|
||||
loop {
|
||||
if let Ok(Some(qrcode_content)) =
|
||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
|
||||
{
|
||||
let decoded_data = BASE64_STANDARD
|
||||
.decode(qrcode_content)
|
||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||
let data: [u8; 32] = decoded_data.try_into().map_err(|_| InvalidData)?;
|
||||
let checksum = calculate_checksum(&data);
|
||||
let small_sum = &checksum[..8];
|
||||
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
|
||||
|
||||
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
|
||||
let question =
|
||||
format!("Do these words match the expected words? {small_mnemonic}");
|
||||
let response = keyfork_prompt::prompt_choice(
|
||||
&mut **prompt,
|
||||
&question,
|
||||
&[YesNo::No, YesNo::Yes],
|
||||
)?;
|
||||
if response == YesNo::No {
|
||||
prompt.prompt_message(PromptMessage::Text(String::from(
|
||||
"Could not establish secure channel, exiting.",
|
||||
)))?;
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
pubkey_data = Some(data);
|
||||
break;
|
||||
} else {
|
||||
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
|
||||
let choice = keyfork_prompt::prompt_choice(
|
||||
&mut **prompt,
|
||||
"A QR code could not be scanned. Retry or continue?",
|
||||
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
|
||||
)?;
|
||||
if choice == RetryScanMnemonic::Continue {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
||||
let their_pubkey = match pubkey_data {
|
||||
Some(pubkey) => pubkey,
|
||||
None => {
|
||||
let validator = MnemonicValidator {
|
||||
word_length: Some(WordLength::Count(24)),
|
||||
};
|
||||
prompt
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_validated_wordlist::<English, _>(
|
||||
QRCODE_COULDNT_READ,
|
||||
3,
|
||||
validator.to_fn(),
|
||||
)?
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| InvalidData)?
|
||||
}
|
||||
let their_pubkey = if let Some(pubkey) = pubkey_data {
|
||||
pubkey
|
||||
} else {
|
||||
let validator = MnemonicValidator {
|
||||
word_length: Some(WordLength::Count(24)),
|
||||
};
|
||||
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
|
||||
prompt_validated_wordlist::<English, _>(
|
||||
&mut **prompt,
|
||||
QRCODE_COULDNT_READ,
|
||||
3,
|
||||
&*validator.to_fn(),
|
||||
)?
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| InvalidData)?
|
||||
};
|
||||
|
||||
// create our shared key
|
||||
|
@ -285,7 +388,6 @@ pub trait Format {
|
|||
// 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
|
||||
|
@ -365,11 +467,11 @@ pub trait Format {
|
|||
"must have less than u8::MAX public keys"
|
||||
);
|
||||
assert_eq!(
|
||||
max,
|
||||
public_keys.len() as u8,
|
||||
max as usize,
|
||||
public_keys.len(),
|
||||
"max must be equal to amount of public keys"
|
||||
);
|
||||
let max = public_keys.len() as u8;
|
||||
let max = u8::try_from(public_keys.len()).expect(bug!("invalid max: {max}", max = max));
|
||||
assert!(max >= threshold, "threshold must not exceed max keys");
|
||||
|
||||
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
|
||||
|
@ -411,9 +513,13 @@ pub(crate) const HUNK_VERSION: u8 = 2;
|
|||
pub(crate) const HUNK_OFFSET: usize = 2;
|
||||
|
||||
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_ERROR: &str = "Unable to scan a QR code. Falling back to text entry.";
|
||||
static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
|
||||
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
|
||||
/// shares, and combine them.
|
||||
|
@ -427,8 +533,9 @@ const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry
|
|||
/// # Panics
|
||||
/// The function may panic if it is given payloads generated using a version of Keyfork that is
|
||||
/// incompatible with the currently running version.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut pm = Terminal::new(stdin(), stdout())?;
|
||||
let mut pm = keyfork_prompt::default_handler()?;
|
||||
|
||||
let mut iter_count = None;
|
||||
let mut shares = vec![];
|
||||
|
@ -449,15 +556,21 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
&BASE64_STANDARD.encode(qrcode_data),
|
||||
ErrorCorrection::Highest,
|
||||
) {
|
||||
let checksum = calculate_checksum(key_mnemonic.as_bytes());
|
||||
let small_sum = &checksum[..8];
|
||||
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
|
||||
pm.prompt_message(PromptMessage::Text(format!(
|
||||
concat!(
|
||||
"QR code #{iter} will be displayed after this prompt. ",
|
||||
"Send the QR code to the next shardholder. ",
|
||||
"Only the next shardholder should scan the QR code."
|
||||
"Only the next shardholder should scan the QR code. ",
|
||||
),
|
||||
iter = iter
|
||||
iter = iter,
|
||||
)))?;
|
||||
pm.prompt_message(PromptMessage::Data(qrcode))?;
|
||||
pm.prompt_message(PromptMessage::Text(format!(
|
||||
"The following should be sent to verify the QR code: {small_mnemonic}"
|
||||
)))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -475,45 +588,55 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
#[cfg(feature = "qrcode")]
|
||||
{
|
||||
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||
if let Ok(Some(qrcode_content)) =
|
||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
||||
{
|
||||
let decoded_data = BASE64_STANDARD
|
||||
.decode(qrcode_content)
|
||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||
assert_eq!(
|
||||
decoded_data.len(),
|
||||
// Include length of public key
|
||||
ENCRYPTED_LENGTH as usize + 32,
|
||||
bug!("invalid payload data")
|
||||
);
|
||||
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
||||
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
||||
} else {
|
||||
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||
};
|
||||
loop {
|
||||
if let Ok(Some(qrcode_content)) =
|
||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
|
||||
{
|
||||
let decoded_data = BASE64_STANDARD
|
||||
.decode(qrcode_content)
|
||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||
assert_eq!(
|
||||
decoded_data.len(),
|
||||
// Include length of public key
|
||||
ENCRYPTED_LENGTH as usize + 32,
|
||||
bug!("invalid payload data")
|
||||
);
|
||||
let _ =
|
||||
pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
||||
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
||||
break;
|
||||
} else {
|
||||
let choice = keyfork_prompt::prompt_choice(
|
||||
&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) {
|
||||
(Some(pubkey), Some(payload)) => (pubkey, payload),
|
||||
_ => {
|
||||
let validator = MnemonicSetValidator {
|
||||
word_lengths: [24, 39],
|
||||
};
|
||||
let (pubkey, payload) = if let Some((pubkey, payload)) = pubkey_data.zip(payload_data) {
|
||||
(pubkey, payload)
|
||||
} else {
|
||||
let validator = MnemonicSetValidator {
|
||||
word_lengths: [24, 39],
|
||||
};
|
||||
|
||||
let [pubkey_mnemonic, payload_mnemonic] = pm
|
||||
.prompt_validated_wordlist::<English, _>(
|
||||
QRCODE_COULDNT_READ,
|
||||
3,
|
||||
validator.to_fn(),
|
||||
)?;
|
||||
let pubkey = pubkey_mnemonic
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| InvalidData)?;
|
||||
let payload = payload_mnemonic.to_bytes();
|
||||
(pubkey, payload)
|
||||
}
|
||||
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
|
||||
&mut *pm,
|
||||
QRCODE_COULDNT_READ,
|
||||
3,
|
||||
&*validator.to_fn(),
|
||||
)?;
|
||||
let pubkey = pubkey_mnemonic
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| InvalidData)?;
|
||||
let payload = payload_mnemonic.to_bytes();
|
||||
(pubkey, payload)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
@ -540,16 +663,13 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
|
||||
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
|
||||
|
||||
match &mut iter_count {
|
||||
Some(n) => {
|
||||
// Must be > 0 to start loop, can't go lower
|
||||
*n -= 1;
|
||||
}
|
||||
None => {
|
||||
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
|
||||
threshold = payload[1];
|
||||
let _ = iter_count.insert(threshold - 1);
|
||||
}
|
||||
if let Some(n) = &mut iter_count {
|
||||
// Must be > 0 to start loop, can't go lower
|
||||
*n -= 1;
|
||||
} else {
|
||||
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
|
||||
threshold = payload[1];
|
||||
let _ = iter_count.insert(threshold - 1);
|
||||
}
|
||||
|
||||
let payload_len = payload.last().expect(bug!("payload should not be empty"));
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
//! OpenPGP Shard functionality.
|
||||
|
||||
#![allow(clippy::expect_fun_call)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Read, Write},
|
||||
marker::PhantomData,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use blahaj::Share;
|
||||
use keyfork_bug::bug;
|
||||
use keyfork_derive_openpgp::{
|
||||
derive_util::{DerivationPath, VariableLengthSeed},
|
||||
|
@ -25,7 +24,7 @@ use openpgp::{
|
|||
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
|
||||
Parse,
|
||||
},
|
||||
policy::{NullPolicy, Policy},
|
||||
policy::{NullPolicy, Policy, StandardPolicy},
|
||||
serialize::{
|
||||
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
|
||||
Marshal,
|
||||
|
@ -34,7 +33,6 @@ use openpgp::{
|
|||
KeyID, PacketPile,
|
||||
};
|
||||
pub use sequoia_openpgp as openpgp;
|
||||
use sharks::Share;
|
||||
|
||||
mod keyring;
|
||||
use keyring::Keyring;
|
||||
|
@ -77,6 +75,10 @@ pub enum Error {
|
|||
/// An IO error occurred.
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[source] std::io::Error),
|
||||
|
||||
/// No valid keys were found for the given recipient.
|
||||
#[error("No valid keys were found for the recipient {0}")]
|
||||
NoValidKeys(KeyID),
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
|
@ -90,7 +92,7 @@ pub struct EncryptedMessage {
|
|||
}
|
||||
|
||||
impl EncryptedMessage {
|
||||
/// Create a new EncryptedMessage from known parts.
|
||||
/// Create a new [`EncryptedMessage`] from known parts.
|
||||
pub fn new(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
|
||||
Self {
|
||||
pkesks: std::mem::take(pkesks),
|
||||
|
@ -156,7 +158,7 @@ impl EncryptedMessage {
|
|||
/// Decrypt the message with a Sequoia policy and decryptor.
|
||||
///
|
||||
/// This method creates a container containing the packets and passes the serialized container
|
||||
/// to a DecryptorBuilder, which is used to decrypt the message.
|
||||
/// to a `DecryptorBuilder`, which is used to decrypt the message.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if it is unable to rebuild the message to decrypt or if it
|
||||
|
@ -181,19 +183,10 @@ impl EncryptedMessage {
|
|||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub struct OpenPGP<P: PromptHandler> {
|
||||
p: PhantomData<P>,
|
||||
}
|
||||
/// Encoding and decoding shards using OpenPGP.
|
||||
pub struct OpenPGP;
|
||||
|
||||
impl<P: PromptHandler> OpenPGP<P> {
|
||||
#[allow(clippy::new_without_default, missing_docs)]
|
||||
pub fn new() -> Self {
|
||||
Self { p: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> OpenPGP<P> {
|
||||
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.
|
||||
|
@ -239,13 +232,20 @@ impl<P: PromptHandler> OpenPGP<P> {
|
|||
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 PublicKey = Cert;
|
||||
type PrivateKeyData = Vec<Cert>;
|
||||
|
@ -263,7 +263,7 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
|
|||
.derive_path(&path)
|
||||
.expect(bug!("valid derivation"));
|
||||
keyfork_derive_openpgp::derive(
|
||||
xprv,
|
||||
&xprv,
|
||||
&[KeyFlags::empty().set_certification().set_signing()],
|
||||
&userid,
|
||||
)
|
||||
|
@ -276,7 +276,7 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
|
|||
key_data: &[Self::PublicKey],
|
||||
threshold: u8,
|
||||
) -> Result<Self::EncryptedData, Self::Error> {
|
||||
let policy = NullPolicy::new();
|
||||
let policy = StandardPolicy::new();
|
||||
let mut pp = vec![SHARD_METADATA_VERSION, threshold];
|
||||
// Note: Sequoia does not export private keys on a Cert, only on a TSK
|
||||
signing_key
|
||||
|
@ -362,7 +362,7 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
|
|||
public_key: &Cert,
|
||||
signing_key: &mut Self::SigningKey,
|
||||
) -> Result<EncryptedMessage> {
|
||||
let policy = NullPolicy::new();
|
||||
let policy = StandardPolicy::new();
|
||||
let valid_cert = public_key
|
||||
.with_policy(&policy, None)
|
||||
.map_err(Error::Sequoia)?;
|
||||
|
@ -442,14 +442,14 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
|
|||
&self,
|
||||
private_keys: Option<Self::PrivateKeyData>,
|
||||
encrypted_data: &[Self::EncryptedData],
|
||||
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
|
||||
// Be as liberal as possible when decrypting.
|
||||
// We don't want to invalidate someone's keys just because the old sig expired.
|
||||
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 keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
|
||||
let mut manager = SmartcardManager::new(prompt.clone());
|
||||
|
||||
let mut encrypted_messages = encrypted_data.iter();
|
||||
|
||||
|
@ -480,9 +480,9 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
|
|||
|
||||
let left_from_threshold = threshold as usize - decrypted_messages.len();
|
||||
if left_from_threshold > 0 {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let new_messages = decrypt_with_manager(
|
||||
left_from_threshold as u8,
|
||||
u8::try_from(left_from_threshold)
|
||||
.expect(bug!("threshold too large: {}", left_from_threshold)),
|
||||
&mut messages,
|
||||
&certs,
|
||||
&policy,
|
||||
|
@ -503,12 +503,12 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
|
|||
&self,
|
||||
private_keys: Option<Self::PrivateKeyData>,
|
||||
encrypted_data: &[Self::EncryptedData],
|
||||
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||
) -> std::result::Result<(Share, u8), 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 keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
|
||||
let mut manager = SmartcardManager::new(prompt.clone());
|
||||
|
||||
let mut encrypted_messages = encrypted_data.iter();
|
||||
|
||||
|
@ -547,24 +547,44 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
|
|||
|
||||
panic!("unable to decrypt shard");
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &Path {
|
||||
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
|
||||
OpenPGP::<P>::discover_certs(self)
|
||||
}
|
||||
fn decrypt_metadata(
|
||||
&self,
|
||||
private_keys: Option<Self::PrivateKeyData>,
|
||||
encrypted_data: &[Self::EncryptedData],
|
||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error> {
|
||||
let policy = NullPolicy::new();
|
||||
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
|
||||
let mut manager = SmartcardManager::new(prompt.clone());
|
||||
let mut encrypted_messages = encrypted_data.iter();
|
||||
|
||||
fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
|
||||
OpenPGP::<P>::discover_certs(self)
|
||||
let metadata = encrypted_messages
|
||||
.next()
|
||||
.expect(bug!(METADATA_MESSAGE_MISSING));
|
||||
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
|
||||
|
||||
let (threshold, _root_cert, certs) = decode_metadata_v1(&metadata_content)?;
|
||||
Ok((threshold, certs))
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &[Cert] {
|
||||
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
|
||||
impl KeyDiscovery<OpenPGP> for &Path {
|
||||
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
|
||||
OpenPGP::discover_certs(self)
|
||||
}
|
||||
|
||||
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
|
||||
OpenPGP::discover_certs(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyDiscovery<OpenPGP> for &[Cert] {
|
||||
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
|
||||
Ok(self.to_vec())
|
||||
}
|
||||
|
||||
fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
|
||||
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
|
||||
Ok(self.to_vec())
|
||||
}
|
||||
}
|
||||
|
@ -626,12 +646,12 @@ fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
|
|||
|
||||
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
|
||||
// single message.
|
||||
fn decrypt_with_manager<P: PromptHandler>(
|
||||
fn decrypt_with_manager(
|
||||
threshold: u8,
|
||||
messages: &mut HashMap<KeyID, EncryptedMessage>,
|
||||
certs: &[Cert],
|
||||
policy: &dyn Policy,
|
||||
manager: &mut SmartcardManager<P>,
|
||||
manager: &mut SmartcardManager,
|
||||
) -> Result<HashMap<KeyID, Vec<u8>>> {
|
||||
let mut decrypted_messages = HashMap::new();
|
||||
|
||||
|
@ -676,11 +696,11 @@ fn decrypt_with_manager<P: PromptHandler>(
|
|||
|
||||
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
|
||||
// decrypt messages with.
|
||||
fn decrypt_with_keyring<P: PromptHandler>(
|
||||
fn decrypt_with_keyring(
|
||||
messages: &mut HashMap<KeyID, EncryptedMessage>,
|
||||
certs: &[Cert],
|
||||
policy: &NullPolicy,
|
||||
keyring: &mut Keyring<P>,
|
||||
keyring: &mut Keyring,
|
||||
) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
|
||||
let mut decrypted_messages = HashMap::new();
|
||||
|
||||
|
@ -710,11 +730,11 @@ fn decrypt_with_keyring<P: PromptHandler>(
|
|||
Ok(decrypted_messages)
|
||||
}
|
||||
|
||||
fn decrypt_metadata<P: PromptHandler>(
|
||||
fn decrypt_metadata(
|
||||
message: &EncryptedMessage,
|
||||
policy: &NullPolicy,
|
||||
keyring: &mut Keyring<P>,
|
||||
manager: &mut SmartcardManager<P>,
|
||||
keyring: &mut Keyring,
|
||||
manager: &mut SmartcardManager,
|
||||
) -> Result<Vec<u8>> {
|
||||
Ok(if keyring.is_empty() {
|
||||
manager.load_any_card()?;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
#![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};
|
||||
|
@ -25,21 +23,19 @@ pub enum Error {
|
|||
Prompt(#[from] PromptError),
|
||||
}
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
pub struct Keyring<P: PromptHandler> {
|
||||
pub struct Keyring {
|
||||
full_certs: Vec<Cert>,
|
||||
root: Option<Cert>,
|
||||
pm: Arc<Mutex<P>>,
|
||||
pm: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> Keyring<P> {
|
||||
pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
impl Keyring {
|
||||
pub fn new(certs: impl AsRef<[Cert]>, p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Self {
|
||||
Self {
|
||||
full_certs: certs.as_ref().to_vec(),
|
||||
root: Default::default(),
|
||||
root: Option::default(),
|
||||
pm: p,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
|
@ -62,7 +58,7 @@ impl<P: PromptHandler> Keyring<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
|
||||
impl VerificationHelper for &mut Keyring {
|
||||
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
|
||||
Ok(ids
|
||||
.iter()
|
||||
|
@ -108,7 +104,7 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
|
||||
impl DecryptionHelper for &mut Keyring {
|
||||
fn decrypt<D>(
|
||||
&mut self,
|
||||
pkesks: &[PKESK],
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
#![allow(clippy::expect_fun_call)]
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, Mutex},
|
||||
rc::Rc,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||
use keyfork_prompt::{
|
||||
prompt_validated_passphrase,
|
||||
validators::{PinValidator, Validator},
|
||||
Error as PromptError, Message, PromptHandler,
|
||||
};
|
||||
|
@ -71,21 +71,21 @@ fn format_name(input: impl AsRef<str>) -> String {
|
|||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct SmartcardManager<P: PromptHandler> {
|
||||
pub struct SmartcardManager {
|
||||
current_card: Option<Card<Open>>,
|
||||
root: Option<Cert>,
|
||||
pm: Arc<Mutex<P>>,
|
||||
pm: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||
pin_cache: HashMap<Fingerprint, String>,
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> SmartcardManager<P> {
|
||||
pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
impl SmartcardManager {
|
||||
pub fn new(p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Self {
|
||||
Self {
|
||||
current_card: None,
|
||||
root: None,
|
||||
pm: p,
|
||||
pin_cache: Default::default(),
|
||||
})
|
||||
pin_cache: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the root cert, returning the old cert
|
||||
|
@ -173,12 +173,11 @@ impl<P: PromptHandler> SmartcardManager<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
|
||||
impl VerificationHelper for &mut SmartcardManager {
|
||||
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
|
||||
#[allow(clippy::flat_map_option)]
|
||||
Ok(ids
|
||||
.iter()
|
||||
.flat_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
|
||||
.filter_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
@ -217,7 +216,7 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
|
||||
impl DecryptionHelper for &mut SmartcardManager {
|
||||
fn decrypt<D>(
|
||||
&mut self,
|
||||
pkesks: &[PKESK],
|
||||
|
@ -275,15 +274,11 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
|
|||
} else {
|
||||
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
|
||||
};
|
||||
let temp_pin = self
|
||||
.pm
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
|
||||
let mut prompt = self.pm.lock().expect(bug!(POISONED_MUTEX));
|
||||
let temp_pin = prompt_validated_passphrase(&mut **prompt, &message, 3, &pin_validator)?;
|
||||
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
|
||||
match verification_status {
|
||||
#[allow(clippy::ignored_unit_patterns)]
|
||||
Ok(_) => {
|
||||
Ok(()) => {
|
||||
self.pin_cache.insert(fp.clone(), temp_pin.clone());
|
||||
pin.replace(temp_pin);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
[package]
|
||||
name = "keyfork"
|
||||
version = "0.2.3"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"completion",
|
||||
"qrcode-decode-backend-rqrr",
|
||||
"sequoia-crypto-backend-nettle",
|
||||
"sequoia-crypto-backend-nettle",
|
||||
]
|
||||
|
||||
completion = ["dep:clap_complete"]
|
||||
|
@ -45,3 +48,8 @@ openpgp-card-sequoia = { workspace = true }
|
|||
openpgp-card = { workspace = true }
|
||||
clap_complete = { version = "4.4.6", optional = true }
|
||||
sequoia-openpgp = { workspace = true }
|
||||
keyforkd-models.workspace = true
|
||||
base64.workspace = true
|
||||
nix = { version = "0.30.0", default-features = false, features = ["process"] }
|
||||
shlex = "1.3.0"
|
||||
tempfile.workspace = true
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
//! 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(Self::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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,21 +1,36 @@
|
|||
use super::Keyfork;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use super::{create, Keyfork};
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use std::{fmt::Display, io::Write, path::PathBuf};
|
||||
|
||||
use keyfork_derive_openpgp::{
|
||||
openpgp::{
|
||||
armor::{Kind, Writer},
|
||||
packet::UserID,
|
||||
serialize::Marshal,
|
||||
types::KeyFlags,
|
||||
},
|
||||
XPrvKey,
|
||||
use keyfork_derive_openpgp::openpgp::{
|
||||
armor::{Kind, Writer},
|
||||
packet::UserID,
|
||||
serialize::Marshal,
|
||||
types::KeyFlags,
|
||||
Cert,
|
||||
};
|
||||
use keyfork_derive_util::DerivationIndex;
|
||||
use keyfork_derive_path_data::paths;
|
||||
use keyfork_derive_util::{
|
||||
request::DerivationAlgorithm, DerivationIndex, DerivationPath, ExtendedPrivateKey as XPrv,
|
||||
IndexError, PrivateKey,
|
||||
};
|
||||
use keyforkd_client::Client;
|
||||
|
||||
type OptWrite = Option<Box<dyn Write>>;
|
||||
|
||||
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)]
|
||||
pub enum DeriveSubcommands {
|
||||
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
|
||||
|
@ -28,27 +43,154 @@ pub enum DeriveSubcommands {
|
|||
/// It is recommended to use the default expiration of one day and to change the expiration
|
||||
/// using an external utility, to ensure the Certify key is usable.
|
||||
#[command(name = "openpgp")]
|
||||
OpenPGP(OpenPGP)
|
||||
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,
|
||||
|
||||
/// The Shard index.
|
||||
Shard,
|
||||
}
|
||||
|
||||
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",
|
||||
Path::Shard => "shard",
|
||||
}
|
||||
}
|
||||
|
||||
fn derivation_path(&self) -> DerivationPath {
|
||||
match self {
|
||||
Self::Default => paths::OPENPGP.clone(),
|
||||
Self::DisasterRecovery => paths::OPENPGP_DISASTER_RECOVERY.clone(),
|
||||
Self::Shard => paths::OPENPGP_SHARD.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct OpenPGP {
|
||||
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
||||
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 {
|
||||
#[allow(clippy::redundant_at_rest_pattern)]
|
||||
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 {
|
||||
fn handle(&self, account: DerivationIndex) -> Result<()> {
|
||||
fn handle(&self, account: DerivationIndex, is_public: bool, writer: OptWrite) -> Result<()> {
|
||||
match self {
|
||||
DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account),
|
||||
DeriveSubcommands::OpenPGP(opgp) => {
|
||||
let path = opgp.derivation_path();
|
||||
let xprv = Client::discover_socket()?
|
||||
.request_xprv::<<OpenPGP as Deriver>::Prv>(&path.chain_push(account))?;
|
||||
if is_public {
|
||||
opgp.derive_public_with_xprv(writer, &xprv)
|
||||
} else {
|
||||
opgp.derive_with_xprv(writer, &xprv)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
|
||||
let path = paths::OPENPGP.clone().chain_push(account);
|
||||
// TODO: should this be customizable?
|
||||
fn cert_from_xprv(&self, xprv: &keyfork_derive_openpgp::XPrv) -> Result<Cert> {
|
||||
let subkeys = vec![
|
||||
KeyFlags::empty().set_certification(),
|
||||
KeyFlags::empty().set_signing(),
|
||||
|
@ -57,17 +199,94 @@ impl OpenPGP {
|
|||
.set_storage_encryption(),
|
||||
KeyFlags::empty().set_authentication(),
|
||||
];
|
||||
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
|
||||
let default_userid = UserID::from(self.user_id.as_str());
|
||||
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
|
||||
|
||||
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
|
||||
let userid = UserID::from(&*self.user_id);
|
||||
keyfork_derive_openpgp::derive(xprv, &subkeys, &userid).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
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 = if let Some(writer) = writer { writer } else {
|
||||
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 = if let Some(writer) = writer { writer } else {
|
||||
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 w)?;
|
||||
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)?;
|
||||
}
|
||||
|
||||
w.finalize()?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +294,7 @@ impl OpenPGP {
|
|||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct Derive {
|
||||
#[command(subcommand)]
|
||||
command: DeriveSubcommands,
|
||||
pub(crate) command: DeriveSubcommands,
|
||||
|
||||
/// Account ID. Required for all derivations.
|
||||
///
|
||||
|
@ -83,12 +302,45 @@ pub struct Derive {
|
|||
/// account ID can often come as a hindrance in the future. As such, it is always required. If
|
||||
/// the account ID is not relevant, it is assumed to be `0`.
|
||||
#[arg(long, global = true, default_value = "0")]
|
||||
account_id: u32,
|
||||
pub(crate) 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 {
|
||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||
let account = DerivationIndex::new(self.account_id, true)?;
|
||||
self.command.handle(account)
|
||||
let writer = if let Some(output) = self.output.as_deref() {
|
||||
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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,42 @@
|
|||
use super::Keyfork;
|
||||
use super::{
|
||||
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 std::fmt::Display;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Display,
|
||||
fs::File,
|
||||
io::{IsTerminal, Write},
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use keyfork_derive_openpgp::{
|
||||
openpgp::{
|
||||
self,
|
||||
armor::{Kind, Writer},
|
||||
packet::{signature::SignatureBuilder, UserID},
|
||||
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)]
|
||||
pub enum SeedSize {
|
||||
|
@ -59,6 +95,7 @@ impl From<&SeedSize> for usize {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
pub enum MnemonicSeedSourceParseError {
|
||||
#[error("Expected one of system, playing, tarot, dice")]
|
||||
|
@ -96,24 +133,41 @@ impl std::str::FromStr for MnemonicSeedSource {
|
|||
}
|
||||
|
||||
impl MnemonicSeedSource {
|
||||
pub fn handle(&self, size: &SeedSize) -> Result<String, Box<dyn std::error::Error>> {
|
||||
pub fn handle(
|
||||
&self,
|
||||
size: &SeedSize,
|
||||
) -> Result<keyfork_mnemonic::Mnemonic, Box<dyn std::error::Error>> {
|
||||
let size = match size {
|
||||
SeedSize::Bits128 => 128,
|
||||
SeedSize::Bits256 => 256,
|
||||
};
|
||||
let seed = match self {
|
||||
MnemonicSeedSource::System => {
|
||||
keyfork_entropy::generate_entropy_of_size(size / 8)?
|
||||
}
|
||||
MnemonicSeedSource::System => keyfork_entropy::generate_entropy_of_size(size / 8)?,
|
||||
MnemonicSeedSource::Playing => todo!(),
|
||||
MnemonicSeedSource::Tarot => todo!(),
|
||||
MnemonicSeedSource::Dice => todo!(),
|
||||
};
|
||||
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?;
|
||||
Ok(mnemonic.to_string())
|
||||
Ok(mnemonic)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(path: &Path) -> impl Fn(std::io::Error) -> Error + use<'_> {
|
||||
|e| Error::IOContext(e, path.to_path_buf())
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
pub enum MnemonicSubcommands {
|
||||
/// Generate a mnemonic using a given entropy source.
|
||||
|
@ -124,6 +178,10 @@ pub enum MnemonicSubcommands {
|
|||
/// method of generating a seed using system entropy, as well as various forms of loading
|
||||
/// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location
|
||||
/// (such as a Trezor "recovery seed card") and never persisted digitally.
|
||||
///
|
||||
/// When using the `--shard`, `--shard-to`, `--encrypt-to`, and `--encrypt-to-self` +
|
||||
/// `--provision` arguments, the mnemonic is _not_ sent to output. The data for the mnemonic is
|
||||
/// then either split using Keyfork Shard or encrypted using OpenPGP.
|
||||
Generate {
|
||||
/// The source from where a seed is created.
|
||||
#[arg(long, value_enum, default_value_t = Default::default())]
|
||||
|
@ -132,17 +190,758 @@ pub enum MnemonicSubcommands {
|
|||
/// The size of the mnemonic, in bits.
|
||||
#[arg(long, default_value_t = Default::default())]
|
||||
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 {
|
||||
if let Some(p) = optional_path {
|
||||
p.as_ref().to_path_buf()
|
||||
} else {
|
||||
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" | "gpg") => false,
|
||||
Some("asc") => true,
|
||||
_ => {
|
||||
eprintln!("unable to determine whether to armor file: {path}", path = path.display());
|
||||
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.into());
|
||||
}
|
||||
|
||||
let (threshold, max) = if let Some(t) = threshold.zip(max) { t } else {
|
||||
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")
|
||||
.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 #{}",
|
||||
(u16::from(i)) + 1,
|
||||
(u16::from(index)) + 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,
|
||||
u8::try_from(certs.len()).expect("provided more than u8::MAX certs"),
|
||||
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_key = root_xprv.derive_path(&opgp.derivation_path().chain_push(account))?;
|
||||
if *public {
|
||||
opgp.derive_public_with_xprv(writer, &derived_key)?;
|
||||
} else {
|
||||
opgp.derive_with_xprv(writer, &derived_key)?;
|
||||
}
|
||||
}
|
||||
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_key = root_xprv.derive_path(&key.derivation_path().chain_push(account))?;
|
||||
if *public {
|
||||
key.derive_public_with_xprv(writer, &derived_key)?;
|
||||
} else {
|
||||
key.derive_with_xprv(writer, &derived_key)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl MnemonicSubcommands {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn handle(
|
||||
&self,
|
||||
_m: &Mnemonic,
|
||||
_keyfork: &Keyfork,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
MnemonicSubcommands::Generate { source, size } => source.handle(size),
|
||||
MnemonicSubcommands::Generate {
|
||||
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(Vec::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(Vec::is_empty);
|
||||
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|
||||
|| shard.as_ref().is_some_and(Vec::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: HashSet<u32> = HashSet::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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,11 @@ mod mnemonic;
|
|||
mod provision;
|
||||
mod recover;
|
||||
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.
|
||||
#[derive(Parser, Clone, Debug)]
|
||||
|
@ -16,6 +20,7 @@ pub struct Keyfork {
|
|||
pub command: KeyforkCommands,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
pub enum KeyforkCommands {
|
||||
/// Derive keys of various formats. These commands require that the Keyfork server is running,
|
||||
|
@ -57,9 +62,6 @@ pub enum KeyforkCommands {
|
|||
/// leaked by any individual deriver.
|
||||
Recover(recover::Recover),
|
||||
|
||||
/// Utilities to automatically manage the setup of Keyfork.
|
||||
Wizard(wizard::Wizard),
|
||||
|
||||
/// Print an autocompletion file to standard output.
|
||||
///
|
||||
/// Keyfork does not manage the installation of completion files. Consult the documentation for
|
||||
|
@ -79,8 +81,7 @@ impl KeyforkCommands {
|
|||
d.handle(keyfork)?;
|
||||
}
|
||||
KeyforkCommands::Mnemonic(m) => {
|
||||
let response = m.command.handle(m, keyfork)?;
|
||||
println!("{response}");
|
||||
m.command.handle(m, keyfork)?;
|
||||
}
|
||||
KeyforkCommands::Shard(s) => {
|
||||
s.command.handle(s, keyfork)?;
|
||||
|
@ -91,19 +92,11 @@ impl KeyforkCommands {
|
|||
KeyforkCommands::Recover(r) => {
|
||||
r.handle(keyfork)?;
|
||||
}
|
||||
KeyforkCommands::Wizard(w) => {
|
||||
w.handle(keyfork)?;
|
||||
}
|
||||
#[cfg(feature = "completion")]
|
||||
KeyforkCommands::Completion { shell } => {
|
||||
let mut command = Keyfork::command();
|
||||
let command_name = command.get_name().to_string();
|
||||
clap_complete::generate(
|
||||
*shell,
|
||||
&mut command,
|
||||
command_name,
|
||||
&mut std::io::stdout(),
|
||||
);
|
||||
clap_complete::generate(*shell, &mut command, command_name, &mut std::io::stdout());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -3,81 +3,138 @@ use crate::config;
|
|||
|
||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||
|
||||
use keyfork_derive_util::{DerivationIndex, ExtendedPrivateKey};
|
||||
|
||||
mod openpgp;
|
||||
|
||||
type Identifier = (String, Option<String>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Provisioner {
|
||||
OpenPGPCard(OpenPGPCard),
|
||||
OpenPGPCard(openpgp::OpenPGPCard),
|
||||
Shard(openpgp::Shard),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Provisioner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Provisioner::OpenPGPCard(_) => f.write_str("openpgp-card"),
|
||||
}
|
||||
f.write_str(self.identifier())
|
||||
}
|
||||
}
|
||||
|
||||
impl Provisioner {
|
||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||
pub fn identifier(&self) -> &'static str {
|
||||
match self {
|
||||
Provisioner::OpenPGPCard(o) => o.discover(),
|
||||
Provisioner::OpenPGPCard(_) => "openpgp-card",
|
||||
Provisioner::Shard(_) => "shard",
|
||||
}
|
||||
}
|
||||
|
||||
fn provision(
|
||||
pub fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
Provisioner::OpenPGPCard(o) => o.discover(),
|
||||
Provisioner::Shard(s) => s.discover(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provision(
|
||||
&self,
|
||||
provisioner: config::Provisioner,
|
||||
provisioner: &config::Provisioner,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
Provisioner::OpenPGPCard(o) => o.provision(provisioner),
|
||||
Provisioner::OpenPGPCard(o) => {
|
||||
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
|
||||
type XPrv = ExtendedPrivateKey<Prv>;
|
||||
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
||||
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
|
||||
.chain_push(account_index);
|
||||
let mut client = keyforkd_client::Client::discover_socket()?;
|
||||
let xprv: XPrv = client.request_xprv(&path)?;
|
||||
o.provision(&xprv, provisioner)
|
||||
}
|
||||
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 {
|
||||
fn value_variants<'a>() -> &'a [Self] {
|
||||
&[Self::OpenPGPCard(OpenPGPCard)]
|
||||
&[
|
||||
Self::OpenPGPCard(openpgp::OpenPGPCard),
|
||||
Self::Shard(openpgp::Shard),
|
||||
]
|
||||
}
|
||||
|
||||
fn to_possible_value(&self) -> Option<PossibleValue> {
|
||||
Some(PossibleValue::new(match self {
|
||||
Self::OpenPGPCard(_) => "openpgp-card",
|
||||
}))
|
||||
Some(PossibleValue::new(self.identifier()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("The given value could not be matched as a provisioner: {0} ({1})")]
|
||||
pub struct ProvisionerFromStrError(String, String);
|
||||
|
||||
impl std::str::FromStr for Provisioner {
|
||||
type Err = ProvisionerFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
<Provisioner as ValueEnum>::from_str(s, false)
|
||||
.map_err(|e| ProvisionerFromStrError(s.to_string(), e))
|
||||
}
|
||||
}
|
||||
|
||||
trait ProvisionExec {
|
||||
type PrivateKey: keyfork_derive_util::PrivateKey + Clone;
|
||||
|
||||
/// Discover all known places the formatted key can be deployed to.
|
||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||
vec![]
|
||||
fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Return the derivation path for deriving keys.
|
||||
fn derivation_prefix() -> keyfork_derive_util::DerivationPath;
|
||||
|
||||
/// Derive a key and deploy it to a target.
|
||||
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OpenPGPCard;
|
||||
|
||||
impl ProvisionExec for OpenPGPCard {
|
||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||
/*
|
||||
vec![
|
||||
(
|
||||
"0006:26144195".to_string(),
|
||||
Some("Yubicats Heywood".to_string()),
|
||||
),
|
||||
(
|
||||
"0006:2614419y".to_string(),
|
||||
Some("Yubicats Heywood".to_string()),
|
||||
),
|
||||
]
|
||||
*/
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn provision(&self, _p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>> {
|
||||
todo!()
|
||||
}
|
||||
fn provision(
|
||||
&self,
|
||||
xprv: &keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
|
||||
p: &config::Provisioner,
|
||||
) -> Result<(), Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
|
@ -94,15 +151,27 @@ pub struct Provision {
|
|||
#[command(subcommand)]
|
||||
pub subcommand: Option<ProvisionSubcommands>,
|
||||
|
||||
provisioner_name: Provisioner,
|
||||
pub provisioner_name: Provisioner,
|
||||
|
||||
/// Account ID.
|
||||
#[arg(long, required(true))]
|
||||
account_id: Option<u32>,
|
||||
#[arg(long, default_value = "0")]
|
||||
pub account_id: u32,
|
||||
|
||||
/// Identifier of the hardware to deploy to, listable by running the `discover` subcommand.
|
||||
#[arg(long, required(true))]
|
||||
identifier: Option<String>,
|
||||
#[arg(long)]
|
||||
pub 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
|
||||
|
@ -118,10 +187,9 @@ impl TryFrom<Provision> for config::Provisioner {
|
|||
|
||||
fn try_from(value: Provision) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
name: value.provisioner_name.to_string(),
|
||||
account: value.account_id.ok_or(MissingField("account_id"))?,
|
||||
account: value.account_id,
|
||||
identifier: value.identifier.ok_or(MissingField("identifier"))?,
|
||||
metadata: Default::default(),
|
||||
metadata: Option::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +198,7 @@ impl Provision {
|
|||
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self.subcommand {
|
||||
Some(ProvisionSubcommands::Discover) => {
|
||||
let mut iter = self.provisioner_name.discover().into_iter().peekable();
|
||||
let mut iter = self.provisioner_name.discover()?.into_iter().peekable();
|
||||
while let Some((identifier, context)) = iter.next() {
|
||||
println!("Identifier: {identifier}");
|
||||
if let Some(context) = context {
|
||||
|
@ -142,7 +210,20 @@ impl Provision {
|
|||
}
|
||||
}
|
||||
None => {
|
||||
self.provisioner_name.provision(self.clone().try_into()?)?;
|
||||
let provisioner_with_identifier = if self.identifier.is_some() {
|
||||
self.clone()
|
||||
} else {
|
||||
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(())
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
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;
|
||||
|
||||
pub type CardList = Vec<(String, Option<String>)>;
|
||||
|
||||
fn discover_cards() -> Result<CardList, 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, &subkeys, &userid)?;
|
||||
|
||||
if !provisioner
|
||||
.metadata
|
||||
.as_ref()
|
||||
.is_some_and(|m| m.contains_key("_skip_cert_output"))
|
||||
{
|
||||
let cert_output = if let Some(cert_output) =
|
||||
provisioner.metadata.as_ref().and_then(|m| m.get("output"))
|
||||
{
|
||||
PathBuf::from(cert_output)
|
||||
} else {
|
||||
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.into());
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,19 @@
|
|||
use super::Keyfork;
|
||||
use clap::{Parser, Subcommand};
|
||||
use nix::{
|
||||
sys::wait::waitpid,
|
||||
unistd::{fork, ForkResult},
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use keyfork_mnemonic::{English, Mnemonic};
|
||||
use keyfork_prompt::{default_terminal, DefaultTerminal};
|
||||
use keyfork_prompt::{
|
||||
default_handler, prompt_validated_wordlist,
|
||||
validators::{
|
||||
mnemonic::{MnemonicChoiceValidator, WordLength},
|
||||
Validator,
|
||||
},
|
||||
};
|
||||
use keyfork_shard::{remote_decrypt, Format};
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
@ -35,8 +45,8 @@ impl RecoverSubcommands {
|
|||
} => {
|
||||
let content = std::fs::read_to_string(shard_file)?;
|
||||
if content.contains("BEGIN PGP MESSAGE") {
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt_handler = default_terminal()?;
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||
let prompt_handler = default_handler()?;
|
||||
// TODO: remove .clone() by making handle() consume self
|
||||
let seed = openpgp.decrypt_all_shards_to_secret(
|
||||
key_discovery.as_deref(),
|
||||
|
@ -54,21 +64,15 @@ impl RecoverSubcommands {
|
|||
Ok(seed)
|
||||
}
|
||||
RecoverSubcommands::Mnemonic {} => {
|
||||
use keyfork_prompt::{
|
||||
validators::{
|
||||
mnemonic::{MnemonicChoiceValidator, WordLength},
|
||||
Validator,
|
||||
},
|
||||
PromptHandler,
|
||||
};
|
||||
let mut term = default_terminal()?;
|
||||
let mut prompt_handler = default_handler()?;
|
||||
let validator = MnemonicChoiceValidator {
|
||||
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
|
||||
};
|
||||
let mnemonic = term.prompt_validated_wordlist::<English, _>(
|
||||
let mnemonic = prompt_validated_wordlist::<English, _>(
|
||||
&mut *prompt_handler,
|
||||
"Mnemonic: ",
|
||||
3,
|
||||
validator.to_fn(),
|
||||
&*validator.to_fn(),
|
||||
)?;
|
||||
Ok(mnemonic.to_bytes())
|
||||
}
|
||||
|
@ -80,12 +84,32 @@ impl RecoverSubcommands {
|
|||
pub struct Recover {
|
||||
#[command(subcommand)]
|
||||
command: RecoverSubcommands,
|
||||
|
||||
/// Daemonize the server once started, restoring control back to the shell.
|
||||
#[arg(long, global = true)]
|
||||
daemon: bool,
|
||||
}
|
||||
|
||||
impl Recover {
|
||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||
let seed = self.command.handle()?;
|
||||
let mnemonic = Mnemonic::try_from_slice(&seed)?;
|
||||
if self.daemon {
|
||||
// SAFETY: Forking threaded programs is unsafe. We know we don't have multiple
|
||||
// threads at this point.
|
||||
match unsafe { fork() }? {
|
||||
ForkResult::Parent { child } => {
|
||||
// wait for the child to die, so we don't exit prematurely
|
||||
waitpid(Some(child), None)?;
|
||||
return Ok(());
|
||||
}
|
||||
ForkResult::Child => {
|
||||
if let ForkResult::Parent { .. } = unsafe { fork() }? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::Keyfork;
|
||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||
use keyfork_prompt::{default_terminal, DefaultTerminal};
|
||||
use keyfork_prompt::default_handler;
|
||||
use keyfork_shard::Format as _;
|
||||
use std::{
|
||||
io::{stdin, stdout, Read, Write},
|
||||
|
@ -50,6 +50,14 @@ trait ShardExec {
|
|||
key_discovery: Option<&Path>,
|
||||
input: impl Read + Send + Sync,
|
||||
) -> Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
fn metadata(
|
||||
&self,
|
||||
key_discovery: Option<&Path>,
|
||||
input: impl Read + Send + Sync,
|
||||
output_pubkeys: &mut impl Write,
|
||||
output: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -64,7 +72,7 @@ impl ShardExec for OpenPGP {
|
|||
secret: &[u8],
|
||||
output: &mut (impl Write + Send + Sync),
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let opgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||
let opgp = keyfork_shard::openpgp::OpenPGP;
|
||||
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
|
||||
}
|
||||
|
||||
|
@ -74,8 +82,8 @@ impl ShardExec for OpenPGP {
|
|||
input: impl Read + Send + Sync,
|
||||
output: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt = default_terminal()?;
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||
let prompt = default_handler()?;
|
||||
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
|
||||
write!(output, "{}", smex::encode(bytes))?;
|
||||
|
||||
|
@ -87,11 +95,37 @@ impl ShardExec for OpenPGP {
|
|||
key_discovery: Option<&Path>,
|
||||
input: impl Read + Send + Sync,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt = default_terminal()?;
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||
let prompt = default_handler()?;
|
||||
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn metadata(
|
||||
&self,
|
||||
key_discovery: Option<&Path>,
|
||||
input: impl Read + Send + Sync,
|
||||
output_pubkeys: &mut impl Write,
|
||||
output: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use keyfork_derive_openpgp::openpgp::{
|
||||
armor::{Kind, Writer},
|
||||
serialize::Marshal,
|
||||
};
|
||||
|
||||
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)]
|
||||
|
@ -141,6 +175,20 @@ pub enum ShardSubcommands {
|
|||
/// The path to discover private keys from.
|
||||
key_discovery: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Decrypt metadata for a shardfile, including the threshold and the public keys. Public keys
|
||||
/// are serialized to a file.
|
||||
Metadata {
|
||||
/// The path to load the Shardfile from.
|
||||
shardfile: PathBuf,
|
||||
|
||||
/// The path to write public keys to.
|
||||
#[arg(long)]
|
||||
output_pubkeys: PathBuf,
|
||||
|
||||
/// The path to discover private keys from.
|
||||
key_discovery: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ShardSubcommands {
|
||||
|
@ -209,6 +257,31 @@ impl ShardSubcommands {
|
|||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||
}
|
||||
}
|
||||
ShardSubcommands::Metadata {
|
||||
shardfile,
|
||||
output_pubkeys,
|
||||
key_discovery,
|
||||
} => {
|
||||
let shard_content = std::fs::read_to_string(shardfile)?;
|
||||
if shard_content.contains("BEGIN PGP MESSAGE") {
|
||||
let _ = format.insert(Format::OpenPGP(OpenPGP));
|
||||
}
|
||||
|
||||
let mut output_pubkeys_file = std::fs::File::create(output_pubkeys)?;
|
||||
|
||||
match format {
|
||||
Some(Format::OpenPGP(o)) => o.metadata(
|
||||
key_discovery.as_deref(),
|
||||
shard_content.as_bytes(),
|
||||
&mut output_pubkeys_file,
|
||||
&mut stdout,
|
||||
),
|
||||
Some(Format::P256(_p)) => {
|
||||
todo!()
|
||||
}
|
||||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,311 +0,0 @@
|
|||
use super::Keyfork;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
|
||||
|
||||
use card_backend_pcsc::PcscBackend;
|
||||
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
|
||||
|
||||
use keyfork_derive_openpgp::{
|
||||
openpgp::{
|
||||
self,
|
||||
armor::{Kind, Writer},
|
||||
packet::UserID,
|
||||
serialize::Marshal,
|
||||
types::KeyFlags,
|
||||
Cert,
|
||||
},
|
||||
XPrv,
|
||||
};
|
||||
use keyfork_derive_path_data::paths;
|
||||
use keyfork_derive_util::DerivationIndex;
|
||||
use keyfork_mnemonic::Mnemonic;
|
||||
use keyfork_prompt::{
|
||||
default_terminal,
|
||||
validators::{SecurePinValidator, Validator},
|
||||
DefaultTerminal, Message, PromptHandler,
|
||||
};
|
||||
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Invalid PIN length: {0}")]
|
||||
pub struct PinLength(usize);
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
||||
// TODO: refactor to use mnemonic derived seed instead of 256 bit entropy to allow for possible
|
||||
// recovery in the future.
|
||||
fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
|
||||
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 = 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)
|
||||
}
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
pub enum WizardSubcommands {
|
||||
GenerateShardSecret(GenerateShardSecret),
|
||||
BottomsUp(BottomsUp),
|
||||
}
|
||||
|
||||
/// Create a 256 bit secret and shard the secret to smart cards.
|
||||
///
|
||||
/// Smart cards will need to be plugged in periodically during the wizard, where they will be
|
||||
/// factory reset and provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered
|
||||
/// with `keyfork recover shard` or `keyfork recover remote-shard`. The share file will be printed
|
||||
/// to standard output.
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct GenerateShardSecret {
|
||||
/// The minimum amount of keys required to decrypt the secret.
|
||||
#[arg(long)]
|
||||
threshold: u8,
|
||||
|
||||
/// The maximum amount of shards.
|
||||
#[arg(long)]
|
||||
max: u8,
|
||||
|
||||
/// The amount of smart cards to provision per-shard.
|
||||
#[arg(long, default_value = "1")]
|
||||
keys_per_shard: u8,
|
||||
|
||||
/// The file to write the generated shard file to.
|
||||
#[arg(long)]
|
||||
output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates,
|
||||
/// deriving the default OpenPGP certificate for the secret.
|
||||
///
|
||||
/// This command was purpose-built for DEFCON and is not intended to be used normally, as it
|
||||
/// implies keys used for sharding have been generated by a custom source.
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct BottomsUp {
|
||||
/// The location of OpenPGP certificates to use when sharding.
|
||||
key_discovery: PathBuf,
|
||||
|
||||
/// The minimum amount of keys required to decrypt the secret.
|
||||
#[arg(long)]
|
||||
threshold: u8,
|
||||
|
||||
/// The file to write the generated shard file to.
|
||||
#[arg(long)]
|
||||
output_shardfile: PathBuf,
|
||||
|
||||
/// The file to write the generated OpenPGP certificate to.
|
||||
#[arg(long)]
|
||||
output_cert: PathBuf,
|
||||
|
||||
/// The User ID for the generated OpenPGP certificate.
|
||||
#[arg(long, default_value = "Disaster Recovery")]
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
impl WizardSubcommands {
|
||||
// dispatch
|
||||
fn handle(&self) -> Result<()> {
|
||||
match self {
|
||||
WizardSubcommands::GenerateShardSecret(gss) => gss.handle(),
|
||||
WizardSubcommands::BottomsUp(bu) => bu.handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateShardSecret {
|
||||
fn handle(&self) -> Result<()> {
|
||||
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
|
||||
let mut pm = default_terminal()?;
|
||||
let mut certs = vec![];
|
||||
let mut seen_cards: HashSet<String> = HashSet::new();
|
||||
let stdout = std::io::stdout();
|
||||
if self.output.is_none() {
|
||||
assert!(
|
||||
!stdout.is_terminal(),
|
||||
"not printing shard to terminal, redirect output"
|
||||
);
|
||||
}
|
||||
|
||||
let user_pin_validator = SecurePinValidator {
|
||||
min_length: Some(6),
|
||||
..Default::default()
|
||||
}
|
||||
.to_fn();
|
||||
let admin_pin_validator = SecurePinValidator {
|
||||
min_length: Some(8),
|
||||
..Default::default()
|
||||
}
|
||||
.to_fn();
|
||||
|
||||
for index in 0..self.max {
|
||||
let cert = derive_key(seed, index)?;
|
||||
for i in 0..self.keys_per_shard {
|
||||
pm.prompt_message(Message::Text(format!(
|
||||
"Please remove all keys and insert key #{} for user #{}",
|
||||
(i as u16) + 1,
|
||||
(index as u16) + 1,
|
||||
)))?;
|
||||
let card_backend = loop {
|
||||
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
|
||||
break c;
|
||||
}
|
||||
pm.prompt_message(Message::Text(
|
||||
"No smart card was found. Please plug in a smart card and press enter"
|
||||
.to_string(),
|
||||
))?;
|
||||
};
|
||||
let user_pin = pm.prompt_validated_passphrase(
|
||||
"Please enter the new smartcard User PIN: ",
|
||||
3,
|
||||
&user_pin_validator,
|
||||
)?;
|
||||
let admin_pin = pm.prompt_validated_passphrase(
|
||||
"Please enter the new smartcard Admin PIN: ",
|
||||
3,
|
||||
&admin_pin_validator,
|
||||
)?;
|
||||
factory_reset_current_card(
|
||||
&mut seen_cards,
|
||||
user_pin.trim(),
|
||||
admin_pin.trim(),
|
||||
&cert,
|
||||
card_backend,
|
||||
)?;
|
||||
}
|
||||
certs.push(cert);
|
||||
}
|
||||
|
||||
let opgp = OpenPGP::<DefaultTerminal>::new();
|
||||
|
||||
if let Some(output_file) = self.output.as_ref() {
|
||||
let output = File::create(output_file)?;
|
||||
opgp.shard_and_encrypt(self.threshold, certs.len() as u8, &seed, &certs[..], output)?;
|
||||
} else {
|
||||
opgp.shard_and_encrypt(
|
||||
self.threshold,
|
||||
certs.len() as u8,
|
||||
&seed,
|
||||
&certs[..],
|
||||
std::io::stdout(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BottomsUp {
|
||||
fn handle(&self) -> Result<()> {
|
||||
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
|
||||
let mnemonic = Mnemonic::from_array(entropy);
|
||||
let seed = mnemonic.generate_seed(None);
|
||||
|
||||
// TODO: should this allow for customizing the account index from 0? Potential for key reuse
|
||||
// errors.
|
||||
let path = paths::OPENPGP_DISASTER_RECOVERY
|
||||
.clone()
|
||||
.chain_push(DerivationIndex::new(0, true)?);
|
||||
let subkeys = [
|
||||
KeyFlags::empty().set_certification(),
|
||||
KeyFlags::empty().set_signing(),
|
||||
KeyFlags::empty()
|
||||
.set_transport_encryption()
|
||||
.set_storage_encryption(),
|
||||
KeyFlags::empty().set_authentication(),
|
||||
];
|
||||
let xprv = XPrv::new(seed)
|
||||
.expect("could not construct master key from seed")
|
||||
.derive_path(&path)?;
|
||||
let userid = UserID::from(self.user_id.as_str());
|
||||
|
||||
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
|
||||
let certfile = File::create(&self.output_cert)?;
|
||||
let mut w = Writer::new(certfile, Kind::PublicKey)?;
|
||||
cert.serialize(&mut w)?;
|
||||
w.finalize()?;
|
||||
|
||||
let opgp = OpenPGP::<DefaultTerminal>::new();
|
||||
let certs = OpenPGP::<DefaultTerminal>::discover_certs(&self.key_discovery)?;
|
||||
|
||||
let shardfile = File::create(&self.output_shardfile)?;
|
||||
opgp.shard_and_encrypt(
|
||||
self.threshold,
|
||||
certs.len() as u8,
|
||||
&entropy,
|
||||
&certs[..],
|
||||
shardfile,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct Wizard {
|
||||
#[command(subcommand)]
|
||||
command: WizardSubcommands,
|
||||
}
|
||||
|
||||
impl Wizard {
|
||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||
self.command.handle()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -2,20 +2,19 @@ use std::collections::HashMap;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Mnemonic {
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Provisioner {
|
||||
pub name: String,
|
||||
pub account: u32,
|
||||
pub identifier: String,
|
||||
pub metadata: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub mnemonic: Mnemonic,
|
||||
pub provisioner: Vec<Provisioner>,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
@ -8,8 +7,10 @@ use clap::Parser;
|
|||
|
||||
use keyfork_bin::{Bin, ClosureBin};
|
||||
|
||||
pub mod clap_ext;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod openpgp_card;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let bin = ClosureBin::new(|| {
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
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 {
|
||||
break user_pin;
|
||||
}
|
||||
pm.prompt_message(Message::Text("User PINs did not match. Retrying.".into()))?;
|
||||
};
|
||||
|
||||
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 {
|
||||
break admin_pin;
|
||||
}
|
||||
pm.prompt_message(Message::Text("Admin PINs did not match. Retrying.".into()))?;
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
[package]
|
||||
name = "keyfork-qrcode"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
repository = "https://git.distrust.co/public/keyfork"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
|
@ -15,8 +18,9 @@ decode-backend-zbar = ["dep:keyfork-zbar"]
|
|||
|
||||
[dependencies]
|
||||
keyfork-bug = { workspace = true }
|
||||
keyfork-zbar = { workspace = true, optional = true }
|
||||
keyfork-zbar = { workspace = true, optional = true, features = ["image"] }
|
||||
image = { workspace = true, default-features = false, features = ["jpeg"] }
|
||||
rqrr = { version = "0.6.0", optional = true }
|
||||
rqrr = { version = "0.9.0", optional = true }
|
||||
thiserror = { workspace = true }
|
||||
v4l = { workspace = true }
|
||||
cfg-if = "1.0.0"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
//!
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use keyfork_qrcode::scan_camera;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let output = scan_camera(Duration::from_secs(60 * 10), 0)?;
|
||||
let output = scan_camera(Duration::from_secs(15), 0)?;
|
||||
if let Some(scanned_text) = output {
|
||||
println!("{scanned_text}");
|
||||
}
|
||||
|
|
|
@ -2,20 +2,23 @@
|
|||
|
||||
use keyfork_bug as bug;
|
||||
|
||||
use image::io::Reader as ImageReader;
|
||||
use bug::POISONED_MUTEX;
|
||||
use image::{ImageBuffer, ImageReader, Luma};
|
||||
use std::{
|
||||
io::{Cursor, Write},
|
||||
time::{Duration, Instant},
|
||||
process::{Command, Stdio},
|
||||
sync::{mpsc::channel, Arc, Condvar, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use v4l::{
|
||||
buffer::Type,
|
||||
io::{userptr::Stream, traits::CaptureStream},
|
||||
io::{traits::CaptureStream, userptr::Stream},
|
||||
video::Capture,
|
||||
FourCC,
|
||||
Device,
|
||||
Device, FourCC,
|
||||
};
|
||||
|
||||
type Image = ImageBuffer<Luma<u8>, Vec<u8>>;
|
||||
|
||||
/// A QR code could not be generated.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum QRGenerationError {
|
||||
|
@ -34,10 +37,10 @@ pub enum QRCodeScanError {
|
|||
/// The camera could not load the requested format.
|
||||
#[error("Camera could not use {expected} format, instead used {actual}")]
|
||||
CameraGaveBadFormat {
|
||||
/// The expected format, in FourCC format.
|
||||
/// The expected format, in `FourCC` format.
|
||||
expected: String,
|
||||
|
||||
/// The actual format, in FourCC format.
|
||||
/// The actual format, in `FourCC` format.
|
||||
actual: String,
|
||||
},
|
||||
|
||||
|
@ -102,60 +105,188 @@ pub fn qrencode(
|
|||
|
||||
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
|
||||
|
||||
/// Continuously scan the `index`-th camera for a QR code.
|
||||
#[cfg(feature = "decode-backend-rqrr")]
|
||||
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||
let device = Device::new(index)?;
|
||||
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||
fmt.fourcc = FourCC::new(b"MPG1");
|
||||
device.set_format(&fmt)?;
|
||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||
let start = Instant::now();
|
||||
trait Scanner {
|
||||
fn scan_image(&mut self, image: Image) -> Option<String>;
|
||||
}
|
||||
|
||||
while Instant::now()
|
||||
.duration_since(start)
|
||||
< timeout
|
||||
{
|
||||
let (buffer, _) = stream.next()?;
|
||||
let image = ImageReader::new(Cursor::new(buffer))
|
||||
.with_guessed_format()?
|
||||
.decode()?
|
||||
.to_luma8();
|
||||
let mut image = rqrr::PreparedImage::prepare(image);
|
||||
for grid in image.detect_grids() {
|
||||
if let Ok((_, content)) = grid.decode() {
|
||||
return Ok(Some(content))
|
||||
#[cfg(feature = "decode-backend-zbar")]
|
||||
mod zbar {
|
||||
use super::{Image, 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
impl Scanner for Zbar {
|
||||
fn scan_image(&mut self, image: Image) -> 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::{Image, Scanner};
|
||||
|
||||
pub struct Rqrr;
|
||||
|
||||
impl Scanner for Rqrr {
|
||||
fn scan_image(&mut self, image: Image) -> 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, clippy::cast_precision_loss)]
|
||||
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}");
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ScanQueue {
|
||||
shutdown: bool,
|
||||
images: Vec<Image>,
|
||||
}
|
||||
|
||||
/// Continuously scan the `index`-th camera for a QR code.
|
||||
#[cfg(feature = "decode-backend-zbar")]
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The function may return an error if the hardware is unable to scan video or if an image could
|
||||
/// not be decoded.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The function may panic if a mutex is poisoned by a thread panicking, which should
|
||||
/// only happen during a mutex, or if it can't send a message over the mpsc channel.
|
||||
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||
let device = Device::new(index)?;
|
||||
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||
let mut fmt = device
|
||||
.format()
|
||||
.unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||
fmt.fourcc = FourCC::new(b"MPG1");
|
||||
device.set_format(&fmt)?;
|
||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||
let start = Instant::now();
|
||||
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
|
||||
|
||||
while Instant::now()
|
||||
.duration_since(start)
|
||||
< 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()));
|
||||
#[allow(unused)]
|
||||
let mut count = 0;
|
||||
|
||||
let thread_count = 4;
|
||||
|
||||
std::thread::scope(|scope| {
|
||||
let scan_queue = ScanQueue {
|
||||
shutdown: false,
|
||||
images: vec![],
|
||||
};
|
||||
|
||||
let arced = Arc::new((Mutex::new(scan_queue), Condvar::new()));
|
||||
let (tx, rx) = channel();
|
||||
|
||||
for _ in 0..thread_count {
|
||||
let tx = tx.clone();
|
||||
let arced = arced.clone();
|
||||
|
||||
scope.spawn(move || {
|
||||
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")
|
||||
}
|
||||
};
|
||||
|
||||
let (queue_mutex, condvar) = &*arced;
|
||||
loop {
|
||||
// NOTE: Carrying the `queue` variable through the loop, so we can
|
||||
// pass it through without re-locking, means that we don't drop the
|
||||
// lock on the mutex. Therefore, we unlock, then immediately
|
||||
// re-lock when we pass the value to wait_while().
|
||||
//
|
||||
// By holding onto the queue until we pass it back to the Condvar,
|
||||
// and checking shutdown, we ensure that there's no way we miss the
|
||||
// shutdown being set before we release the guard on the queue.
|
||||
let queue = queue_mutex.lock().expect(bug::bug!(POISONED_MUTEX));
|
||||
if queue.shutdown {
|
||||
break;
|
||||
}
|
||||
let mut queue = condvar
|
||||
.wait_while(queue, |queue| {
|
||||
queue.images.is_empty() && !queue.shutdown
|
||||
})
|
||||
.expect(bug::bug!(POISONED_MUTEX));
|
||||
if let Some(image) = queue.images.pop() {
|
||||
// drop the queue here since this is what's going to take time
|
||||
drop(queue);
|
||||
if let Some(content) = scanner.scan_image(image) {
|
||||
if tx.send(content).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
while Instant::now().duration_since(start) < timeout {
|
||||
if let Ok(content) = rx.try_recv() {
|
||||
arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true;
|
||||
arced.1.notify_all();
|
||||
return Ok(Some(content));
|
||||
}
|
||||
|
||||
count += 1;
|
||||
let (buffer, _) = stream.next()?;
|
||||
let image = ImageReader::new(Cursor::new(buffer))
|
||||
.with_guessed_format()?
|
||||
.decode()?
|
||||
.to_luma8();
|
||||
|
||||
arced
|
||||
.0
|
||||
.lock()
|
||||
.expect(bug::bug!(POISONED_MUTEX))
|
||||
.images
|
||||
.push(image);
|
||||
arced.1.notify_one();
|
||||
}
|
||||
|
||||
// dbg_elapsed(count, start);
|
||||
|
||||
arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true;
|
||||
arced.1.notify_all();
|
||||
|
||||
Ok(None)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,10 +5,13 @@ repository = "https://git.distrust.co/public/keyfork"
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = { version = "0.68", default-features = false, features = ["runtime"] }
|
||||
bindgen = { version = "0.70", default-features = false, features = ["runtime"] }
|
||||
pkg-config = "0.3"
|
||||
|
|
|
@ -45,7 +45,7 @@ fn main() -> Result<()> {
|
|||
if let Err(e) = generate_bindings_file() {
|
||||
eprintln!("Building zbar-sys failed: {e}");
|
||||
eprintln!("Ensure zbar headers, libclang, and pkg-config are installed");
|
||||
return Err(e)
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(clippy::unreadable_literal, clippy::pub_underscore_fields)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
[package]
|
||||
name = "keyfork-zbar"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
repository = "https://git.distrust.co/public/keyfork"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
|
|
|
@ -7,7 +7,7 @@ use std::{
|
|||
|
||||
use keyfork_zbar::{image::Image, image_scanner::ImageScanner};
|
||||
|
||||
use image::io::Reader as ImageReader;
|
||||
use image::ImageReader;
|
||||
use v4l::{
|
||||
buffer::Type,
|
||||
io::{traits::CaptureStream, userptr::Stream},
|
||||
|
@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.decode()?,
|
||||
);
|
||||
|
||||
if let Some(symbol) = scanner.scan_image(&image).get(0) {
|
||||
if let Some(symbol) = scanner.scan_image(&image).first() {
|
||||
println!("{}", String::from_utf8_lossy(symbol.data()));
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
@ -20,16 +20,13 @@ impl Image {
|
|||
|
||||
/// Link: [`sys::zbar_image_set_format`]
|
||||
///
|
||||
/// A FourCC code can be given in the format:
|
||||
/// A `FourCC` code can be given in the format:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// self.set_format(b"Y800")
|
||||
/// ```
|
||||
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
|
||||
let fourcc: u64 = fourcc[0] as u64
|
||||
| ((fourcc[1] as u64) << 8)
|
||||
| ((fourcc[2] as u64) << 16)
|
||||
| ((fourcc[3] as u64) << 24);
|
||||
let fourcc = std::os::raw::c_ulong::from(u32::from_le_bytes(*fourcc));
|
||||
unsafe { sys::zbar_image_set_format(self.inner, fourcc) }
|
||||
}
|
||||
|
||||
|
@ -43,12 +40,7 @@ impl Image {
|
|||
/// Accepts raw data in the configured format. See: [`Image::set_format`]
|
||||
fn set_data(&mut self, data: Vec<u8>) {
|
||||
unsafe {
|
||||
sys::zbar_image_set_data(
|
||||
self.inner,
|
||||
data.as_ptr().cast(),
|
||||
data.len() as u64,
|
||||
None,
|
||||
)
|
||||
sys::zbar_image_set_data(self.inner, data.as_ptr().cast(), data.len() as u64, None);
|
||||
}
|
||||
// keep data in self to avoid use after free when data goes out of scope
|
||||
let _ = self.inner_data.insert(data);
|
||||
|
@ -58,7 +50,7 @@ impl Image {
|
|||
#[cfg(feature = "image")]
|
||||
mod impls {
|
||||
use super::*;
|
||||
use image::{DynamicImage, GenericImageView};
|
||||
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma};
|
||||
|
||||
impl From<DynamicImage> for Image {
|
||||
fn from(value: DynamicImage) -> Self {
|
||||
|
@ -70,6 +62,17 @@ mod impls {
|
|||
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 {
|
||||
|
|
|
@ -22,7 +22,7 @@ pub struct ImageScanner {
|
|||
}
|
||||
|
||||
impl ImageScanner {
|
||||
/// create a new ImageScanner.
|
||||
/// Create a new `ImageScanner`.
|
||||
///
|
||||
/// Link: [`sys::zbar_image_scanner_create`]
|
||||
pub fn new() -> Self {
|
||||
|
@ -31,7 +31,7 @@ impl ImageScanner {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set a configuration option for the ImageScanner.
|
||||
/// Set a configuration option.
|
||||
///
|
||||
/// Link: [`sys::zbar_image_scanner_set_config`]
|
||||
///
|
||||
|
@ -58,10 +58,7 @@ impl ImageScanner {
|
|||
/// Link: [`sys::zbar_scan_image`]
|
||||
///
|
||||
/// TODO: return an iterator over scanned values
|
||||
pub fn scan_image(
|
||||
&mut self,
|
||||
image: &Image,
|
||||
) -> Vec<Symbol> {
|
||||
pub fn scan_image(&mut self, image: &Image) -> Vec<Symbol> {
|
||||
unsafe { sys::zbar_scan_image(self.inner, image.inner) };
|
||||
let mut result = vec![];
|
||||
let mut symbol = unsafe { sys::zbar_image_first_symbol(image.inner) };
|
||||
|
@ -70,7 +67,7 @@ impl ImageScanner {
|
|||
let symbol_data = unsafe { sys::zbar_symbol_get_data(symbol) };
|
||||
let symbol_data_len = unsafe { sys::zbar_symbol_get_data_length(symbol) };
|
||||
let symbol_slice = unsafe {
|
||||
std::slice::from_raw_parts(symbol_data as *const u8, symbol_data_len as usize)
|
||||
std::slice::from_raw_parts(symbol_data.cast::<u8>(), symbol_data_len as usize)
|
||||
};
|
||||
result.push(Symbol::new(symbol_type, symbol_slice));
|
||||
symbol = unsafe { sys::zbar_symbol_next(symbol) };
|
||||
|
|
|
@ -11,6 +11,6 @@ pub use sys::zbar_config_e as Config;
|
|||
pub use sys::zbar_modifier_e as Modifier;
|
||||
pub use sys::zbar_orientation_e as Orientation;
|
||||
|
||||
pub mod image_scanner;
|
||||
pub mod image;
|
||||
pub mod image_scanner;
|
||||
pub mod symbol;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
//!
|
||||
//! A Symbol represents some form of encoded data.
|
||||
|
||||
#![allow(clippy::used_underscore_binding)]
|
||||
|
||||
use super::sys;
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "keyfork-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
assert_cmd = "2.0.16"
|
||||
keyforkd = { workspace = true, features = ["default"] }
|
||||
sequoia-openpgp = { workspace = true, features = ["crypto-nettle"] }
|
|
@ -0,0 +1 @@
|
|||
mod openpgp;
|
|
@ -0,0 +1,56 @@
|
|||
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");
|
||||
}
|
||||
assert!(key_formats.is_empty(), "remaining key formats: {key_formats:?}");
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
mod derive;
|
|
@ -0,0 +1,4 @@
|
|||
#![allow(missing_docs)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod keyfork;
|
|
@ -4,6 +4,9 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -45,6 +45,7 @@ use std::process::ExitCode;
|
|||
/// A result that may contain any error.
|
||||
pub type ProcessResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn report_err(e: Box<dyn std::error::Error>) {
|
||||
eprintln!("Unable to run command: {e}");
|
||||
let mut source = e.source();
|
||||
|
@ -68,7 +69,7 @@ pub trait Bin {
|
|||
#[allow(clippy::missing_errors_doc)]
|
||||
fn validate_args(&self, args: impl Iterator<Item = String>) -> ProcessResult<Self::Args>;
|
||||
|
||||
/// Run the binary
|
||||
/// Run the binary
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
fn run(&self, args: Self::Args) -> ProcessResult;
|
||||
|
||||
|
@ -102,10 +103,13 @@ pub trait Bin {
|
|||
|
||||
/// A Bin that doesn't take any arguments.
|
||||
pub struct ClosureBin<F: Fn() -> ProcessResult> {
|
||||
closure: F
|
||||
closure: F,
|
||||
}
|
||||
|
||||
impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
|
||||
impl<F> ClosureBin<F>
|
||||
where
|
||||
F: Fn() -> ProcessResult,
|
||||
{
|
||||
/// Create a new Bin from a closure.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -120,13 +124,14 @@ impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
|
|||
/// bin.main();
|
||||
/// ```
|
||||
pub fn new(closure: F) -> Self {
|
||||
Self {
|
||||
closure
|
||||
}
|
||||
Self { closure }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Bin for ClosureBin<F> where F: Fn() -> ProcessResult {
|
||||
impl<F> Bin for ClosureBin<F>
|
||||
where
|
||||
F: Fn() -> ProcessResult,
|
||||
{
|
||||
type Args = ();
|
||||
|
||||
fn validate_args(&self, _args: impl Iterator<Item = String>) -> ProcessResult<Self::Args> {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[package]
|
||||
name = "keyfork-bug"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -16,6 +16,12 @@
|
|||
//! ```
|
||||
//!
|
||||
//! ```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;
|
||||
//!
|
||||
|
@ -83,6 +89,29 @@ macro_rules! bug {
|
|||
}};
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "keyfork-crossterm-ioctl-shim"
|
||||
version = "0.1.0"
|
||||
description = "A shim for working with crossterm ioctls with custom fd's"
|
||||
repository = "https://git.distrust.co/public/keyfork"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2.172"
|
|
@ -0,0 +1,113 @@
|
|||
//! A shim for replacing some Crossterm methods with methods tied to a file descriptor.
|
||||
|
||||
use libc::termios as Termios;
|
||||
use std::{os::fd::RawFd, io::IsTerminal};
|
||||
|
||||
/// The provided file descriptor was not a terminal.
|
||||
#[derive(Debug)]
|
||||
pub struct NotATerminal;
|
||||
|
||||
impl std::fmt::Display for NotATerminal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("The provided file descriptor is not a terminal")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NotATerminal {}
|
||||
|
||||
/// A terminal controller.
|
||||
pub struct TerminalIoctl {
|
||||
fd: RawFd,
|
||||
stored_termios: Option<Termios>,
|
||||
}
|
||||
|
||||
type Result<T, E = std::io::Error> = std::result::Result<T, E>;
|
||||
|
||||
fn assert_io(result: i32) -> Result<i32> {
|
||||
if result == -1 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalIoctl {
|
||||
/// Construct a new controller for the given file descriptor.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The method may return an error if the file descriptor is not a terminal.
|
||||
pub fn new(fd: RawFd) -> Result<Self> {
|
||||
// SAFETY: We do not invoke any function that closes the file descriptor, and
|
||||
// the borrowed fd is dropped as the function returns.
|
||||
let borrowed_fd = unsafe {
|
||||
std::os::fd::BorrowedFd::borrow_raw(fd)
|
||||
};
|
||||
if !borrowed_fd.is_terminal() {
|
||||
return Err(std::io::Error::other(NotATerminal));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
fd,
|
||||
stored_termios: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_termios(&self) -> Result<Termios> {
|
||||
let mut termios = unsafe { std::mem::zeroed() };
|
||||
assert_io(unsafe { libc::tcgetattr(self.fd, &mut termios) })?;
|
||||
Ok(termios)
|
||||
}
|
||||
|
||||
/// Enable raw mode for the given terminal.
|
||||
///
|
||||
/// Replaces: [`crossterm::terminal::enable_raw_mode`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The method may return an error if
|
||||
pub fn enable_raw_mode(&mut self) -> Result<()> {
|
||||
if self.stored_termios.is_none() {
|
||||
let mut termios = self.get_termios()?;
|
||||
let original_mode_ios = termios;
|
||||
|
||||
unsafe { libc::cfmakeraw(&mut termios) };
|
||||
assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?;
|
||||
self.stored_termios = Some(original_mode_ios);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disable raw mode for the given terminal.
|
||||
///
|
||||
/// Replaces: [`crossterm::terminal::disable_raw_mode`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The method may propagate errors encountered when interacting with the terminal.
|
||||
pub fn disable_raw_mode(&mut self) -> Result<()> {
|
||||
if let Some(termios) = self.stored_termios.take() {
|
||||
assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the size for the given terminal.
|
||||
///
|
||||
/// Replaces: [`crossterm::terminal::size`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The method may propagate errors encountered when interacting with the terminal.
|
||||
pub fn size(&self) -> Result<(u16, u16)> {
|
||||
let mut size = libc::winsize {
|
||||
ws_row: 0,
|
||||
ws_col: 0,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
assert_io(unsafe { libc::ioctl(self.fd, libc::TIOCGWINSZ, &mut size) })?;
|
||||
Ok((size.ws_col, size.ws_row))
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
# Build only pushed (merged) master or any pull request. This avoids the
|
||||
# pull request to be build twice.
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
language: rust
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
os:
|
||||
- linux
|
||||
- windows
|
||||
- osx
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
quiet: true
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
|
||||
before_script:
|
||||
- export PATH=$PATH:/home/travis/.cargo/bin
|
||||
- rustup component add rustfmt
|
||||
- rustup component add clippy
|
||||
|
||||
script:
|
||||
- cargo fmt --version
|
||||
- rustup --version
|
||||
- rustc --version
|
||||
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo fmt --all -- --check; fi
|
||||
- cargo clippy -- -D clippy::all
|
||||
- cargo build
|
||||
- cargo test --lib -- --nocapture --test-threads 1
|
||||
- cargo test --lib --features serde -- --nocapture --test-threads 1
|
||||
- cargo test --lib --features event-stream -- --nocapture --test-threads 1
|
||||
- cargo test --all-features -- --nocapture --test-threads 1
|
||||
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo package; fi
|
|
@ -1,744 +0,0 @@
|
|||
# Version 0.27.1
|
||||
|
||||
## Added ⭐
|
||||
- Add support for (de)serializing `Reset` `Color`
|
||||
|
||||
# Version 0.27
|
||||
|
||||
## Added ⭐
|
||||
|
||||
- Add `NO_COLOR` support (https://no-color.org/)
|
||||
- Add option to force overwrite `NO_COLOR` (#802)
|
||||
- Add support for scroll left/right events on windows and unix systems (#788).
|
||||
- Add `window_size` function to fetch pixel width/height of screen for more sophisticated rendering in terminals.
|
||||
- Add support for deserializing hex color strings to `Color` e.g #fffff.
|
||||
|
||||
## Changes
|
||||
|
||||
- Make the events module an optional feature `events` (to make crossterm more lightweight) (#776)
|
||||
|
||||
## Breaking ⚠️
|
||||
|
||||
- Set minimum rustc version to 1.58 (#798)
|
||||
- Change all error types to `std::io::Result` (#765)
|
||||
|
||||
# Version 0.26.1
|
||||
|
||||
## Added ⭐
|
||||
|
||||
- Add synchronized output/update control (#756)
|
||||
- Add kitty report alternate keys functionality (#754)
|
||||
- Updates dev dependencies.
|
||||
|
||||
## Fixed 🐛
|
||||
- Fix icorrect return in kitty keyboard enhancement check (#751)
|
||||
- Fix panic when using `use-dev-tty` feature flag (#762)
|
||||
|
||||
# Version 0.26.0
|
||||
## Added ⭐
|
||||
|
||||
- Add `SetCursorStyle` to set the cursor apearance and visibility. (#742)
|
||||
- Add a function to check if kitty keyboard enhancement protocol is available. (#732)
|
||||
- Add filedescriptors poll in order to move away from mio in the future (can be used via `use-dev-tty`). (#735)
|
||||
|
||||
## Fixed 🐛
|
||||
- Improved F1-F4 handling for kitty keyboard protocol. (#736)
|
||||
- Improved parsing of event types/modifiers with certain keys for kitty protocol. (#716)
|
||||
|
||||
## Breaking ⚠️
|
||||
- Remove `SetCursorShape` in favour of `SetCursorStyle`. (#742)
|
||||
- Make Windows resize event match `terminal::size` (#714)
|
||||
- Rust 1.58 or later is now required.
|
||||
- Add key release event for windows. (#745)
|
||||
|
||||
# Version 0.25.0
|
||||
BREAKING: `Copy` trait is removed from `Event`, you can keep it by removing the "bracked-paste" feature flag. However this flag might be standardized in the future.
|
||||
We removed the `Copy` from `Event` because the new `Paste` event, which contains a pasted string into the terminal, which is a non-copy string.
|
||||
|
||||
- Add ability to paste a string in into the terminal and fetch the pasted string via events (see `Event::Paste` and `EnableBracketedPaste `).
|
||||
- Add support for functional key codes from kitty keyboard protocol. Try out by `PushKeyboardEnhancementFlags`. This protocol allows for:
|
||||
- See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers
|
||||
- Press, Repeat, Release event kinds.
|
||||
- SUPER, HYPER, META modifiers.
|
||||
- Media keycodes
|
||||
- Right/left SHIFT, Control, Alt, Super, Hyper, Meta
|
||||
- IsoLevel3Shift, IsoLevel5Shift
|
||||
- Capslock, scroll lock, numlock
|
||||
- Printscreen, pauze, menue, keyboard begin.
|
||||
- Create `SetStyle` command to allow setting various styling in one command.
|
||||
- Terminal Focus events (see `Event::FocusGained` and `Event::FocusLost`)
|
||||
|
||||
# Version 0.24.0
|
||||
- Add DoubleUnderlined, Undercurled, Underdots the text, Underdotted, Underdashes, Underdashed attributes and allow coloring their foreground / background color.
|
||||
- Fix windows unicode character parsing, this fixed various key combinations and support typing unicode characters.
|
||||
- Consistency and better documentation on mouse cursor operations (BREAKING CHANGE).
|
||||
- MoveTo, MoveToColumn, MoveToRow are 0-based. (left top most cell is 0,0). Moving like this is absolute
|
||||
- MoveToNextLine, MoveToPreviousLine, MoveUp, MoveDown, MoveRight, MoveLeft are 1-based,. Moving like this is relative. Moving 1 left means moving 1 left. Moving 0 to the left is not possible, wikipedia states that most terminals will just default to 1.
|
||||
- terminal::size returns error when previously it returned (0,0).
|
||||
- Remove println from serialisation code.
|
||||
- Fix mouse up for middle and right buttons.
|
||||
- Fix escape codes on Git-Bash + Windows Terminal / Alacritty / WezTerm.
|
||||
- Add support for cursor keys in application mode.
|
||||
# Version 0.23.2
|
||||
- Update signal-hook and mio to version 0.8.
|
||||
|
||||
# Version 0.23.1
|
||||
- Fix control key parsing problem.
|
||||
|
||||
# Version 0.23
|
||||
- Update dependencies.
|
||||
- Add 0 check for all cursor functions to prevent undefined behaviour.
|
||||
- Add CSIu key parsing for unix.
|
||||
- Improve control character window key parsing supporting (e.g. CTRL [ and ])
|
||||
- Update library to 2021 edition.
|
||||
|
||||
# Version 0.22.1
|
||||
- Update yanked version crossterm-winapi and move to crossterm-winapi 0.9.0.
|
||||
- Changed panic to error when calling disable-mouse capture without setting it first.
|
||||
- Update bitflags dependency.
|
||||
|
||||
# Version 0.22
|
||||
- Fix serde Color serialisation/deserialization inconsistency.
|
||||
- Update crossterm-winapi 0.8.1 to fix panic for certain mouse events
|
||||
|
||||
# Version 0.21
|
||||
- Expose `is_raw` function.
|
||||
- Add 'purge' option on unix system, this clears the entire screen buffer.
|
||||
- Improve serialisation for color enum values.
|
||||
|
||||
# Version 0.20
|
||||
- Update from signal-hook with 'mio-feature flag' to signal-hook-mio 0.2.1.
|
||||
- Manually implements Eq, PartialEq and Hash for KeyEvent improving equality checks and hash calculation.
|
||||
- `crossterm::ErrorKind` to `io::Error`.
|
||||
- Added Cursor Shape Support.
|
||||
- Add support for function keys F13...F20.
|
||||
- Support taking any Display in `SetTitle` command.
|
||||
- Remove lazy_static dependency.
|
||||
- Remove extra Clone bounds in the style module.
|
||||
- Add `MoveToRow` command.
|
||||
- Remove writer parameter from execute_winapi
|
||||
|
||||
# Version 0.19
|
||||
- Use single thread for async event reader.
|
||||
- Patch timeout handling for event polling this was not working correctly.
|
||||
- Add unix support for more key combinations mainly complex ones with ALT/SHIFT/CTRL.
|
||||
- Derive `PartialEq` and `Eq` for ContentStyle
|
||||
- Fix windows resize event size, this used to be the buffer size but is screen size now.
|
||||
- Change `Command::ansi_code` to `Command::write_ansi`, this way the ansi code will be written to given formatter.
|
||||
|
||||
# Version 0.18.2
|
||||
- Fix panic when only setting bold and redirecting stdout.
|
||||
- Use `tty_fd` for set/get terminal attributes
|
||||
|
||||
# Version 0.18.1
|
||||
- Fix enabling ANSI support when stdout is redirected
|
||||
- Update crossterm-winapi to 0.6.2
|
||||
|
||||
# Version 0.18.0
|
||||
- Fix get position bug
|
||||
- Fix windows 8 or lower write to user-given stdout instead of stdout.
|
||||
- Make MoveCursor(Left/Right/Up/Dow) command with input 0 not move.
|
||||
- Switch to futures-core to reduce dependencies.
|
||||
- Command API restricts to only accept `std::io::Write`
|
||||
- Make `supports_ansi` public
|
||||
- Implement ALT + numbers windows systems.
|
||||
|
||||
# Version 0.17.7
|
||||
- Fix cursor position retrieval bug linux.
|
||||
|
||||
# Version 0.17.6
|
||||
- Add functionality to retrieve color based on passed ansi code.
|
||||
- Switch from 'futures' to 'futures-util' crate to reduce dependency count
|
||||
- Mio 0.7 update
|
||||
- signal-hook update
|
||||
- Make windows raw_mode act on CONIN$
|
||||
- Added From<(u8, u8, u8)> Trait to Color::Rgb Enum
|
||||
- Implement Color::try_from()
|
||||
- Implement styler traits for `&'a str`
|
||||
|
||||
# Version 0.17.5
|
||||
- Improved support of keymodifier for linux, arrow keys, function keys, home keys etc.
|
||||
- Add `SetTitle` command to change the terminal title.
|
||||
- Mio 0.7 update
|
||||
|
||||
# Version 0.17.4
|
||||
- Add macros for `Colorize` and `Styler` impls, add an impl for `String`
|
||||
- Add shift modifier to uppercase char events on unix
|
||||
|
||||
# Version 0.17.3
|
||||
- Fix get terminal size mac os, this did not report the correct size.
|
||||
|
||||
# Version 0.17.2
|
||||
- Windows unicode support
|
||||
|
||||
# Version 0.17.1
|
||||
- Reverted bug in 0.17.0: "Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing.".
|
||||
- Support for querying whether the current instance is a TTY.
|
||||
|
||||
# Version 0.17
|
||||
- Impl Display for MoveToColumn, MoveToNextLine, MoveToPreviousLine
|
||||
- Make unix event reader always use `/dev/tty`.
|
||||
- Direct write command ansi_codes into formatter instead of double allocation.
|
||||
- Add NONE flag to KeyModifiers
|
||||
- Add support for converting chars to StylizedContent
|
||||
- Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing.
|
||||
|
||||
# Version 0.16.0
|
||||
- Change attribute vector in `ContentStyle` to bitmask.
|
||||
- Add `SetAttributes` command.
|
||||
- Add `Attributes` type, which is a bitfield of enabled attributes.
|
||||
- Remove `exit()`, was useless.
|
||||
|
||||
# Version 0.15.0
|
||||
- Fix CTRL + J key combination. This used to return an ENTER event.
|
||||
- Add a generic implementation `Command` for `&T: Command`. This allows commands to be queued by reference, as well as by value.
|
||||
- Remove unnecessary `Clone` trait bounds from `StyledContent`.
|
||||
- Add `StyledContent::style_mut`.
|
||||
- Handle error correctly for `execute!` and `queue!`.
|
||||
- Fix minor syntax bug in `execute!` and `queue!`.
|
||||
- Change `ContentStyle::apply` to take self by value instead of reference, to prevent an unnecessary extra clone.
|
||||
- Added basic trait implementations (`Debug`, `Clone`, `Copy`, etc) to all of the command structs
|
||||
- `ResetColor` uses `&'static str` instead of `String`
|
||||
|
||||
# Version 0.14.2
|
||||
- Fix TIOCGWINSZ for FreeBSD
|
||||
|
||||
# Version 0.14.1
|
||||
- Made windows cursor position relative to the window instead absolute to the screen buffer windows.
|
||||
- Fix windows bug with `queue` macro were it consumed a type and required an type to be `Copy`.
|
||||
|
||||
# Version 0.14
|
||||
|
||||
- Replace the `input` module with brand new `event` module
|
||||
- Terminal Resize Events
|
||||
- Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and
|
||||
- futures Stream (feature 'event-stream')
|
||||
- Poll/read API
|
||||
- It's **highly recommended** to read the
|
||||
[Upgrade from 0.13 to 0.14](https://github.com/crossterm-rs/crossterm/wiki/Upgrade-from-0.13-to-0.14)
|
||||
documentation
|
||||
- Replace `docs/UPGRADE.md` with the [Upgrade Paths](https://github.com/crossterm-rs/crossterm/wiki#upgrade-paths)
|
||||
documentation
|
||||
- Add `MoveToColumn`, `MoveToPreviousLine`, `MoveToNextLine` commands
|
||||
- Merge `screen` module into `terminal`
|
||||
- Remove `screen::AlternateScreen`
|
||||
- Remove `screen::Rawscreen`
|
||||
* Move and rename `Rawscreen::into_raw_mode` and `Rawscreen::disable_raw_mode` to `terminal::enable_raw_mode` and `terminal::disable_raw_mode`
|
||||
- Move `screen::EnterAlternateScreen` and `screen::LeaveAlternateScreen` to `terminal::EnterAlternateScreen` and `terminal::LeaveAlternateScreen`
|
||||
- Replace `utils::Output` command with `style::Print` command
|
||||
- Fix enable/disable mouse capture commands on Windows
|
||||
- Allow trailing comma `queue!` & `execute!` macros
|
||||
|
||||
# Version 0.13.3
|
||||
|
||||
- Remove thread from AsyncReader on Windows.
|
||||
- Improve HANDLE management windows.
|
||||
|
||||
# Version 0.13.2
|
||||
|
||||
- New `input::stop_reading_thread()` function
|
||||
- Temporary workaround for the UNIX platform to stop the background
|
||||
reading thread and close the file descriptor
|
||||
- This function will be removed in the next version
|
||||
|
||||
# Version 0.13.1
|
||||
|
||||
- Async Reader fix, join background thread and avoid looping forever on windows.
|
||||
|
||||
# Version 0.13.0
|
||||
|
||||
**Major API-change, removed old-api**
|
||||
|
||||
- Remove `Crossterm` type
|
||||
- Remove `TerminalCursor`, `TerminalColor`, `Terminal`
|
||||
- Remove `cursor()`, `color()` , `terminal()`
|
||||
- Remove re-exports at root, accessible via `module::types` (`cursor::MoveTo`)
|
||||
- `input` module
|
||||
- Derive 'Copy' for 'KeyEvent'
|
||||
- Add the `EnableMouseCapture` and `EnableMouseCapture` commands
|
||||
- `cursor` module
|
||||
- Introduce static function `crossterm::cursor::position` in place of `TerminalCursor::pos`
|
||||
- Rename `Goto` to `MoveTo`
|
||||
- Rename `Up` to `MoveLeft`
|
||||
- Rename `Right` to `MoveRight`
|
||||
- Rename `Down` to `MoveDown`
|
||||
- Rename `BlinkOn` to `EnableBlinking`
|
||||
- Rename `BlinkOff` to `DisableBlinking`
|
||||
- Rename `ResetPos` to `ResetPosition`
|
||||
- Rename `SavePos` to `SavePosition`
|
||||
- `terminal`
|
||||
- Introduce static function `crossterm::terminal::size` in place of `Terminal::size`
|
||||
- Introduce static function `crossterm::terminal::exit` in place of `Terminal::exit`
|
||||
- `style module`
|
||||
- Rename `ObjectStyle` to `ContentStyle`. Now full names are used for methods
|
||||
- Rename `StyledObject` to `StyledContent` and made members private
|
||||
- Rename `PrintStyledFont` to `PrintStyledContent`
|
||||
- Rename `attr` method to `attribute`.
|
||||
- Rename `Attribute::NoInverse` to `NoReverse`
|
||||
- Update documentation
|
||||
- Made `Colored` private, user should use commands instead
|
||||
- Rename `SetFg` -> `SetForegroundColor`
|
||||
- Rename `SetBg` -> `SetBackgroundColor`
|
||||
- Rename `SetAttr` -> `SetAttribute`
|
||||
- Rename `ContentStyle::fg_color` -> `ContentStyle::foreground_color`
|
||||
- Rename `ContentStyle::bg_color` -> `ContentStyle::background_color`
|
||||
- Rename `ContentStyle::attrs` -> `ContentStyle::attributes`
|
||||
- Improve documentation
|
||||
- Unix terminal size calculation with TPUT
|
||||
|
||||
# Version 0.12.1
|
||||
|
||||
- Move all the `crossterm_` crates code was moved to the `crossterm` crate
|
||||
- `crossterm_cursor` is in the `cursor` module, etc.
|
||||
- All these modules are public
|
||||
- No public API breaking changes
|
||||
|
||||
# Version 0.12.0
|
||||
|
||||
- Following crates are deprecated and no longer maintained
|
||||
- `crossterm_cursor`
|
||||
- `crossterm_input`
|
||||
- `crossterm_screen`
|
||||
- `crossterm_style`
|
||||
- `crossterm_terminal`
|
||||
- `crossterm_utils`
|
||||
|
||||
## `crossterm_cursor` 0.4.0
|
||||
|
||||
- Fix examples link ([PR #6](https://github.com/crossterm-rs/crossterm-cursor/pull/6))
|
||||
- Sync documentation style ([PR #7](https://github.com/crossterm-rs/crossterm-cursor/pull/7))
|
||||
- Remove all references to the crossterm book ([PR #8](https://github.com/crossterm-rs/crossterm-cursor/pull/8))
|
||||
- Replace `RAW_MODE_ENABLED` with `is_raw_mode_enabled` ([PR #9](https://github.com/crossterm-rs/crossterm-cursor/pull/9))
|
||||
- Use `SyncReader` & `InputEvent::CursorPosition` for `pos_raw()` ([PR #10](https://github.com/crossterm-rs/crossterm-cursor/pull/10))
|
||||
|
||||
## `crossterm_input` 0.5.0
|
||||
|
||||
- Sync documentation style ([PR #4](https://github.com/crossterm-rs/crossterm-input/pull/4))
|
||||
- Sync `SyncReader::next()` Windows and UNIX behavior ([PR #5](https://github.com/crossterm-rs/crossterm-input/pull/5))
|
||||
- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-input/pull/6))
|
||||
- Mouse coordinates synchronized with the cursor ([PR #7](https://github.com/crossterm-rs/crossterm-input/pull/7))
|
||||
- Upper/left reported as `(0, 0)`
|
||||
- Fix bug that read sync didn't block (Windows) ([PR #8](https://github.com/crossterm-rs/crossterm-input/pull/8))
|
||||
- Refactor UNIX readers ([PR #9](https://github.com/crossterm-rs/crossterm-input/pull/9))
|
||||
- AsyncReader produces mouse events
|
||||
- One reading thread per application, not per `AsyncReader`
|
||||
- Cursor position no longer consumed by another `AsyncReader`
|
||||
- Implement sync reader for read_char (requires raw mode)
|
||||
- Fix `SIGTTIN` when executed under the LLDB
|
||||
- Add mio for reading from FD and more efficient polling (UNIX only)
|
||||
- Sync UNIX and Windows vertical mouse position ([PR #11](https://github.com/crossterm-rs/crossterm-input/pull/11))
|
||||
- Top is always reported as `0`
|
||||
|
||||
## `crossterm_screen` 0.3.2
|
||||
|
||||
- `to_alternate` switch back to main screen if it fails to switch into raw mode ([PR #4](https://github.com/crossterm-rs/crossterm-screen/pull/4))
|
||||
- Improve the documentation ([PR #5](https://github.com/crossterm-rs/crossterm-screen/pull/5))
|
||||
- Public API
|
||||
- Include the book content in the documentation
|
||||
- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-screen/pull/6))
|
||||
- New commands introduced ([PR #7](https://github.com/crossterm-rs/crossterm-screen/pull/7))
|
||||
- `EnterAlternateScreen`
|
||||
- `LeaveAlternateScreen`
|
||||
- Sync Windows and UNIX raw mode behavior ([PR #8](https://github.com/crossterm-rs/crossterm-screen/pull/8))
|
||||
|
||||
## `crossterm_style` 0.5.2
|
||||
|
||||
- Refactor ([PR #2](https://github.com/crossterm-rs/crossterm-style/pull/2))
|
||||
- Added unit tests
|
||||
- Improved documentation and added book page to `lib.rs`
|
||||
- Fixed bug with `SetBg` command, WinApi logic
|
||||
- Fixed bug with `StyledObject`, used stdout for resetting terminal color
|
||||
- Introduced `ResetColor` command
|
||||
- Sync documentation style ([PR #3](https://github.com/crossterm-rs/crossterm-style/pull/3))
|
||||
- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-style/pull/4))
|
||||
- Windows 7 grey/white foreground/intensity swapped ([PR #5](https://github.com/crossterm-rs/crossterm-style/pull/5))
|
||||
|
||||
## `crossterm_terminal` 0.3.2
|
||||
|
||||
- Removed `crossterm_cursor::sys` dependency ([PR #2](https://github.com/crossterm-rs/crossterm-terminal/pull/2))
|
||||
- Internal refactoring & documentation ([PR #3](https://github.com/crossterm-rs/crossterm-terminal/pull/3))
|
||||
- Removed all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-terminal/pull/4))
|
||||
|
||||
## `crossterm_utils` 0.4.0
|
||||
|
||||
- Add deprecation note ([PR #3](https://github.com/crossterm-rs/crossterm-utils/pull/3))
|
||||
- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-utils/pull/4))
|
||||
- Remove unsafe static mut ([PR #5](https://github.com/crossterm-rs/crossterm-utils/pull/5))
|
||||
- `sys::unix::RAW_MODE_ENABLED` replaced with `sys::unix::is_raw_mode_enabled()` (breaking)
|
||||
- New `lazy_static` dependency
|
||||
|
||||
## `crossterm_winapi` 0.3.0
|
||||
|
||||
- Make read sync block for windows systems ([PR #2](https://github.com/crossterm-rs/crossterm-winapi/pull/2))
|
||||
|
||||
# Version 0.11.1
|
||||
|
||||
- Maintenance release
|
||||
- All sub-crates were moved to their own repositories in the `crossterm-rs` organization
|
||||
|
||||
# Version 0.11.0
|
||||
|
||||
As a preparation for crossterm 0.1.0 we have moved crossterm to an organisation called 'crossterm-rs'.
|
||||
|
||||
### Code Quality
|
||||
|
||||
- Code Cleanup: [warning-cleanup], [crossterm_style-cleanup], [crossterm_screen-cleanup], [crossterm_terminal-cleanup], [crossterm_utils-cleanup], [2018-cleanup], [api-cleanup-1], [api-cleanup-2], [api-cleanup-3]
|
||||
- Examples: [example-cleanup_1], [example-cleanup_2], [example-fix], [commandbar-fix], [snake-game-improved]
|
||||
- Fixed all broken tests and added tests
|
||||
|
||||
### Important Changes
|
||||
|
||||
- Return written bytes: [return-written-bytes]
|
||||
- Added derives: `Debug` for `ObjectStyle` [debug-derive], Serialize/Deserialize for key events [serde]
|
||||
- Improved error handling:
|
||||
- Return `crossterm::Result` from all api's: [return_crossterm_result]
|
||||
* `TerminalCursor::pos()` returns `Result<(u16, u16)>`
|
||||
* `Terminal::size()` returns `Result<(u16, u16)>`
|
||||
* `TerminalCursor::move_*` returns `crossterm::Result`
|
||||
* `ExecutableCommand::queue` returns `crossterm::Result`
|
||||
* `QueueableCommand::queue` returns `crossterm::Result`
|
||||
* `get_available_color_count` returns no result
|
||||
* `RawScreen::into_raw_mode` returns `crossterm::Result` instead of `io::Result`
|
||||
* `RawScreen::disable_raw_mode` returns `crossterm::Result` instead of `io::Result`
|
||||
* `AlternateScreen::to_alternate` returns `crossterm::Result` instead of `io::Result`
|
||||
* `TerminalInput::read_line` returns `crossterm::Result` instead of `io::Result`
|
||||
* `TerminalInput::read_char` returns `crossterm::Result` instead of `io::Result`
|
||||
* Maybe I forgot something, a lot of functions have changed
|
||||
- Removed all unwraps/expects from library
|
||||
- Add KeyEvent::Enter and KeyEvent::Tab: [added-key-event-enter], [added-key-event-tab]
|
||||
- Sync set/get terminal size behaviour: [fixed-get-set-terminal-size]
|
||||
- Method renames:
|
||||
* `AsyncReader::stop_reading()` to `stop()`
|
||||
* `RawScreen::disable_raw_mode_on_drop` to `keep_raw_mode_on_drop`
|
||||
* `TerminalCursor::reset_position()` to `restore_position()`
|
||||
* `Command::get_anis_code()` to `ansi_code()`
|
||||
* `available_color_count` to `available_color_count()`
|
||||
* `Terminal::terminal_size` to `Terminal::size`
|
||||
* `Console::get_handle` to `Console::handle`
|
||||
- All `i16` values for indexing: set size, set cursor pos, scrolling synced to `u16` values
|
||||
- Command API takes mutable self instead of self
|
||||
|
||||
[serde]: https://github.com/crossterm-rs/crossterm/pull/190
|
||||
|
||||
[debug-derive]: https://github.com/crossterm-rs/crossterm/pull/192
|
||||
[example-fix]: https://github.com/crossterm-rs/crossterm/pull/193
|
||||
[commandbar-fix]: https://github.com/crossterm-rs/crossterm/pull/204
|
||||
|
||||
[warning-cleanup]: https://github.com/crossterm-rs/crossterm/pull/198
|
||||
[example-cleanup_1]: https://github.com/crossterm-rs/crossterm/pull/196
|
||||
[example-cleanup_2]: https://github.com/crossterm-rs/crossterm/pull/225
|
||||
[snake-game-improved]: https://github.com/crossterm-rs/crossterm/pull/231
|
||||
[crossterm_style-cleanup]: https://github.com/crossterm-rs/crossterm/pull/208
|
||||
[crossterm_screen-cleanup]: https://github.com/crossterm-rs/crossterm/pull/209
|
||||
[crossterm_terminal-cleanup]: https://github.com/crossterm-rs/crossterm/pull/210
|
||||
[crossterm_utils-cleanup]: https://github.com/crossterm-rs/crossterm/pull/211
|
||||
[2018-cleanup]: https://github.com/crossterm-rs/crossterm/pull/222
|
||||
[wild-card-cleanup]: https://github.com/crossterm-rs/crossterm/pull/224
|
||||
|
||||
[api-cleanup-1]: https://github.com/crossterm-rs/crossterm/pull/235
|
||||
[api-cleanup-2]: https://github.com/crossterm-rs/crossterm/pull/238
|
||||
[api-cleanup-3]: https://github.com/crossterm-rs/crossterm/pull/240
|
||||
|
||||
[return-written-bytes]: https://github.com/crossterm-rs/crossterm/pull/212
|
||||
|
||||
[return_crossterm_result]: https://github.com/crossterm-rs/crossterm/pull/232
|
||||
[added-key-event-tab]: https://github.com/crossterm-rs/crossterm/pull/239
|
||||
[added-key-event-enter]: https://github.com/crossterm-rs/crossterm/pull/236
|
||||
[fixed-get-set-terminal-size]: https://github.com/crossterm-rs/crossterm/pull/242
|
||||
|
||||
# Version 0.10.1
|
||||
|
||||
# Version 0.10.0 ~ yanked
|
||||
- Implement command API, to have better performance and more control over how and when commands are executed. [PR](https://github.com/crossterm-rs/crossterm/commit/1a60924abd462ab169b6706aab68f4cca31d7bc2), [issue](https://github.com/crossterm-rs/crossterm/issues/171)
|
||||
- Fix showing, hiding cursor windows implementation
|
||||
- Remove some of the parsing logic from windows keys to ansi codes to key events [PR](https://github.com/crossterm-rs/crossterm/commit/762c3a9b8e3d1fba87acde237f8ed09e74cd9ecd)
|
||||
- Made terminal size 1-based [PR](https://github.com/crossterm-rs/crossterm/commit/d689d7e8ed46a335474b8262bd76f21feaaf0c50)
|
||||
- Add some derives
|
||||
|
||||
# Version 0.9.6
|
||||
|
||||
- Copy for KeyEvent
|
||||
- CTRL + Left, Down, Up, Right key support
|
||||
- SHIFT + Left, Down, Up, Right key support
|
||||
- Fixed UNIX cursor position bug [issue](https://github.com/crossterm-rs/crossterm/issues/140), [PR](https://github.com/crossterm-rs/crossterm/pull/152)
|
||||
|
||||
# Version 0.9.5
|
||||
|
||||
- Prefetch buffer size for more efficient windows input reads. [PR](https://github.com/crossterm-rs/crossterm/pull/144)
|
||||
|
||||
# Version 0.9.4
|
||||
|
||||
- Reset foreground and background color individually. [PR](https://github.com/crossterm-rs/crossterm/pull/138)
|
||||
- Backtap input support. [PR](https://github.com/crossterm-rs/crossterm/pull/129)
|
||||
- Corrected white/grey and added dark grey.
|
||||
- Fixed getting cursor position with raw screen enabled. [PR](https://github.com/crossterm-rs/crossterm/pull/134)
|
||||
- Removed one redundant stdout lock
|
||||
|
||||
# Version 0.9.3
|
||||
|
||||
- Removed println from `SyncReader`
|
||||
|
||||
## Version 0.9.2
|
||||
|
||||
- Terminal size linux was not 0-based
|
||||
- Windows mouse input event position was 0-based and should be 1-based
|
||||
- Result, ErrorKind are made re-exported
|
||||
- Fixed some special key combination detections for UNIX systems
|
||||
- Made FreeBSD compile
|
||||
|
||||
## Version 0.9.1
|
||||
|
||||
- Fixed libc compile error
|
||||
|
||||
## Version 0.9.0 (yanked)
|
||||
|
||||
This release is all about moving to a stabilized API for 1.0.
|
||||
|
||||
- Major refactor and cleanup.
|
||||
- Improved performance;
|
||||
- No locking when writing to stdout.
|
||||
- UNIX doesn't have any dynamic dispatch anymore.
|
||||
- Windows has improved the way to check if ANSI modes are enabled.
|
||||
- Removed lot's of complex API calls: `from_screen`, `from_output`
|
||||
- Removed `Arc<TerminalOutput>` from all internal Api's.
|
||||
- Removed termios dependency for UNIX systems.
|
||||
- Upgraded deps.
|
||||
- Removed about 1000 lines of code
|
||||
- `TerminalOutput`
|
||||
- `Screen`
|
||||
- unsafe code
|
||||
- Some duplicated code introduced by a previous refactor.
|
||||
- Raw modes UNIX systems improved
|
||||
- Added `NoItalic` attribute
|
||||
|
||||
## Version 0.8.2
|
||||
|
||||
- Bug fix for sync reader UNIX.
|
||||
|
||||
## Version 0.8.1
|
||||
|
||||
- Added public re-exports for input.
|
||||
|
||||
# Version 0.8.0
|
||||
|
||||
- Introduced KeyEvents
|
||||
- Introduced MouseEvents
|
||||
- Upgraded crossterm_winapi 0.2
|
||||
|
||||
# Version 0.7.0
|
||||
|
||||
- Introduced more `Attributes`
|
||||
- Introduced easier ways to style text [issue 87](https://github.com/crossterm-rs/crossterm/issues/87).
|
||||
- Removed `ColorType` since it was unnecessary.
|
||||
|
||||
# Version 0.6.0
|
||||
|
||||
- Introduced feature flags; input, cursor, style, terminal, screen.
|
||||
- All modules are moved to their own crate.
|
||||
- Introduced crossterm workspace
|
||||
- Less dependencies.
|
||||
- Improved namespaces.
|
||||
|
||||
[PR 84](https://github.com/crossterm-rs/crossterm/pull/84)
|
||||
|
||||
# Version 0.5.5
|
||||
|
||||
- Error module is made public [PR 78](https://github.com/crossterm-rs/crossterm/pull/78).
|
||||
|
||||
# Version 0.5.4
|
||||
|
||||
- WinApi rewrite and correctly error handled [PR 67](https://github.com/crossterm-rs/crossterm/pull/67)
|
||||
- Windows attribute support [PR 62](https://github.com/crossterm-rs/crossterm/pull/62)
|
||||
- Readline bug fix windows systems [PR 62](https://github.com/crossterm-rs/crossterm/pull/62)
|
||||
- Error handling improvement.
|
||||
- General refactoring, all warnings removed.
|
||||
- Documentation improvement.
|
||||
|
||||
# Version 0.5.1
|
||||
|
||||
- Documentation refactor.
|
||||
- Fixed broken API documentation [PR 53](https://github.com/crossterm-rs/crossterm/pull/53).
|
||||
|
||||
# Version 0.5.0
|
||||
|
||||
- Added ability to pause the terminal [issue](https://github.com/crossterm-rs/crossterm/issues/39)
|
||||
- RGB support for Windows 10 systems
|
||||
- ANSI color value (255) color support
|
||||
- More convenient API, no need to care about `Screen` unless working with when working with alternate or raw screen [PR](https://github.com/crossterm-rs/crossterm/pull/44)
|
||||
- Implemented Display for styled object
|
||||
|
||||
# Version 0.4.3
|
||||
|
||||
- Fixed bug [issue 41](https://github.com/crossterm-rs/crossterm/issues/41)
|
||||
|
||||
# Version 0.4.2
|
||||
|
||||
- Added functionality to make a styled object writable to screen [issue 33](https://github.com/crossterm-rs/crossterm/issues/33)
|
||||
- Added unit tests.
|
||||
- Bugfix with getting terminal size unix.
|
||||
- Bugfix with returning written bytes [pull request 31](https://github.com/crossterm-rs/crossterm/pull/31)
|
||||
- removed methods calls: `as_any()` and `as_any_mut()` from `TerminalOutput`
|
||||
|
||||
# Version 0.4.1
|
||||
|
||||
- Fixed resizing of ansi terminal with and height where in the wrong order.
|
||||
|
||||
# Version 0.4.0
|
||||
|
||||
- Input support (read_line, read_char, read_async, read_until_async)
|
||||
- Styling module improved
|
||||
- Everything is multithreaded (`Send`, `Sync`)
|
||||
- Performance enhancements: removed mutexes, removed state manager, removed context type removed unnecessarily RC types.
|
||||
- Bug fix resetting console color.
|
||||
- Bug fix whit undoing raw modes.
|
||||
- More correct error handling.
|
||||
- Overall command improvement.
|
||||
- Overall refactor of code.
|
||||
|
||||
# Version 0.3.0
|
||||
|
||||
This version has some braking changes check [upgrade manual](UPGRADE%20Manual.md) for more information about what is changed.
|
||||
I think you should not switch to version `0.3.0` if you aren't going to use the AlternateScreen feature.
|
||||
Because you will have some work to get to the new version of crossterm depending on your situation.
|
||||
|
||||
Some Features crossterm 0.3.0
|
||||
- Alternate Screen for windows and unix systems.
|
||||
- Raw screen for unix and windows systems [Issue 5](https://github.com/crossterm-rs/crossterm/issues/5)..
|
||||
- Hiding an showing the cursor.
|
||||
- Control over blinking of the terminal cursor (only some terminals are supporting this).
|
||||
- The terminal state will be set to its original state when process ends [issue7](https://github.com/crossterm-rs/crossterm/issues/7).
|
||||
- exit the current process.
|
||||
|
||||
## Alternate screen
|
||||
|
||||
This create supports alternate screen for both windows and unix systems. You can use
|
||||
|
||||
*Nix style applications often utilize an alternate screen buffer, so that they can modify the entire contents of the buffer, without affecting the application that started them.
|
||||
The alternate buffer is exactly the dimensions of the window, without any scrollback region.
|
||||
For an example of this behavior, consider when vim is launched from bash.
|
||||
Vim uses the entirety of the screen to edit the file, then returning to bash leaves the original buffer unchanged.
|
||||
|
||||
I Highly recommend you to check the `examples/program_examples/first_depth_search` for seeing this in action.
|
||||
|
||||
## Raw screen
|
||||
|
||||
This crate now supports raw screen for both windows and unix systems.
|
||||
What exactly is raw state:
|
||||
- No line buffering.
|
||||
Normally the terminals uses line buffering. This means that the input will be send to the terminal line by line.
|
||||
With raw mode the input will be send one byte at a time.
|
||||
- Input
|
||||
All input has to be written manually by the programmer.
|
||||
- Characters
|
||||
The characters are not processed by the terminal driver, but are sent straight through.
|
||||
Special character have no meaning, like backspace will not be interpret as backspace but instead will be directly send to the terminal.
|
||||
With these modes you can easier design the terminal screen.
|
||||
|
||||
## Some functionalities added
|
||||
|
||||
- Hiding and showing terminal cursor
|
||||
- Enable or disabling blinking of the cursor for unix systems (this is not widely supported)
|
||||
- Restoring the terminal to original modes.
|
||||
- Added a [wrapper](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) for managing all the functionalities of crossterm `Crossterm`.
|
||||
- Exit the current running process
|
||||
|
||||
## Examples
|
||||
Added [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) for each version of the crossterm version.
|
||||
Also added a folder with some [real life examples](https://github.com/crossterm-rs/crossterm/tree/master/examples/program_examples).
|
||||
|
||||
## Context
|
||||
|
||||
What is the `Context` all about? This `Context` has several reasons why it is introduced into `crossterm version 0.3.0`.
|
||||
These points are related to the features like `Alternatescreen` and managing the terminal state.
|
||||
|
||||
- At first `Terminal state`:
|
||||
|
||||
Because this is a terminal manipulating library there will be made changes to terminal when running an process.
|
||||
If you stop the process you want the terminal back in its original state.
|
||||
Therefore, I need to track the changes made to the terminal.
|
||||
|
||||
- At second `Handle to the console`
|
||||
|
||||
In Rust we can use `stdout()` to get an handle to the current default console handle.
|
||||
For example when in unix systems you want to print something to the main screen you can use the following code:
|
||||
|
||||
write!(std::io::stdout(), "{}", "some text").
|
||||
|
||||
But things change when we are in alternate screen modes.
|
||||
We can not simply use `stdout()` to get a handle to the alternate screen, since this call returns the current default console handle (handle to mainscreen).
|
||||
|
||||
Because of that we need to store an handle to the current screen.
|
||||
This handle could be used to put into alternate screen modes and back into main screen modes.
|
||||
Through this stored handle Crossterm can execute its command and write on and to the current screen whether it be alternate screen or main screen.
|
||||
|
||||
For unix systems we store the handle gotten from `stdout()` for windows systems that are not supporting ANSI escape codes we store WinApi `HANDLE` struct witch will provide access to the current screen.
|
||||
|
||||
So to recap this `Context` struct is a wrapper for a type that manges terminal state changes.
|
||||
When this `Context` goes out of scope all changes made will be undone.
|
||||
Also is this `Context` is a wrapper for access to the current console screen.
|
||||
|
||||
Because Crossterm needs access to the above to types quite often I have chosen to add those two in one struct called `Context` so that this type could be shared throughout library.
|
||||
Check this link for more info: [cleanup of rust code](https://stackoverflow.com/questions/48732387/how-can-i-run-clean-up-code-in-a-rust-library).
|
||||
More info over writing to alternate screen buffer on windows and unix see this [link](https://github.com/crossterm-rs/crossterm/issues/17)
|
||||
|
||||
__Now the user has to pass an context type to the modules of Crossterm like this:__
|
||||
|
||||
let context = Context::new();
|
||||
|
||||
let cursor = cursor(&context);
|
||||
let terminal = terminal(&context);
|
||||
let color = color(&context);
|
||||
|
||||
Because this looks a little odd I will provide a type widths will manage the `Context` for you. You can call the different modules like the following:
|
||||
|
||||
let crossterm = Crossterm::new();
|
||||
let color = crossterm.color();
|
||||
let cursor = crossterm.cursor();
|
||||
let terminal = crossterm.terminal();
|
||||
|
||||
|
||||
### Alternate screen
|
||||
When you want to switch to alternate screen there are a couple of things to keep in mind for it to work correctly.
|
||||
First off some code of how to switch to Alternate screen, for more info check the [alternate screen example](https://github.com/crossterm-rs/crossterm/blob/master/examples/alternate_screen.rs).
|
||||
|
||||
_Create alternate screen from `Context`_
|
||||
|
||||
// create context.
|
||||
let context = crossterm::Context::new();
|
||||
// create instance of Alternatescreen by the given context, this will also switch to it.
|
||||
let mut screen = crossterm::AlternateScreen::from(context.clone());
|
||||
// write to the alternate screen.
|
||||
write!(screen, "test");
|
||||
|
||||
_Create alternate screen from `Crossterm`:_
|
||||
|
||||
// create context.
|
||||
let crossterm = ::crossterm::Crossterm::new();
|
||||
// create instance of Alternatescreen by the given reference to crossterm, this will also switch to it.
|
||||
let mut screen = crossterm::AlternateScreen::from(&crossterm);
|
||||
// write to the alternate screen.
|
||||
write!(screen, "test");
|
||||
|
||||
like demonstrated above, to get the functionalities of `cursor(), color(), terminal()` also working on alternate screen.
|
||||
You need to pass it the same `Context` as you have passed to the previous three called functions,
|
||||
If you don't use the same `Context` in `cursor(), color(), terminal()` than these modules will be using the main screen and you will not see anything at the alternate screen. If you use the [Crossterm](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) type you can get the `Context` from it by calling the crossterm.get_context() whereafter you can create the AlternateScreen from it.
|
||||
|
||||
# Version 0.2.2
|
||||
|
||||
- Bug see [issue 15](https://github.com/crossterm-rs/crossterm/issues/15)
|
||||
|
||||
# Version 0.2.1
|
||||
|
||||
- Default ANSI escape codes for windows machines, if windows does not support ANSI switch back to WinApi.
|
||||
- method grammar mistake fixed [Issue 3](https://github.com/crossterm-rs/crossterm/issues/3)
|
||||
- Some Refactorings in method names see [issue 4](https://github.com/crossterm-rs/crossterm/issues/4)
|
||||
- Removed bin reference from crate [Issue 6](https://github.com/crossterm-rs/crossterm/issues/6)
|
||||
- Get position unix fixed [issue 8](https://github.com/crossterm-rs/crossterm/issues/8)
|
||||
|
||||
# Version 0.2
|
||||
|
||||
- 256 color support.
|
||||
- Text Attributes like: bold, italic, underscore and crossed word etc.
|
||||
- Custom ANSI color code input to set fore- and background color for unix.
|
||||
- Storing the current cursor position and resetting to that stored cursor position later.
|
||||
- Resizing the terminal.
|
|
@ -1,97 +0,0 @@
|
|||
[package]
|
||||
name = "keyfork-crossterm"
|
||||
version = "0.27.1"
|
||||
# authors = ["T. Post"]
|
||||
authors = ["Ryan Heywood <ryan@distrust.co>"]
|
||||
description = "A crossplatform terminal library for manipulating terminals."
|
||||
repository = "https://git.distrust.co/public/keyfork"
|
||||
# documentation = "https://docs.rs/crossterm/"
|
||||
license = "MIT"
|
||||
# keywords = ["event", "color", "cli", "input", "terminal"]
|
||||
# exclude = ["target", "Cargo.lock"]
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
rust-version = "1.58.0"
|
||||
# categories = ["command-line-interface", "command-line-utilities"]
|
||||
|
||||
# [lib]
|
||||
# name = "crossterm"
|
||||
# path = "src/lib.rs"
|
||||
|
||||
# Build documentation with all features -> EventStream is available
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
# Features
|
||||
[features]
|
||||
default = ["bracketed-paste", "windows", "events"]
|
||||
windows = ["dep:winapi", "dep:crossterm_winapi"] # Disables winapi dependencies from being included into the binary (SHOULD NOT be disabled on windows).
|
||||
bracketed-paste = [] # Enables triggering a `Event::Paste` when pasting text into the terminal.
|
||||
event-stream = ["dep:futures-core", "events"] # Enables async events
|
||||
use-dev-tty = ["filedescriptor"] # Enables raw file descriptor polling / selecting instead of mio.
|
||||
events = ["dep:mio", "dep:signal-hook", "dep:signal-hook-mio"] # Enables reading input/events from the system.
|
||||
serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
|
||||
|
||||
# Shared dependencies
|
||||
[dependencies]
|
||||
bitflags = {version = "2.3" }
|
||||
parking_lot = "0.12"
|
||||
|
||||
# optional deps only added when requested
|
||||
futures-core = { version = "0.3", optional = true, default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
||||
# Windows dependencies
|
||||
[target.'cfg(windows)'.dependencies.winapi]
|
||||
version = "0.3.9"
|
||||
features = ["winuser", "winerror"]
|
||||
optional = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
crossterm_winapi = { version = "0.9.1", optional = true }
|
||||
|
||||
# UNIX dependencies
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
signal-hook = { version = "0.3.17", optional = true }
|
||||
filedescriptor = { version = "0.8", optional = true }
|
||||
mio = { version = "0.8", features = ["os-poll"], optional = true }
|
||||
signal-hook-mio = { version = "0.2.3", features = ["support-v0_8"], optional = true }
|
||||
|
||||
# Dev dependencies (examples, ...)
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
futures = "0.3"
|
||||
futures-timer = "3.0"
|
||||
async-std = "1.12"
|
||||
serde_json = { workspace = true }
|
||||
serial_test = "2.0.0"
|
||||
|
||||
# Examples
|
||||
[[example]]
|
||||
name = "event-read"
|
||||
required-features = ["bracketed-paste", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-match-modifiers"
|
||||
required-features = ["bracketed-paste", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-poll-read"
|
||||
required-features = ["bracketed-paste", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-stream-async-std"
|
||||
required-features = ["event-stream", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-stream-tokio"
|
||||
required-features = ["event-stream", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-read-char-line"
|
||||
required-features = ["events"]
|
||||
|
||||
[[example]]
|
||||
name = "stderr"
|
||||
required-features = ["events"]
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Timon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1 +0,0 @@
|
|||
Forked from https://github.com/crossterm-rs/crossterm
|
|
@ -1,213 +0,0 @@
|
|||
<h1 align="center"><img width="440" src="docs/crossterm_full.png" /></h1>
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2) ![Travis][s7] [![Latest Version][s1]][l1] [![MIT][s2]][l2] [![docs][s3]][l3] ![Lines of Code][s6] [![Join us on Discord][s5]][l5]
|
||||
|
||||
# Cross-platform Terminal Manipulation Library
|
||||
|
||||
Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested,
|
||||
see [Tested Terminals](#tested-terminals) for more info).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Tested Terminals](#tested-terminals)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Feature Flags](#feature-flags)
|
||||
- [Dependency Justification](#dependency-justification)
|
||||
- [Other Resources](#other-resources)
|
||||
- [Used By](#used-by)
|
||||
- [Contributing](#contributing)
|
||||
- [Authors](#authors)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
- Cross-platform
|
||||
- Multi-threaded (send, sync)
|
||||
- Detailed documentation
|
||||
- Few dependencies
|
||||
- Full control over writing and flushing output buffer
|
||||
- Is tty
|
||||
- Cursor
|
||||
- Move the cursor N times (up, down, left, right)
|
||||
- Move to previous / next line
|
||||
- Move to column
|
||||
- Set/get the cursor position
|
||||
- Store the cursor position and restore to it later
|
||||
- Hide/show the cursor
|
||||
- Enable/disable cursor blinking (not all terminals do support this feature)
|
||||
- Styled output
|
||||
- Foreground color (16 base colors)
|
||||
- Background color (16 base colors)
|
||||
- 256 (ANSI) color support (Windows 10 and UNIX only)
|
||||
- RGB color support (Windows 10 and UNIX only)
|
||||
- Text attributes like bold, italic, underscore, crossed, etc
|
||||
- Terminal
|
||||
- Clear (all lines, current line, from cursor down and up, until new line)
|
||||
- Scroll up, down
|
||||
- Set/get the terminal size
|
||||
- Exit current process
|
||||
- Alternate screen
|
||||
- Raw screen
|
||||
- Set terminal title
|
||||
- Enable/disable line wrapping
|
||||
- Event
|
||||
- Input Events
|
||||
- Mouse Events (press, release, position, button, drag)
|
||||
- Terminal Resize Events
|
||||
- Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and
|
||||
- futures Stream (feature 'event-stream')
|
||||
- Poll/read API
|
||||
|
||||
<!--
|
||||
WARNING: Do not change following heading title as it's used in the URL by other crates!
|
||||
-->
|
||||
|
||||
### Tested Terminals
|
||||
|
||||
- Console Host
|
||||
- Windows 10 (Pro)
|
||||
- Windows 8.1 (N)
|
||||
- Ubuntu Desktop Terminal
|
||||
- Ubuntu 17.10
|
||||
- Pop!_OS ( Ubuntu ) 20.04
|
||||
- (Arch, Manjaro) KDE Konsole
|
||||
- (Arch, NixOS) Kitty
|
||||
- Linux Mint
|
||||
- (OpenSuse) Alacritty
|
||||
- (Chrome OS) Crostini
|
||||
- Apple
|
||||
- macOS Monterey 12.7.1 (Intel-Chip)
|
||||
|
||||
This crate supports all UNIX terminals and Windows terminals down to Windows 7; however, not all of the
|
||||
terminals have been tested. If you have used this library for a terminal other than the above list without
|
||||
issues, then feel free to add it to the above list - I really would appreciate it!
|
||||
|
||||
## Getting Started
|
||||
_see the [examples directory](examples/) and [documentation](https://docs.rs/crossterm/) for more advanced examples._
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Click to show Cargo.toml.
|
||||
</summary>
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
crossterm = "0.27"
|
||||
```
|
||||
|
||||
</details>
|
||||
<p></p>
|
||||
|
||||
```rust
|
||||
use std::io::{stdout, Write};
|
||||
|
||||
use crossterm::{
|
||||
execute,
|
||||
style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
|
||||
ExecutableCommand, Result,
|
||||
event,
|
||||
};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
// using the macro
|
||||
execute!(
|
||||
stdout(),
|
||||
SetForegroundColor(Color::Blue),
|
||||
SetBackgroundColor(Color::Red),
|
||||
Print("Styled text here."),
|
||||
ResetColor
|
||||
)?;
|
||||
|
||||
// or using functions
|
||||
stdout()
|
||||
.execute(SetForegroundColor(Color::Blue))?
|
||||
.execute(SetBackgroundColor(Color::Red))?
|
||||
.execute(Print("Styled text here."))?
|
||||
.execute(ResetColor)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Checkout this [list](https://docs.rs/crossterm/latest/crossterm/index.html#supported-commands) with all possible commands.
|
||||
|
||||
### Feature Flags
|
||||
|
||||
```toml
|
||||
[dependencies.crossterm]
|
||||
version = "0.27"
|
||||
features = ["event-stream"]
|
||||
```
|
||||
|
||||
| Feature | Description |
|
||||
|:---------------|:---------------------------------------------|
|
||||
| `event-stream` | `futures::Stream` producing `Result<Event>`. |
|
||||
| `serde` | (De)serializing of events. |
|
||||
| `events` | Reading input/system events (enabled by default) |
|
||||
| `filedescriptor` | Use raw filedescriptor for all events rather then mio dependency |
|
||||
|
||||
|
||||
To use crossterm as a very thin layer you can disable the `events` feature or use `filedescriptor` feature.
|
||||
This can disable `mio` / `signal-hook` / `signal-hook-mio` dependencies.
|
||||
|
||||
### Dependency Justification
|
||||
|
||||
| Dependency | Used for | Included |
|
||||
|:---------------|:---------------------------------------------------------------------------------|:--------------------------------------|
|
||||
| `bitflags` | `KeyModifiers`, those are differ based on input. | always |
|
||||
| `parking_lot` | locking `RwLock`s with a timeout, const mutexes. | always |
|
||||
| `libc` | UNIX terminal_size/raw modes/set_title and several other low level functionality. | optional (`events` feature), UNIX only |
|
||||
| `Mio` | event readiness polling, waking up poller | optional (`events` feature), UNIX only |
|
||||
| `signal-hook` | signal-hook is used to handle terminal resize SIGNAL with Mio. | optional (`events` feature),UNIX only |
|
||||
| `winapi` | Used for low-level windows system calls which ANSI codes can't replace | windows only |
|
||||
| `futures-core` | For async stream of events | only with `event-stream` feature flag |
|
||||
| `serde` | ***ser***ializing and ***de***serializing of events | only with `serde` feature flag |
|
||||
|
||||
### Other Resources
|
||||
|
||||
- [API documentation](https://docs.rs/crossterm/)
|
||||
- [Deprecated examples repository](https://github.com/crossterm-rs/examples)
|
||||
|
||||
## Used By
|
||||
|
||||
- [Broot](https://dystroy.org/broot/)
|
||||
- [Cursive](https://github.com/gyscos/Cursive)
|
||||
- [TUI](https://github.com/fdehau/tui-rs)
|
||||
- [Rust-sloth](https://github.com/ecumene/rust-sloth)
|
||||
- [Rusty-rain](https://github.com/cowboy8625/rusty-rain)
|
||||
|
||||
## Contributing
|
||||
|
||||
We highly appreciate when anyone contributes to this crate. Before you do, please,
|
||||
read the [Contributing](docs/CONTRIBUTING.md) guidelines.
|
||||
|
||||
## Authors
|
||||
|
||||
* **Timon Post** - *Project Owner & creator*
|
||||
|
||||
## License
|
||||
|
||||
This project, `crossterm` and all its sub-crates: `crossterm_screen`, `crossterm_cursor`, `crossterm_style`,
|
||||
`crossterm_input`, `crossterm_terminal`, `crossterm_winapi`, `crossterm_utils` are licensed under the MIT
|
||||
License - see the [LICENSE](https://github.com/crossterm-rs/crossterm/blob/master/LICENSE) file for details.
|
||||
|
||||
[s1]: https://img.shields.io/crates/v/crossterm.svg
|
||||
[l1]: https://crates.io/crates/crossterm
|
||||
|
||||
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[l2]: ./LICENSE
|
||||
|
||||
[s3]: https://docs.rs/crossterm/badge.svg
|
||||
[l3]: https://docs.rs/crossterm/
|
||||
|
||||
[s3]: https://docs.rs/crossterm/badge.svg
|
||||
[l3]: https://docs.rs/crossterm/
|
||||
|
||||
[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
|
||||
[l5]: https://discord.gg/K4nyTDB
|
||||
|
||||
[s6]: https://tokei.rs/b1/github/crossterm-rs/crossterm?category=code
|
||||
[s7]: https://travis-ci.org/crossterm-rs/crossterm.svg?branch=master
|
|
@ -1 +0,0 @@
|
|||
book
|
|
@ -1,65 +0,0 @@
|
|||
# Contributing
|
||||
|
||||
I would appreciate any contributions to this crate. However, some things are handy to know.
|
||||
|
||||
## Code Style
|
||||
|
||||
### Import Order
|
||||
|
||||
All imports are semantically grouped and ordered. The order is:
|
||||
|
||||
- standard library (`use std::...`)
|
||||
- external crates (`use rand::...`)
|
||||
- current crate (`use crate::...`)
|
||||
- parent module (`use super::..`)
|
||||
- current module (`use self::...`)
|
||||
- module declaration (`mod ...`)
|
||||
|
||||
There must be an empty line between groups. An example:
|
||||
|
||||
```rust
|
||||
use crossterm_utils::{csi, write_cout, Result};
|
||||
|
||||
use crate::sys::{get_cursor_position, show_cursor};
|
||||
|
||||
use super::Cursor;
|
||||
```
|
||||
|
||||
#### CLion Tips
|
||||
|
||||
The CLion IDE does this for you (_Menu_ -> _Code_ -> _Optimize Imports_). Be aware that the CLion sorts
|
||||
imports in a group in a different way when compared to the `rustfmt`. It's effectively two steps operation
|
||||
to get proper grouping & sorting:
|
||||
|
||||
* _Menu_ -> _Code_ -> _Optimize Imports_ - group & semantically order imports
|
||||
* `cargo fmt` - fix ordering within the group
|
||||
|
||||
Second step can be automated via _CLion_ -> _Preferences_ ->
|
||||
_Languages & Frameworks_ -> _Rust_ -> _Rustfmt_ -> _Run rustfmt on save_.
|
||||
|
||||
### Max Line Length
|
||||
|
||||
| Type | Max line length |
|
||||
|:---------------------|----------------:|
|
||||
| Code | 100 |
|
||||
| Comments in the code | 120 |
|
||||
| Documentation | 120 |
|
||||
|
||||
100 is the [`max_width`](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width)
|
||||
default value.
|
||||
|
||||
120 is because of the GitHub. The editor & viewer width there is +- 123 characters.
|
||||
|
||||
### Warnings
|
||||
|
||||
The code must be warning free. It's quite hard to find an error if the build logs are polluted with warnings.
|
||||
If you decide to silent a warning with (`#[allow(...)]`), please add a comment why it's required.
|
||||
|
||||
Always consult the [Travis CI](https://travis-ci.org/crossterm-rs/crossterm/pull_requests) build logs.
|
||||
|
||||
### Forbidden Warnings
|
||||
|
||||
Search for `#![deny(...)]` in the code:
|
||||
|
||||
* `unused_must_use`
|
||||
* `unused_imports`
|
Binary file not shown.
Before Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
|
@ -1,103 +0,0 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="433.000000pt" viewBox="0 0 700.000000 433.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,433.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M987 3178 c-41 -74 -41 -74 -90 -34 -63 52 -83 48 -112 -19 -14 -30
|
||||
-27 -55 -30 -55 -3 0 -24 14 -47 30 -43 32 -79 38 -95 18 -6 -7 -13 -35 -17
|
||||
-62 -3 -27 -8 -51 -11 -54 -2 -2 -22 3 -45 12 -89 35 -118 16 -106 -69 4 -24
|
||||
3 -46 -2 -49 -6 -3 -35 -1 -67 6 -48 9 -59 9 -72 -5 -13 -13 -14 -22 -4 -61
|
||||
19 -75 19 -75 -57 -78 -61 -3 -67 -5 -70 -26 -2 -13 4 -41 12 -63 25 -61 22
|
||||
-66 -36 -74 -29 -4 -59 -12 -65 -17 -22 -18 -14 -53 22 -95 19 -23 35 -43 35
|
||||
-46 0 -2 -12 -7 -27 -10 -47 -10 -93 -44 -93 -69 0 -13 18 -40 41 -64 l41 -42
|
||||
-46 -27 c-42 -24 -46 -30 -46 -64 0 -33 5 -40 40 -60 22 -12 40 -25 40 -28 0
|
||||
-2 -16 -23 -35 -46 -53 -61 -46 -86 28 -112 31 -11 57 -24 57 -30 0 -5 -14
|
||||
-23 -31 -39 -60 -60 -45 -109 37 -119 25 -2 48 -7 51 -11 3 -3 -2 -25 -11 -49
|
||||
-32 -86 -16 -113 63 -102 31 4 51 3 56 -4 3 -6 1 -37 -5 -67 -16 -78 -4 -90
|
||||
74 -74 30 6 60 9 65 5 6 -3 7 -27 4 -55 -4 -33 -2 -55 7 -65 16 -19 57 -19 90
|
||||
0 46 26 59 20 66 -36 12 -86 45 -100 108 -44 22 19 45 35 51 35 6 0 19 -26 30
|
||||
-58 26 -75 52 -80 112 -22 l43 40 26 -42 c31 -50 41 -58 71 -58 16 0 31 15 54
|
||||
53 l33 52 39 -42 c50 -54 77 -56 106 -8 11 20 24 47 27 60 8 31 14 31 48 0 71
|
||||
-66 107 -57 121 32 4 26 9 51 11 55 3 4 23 -2 47 -12 52 -24 93 -25 101 -4 3
|
||||
9 6 40 6 70 0 61 1 61 76 43 39 -10 48 -9 61 4 14 13 14 24 5 72 -7 32 -9 62
|
||||
-6 67 4 6 25 7 50 3 63 -9 84 0 84 36 0 16 -7 46 -15 66 -8 19 -15 39 -15 44
|
||||
0 5 23 11 51 15 85 11 99 43 49 112 -16 23 -30 45 -30 49 0 5 24 17 53 28 68
|
||||
27 73 53 22 112 -19 23 -35 44 -35 47 0 2 22 17 50 33 41 23 50 33 50 55 0 22
|
||||
-9 32 -50 54 -27 15 -50 32 -50 39 0 6 16 26 35 45 19 19 35 44 35 56 0 25
|
||||
-40 59 -83 69 -15 4 -27 10 -27 14 0 3 14 24 30 45 32 42 38 77 18 94 -7 5
|
||||
-37 13 -66 17 -43 6 -53 10 -49 24 40 138 39 141 -48 141 -36 0 -65 4 -65 9 0
|
||||
5 5 32 12 61 10 44 9 53 -5 67 -14 14 -23 15 -67 5 -29 -7 -55 -12 -60 -12 -5
|
||||
0 -10 30 -12 68 -3 65 -4 67 -31 70 -16 2 -46 -4 -68 -12 -22 -9 -43 -16 -48
|
||||
-16 -5 0 -12 24 -16 53 -4 28 -12 58 -17 65 -17 20 -53 14 -97 -19 -24 -17
|
||||
-44 -29 -44 -27 -59 121 -75 130 -137 74 -22 -20 -44 -36 -48 -36 -4 0 -21 23
|
||||
-37 50 -37 62 -70 69 -98 18z m41 -398 c17 0 44 18 83 55 32 30 66 55 76 55
|
||||
32 0 125 -31 186 -61 l57 -29 0 -88 c0 -62 -4 -94 -15 -109 l-15 -22 -44 19
|
||||
c-90 40 -166 54 -301 54 -105 0 -145 -4 -210 -23 -197 -55 -335 -178 -386
|
||||
-341 -29 -92 -28 -227 1 -315 12 -37 20 -68 19 -69 -10 -9 -107 -66 -112 -66
|
||||
-6 0 -33 66 -53 130 -8 25 -17 95 -21 156 l-6 110 32 13 c17 8 31 17 31 21 0
|
||||
4 12 11 28 15 52 13 72 33 72 73 0 20 -7 50 -15 65 -22 44 -29 117 -13 154 31
|
||||
74 177 207 285 262 23 11 72 29 110 40 l68 19 59 -59 c41 -41 66 -59 84 -59z
|
||||
m206 -307 c68 -22 133 -83 168 -160 l28 -63 85 0 85 0 0 40 c0 39 0 40 19 21
|
||||
10 -10 33 -22 50 -26 17 -4 31 -10 31 -15 0 -5 15 -14 34 -21 33 -12 35 -14
|
||||
38 -73 10 -156 -34 -309 -117 -411 -19 -23 -35 -45 -35 -48 0 -3 -12 -18 -27
|
||||
-33 -26 -26 -28 -27 -88 -15 -120 22 -129 17 -151 -81 -8 -35 -16 -73 -19 -85
|
||||
-11 -47 -251 -92 -387 -73 -40 6 -80 12 -88 15 -131 36 -129 35 -139 68 -6 18
|
||||
-15 56 -21 86 -5 30 -15 60 -21 68 -14 16 -75 17 -135 2 -39 -10 -48 -9 -62 5
|
||||
-19 19 -23 15 63 65 l49 29 31 -23 c46 -36 143 -81 215 -101 90 -25 328 -26
|
||||
420 -1 204 55 336 187 348 350 l4 57 -85 0 -86 0 -7 -37 c-23 -130 -113 -200
|
||||
-274 -217 -182 -18 -314 57 -356 201 -24 82 -16 259 15 328 66 148 229 206
|
||||
415 148z"/>
|
||||
<path d="M4557 2523 c-4 -3 -7 -17 -7 -30 0 -59 -47 -109 -116 -123 -53 -11
|
||||
-54 -12 -54 -46 l0 -34 50 0 50 0 0 -135 c0 -75 5 -145 11 -158 16 -36 64 -59
|
||||
133 -64 115 -10 158 27 172 145 l7 62 -46 0 -46 0 -3 -47 c-3 -41 -6 -48 -25
|
||||
-51 -23 -3 -23 -2 -23 122 l0 126 60 0 60 0 0 40 0 40 -60 0 -60 0 0 80 0 80
|
||||
-48 0 c-27 0 -52 -3 -55 -7z"/>
|
||||
<path d="M2258 2373 l-118 -4 0 -39 c0 -36 2 -39 33 -42 l32 -3 3 -132 3 -133
|
||||
-36 0 c-34 0 -35 -1 -35 -40 l0 -40 190 0 190 0 0 40 0 39 -57 3 -58 3 -3 84
|
||||
c-3 94 7 131 46 161 34 28 42 25 42 -14 0 -44 22 -61 81 -61 63 0 94 28 94 85
|
||||
0 57 -37 91 -104 98 -57 5 -134 -18 -151 -46 -7 -14 -9 -10 -10 16 0 20 -5 31
|
||||
-12 30 -7 -1 -66 -4 -130 -5z"/>
|
||||
<path d="M2867 2370 c-65 -17 -116 -49 -144 -92 -25 -37 -28 -51 -28 -124 0
|
||||
-75 3 -86 30 -125 18 -26 50 -53 80 -68 44 -23 61 -26 160 -26 101 0 115 2
|
||||
168 28 40 20 66 42 85 70 24 36 27 50 27 123 0 75 -3 86 -30 125 -45 64 -108
|
||||
91 -220 95 -49 2 -107 -1 -128 -6z m137 -81 c34 -16 51 -62 50 -139 -1 -108
|
||||
-45 -158 -115 -130 -36 15 -54 60 -54 135 0 113 47 166 119 134z"/>
|
||||
<path d="M3418 2364 c-117 -36 -155 -161 -68 -226 15 -11 78 -29 148 -43 134
|
||||
-27 142 -30 142 -50 0 -18 -34 -28 -87 -27 -61 0 -109 19 -137 53 -20 23 -33
|
||||
29 -65 29 l-41 0 0 -80 0 -80 44 0 c24 0 46 5 48 11 3 8 20 6 58 -5 206 -61
|
||||
407 55 335 193 -19 37 -63 57 -182 81 -142 29 -143 29 -143 44 0 21 57 37 104
|
||||
30 52 -9 92 -29 106 -54 8 -15 21 -20 55 -20 l45 0 0 80 0 80 -39 0 c-24 0
|
||||
-41 -5 -44 -14 -5 -13 -13 -13 -59 0 -67 17 -159 17 -220 -2z"/>
|
||||
<path d="M3968 2363 c-66 -22 -100 -60 -106 -121 -8 -91 25 -115 213 -152 122
|
||||
-24 137 -32 113 -56 -12 -12 -34 -17 -77 -16 -68 1 -125 23 -148 58 -12 18
|
||||
-24 24 -54 24 l-39 0 0 -80 0 -80 38 0 c21 0 42 4 48 10 6 6 27 4 59 -6 113
|
||||
-35 279 -1 328 68 26 35 26 101 0 136 -28 38 -55 49 -188 76 -63 13 -118 26
|
||||
-122 29 -12 13 7 34 36 40 51 11 127 -11 158 -44 22 -23 36 -29 70 -29 l43 0
|
||||
0 80 0 80 -39 0 c-21 0 -44 -6 -50 -14 -9 -11 -19 -11 -63 0 -68 18 -161 17
|
||||
-220 -3z"/>
|
||||
<path d="M5025 2371 c-125 -31 -199 -135 -180 -253 20 -117 99 -176 250 -185
|
||||
162 -11 272 39 300 135 6 21 4 22 -47 22 -45 0 -55 -3 -63 -22 -16 -36 -53
|
||||
-49 -123 -45 -69 3 -105 27 -117 80 l-7 27 181 0 181 0 0 38 c0 51 -41 134
|
||||
-80 162 -17 12 -52 29 -77 36 -48 15 -167 17 -218 5z"/>
|
||||
<path d="M5558 2373 l-118 -4 0 -39 c0 -37 2 -40 28 -40 51 0 53 -7 50 -141
|
||||
l-3 -124 -32 -3 c-31 -3 -33 -6 -33 -43 l0 -39 190 0 190 0 0 40 0 40 -60 0
|
||||
-60 0 0 103 0 103 33 32 c41 40 55 41 49 3 -10 -72 115 -97 165 -32 22 28 16
|
||||
89 -11 116 -44 43 -148 46 -206 5 l-30 -21 0 25 c0 19 -5 25 -17 24 -10 -1
|
||||
-71 -4 -135 -5z"/>
|
||||
<path d="M6327 2366 c-20 -8 -44 -19 -52 -26 -13 -10 -15 -9 -15 9 0 20 -4 21
|
||||
-130 21 l-130 0 0 -39 c0 -37 2 -40 33 -43 l32 -3 3 -132 3 -133 -36 0 c-34 0
|
||||
-35 -1 -35 -40 l0 -40 160 0 160 0 0 39 c0 36 -3 40 -27 43 l-28 3 -3 106 c-3
|
||||
119 5 135 69 145 60 10 73 -10 78 -115 4 -117 -2 -141 -34 -141 -22 0 -25 -4
|
||||
-25 -40 l0 -40 150 0 150 0 0 39 c0 36 -3 40 -27 43 l-28 3 0 108 c0 102 1
|
||||
110 24 128 29 24 72 24 100 2 20 -17 22 -26 19 -128 l-3 -110 -27 -3 c-25 -3
|
||||
-28 -7 -28 -43 l0 -39 160 0 160 0 0 40 c0 39 -1 40 -34 40 -22 0 -36 6 -40
|
||||
16 -3 9 -6 67 -6 129 0 130 -8 156 -59 188 -30 18 -50 22 -122 22 -76 0 -90
|
||||
-3 -126 -27 -37 -25 -43 -26 -55 -11 -34 42 -153 56 -231 29z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 6.8 KiB |
|
@ -1,14 +0,0 @@
|
|||
# Known Problems
|
||||
|
||||
There are some problems I discovered during development.
|
||||
And I don't think it has to do anything with crossterm but it has to do whit how terminals handle ANSI or WinApi.
|
||||
|
||||
## WinAPI
|
||||
|
||||
- Power shell does not interpreter 'DarkYellow' and is instead using gray instead, cmd is working perfectly fine.
|
||||
- Power shell inserts an '\n' (enter) when the program starts, this enter is the one you pressed when running the command.
|
||||
- After the program ran, power shell will reset the background and foreground colors.
|
||||
|
||||
## UNIX-terminals
|
||||
|
||||
The Arc and Manjaro KDE Konsole's are not seeming to resize the terminal instead they are resizing the buffer.
|
|
@ -1,40 +0,0 @@
|
|||
![Lines of Code][s7] [![MIT][s2]][l2] [![Join us on Discord][s5]][l5]
|
||||
|
||||
# Crossterm Examples
|
||||
|
||||
The examples are compatible with the latest release.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
├── examples
|
||||
│ └── interactive-test
|
||||
│ └── event-*
|
||||
│ └── stderr
|
||||
```
|
||||
| File Name | Description | Topics |
|
||||
|:----------------------------|:-------------------------------|:------------------------------------------|
|
||||
| `examples/interactive-test` | interactive, walk through, demo | cursor, style, event |
|
||||
| `event-*` | event reading demos | (async) event reading |
|
||||
| `stderr` | crossterm over stderr demo | raw mode, alternate screen, custom output |
|
||||
| `is_tty` | Is this instance a tty ? | tty |
|
||||
|
||||
## Run examples
|
||||
|
||||
```bash
|
||||
$ cargo run --example [file name]
|
||||
```
|
||||
|
||||
To run the interactive-demo go into the folder `examples/interactive-demo` and run `cargo run`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details.
|
||||
|
||||
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[l2]: LICENSE
|
||||
|
||||
[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
|
||||
[l5]: https://discord.gg/K4nyTDB
|
||||
|
||||
[s7]: https://travis-ci.org/crossterm-rs/examples.svg?branch=master
|
|
@ -1,68 +0,0 @@
|
|||
//! Demonstrates how to match on modifiers like: Control, alt, shift.
|
||||
//!
|
||||
//! cargo run --example event-match-modifiers
|
||||
|
||||
use keyfork_crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
fn match_event(read_event: Event) {
|
||||
match read_event {
|
||||
// Match one one modifier:
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
code,
|
||||
..
|
||||
}) => {
|
||||
println!("Control + {:?}", code);
|
||||
}
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
code,
|
||||
..
|
||||
}) => {
|
||||
println!("Shift + {:?}", code);
|
||||
}
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::ALT,
|
||||
code,
|
||||
..
|
||||
}) => {
|
||||
println!("Alt + {:?}", code);
|
||||
}
|
||||
|
||||
// Match on multiple modifiers:
|
||||
Event::Key(KeyEvent {
|
||||
code, modifiers, ..
|
||||
}) => {
|
||||
if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) {
|
||||
println!("Alt + Shift {:?}", code);
|
||||
} else {
|
||||
println!("({:?}) with key: {:?}", modifiers, code)
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('z'),
|
||||
KeyModifiers::CONTROL,
|
||||
)));
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Left,
|
||||
KeyModifiers::SHIFT,
|
||||
)));
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Delete,
|
||||
KeyModifiers::ALT,
|
||||
)));
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Right,
|
||||
KeyModifiers::ALT | KeyModifiers::SHIFT,
|
||||
)));
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Home,
|
||||
KeyModifiers::ALT | KeyModifiers::CONTROL,
|
||||
)));
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
//! Demonstrates how to match on modifiers like: Control, alt, shift.
|
||||
//!
|
||||
//! cargo run --example event-poll-read
|
||||
|
||||
use std::{io, time::Duration};
|
||||
|
||||
use keyfork_crossterm::{
|
||||
cursor::position,
|
||||
event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
|
||||
const HELP: &str = r#"Blocking poll() & non-blocking read()
|
||||
- Keyboard, mouse and terminal resize events enabled
|
||||
- Prints "." every second if there's no event
|
||||
- Hit "c" to print current cursor position
|
||||
- Use Esc to quit
|
||||
"#;
|
||||
|
||||
fn print_events() -> io::Result<()> {
|
||||
loop {
|
||||
// Wait up to 1s for another event
|
||||
if poll(Duration::from_millis(1_000))? {
|
||||
// It's guaranteed that read() won't block if `poll` returns `Ok(true)`
|
||||
let event = read()?;
|
||||
|
||||
println!("Event::{:?}\r", event);
|
||||
|
||||
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||
println!("Cursor position: {:?}\r", position());
|
||||
}
|
||||
|
||||
if event == Event::Key(KeyCode::Esc.into()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Timeout expired, no event for 1s
|
||||
println!(".\r");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
println!("{}", HELP);
|
||||
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnableMouseCapture)?;
|
||||
|
||||
if let Err(e) = print_events() {
|
||||
println!("Error: {:?}\r", e);
|
||||
}
|
||||
|
||||
execute!(stdout, DisableMouseCapture)?;
|
||||
|
||||
disable_raw_mode()
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
//! Demonstrates how to block read characters or a full line.
|
||||
//! Just note that crossterm is not required to do this and can be done with `io::stdin()`.
|
||||
//!
|
||||
//! cargo run --example event-read-char-line
|
||||
|
||||
use std::io;
|
||||
|
||||
use keyfork_crossterm::event::{self, Event, KeyCode, KeyEvent};
|
||||
|
||||
/// Read a character from input.
|
||||
pub fn read_char() -> io::Result<char> {
|
||||
loop {
|
||||
if let Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
..
|
||||
}) = event::read()?
|
||||
{
|
||||
return Ok(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a line from input.
|
||||
pub fn read_line() -> io::Result<String> {
|
||||
let mut line = String::new();
|
||||
while let Event::Key(KeyEvent { code, .. }) = event::read()? {
|
||||
match code {
|
||||
KeyCode::Enter => {
|
||||
break;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
line.push(c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("read line:");
|
||||
println!("{:?}", read_line());
|
||||
println!("read char:");
|
||||
println!("{:?}", read_char());
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
//! Demonstrates how to block read events.
|
||||
//!
|
||||
//! cargo run --example event-read
|
||||
|
||||
use std::io;
|
||||
|
||||
use keyfork_crossterm::event::{
|
||||
poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use keyfork_crossterm::{
|
||||
cursor::position,
|
||||
event::{
|
||||
read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, Event, KeyCode,
|
||||
},
|
||||
execute, queue,
|
||||
terminal::{disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
const HELP: &str = r#"Blocking read()
|
||||
- Keyboard, mouse, focus and terminal resize events enabled
|
||||
- Hit "c" to print current cursor position
|
||||
- Use Esc to quit
|
||||
"#;
|
||||
|
||||
fn print_events() -> io::Result<()> {
|
||||
loop {
|
||||
// Blocking read
|
||||
let event = read()?;
|
||||
|
||||
println!("Event: {:?}\r", event);
|
||||
|
||||
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||
println!("Cursor position: {:?}\r", position());
|
||||
}
|
||||
|
||||
if let Event::Resize(x, y) = event {
|
||||
let (original_size, new_size) = flush_resize_events((x, y));
|
||||
println!("Resize from: {:?}, to: {:?}\r", original_size, new_size);
|
||||
}
|
||||
|
||||
if event == Event::Key(KeyCode::Esc.into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Resize events can occur in batches.
|
||||
// With a simple loop they can be flushed.
|
||||
// This function will keep the first and last resize event.
|
||||
fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) {
|
||||
let mut last_resize = first_resize;
|
||||
while let Ok(true) = poll(Duration::from_millis(50)) {
|
||||
if let Ok(Event::Resize(x, y)) = read() {
|
||||
last_resize = (x, y);
|
||||
}
|
||||
}
|
||||
|
||||
(first_resize, last_resize)
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
println!("{}", HELP);
|
||||
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
let supports_keyboard_enhancement = matches!(
|
||||
keyfork_crossterm::terminal::supports_keyboard_enhancement(),
|
||||
Ok(true)
|
||||
);
|
||||
|
||||
if supports_keyboard_enhancement {
|
||||
queue!(
|
||||
stdout,
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
)
|
||||
)?;
|
||||
}
|
||||
|
||||
execute!(
|
||||
stdout,
|
||||
EnableBracketedPaste,
|
||||
EnableFocusChange,
|
||||
EnableMouseCapture,
|
||||
)?;
|
||||
|
||||
if let Err(e) = print_events() {
|
||||
println!("Error: {:?}\r", e);
|
||||
}
|
||||
|
||||
if supports_keyboard_enhancement {
|
||||
queue!(stdout, PopKeyboardEnhancementFlags)?;
|
||||
}
|
||||
|
||||
execute!(
|
||||
stdout,
|
||||
DisableBracketedPaste,
|
||||
PopKeyboardEnhancementFlags,
|
||||
DisableFocusChange,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
|
||||
disable_raw_mode()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue