Compare commits
No commits in common. "main" and "ryan/use-instant-time-qrcode" have entirely different histories.
main
...
ryan/use-i
|
@ -1 +0,0 @@
|
||||||
audits filter=lfs diff=lfs merge=lfs -text
|
|
557
CHANGELOG.md
557
CHANGELOG.md
|
@ -1,557 +0,0 @@
|
||||||
# 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
|
|
||||||
would not be able to properly verify the length of remote shard QR codes.
|
|
||||||
|
|
||||||
# Keyfork v0.2.0
|
|
||||||
|
|
||||||
Some of the changes in this release are based on feedback from audits
|
|
||||||
(publications coming soon!). The previous version of Keyfork, in almost every
|
|
||||||
configuration, is safe to use. The most significant change in this version
|
|
||||||
affects Keyfork Shard, which has an incompatible difference between this
|
|
||||||
version and the previous version. Information about shards, such as the length
|
|
||||||
of the shard, could be leaked and discovered by an attacker when using the
|
|
||||||
Remote Shard recovery mechanism.
|
|
||||||
|
|
||||||
An additional change is the requirement of hardened indices on the first two
|
|
||||||
levels of key derivation. This is due to Keyfork potentially leaking private
|
|
||||||
keys when hardened derivation is not used. To be completely honest, I don't
|
|
||||||
entirely understand the math behind it.
|
|
||||||
|
|
||||||
There is no reason to upgrade if Keyfork has been used as-is, as all supported
|
|
||||||
provisioners at this point in time require hardened derivation at all steps.
|
|
||||||
|
|
||||||
### Changes in keyfork:
|
|
||||||
|
|
||||||
```
|
|
||||||
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
|
|
||||||
5d2309e keyfork-prompt: add SecurePinValidator for making new, secure, PINs
|
|
||||||
cdf4015 keyfork wizard: use correct derivation path for re-deriving shard decryption keys
|
|
||||||
f0e5ae9 keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE
|
|
||||||
289cec3 keyfork wizard: upcast i and index to avoid wrapping add
|
|
||||||
9394500 keyfork-shard: generate nonce using hkdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyfork-derive-openpgp:
|
|
||||||
|
|
||||||
```
|
|
||||||
f0e5ae9 keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE
|
|
||||||
9f089e7 keyfork-derive-openpgp: use .first() in place of .get(0)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyfork-derive-util:
|
|
||||||
|
|
||||||
```
|
|
||||||
de4e98a keyfork-derive-util: black-box checking all zeroes
|
|
||||||
48ccd7c keyfork-derive-util: add note about potential side-channel when verifying keys
|
|
||||||
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
|
|
||||||
1de466c keyfork-derive-util: allow zeroable input for non-master-key derivation
|
|
||||||
61871a7 keyfork-derive-util: make private and public test keys more visible
|
|
||||||
2bca0a1 keyfork-derive-util: make Test{Public,Private}Key public, rename Internal algorithm
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyfork-entropy:
|
|
||||||
|
|
||||||
```
|
|
||||||
5438f4e keyfork-entropy: downgrade entropy size limit to warning
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyfork-mnemonic-util:
|
|
||||||
|
|
||||||
```
|
|
||||||
001fc0b remove trailing hitespace :(
|
|
||||||
6a265ad keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyfork-prompt:
|
|
||||||
|
|
||||||
```
|
|
||||||
5d2309e keyfork-prompt: add SecurePinValidator for making new, secure, PINs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyfork-qrcode:
|
|
||||||
|
|
||||||
```
|
|
||||||
fa125e7 keyfork-qrcode: prefer Instant over SystemTime for infallible time comparison
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyfork-shard:
|
|
||||||
|
|
||||||
```
|
|
||||||
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
|
|
||||||
1a036a0 keyfork-shard: clean up documentation for encrypted shard padding
|
|
||||||
e068743 keyfork-shard: display error message on duplicate key fingerprints found
|
|
||||||
23db509 keyfork-shard: improve wording for counting shardholders
|
|
||||||
9461772 keyfork-shard: ignore duplicate certificate entries
|
|
||||||
6a265ad keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes
|
|
||||||
c0b19e2 keyfork-shard: assert shared secrets are contributory
|
|
||||||
0fe5301 keyfork-shard: add in bug messages
|
|
||||||
08a66e2 keyfork-shard: base64 encode content instead of base16
|
|
||||||
6fa434e keyfork-shard: shorten length and pad inside encrypted block
|
|
||||||
9394500 keyfork-shard: generate nonce using hkdf
|
|
||||||
194d475 keyfork-shard: validate signatures using shard-specific validation requirements
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyfork-zbar:
|
|
||||||
|
|
||||||
```
|
|
||||||
0c76869 .cargo/config.toml: add registry configuration :)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyforkd:
|
|
||||||
|
|
||||||
```
|
|
||||||
bcfcc87 keyforkd: add warning when loading seed with less than 128 bits
|
|
||||||
40551a5 keyforkd: require hardened derivation on two highest indexes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyforkd-client:
|
|
||||||
|
|
||||||
```
|
|
||||||
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
|
|
||||||
1de466c keyfork-derive-util: allow zeroable input for non-master-key derivation
|
|
||||||
40551a5 keyforkd: require hardened derivation on two highest indexes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes in keyforkd-models:
|
|
||||||
|
|
||||||
```
|
|
||||||
40551a5 keyforkd: require hardened derivation on two highest indexes
|
|
||||||
```
|
|
||||||
|
|
||||||
# Keyfork v0.1.0
|
|
||||||
|
|
||||||
### Tagged releases:
|
|
||||||
|
|
||||||
* `keyfork-bin 0.1.0`
|
|
||||||
* `keyfork-bug 0.1.0`
|
|
||||||
* `keyfork-crossterm 0.27.1`
|
|
||||||
* `keyfork-derive-key 0.1.0`
|
|
||||||
* `keyfork-derive-openpgp 0.1.0`
|
|
||||||
* `keyfork-derive-path-data 0.1.0`
|
|
||||||
* `keyfork-derive-util 0.1.0`
|
|
||||||
* `keyfork-entropy 0.1.0`
|
|
||||||
* `keyfork-frame 0.1.0`
|
|
||||||
* `keyfork-mnemonic-util 0.2.0`
|
|
||||||
* `keyfork-prompt 0.1.0`
|
|
||||||
* `keyfork-qrcode 0.1.0`
|
|
||||||
* `keyfork-shard 0.1.0`
|
|
||||||
* `keyfork-slip10-test-data 0.1.0`
|
|
||||||
* `keyfork 0.1.0`
|
|
||||||
* `keyfork-zbar-sys 0.1.0`
|
|
||||||
* `keyfork-zbar 0.1.0`
|
|
||||||
* `keyforkd-client 0.1.0`
|
|
||||||
* `keyforkd-models 0.1.0`
|
|
||||||
* `keyforkd 0.1.0`
|
|
||||||
* `smex 0.1.0`
|
|
File diff suppressed because it is too large
Load Diff
76
Cargo.toml
76
Cargo.toml
|
@ -19,85 +19,13 @@ members = [
|
||||||
"crates/util/keyfork-crossterm",
|
"crates/util/keyfork-crossterm",
|
||||||
"crates/util/keyfork-entropy",
|
"crates/util/keyfork-entropy",
|
||||||
"crates/util/keyfork-frame",
|
"crates/util/keyfork-frame",
|
||||||
"crates/util/keyfork-mnemonic",
|
"crates/util/keyfork-mnemonic-util",
|
||||||
"crates/util/keyfork-prompt",
|
"crates/util/keyfork-prompt",
|
||||||
"crates/util/keyfork-slip10-test-data",
|
"crates/util/keyfork-slip10-test-data",
|
||||||
"crates/util/smex",
|
"crates/util/smex",
|
||||||
"crates/tests",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
|
||||||
all = { level = "deny", priority = -1 }
|
|
||||||
pedantic = { level = "warn", priority = -1 }
|
|
||||||
|
|
||||||
# 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-openpgp = { version = "0.1.2", path = "crates/derive/keyfork-derive-openpgp", registry = "distrust", default-features = false }
|
|
||||||
keyfork-derive-path-data = { version = "0.1.1", path = "crates/derive/keyfork-derive-path-data", registry = "distrust", default-features = false }
|
|
||||||
keyfork-derive-util = { version = "0.2.0", path = "crates/derive/keyfork-derive-util", registry = "distrust", default-features = false }
|
|
||||||
keyfork-shard = { version = "0.3.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.1", path = "crates/util/keyfork-bug", registry = "distrust", default-features = false }
|
|
||||||
keyfork-crossterm = { version = "0.27.1", path = "crates/util/keyfork-crossterm", registry = "distrust", default-features = false }
|
|
||||||
keyfork-entropy = { version = "0.1.1", path = "crates/util/keyfork-entropy", registry = "distrust", default-features = false }
|
|
||||||
keyfork-frame = { version = "0.1.0", path = "crates/util/keyfork-frame", registry = "distrust", default-features = false }
|
|
||||||
keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false }
|
|
||||||
keyfork-prompt = { version = "0.2.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 }
|
|
||||||
|
|
||||||
# External dependencies
|
|
||||||
|
|
||||||
# Cryptography
|
|
||||||
ed25519-dalek = "2.1.1"
|
|
||||||
hmac = "0.12.1"
|
|
||||||
k256 = { version = "0.13.3", default-features = false, features = ["std"] }
|
|
||||||
sha2 = "0.10.8"
|
|
||||||
|
|
||||||
# OpenPGP
|
|
||||||
card-backend-pcsc = "0.5.0"
|
|
||||||
openpgp-card = { version = "0.4.1" }
|
|
||||||
openpgp-card-sequoia = { version = "0.2.0", default-features = false }
|
|
||||||
sequoia-openpgp = { version = "1.21.2", default-features = false, features = ["compression"] }
|
|
||||||
|
|
||||||
# Serialization
|
|
||||||
bincode = "1.3.3"
|
|
||||||
serde = { version= "1.0.195", features = ["derive"] }
|
|
||||||
serde_json = "1.0.111"
|
|
||||||
|
|
||||||
# Misc.
|
|
||||||
anyhow = "1.0.79"
|
|
||||||
hex-literal = "0.4.1"
|
|
||||||
image = { version = "0.25.2", default-features = false }
|
|
||||||
thiserror = "1.0.56"
|
|
||||||
tokio = "1.35.1"
|
|
||||||
v4l = "0.14.0"
|
|
||||||
base64 = "0.22.1"
|
|
||||||
tempfile = "3.17.1"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
debug = true
|
|
||||||
|
|
||||||
[profile.dev.package.keyfork-qrcode]
|
[profile.dev.package.keyfork-qrcode]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Releasing new versions
|
|
||||||
|
|
||||||
* Add and review a new blurb to the changelog by running the
|
|
||||||
`make-changelog-blurb.sh` script and appending the result to the top of
|
|
||||||
the file.
|
|
||||||
* Make sure to add some human-readable snippets at the top!
|
|
||||||
* Update all versions of crates listed in the changelog.
|
|
||||||
* Commit changes.
|
|
||||||
* Run the `sign-new-versions.sh` script to tag the new versions.
|
|
||||||
* Run the `publish.sh` script to push the latest packages to the Distrust
|
|
||||||
Cargo registry.
|
|
|
@ -178,8 +178,7 @@ keyfork recover mnemonic
|
||||||
|
|
||||||
This guide assumes you are sharding to an `N`-of-`M` system with `I` smart
|
This guide assumes you are sharding to an `N`-of-`M` system with `I` smart
|
||||||
cards per shardholder. The variables will be used in the following commands as
|
cards per shardholder. The variables will be used in the following commands as
|
||||||
`$N`, `$M`, and `$I`. The smart card OpenPGP slots will be factory reset during
|
`$N`, `$M`, and `$I`. The smart cards will be factory reset during the process.
|
||||||
the process.
|
|
||||||
|
|
||||||
On an airgapped system, run the following command to generate a file containing
|
On an airgapped system, run the following command to generate a file containing
|
||||||
encrypted shards of a generated seed:
|
encrypted shards of a generated seed:
|
||||||
|
|
Binary file not shown.
Binary file not shown.
92
bacon.toml
92
bacon.toml
|
@ -1,92 +0,0 @@
|
||||||
# This is a configuration file for the bacon tool
|
|
||||||
#
|
|
||||||
# Bacon repository: https://github.com/Canop/bacon
|
|
||||||
# Complete help on configuration: https://dystroy.org/bacon/config/
|
|
||||||
# You can also check bacon's own bacon.toml file
|
|
||||||
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
|
|
||||||
|
|
||||||
default_job = "check"
|
|
||||||
|
|
||||||
[jobs.check]
|
|
||||||
command = ["cargo", "check", "--color", "always"]
|
|
||||||
need_stdout = false
|
|
||||||
|
|
||||||
[jobs.check-all]
|
|
||||||
command = ["cargo", "check", "--all-targets", "--color", "always"]
|
|
||||||
need_stdout = false
|
|
||||||
|
|
||||||
[jobs.clippy]
|
|
||||||
command = [
|
|
||||||
"cargo", "clippy",
|
|
||||||
"--all-targets",
|
|
||||||
"--color", "always",
|
|
||||||
]
|
|
||||||
need_stdout = false
|
|
||||||
|
|
||||||
[jobs.clippy-unwrap]
|
|
||||||
command = [
|
|
||||||
"cargo", "clippy",
|
|
||||||
"--lib",
|
|
||||||
"--color", "always",
|
|
||||||
"--",
|
|
||||||
"-W",
|
|
||||||
"clippy::unwrap_used",
|
|
||||||
"-W",
|
|
||||||
"clippy::expect_used",
|
|
||||||
]
|
|
||||||
need_stdout = false
|
|
||||||
|
|
||||||
# This job lets you run
|
|
||||||
# - all tests: bacon test
|
|
||||||
# - a specific test: bacon test -- config::test_default_files
|
|
||||||
# - the tests of a package: bacon test -- -- -p config
|
|
||||||
[jobs.test]
|
|
||||||
command = [
|
|
||||||
"cargo", "test", "--color", "always",
|
|
||||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
|
||||||
]
|
|
||||||
need_stdout = true
|
|
||||||
|
|
||||||
[jobs.doc]
|
|
||||||
command = ["cargo", "doc", "--color", "always", "--no-deps"]
|
|
||||||
need_stdout = false
|
|
||||||
|
|
||||||
# If the doc compiles, then it opens in your browser and bacon switches
|
|
||||||
# to the previous job
|
|
||||||
[jobs.doc-open]
|
|
||||||
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
|
|
||||||
need_stdout = false
|
|
||||||
on_success = "back" # so that we don't open the browser at each change
|
|
||||||
|
|
||||||
# You can run your application and have the result displayed in bacon,
|
|
||||||
# *if* it makes sense for this crate.
|
|
||||||
# Don't forget the `--color always` part or the errors won't be
|
|
||||||
# properly parsed.
|
|
||||||
# If your program never stops (eg a server), you may set `background`
|
|
||||||
# to false to have the cargo run output immediately displayed instead
|
|
||||||
# of waiting for program's end.
|
|
||||||
[jobs.run]
|
|
||||||
command = [
|
|
||||||
"cargo", "run",
|
|
||||||
"--color", "always",
|
|
||||||
# put launch parameters for your program behind a `--` separator
|
|
||||||
]
|
|
||||||
need_stdout = true
|
|
||||||
allow_warnings = true
|
|
||||||
background = true
|
|
||||||
|
|
||||||
# This parameterized job runs the example of your choice, as soon
|
|
||||||
# as the code compiles.
|
|
||||||
# Call it as
|
|
||||||
# bacon ex -- my-example
|
|
||||||
[jobs.ex]
|
|
||||||
command = ["cargo", "run", "--color", "always", "--example"]
|
|
||||||
need_stdout = true
|
|
||||||
allow_warnings = true
|
|
||||||
|
|
||||||
# You may define here keybindings that would be specific to
|
|
||||||
# a project, for example a shortcut to launch a specific job.
|
|
||||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
|
||||||
# should go in your personal global prefs.toml file instead.
|
|
||||||
[keybindings]
|
|
||||||
# alt-m = "job:my-job"
|
|
|
@ -1 +0,0 @@
|
||||||
doc-valid-idents = ["OpenPGP", ".."]
|
|
|
@ -1,12 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyforkd-client"
|
name = "keyforkd-client"
|
||||||
version = "0.2.2"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -15,14 +12,14 @@ ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"]
|
||||||
secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
|
secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-derive-util = { workspace = true, default-features = false }
|
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false, registry = "distrust" }
|
||||||
keyfork-frame = { workspace = true }
|
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame", registry = "distrust" }
|
||||||
keyforkd-models = { workspace = true }
|
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models", registry = "distrust" }
|
||||||
bincode = { workspace = true }
|
bincode = "1.3.3"
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.49"
|
||||||
k256 = { workspace = true, default-features = false, features = ["std"], optional = true }
|
k256 = { version = "0.13.3", optional = true }
|
||||||
ed25519-dalek = { workspace = true, optional = true }
|
ed25519-dalek = { version = "2.1.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
keyfork-slip10-test-data = { workspace = true }
|
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data", registry = "distrust" }
|
||||||
keyforkd = { workspace = true }
|
keyforkd = { path = "../keyforkd", registry = "distrust" }
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
//! # The Keyforkd Client
|
//! # The Keyforkd Client
|
||||||
//!
|
//!
|
||||||
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation
|
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation
|
||||||
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". This allows
|
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". The server is operated
|
||||||
//! enforcement of policies, such as requiring at least two leves of a derivation path (for
|
//! on a UNIX socket with messages sent using the Keyfork Frame format.
|
||||||
//! instance, `m/0'` would not be allowed, but `m/0'/0'` would). The server is operated on a UNIX
|
|
||||||
//! socket with messages sent using the Keyfork Frame format.
|
|
||||||
//!
|
//!
|
||||||
//! Programs using the Keyfork Client should ensure they are built against a compatible version of
|
//! Programs using the Keyfork Client should ensure they are built against a compatible version of
|
||||||
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
|
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
|
||||||
|
@ -12,161 +10,53 @@
|
||||||
//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but
|
//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but
|
||||||
//! `1.x.y` will not be compatible with `2.0.0`.
|
//! `1.x.y` will not be compatible with `2.0.0`.
|
||||||
//!
|
//!
|
||||||
//! The Keyfork Client documentation makes extensive use of the `keyforkd::test_util` module.
|
//! Presently, the Keyfork server only supports the following requests:
|
||||||
//! This provides testing infrastructure to set up a temporary Keyfork Daemon. In
|
|
||||||
//! your code, you should assume the daemon has already been initialized, whether by another
|
|
||||||
//! process, on another terminal, or some other instance. At no point should a program deriving an
|
|
||||||
//! "endpoint" key have control over a mnemonic or a seed.
|
|
||||||
//!
|
//!
|
||||||
//! ## Server Requests
|
//! * Derive Key
|
||||||
//!
|
|
||||||
//! Keyfork is designed as a client-request/server-response model. The client sends a request, such
|
|
||||||
//! as a derivation request, and the server sends its response. Presently, the Keyfork server
|
|
||||||
//! supports the following requests:
|
|
||||||
//!
|
|
||||||
//! ### Request: Derive Key
|
|
||||||
//!
|
|
||||||
//! The client creates a derivation path of at least two indices and requests a derived `XPrv`
|
|
||||||
//! (Extended Private Key) from the server.
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! use std::str::FromStr;
|
|
||||||
//!
|
|
||||||
//! use keyforkd_client::Client;
|
|
||||||
//! use keyfork_derive_util::DerivationPath;
|
|
||||||
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
|
||||||
//! // use k256::SecretKey as PrivateKey;
|
|
||||||
//! // use ed25519_dalek::SigningKey as PrivateKey;
|
|
||||||
//!
|
|
||||||
//! #[derive(Debug, thiserror::Error)]
|
|
||||||
//! enum Error {
|
|
||||||
//! #[error(transparent)]
|
|
||||||
//! Path(#[from] keyfork_derive_util::PathError),
|
|
||||||
//!
|
|
||||||
//! #[error(transparent)]
|
|
||||||
//! Keyforkd(#[from] keyforkd_client::Error),
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! fn main() -> Result<(), Error> {
|
|
||||||
//! # let seed = b"funky accordion noises";
|
|
||||||
//! # keyforkd::test_util::run_test(seed, |socket_path| {
|
|
||||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
|
|
||||||
//! let mut client = Client::discover_socket()?;
|
|
||||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
|
|
||||||
//! # Ok::<_, Error>(())
|
|
||||||
//! # })?;
|
|
||||||
//! Ok(())
|
|
||||||
//! }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ---
|
|
||||||
//!
|
|
||||||
//! Request objects are typically handled by the Keyfork Client library (such as with
|
|
||||||
//! [`Client::request_xprv`]). While unadvised, clients can also attempt to handle their own
|
|
||||||
//! requests, using [`Client::request`].
|
|
||||||
//!
|
//!
|
||||||
//! ## Extended Private Keys
|
//! ## Extended Private Keys
|
||||||
//!
|
//!
|
||||||
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
|
//! 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
|
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
|
||||||
//! will be derived similarly between the server and the client.
|
//! will be derived similarly between the server and the client.
|
||||||
//!
|
//!
|
||||||
|
//! # Examples
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use std::str::FromStr;
|
//! use std::str::FromStr;
|
||||||
//!
|
//!
|
||||||
//! use keyforkd_client::Client;
|
//! use keyforkd_client::Client;
|
||||||
//! use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
//! use keyfork_derive_util::DerivationPath;
|
||||||
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||||
//! // use k256::SecretKey as PrivateKey;
|
//! // use k256::SecretKey as PrivateKey;
|
||||||
//! // use ed25519_dalek::SigningKey as PrivateKey;
|
//! // use ed25519_dalek::SigningKey as PrivateKey;
|
||||||
//! # fn check_wallet<T>(_: T) {}
|
|
||||||
//!
|
//!
|
||||||
//! #[derive(Debug, thiserror::Error)]
|
|
||||||
//! enum Error {
|
|
||||||
//! #[error(transparent)]
|
|
||||||
//! Index(#[from] keyfork_derive_util::IndexError),
|
|
||||||
//!
|
|
||||||
//! #[error(transparent)]
|
|
||||||
//! Path(#[from] keyfork_derive_util::PathError),
|
|
||||||
//!
|
|
||||||
//! #[error(transparent)]
|
|
||||||
//! PrivateKey(#[from] keyfork_derive_util::PrivateKeyError),
|
|
||||||
//!
|
|
||||||
//! #[error(transparent)]
|
|
||||||
//! Keyforkd(#[from] keyforkd_client::Error),
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! fn main() -> Result<(), Error> {
|
|
||||||
//! # let seed = b"funky accordion noises";
|
//! # let seed = b"funky accordion noises";
|
||||||
//! # keyforkd::test_util::run_test(seed, |socket_path| {
|
//! # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'/0'/0")?;
|
//! # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
|
||||||
//! let mut client = Client::discover_socket()?;
|
|
||||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
|
|
||||||
//! // scan first 20 wallets
|
|
||||||
//! for index in 0..20 {
|
|
||||||
//! // use non-hardened derivation
|
|
||||||
//! let new_xprv = xprv.derive_child(&DerivationIndex::new(index, false)?);
|
|
||||||
//! check_wallet(new_xprv)
|
|
||||||
//! }
|
|
||||||
//! # Ok::<_, Error>(())
|
|
||||||
//! # })?;
|
|
||||||
//! Ok(())
|
|
||||||
//! }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## Testing Infrastructure
|
|
||||||
//!
|
|
||||||
//! In tests, the `keyforkd::test_util` module and `TestPrivateKeys` can be used. These provide
|
|
||||||
//! useful utilities for writing tests that interact with the Keyfork Server without needing to
|
|
||||||
//! manually create the server for the purpose of the test. The `run_test` method can be used to
|
|
||||||
//! run a test, which can handle both returning errors and correctly translating panics (though,
|
|
||||||
//! the panics definitely won't look tidy).
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! use std::str::FromStr;
|
|
||||||
//!
|
|
||||||
//! use keyforkd_client::Client;
|
|
||||||
//! use keyfork_derive_util::DerivationPath;
|
|
||||||
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
|
||||||
//!
|
|
||||||
//! #[derive(Debug, thiserror::Error)]
|
|
||||||
//! enum Error {
|
|
||||||
//! #[error(transparent)]
|
|
||||||
//! Path(#[from] keyfork_derive_util::PathError),
|
|
||||||
//!
|
|
||||||
//! #[error(transparent)]
|
|
||||||
//! Keyforkd(#[from] keyforkd_client::Error),
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! fn main() -> Result<(), Error> {
|
|
||||||
//! let seed = b"funky accordion noises";
|
|
||||||
//! keyforkd::test_util::run_test(seed, |socket_path| {
|
|
||||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
|
|
||||||
//! let mut client = Client::discover_socket()?;
|
|
||||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
|
|
||||||
//! Ok::<_, Error>(())
|
|
||||||
//! })?;
|
|
||||||
//! Ok(())
|
|
||||||
//! }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! If you would rather write tests to panic rather than error, or would rather not deal with error
|
|
||||||
//! types, the Panicable type should be used, which will handle the Error type for the closure.
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! use std::str::FromStr;
|
|
||||||
//!
|
|
||||||
//! use keyforkd_client::Client;
|
|
||||||
//! use keyfork_derive_util::DerivationPath;
|
|
||||||
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
|
||||||
//!
|
|
||||||
//! let seed = b"funky accordion noises";
|
|
||||||
//! keyforkd::test_util::run_test(seed, |socket_path| {
|
|
||||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
||||||
//! let mut client = Client::discover_socket().unwrap();
|
//! let mut client = Client::discover_socket().unwrap();
|
||||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
||||||
//! keyforkd::test_util::Panicable::Ok(())
|
//! # keyforkd::test_util::Infallible::Ok(())
|
||||||
|
//! # }).unwrap();
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used.
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use std::str::FromStr;
|
||||||
|
//!
|
||||||
|
//! use keyforkd_client::Client;
|
||||||
|
//! use keyfork_derive_util::DerivationPath;
|
||||||
|
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||||
|
//!
|
||||||
|
//! let seed = b"funky accordion noises";
|
||||||
|
//! keyforkd::test_util::run_test(seed, |socket_path| {
|
||||||
|
//! std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
|
||||||
|
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
||||||
|
//! let mut client = Client::discover_socket().unwrap();
|
||||||
|
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
||||||
|
//! keyforkd::test_util::Infallible::Ok(())
|
||||||
//! }).unwrap();
|
//! }).unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
@ -199,10 +89,6 @@ pub enum Error {
|
||||||
#[error("Socket was unable to connect to {1}: {0} (make sure keyforkd is running)")]
|
#[error("Socket was unable to connect to {1}: {0} (make sure keyforkd is running)")]
|
||||||
Connect(std::io::Error, PathBuf),
|
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.
|
/// Data could not be written to, or read from, the socket.
|
||||||
#[error("Could not write to or from the socket: {0}")]
|
#[error("Could not write to or from the socket: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
@ -222,10 +108,6 @@ pub enum Error {
|
||||||
/// An error encountered in Keyforkd.
|
/// An error encountered in Keyforkd.
|
||||||
#[error("Error in Keyforkd: {0}")]
|
#[error("Error in Keyforkd: {0}")]
|
||||||
Keyforkd(#[from] KeyforkdError),
|
Keyforkd(#[from] KeyforkdError),
|
||||||
|
|
||||||
/// An invalid key was returned.
|
|
||||||
#[error("Invalid key returned")]
|
|
||||||
InvalidKey,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
|
@ -241,9 +123,12 @@ pub fn get_socket() -> Result<UnixStream, Error> {
|
||||||
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
||||||
.collect::<HashMap<String, String>>();
|
.collect::<HashMap<String, String>>();
|
||||||
let mut socket_path: PathBuf;
|
let mut socket_path: PathBuf;
|
||||||
if let Some(occupied) = socket_vars.get("KEYFORKD_SOCKET_PATH") {
|
#[allow(clippy::single_match_else)]
|
||||||
|
match socket_vars.get("KEYFORKD_SOCKET_PATH") {
|
||||||
|
Some(occupied) => {
|
||||||
socket_path = PathBuf::from(occupied);
|
socket_path = PathBuf::from(occupied);
|
||||||
} else {
|
}
|
||||||
|
None => {
|
||||||
socket_path = PathBuf::from(
|
socket_path = PathBuf::from(
|
||||||
socket_vars
|
socket_vars
|
||||||
.get("XDG_RUNTIME_DIR")
|
.get("XDG_RUNTIME_DIR")
|
||||||
|
@ -251,6 +136,7 @@ pub fn get_socket() -> Result<UnixStream, Error> {
|
||||||
);
|
);
|
||||||
socket_path.extend(["keyforkd", "keyforkd.sock"]);
|
socket_path.extend(["keyforkd", "keyforkd.sock"]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
UnixStream::connect(&socket_path).map_err(|e| Error::Connect(e, socket_path))
|
UnixStream::connect(&socket_path).map_err(|e| Error::Connect(e, socket_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +152,7 @@ pub struct Client {
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client from a given already-connected [`UnixStream`]. This function is
|
/// 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.
|
/// [`Client::discover_socket`] should be preferred.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -275,9 +161,10 @@ impl Client {
|
||||||
///
|
///
|
||||||
/// # let seed = b"funky accordion noises";
|
/// # let seed = b"funky accordion noises";
|
||||||
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||||
/// let mut socket = get_socket()?;
|
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
|
||||||
|
/// let mut socket = get_socket().unwrap();
|
||||||
/// let mut client = Client::new(socket);
|
/// let mut client = Client::new(socket);
|
||||||
/// # Ok::<_, keyforkd_client::Error>(())
|
/// # keyforkd::test_util::Infallible::Ok(())
|
||||||
/// # }).unwrap();
|
/// # }).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new(socket: UnixStream) -> Self {
|
pub fn new(socket: UnixStream) -> Self {
|
||||||
|
@ -296,8 +183,9 @@ impl Client {
|
||||||
///
|
///
|
||||||
/// # let seed = b"funky accordion noises";
|
/// # let seed = b"funky accordion noises";
|
||||||
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||||
/// let mut client = Client::discover_socket()?;
|
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
|
||||||
/// # Ok::<_, keyforkd_client::Error>(())
|
/// let mut client = Client::discover_socket().unwrap();
|
||||||
|
/// # keyforkd::test_util::Infallible::Ok(())
|
||||||
/// # }).unwrap();
|
/// # }).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn discover_socket() -> Result<Self> {
|
pub fn discover_socket() -> Result<Self> {
|
||||||
|
@ -325,10 +213,11 @@ impl Client {
|
||||||
///
|
///
|
||||||
/// # let seed = b"funky accordion noises";
|
/// # let seed = b"funky accordion noises";
|
||||||
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||||
|
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
|
||||||
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
||||||
/// let mut client = Client::discover_socket().unwrap();
|
/// let mut client = Client::discover_socket().unwrap();
|
||||||
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
||||||
/// # keyforkd::test_util::Panicable::Ok(())
|
/// # keyforkd::test_util::Infallible::Ok(())
|
||||||
/// # }).unwrap();
|
/// # }).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
|
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
|
||||||
|
@ -344,27 +233,25 @@ impl Client {
|
||||||
return Err(Error::InvalidResponse);
|
return Err(Error::InvalidResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
let depth = u8::try_from(path.len())?;
|
let depth = path.len() as u8;
|
||||||
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code)
|
Ok(ExtendedPrivateKey::new_from_parts(
|
||||||
.map_err(|_| Error::InvalidKey)
|
&d.data,
|
||||||
|
depth,
|
||||||
|
d.chain_code,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
_ => Err(Error::InvalidResponse),
|
_ => Err(Error::InvalidResponse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
|
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
|
||||||
///
|
///
|
||||||
/// This function does not properly assert the association between a request type and a
|
|
||||||
/// response type, and does not perform any serialization of native objects into Request or
|
|
||||||
/// Response types, and should only be used when absolutely necessary.
|
|
||||||
///
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if:
|
/// An error may be returned if:
|
||||||
/// * Reading or writing from or to the socket encountered an error.
|
/// * Reading or writing from or to the socket encountered an error.
|
||||||
/// * Bincode could not serialize the request or deserialize the response.
|
/// * Bincode could not serialize the request or deserialize the response.
|
||||||
/// * An error occurred in Keyforkd.
|
/// * An error occurred in Keyforkd.
|
||||||
|
#[doc(hidden)]
|
||||||
pub fn request(&mut self, req: &Request) -> Result<Response> {
|
pub fn request(&mut self, req: &Request) -> Result<Response> {
|
||||||
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
|
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
|
||||||
let resp = try_decode_from(&mut self.socket)?;
|
let resp = try_decode_from(&mut self.socket)?;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::Client;
|
use crate::Client;
|
||||||
use keyfork_derive_util::{request::*, DerivationPath};
|
use keyfork_derive_util::{request::*, DerivationPath};
|
||||||
use keyfork_slip10_test_data::test_data;
|
use keyfork_slip10_test_data::test_data;
|
||||||
use keyforkd::test_util::{run_test, Panicable};
|
use keyforkd::test_util::{run_test, Infallible};
|
||||||
use std::{os::unix::net::UnixStream, str::FromStr};
|
use std::{os::unix::net::UnixStream, str::FromStr};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -9,13 +9,14 @@ use std::{os::unix::net::UnixStream, str::FromStr};
|
||||||
fn secp256k1_test_suite() {
|
fn secp256k1_test_suite() {
|
||||||
use k256::SecretKey;
|
use k256::SecretKey;
|
||||||
|
|
||||||
let tests = test_data().unwrap().remove("secp256k1").unwrap();
|
let tests = test_data()
|
||||||
|
.unwrap()
|
||||||
|
.remove(&"secp256k1".to_string())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
for seed_test in tests {
|
for seed_test in tests {
|
||||||
let seed = seed_test.seed;
|
let seed = seed_test.seed;
|
||||||
run_test(
|
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
|
||||||
&seed,
|
|
||||||
move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
|
|
||||||
for test in seed_test.tests {
|
for test in seed_test.tests {
|
||||||
let socket = UnixStream::connect(socket_path).unwrap();
|
let socket = UnixStream::connect(socket_path).unwrap();
|
||||||
let mut client = Client::new(socket);
|
let mut client = Client::new(socket);
|
||||||
|
@ -24,9 +25,6 @@ fn secp256k1_test_suite() {
|
||||||
if chain_len < 2 {
|
if chain_len < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if chain.iter().take(2).any(|index| !index.is_hardened()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Consistency check: ensure the server and the client can each derive the same
|
// Consistency check: ensure the server and the client can each derive the same
|
||||||
// key using an XPrv, for all but the last XPrv, which is verified after this
|
// key using an XPrv, for all but the last XPrv, which is verified after this
|
||||||
for i in 2..chain_len {
|
for i in 2..chain_len {
|
||||||
|
@ -59,8 +57,7 @@ fn secp256k1_test_suite() {
|
||||||
assert_eq!(&response.data, test.private_key.as_slice());
|
assert_eq!(&response.data, test.private_key.as_slice());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,7 +67,7 @@ fn secp256k1_test_suite() {
|
||||||
fn ed25519_test_suite() {
|
fn ed25519_test_suite() {
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
|
||||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||||
|
|
||||||
for seed_test in tests {
|
for seed_test in tests {
|
||||||
let seed = seed_test.seed;
|
let seed = seed_test.seed;
|
||||||
|
@ -109,7 +106,7 @@ fn ed25519_test_suite() {
|
||||||
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
|
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
|
||||||
assert_eq!(&response.data, test.private_key.as_slice());
|
assert_eq!(&response.data, test.private_key.as_slice());
|
||||||
}
|
}
|
||||||
Panicable::Ok(())
|
Infallible::Ok(())
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyforkd-models"
|
name = "keyforkd-models"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-derive-util = { workspace = true, default-features = false }
|
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false, registry = "distrust" }
|
||||||
serde = { workspace = true }
|
serde = { version = "1.0.190", features = ["derive"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.50"
|
||||||
|
|
|
@ -43,10 +43,6 @@ pub enum DerivationError {
|
||||||
#[error("Invalid derivation length: Expected at least 2, actual: {0}")]
|
#[error("Invalid derivation length: Expected at least 2, actual: {0}")]
|
||||||
InvalidDerivationLength(usize),
|
InvalidDerivationLength(usize),
|
||||||
|
|
||||||
/// The derivation request did not use hardened derivation on the 2 highest indexes.
|
|
||||||
#[error("Invalid derivation paths: expected index #{0} (1) to be hardened")]
|
|
||||||
InvalidDerivationPath(usize, u32),
|
|
||||||
|
|
||||||
/// An error occurred while deriving data.
|
/// An error occurred while deriving data.
|
||||||
#[error("Derivation error: {0}")]
|
#[error("Derivation error: {0}")]
|
||||||
Derivation(String),
|
Derivation(String),
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyforkd"
|
name = "keyforkd"
|
||||||
version = "0.1.4"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -15,28 +12,28 @@ tracing = ["tower/tracing", "tokio/tracing", "dep:tracing", "dep:tracing-subscri
|
||||||
multithread = ["tokio/rt-multi-thread"]
|
multithread = ["tokio/rt-multi-thread"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-bug = { workspace = true }
|
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
|
||||||
keyfork-derive-util = { workspace = true }
|
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", registry = "distrust" }
|
||||||
keyfork-frame = { workspace = true, features = ["async"] }
|
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame", features = ["async"], registry = "distrust" }
|
||||||
keyfork-mnemonic = { workspace = true }
|
keyfork-mnemonic-util = { version = "0.2.0", path = "../../util/keyfork-mnemonic-util", registry = "distrust" }
|
||||||
keyfork-derive-path-data = { workspace = true }
|
keyfork-derive-path-data = { version = "0.1.0", path = "../../derive/keyfork-derive-path-data", registry = "distrust" }
|
||||||
keyforkd-models = { workspace = true }
|
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models", registry = "distrust" }
|
||||||
|
|
||||||
# Not personally audited
|
# Not personally audited
|
||||||
bincode = { workspace = true }
|
bincode = "1.3.3"
|
||||||
|
|
||||||
# Ecosystem trust, not personally audited
|
# Ecosystem trust, not personally audited
|
||||||
tokio = { workspace = true, features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
|
tokio = { version = "1.32.0", features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
|
||||||
tracing = { version = "0.1.37", optional = true }
|
tracing = { version = "0.1.37", optional = true }
|
||||||
tracing-error = { version = "0.2.0", optional = true }
|
tracing-error = { version = "0.2.0", optional = true }
|
||||||
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
|
||||||
tower = { version = "0.4.13", features = ["tokio", "util"] }
|
tower = { version = "0.4.13", features = ["tokio", "util"] }
|
||||||
|
|
||||||
# Personally audited
|
# Personally audited
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.47"
|
||||||
serde = { workspace = true }
|
serde = { version = "1.0.186", features = ["derive"] }
|
||||||
tempfile = { workspace = true }
|
tempfile = { version = "3.10.0", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = { workspace = true }
|
hex-literal = "0.4.1"
|
||||||
keyfork-slip10-test-data = { workspace = true }
|
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data", registry = "distrust" }
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use keyfork_mnemonic::Mnemonic;
|
pub use keyfork_mnemonic_util::Mnemonic;
|
||||||
pub use tower::ServiceBuilder;
|
pub use tower::ServiceBuilder;
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
|
@ -57,7 +57,7 @@ pub async fn start_and_run_server_on(
|
||||||
let service = ServiceBuilder::new()
|
let service = ServiceBuilder::new()
|
||||||
.layer(middleware::BincodeLayer::new())
|
.layer(middleware::BincodeLayer::new())
|
||||||
// TODO: passphrase support and/or store passphrase with mnemonic
|
// TODO: passphrase support and/or store passphrase with mnemonic
|
||||||
.service(Keyforkd::new(mnemonic.generate_seed(None).to_vec()));
|
.service(Keyforkd::new(mnemonic.generate_seed(None)));
|
||||||
|
|
||||||
let mut server = match UnixServer::bind(socket_path) {
|
let mut server = match UnixServer::bind(socket_path) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
@ -89,10 +89,14 @@ pub async fn start_and_run_server(mnemonic: Mnemonic) -> Result<(), Box<dyn std:
|
||||||
let runtime_vars = std::env::vars()
|
let runtime_vars = std::env::vars()
|
||||||
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
||||||
.collect::<HashMap<String, String>>();
|
.collect::<HashMap<String, String>>();
|
||||||
let runtime_path = if let Some(occupied) = runtime_vars.get("KEYFORKD_SOCKET_PATH") {
|
let mut runtime_path: PathBuf;
|
||||||
PathBuf::from(occupied)
|
#[allow(clippy::single_match_else)]
|
||||||
} else {
|
match runtime_vars.get("KEYFORKD_SOCKET_PATH") {
|
||||||
let mut runtime_path = PathBuf::from(
|
Some(occupied) => {
|
||||||
|
runtime_path = PathBuf::from(occupied);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
runtime_path = PathBuf::from(
|
||||||
runtime_vars
|
runtime_vars
|
||||||
.get("XDG_RUNTIME_DIR")
|
.get("XDG_RUNTIME_DIR")
|
||||||
.ok_or(KeyforkdError::NoSocketPath)?,
|
.ok_or(KeyforkdError::NoSocketPath)?,
|
||||||
|
@ -104,8 +108,8 @@ pub async fn start_and_run_server(mnemonic: Mnemonic) -> Result<(), Box<dyn std:
|
||||||
tokio::fs::create_dir(&runtime_path).await?;
|
tokio::fs::create_dir(&runtime_path).await?;
|
||||||
}
|
}
|
||||||
runtime_path.push("keyforkd.sock");
|
runtime_path.push("keyforkd.sock");
|
||||||
runtime_path
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
debug!(
|
debug!(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! Launch the Keyfork Server from using a mnemonic passed through standard input.
|
//!
|
||||||
|
|
||||||
use keyfork_mnemonic::Mnemonic;
|
use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
|
||||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ pub struct BincodeLayer<'a, Request> {
|
||||||
phantom_request: PhantomData<&'a Request>,
|
phantom_request: PhantomData<&'a Request>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Request> BincodeLayer<'_, Request> {
|
impl<'a, Request> BincodeLayer<'a, Request> {
|
||||||
/// Create a new [`BincodeLayer`].
|
/// Create a new [`BincodeLayer`].
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -21,7 +21,7 @@ impl<Request> BincodeLayer<'_, Request> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Request> Default for BincodeLayer<'_, Request> {
|
impl<'a, Request> Default for BincodeLayer<'a, Request> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
|
@ -162,9 +162,6 @@ mod tests {
|
||||||
.call(content.clone())
|
.call(content.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(result, serialize(&Result::<Test, Infallible>::Ok(test)).unwrap());
|
||||||
result,
|
|
||||||
serialize(&Result::<Test, Infallible>::Ok(test)).unwrap()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,8 +49,7 @@ impl IsDisconnect for EncodeError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnixServer {
|
impl UnixServer {
|
||||||
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also
|
/// 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.
|
||||||
/// creates a `ctrl_c` handler to automatically clean up the socket file.
|
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// This function may return an error if the socket can't be bound.
|
/// This function may return an error if the socket can't be bound.
|
||||||
|
|
|
@ -12,7 +12,7 @@ use keyfork_derive_path_data::guess_target;
|
||||||
// use keyfork_derive_util::request::{DerivationError, DerivationRequest, DerivationResponse};
|
// use keyfork_derive_util::request::{DerivationError, DerivationRequest, DerivationResponse};
|
||||||
use keyforkd_models::{DerivationError, Error, Request, Response};
|
use keyforkd_models::{DerivationError, Error, Request, Response};
|
||||||
use tower::Service;
|
use tower::Service;
|
||||||
use tracing::{info, warn};
|
use tracing::info;
|
||||||
|
|
||||||
// NOTE: All values implemented in Keyforkd must implement Clone with low overhead, either by
|
// NOTE: All values implemented in Keyforkd must implement Clone with low overhead, either by
|
||||||
// using an Arc or by having a small signature. This is because Service<T> takes &mut self.
|
// using an Arc or by having a small signature. This is because Service<T> takes &mut self.
|
||||||
|
@ -38,12 +38,6 @@ impl std::fmt::Debug for Keyforkd {
|
||||||
impl Keyforkd {
|
impl Keyforkd {
|
||||||
/// Create a new instance of Keyfork from a given seed.
|
/// Create a new instance of Keyfork from a given seed.
|
||||||
pub fn new(seed: Vec<u8>) -> Self {
|
pub fn new(seed: Vec<u8>) -> Self {
|
||||||
if seed.len() < 16 {
|
|
||||||
warn!(
|
|
||||||
"Entropy size is lower than 128 bits: {} bits.",
|
|
||||||
seed.len() * 8
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Self {
|
Self {
|
||||||
seed: Arc::new(seed),
|
seed: Arc::new(seed),
|
||||||
}
|
}
|
||||||
|
@ -75,18 +69,6 @@ impl Service<Request> for Keyforkd {
|
||||||
return Err(DerivationError::InvalidDerivationLength(len).into());
|
return Err(DerivationError::InvalidDerivationLength(len).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((i, unhardened_index)) = req
|
|
||||||
.path()
|
|
||||||
.iter()
|
|
||||||
.take(2)
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, index)| !index.is_hardened())
|
|
||||||
{
|
|
||||||
return Err(
|
|
||||||
DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
if let Some(target) = guess_target(req.path()) {
|
if let Some(target) = guess_target(req.path()) {
|
||||||
info!("Deriving path: {target}");
|
info!("Deriving path: {target}");
|
||||||
|
@ -114,7 +96,10 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn properly_derives_secp256k1() {
|
async fn properly_derives_secp256k1() {
|
||||||
let tests = test_data().unwrap().remove("secp256k1").unwrap();
|
let tests = test_data()
|
||||||
|
.unwrap()
|
||||||
|
.remove(&"secp256k1".to_string())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
for per_seed in tests {
|
for per_seed in tests {
|
||||||
let seed = &per_seed.seed;
|
let seed = &per_seed.seed;
|
||||||
|
@ -125,9 +110,6 @@ mod tests {
|
||||||
if chain.len() < 2 {
|
if chain.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if chain.iter().take(2).any(|index| !index.is_hardened()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
|
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
|
||||||
let response: DerivationResponse = keyforkd
|
let response: DerivationResponse = keyforkd
|
||||||
.ready()
|
.ready()
|
||||||
|
@ -146,7 +128,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn properly_derives_ed25519() {
|
async fn properly_derives_ed25519() {
|
||||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||||
|
|
||||||
for per_seed in tests {
|
for per_seed in tests {
|
||||||
let seed = &per_seed.seed;
|
let seed = &per_seed.seed;
|
||||||
|
@ -174,7 +156,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[should_panic(expected = "InvalidDerivationLength(0)")]
|
#[should_panic]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn errors_on_no_path() {
|
async fn errors_on_no_path() {
|
||||||
let tests = [(
|
let tests = [(
|
||||||
|
@ -200,7 +182,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[should_panic(expected = "InvalidDerivationLength(1)")]
|
#[should_panic]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn errors_on_short_path() {
|
async fn errors_on_short_path() {
|
||||||
let tests = [(
|
let tests = [(
|
||||||
|
|
|
@ -12,21 +12,20 @@ use keyfork_bug::bug;
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
#[error("This error can never be instantiated")]
|
#[error("This error can never be instantiated")]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub enum UninstantiableError {}
|
pub struct InfallibleError {
|
||||||
|
protected: (),
|
||||||
|
}
|
||||||
|
|
||||||
/// A panicable result. This type can be used when a closure chooses to panic instead of
|
/// An infallible result. This type can be used to represent a function that should never error.
|
||||||
/// returning an error. This doesn't necessarily mean a closure _has_ to panic, and its absence
|
|
||||||
/// doesn't imply a closure _can't_ panic, but this is a useful utility function for writing tests,
|
|
||||||
/// to avoid the necessity of making custom error types.
|
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use keyforkd::test_util::Panicable;
|
/// use keyforkd::test_util::Infallible;
|
||||||
/// let closure = || {
|
/// let closure = || {
|
||||||
/// Panicable::Ok(())
|
/// Infallible::Ok(())
|
||||||
/// };
|
/// };
|
||||||
/// assert!(closure().is_ok());
|
/// assert!(closure().is_ok());
|
||||||
/// ```
|
/// ```
|
||||||
pub type Panicable = std::result::Result<(), UninstantiableError>;
|
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
|
||||||
|
|
||||||
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
|
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
|
||||||
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
|
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
|
||||||
|
@ -40,8 +39,6 @@ pub type Panicable = std::result::Result<(), UninstantiableError>;
|
||||||
/// runtime.
|
/// runtime.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// The test utility provides a socket that can be connected to for deriving keys.
|
|
||||||
///
|
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use std::os::unix::net::UnixStream;
|
/// use std::os::unix::net::UnixStream;
|
||||||
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
@ -49,22 +46,10 @@ pub type Panicable = std::result::Result<(), UninstantiableError>;
|
||||||
/// UnixStream::connect(&path).map(|_| ())
|
/// UnixStream::connect(&path).map(|_| ())
|
||||||
/// }).unwrap();
|
/// }).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
///
|
|
||||||
/// The `keyforkd-client` crate uses the `KEYFORKD_SOCKET_PATH` variable to determine the default
|
|
||||||
/// socket path. The test will export the environment variable so it may be used by default.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use std::os::unix::net::UnixStream;
|
|
||||||
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
||||||
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
|
|
||||||
/// assert_eq!(std::env::var_os("KEYFORKD_SOCKET_PATH").unwrap(), path.as_os_str());
|
|
||||||
/// UnixStream::connect(&path).map(|_| ())
|
|
||||||
/// }).unwrap();
|
|
||||||
/// ```
|
|
||||||
#[allow(clippy::missing_errors_doc)]
|
#[allow(clippy::missing_errors_doc)]
|
||||||
pub fn run_test<F, E>(seed: &[u8], closure: F) -> std::result::Result<(), E>
|
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
|
||||||
where
|
where
|
||||||
F: FnOnce(&std::path::Path) -> std::result::Result<(), E> + Send + 'static,
|
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
|
||||||
E: Send + 'static,
|
E: Send + 'static,
|
||||||
{
|
{
|
||||||
let rt = Builder::new_multi_thread()
|
let rt = Builder::new_multi_thread()
|
||||||
|
@ -76,7 +61,7 @@ where
|
||||||
));
|
));
|
||||||
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
|
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
|
||||||
let socket_path = socket_dir.path().join("keyforkd.sock");
|
let socket_path = socket_dir.path().join("keyforkd.sock");
|
||||||
let result = rt.block_on(async move {
|
rt.block_on(async move {
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
||||||
let server_handle = tokio::spawn({
|
let server_handle = tokio::spawn({
|
||||||
let socket_path = socket_path.clone();
|
let socket_path = socket_path.clone();
|
||||||
|
@ -89,30 +74,21 @@ where
|
||||||
.expect(bug!("couldn't send server start signal"));
|
.expect(bug!("couldn't send server start signal"));
|
||||||
let service = ServiceBuilder::new()
|
let service = ServiceBuilder::new()
|
||||||
.layer(middleware::BincodeLayer::new())
|
.layer(middleware::BincodeLayer::new())
|
||||||
.service(Keyforkd::new(seed.clone()));
|
.service(Keyforkd::new(seed.to_vec()));
|
||||||
server
|
server.run(service).await.expect(bug!("Unable to start service"));
|
||||||
.run(service)
|
|
||||||
.await
|
|
||||||
.expect(bug!("Unable to start service"));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rx.recv()
|
rx.recv()
|
||||||
.await
|
.await
|
||||||
.expect(bug!("can't receive server start signal from channel"));
|
.expect(bug!("can't receive server start signal from channel"));
|
||||||
std::env::set_var("KEYFORKD_SOCKET_PATH", &socket_path);
|
|
||||||
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
|
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
|
||||||
|
|
||||||
let result = test_handle.await;
|
let result = test_handle.await;
|
||||||
server_handle.abort();
|
server_handle.abort();
|
||||||
result
|
result
|
||||||
});
|
})
|
||||||
if let Err(e) = result {
|
.expect(bug!("runtime could not join all threads"))
|
||||||
if let Ok(reason) = e.try_into_panic() {
|
|
||||||
std::panic::resume_unwind(reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -122,6 +98,6 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_run_test() {
|
fn test_run_test() {
|
||||||
let seed = b"beefbeef";
|
let seed = b"beefbeef";
|
||||||
run_test(seed, |_path| Panicable::Ok(())).expect("infallible");
|
run_test(seed, |_path| Infallible::Ok(())).expect("infallible");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
[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"
|
|
|
@ -1,69 +0,0 @@
|
||||||
use std::{env, process::ExitCode, str::FromStr};
|
|
||||||
|
|
||||||
use keyfork_derive_path_data::paths;
|
|
||||||
use keyfork_derive_util::{DerivationPath, ExtendedPrivateKey, PathError};
|
|
||||||
use keyforkd_client::Client;
|
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
|
||||||
|
|
||||||
type XPrv = ExtendedPrivateKey<SigningKey>;
|
|
||||||
|
|
||||||
/// Any error that can occur while deriving a key.
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
/// The given path could not be parsed.
|
|
||||||
#[error("Could not parse the given path: {0}")]
|
|
||||||
PathFormat(#[from] PathError),
|
|
||||||
|
|
||||||
/// The request to derive data failed.
|
|
||||||
#[error("Unable to perform key derivation request: {0}")]
|
|
||||||
KeyforkdClient(#[from] keyforkd_client::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
fn validate(path: &str) -> Result<DerivationPath> {
|
|
||||||
let index = paths::AGE.inner().first().unwrap();
|
|
||||||
|
|
||||||
let path = DerivationPath::from_str(path)?;
|
|
||||||
assert!(
|
|
||||||
path.len() >= 2,
|
|
||||||
"Expected path of at least m/{index}/account_id'"
|
|
||||||
);
|
|
||||||
|
|
||||||
let given_index = path.iter().next().expect("checked .len() above");
|
|
||||||
assert_eq!(
|
|
||||||
index, given_index,
|
|
||||||
"Expected derivation path starting with m/{index}, got: {given_index}",
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut args = env::args();
|
|
||||||
let program_name = args.next().expect("program name");
|
|
||||||
let args = args.collect::<Vec<_>>();
|
|
||||||
let path = match args.as_slice() {
|
|
||||||
[path] => validate(path)?,
|
|
||||||
_ => panic!("Usage: {program_name} path"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut client = Client::discover_socket()?;
|
|
||||||
// TODO: should this key be clamped to Curve25519 specs?
|
|
||||||
let xprv: XPrv = client.request_xprv(&path)?;
|
|
||||||
let hrp = bech32::Hrp::parse("AGE-SECRET-KEY-")?;
|
|
||||||
let age_key = bech32::encode::<bech32::Bech32>(hrp, &xprv.private_key().to_bytes())?;
|
|
||||||
println!("{}", age_key.to_uppercase());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
|
||||||
if let Err(e) = run() {
|
|
||||||
eprintln!("Error: {e}");
|
|
||||||
ExitCode::FAILURE
|
|
||||||
} else {
|
|
||||||
ExitCode::SUCCESS
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-derive-key"
|
name = "keyfork-derive-key"
|
||||||
version = "0.1.2"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-derive-util = { workspace = true }
|
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", registry = "distrust" }
|
||||||
keyforkd-client = { workspace = true }
|
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", registry = "distrust" }
|
||||||
smex = { workspace = true }
|
smex = { version = "0.1.0", path = "../../util/smex", registry = "distrust" }
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.48"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! Query the Keyfork Server to generate a hex-encoded key for a given algorithm.
|
//!
|
||||||
|
|
||||||
use std::{env, process::ExitCode, str::FromStr};
|
use std::{env, process::ExitCode, str::FromStr};
|
||||||
|
|
||||||
|
@ -46,10 +46,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut client = Client::discover_socket()?;
|
let mut client = Client::discover_socket()?;
|
||||||
let request = DerivationRequest::new(algo, &path);
|
let request = DerivationRequest::new(algo, &path);
|
||||||
let response = client.request(&request.into())?;
|
let response = client.request(&request.into())?;
|
||||||
println!(
|
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
|
||||||
"{}",
|
|
||||||
smex::encode(DerivationResponse::try_from(response)?.data)
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-derive-openpgp"
|
name = "keyfork-derive-openpgp"
|
||||||
version = "0.1.5"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[features]
|
[features]
|
||||||
default = ["bin"]
|
default = ["bin"]
|
||||||
bin = ["sequoia-openpgp/crypto-nettle"]
|
bin = ["sequoia-openpgp/crypto-nettle"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
|
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"], registry = "distrust" }
|
||||||
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
|
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", default-features = false, features = ["ed25519"], registry = "distrust" }
|
||||||
ed25519-dalek = { workspace = true }
|
ed25519-dalek = "2.0.0"
|
||||||
sequoia-openpgp = { workspace = true }
|
sequoia-openpgp = { version = "1.17.0", default-features = false }
|
||||||
anyhow = { workspace = true }
|
anyhow = "1.0.75"
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.49"
|
||||||
keyfork-derive-path-data = { workspace = true }
|
|
||||||
|
|
|
@ -19,14 +19,8 @@ use sequoia_openpgp::{
|
||||||
Cert, Packet,
|
Cert, Packet,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
|
|
||||||
// something like Prv or PrvKey.
|
|
||||||
|
|
||||||
/// The private key type used with OpenPGP.
|
|
||||||
pub type XPrvKey = SigningKey;
|
pub type XPrvKey = SigningKey;
|
||||||
|
pub type XPrv = ExtendedPrivateKey<SigningKey>;
|
||||||
/// The extended private key type used with OpenPGP.
|
|
||||||
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
|
|
||||||
|
|
||||||
/// An error occurred while creating an OpenPGP key.
|
/// An error occurred while creating an OpenPGP key.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
@ -65,17 +59,13 @@ pub enum Error {
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
/// Create an OpenPGP Cert with private key data, with derived keys from the given derivation
|
/// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
|
||||||
/// response, keys, and User ID.
|
/// ID.
|
||||||
///
|
|
||||||
/// Certificates are created with a default expiration of one day, but may be configured to expire
|
|
||||||
/// later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as "15d" (15
|
|
||||||
/// days), "1m" (one month), or "2y" (two years).
|
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may error for any condition mentioned in [`Error`].
|
/// The function may error for any condition mentioned in [`Error`].
|
||||||
pub fn derive(xprv: &XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
let primary_key_flags = match keys.first() {
|
let primary_key_flags = match keys.get(0) {
|
||||||
Some(kf) if kf.for_certification() => kf,
|
Some(kf) if kf.for_certification() => kf,
|
||||||
_ => return Err(Error::NotCert),
|
_ => return Err(Error::NotCert),
|
||||||
};
|
};
|
||||||
|
@ -119,7 +109,7 @@ pub fn derive(xprv: &XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
|
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
|
||||||
let policy = sequoia_openpgp::policy::StandardPolicy::new();
|
let policy = sequoia_openpgp::policy::StandardPolicy::new();
|
||||||
|
|
||||||
// Set certificate expiration to configured expiration or (default) one day
|
// Set certificate expiration to one day
|
||||||
let mut keypair = primary_key.clone().into_keypair()?;
|
let mut keypair = primary_key.clone().into_keypair()?;
|
||||||
let signatures =
|
let signatures =
|
||||||
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;
|
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
//! Query the Keyfork Servre to derive an OpenPGP Secret Key.
|
//!
|
||||||
|
|
||||||
use std::{env, process::ExitCode, str::FromStr};
|
use std::{env, process::ExitCode, str::FromStr};
|
||||||
|
|
||||||
use keyfork_derive_path_data::paths;
|
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||||
use keyfork_derive_util::DerivationPath;
|
|
||||||
use keyforkd_client::Client;
|
use keyforkd_client::Client;
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
@ -79,17 +78,16 @@ fn validate(
|
||||||
subkey_format: &str,
|
subkey_format: &str,
|
||||||
default_userid: &str,
|
default_userid: &str,
|
||||||
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
|
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
|
||||||
let index = paths::OPENPGP.inner().first().unwrap();
|
let mut pgp_u32 = [0u8; 4];
|
||||||
|
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
|
||||||
|
let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||||
|
|
||||||
let path = DerivationPath::from_str(path)?;
|
let path = DerivationPath::from_str(path)?;
|
||||||
assert!(
|
assert_eq!(2, path.len(), "Expected path of m/{index}/account_id'");
|
||||||
path.len() >= 2,
|
|
||||||
"Expected path of at least m/{index}/account_id'"
|
|
||||||
);
|
|
||||||
|
|
||||||
let given_index = path.iter().next().expect("checked .len() above");
|
let given_index = path.iter().next().expect("checked .len() above");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
index, given_index,
|
&index, given_index,
|
||||||
"Expected derivation path starting with m/{index}, got: {given_index}",
|
"Expected derivation path starting with m/{index}, got: {given_index}",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -120,11 +118,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.map(|kt| kt.inner().clone())
|
.map(|kt| kt.inner().clone())
|
||||||
.collect::<Vec<_>>();
|
.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)?;
|
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
|
||||||
|
|
||||||
for packet in cert.as_tsk().into_packets() {
|
for packet in cert.into_packets() {
|
||||||
packet.serialize(&mut w)?;
|
packet.serialize(&mut w)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-derive-path-data"
|
name = "keyfork-derive-path-data"
|
||||||
version = "0.1.3"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-derive-util = { workspace = true, default-features = false }
|
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, registry = "distrust" }
|
||||||
once_cell = "1.19.0"
|
|
||||||
|
|
|
@ -2,129 +2,32 @@
|
||||||
|
|
||||||
#![allow(clippy::unreadable_literal)]
|
#![allow(clippy::unreadable_literal)]
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||||
|
|
||||||
/// All common paths for key derivation.
|
|
||||||
pub mod paths {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
/// The default derivation path for OpenPGP.
|
/// The default derivation path for OpenPGP.
|
||||||
pub static OPENPGP: LazyLock<DerivationPath> = LazyLock::new(|| {
|
pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true);
|
||||||
DerivationPath::default().chain_push(DerivationIndex::new_unchecked(
|
|
||||||
u32::from_be_bytes(*b"\x00pgp"),
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The derivation path for OpenPGP certificates used for sharding.
|
|
||||||
pub static OPENPGP_SHARD: LazyLock<DerivationPath> = LazyLock::new(|| {
|
|
||||||
DerivationPath::default()
|
|
||||||
.chain_push(DerivationIndex::new_unchecked(
|
|
||||||
u32::from_be_bytes(*b"\x00pgp"),
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
.chain_push(DerivationIndex::new_unchecked(
|
|
||||||
u32::from_be_bytes(*b"shrd"),
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The derivation path for OpenPGP certificates used for disaster recovery.
|
|
||||||
pub static OPENPGP_DISASTER_RECOVERY: LazyLock<DerivationPath> = LazyLock::new(|| {
|
|
||||||
DerivationPath::default()
|
|
||||||
.chain_push(DerivationIndex::new_unchecked(
|
|
||||||
u32::from_be_bytes(*b"\x00pgp"),
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
.chain_push(DerivationIndex::new_unchecked(
|
|
||||||
u32::from_be_bytes(*b"\x00\x00dr"),
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine if a prefix matches and whether the next index exists.
|
|
||||||
fn prefix_matches(given: &DerivationPath, target: &DerivationPath) -> Option<DerivationIndex> {
|
|
||||||
if given.len() <= target.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if target
|
|
||||||
.iter()
|
|
||||||
.zip(given.iter())
|
|
||||||
.all(|(left, right)| left == right)
|
|
||||||
{
|
|
||||||
given.iter().nth(target.len()).cloned()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A derivation target.
|
/// A derivation target.
|
||||||
#[derive(Debug)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Target {
|
pub enum Target {
|
||||||
/// An OpenPGP key, whose account is the given index.
|
/// An OpenPGP key, whose account is the given index.
|
||||||
OpenPGP(DerivationIndex),
|
OpenPGP(DerivationIndex),
|
||||||
|
|
||||||
/// An OpenPGP key used for sharding.
|
|
||||||
OpenPGPShard(DerivationIndex),
|
|
||||||
|
|
||||||
/// An OpenPGP key used for disaster recovery.
|
|
||||||
OpenPGPDisasterRecovery(DerivationIndex),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Target {
|
impl std::fmt::Display for Target {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Target::OpenPGP(account) => {
|
Self::OpenPGP(account) => {
|
||||||
write!(f, "OpenPGP key (account {account})")
|
write!(f, "OpenPGP key (account {account})")
|
||||||
}
|
}
|
||||||
Target::OpenPGPShard(shard_index) => {
|
|
||||||
write!(f, "OpenPGP Shard key (shard index {shard_index})")
|
|
||||||
}
|
|
||||||
Target::OpenPGPDisasterRecovery(account) => {
|
|
||||||
write!(f, "OpenPGP Disaster Recovery key (account {account})")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! test_match {
|
|
||||||
($var:ident, $shard:path, $target:path) => {
|
|
||||||
if let Some(index) = prefix_matches($var, &$shard) {
|
|
||||||
return Some($target(index));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine the closest [`Target`] for the given path. This method is intended to be used by
|
/// Determine the closest [`Target`] for the given path. This method is intended to be used by
|
||||||
/// `keyforkd` to provide an optional textual prompt to what a client is attempting to derive.
|
/// `keyforkd` to provide an optional textual prompt to what a client is attempting to derive.
|
||||||
pub fn guess_target(path: &DerivationPath) -> Option<Target> {
|
pub fn guess_target(path: &DerivationPath) -> Option<Target> {
|
||||||
test_match!(path, paths::OPENPGP_SHARD, Target::OpenPGPShard);
|
Some(match path.iter().collect::<Vec<_>>()[..] {
|
||||||
test_match!(
|
[t, index] if t == &OPENPGP => Target::OpenPGP(index.clone()),
|
||||||
path,
|
_ => return None,
|
||||||
paths::OPENPGP_DISASTER_RECOVERY,
|
})
|
||||||
Target::OpenPGPDisasterRecovery
|
|
||||||
);
|
|
||||||
test_match!(path, paths::OPENPGP, Target::OpenPGP);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let index = DerivationIndex::new(5312, false).unwrap();
|
|
||||||
let dr_key = paths::OPENPGP_DISASTER_RECOVERY
|
|
||||||
.clone()
|
|
||||||
.chain_push(index.clone());
|
|
||||||
match guess_target(&dr_key) {
|
|
||||||
Some(Target::OpenPGPDisasterRecovery(idx)) if idx == index => (),
|
|
||||||
bad => panic!("invalid value: {bad:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-derive-util"
|
name = "keyfork-derive-util"
|
||||||
version = "0.2.2"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -15,25 +12,25 @@ secp256k1 = ["k256"]
|
||||||
ed25519 = ["ed25519-dalek"]
|
ed25519 = ["ed25519-dalek"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-mnemonic = { workspace = true }
|
keyfork-mnemonic-util = { version = "0.2.0", path = "../../util/keyfork-mnemonic-util", registry = "distrust" }
|
||||||
keyfork-bug = { workspace = true }
|
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
|
||||||
|
|
||||||
# Included in Rust
|
# Included in Rust
|
||||||
digest = "0.10.7"
|
digest = "0.10.7"
|
||||||
sha2 = { workspace = true }
|
sha2 = "0.10.7"
|
||||||
|
|
||||||
# Rust-Crypto ecosystem, not personally audited
|
# Rust-Crypto ecosystem, not personally audited
|
||||||
ripemd = "0.1.3"
|
ripemd = "0.1.3"
|
||||||
hmac = { workspace = true, features = ["std"] }
|
hmac = { version = "0.12.1", features = ["std"] }
|
||||||
|
|
||||||
# Personally audited
|
# Personally audited
|
||||||
serde = { workspace = true }
|
serde = { version = "1.0.186", features = ["derive"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.47"
|
||||||
|
|
||||||
# Optional, not personally audited
|
# Optional, not personally audited
|
||||||
k256 = { workspace = true, default-features = false, features = ["std", "arithmetic"], optional = true }
|
k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true }
|
||||||
ed25519-dalek = { workspace = true, optional = true }
|
ed25519-dalek = { version = "2.0.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = { workspace = true }
|
hex-literal = "0.4.1"
|
||||||
keyfork-slip10-test-data = { workspace = true }
|
keyfork-slip10-test-data = { version = "0.1.0", path = "../../util/keyfork-slip10-test-data", registry = "distrust" }
|
||||||
|
|
|
@ -23,7 +23,7 @@ performed directly on a master seed. This is how Keyforkd works internally.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use keyfork_mnemonic::Mnemonic;
|
use keyfork_mnemonic_util::Mnemonic;
|
||||||
use keyfork_derive_util::{*, request::*};
|
use keyfork_derive_util::{*, request::*};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
//! # Examples
|
//! # Examples
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use std::str::FromStr;
|
//! use std::str::FromStr;
|
||||||
//! use keyfork_mnemonic::Mnemonic;
|
//! use keyfork_mnemonic_util::Mnemonic;
|
||||||
//! use keyfork_derive_util::{*, request::*};
|
//! use keyfork_derive_util::{*, request::*};
|
||||||
//! use k256::SecretKey;
|
//! use k256::SecretKey;
|
||||||
//!
|
//!
|
||||||
|
@ -41,9 +41,9 @@
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
///
|
||||||
pub mod private_key;
|
pub mod private_key;
|
||||||
#[allow(missing_docs)]
|
///
|
||||||
pub mod public_key;
|
pub mod public_key;
|
||||||
|
|
||||||
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};
|
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};
|
||||||
|
|
|
@ -27,10 +27,6 @@ pub enum Error {
|
||||||
/// The given slice was of an inappropriate size to create a Private Key.
|
/// The given slice was of an inappropriate size to create a Private Key.
|
||||||
#[error("The given slice was of an inappropriate size to create a Private Key")]
|
#[error("The given slice was of an inappropriate size to create a Private Key")]
|
||||||
InvalidSliceError(#[from] std::array::TryFromSliceError),
|
InvalidSliceError(#[from] std::array::TryFromSliceError),
|
||||||
|
|
||||||
/// The given data was not a valid key for the chosen key type.
|
|
||||||
#[error("The given data was not a valid key for the chosen key type")]
|
|
||||||
InvalidKey,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result<T, E = Error> = std::result::Result<T, E>;
|
type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
@ -52,7 +48,7 @@ pub struct VariableLengthSeed<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> VariableLengthSeed<'a> {
|
impl<'a> VariableLengthSeed<'a> {
|
||||||
/// Create a new `VariableLengthSeed`.
|
/// Create a new VariableLengthSeed.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
@ -128,10 +124,10 @@ mod serde_with {
|
||||||
K: PrivateKey + Clone,
|
K: PrivateKey + Clone,
|
||||||
{
|
{
|
||||||
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
|
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
|
||||||
let bytes: [u8; 32] = variable_len_bytes.try_into().expect(bug!(
|
let bytes: [u8; 32] = variable_len_bytes
|
||||||
"unable to parse serialized private key; no support for static len"
|
.try_into()
|
||||||
));
|
.expect(bug!("unable to parse serialized private key; no support for static len"));
|
||||||
Ok(K::from_bytes(&bytes).expect(bug!("could not deserialize key with invalid scalar")))
|
Ok(K::from_bytes(&bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,9 +148,13 @@ where
|
||||||
/// Generate a new [`ExtendedPrivateKey`] from a seed, ideally from a 12-word or 24-word
|
/// Generate a new [`ExtendedPrivateKey`] from a seed, ideally from a 12-word or 24-word
|
||||||
/// mnemonic, but may take 16-byte seeds.
|
/// mnemonic, but may take 16-byte seeds.
|
||||||
///
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may return an error if the derived master key could not be parsed as a key for
|
/// An error may be returned if:
|
||||||
/// the given algorithm (such as exceeding values on the secp256k1 curve).
|
/// * The given seed had an incorrect length.
|
||||||
|
/// * A `HmacSha512` can't be constructed.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
@ -167,12 +167,11 @@ where
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Self {
|
||||||
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Result<Self> {
|
|
||||||
Self::new_internal(seed.as_private_key())
|
Self::new_internal(seed.as_private_key())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_internal(seed: &[u8]) -> Result<Self> {
|
fn new_internal(seed: &[u8]) -> Self {
|
||||||
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
|
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
|
||||||
.expect(bug!("HmacSha512 InvalidLength should be infallible"))
|
.expect(bug!("HmacSha512 InvalidLength should be infallible"))
|
||||||
.chain_update(seed)
|
.chain_update(seed)
|
||||||
|
@ -180,30 +179,13 @@ where
|
||||||
.into_bytes();
|
.into_bytes();
|
||||||
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
|
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
|
||||||
|
|
||||||
// Verify the master key is nonzero, hopefully avoiding side-channel attacks.
|
Self::new_from_parts(
|
||||||
let mut has_any_nonzero = false;
|
|
||||||
// deoptimize arithmetic smartness
|
|
||||||
for byte in private_key.iter().map(std::hint::black_box) {
|
|
||||||
if *byte != 0 {
|
|
||||||
// deoptimize break
|
|
||||||
has_any_nonzero = std::hint::black_box(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
has_any_nonzero,
|
|
||||||
bug!("hmac function returned all-zero master key")
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::from_parts(
|
|
||||||
private_key
|
private_key
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
|
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
|
||||||
0,
|
0,
|
||||||
// Checked: chain_code is always the same length, hash is static size
|
// Checked: chain_code is always the same length, hash is static size
|
||||||
chain_code
|
chain_code.try_into().expect(bug!("Invalid chain code length")),
|
||||||
.try_into()
|
|
||||||
.expect(bug!("Invalid chain code length")),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,16 +205,13 @@ where
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
|
pub fn new_from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self {
|
||||||
match K::from_bytes(key) {
|
Self {
|
||||||
Ok(key) => Ok(Self {
|
private_key: K::from_bytes(key),
|
||||||
private_key: key,
|
|
||||||
depth,
|
depth,
|
||||||
chain_code,
|
chain_code,
|
||||||
}),
|
|
||||||
Err(_) => Err(Error::InvalidKey),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,15 +225,12 @@ where
|
||||||
/// # public_key::TestPublicKey as PublicKey,
|
/// # public_key::TestPublicKey as PublicKey,
|
||||||
/// # private_key::TestPrivateKey as PrivateKey,
|
/// # private_key::TestPrivateKey as PrivateKey,
|
||||||
/// # };
|
/// # };
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let key: &[u8; 32] = //
|
/// let key: &[u8; 32] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
|
||||||
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key)?);
|
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key));
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn private_key(&self) -> &K {
|
pub fn private_key(&self) -> &K {
|
||||||
&self.private_key
|
&self.private_key
|
||||||
|
@ -279,14 +255,14 @@ where
|
||||||
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
|
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
|
||||||
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
|
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
|
||||||
/// # ];
|
/// # ];
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
|
||||||
/// let xpub = xprv.extended_public_key();
|
/// let xpub = xprv.extended_public_key();
|
||||||
/// assert_eq!(known_key, xpub.public_key().to_bytes());
|
/// assert_eq!(known_key, xpub.public_key().to_bytes());
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
|
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
|
||||||
ExtendedPublicKey::from_parts(self.public_key(), self.depth, self.chain_code)
|
ExtendedPublicKey::new_from_parts(self.public_key(), self.depth, self.chain_code)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a public key for the current [`PrivateKey`].
|
/// Return a public key for the current [`PrivateKey`].
|
||||||
|
@ -303,7 +279,7 @@ where
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let seed: &[u8; 64] = //
|
/// let seed: &[u8; 64] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
|
||||||
/// let pubkey = xprv.public_key();
|
/// let pubkey = xprv.public_key();
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
|
@ -321,15 +297,12 @@ where
|
||||||
/// # public_key::TestPublicKey as PublicKey,
|
/// # public_key::TestPublicKey as PublicKey,
|
||||||
/// # private_key::TestPrivateKey as PrivateKey,
|
/// # private_key::TestPrivateKey as PrivateKey,
|
||||||
/// # };
|
/// # };
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let key: &[u8; 32] = //
|
/// let key: &[u8; 32] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
|
||||||
/// assert_eq!(xprv.depth(), 4);
|
/// assert_eq!(xprv.depth(), 4);
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn depth(&self) -> u8 {
|
pub fn depth(&self) -> u8 {
|
||||||
self.depth
|
self.depth
|
||||||
|
@ -344,15 +317,12 @@ where
|
||||||
/// # public_key::TestPublicKey as PublicKey,
|
/// # public_key::TestPublicKey as PublicKey,
|
||||||
/// # private_key::TestPrivateKey as PrivateKey,
|
/// # private_key::TestPrivateKey as PrivateKey,
|
||||||
/// # };
|
/// # };
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let key: &[u8; 32] = //
|
/// let key: &[u8; 32] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
|
||||||
/// assert_eq!(chain_code, &xprv.chain_code());
|
/// assert_eq!(chain_code, &xprv.chain_code());
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn chain_code(&self) -> [u8; 32] {
|
pub fn chain_code(&self) -> [u8; 32] {
|
||||||
self.chain_code
|
self.chain_code
|
||||||
|
@ -374,7 +344,7 @@ where
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let seed: &[u8; 64] = //
|
/// let seed: &[u8; 64] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
|
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
|
||||||
/// let path = DerivationPath::default()
|
/// let path = DerivationPath::default()
|
||||||
/// .chain_push(DerivationIndex::new(44, true)?)
|
/// .chain_push(DerivationIndex::new(44, true)?)
|
||||||
/// .chain_push(DerivationIndex::new(0, true)?)
|
/// .chain_push(DerivationIndex::new(0, true)?)
|
||||||
|
@ -420,7 +390,7 @@ where
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let seed: &[u8; 64] = //
|
/// let seed: &[u8; 64] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
|
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
|
||||||
/// let bip44_wallet = DerivationPath::default()
|
/// let bip44_wallet = DerivationPath::default()
|
||||||
/// .chain_push(DerivationIndex::new(44, true)?)
|
/// .chain_push(DerivationIndex::new(44, true)?)
|
||||||
/// .chain_push(DerivationIndex::new(0, true)?)
|
/// .chain_push(DerivationIndex::new(0, true)?)
|
||||||
|
|
|
@ -11,8 +11,8 @@ const KEY_SIZE: usize = 256;
|
||||||
/// Errors associated with creating or deriving Extended Public Keys.
|
/// Errors associated with creating or deriving Extended Public Keys.
|
||||||
#[derive(Error, Clone, Debug)]
|
#[derive(Error, Clone, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// BIP-0032 does not support hardened public key derivation from parent public keys.
|
/// BIP-0032 does not support deriving public keys from hardened private keys.
|
||||||
#[error("Hardened child public keys may not be derived from parent public keys")]
|
#[error("Public keys may not be derived when hardened")]
|
||||||
HardenedIndex,
|
HardenedIndex,
|
||||||
|
|
||||||
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
|
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
|
||||||
|
@ -60,11 +60,11 @@ where
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let pubkey = PublicKey::from_bytes(key);
|
/// let pubkey = PublicKey::from_bytes(key);
|
||||||
/// let xpub = ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
/// let xpub = ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
|
pub fn new_from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
|
||||||
Self {
|
Self {
|
||||||
public_key,
|
public_key,
|
||||||
depth,
|
depth,
|
||||||
|
@ -86,7 +86,7 @@ where
|
||||||
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// # let pubkey = PublicKey::from_bytes(key);
|
/// # let pubkey = PublicKey::from_bytes(key);
|
||||||
/// let xpub = //
|
/// let xpub = //
|
||||||
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
|
||||||
/// let pubkey = xpub.public_key();
|
/// let pubkey = xpub.public_key();
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
|
@ -121,7 +121,7 @@ where
|
||||||
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// # let pubkey = PublicKey::from_bytes(key);
|
/// # let pubkey = PublicKey::from_bytes(key);
|
||||||
/// let xpub = //
|
/// let xpub = //
|
||||||
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
|
||||||
/// let index = DerivationIndex::new(0, false)?;
|
/// let index = DerivationIndex::new(0, false)?;
|
||||||
/// let child = xpub.derive_child(&index)?;
|
/// let child = xpub.derive_child(&index)?;
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
|
|
|
@ -117,7 +117,7 @@ mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "IndexTooLarge")]
|
#[should_panic]
|
||||||
fn fails_on_high_index() {
|
fn fails_on_high_index() {
|
||||||
DerivationIndex::new(0x8000_0001, false).unwrap();
|
DerivationIndex::new(0x8000_0001, false).unwrap();
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "IndexTooLarge")]
|
#[should_panic]
|
||||||
fn from_str_fails_on_high_index() {
|
fn from_str_fails_on_high_index() {
|
||||||
DerivationIndex::from_str(&0x8000_0001u32.to_string()).unwrap();
|
DerivationIndex::from_str(&0x8000_0001u32.to_string()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#![allow(clippy::module_name_repetitions)]
|
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
pub mod extended_key;
|
pub mod extended_key;
|
||||||
|
@ -17,10 +17,7 @@ pub mod public_key;
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use crate::extended_key::{
|
pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}};
|
||||||
private_key::{Error as XPrvError, ExtendedPrivateKey, VariableLengthSeed},
|
|
||||||
public_key::{Error as XPubError, ExtendedPublicKey},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
index::{DerivationIndex, Error as IndexError},
|
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:
|
/// 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
|
/// The prefix for the path must be `m`, and all indices must be integers between 0 and
|
||||||
/// 2^31.
|
/// 2^31.
|
||||||
|
@ -35,8 +35,8 @@ impl DerivationPath {
|
||||||
self.path.iter()
|
self.path.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The amount of segments in the [`DerivationPath`]. For consistency, a [`usize`] is returned,
|
/// The amount of segments in the DerivationPath. For consistency, a [`usize`] is returned, but
|
||||||
/// but BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
|
/// BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.path.len()
|
self.path.len()
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "UnknownPathPrefix")]
|
#[should_panic]
|
||||||
fn requires_master_path() {
|
fn requires_master_path() {
|
||||||
DerivationPath::from_str("1234/5678'").unwrap();
|
DerivationPath::from_str("1234/5678'").unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ use crate::PublicKey;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use keyfork_bug::bug;
|
||||||
|
|
||||||
pub(crate) type PrivateKeyBytes = [u8; 32];
|
pub(crate) type PrivateKeyBytes = [u8; 32];
|
||||||
|
|
||||||
/// Functions required to use an `ExtendedPrivateKey`.
|
/// Functions required to use an `ExtendedPrivateKey`.
|
||||||
|
@ -24,7 +26,7 @@ pub trait PrivateKey: Sized {
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let private_key = OurPrivateKey::from_bytes(key_data);
|
/// let private_key = OurPrivateKey::from_bytes(key_data);
|
||||||
/// ```
|
/// ```
|
||||||
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err>;
|
fn from_bytes(b: &PrivateKeyBytes) -> Self;
|
||||||
|
|
||||||
/// Convert a &Self to bytes.
|
/// Convert a &Self to bytes.
|
||||||
///
|
///
|
||||||
|
@ -36,7 +38,7 @@ pub trait PrivateKey: Sized {
|
||||||
/// # };
|
/// # };
|
||||||
/// let key_data: &[u8; 32] = //
|
/// let key_data: &[u8; 32] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
|
/// let private_key = OurPrivateKey::from_bytes(key_data);
|
||||||
/// assert_eq!(key_data, &private_key.to_bytes());
|
/// assert_eq!(key_data, &private_key.to_bytes());
|
||||||
/// ```
|
/// ```
|
||||||
fn to_bytes(&self) -> PrivateKeyBytes;
|
fn to_bytes(&self) -> PrivateKeyBytes;
|
||||||
|
@ -71,7 +73,7 @@ pub trait PrivateKey: Sized {
|
||||||
/// # };
|
/// # };
|
||||||
/// let key_data: &[u8; 32] = //
|
/// let key_data: &[u8; 32] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
|
/// let private_key = OurPrivateKey::from_bytes(key_data);
|
||||||
/// let public_key = private_key.public_key();
|
/// let public_key = private_key.public_key();
|
||||||
/// ```
|
/// ```
|
||||||
fn public_key(&self) -> Self::PublicKey;
|
fn public_key(&self) -> Self::PublicKey;
|
||||||
|
@ -83,7 +85,7 @@ pub trait PrivateKey: Sized {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// An error may be returned if:
|
/// An error may be returned if:
|
||||||
/// * An all-zero `other` is provided.
|
/// * A nonzero `other` is provided.
|
||||||
/// * An error specific to the given algorithm was encountered.
|
/// * An error specific to the given algorithm was encountered.
|
||||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
|
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||||
|
|
||||||
|
@ -100,10 +102,6 @@ pub enum PrivateKeyError {
|
||||||
/// For the given algorithm, the private key must be nonzero.
|
/// For the given algorithm, the private key must be nonzero.
|
||||||
#[error("The provided private key must be nonzero, but is not")]
|
#[error("The provided private key must be nonzero, but is not")]
|
||||||
NonZero,
|
NonZero,
|
||||||
|
|
||||||
/// A scalar could not be constructed for the given algorithm.
|
|
||||||
#[error("A scalar could not be constructed for the given algorithm")]
|
|
||||||
InvalidScalar,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
|
@ -118,8 +116,8 @@ impl PrivateKey for k256::SecretKey {
|
||||||
"Bitcoin seed"
|
"Bitcoin seed"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
fn from_bytes(b: &PrivateKeyBytes) -> Self {
|
||||||
Self::from_slice(b).map_err(|_| PrivateKeyError::InvalidScalar)
|
Self::from_slice(b).expect(bug!("Invalid private key bytes"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_bytes(&self) -> PrivateKeyBytes {
|
fn to_bytes(&self) -> PrivateKeyBytes {
|
||||||
|
@ -132,19 +130,20 @@ impl PrivateKey for k256::SecretKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||||
use k256::elliptic_curve::ScalarPrimitive;
|
if other.iter().all(|n| n == &0) {
|
||||||
use k256::{Scalar, Secp256k1};
|
return Err(PrivateKeyError::NonZero);
|
||||||
|
}
|
||||||
// Construct a scalar from bytes
|
let other = *other;
|
||||||
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(other.into());
|
// Checked: See above nonzero check
|
||||||
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
|
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
|
||||||
let scalar = scalar.ok_or(PrivateKeyError::InvalidScalar)?;
|
.expect(bug!("Should have been able to get a NonZeroScalar"));
|
||||||
let scalar = Scalar::from(scalar);
|
|
||||||
|
|
||||||
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
|
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
|
||||||
let nonzero_scalar = Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
|
Ok(
|
||||||
.ok_or(PrivateKeyError::NonZero)?;
|
Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
|
||||||
Ok(Self::from(nonzero_scalar))
|
.map(Into::into)
|
||||||
|
.expect(bug!("Should be able to make Key")),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,8 +156,8 @@ impl PrivateKey for ed25519_dalek::SigningKey {
|
||||||
"ed25519 seed"
|
"ed25519 seed"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
fn from_bytes(b: &PrivateKeyBytes) -> Self {
|
||||||
Ok(Self::from_bytes(b))
|
Self::from_bytes(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_bytes(&self) -> PrivateKeyBytes {
|
fn to_bytes(&self) -> PrivateKeyBytes {
|
||||||
|
@ -181,8 +180,7 @@ impl PrivateKey for ed25519_dalek::SigningKey {
|
||||||
|
|
||||||
use crate::public_key::TestPublicKey;
|
use crate::public_key::TestPublicKey;
|
||||||
|
|
||||||
/// A private key that can be used for testing purposes. Does not utilize any significant
|
#[doc(hidden)]
|
||||||
/// cryptographic operations.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct TestPrivateKey {
|
pub struct TestPrivateKey {
|
||||||
key: [u8; 32],
|
key: [u8; 32],
|
||||||
|
@ -202,8 +200,10 @@ impl PrivateKey for TestPrivateKey {
|
||||||
type PublicKey = TestPublicKey;
|
type PublicKey = TestPublicKey;
|
||||||
type Err = PrivateKeyError;
|
type Err = PrivateKeyError;
|
||||||
|
|
||||||
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
fn from_bytes(b: &PrivateKeyBytes) -> Self {
|
||||||
Ok(Self { key: *b })
|
Self {
|
||||||
|
key: *b
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_bytes(&self) -> PrivateKeyBytes {
|
fn to_bytes(&self) -> PrivateKeyBytes {
|
||||||
|
|
|
@ -30,7 +30,7 @@ pub trait PublicKey: Sized {
|
||||||
/// # };
|
/// # };
|
||||||
/// let key_data: &[u8; 32] = //
|
/// let key_data: &[u8; 32] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
|
/// let private_key = OurPrivateKey::from_bytes(key_data);
|
||||||
/// let public_key_bytes = private_key.public_key().to_bytes();
|
/// let public_key_bytes = private_key.public_key().to_bytes();
|
||||||
/// ```
|
/// ```
|
||||||
fn to_bytes(&self) -> PublicKeyBytes;
|
fn to_bytes(&self) -> PublicKeyBytes;
|
||||||
|
@ -42,7 +42,7 @@ pub trait PublicKey: Sized {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// An error may be returned if:
|
/// An error may be returned if:
|
||||||
/// * An all-zero `other` is provided.
|
/// * A nonzero `other` is provided.
|
||||||
/// * An error specific to the given algorithm was encountered.
|
/// * An error specific to the given algorithm was encountered.
|
||||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
|
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ pub trait PublicKey: Sized {
|
||||||
/// # };
|
/// # };
|
||||||
/// let key_data: &[u8; 32] = //
|
/// let key_data: &[u8; 32] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
|
/// let private_key = OurPrivateKey::from_bytes(key_data);
|
||||||
/// let fingerprint = private_key.public_key().fingerprint();
|
/// let fingerprint = private_key.public_key().fingerprint();
|
||||||
/// ```
|
/// ```
|
||||||
fn fingerprint(&self) -> [u8; 4] {
|
fn fingerprint(&self) -> [u8; 4] {
|
||||||
|
@ -77,10 +77,6 @@ pub enum PublicKeyError {
|
||||||
#[error("The provided public key must be nonzero, but is not")]
|
#[error("The provided public key must be nonzero, but is not")]
|
||||||
NonZero,
|
NonZero,
|
||||||
|
|
||||||
/// A scalar could not be constructed for the given algorithm.
|
|
||||||
#[error("A scalar could not be constructed for the given algorithm")]
|
|
||||||
InvalidScalar,
|
|
||||||
|
|
||||||
/// Public key derivation is unsupported for this algorithm.
|
/// Public key derivation is unsupported for this algorithm.
|
||||||
#[error("Public key derivation is unsupported for this algorithm")]
|
#[error("Public key derivation is unsupported for this algorithm")]
|
||||||
DerivationUnsupported,
|
DerivationUnsupported,
|
||||||
|
@ -89,7 +85,7 @@ pub enum PublicKeyError {
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
use k256::{
|
use k256::{
|
||||||
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
|
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
|
||||||
AffinePoint,
|
AffinePoint, NonZeroScalar,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
|
@ -109,16 +105,14 @@ impl PublicKey for k256::PublicKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
|
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||||
use k256::elliptic_curve::ScalarPrimitive;
|
if other.iter().all(|n| n == &0) {
|
||||||
use k256::{Scalar, Secp256k1};
|
return Err(PublicKeyError::NonZero);
|
||||||
|
}
|
||||||
|
// Checked: See above
|
||||||
|
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
|
||||||
|
.expect(bug!("Should have been able to get a NonZeroScalar"));
|
||||||
|
|
||||||
// Construct a scalar from bytes
|
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
|
||||||
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into());
|
|
||||||
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
|
|
||||||
let scalar = scalar.ok_or(PublicKeyError::InvalidScalar)?;
|
|
||||||
let scalar = Scalar::from(scalar);
|
|
||||||
|
|
||||||
let point = self.to_projective() + (AffinePoint::generator() * scalar);
|
|
||||||
Ok(Self::from_affine(point.into())
|
Ok(Self::from_affine(point.into())
|
||||||
.expect(bug!("Could not from_affine after scalar arithmetic")))
|
.expect(bug!("Could not from_affine after scalar arithmetic")))
|
||||||
}
|
}
|
||||||
|
@ -148,15 +142,14 @@ impl PublicKey for VerifyingKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A public key that can be used for testing purposes. Does not utilize any significant
|
#[doc(hidden)]
|
||||||
/// cryptographic operations.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TestPublicKey {
|
pub struct TestPublicKey {
|
||||||
pub(crate) key: [u8; 33],
|
pub(crate) key: [u8; 33],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestPublicKey {
|
impl TestPublicKey {
|
||||||
/// Create a new [`TestPublicKey`] from the given bytes.
|
#[doc(hidden)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn from_bytes(b: &[u8]) -> Self {
|
pub fn from_bytes(b: &[u8]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
@ -24,7 +24,7 @@ use crate::{
|
||||||
DerivationPath, ExtendedPrivateKey,
|
DerivationPath, ExtendedPrivateKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_mnemonic::{Mnemonic, MnemonicGenerationError};
|
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// An error encountered while deriving a key.
|
/// An error encountered while deriving a key.
|
||||||
|
@ -57,7 +57,7 @@ pub enum DerivationAlgorithm {
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
Secp256k1,
|
Secp256k1,
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
TestAlgorithm,
|
Internal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerivationAlgorithm {
|
impl DerivationAlgorithm {
|
||||||
|
@ -70,7 +70,7 @@ impl DerivationAlgorithm {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "ed25519")]
|
#[cfg(feature = "ed25519")]
|
||||||
Self::Ed25519 => {
|
Self::Ed25519 => {
|
||||||
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed)?;
|
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed);
|
||||||
let derived_key = key.derive_path(path)?;
|
let derived_key = key.derive_path(path)?;
|
||||||
Ok(DerivationResponse::with_algo_and_xprv(
|
Ok(DerivationResponse::with_algo_and_xprv(
|
||||||
self.clone(),
|
self.clone(),
|
||||||
|
@ -79,15 +79,15 @@ impl DerivationAlgorithm {
|
||||||
}
|
}
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
Self::Secp256k1 => {
|
Self::Secp256k1 => {
|
||||||
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed)?;
|
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed);
|
||||||
let derived_key = key.derive_path(path)?;
|
let derived_key = key.derive_path(path)?;
|
||||||
Ok(DerivationResponse::with_algo_and_xprv(
|
Ok(DerivationResponse::with_algo_and_xprv(
|
||||||
self.clone(),
|
self.clone(),
|
||||||
&derived_key,
|
&derived_key,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Self::TestAlgorithm => {
|
Self::Internal => {
|
||||||
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed)?;
|
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed);
|
||||||
let derived_key = key.derive_path(path)?;
|
let derived_key = key.derive_path(path)?;
|
||||||
Ok(DerivationResponse::with_algo_and_xprv(
|
Ok(DerivationResponse::with_algo_and_xprv(
|
||||||
self.clone(),
|
self.clone(),
|
||||||
|
@ -120,7 +120,7 @@ pub trait AsAlgorithm: PrivateKey {
|
||||||
|
|
||||||
impl AsAlgorithm for TestPrivateKey {
|
impl AsAlgorithm for TestPrivateKey {
|
||||||
fn as_algorithm() -> DerivationAlgorithm {
|
fn as_algorithm() -> DerivationAlgorithm {
|
||||||
DerivationAlgorithm::TestAlgorithm
|
DerivationAlgorithm::Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ impl DerivationRequest {
|
||||||
/// # };
|
/// # };
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let algo: DerivationAlgorithm = //
|
/// let algo: DerivationAlgorithm = //
|
||||||
/// # DerivationAlgorithm::TestAlgorithm;
|
/// # DerivationAlgorithm::Internal;
|
||||||
/// let path: DerivationPath = //
|
/// let path: DerivationPath = //
|
||||||
/// # DerivationPath::default();
|
/// # DerivationPath::default();
|
||||||
/// let request = DerivationRequest::new(algo, &path);
|
/// let request = DerivationRequest::new(algo, &path);
|
||||||
|
@ -169,7 +169,7 @@ impl DerivationRequest {
|
||||||
/// # };
|
/// # };
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let algo: DerivationAlgorithm = //
|
/// let algo: DerivationAlgorithm = //
|
||||||
/// # DerivationAlgorithm::TestAlgorithm;
|
/// # DerivationAlgorithm::Internal;
|
||||||
/// let path: DerivationPath = //
|
/// let path: DerivationPath = //
|
||||||
/// # DerivationPath::default();
|
/// # DerivationPath::default();
|
||||||
/// let request = DerivationRequest::new(algo, &path);
|
/// let request = DerivationRequest::new(algo, &path);
|
||||||
|
@ -194,12 +194,12 @@ impl DerivationRequest {
|
||||||
/// # private_key::TestPrivateKey as PrivateKey,
|
/// # private_key::TestPrivateKey as PrivateKey,
|
||||||
/// # };
|
/// # };
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mnemonic: keyfork_mnemonic::Mnemonic = //
|
/// let mnemonic: keyfork_mnemonic_util::Mnemonic = //
|
||||||
/// # keyfork_mnemonic::Mnemonic::from_entropy(
|
/// # keyfork_mnemonic_util::Mnemonic::from_entropy(
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
/// # )?;
|
/// # )?;
|
||||||
/// let algo: DerivationAlgorithm = //
|
/// let algo: DerivationAlgorithm = //
|
||||||
/// # DerivationAlgorithm::TestAlgorithm;
|
/// # DerivationAlgorithm::Internal;
|
||||||
/// let path: DerivationPath = //
|
/// let path: DerivationPath = //
|
||||||
/// # DerivationPath::default();
|
/// # DerivationPath::default();
|
||||||
/// let request = DerivationRequest::new(algo, &path);
|
/// let request = DerivationRequest::new(algo, &path);
|
||||||
|
@ -228,7 +228,7 @@ impl DerivationRequest {
|
||||||
/// let seed: &[u8; 64] = //
|
/// let seed: &[u8; 64] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let algo: DerivationAlgorithm = //
|
/// let algo: DerivationAlgorithm = //
|
||||||
/// # DerivationAlgorithm::TestAlgorithm;
|
/// # DerivationAlgorithm::Internal;
|
||||||
/// let path: DerivationPath = //
|
/// let path: DerivationPath = //
|
||||||
/// # DerivationPath::default();
|
/// # DerivationPath::default();
|
||||||
/// let request = DerivationRequest::new(algo, &path);
|
/// let request = DerivationRequest::new(algo, &path);
|
||||||
|
@ -300,9 +300,11 @@ mod secp256k1 {
|
||||||
|
|
||||||
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
||||||
match value.algorithm {
|
match value.algorithm {
|
||||||
DerivationAlgorithm::Secp256k1 => {
|
DerivationAlgorithm::Secp256k1 => Ok(Self::new_from_parts(
|
||||||
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into)
|
&value.data,
|
||||||
}
|
value.depth,
|
||||||
|
value.chain_code,
|
||||||
|
)),
|
||||||
_ => Err(Self::Error::Algorithm),
|
_ => Err(Self::Error::Algorithm),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,9 +335,11 @@ mod ed25519 {
|
||||||
|
|
||||||
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
||||||
match value.algorithm {
|
match value.algorithm {
|
||||||
DerivationAlgorithm::Ed25519 => {
|
DerivationAlgorithm::Ed25519 => Ok(Self::new_from_parts(
|
||||||
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into)
|
&value.data,
|
||||||
}
|
value.depth,
|
||||||
|
value.chain_code,
|
||||||
|
)),
|
||||||
_ => Err(Self::Error::Algorithm),
|
_ => Err(Self::Error::Algorithm),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,10 @@ use keyfork_slip10_test_data::{test_data, Test};
|
||||||
fn secp256k1() {
|
fn secp256k1() {
|
||||||
use k256::SecretKey;
|
use k256::SecretKey;
|
||||||
|
|
||||||
let tests = test_data().unwrap().remove("secp256k1").unwrap();
|
let tests = test_data()
|
||||||
|
.unwrap()
|
||||||
|
.remove(&"secp256k1".to_string())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
for per_seed in tests {
|
for per_seed in tests {
|
||||||
let seed = &per_seed.seed;
|
let seed = &per_seed.seed;
|
||||||
|
@ -28,7 +31,7 @@ fn secp256k1() {
|
||||||
|
|
||||||
// Tests for ExtendedPrivateKey
|
// Tests for ExtendedPrivateKey
|
||||||
let varlen_seed = VariableLengthSeed::new(seed);
|
let varlen_seed = VariableLengthSeed::new(seed);
|
||||||
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed).unwrap();
|
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
|
||||||
let derived_key = xkey.derive_path(&chain).unwrap();
|
let derived_key = xkey.derive_path(&chain).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derived_key.chain_code().as_slice(),
|
derived_key.chain_code().as_slice(),
|
||||||
|
@ -59,7 +62,7 @@ fn secp256k1() {
|
||||||
fn ed25519() {
|
fn ed25519() {
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
|
||||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||||
|
|
||||||
for per_seed in tests {
|
for per_seed in tests {
|
||||||
let seed = &per_seed.seed;
|
let seed = &per_seed.seed;
|
||||||
|
@ -74,7 +77,7 @@ fn ed25519() {
|
||||||
|
|
||||||
// Tests for ExtendedPrivateKey
|
// Tests for ExtendedPrivateKey
|
||||||
let varlen_seed = VariableLengthSeed::new(seed);
|
let varlen_seed = VariableLengthSeed::new(seed);
|
||||||
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed).unwrap();
|
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
|
||||||
let derived_key = xkey.derive_path(&chain).unwrap();
|
let derived_key = xkey.derive_path(&chain).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derived_key.chain_code().as_slice(),
|
derived_key.chain_code().as_slice(),
|
||||||
|
@ -102,24 +105,24 @@ fn ed25519() {
|
||||||
|
|
||||||
#[cfg(feature = "ed25519")]
|
#[cfg(feature = "ed25519")]
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "HardenedDerivationRequired")]
|
#[should_panic]
|
||||||
fn panics_with_unhardened_derivation() {
|
fn panics_with_unhardened_derivation() {
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
|
||||||
let seed = hex!("000102030405060708090a0b0c0d0e0f");
|
let seed = hex!("000102030405060708090a0b0c0d0e0f");
|
||||||
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
|
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
|
||||||
xkey.derive_path(&DerivationPath::from_str("m/0").unwrap())
|
xkey.derive_path(&DerivationPath::from_str("m/0").unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ed25519")]
|
#[cfg(feature = "ed25519")]
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "Depth")]
|
#[should_panic]
|
||||||
fn panics_at_depth() {
|
fn panics_at_depth() {
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
|
||||||
let seed = hex!("000102030405060708090a0b0c0d0e0f");
|
let seed = hex!("000102030405060708090a0b0c0d0e0f");
|
||||||
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
|
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
|
||||||
for i in 0..=u32::from(u8::MAX) {
|
for i in 0..=u32::from(u8::MAX) {
|
||||||
xkey = xkey
|
xkey = xkey
|
||||||
.derive_child(&DerivationIndex::new(i, true).unwrap())
|
.derive_child(&DerivationIndex::new(i, true).unwrap())
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-shard"
|
name = "keyfork-shard"
|
||||||
version = "0.3.4"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -17,26 +14,26 @@ openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "de
|
||||||
qrcode = ["keyfork-qrcode"]
|
qrcode = ["keyfork-qrcode"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-bug = { workspace = true }
|
keyfork-bug = { version = "0.1.0", path = "../util/keyfork-bug", registry = "distrust" }
|
||||||
keyfork-prompt = { workspace = true, default-features = false, features = ["mnemonic"] }
|
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"], registry = "distrust" }
|
||||||
keyfork-qrcode = { workspace = true, optional = true, default-features = false }
|
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", optional = true, default-features = false, registry = "distrust" }
|
||||||
smex = { workspace = true }
|
smex = { version = "0.1.0", path = "../util/smex", registry = "distrust" }
|
||||||
thiserror = { workspace = true }
|
|
||||||
|
sharks = "0.5.0"
|
||||||
|
thiserror = "1.0.50"
|
||||||
|
|
||||||
# Remote operator mode
|
# Remote operator mode
|
||||||
keyfork-mnemonic = { workspace = true }
|
keyfork-mnemonic-util = { version = "0.2.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
|
||||||
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
|
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
|
||||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
hkdf = { version = "0.12.4", features = ["std"] }
|
hkdf = { version = "0.12.4", features = ["std"] }
|
||||||
sha2 = { workspace = true }
|
sha2 = "0.10.8"
|
||||||
|
|
||||||
# OpenPGP
|
# OpenPGP
|
||||||
keyfork-derive-openpgp = { workspace = true, default-features = false }
|
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp", default-features = false, registry = "distrust" }
|
||||||
anyhow = { workspace = true, optional = true }
|
anyhow = { version = "1.0.79", optional = true }
|
||||||
card-backend = { version = "0.2.0", optional = true }
|
card-backend = { version = "0.2.0", optional = true }
|
||||||
card-backend-pcsc = { workspace = true, optional = true }
|
card-backend-pcsc = { version = "0.5.0", optional = true }
|
||||||
openpgp-card-sequoia = { workspace = true, optional = true }
|
openpgp-card-sequoia = { version = "0.2.0", optional = true, default-features = false }
|
||||||
openpgp-card = { workspace = true, optional = true }
|
openpgp-card = { version = "0.4.0", optional = true }
|
||||||
sequoia-openpgp = { workspace = true, optional = true }
|
sequoia-openpgp = { version = "1.17.0", optional = true, default-features = false }
|
||||||
base64 = { workspace = true }
|
|
||||||
blahaj = "0.6.0"
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! Combine OpenPGP shards and output the hex-encoded secret.
|
//!
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
|
@ -7,7 +7,7 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_prompt::default_handler;
|
use keyfork_prompt::{DefaultTerminal, default_terminal};
|
||||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
@ -32,14 +32,10 @@ fn run() -> Result<()> {
|
||||||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||||
let prompt_handler = default_handler()?;
|
let prompt_handler = default_terminal()?;
|
||||||
|
|
||||||
let bytes = openpgp.decrypt_all_shards_to_secret(
|
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?;
|
||||||
key_discovery.as_deref(),
|
|
||||||
messages_file,
|
|
||||||
prompt_handler,
|
|
||||||
)?;
|
|
||||||
print!("{}", smex::encode(bytes));
|
print!("{}", smex::encode(bytes));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! Decrypt a single OpenPGP shard and encapsulate it for remote transport.
|
//!
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
|
@ -7,8 +7,8 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_prompt::default_handler;
|
use keyfork_prompt::{DefaultTerminal, default_terminal};
|
||||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
@ -32,14 +32,10 @@ fn run() -> Result<()> {
|
||||||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||||
let prompt_handler = default_handler()?;
|
let prompt_handler = default_terminal()?;
|
||||||
|
|
||||||
openpgp.decrypt_one_shard_for_transport(
|
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?;
|
||||||
key_discovery.as_deref(),
|
|
||||||
messages_file,
|
|
||||||
prompt_handler,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
//! 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;
|
use keyfork_shard::remote_decrypt;
|
||||||
|
|
||||||
|
@ -13,7 +16,7 @@ fn run() -> Result<()> {
|
||||||
match args.as_slice() {
|
match args.as_slice() {
|
||||||
[] => (),
|
[] => (),
|
||||||
_ => panic!("Usage: {program_name}"),
|
_ => panic!("Usage: {program_name}"),
|
||||||
}
|
};
|
||||||
|
|
||||||
let mut bytes = vec![];
|
let mut bytes = vec![];
|
||||||
remote_decrypt(&mut bytes)?;
|
remote_decrypt(&mut bytes)?;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
//! Split a hex-encoded secret into OpenPGP shards
|
//!
|
||||||
|
|
||||||
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
||||||
|
|
||||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
use keyfork_prompt::terminal::DefaultTerminal;
|
||||||
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Error {
|
enum Error {
|
||||||
|
@ -50,15 +51,9 @@ fn run() -> Result<()> {
|
||||||
smex::decode(line?)?
|
smex::decode(line?)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||||
|
|
||||||
openpgp.shard_and_encrypt(
|
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
|
||||||
threshold,
|
|
||||||
max,
|
|
||||||
&input,
|
|
||||||
key_discovery.as_path(),
|
|
||||||
std::io::stdout(),
|
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,77 +1,28 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![allow(clippy::expect_fun_call)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{Read, Write},
|
io::{stdin, stdout, Read, Write},
|
||||||
rc::Rc,
|
sync::{Arc, Mutex},
|
||||||
str::FromStr,
|
|
||||||
sync::{LazyLock, Mutex},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{consts::U12, Aead},
|
aead::{consts::U12, Aead, AeadCore, OsRng},
|
||||||
Aes256Gcm, KeyInit, Nonce,
|
Aes256Gcm, KeyInit, Nonce,
|
||||||
};
|
};
|
||||||
use base64::prelude::{Engine, BASE64_STANDARD};
|
|
||||||
use blahaj::{Share, Sharks};
|
|
||||||
use hkdf::Hkdf;
|
use hkdf::Hkdf;
|
||||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||||
use keyfork_mnemonic::{English, Mnemonic};
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
prompt_validated_wordlist,
|
validators::{mnemonic::MnemonicSetValidator, Validator},
|
||||||
validators::{
|
Message as PromptMessage, PromptHandler, Terminal,
|
||||||
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
|
||||||
Validator,
|
|
||||||
},
|
|
||||||
Message as PromptMessage, PromptHandler, YesNo,
|
|
||||||
};
|
};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::Sha256;
|
||||||
|
use sharks::{Share, Sharks};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
const PLAINTEXT_LENGTH: u8 = 32 // shard
|
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
|
||||||
+ 1 // index
|
const ENC_LEN: u8 = 4 * 16;
|
||||||
+ 1 // threshold
|
|
||||||
+ 1 // version
|
|
||||||
+ 1; // length;
|
|
||||||
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
|
||||||
enum RetryScanMnemonic {
|
|
||||||
Retry,
|
|
||||||
Continue,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl keyfork_prompt::Choice for RetryScanMnemonic {
|
|
||||||
fn identifier(&self) -> Option<char> {
|
|
||||||
Some(match self {
|
|
||||||
RetryScanMnemonic::Retry => 'r',
|
|
||||||
RetryScanMnemonic::Continue => 'c',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for RetryScanMnemonic {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
RetryScanMnemonic::Retry => write!(f, "Retry scanning mnemonic."),
|
|
||||||
RetryScanMnemonic::Continue => write!(f, "Continue to manual mnemonic entry."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")]
|
#[cfg(feature = "openpgp")]
|
||||||
pub mod openpgp;
|
pub mod openpgp;
|
||||||
|
@ -91,10 +42,9 @@ pub trait KeyDiscovery<F: Format + ?Sized> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The method may return an error if private keys could not be loaded from the given
|
/// 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
|
/// 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
|
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
|
||||||
/// never exist on-system) or an empty container (such as an empty Vec); in either case, this
|
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
|
||||||
/// method _must not_ return an error if keys are accessible but can't be transferred into
|
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
|
||||||
/// memory.
|
|
||||||
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
|
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,8 +71,8 @@ pub trait Format {
|
||||||
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
|
/// 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
|
/// 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
|
/// 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
|
/// will use the same key_data for both, ensuring an iteration of this method will match with
|
||||||
/// with iterations in methods called later.
|
/// iterations in methods called later.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The method may return an error if encryption to any of the public keys fails.
|
/// The method may return an error if encryption to any of the public keys fails.
|
||||||
|
@ -177,10 +127,10 @@ pub trait Format {
|
||||||
&self,
|
&self,
|
||||||
private_keys: Option<Self::PrivateKeyData>,
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
encrypted_messages: &[Self::EncryptedData],
|
encrypted_messages: &[Self::EncryptedData],
|
||||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||||
) -> Result<(Vec<Share>, u8), Self::Error>;
|
) -> Result<(Vec<Share>, u8), Self::Error>;
|
||||||
|
|
||||||
/// Decrypt a single share and associated metadata from a readable input. For the current
|
/// Decrypt a single share and associated metadata from a reaable input. For the current
|
||||||
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
|
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
|
||||||
/// combine secrets.
|
/// combine secrets.
|
||||||
///
|
///
|
||||||
|
@ -191,43 +141,9 @@ pub trait Format {
|
||||||
&self,
|
&self,
|
||||||
private_keys: Option<Self::PrivateKeyData>,
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
encrypted_data: &[Self::EncryptedData],
|
encrypted_data: &[Self::EncryptedData],
|
||||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||||
) -> Result<(Share, u8), Self::Error>;
|
) -> Result<(Share, u8), Self::Error>;
|
||||||
|
|
||||||
/// Decrypt the public keys and metadata from encrypted data.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
|
|
||||||
/// could neither be encrypted nor parsed.
|
|
||||||
fn decrypt_metadata(
|
|
||||||
&self,
|
|
||||||
private_keys: Option<Self::PrivateKeyData>,
|
|
||||||
encrypted_data: &[Self::EncryptedData],
|
|
||||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
|
||||||
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error>;
|
|
||||||
|
|
||||||
/// Decrypt the public keys and metadata from a Shardfile.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
|
|
||||||
/// could neither be encrypted nor parsed.
|
|
||||||
fn decrypt_metadata_from_file(
|
|
||||||
&self,
|
|
||||||
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
|
||||||
reader: impl Read + Send + Sync,
|
|
||||||
prompt: Box<dyn PromptHandler>,
|
|
||||||
) -> Result<(u8, Vec<Self::PublicKey>), Self::Error> {
|
|
||||||
let private_keys = private_key_discovery
|
|
||||||
.map(|p| p.discover_private_keys())
|
|
||||||
.transpose()?;
|
|
||||||
let encrypted_messages = self.parse_shard_file(reader)?;
|
|
||||||
self.decrypt_metadata(
|
|
||||||
private_keys,
|
|
||||||
&encrypted_messages,
|
|
||||||
Rc::new(Mutex::new(prompt)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt multiple shares and combine them to recreate a secret.
|
/// Decrypt multiple shares and combine them to recreate a secret.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
@ -237,7 +153,7 @@ pub trait Format {
|
||||||
&self,
|
&self,
|
||||||
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||||
reader: impl Read + Send + Sync,
|
reader: impl Read + Send + Sync,
|
||||||
prompt: Box<dyn PromptHandler>,
|
prompt: impl PromptHandler,
|
||||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
let private_keys = private_key_discovery
|
let private_keys = private_key_discovery
|
||||||
.map(|p| p.discover_private_keys())
|
.map(|p| p.discover_private_keys())
|
||||||
|
@ -246,7 +162,7 @@ pub trait Format {
|
||||||
let (shares, threshold) = self.decrypt_all_shards(
|
let (shares, threshold) = self.decrypt_all_shards(
|
||||||
private_keys,
|
private_keys,
|
||||||
&encrypted_messages,
|
&encrypted_messages,
|
||||||
Rc::new(Mutex::new(prompt)),
|
Arc::new(Mutex::new(prompt)),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let secret = Sharks(threshold)
|
let secret = Sharks(threshold)
|
||||||
|
@ -263,14 +179,13 @@ pub trait Format {
|
||||||
/// The method may return an error if a share can't be decrypted. The method will not return an
|
/// 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
|
/// 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.
|
/// QR code; instead, a mnemonic prompt will be used.
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn decrypt_one_shard_for_transport(
|
fn decrypt_one_shard_for_transport(
|
||||||
&self,
|
&self,
|
||||||
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||||
reader: impl Read + Send + Sync,
|
reader: impl Read + Send + Sync,
|
||||||
prompt: Box<dyn PromptHandler>,
|
prompt: impl PromptHandler,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let prompt = Rc::new(Mutex::new(prompt));
|
let prompt = Arc::new(Mutex::new(prompt));
|
||||||
|
|
||||||
// parse input
|
// parse input
|
||||||
let private_keys = private_key_discovery
|
let private_keys = private_key_discovery
|
||||||
|
@ -279,6 +194,7 @@ pub trait Format {
|
||||||
let encrypted_messages = self.parse_shard_file(reader)?;
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||||
|
|
||||||
// establish AES-256-GCM key via ECDH
|
// establish AES-256-GCM key via ECDH
|
||||||
|
let mut nonce_data: Option<[u8; 12]> = None;
|
||||||
let mut pubkey_data: Option<[u8; 32]> = None;
|
let mut pubkey_data: Option<[u8; 32]> = None;
|
||||||
|
|
||||||
// receive remote data via scanning QR code from camera
|
// receive remote data via scanning QR code from camera
|
||||||
|
@ -288,85 +204,58 @@ pub trait Format {
|
||||||
.lock()
|
.lock()
|
||||||
.expect(bug!(POISONED_MUTEX))
|
.expect(bug!(POISONED_MUTEX))
|
||||||
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
loop {
|
if let Ok(Some(hex)) =
|
||||||
if let Ok(Some(qrcode_content)) =
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
||||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
|
|
||||||
{
|
{
|
||||||
let decoded_data = BASE64_STANDARD
|
let decoded_data = smex::decode(&hex)?;
|
||||||
.decode(qrcode_content)
|
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
|
||||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
|
||||||
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 {
|
} else {
|
||||||
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
|
prompt
|
||||||
let choice = keyfork_prompt::prompt_choice(
|
.lock()
|
||||||
&mut **prompt,
|
.expect(bug!(POISONED_MUTEX))
|
||||||
"A QR code could not be scanned. Retry or continue?",
|
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||||
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
|
};
|
||||||
)?;
|
|
||||||
if choice == RetryScanMnemonic::Continue {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
||||||
let their_pubkey = if let Some(pubkey) = pubkey_data {
|
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
|
||||||
pubkey
|
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
|
||||||
} else {
|
_ => {
|
||||||
let validator = MnemonicValidator {
|
let validator = MnemonicSetValidator {
|
||||||
word_length: Some(WordLength::Count(24)),
|
word_lengths: [9, 24],
|
||||||
};
|
};
|
||||||
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
|
let [nonce_mnemonic, pubkey_mnemonic] = prompt
|
||||||
prompt_validated_wordlist::<English, _>(
|
.lock()
|
||||||
&mut **prompt,
|
.expect(bug!(POISONED_MUTEX))
|
||||||
|
.prompt_validated_wordlist::<English, _>(
|
||||||
QRCODE_COULDNT_READ,
|
QRCODE_COULDNT_READ,
|
||||||
3,
|
3,
|
||||||
&*validator.to_fn(),
|
validator.to_fn(),
|
||||||
)?
|
)?;
|
||||||
|
|
||||||
|
let nonce = nonce_mnemonic
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| InvalidData)?
|
.map_err(|_| InvalidData)?;
|
||||||
|
let pubkey = pubkey_mnemonic
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| InvalidData)?;
|
||||||
|
(nonce, pubkey)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// create our shared key
|
// create our shared key
|
||||||
let our_key = EphemeralSecret::random();
|
let our_key = EphemeralSecret::random();
|
||||||
let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
|
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
||||||
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
|
let shared_secret = our_key
|
||||||
assert!(
|
.diffie_hellman(&PublicKey::from(their_pubkey))
|
||||||
shared_secret.was_contributory(),
|
.to_bytes();
|
||||||
bug!("shared secret might be insecure")
|
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
||||||
);
|
let mut hkdf_output = [0u8; 256 / 8];
|
||||||
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
|
hkdf.expand(&[], &mut hkdf_output)?;
|
||||||
|
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
||||||
let mut shared_key_data = [0u8; 256 / 8];
|
|
||||||
hkdf.expand(b"key", &mut shared_key_data)?;
|
|
||||||
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
|
|
||||||
|
|
||||||
let mut nonce_data = [0u8; 12];
|
|
||||||
hkdf.expand(b"nonce", &mut nonce_data)?;
|
|
||||||
let nonce = Nonce::<U12>::from_slice(&nonce_data);
|
|
||||||
|
|
||||||
// decrypt a single shard and create the payload
|
// decrypt a single shard and create the payload
|
||||||
let (share, threshold) =
|
let (share, threshold) =
|
||||||
|
@ -375,46 +264,49 @@ pub trait Format {
|
||||||
payload.insert(0, HUNK_VERSION);
|
payload.insert(0, HUNK_VERSION);
|
||||||
payload.insert(1, threshold);
|
payload.insert(1, threshold);
|
||||||
assert!(
|
assert!(
|
||||||
payload.len() < PLAINTEXT_LENGTH as usize,
|
payload.len() <= ENC_LEN as usize,
|
||||||
"invalid share length (too long, must be less than {PLAINTEXT_LENGTH} bytes)"
|
"invalid share length (too long, max {ENC_LEN} bytes)"
|
||||||
);
|
);
|
||||||
|
|
||||||
// convert plaintext to static-size payload
|
|
||||||
#[allow(clippy::assertions_on_constants)]
|
|
||||||
{
|
|
||||||
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
|
|
||||||
// duplicate mnemonic words. This version does not include that, and instead uses a
|
|
||||||
// repeated length byte.
|
|
||||||
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
|
|
||||||
"previously asserted length must be < {PLAINTEXT_LENGTH}",
|
|
||||||
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
|
|
||||||
)); PLAINTEXT_LENGTH as usize];
|
|
||||||
plaintext_bytes[..payload.len()].clone_from_slice(&payload);
|
|
||||||
|
|
||||||
// encrypt data
|
// encrypt data
|
||||||
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?;
|
let nonce = Nonce::<U12>::from_slice(&nonce);
|
||||||
assert_eq!(
|
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
|
||||||
encrypted_bytes.len(),
|
|
||||||
ENCRYPTED_LENGTH as usize,
|
|
||||||
bug!("encrypted bytes size != expected len"),
|
|
||||||
);
|
|
||||||
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
|
|
||||||
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
|
|
||||||
|
|
||||||
let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes);
|
// convert data to a static-size payload
|
||||||
|
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
|
||||||
|
#[allow(clippy::assertions_on_constants)]
|
||||||
|
{
|
||||||
|
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
|
||||||
|
}
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
|
||||||
|
assert!(
|
||||||
|
payload_bytes.len() < out_bytes.len(),
|
||||||
|
"encrypted payload larger than acceptable limit"
|
||||||
|
);
|
||||||
|
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
|
||||||
|
|
||||||
|
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
|
||||||
|
// difficulty when entering in prompts manually, as one's place could be lost due to
|
||||||
|
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
|
||||||
|
// but not including the last byte.
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
*byte = (i % u8::MAX as usize) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// safety: size of out_bytes is constant and always % 4 == 0
|
||||||
|
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
|
||||||
|
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
||||||
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
||||||
qrcode_data.extend(payload_mnemonic.as_bytes());
|
qrcode_data.extend(payload_mnemonic.as_bytes());
|
||||||
if let Ok(qrcode) = qrencode(
|
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
||||||
&BASE64_STANDARD.encode(qrcode_data),
|
|
||||||
ErrorCorrection::Highest,
|
|
||||||
) {
|
|
||||||
prompt
|
prompt
|
||||||
.lock()
|
.lock()
|
||||||
.expect(bug!(POISONED_MUTEX))
|
.expect(bug!(POISONED_MUTEX))
|
||||||
|
@ -467,11 +359,11 @@ pub trait Format {
|
||||||
"must have less than u8::MAX public keys"
|
"must have less than u8::MAX public keys"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
max as usize,
|
max,
|
||||||
public_keys.len(),
|
public_keys.len() as u8,
|
||||||
"max must be equal to amount of public keys"
|
"max must be equal to amount of public keys"
|
||||||
);
|
);
|
||||||
let max = u8::try_from(public_keys.len()).expect(bug!("invalid max: {max}", max = max));
|
let max = public_keys.len() as u8;
|
||||||
assert!(max >= threshold, "threshold must not exceed max keys");
|
assert!(max >= threshold, "threshold must not exceed max keys");
|
||||||
|
|
||||||
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
|
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
|
||||||
|
@ -509,17 +401,13 @@ pub struct InvalidData;
|
||||||
/// 1 byte: Version
|
/// 1 byte: Version
|
||||||
/// 1 byte: Threshold
|
/// 1 byte: Threshold
|
||||||
/// Data: &[u8]
|
/// Data: &[u8]
|
||||||
pub(crate) const HUNK_VERSION: u8 = 2;
|
pub(crate) const HUNK_VERSION: u8 = 1;
|
||||||
pub(crate) const HUNK_OFFSET: usize = 2;
|
pub(crate) const HUNK_OFFSET: usize = 2;
|
||||||
|
|
||||||
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
||||||
|
const QRCODE_TIMEOUT: u64 = 60; // One minute
|
||||||
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
|
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
|
||||||
static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
|
const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry.";
|
||||||
std::env::var("KEYFORK_QRCODE_TIMEOUT")
|
|
||||||
.ok()
|
|
||||||
.and_then(|t| u64::from_str(&t).ok())
|
|
||||||
.unwrap_or(60)
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
|
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
|
||||||
/// shares, and combine them.
|
/// shares, and combine them.
|
||||||
|
@ -533,9 +421,8 @@ static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// The function may panic if it is given payloads generated using a version of Keyfork that is
|
/// The function may panic if it is given payloads generated using a version of Keyfork that is
|
||||||
/// incompatible with the currently running version.
|
/// incompatible with the currently running version.
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut pm = keyfork_prompt::default_handler()?;
|
let mut pm = Terminal::new(stdin(), stdout())?;
|
||||||
|
|
||||||
let mut iter_count = None;
|
let mut iter_count = None;
|
||||||
let mut shares = vec![];
|
let mut shares = vec![];
|
||||||
|
@ -545,40 +432,36 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
|
|
||||||
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
||||||
iter += 1;
|
iter += 1;
|
||||||
|
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||||
|
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
|
||||||
let our_key = EphemeralSecret::random();
|
let our_key = EphemeralSecret::random();
|
||||||
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
|
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
||||||
|
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
||||||
let qrcode_data = key_mnemonic.to_bytes();
|
let mut qrcode_data = nonce_mnemonic.to_bytes();
|
||||||
if let Ok(qrcode) = qrencode(
|
qrcode_data.extend(key_mnemonic.as_bytes());
|
||||||
&BASE64_STANDARD.encode(qrcode_data),
|
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
||||||
ErrorCorrection::Highest,
|
|
||||||
) {
|
|
||||||
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!(
|
pm.prompt_message(PromptMessage::Text(format!(
|
||||||
concat!(
|
concat!(
|
||||||
"QR code #{iter} will be displayed after this prompt. ",
|
"A QR code will be displayed after this prompt. ",
|
||||||
"Send the QR code to the next shardholder. ",
|
"Send the QR code to only shardholder {iter}. ",
|
||||||
"Only the next shardholder should scan the QR code. ",
|
"Nobody else should scan this QR code."
|
||||||
),
|
),
|
||||||
iter = iter,
|
iter = iter
|
||||||
)))?;
|
)))?;
|
||||||
pm.prompt_message(PromptMessage::Data(qrcode))?;
|
pm.prompt_message(PromptMessage::Data(qrcode))?;
|
||||||
pm.prompt_message(PromptMessage::Text(format!(
|
|
||||||
"The following should be sent to verify the QR code: {small_mnemonic}"
|
|
||||||
)))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pm.prompt_message(PromptMessage::Text(format!(
|
pm.prompt_message(PromptMessage::Text(format!(
|
||||||
concat!(
|
concat!(
|
||||||
"Upon request, these words should be sent to the shardholder: ",
|
"Upon request, these words should be sent to shardholder {iter}: ",
|
||||||
"{key_mnemonic}"
|
"{nonce_mnemonic} {key_mnemonic}"
|
||||||
),
|
),
|
||||||
|
iter = iter,
|
||||||
|
nonce_mnemonic = nonce_mnemonic,
|
||||||
key_mnemonic = key_mnemonic,
|
key_mnemonic = key_mnemonic,
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
|
@ -588,48 +471,29 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
loop {
|
if let Ok(Some(hex)) =
|
||||||
if let Ok(Some(qrcode_content)) =
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
||||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
|
|
||||||
{
|
{
|
||||||
let decoded_data = BASE64_STANDARD
|
let decoded_data = smex::decode(&hex)?;
|
||||||
.decode(qrcode_content)
|
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
||||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
|
||||||
assert_eq!(
|
|
||||||
decoded_data.len(),
|
|
||||||
// Include length of public key
|
|
||||||
ENCRYPTED_LENGTH as usize + 32,
|
|
||||||
bug!("invalid payload data")
|
|
||||||
);
|
|
||||||
let _ =
|
|
||||||
pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
|
||||||
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
||||||
break;
|
|
||||||
} else {
|
} else {
|
||||||
let choice = keyfork_prompt::prompt_choice(
|
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||||
&mut *pm,
|
};
|
||||||
"A QR code could not be scanned. Retry or continue?",
|
|
||||||
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
|
|
||||||
)?;
|
|
||||||
if choice == RetryScanMnemonic::Continue {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (pubkey, payload) = if let Some((pubkey, payload)) = pubkey_data.zip(payload_data) {
|
let (pubkey, payload) = match (pubkey_data, payload_data) {
|
||||||
(pubkey, payload)
|
(Some(pubkey), Some(payload)) => (pubkey, payload),
|
||||||
} else {
|
_ => {
|
||||||
let validator = MnemonicSetValidator {
|
let validator = MnemonicSetValidator {
|
||||||
word_lengths: [24, 39],
|
word_lengths: [24, 48],
|
||||||
};
|
};
|
||||||
|
|
||||||
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
|
let [pubkey_mnemonic, payload_mnemonic] = pm
|
||||||
&mut *pm,
|
.prompt_validated_wordlist::<English, _>(
|
||||||
QRCODE_COULDNT_READ,
|
QRCODE_COULDNT_READ,
|
||||||
3,
|
3,
|
||||||
&*validator.to_fn(),
|
validator.to_fn(),
|
||||||
)?;
|
)?;
|
||||||
let pubkey = pubkey_mnemonic
|
let pubkey = pubkey_mnemonic
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
|
@ -637,43 +501,32 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
.map_err(|_| InvalidData)?;
|
.map_err(|_| InvalidData)?;
|
||||||
let payload = payload_mnemonic.to_bytes();
|
let payload = payload_mnemonic.to_bytes();
|
||||||
(pubkey, payload)
|
(pubkey, payload)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
|
||||||
payload.len(),
|
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
||||||
ENCRYPTED_LENGTH as usize,
|
let mut hkdf_output = [0u8; 256 / 8];
|
||||||
bug!("invalid payload data")
|
hkdf.expand(&[], &mut hkdf_output)?;
|
||||||
);
|
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
||||||
|
|
||||||
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
|
let payload =
|
||||||
assert!(
|
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
|
||||||
shared_secret.was_contributory(),
|
|
||||||
bug!("shared secret might be insecure")
|
|
||||||
);
|
|
||||||
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
|
|
||||||
|
|
||||||
let mut shared_key_data = [0u8; 256 / 8];
|
|
||||||
hkdf.expand(b"key", &mut shared_key_data)?;
|
|
||||||
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
|
|
||||||
|
|
||||||
let mut nonce_data = [0u8; 12];
|
|
||||||
hkdf.expand(b"nonce", &mut nonce_data)?;
|
|
||||||
let nonce = Nonce::<U12>::from_slice(&nonce_data);
|
|
||||||
|
|
||||||
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
|
|
||||||
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
|
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
|
||||||
|
|
||||||
if let Some(n) = &mut iter_count {
|
match &mut iter_count {
|
||||||
|
Some(n) => {
|
||||||
// Must be > 0 to start loop, can't go lower
|
// Must be > 0 to start loop, can't go lower
|
||||||
*n -= 1;
|
*n -= 1;
|
||||||
} else {
|
}
|
||||||
|
None => {
|
||||||
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
|
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
|
||||||
threshold = payload[1];
|
threshold = payload[1];
|
||||||
let _ = iter_count.insert(threshold - 1);
|
let _ = iter_count.insert(threshold - 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let payload_len = payload.last().expect(bug!("payload should not be empty"));
|
shares.push(payload[HUNK_OFFSET..].to_vec());
|
||||||
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let shares = shares
|
let shares = shares
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
//! OpenPGP Shard functionality.
|
//! OpenPGP Shard functionality.
|
||||||
|
|
||||||
|
#![allow(clippy::expect_fun_call)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
|
marker::PhantomData,
|
||||||
path::Path,
|
path::Path,
|
||||||
rc::Rc,
|
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::Mutex,
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use blahaj::Share;
|
|
||||||
use keyfork_bug::bug;
|
use keyfork_bug::bug;
|
||||||
use keyfork_derive_openpgp::{
|
use keyfork_derive_openpgp::{
|
||||||
derive_util::{DerivationPath, VariableLengthSeed},
|
derive_util::{DerivationPath, VariableLengthSeed},
|
||||||
|
@ -33,6 +34,7 @@ use openpgp::{
|
||||||
KeyID, PacketPile,
|
KeyID, PacketPile,
|
||||||
};
|
};
|
||||||
pub use sequoia_openpgp as openpgp;
|
pub use sequoia_openpgp as openpgp;
|
||||||
|
use sharks::Share;
|
||||||
|
|
||||||
mod keyring;
|
mod keyring;
|
||||||
use keyring::Keyring;
|
use keyring::Keyring;
|
||||||
|
@ -75,10 +77,6 @@ pub enum Error {
|
||||||
/// An IO error occurred.
|
/// An IO error occurred.
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(#[source] std::io::Error),
|
Io(#[source] std::io::Error),
|
||||||
|
|
||||||
/// No valid keys were found for the given recipient.
|
|
||||||
#[error("No valid keys were found for the recipient {0}")]
|
|
||||||
NoValidKeys(KeyID),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
|
@ -92,7 +90,7 @@ pub struct EncryptedMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn new(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pkesks: std::mem::take(pkesks),
|
pkesks: std::mem::take(pkesks),
|
||||||
|
@ -158,7 +156,7 @@ impl EncryptedMessage {
|
||||||
/// Decrypt the message with a Sequoia policy and decryptor.
|
/// Decrypt the message with a Sequoia policy and decryptor.
|
||||||
///
|
///
|
||||||
/// This method creates a container containing the packets and passes the serialized container
|
/// 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
|
/// # Errors
|
||||||
/// The method may return an error if it is unable to rebuild the message to decrypt or if it
|
/// The method may return an error if it is unable to rebuild the message to decrypt or if it
|
||||||
|
@ -183,69 +181,52 @@ impl EncryptedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encoding and decoding shards using OpenPGP.
|
|
||||||
pub struct OpenPGP;
|
|
||||||
|
|
||||||
impl OpenPGP {
|
|
||||||
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them.
|
|
||||||
///
|
///
|
||||||
/// Certificates are read from a file, or from files one level deep in a directory.
|
pub struct OpenPGP<P: PromptHandler> {
|
||||||
/// Certificates with duplicated fingerprints will be discarded.
|
p: PhantomData<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PromptHandler> OpenPGP<P> {
|
||||||
|
#[allow(clippy::new_without_default, missing_docs)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { p: PhantomData }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PromptHandler> OpenPGP<P> {
|
||||||
|
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
||||||
|
/// from a file, or from files one level deep in a directory.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may return an error if it is unable to read the directory or if Sequoia is
|
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
|
||||||
/// unable to load certificates from the file.
|
/// to load certificates from the file.
|
||||||
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
let mut pubkeys = std::collections::HashSet::new();
|
|
||||||
let mut certs = HashMap::new();
|
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
for maybe_cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
|
let mut vec = vec![];
|
||||||
let cert = maybe_cert.map_err(Error::Sequoia)?;
|
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
|
||||||
let certfp = cert.fingerprint();
|
vec.push(cert.map_err(Error::Sequoia)?);
|
||||||
for key in cert.keys() {
|
|
||||||
let fp = key.fingerprint();
|
|
||||||
if pubkeys.contains(&fp) {
|
|
||||||
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
|
|
||||||
}
|
|
||||||
pubkeys.insert(fp);
|
|
||||||
}
|
|
||||||
certs.insert(certfp, cert);
|
|
||||||
}
|
}
|
||||||
|
Ok(vec)
|
||||||
} else {
|
} else {
|
||||||
|
let mut vec = vec![];
|
||||||
for entry in path
|
for entry in path
|
||||||
.read_dir()
|
.read_dir()
|
||||||
.map_err(Error::Io)?
|
.map_err(Error::Io)?
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.filter(|p| p.path().is_file())
|
.filter(|p| p.path().is_file())
|
||||||
{
|
{
|
||||||
let cert = Cert::from_file(entry.path()).map_err(Error::Sequoia)?;
|
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
|
||||||
let certfp = cert.fingerprint();
|
|
||||||
for key in cert.keys() {
|
|
||||||
let fp = key.fingerprint();
|
|
||||||
if pubkeys.contains(&fp) {
|
|
||||||
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
|
|
||||||
}
|
}
|
||||||
pubkeys.insert(fp);
|
Ok(vec)
|
||||||
}
|
}
|
||||||
certs.insert(certfp, cert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for cert in certs.values() {
|
|
||||||
let policy = StandardPolicy::new();
|
|
||||||
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
|
|
||||||
if get_encryption_keys(&valid_cert).next().is_none() {
|
|
||||||
return Err(Error::NoValidKeys(valid_cert.keyid()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(certs.into_values().collect())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const METADATA_MESSAGE_MISSING: &str = "Metadata message was not found in parsed packets";
|
const METADATA_MESSAGE_MISSING: &str = "Metadata message was not found in parsed packets";
|
||||||
|
|
||||||
impl Format for OpenPGP {
|
impl<P: PromptHandler> Format for OpenPGP<P> {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type PublicKey = Cert;
|
type PublicKey = Cert;
|
||||||
type PrivateKeyData = Vec<Cert>;
|
type PrivateKeyData = Vec<Cert>;
|
||||||
|
@ -259,11 +240,10 @@ impl Format for OpenPGP {
|
||||||
let userid = UserID::from("keyfork-sss");
|
let userid = UserID::from("keyfork-sss");
|
||||||
let path = DerivationPath::from_str("m/7366512'/0'").expect(bug!("valid derivation path"));
|
let path = DerivationPath::from_str("m/7366512'/0'").expect(bug!("valid derivation path"));
|
||||||
let xprv = XPrv::new(seed)
|
let xprv = XPrv::new(seed)
|
||||||
.expect(bug!("could not create XPrv from key"))
|
|
||||||
.derive_path(&path)
|
.derive_path(&path)
|
||||||
.expect(bug!("valid derivation"));
|
.expect(bug!("valid derivation"));
|
||||||
keyfork_derive_openpgp::derive(
|
keyfork_derive_openpgp::derive(
|
||||||
&xprv,
|
xprv,
|
||||||
&[KeyFlags::empty().set_certification().set_signing()],
|
&[KeyFlags::empty().set_certification().set_signing()],
|
||||||
&userid,
|
&userid,
|
||||||
)
|
)
|
||||||
|
@ -442,14 +422,14 @@ impl Format for OpenPGP {
|
||||||
&self,
|
&self,
|
||||||
private_keys: Option<Self::PrivateKeyData>,
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
encrypted_data: &[Self::EncryptedData],
|
encrypted_data: &[Self::EncryptedData],
|
||||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||||
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
|
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
|
||||||
// Be as liberal as possible when decrypting.
|
// Be as liberal as possible when decrypting.
|
||||||
// We don't want to invalidate someone's keys just because the old sig expired.
|
// We don't want to invalidate someone's keys just because the old sig expired.
|
||||||
let policy = NullPolicy::new();
|
let policy = NullPolicy::new();
|
||||||
|
|
||||||
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
|
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
|
||||||
let mut manager = SmartcardManager::new(prompt.clone());
|
let mut manager = SmartcardManager::new(prompt.clone())?;
|
||||||
|
|
||||||
let mut encrypted_messages = encrypted_data.iter();
|
let mut encrypted_messages = encrypted_data.iter();
|
||||||
|
|
||||||
|
@ -480,9 +460,9 @@ impl Format for OpenPGP {
|
||||||
|
|
||||||
let left_from_threshold = threshold as usize - decrypted_messages.len();
|
let left_from_threshold = threshold as usize - decrypted_messages.len();
|
||||||
if left_from_threshold > 0 {
|
if left_from_threshold > 0 {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let new_messages = decrypt_with_manager(
|
let new_messages = decrypt_with_manager(
|
||||||
u8::try_from(left_from_threshold)
|
left_from_threshold as u8,
|
||||||
.expect(bug!("threshold too large: {}", left_from_threshold)),
|
|
||||||
&mut messages,
|
&mut messages,
|
||||||
&certs,
|
&certs,
|
||||||
&policy,
|
&policy,
|
||||||
|
@ -503,12 +483,12 @@ impl Format for OpenPGP {
|
||||||
&self,
|
&self,
|
||||||
private_keys: Option<Self::PrivateKeyData>,
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
encrypted_data: &[Self::EncryptedData],
|
encrypted_data: &[Self::EncryptedData],
|
||||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||||
) -> std::result::Result<(Share, u8), Self::Error> {
|
) -> std::result::Result<(Share, u8), Self::Error> {
|
||||||
let policy = NullPolicy::new();
|
let policy = NullPolicy::new();
|
||||||
|
|
||||||
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
|
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
|
||||||
let mut manager = SmartcardManager::new(prompt.clone());
|
let mut manager = SmartcardManager::new(prompt.clone())?;
|
||||||
|
|
||||||
let mut encrypted_messages = encrypted_data.iter();
|
let mut encrypted_messages = encrypted_data.iter();
|
||||||
|
|
||||||
|
@ -547,44 +527,24 @@ impl Format for OpenPGP {
|
||||||
|
|
||||||
panic!("unable to decrypt shard");
|
panic!("unable to decrypt shard");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn decrypt_metadata(
|
impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &Path {
|
||||||
&self,
|
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
|
||||||
private_keys: Option<Self::PrivateKeyData>,
|
OpenPGP::<P>::discover_certs(self)
|
||||||
encrypted_data: &[Self::EncryptedData],
|
}
|
||||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
|
||||||
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error> {
|
|
||||||
let policy = NullPolicy::new();
|
|
||||||
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
|
|
||||||
let mut manager = SmartcardManager::new(prompt.clone());
|
|
||||||
let mut encrypted_messages = encrypted_data.iter();
|
|
||||||
|
|
||||||
let metadata = encrypted_messages
|
fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
|
||||||
.next()
|
OpenPGP::<P>::discover_certs(self)
|
||||||
.expect(bug!(METADATA_MESSAGE_MISSING));
|
|
||||||
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
|
|
||||||
|
|
||||||
let (threshold, _root_cert, certs) = decode_metadata_v1(&metadata_content)?;
|
|
||||||
Ok((threshold, certs))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyDiscovery<OpenPGP> for &Path {
|
impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &[Cert] {
|
||||||
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
|
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
|
||||||
OpenPGP::discover_certs(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
|
|
||||||
OpenPGP::discover_certs(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyDiscovery<OpenPGP> for &[Cert] {
|
|
||||||
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
|
|
||||||
Ok(self.to_vec())
|
Ok(self.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
|
fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
|
||||||
Ok(self.to_vec())
|
Ok(self.to_vec())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -597,8 +557,7 @@ fn get_encryption_keys<'a>(
|
||||||
openpgp::packet::key::UnspecifiedRole,
|
openpgp::packet::key::UnspecifiedRole,
|
||||||
> {
|
> {
|
||||||
cert.keys()
|
cert.keys()
|
||||||
// NOTE: this causes complications on Airgap systems
|
.alive()
|
||||||
// .alive()
|
|
||||||
.revoked(false)
|
.revoked(false)
|
||||||
.supported()
|
.supported()
|
||||||
.for_storage_encryption()
|
.for_storage_encryption()
|
||||||
|
@ -646,12 +605,12 @@ fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
|
||||||
|
|
||||||
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
|
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
|
||||||
// single message.
|
// single message.
|
||||||
fn decrypt_with_manager(
|
fn decrypt_with_manager<P: PromptHandler>(
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
messages: &mut HashMap<KeyID, EncryptedMessage>,
|
messages: &mut HashMap<KeyID, EncryptedMessage>,
|
||||||
certs: &[Cert],
|
certs: &[Cert],
|
||||||
policy: &dyn Policy,
|
policy: &dyn Policy,
|
||||||
manager: &mut SmartcardManager,
|
manager: &mut SmartcardManager<P>,
|
||||||
) -> Result<HashMap<KeyID, Vec<u8>>> {
|
) -> Result<HashMap<KeyID, Vec<u8>>> {
|
||||||
let mut decrypted_messages = HashMap::new();
|
let mut decrypted_messages = HashMap::new();
|
||||||
|
|
||||||
|
@ -696,11 +655,11 @@ fn decrypt_with_manager(
|
||||||
|
|
||||||
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
|
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
|
||||||
// decrypt messages with.
|
// decrypt messages with.
|
||||||
fn decrypt_with_keyring(
|
fn decrypt_with_keyring<P: PromptHandler>(
|
||||||
messages: &mut HashMap<KeyID, EncryptedMessage>,
|
messages: &mut HashMap<KeyID, EncryptedMessage>,
|
||||||
certs: &[Cert],
|
certs: &[Cert],
|
||||||
policy: &NullPolicy,
|
policy: &NullPolicy,
|
||||||
keyring: &mut Keyring,
|
keyring: &mut Keyring<P>,
|
||||||
) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
|
) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
|
||||||
let mut decrypted_messages = HashMap::new();
|
let mut decrypted_messages = HashMap::new();
|
||||||
|
|
||||||
|
@ -730,11 +689,11 @@ fn decrypt_with_keyring(
|
||||||
Ok(decrypted_messages)
|
Ok(decrypted_messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypt_metadata(
|
fn decrypt_metadata<P: PromptHandler>(
|
||||||
message: &EncryptedMessage,
|
message: &EncryptedMessage,
|
||||||
policy: &NullPolicy,
|
policy: &NullPolicy,
|
||||||
keyring: &mut Keyring,
|
keyring: &mut Keyring<P>,
|
||||||
manager: &mut SmartcardManager,
|
manager: &mut SmartcardManager<P>,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
Ok(if keyring.is_empty() {
|
Ok(if keyring.is_empty() {
|
||||||
manager.load_any_card()?;
|
manager.load_any_card()?;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use std::{rc::Rc, sync::Mutex};
|
#![allow(clippy::expect_fun_call)]
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||||
use keyfork_prompt::{Error as PromptError, PromptHandler};
|
use keyfork_prompt::{Error as PromptError, PromptHandler};
|
||||||
|
@ -23,19 +25,21 @@ pub enum Error {
|
||||||
Prompt(#[from] PromptError),
|
Prompt(#[from] PromptError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Keyring {
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub struct Keyring<P: PromptHandler> {
|
||||||
full_certs: Vec<Cert>,
|
full_certs: Vec<Cert>,
|
||||||
root: Option<Cert>,
|
root: Option<Cert>,
|
||||||
pm: Rc<Mutex<Box<dyn PromptHandler>>>,
|
pm: Arc<Mutex<P>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Keyring {
|
impl<P: PromptHandler> Keyring<P> {
|
||||||
pub fn new(certs: impl AsRef<[Cert]>, p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Self {
|
pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
|
||||||
Self {
|
Ok(Self {
|
||||||
full_certs: certs.as_ref().to_vec(),
|
full_certs: certs.as_ref().to_vec(),
|
||||||
root: Option::default(),
|
root: Default::default(),
|
||||||
pm: p,
|
pm: p,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
|
@ -58,7 +62,7 @@ impl Keyring {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VerificationHelper for &mut Keyring {
|
impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
|
||||||
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
|
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
|
||||||
Ok(ids
|
Ok(ids
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -80,23 +84,13 @@ impl VerificationHelper for &mut Keyring {
|
||||||
aead_algo,
|
aead_algo,
|
||||||
} => {}
|
} => {}
|
||||||
MessageLayer::SignatureGroup { results } => {
|
MessageLayer::SignatureGroup { results } => {
|
||||||
match &results[..] {
|
|
||||||
[Ok(_)] => {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
|
||||||
// return Err(e.context("Invalid signature"));
|
|
||||||
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
for result in results {
|
for result in results {
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
|
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
||||||
|
// return Err(e.context("Invalid signature"));
|
||||||
return Err(anyhow::anyhow!("Invalid signature: {e}"));
|
return Err(anyhow::anyhow!("Invalid signature: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +98,7 @@ impl VerificationHelper for &mut Keyring {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecryptionHelper for &mut Keyring {
|
impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
|
||||||
fn decrypt<D>(
|
fn decrypt<D>(
|
||||||
&mut self,
|
&mut self,
|
||||||
pkesks: &[PKESK],
|
pkesks: &[PKESK],
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
|
#![allow(clippy::expect_fun_call)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
rc::Rc,
|
sync::{Arc, Mutex},
|
||||||
sync::Mutex,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
prompt_validated_passphrase,
|
|
||||||
validators::{PinValidator, Validator},
|
validators::{PinValidator, Validator},
|
||||||
Error as PromptError, Message, PromptHandler,
|
Error as PromptError, Message, PromptHandler,
|
||||||
};
|
};
|
||||||
|
@ -71,21 +71,21 @@ fn format_name(input: impl AsRef<str>) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub struct SmartcardManager {
|
pub struct SmartcardManager<P: PromptHandler> {
|
||||||
current_card: Option<Card<Open>>,
|
current_card: Option<Card<Open>>,
|
||||||
root: Option<Cert>,
|
root: Option<Cert>,
|
||||||
pm: Rc<Mutex<Box<dyn PromptHandler>>>,
|
pm: Arc<Mutex<P>>,
|
||||||
pin_cache: HashMap<Fingerprint, String>,
|
pin_cache: HashMap<Fingerprint, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SmartcardManager {
|
impl<P: PromptHandler> SmartcardManager<P> {
|
||||||
pub fn new(p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Self {
|
pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
|
||||||
Self {
|
Ok(Self {
|
||||||
current_card: None,
|
current_card: None,
|
||||||
root: None,
|
root: None,
|
||||||
pm: p,
|
pm: p,
|
||||||
pin_cache: HashMap::default(),
|
pin_cache: Default::default(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the root cert, returning the old cert
|
// Sets the root cert, returning the old cert
|
||||||
|
@ -173,11 +173,12 @@ impl SmartcardManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VerificationHelper for &mut SmartcardManager {
|
impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
|
||||||
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
|
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
|
||||||
|
#[allow(clippy::flat_map_option)]
|
||||||
Ok(ids
|
Ok(ids
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
|
.flat_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
@ -192,23 +193,12 @@ impl VerificationHelper for &mut SmartcardManager {
|
||||||
aead_algo,
|
aead_algo,
|
||||||
} => {}
|
} => {}
|
||||||
MessageLayer::SignatureGroup { results } => {
|
MessageLayer::SignatureGroup { results } => {
|
||||||
match &results[..] {
|
|
||||||
[Ok(_)] => {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
|
||||||
// return Err(e.context("Invalid signature"));
|
|
||||||
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
for result in results {
|
for result in results {
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
return Err(anyhow::anyhow!("Invalid signature: {e}"));
|
// FIXME: anyhow leak
|
||||||
|
return Err(anyhow::anyhow!("Verification error: {}", e.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,7 +206,7 @@ impl VerificationHelper for &mut SmartcardManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecryptionHelper for &mut SmartcardManager {
|
impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
|
||||||
fn decrypt<D>(
|
fn decrypt<D>(
|
||||||
&mut self,
|
&mut self,
|
||||||
pkesks: &[PKESK],
|
pkesks: &[PKESK],
|
||||||
|
@ -274,11 +264,15 @@ impl DecryptionHelper for &mut SmartcardManager {
|
||||||
} else {
|
} else {
|
||||||
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
|
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
|
||||||
};
|
};
|
||||||
let mut prompt = self.pm.lock().expect(bug!(POISONED_MUTEX));
|
let temp_pin =
|
||||||
let temp_pin = prompt_validated_passphrase(&mut **prompt, &message, 3, &pin_validator)?;
|
self.pm
|
||||||
|
.lock()
|
||||||
|
.expect(bug!(POISONED_MUTEX))
|
||||||
|
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
|
||||||
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
|
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
|
||||||
match verification_status {
|
match verification_status {
|
||||||
Ok(()) => {
|
#[allow(clippy::ignored_unit_patterns)]
|
||||||
|
Ok(_) => {
|
||||||
self.pin_cache.insert(fp.clone(), temp_pin.clone());
|
self.pin_cache.insert(fp.clone(), temp_pin.clone());
|
||||||
pin.replace(temp_pin);
|
pin.replace(temp_pin);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork"
|
name = "keyfork"
|
||||||
version = "0.3.3"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
"completion",
|
"completion",
|
||||||
|
@ -26,30 +23,24 @@ sequoia-crypto-backend-openssl = ["sequoia-openpgp/crypto-openssl"]
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-bin = { workspace = true }
|
keyfork-bin = { version = "0.1.0", path = "../util/keyfork-bin", registry = "distrust" }
|
||||||
keyforkd = { workspace = true, features = ["tracing"] }
|
keyforkd = { version = "0.1.0", path = "../daemon/keyforkd", features = ["tracing"], registry = "distrust" }
|
||||||
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
|
keyforkd-client = { version = "0.1.0", path = "../daemon/keyforkd-client", default-features = false, features = ["ed25519"], registry = "distrust" }
|
||||||
keyfork-derive-util = { workspace = true, default-features = true }
|
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp", registry = "distrust" }
|
||||||
keyfork-derive-openpgp = { workspace = true }
|
keyfork-derive-util = { version = "0.1.0", path = "../derive/keyfork-derive-util", default-features = false, features = ["ed25519"], registry = "distrust" }
|
||||||
keyfork-derive-path-data = { workspace = true }
|
keyfork-entropy = { version = "0.1.0", path = "../util/keyfork-entropy", registry = "distrust" }
|
||||||
keyfork-entropy = { workspace = true }
|
keyfork-mnemonic-util = { version = "0.2.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
|
||||||
keyfork-mnemonic = { workspace = true }
|
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", registry = "distrust" }
|
||||||
keyfork-prompt = { workspace = true }
|
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false, registry = "distrust" }
|
||||||
keyfork-qrcode = { workspace = true, default-features = false }
|
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"], registry = "distrust" }
|
||||||
keyfork-shard = { workspace = true, default-features = false, features = ["openpgp", "openpgp-card", "qrcode"] }
|
smex = { version = "0.1.0", path = "../util/smex", registry = "distrust" }
|
||||||
smex = { workspace = true }
|
|
||||||
|
|
||||||
clap = { version = "4.4.2", features = ["derive", "env", "wrap_help"] }
|
clap = { version = "4.4.2", features = ["derive", "env", "wrap_help"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.48"
|
||||||
serde = { workspace = true }
|
serde = { version = "1.0.192", features = ["derive"] }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
tokio = { version = "1.35.1", default-features = false, features = ["rt-multi-thread"] }
|
||||||
card-backend-pcsc = { workspace = true }
|
card-backend-pcsc = "0.5.0"
|
||||||
openpgp-card-sequoia = { workspace = true }
|
openpgp-card-sequoia = { version = "0.2.0", default-features = false }
|
||||||
openpgp-card = { workspace = true }
|
openpgp-card = "0.4.1"
|
||||||
clap_complete = { version = "4.4.6", optional = true }
|
clap_complete = { version = "4.4.6", optional = true }
|
||||||
sequoia-openpgp = { workspace = true }
|
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] }
|
||||||
keyforkd-models.workspace = true
|
|
||||||
base64.workspace = true
|
|
||||||
nix = { version = "0.29.0", default-features = false, features = ["process"] }
|
|
||||||
shlex = "1.3.0"
|
|
||||||
tempfile.workspace = true
|
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
//! Extensions to clap.
|
|
||||||
|
|
||||||
use std::{collections::HashMap, str::FromStr};
|
|
||||||
|
|
||||||
/// An error that occurred while parsing a base value or its
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ValueParseError {
|
|
||||||
/// No value was given; the required type could not be parsed.
|
|
||||||
#[error("No value was given")]
|
|
||||||
NoValue,
|
|
||||||
|
|
||||||
/// The first value could not properly be parsed.
|
|
||||||
#[error("Could not parse first value: {0}")]
|
|
||||||
BadParse(String),
|
|
||||||
|
|
||||||
/// Additional values were added, but not in a key=value format.
|
|
||||||
#[error("A key-value pair was not given")]
|
|
||||||
BadKeyValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A helper struct to parse key-value arguments, without any prior argument.
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct Options {
|
|
||||||
/// The values provided.
|
|
||||||
pub values: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Options {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let mut iter = self.values.iter().peekable();
|
|
||||||
while let Some((key, value)) = iter.next() {
|
|
||||||
write!(f, "{key}={value}")?;
|
|
||||||
if iter.peek().is_some() {
|
|
||||||
write!(f, ",")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Options {
|
|
||||||
type Err = ValueParseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
if s.is_empty() {
|
|
||||||
return Ok(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,196 +1,45 @@
|
||||||
use super::{create, Keyfork};
|
use super::Keyfork;
|
||||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand};
|
||||||
use std::{fmt::Display, io::Write, path::PathBuf};
|
|
||||||
|
|
||||||
use keyfork_derive_openpgp::openpgp::{
|
use keyfork_derive_openpgp::{
|
||||||
|
openpgp::{
|
||||||
armor::{Kind, Writer},
|
armor::{Kind, Writer},
|
||||||
packet::UserID,
|
packet::UserID,
|
||||||
serialize::Marshal,
|
serialize::Marshal,
|
||||||
types::KeyFlags,
|
types::KeyFlags,
|
||||||
Cert,
|
},
|
||||||
};
|
XPrvKey,
|
||||||
use keyfork_derive_path_data::paths;
|
|
||||||
use keyfork_derive_util::{
|
|
||||||
request::DerivationAlgorithm, DerivationIndex, DerivationPath, ExtendedPrivateKey as XPrv,
|
|
||||||
IndexError, PrivateKey,
|
|
||||||
};
|
};
|
||||||
|
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||||
use keyforkd_client::Client;
|
use keyforkd_client::Client;
|
||||||
|
|
||||||
type OptWrite = Option<Box<dyn Write>>;
|
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
pub trait Deriver {
|
|
||||||
type Prv: PrivateKey + Clone;
|
|
||||||
const DERIVATION_ALGORITHM: DerivationAlgorithm;
|
|
||||||
|
|
||||||
fn derivation_path(&self) -> DerivationPath;
|
|
||||||
|
|
||||||
fn derive_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()>;
|
|
||||||
|
|
||||||
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
pub enum DeriveSubcommands {
|
pub enum DeriveSubcommands {
|
||||||
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
|
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
|
||||||
/// ASCII Armor, a format usable by most programs using OpenPGP.
|
/// ASCII Armor, a format usable by most programs using OpenPGP.
|
||||||
///
|
///
|
||||||
/// Certificates are created with a default expiration of one day, but may be configured to
|
/// The key is generated with a 24-hour expiration time. The operation to set the expiration
|
||||||
/// expire later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as
|
/// time to a higher value is left to the user to ensure the key is usable by the user.
|
||||||
/// "15d" (15 days), "1m" (one month), or "2y" (two years).
|
|
||||||
///
|
|
||||||
/// It is recommended to use the default expiration of one day and to change the expiration
|
|
||||||
/// using an external utility, to ensure the Certify key is usable.
|
|
||||||
#[command(name = "openpgp")]
|
#[command(name = "openpgp")]
|
||||||
OpenPGP(OpenPGP),
|
OpenPGP {
|
||||||
|
|
||||||
/// Derive an Ed25519 key for a specific algorithm, in a given format.
|
|
||||||
Key(Key),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derivation path to use when deriving OpenPGP keys.
|
|
||||||
#[derive(ValueEnum, Clone, Debug, Default)]
|
|
||||||
pub enum Path {
|
|
||||||
/// The default derivation path; no additional index is used.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
|
|
||||||
/// The Disaster Recovery index.
|
|
||||||
DisasterRecovery,
|
|
||||||
|
|
||||||
/// 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.
|
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
||||||
user_id: String,
|
user_id: String,
|
||||||
|
},
|
||||||
/// Derivation path to use when deriving OpenPGP keys.
|
|
||||||
#[arg(long, required = false, default_value = "default")]
|
|
||||||
derivation_path: Path,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A format for exporting a key.
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
|
||||||
pub enum KeyFormat {
|
|
||||||
Hex,
|
|
||||||
Base64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An invalid slug was provided.
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum InvalidSlug {
|
|
||||||
/// The value provided was longer than four bytes.
|
|
||||||
#[error("The value provided was longer than four bytes: {0}")]
|
|
||||||
InvalidSize(usize),
|
|
||||||
|
|
||||||
/// The value provided was higher than the maximum derivation index.
|
|
||||||
#[error("The value provided was higher than the maximum derivation index: {0}")]
|
|
||||||
InvalidValue(#[from] IndexError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Slug(DerivationIndex);
|
|
||||||
|
|
||||||
impl std::str::FromStr for Slug {
|
|
||||||
type Err = InvalidSlug;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
|
||||||
let bytes = s.as_bytes();
|
|
||||||
let mut parseable_bytes = [0u8; 4];
|
|
||||||
if bytes.len() <= 4 && !bytes.is_empty() {
|
|
||||||
parseable_bytes[(4 - bytes.len())..4].copy_from_slice(bytes);
|
|
||||||
} else {
|
|
||||||
return Err(InvalidSlug::InvalidSize(bytes.len()));
|
|
||||||
}
|
|
||||||
let slug = u32::from_be_bytes(parseable_bytes);
|
|
||||||
let index = DerivationIndex::new(slug, true)?;
|
|
||||||
Ok(Slug(index))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Slug {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
#[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 {
|
impl DeriveSubcommands {
|
||||||
fn handle(&self, account: DerivationIndex, is_public: bool, writer: OptWrite) -> Result<()> {
|
fn handle(&self, account: DerivationIndex) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
DeriveSubcommands::OpenPGP(opgp) => {
|
DeriveSubcommands::OpenPGP { user_id } => {
|
||||||
let path = opgp.derivation_path();
|
let mut pgp_u32 = [0u8; 4];
|
||||||
let xprv = Client::discover_socket()?
|
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
|
||||||
.request_xprv::<<OpenPGP as Deriver>::Prv>(&path.chain_push(account))?;
|
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||||
if is_public {
|
let path = DerivationPath::default()
|
||||||
opgp.derive_public_with_xprv(writer, &xprv)
|
.chain_push(chain)
|
||||||
} else {
|
.chain_push(account);
|
||||||
opgp.derive_with_xprv(writer, &xprv)
|
// TODO: should this be customizable?
|
||||||
}
|
|
||||||
}
|
|
||||||
DeriveSubcommands::Key(key) => {
|
|
||||||
let path = key.derivation_path();
|
|
||||||
let xprv = Client::discover_socket()?
|
|
||||||
.request_xprv::<<Key as Deriver>::Prv>(&path.chain_push(account))?;
|
|
||||||
if is_public {
|
|
||||||
key.derive_public_with_xprv(writer, &xprv)
|
|
||||||
} else {
|
|
||||||
key.derive_with_xprv(writer, &xprv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OpenPGP {
|
|
||||||
fn cert_from_xprv(&self, xprv: &keyfork_derive_openpgp::XPrv) -> Result<Cert> {
|
|
||||||
let subkeys = vec![
|
let subkeys = vec![
|
||||||
KeyFlags::empty().set_certification(),
|
KeyFlags::empty().set_certification(),
|
||||||
KeyFlags::empty().set_signing(),
|
KeyFlags::empty().set_signing(),
|
||||||
|
@ -199,94 +48,20 @@ impl OpenPGP {
|
||||||
.set_storage_encryption(),
|
.set_storage_encryption(),
|
||||||
KeyFlags::empty().set_authentication(),
|
KeyFlags::empty().set_authentication(),
|
||||||
];
|
];
|
||||||
|
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
|
||||||
|
let default_userid = UserID::from(user_id.as_str());
|
||||||
|
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
|
||||||
|
|
||||||
let userid = UserID::from(&*self.user_id);
|
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
|
||||||
keyfork_derive_openpgp::derive(xprv, &subkeys, &userid).map_err(Into::into)
|
|
||||||
|
for packet in cert.into_packets() {
|
||||||
|
packet.serialize(&mut w)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.finalize()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deriver for OpenPGP {
|
|
||||||
type Prv = keyfork_derive_openpgp::XPrvKey;
|
|
||||||
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
|
|
||||||
|
|
||||||
fn derivation_path(&self) -> DerivationPath {
|
|
||||||
self.derivation_path.derivation_path()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()> {
|
|
||||||
let cert = self.cert_from_xprv(xprv)?;
|
|
||||||
let writer = 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 writer)?;
|
|
||||||
}
|
|
||||||
writer.finalize()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deriver for Key {
|
|
||||||
// HACK: We're abusing that we use the same key as OpenPGP. Maybe we should use ed25519_dalek.
|
|
||||||
type Prv = keyfork_derive_openpgp::XPrvKey;
|
|
||||||
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
|
|
||||||
|
|
||||||
fn derivation_path(&self) -> DerivationPath {
|
|
||||||
DerivationPath::default().chain_push(self.slug.0.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()> {
|
|
||||||
let (formatted, ext) = match self.format {
|
|
||||||
KeyFormat::Hex => (smex::encode(xprv.private_key().to_bytes()), "hex"),
|
|
||||||
KeyFormat::Base64 => {
|
|
||||||
use base64::prelude::*;
|
|
||||||
(BASE64_STANDARD.encode(xprv.private_key().to_bytes()), "b64")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let filename =
|
|
||||||
PathBuf::from(smex::encode(xprv.public_key().to_bytes())).with_extension(ext);
|
|
||||||
if let Some(mut writer) = writer {
|
|
||||||
writeln!(writer, "{formatted}")?;
|
|
||||||
} else {
|
|
||||||
std::fs::write(&filename, formatted)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()> {
|
|
||||||
let (formatted, ext) = match self.format {
|
|
||||||
KeyFormat::Hex => (smex::encode(xprv.public_key().to_bytes()), "hex"),
|
|
||||||
KeyFormat::Base64 => {
|
|
||||||
use base64::prelude::*;
|
|
||||||
(BASE64_STANDARD.encode(xprv.public_key().to_bytes()), "b64")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let filename =
|
|
||||||
PathBuf::from(smex::encode(xprv.public_key().to_bytes())).with_extension(ext);
|
|
||||||
if let Some(mut writer) = writer {
|
|
||||||
writeln!(writer, "{formatted}")?;
|
|
||||||
} else {
|
|
||||||
std::fs::write(&filename, formatted)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -294,7 +69,7 @@ impl Deriver for Key {
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
pub struct Derive {
|
pub struct Derive {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub(crate) command: DeriveSubcommands,
|
command: DeriveSubcommands,
|
||||||
|
|
||||||
/// Account ID. Required for all derivations.
|
/// Account ID. Required for all derivations.
|
||||||
///
|
///
|
||||||
|
@ -302,45 +77,12 @@ pub struct Derive {
|
||||||
/// account ID can often come as a hindrance in the future. As such, it is always required. If
|
/// account ID can often come as a hindrance in the future. As such, it is always required. If
|
||||||
/// the account ID is not relevant, it is assumed to be `0`.
|
/// the account ID is not relevant, it is assumed to be `0`.
|
||||||
#[arg(long, global = true, default_value = "0")]
|
#[arg(long, global = true, default_value = "0")]
|
||||||
pub(crate) account_id: u32,
|
account_id: u32,
|
||||||
|
|
||||||
/// Whether derivation should return the public key or a private key.
|
|
||||||
#[arg(long, global = true)]
|
|
||||||
pub(crate) public: bool,
|
|
||||||
|
|
||||||
/// Whether the file should be written to standard output, or to a filename generated by the
|
|
||||||
/// derivation system.
|
|
||||||
#[arg(long, global = true, default_value = "false")]
|
|
||||||
pub to_stdout: bool,
|
|
||||||
|
|
||||||
/// The file to write the derived public key to, if not standard output. If omitted, a filename
|
|
||||||
/// will be generated by the relevant deriver.
|
|
||||||
#[arg(long, global = true, conflicts_with = "to_stdout")]
|
|
||||||
pub output: Option<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Derive {
|
impl Derive {
|
||||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||||
let account = DerivationIndex::new(self.account_id, true)?;
|
let account = DerivationIndex::new(self.account_id, true)?;
|
||||||
let writer = if let Some(output) = self.output.as_deref() {
|
self.command.handle(account)
|
||||||
Some(Box::new(std::fs::File::create(output)?) as Box<dyn Write>)
|
|
||||||
} else if self.to_stdout {
|
|
||||||
Some(Box::new(std::io::stdout()) as Box<dyn Write>)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
self.command.handle(account, self.public, writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Derive {
|
|
||||||
type Err = clap::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
|
||||||
Derive::try_parse_from(
|
|
||||||
[String::from("derive")]
|
|
||||||
.into_iter()
|
|
||||||
.chain(shlex::Shlex::new(s)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,6 @@
|
||||||
use super::{
|
use super::Keyfork;
|
||||||
create,
|
|
||||||
derive::{self, Deriver},
|
|
||||||
provision, Keyfork,
|
|
||||||
};
|
|
||||||
use crate::{clap_ext::*, config, openpgp_card::factory_reset_current_card};
|
|
||||||
use card_backend_pcsc::PcscBackend;
|
|
||||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||||
use std::{
|
use std::fmt::Display;
|
||||||
collections::{HashMap, 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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub enum SeedSize {
|
pub enum SeedSize {
|
||||||
|
@ -95,7 +59,6 @@ impl From<&SeedSize> for usize {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, thiserror::Error)]
|
#[derive(Clone, Debug, thiserror::Error)]
|
||||||
pub enum MnemonicSeedSourceParseError {
|
pub enum MnemonicSeedSourceParseError {
|
||||||
#[error("Expected one of system, playing, tarot, dice")]
|
#[error("Expected one of system, playing, tarot, dice")]
|
||||||
|
@ -133,41 +96,24 @@ impl std::str::FromStr for MnemonicSeedSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MnemonicSeedSource {
|
impl MnemonicSeedSource {
|
||||||
pub fn handle(
|
pub fn handle(&self, size: &SeedSize) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
&self,
|
|
||||||
size: &SeedSize,
|
|
||||||
) -> Result<keyfork_mnemonic::Mnemonic, Box<dyn std::error::Error>> {
|
|
||||||
let size = match size {
|
let size = match size {
|
||||||
SeedSize::Bits128 => 128,
|
SeedSize::Bits128 => 128,
|
||||||
SeedSize::Bits256 => 256,
|
SeedSize::Bits256 => 256,
|
||||||
};
|
};
|
||||||
let seed = match self {
|
let seed = match self {
|
||||||
MnemonicSeedSource::System => keyfork_entropy::generate_entropy_of_size(size / 8)?,
|
MnemonicSeedSource::System => {
|
||||||
|
keyfork_entropy::generate_entropy_of_size(size / 8)?
|
||||||
|
}
|
||||||
MnemonicSeedSource::Playing => todo!(),
|
MnemonicSeedSource::Playing => todo!(),
|
||||||
MnemonicSeedSource::Tarot => todo!(),
|
MnemonicSeedSource::Tarot => todo!(),
|
||||||
MnemonicSeedSource::Dice => todo!(),
|
MnemonicSeedSource::Dice => todo!(),
|
||||||
};
|
};
|
||||||
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?;
|
let mnemonic = keyfork_mnemonic_util::Mnemonic::from_bytes(&seed)?;
|
||||||
Ok(mnemonic)
|
Ok(mnemonic.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error occurred while performing an operation.
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
/// An error occurred when interacting iwth a file.
|
|
||||||
#[error("Error while performing IO operation on: {1}")]
|
|
||||||
IOContext(#[source] std::io::Error, PathBuf),
|
|
||||||
|
|
||||||
/// A required option was not provided.
|
|
||||||
#[error("The required option {0} was not provided")]
|
|
||||||
MissingOption(&'static str),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn context_stub(path: &Path) -> impl Fn(std::io::Error) -> Error + use<'_> {
|
|
||||||
|e| Error::IOContext(e, path.to_path_buf())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
pub enum MnemonicSubcommands {
|
pub enum MnemonicSubcommands {
|
||||||
/// Generate a mnemonic using a given entropy source.
|
/// Generate a mnemonic using a given entropy source.
|
||||||
|
@ -178,10 +124,6 @@ pub enum MnemonicSubcommands {
|
||||||
/// method of generating a seed using system entropy, as well as various forms of loading
|
/// method of generating a seed using system entropy, as well as various forms of loading
|
||||||
/// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location
|
/// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location
|
||||||
/// (such as a Trezor "recovery seed card") and never persisted digitally.
|
/// (such as a Trezor "recovery seed card") and never persisted digitally.
|
||||||
///
|
|
||||||
/// When using the `--shard`, `--shard-to`, `--encrypt-to`, and `--encrypt-to-self` +
|
|
||||||
/// `--provision` arguments, the mnemonic is _not_ sent to output. The data for the mnemonic is
|
|
||||||
/// then either split using Keyfork Shard or encrypted using OpenPGP.
|
|
||||||
Generate {
|
Generate {
|
||||||
/// The source from where a seed is created.
|
/// The source from where a seed is created.
|
||||||
#[arg(long, value_enum, default_value_t = Default::default())]
|
#[arg(long, value_enum, default_value_t = Default::default())]
|
||||||
|
@ -190,758 +132,17 @@ pub enum MnemonicSubcommands {
|
||||||
/// The size of the mnemonic, in bits.
|
/// The size of the mnemonic, in bits.
|
||||||
#[arg(long, default_value_t = Default::default())]
|
#[arg(long, default_value_t = Default::default())]
|
||||||
size: SeedSize,
|
size: SeedSize,
|
||||||
|
|
||||||
/// Derive a key. By default, a private key is derived. Unlike other arguments in this
|
|
||||||
/// file, arguments must be passed using the format similar to the CLI. For example:
|
|
||||||
/// `--derive='openpgp --public "Ryan Heywood <ryan@distrust.co>"'` would be synonymous
|
|
||||||
/// with starting the Keyfork daemon with the provided mnemonic, then running
|
|
||||||
/// `keyfork derive openpgp --public "Ryan Heywood <ryan@distrust.co>"`.
|
|
||||||
///
|
|
||||||
/// The output of the derived key is written to a filename based on the content of the key;
|
|
||||||
/// for instance, OpenPGP keys are written to a file identifiable by the certificate's
|
|
||||||
/// fingerprint. This behavior can be changed by using the `--to-stdout` or `--output`
|
|
||||||
/// modifiers to the `--derive` command.
|
|
||||||
#[arg(long)]
|
|
||||||
derive: Option<derive::Derive>,
|
|
||||||
|
|
||||||
/// Encrypt the mnemonic to an OpenPGP certificate in the provided path.
|
|
||||||
///
|
|
||||||
/// When given arguments in the format `--encrypt-to input.asc,output=output.asc`, the
|
|
||||||
/// output of the encryption will be written to `output.asc`. Otherwise, the default
|
|
||||||
/// behavior is to write the output to `input.enc.asc`. If the output file already exists,
|
|
||||||
/// it will not be overwritten, and the command will exit unsuccessfully.
|
|
||||||
#[arg(long)]
|
|
||||||
encrypt_to: Option<Vec<ValueWithOptions<PathBuf>>>,
|
|
||||||
|
|
||||||
/// Shard the mnemonic to the certificates in the given Shardfile. Requires a decrypt
|
|
||||||
/// operation on the Shardfile to access the metadata and certificates.
|
|
||||||
///
|
|
||||||
/// When given arguments in the format `--shard-to input.asc,output=output.asc`, the
|
|
||||||
/// output of the encryption will be written to `output.asc`. Otherwise, the default
|
|
||||||
/// behavior is to write the output to `input.new.asc`. If the output file already exists,
|
|
||||||
/// it will not be overwritten, and the command will exit unsuccessfully.
|
|
||||||
#[arg(long)]
|
|
||||||
shard_to: Option<Vec<ValueWithOptions<PathBuf>>>,
|
|
||||||
|
|
||||||
/// Shard the mnemonic to the provided certificates.
|
|
||||||
///
|
|
||||||
/// The following additional arguments are available:
|
|
||||||
///
|
|
||||||
/// * `threshold`, m: the minimum amount of shares required to reconstitute the shard. By
|
|
||||||
/// default, this is the amount of certificates provided.
|
|
||||||
///
|
|
||||||
/// * `max`, n: the maximum amount of shares. When provided, this is used to ensure the
|
|
||||||
/// certificate count is correct. This is required when using `threshold` or `m`.
|
|
||||||
///
|
|
||||||
/// * `output`: the file to write the generated Shardfile to. By default, assuming the
|
|
||||||
/// certificate input is `input.asc`, the generated Shardfile would be written to
|
|
||||||
/// `input.shard.asc`.
|
|
||||||
#[arg(long)]
|
|
||||||
shard: Option<Vec<ValueWithOptions<PathBuf>>>,
|
|
||||||
|
|
||||||
/// Encrypt the mnemonic to an OpenPGP certificate derived from the mnemonic, writing the
|
|
||||||
/// output to the provided path. This command must be run in combination with
|
|
||||||
/// `--provision openpgp-card`, `--derive openpgp`, or another OpenPGP key derivation
|
|
||||||
/// mechanism, to ensure the generated mnemonic would be decryptable.
|
|
||||||
///
|
|
||||||
/// When used in combination with `--derive` or `--provision` with OpenPGP configurations,
|
|
||||||
/// the default behavior is to encrypt the mnemonic to all derived and provisioned
|
|
||||||
/// accounts. By default, the account `0` is used.
|
|
||||||
#[arg(long)]
|
|
||||||
encrypt_to_self: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Shard the mnemonic to freshly-generated OpenPGP certificates derived from the mnemonic,
|
|
||||||
/// writing the output to the provided path, and provisioning OpenPGP smartcards with the
|
|
||||||
/// new certificates.
|
|
||||||
///
|
|
||||||
/// The following additional arguments are required:
|
|
||||||
///
|
|
||||||
/// * `threshold`, m: the minimum amount of shares required to reconstitute the shard.
|
|
||||||
///
|
|
||||||
/// * `max`, n: the maximum amount of shares.
|
|
||||||
///
|
|
||||||
/// * `cards_per_shard`: the amount of OpenPGP smartcards to provision per shardholder.
|
|
||||||
///
|
|
||||||
/// * `cert_output`: the file to write all generated OpenPGP certificates to; if not
|
|
||||||
/// provided, files will be automatically generated for each certificate.
|
|
||||||
#[arg(long)]
|
|
||||||
shard_to_self: Option<ValueWithOptions<PathBuf>>,
|
|
||||||
|
|
||||||
/// Provision a key derived from the mnemonic to a piece of hardware such as an OpenPGP
|
|
||||||
/// smartcard. This argument is required when used with `--encrypt-to-self`.
|
|
||||||
///
|
|
||||||
/// Provisioners may choose to output a public key to the current directory by default, but
|
|
||||||
/// this functionality may be altered on a by-provisioner basis by providing the `output=`
|
|
||||||
/// option to `--provisioner-config`. Additionally, Keyfork may choose to disable
|
|
||||||
/// provisioner output if a matching public key has been derived using `--derive`, which
|
|
||||||
/// may allow for controlling additional metadata that is not relevant to the provisioned
|
|
||||||
/// keys, such as an OpenPGP User ID.
|
|
||||||
#[arg(long)]
|
|
||||||
provision: Option<provision::Provision>,
|
|
||||||
|
|
||||||
/// The amount of times the provisioner should be run. If provisioning multiple devices at
|
|
||||||
/// once, this number should be specified to the number of devices, and all devices should
|
|
||||||
/// be plugged into the system at the same time.
|
|
||||||
#[arg(long, requires = "provision", default_value = "1")]
|
|
||||||
provision_count: usize,
|
|
||||||
|
|
||||||
/// The configuration to pass to the provisioner. These values are specific to each
|
|
||||||
/// provisioner, and should be provided in a `key=value,key=value` format. Most
|
|
||||||
/// provisioners only expect an `output=` option, to be used in place of the default output
|
|
||||||
/// path, if the provisioner needs to write data to a file, such as an OpenPGP certificate.
|
|
||||||
#[arg(long, requires = "provision", default_value_t = Options::default())]
|
|
||||||
provision_config: Options,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: This function defaults to `.asc` in the event no extension is found.
|
|
||||||
// This is specific to OpenPGP. If you want to use this function elsewhere (why?),
|
|
||||||
// be sure to use a relevant extension for your context.
|
|
||||||
fn determine_valid_output_path<T: AsRef<Path>>(
|
|
||||||
path: &Path,
|
|
||||||
mid_ext: &str,
|
|
||||||
optional_path: Option<T>,
|
|
||||||
) -> PathBuf {
|
|
||||||
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 {
|
impl MnemonicSubcommands {
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn handle(
|
pub fn handle(
|
||||||
&self,
|
&self,
|
||||||
_m: &Mnemonic,
|
_m: &Mnemonic,
|
||||||
_keyfork: &Keyfork,
|
_keyfork: &Keyfork,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
match self {
|
match self {
|
||||||
MnemonicSubcommands::Generate {
|
MnemonicSubcommands::Generate { source, size } => source.handle(size),
|
||||||
source,
|
|
||||||
size,
|
|
||||||
derive,
|
|
||||||
encrypt_to,
|
|
||||||
shard_to,
|
|
||||||
shard,
|
|
||||||
encrypt_to_self,
|
|
||||||
shard_to_self,
|
|
||||||
provision,
|
|
||||||
provision_count,
|
|
||||||
provision_config,
|
|
||||||
} => {
|
|
||||||
// NOTE: We should never have a case where there's Some() of empty vec, but
|
|
||||||
// we will make sure to check it just in case.
|
|
||||||
//
|
|
||||||
// We do not print the mnemonic if we are:
|
|
||||||
// * Encrypting to an existing, usable key
|
|
||||||
// * Encrypting to a newly provisioned key
|
|
||||||
// * Sharding to an existing Shardfile with usable keys
|
|
||||||
// * Sharding to existing, usable keys
|
|
||||||
// * Sharding to newly provisioned keys
|
|
||||||
let mut will_print_mnemonic =
|
|
||||||
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(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,11 +5,7 @@ mod mnemonic;
|
||||||
mod provision;
|
mod provision;
|
||||||
mod recover;
|
mod recover;
|
||||||
mod shard;
|
mod shard;
|
||||||
|
mod wizard;
|
||||||
pub fn create(path: &std::path::Path) -> std::io::Result<std::fs::File> {
|
|
||||||
eprintln!("Writing derived key to: {path}", path = path.display());
|
|
||||||
std::fs::File::create(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Kitchen Sink of Entropy.
|
/// The Kitchen Sink of Entropy.
|
||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
@ -20,7 +16,6 @@ pub struct Keyfork {
|
||||||
pub command: KeyforkCommands,
|
pub command: KeyforkCommands,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
pub enum KeyforkCommands {
|
pub enum KeyforkCommands {
|
||||||
/// Derive keys of various formats. These commands require that the Keyfork server is running,
|
/// Derive keys of various formats. These commands require that the Keyfork server is running,
|
||||||
|
@ -62,6 +57,9 @@ pub enum KeyforkCommands {
|
||||||
/// leaked by any individual deriver.
|
/// leaked by any individual deriver.
|
||||||
Recover(recover::Recover),
|
Recover(recover::Recover),
|
||||||
|
|
||||||
|
/// Utilities to automatically manage the setup of Keyfork.
|
||||||
|
Wizard(wizard::Wizard),
|
||||||
|
|
||||||
/// Print an autocompletion file to standard output.
|
/// Print an autocompletion file to standard output.
|
||||||
///
|
///
|
||||||
/// Keyfork does not manage the installation of completion files. Consult the documentation for
|
/// Keyfork does not manage the installation of completion files. Consult the documentation for
|
||||||
|
@ -81,7 +79,8 @@ impl KeyforkCommands {
|
||||||
d.handle(keyfork)?;
|
d.handle(keyfork)?;
|
||||||
}
|
}
|
||||||
KeyforkCommands::Mnemonic(m) => {
|
KeyforkCommands::Mnemonic(m) => {
|
||||||
m.command.handle(m, keyfork)?;
|
let response = m.command.handle(m, keyfork)?;
|
||||||
|
println!("{response}");
|
||||||
}
|
}
|
||||||
KeyforkCommands::Shard(s) => {
|
KeyforkCommands::Shard(s) => {
|
||||||
s.command.handle(s, keyfork)?;
|
s.command.handle(s, keyfork)?;
|
||||||
|
@ -92,11 +91,19 @@ impl KeyforkCommands {
|
||||||
KeyforkCommands::Recover(r) => {
|
KeyforkCommands::Recover(r) => {
|
||||||
r.handle(keyfork)?;
|
r.handle(keyfork)?;
|
||||||
}
|
}
|
||||||
|
KeyforkCommands::Wizard(w) => {
|
||||||
|
w.handle(keyfork)?;
|
||||||
|
}
|
||||||
#[cfg(feature = "completion")]
|
#[cfg(feature = "completion")]
|
||||||
KeyforkCommands::Completion { shell } => {
|
KeyforkCommands::Completion { shell } => {
|
||||||
let mut command = Keyfork::command();
|
let mut command = Keyfork::command();
|
||||||
let command_name = command.get_name().to_string();
|
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(())
|
Ok(())
|
||||||
|
|
|
@ -3,138 +3,81 @@ use crate::config;
|
||||||
|
|
||||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||||
|
|
||||||
use keyfork_derive_util::{DerivationIndex, ExtendedPrivateKey};
|
|
||||||
|
|
||||||
mod openpgp;
|
|
||||||
|
|
||||||
type Identifier = (String, Option<String>);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Provisioner {
|
pub enum Provisioner {
|
||||||
OpenPGPCard(openpgp::OpenPGPCard),
|
OpenPGPCard(OpenPGPCard),
|
||||||
Shard(openpgp::Shard),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Provisioner {
|
impl std::fmt::Display for Provisioner {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(self.identifier())
|
match self {
|
||||||
|
Provisioner::OpenPGPCard(_) => f.write_str("openpgp-card"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Provisioner {
|
impl Provisioner {
|
||||||
pub fn identifier(&self) -> &'static str {
|
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||||
match self {
|
|
||||||
Provisioner::OpenPGPCard(_) => "openpgp-card",
|
|
||||||
Provisioner::Shard(_) => "shard",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
|
|
||||||
match self {
|
match self {
|
||||||
Provisioner::OpenPGPCard(o) => o.discover(),
|
Provisioner::OpenPGPCard(o) => o.discover(),
|
||||||
Provisioner::Shard(s) => s.discover(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn provision(
|
fn provision(
|
||||||
&self,
|
&self,
|
||||||
provisioner: &config::Provisioner,
|
provisioner: config::Provisioner,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
match self {
|
match self {
|
||||||
Provisioner::OpenPGPCard(o) => {
|
Provisioner::OpenPGPCard(o) => o.provision(provisioner),
|
||||||
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
|
|
||||||
type XPrv = ExtendedPrivateKey<Prv>;
|
|
||||||
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
|
||||||
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
|
|
||||||
.chain_push(account_index);
|
|
||||||
let mut client = keyforkd_client::Client::discover_socket()?;
|
|
||||||
let xprv: XPrv = client.request_xprv(&path)?;
|
|
||||||
o.provision(&xprv, provisioner)
|
|
||||||
}
|
|
||||||
Provisioner::Shard(s) => {
|
|
||||||
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
|
|
||||||
type XPrv = ExtendedPrivateKey<Prv>;
|
|
||||||
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
|
||||||
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
|
|
||||||
.chain_push(account_index);
|
|
||||||
let mut client = keyforkd_client::Client::discover_socket()?;
|
|
||||||
let xprv: XPrv = client.request_xprv(&path)?;
|
|
||||||
s.provision(&xprv, provisioner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn provision_with_mnemonic(
|
|
||||||
&self,
|
|
||||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
|
||||||
provisioner: &config::Provisioner,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
match self {
|
|
||||||
Provisioner::OpenPGPCard(o) => {
|
|
||||||
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
|
|
||||||
type XPrv = ExtendedPrivateKey<Prv>;
|
|
||||||
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
|
||||||
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
|
|
||||||
.chain_push(account_index);
|
|
||||||
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
|
|
||||||
o.provision(&xprv, provisioner)
|
|
||||||
}
|
|
||||||
Provisioner::Shard(s) => {
|
|
||||||
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
|
|
||||||
type XPrv = ExtendedPrivateKey<Prv>;
|
|
||||||
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
|
||||||
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
|
|
||||||
.chain_push(account_index);
|
|
||||||
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
|
|
||||||
s.provision(&xprv, provisioner)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValueEnum for Provisioner {
|
impl ValueEnum for Provisioner {
|
||||||
fn value_variants<'a>() -> &'a [Self] {
|
fn value_variants<'a>() -> &'a [Self] {
|
||||||
&[
|
&[Self::OpenPGPCard(OpenPGPCard)]
|
||||||
Self::OpenPGPCard(openpgp::OpenPGPCard),
|
|
||||||
Self::Shard(openpgp::Shard),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_possible_value(&self) -> Option<PossibleValue> {
|
fn to_possible_value(&self) -> Option<PossibleValue> {
|
||||||
Some(PossibleValue::new(self.identifier()))
|
Some(PossibleValue::new(match self {
|
||||||
}
|
Self::OpenPGPCard(_) => "openpgp-card",
|
||||||
}
|
}))
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
#[error("The given value could not be matched as a provisioner: {0} ({1})")]
|
|
||||||
pub struct ProvisionerFromStrError(String, String);
|
|
||||||
|
|
||||||
impl std::str::FromStr for Provisioner {
|
|
||||||
type Err = ProvisionerFromStrError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
<Provisioner as ValueEnum>::from_str(s, false)
|
|
||||||
.map_err(|e| ProvisionerFromStrError(s.to_string(), e))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ProvisionExec {
|
trait ProvisionExec {
|
||||||
type PrivateKey: keyfork_derive_util::PrivateKey + Clone;
|
|
||||||
|
|
||||||
/// Discover all known places the formatted key can be deployed to.
|
/// Discover all known places the formatted key can be deployed to.
|
||||||
fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
|
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||||
Ok(vec![])
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the derivation path for deriving keys.
|
|
||||||
fn derivation_prefix() -> keyfork_derive_util::DerivationPath;
|
|
||||||
|
|
||||||
/// Derive a key and deploy it to a target.
|
/// Derive a key and deploy it to a target.
|
||||||
fn provision(
|
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
&self,
|
}
|
||||||
xprv: &keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
|
|
||||||
p: &config::Provisioner,
|
#[derive(Clone, Debug)]
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
pub struct OpenPGPCard;
|
||||||
|
|
||||||
|
impl ProvisionExec for OpenPGPCard {
|
||||||
|
fn discover(&self) -> Vec<(String, Option<String>)> {
|
||||||
|
/*
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"0006:26144195".to_string(),
|
||||||
|
Some("Yubicats Heywood".to_string()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"0006:2614419y".to_string(),
|
||||||
|
Some("Yubicats Heywood".to_string()),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provision(&self, _p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
|
@ -151,27 +94,15 @@ pub struct Provision {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub subcommand: Option<ProvisionSubcommands>,
|
pub subcommand: Option<ProvisionSubcommands>,
|
||||||
|
|
||||||
pub provisioner_name: Provisioner,
|
provisioner_name: Provisioner,
|
||||||
|
|
||||||
/// Account ID.
|
/// Account ID.
|
||||||
#[arg(long, default_value = "0")]
|
#[arg(long, required(true))]
|
||||||
pub account_id: u32,
|
account_id: Option<u32>,
|
||||||
|
|
||||||
/// Identifier of the hardware to deploy to, listable by running the `discover` subcommand.
|
/// Identifier of the hardware to deploy to, listable by running the `discover` subcommand.
|
||||||
#[arg(long)]
|
#[arg(long, required(true))]
|
||||||
pub identifier: Option<String>,
|
identifier: Option<String>,
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Provision {
|
|
||||||
type Err = clap::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
|
||||||
Provision::try_parse_from(
|
|
||||||
[String::from("provision")]
|
|
||||||
.into_iter()
|
|
||||||
.chain(shlex::Shlex::new(s)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: In the future, this impl will be used by `keyfork recover` to reprovision hardware from
|
// NOTE: In the future, this impl will be used by `keyfork recover` to reprovision hardware from
|
||||||
|
@ -187,9 +118,10 @@ impl TryFrom<Provision> for config::Provisioner {
|
||||||
|
|
||||||
fn try_from(value: Provision) -> Result<Self, Self::Error> {
|
fn try_from(value: Provision) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
account: value.account_id,
|
name: value.provisioner_name.to_string(),
|
||||||
|
account: value.account_id.ok_or(MissingField("account_id"))?,
|
||||||
identifier: value.identifier.ok_or(MissingField("identifier"))?,
|
identifier: value.identifier.ok_or(MissingField("identifier"))?,
|
||||||
metadata: Option::default(),
|
metadata: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,7 +130,7 @@ impl Provision {
|
||||||
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
match self.subcommand {
|
match self.subcommand {
|
||||||
Some(ProvisionSubcommands::Discover) => {
|
Some(ProvisionSubcommands::Discover) => {
|
||||||
let mut iter = self.provisioner_name.discover()?.into_iter().peekable();
|
let mut iter = self.provisioner_name.discover().into_iter().peekable();
|
||||||
while let Some((identifier, context)) = iter.next() {
|
while let Some((identifier, context)) = iter.next() {
|
||||||
println!("Identifier: {identifier}");
|
println!("Identifier: {identifier}");
|
||||||
if let Some(context) = context {
|
if let Some(context) = context {
|
||||||
|
@ -210,20 +142,7 @@ impl Provision {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let provisioner_with_identifier = if self.identifier.is_some() {
|
self.provisioner_name.provision(self.clone().try_into()?)?;
|
||||||
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(())
|
Ok(())
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
use super::ProvisionExec;
|
|
||||||
use crate::{
|
|
||||||
config,
|
|
||||||
openpgp_card::{factory_reset_current_card, get_new_pins},
|
|
||||||
};
|
|
||||||
|
|
||||||
use card_backend_pcsc::PcscBackend;
|
|
||||||
use keyfork_derive_openpgp::{
|
|
||||||
openpgp::{
|
|
||||||
armor::{Kind, Writer},
|
|
||||||
packet::UserID,
|
|
||||||
serialize::Serialize,
|
|
||||||
types::KeyFlags,
|
|
||||||
},
|
|
||||||
XPrv,
|
|
||||||
};
|
|
||||||
use keyfork_prompt::default_handler;
|
|
||||||
use openpgp_card_sequoia::{state::Open, Card};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
#[error("Provisioner was unable to find a matching smartcard")]
|
|
||||||
struct NoMatchingSmartcard;
|
|
||||||
|
|
||||||
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,19 +1,9 @@
|
||||||
use super::Keyfork;
|
use super::Keyfork;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use nix::{
|
|
||||||
sys::wait::waitpid,
|
|
||||||
unistd::{fork, ForkResult},
|
|
||||||
};
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use keyfork_mnemonic::{English, Mnemonic};
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{default_terminal, DefaultTerminal};
|
||||||
default_handler, prompt_validated_wordlist,
|
|
||||||
validators::{
|
|
||||||
mnemonic::{MnemonicChoiceValidator, WordLength},
|
|
||||||
Validator,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use keyfork_shard::{remote_decrypt, Format};
|
use keyfork_shard::{remote_decrypt, Format};
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
@ -45,8 +35,8 @@ impl RecoverSubcommands {
|
||||||
} => {
|
} => {
|
||||||
let content = std::fs::read_to_string(shard_file)?;
|
let content = std::fs::read_to_string(shard_file)?;
|
||||||
if content.contains("BEGIN PGP MESSAGE") {
|
if content.contains("BEGIN PGP MESSAGE") {
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||||
let prompt_handler = default_handler()?;
|
let prompt_handler = default_terminal()?;
|
||||||
// TODO: remove .clone() by making handle() consume self
|
// TODO: remove .clone() by making handle() consume self
|
||||||
let seed = openpgp.decrypt_all_shards_to_secret(
|
let seed = openpgp.decrypt_all_shards_to_secret(
|
||||||
key_discovery.as_deref(),
|
key_discovery.as_deref(),
|
||||||
|
@ -64,15 +54,21 @@ impl RecoverSubcommands {
|
||||||
Ok(seed)
|
Ok(seed)
|
||||||
}
|
}
|
||||||
RecoverSubcommands::Mnemonic {} => {
|
RecoverSubcommands::Mnemonic {} => {
|
||||||
let mut prompt_handler = default_handler()?;
|
use keyfork_prompt::{
|
||||||
|
validators::{
|
||||||
|
mnemonic::{MnemonicChoiceValidator, WordLength},
|
||||||
|
Validator,
|
||||||
|
},
|
||||||
|
PromptHandler,
|
||||||
|
};
|
||||||
|
let mut term = default_terminal()?;
|
||||||
let validator = MnemonicChoiceValidator {
|
let validator = MnemonicChoiceValidator {
|
||||||
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
|
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
|
||||||
};
|
};
|
||||||
let mnemonic = prompt_validated_wordlist::<English, _>(
|
let mnemonic = term.prompt_validated_wordlist::<English, _>(
|
||||||
&mut *prompt_handler,
|
|
||||||
"Mnemonic: ",
|
"Mnemonic: ",
|
||||||
3,
|
3,
|
||||||
&*validator.to_fn(),
|
validator.to_fn(),
|
||||||
)?;
|
)?;
|
||||||
Ok(mnemonic.to_bytes())
|
Ok(mnemonic.to_bytes())
|
||||||
}
|
}
|
||||||
|
@ -84,32 +80,12 @@ impl RecoverSubcommands {
|
||||||
pub struct Recover {
|
pub struct Recover {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: RecoverSubcommands,
|
command: RecoverSubcommands,
|
||||||
|
|
||||||
/// Daemonize the server once started, restoring control back to the shell.
|
|
||||||
#[arg(long, global = true)]
|
|
||||||
daemon: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recover {
|
impl Recover {
|
||||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||||
let seed = self.command.handle()?;
|
let seed = self.command.handle()?;
|
||||||
let mnemonic = Mnemonic::try_from_slice(&seed)?;
|
let mnemonic = Mnemonic::from_bytes(&seed)?;
|
||||||
if self.daemon {
|
|
||||||
// SAFETY: Forking threaded programs is unsafe. We know we don't have multiple
|
|
||||||
// threads at this point.
|
|
||||||
match unsafe { fork() }? {
|
|
||||||
ForkResult::Parent { child } => {
|
|
||||||
// wait for the child to die, so we don't exit prematurely
|
|
||||||
waitpid(Some(child), None)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
ForkResult::Child => {
|
|
||||||
if let ForkResult::Parent { .. } = unsafe { fork() }? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::Keyfork;
|
use super::Keyfork;
|
||||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||||
use keyfork_prompt::default_handler;
|
use keyfork_prompt::{default_terminal, DefaultTerminal};
|
||||||
use keyfork_shard::Format as _;
|
use keyfork_shard::Format as _;
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdin, stdout, Read, Write},
|
io::{stdin, stdout, Read, Write},
|
||||||
|
@ -50,14 +50,6 @@ trait ShardExec {
|
||||||
key_discovery: Option<&Path>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
fn metadata(
|
|
||||||
&self,
|
|
||||||
key_discovery: Option<&Path>,
|
|
||||||
input: impl Read + Send + Sync,
|
|
||||||
output_pubkeys: &mut impl Write,
|
|
||||||
output: &mut impl Write,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -72,7 +64,7 @@ impl ShardExec for OpenPGP {
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
output: &mut (impl Write + Send + Sync),
|
output: &mut (impl Write + Send + Sync),
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let opgp = keyfork_shard::openpgp::OpenPGP;
|
let opgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||||
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
|
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,8 +74,8 @@ impl ShardExec for OpenPGP {
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
output: &mut impl Write,
|
output: &mut impl Write,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||||
let prompt = default_handler()?;
|
let prompt = default_terminal()?;
|
||||||
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
|
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
|
||||||
write!(output, "{}", smex::encode(bytes))?;
|
write!(output, "{}", smex::encode(bytes))?;
|
||||||
|
|
||||||
|
@ -95,37 +87,11 @@ impl ShardExec for OpenPGP {
|
||||||
key_discovery: Option<&Path>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||||
let prompt = default_handler()?;
|
let prompt = default_terminal()?;
|
||||||
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
|
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata(
|
|
||||||
&self,
|
|
||||||
key_discovery: Option<&Path>,
|
|
||||||
input: impl Read + Send + Sync,
|
|
||||||
output_pubkeys: &mut impl Write,
|
|
||||||
output: &mut impl Write,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
use keyfork_derive_openpgp::openpgp::{
|
|
||||||
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)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -175,20 +141,6 @@ pub enum ShardSubcommands {
|
||||||
/// The path to discover private keys from.
|
/// The path to discover private keys from.
|
||||||
key_discovery: Option<PathBuf>,
|
key_discovery: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Decrypt metadata for a shardfile, including the threshold and the public keys. Public keys
|
|
||||||
/// are serialized to a file.
|
|
||||||
Metadata {
|
|
||||||
/// The path to load the Shardfile from.
|
|
||||||
shardfile: PathBuf,
|
|
||||||
|
|
||||||
/// The path to write public keys to.
|
|
||||||
#[arg(long)]
|
|
||||||
output_pubkeys: PathBuf,
|
|
||||||
|
|
||||||
/// The path to discover private keys from.
|
|
||||||
key_discovery: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShardSubcommands {
|
impl ShardSubcommands {
|
||||||
|
@ -257,31 +209,6 @@ impl ShardSubcommands {
|
||||||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ShardSubcommands::Metadata {
|
|
||||||
shardfile,
|
|
||||||
output_pubkeys,
|
|
||||||
key_discovery,
|
|
||||||
} => {
|
|
||||||
let shard_content = std::fs::read_to_string(shardfile)?;
|
|
||||||
if shard_content.contains("BEGIN PGP MESSAGE") {
|
|
||||||
let _ = format.insert(Format::OpenPGP(OpenPGP));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut output_pubkeys_file = std::fs::File::create(output_pubkeys)?;
|
|
||||||
|
|
||||||
match format {
|
|
||||||
Some(Format::OpenPGP(o)) => o.metadata(
|
|
||||||
key_discovery.as_deref(),
|
|
||||||
shard_content.as_bytes(),
|
|
||||||
&mut output_pubkeys_file,
|
|
||||||
&mut stdout,
|
|
||||||
),
|
|
||||||
Some(Format::P256(_p)) => {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,229 @@
|
||||||
|
use super::Keyfork;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
|
||||||
|
|
||||||
|
use card_backend_pcsc::PcscBackend;
|
||||||
|
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
|
||||||
|
|
||||||
|
use keyfork_derive_openpgp::{
|
||||||
|
openpgp::{self, packet::UserID, types::KeyFlags, Cert},
|
||||||
|
XPrv,
|
||||||
|
};
|
||||||
|
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||||
|
use keyfork_prompt::{
|
||||||
|
validators::{PinValidator, Validator},
|
||||||
|
Message, PromptHandler, DefaultTerminal, default_terminal
|
||||||
|
};
|
||||||
|
|
||||||
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[error("Invalid PIN length: {0}")]
|
||||||
|
pub struct PinLength(usize);
|
||||||
|
|
||||||
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
|
||||||
|
let subkeys = vec![
|
||||||
|
KeyFlags::empty().set_certification(),
|
||||||
|
KeyFlags::empty().set_signing(),
|
||||||
|
KeyFlags::empty()
|
||||||
|
.set_transport_encryption()
|
||||||
|
.set_storage_encryption(),
|
||||||
|
KeyFlags::empty().set_authentication(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut pgp_u32 = [0u8; 4];
|
||||||
|
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
|
||||||
|
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||||
|
let mut shrd_u32 = [0u8; 4];
|
||||||
|
shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::<Vec<u8>>());
|
||||||
|
let account = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||||
|
let subkey = DerivationIndex::new(u32::from(index), true)?;
|
||||||
|
let path = DerivationPath::default()
|
||||||
|
.chain_push(chain)
|
||||||
|
.chain_push(account)
|
||||||
|
.chain_push(subkey);
|
||||||
|
let xprv = XPrv::new(seed).derive_path(&path)?;
|
||||||
|
let userid = UserID::from(format!("Keyfork Shard {index}"));
|
||||||
|
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
|
||||||
|
Ok(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: extract into crate
|
||||||
|
/// Factory reset the current card so long as it does not match the last-used backend.
|
||||||
|
fn factory_reset_current_card(
|
||||||
|
seen_cards: &mut HashSet<String>,
|
||||||
|
user_pin: &str,
|
||||||
|
admin_pin: &str,
|
||||||
|
cert: &Cert,
|
||||||
|
card_backend: PcscBackend,
|
||||||
|
) -> Result<()> {
|
||||||
|
let policy = openpgp::policy::NullPolicy::new();
|
||||||
|
let valid_cert = cert.with_policy(&policy, None)?;
|
||||||
|
let signing_key = valid_cert
|
||||||
|
.keys()
|
||||||
|
.for_signing()
|
||||||
|
.secret()
|
||||||
|
.next()
|
||||||
|
.expect("no signing key found");
|
||||||
|
let decryption_key = valid_cert
|
||||||
|
.keys()
|
||||||
|
.for_storage_encryption()
|
||||||
|
.secret()
|
||||||
|
.next()
|
||||||
|
.expect("no decryption key found");
|
||||||
|
let authentication_key = valid_cert
|
||||||
|
.keys()
|
||||||
|
.for_authentication()
|
||||||
|
.secret()
|
||||||
|
.next()
|
||||||
|
.expect("no authentication key found");
|
||||||
|
let mut card = Card::<Open>::new(card_backend)?;
|
||||||
|
let mut transaction = card.transaction()?;
|
||||||
|
let application_identifier = transaction.application_identifier()?.ident();
|
||||||
|
if seen_cards.contains(&application_identifier) {
|
||||||
|
// we were given the same card, error
|
||||||
|
panic!("Previously used card {application_identifier} was reused");
|
||||||
|
} else {
|
||||||
|
seen_cards.insert(application_identifier);
|
||||||
|
}
|
||||||
|
transaction.factory_reset()?;
|
||||||
|
let mut admin = transaction.to_admin_card("12345678")?;
|
||||||
|
admin.upload_key(signing_key, KeyType::Signing, None)?;
|
||||||
|
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
|
||||||
|
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
|
||||||
|
transaction.change_user_pin("123456", user_pin)?;
|
||||||
|
transaction.change_admin_pin("12345678", admin_pin)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_shard_secret(
|
||||||
|
threshold: u8,
|
||||||
|
max: u8,
|
||||||
|
keys_per_shard: u8,
|
||||||
|
output_file: &Option<PathBuf>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let seed = keyfork_entropy::generate_entropy_of_const_size::<{256 / 8}>()?;
|
||||||
|
let mut pm = default_terminal()?;
|
||||||
|
let mut certs = vec![];
|
||||||
|
let mut seen_cards: HashSet<String> = HashSet::new();
|
||||||
|
let stdout = std::io::stdout();
|
||||||
|
if output_file.is_none() {
|
||||||
|
assert!(
|
||||||
|
!stdout.is_terminal(),
|
||||||
|
"not printing shard to terminal, redirect output"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_pin_validator = PinValidator {
|
||||||
|
min_length: Some(6),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.to_fn();
|
||||||
|
let admin_pin_validator = PinValidator {
|
||||||
|
min_length: Some(8),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.to_fn();
|
||||||
|
|
||||||
|
for index in 0..max {
|
||||||
|
let cert = derive_key(seed, index)?;
|
||||||
|
for i in 0..keys_per_shard {
|
||||||
|
pm.prompt_message(Message::Text(format!(
|
||||||
|
"Please remove all keys and insert key #{} for user #{}",
|
||||||
|
i + 1,
|
||||||
|
index + 1,
|
||||||
|
)))?;
|
||||||
|
let card_backend = loop {
|
||||||
|
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
|
||||||
|
break c;
|
||||||
|
}
|
||||||
|
pm.prompt_message(Message::Text(
|
||||||
|
"No smart card was found. Please plug in a smart card and press enter"
|
||||||
|
.to_string(),
|
||||||
|
))?;
|
||||||
|
};
|
||||||
|
let user_pin = pm.prompt_validated_passphrase(
|
||||||
|
"Please enter the new smartcard User PIN: ",
|
||||||
|
3,
|
||||||
|
&user_pin_validator,
|
||||||
|
)?;
|
||||||
|
let admin_pin = pm.prompt_validated_passphrase(
|
||||||
|
"Please enter the new smartcard Admin PIN: ",
|
||||||
|
3,
|
||||||
|
&admin_pin_validator,
|
||||||
|
)?;
|
||||||
|
factory_reset_current_card(
|
||||||
|
&mut seen_cards,
|
||||||
|
user_pin.trim(),
|
||||||
|
admin_pin.trim(),
|
||||||
|
&cert,
|
||||||
|
card_backend,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
certs.push(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
let opgp = OpenPGP::<DefaultTerminal>::new();
|
||||||
|
|
||||||
|
if let Some(output_file) = output_file {
|
||||||
|
let output = File::create(output_file)?;
|
||||||
|
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
|
||||||
|
} else {
|
||||||
|
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
|
pub enum WizardSubcommands {
|
||||||
|
/// Create a 256 bit secret and shard the secret to smart cards.
|
||||||
|
///
|
||||||
|
/// Smart cards will need to be plugged in periodically during the wizard, where they will be factory reset and
|
||||||
|
/// provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered with `keyfork recover shard` or
|
||||||
|
/// `keyfork recover remote-shard`. The share file will be printed to standard output.
|
||||||
|
GenerateShardSecret {
|
||||||
|
/// The minimum amount of keys required to decrypt the secret.
|
||||||
|
#[arg(long)]
|
||||||
|
threshold: u8,
|
||||||
|
|
||||||
|
/// The maximum amount of shards.
|
||||||
|
#[arg(long)]
|
||||||
|
max: u8,
|
||||||
|
|
||||||
|
/// The amount of smart cards to provision per-shard.
|
||||||
|
#[arg(long, default_value = "1")]
|
||||||
|
keys_per_shard: u8,
|
||||||
|
|
||||||
|
/// The file to write the generated shard file to.
|
||||||
|
#[arg(long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WizardSubcommands {
|
||||||
|
fn handle(&self) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
WizardSubcommands::GenerateShardSecret {
|
||||||
|
threshold,
|
||||||
|
max,
|
||||||
|
keys_per_shard,
|
||||||
|
output,
|
||||||
|
} => generate_shard_secret(*threshold, *max, *keys_per_shard, output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
pub struct Wizard {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: WizardSubcommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wizard {
|
||||||
|
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||||
|
self.command.handle()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,19 +2,20 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Mnemonic {
|
pub struct Mnemonic {
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Provisioner {
|
pub struct Provisioner {
|
||||||
|
pub name: String,
|
||||||
pub account: u32,
|
pub account: u32,
|
||||||
pub identifier: String,
|
pub identifier: String,
|
||||||
pub metadata: Option<HashMap<String, String>>,
|
pub metadata: Option<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub mnemonic: Mnemonic,
|
pub mnemonic: Mnemonic,
|
||||||
pub provisioner: Vec<Provisioner>,
|
pub provisioner: Vec<Provisioner>,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
#![allow(clippy::module_name_repetitions)]
|
#![allow(clippy::module_name_repetitions)]
|
||||||
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
@ -7,10 +8,8 @@ use clap::Parser;
|
||||||
|
|
||||||
use keyfork_bin::{Bin, ClosureBin};
|
use keyfork_bin::{Bin, ClosureBin};
|
||||||
|
|
||||||
pub mod clap_ext;
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod openpgp_card;
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let bin = ClosureBin::new(|| {
|
let bin = ClosureBin::new(|| {
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
use card_backend_pcsc::PcscBackend;
|
|
||||||
use keyfork_derive_openpgp::openpgp::{policy::Policy, Cert};
|
|
||||||
use keyfork_prompt::{
|
|
||||||
prompt_validated_passphrase,
|
|
||||||
validators::{SecurePinValidator, Validator},
|
|
||||||
Message, PromptHandler,
|
|
||||||
};
|
|
||||||
use openpgp_card_sequoia::{state::Open, types::KeyType, types::TouchPolicy, Card};
|
|
||||||
|
|
||||||
pub fn get_new_pins(
|
|
||||||
pm: &mut dyn PromptHandler,
|
|
||||||
) -> Result<(String, String), Box<dyn std::error::Error>> {
|
|
||||||
let user_pin_validator = SecurePinValidator {
|
|
||||||
min_length: Some(6),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.to_fn();
|
|
||||||
let admin_pin_validator = SecurePinValidator {
|
|
||||||
min_length: Some(8),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.to_fn();
|
|
||||||
|
|
||||||
let user_pin = loop {
|
|
||||||
let user_pin = prompt_validated_passphrase(
|
|
||||||
&mut *pm,
|
|
||||||
"Please enter the new smartcard User PIN: ",
|
|
||||||
3,
|
|
||||||
&user_pin_validator,
|
|
||||||
)?;
|
|
||||||
let validated_user_pin = prompt_validated_passphrase(
|
|
||||||
&mut *pm,
|
|
||||||
"Please verify the new smartcard User PIN: ",
|
|
||||||
3,
|
|
||||||
&user_pin_validator,
|
|
||||||
)?;
|
|
||||||
if user_pin == validated_user_pin {
|
|
||||||
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,13 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-qrcode"
|
name = "keyfork-qrcode"
|
||||||
version = "0.1.3"
|
version = "0.1.1"
|
||||||
repository = "https://git.distrust.co/public/keyfork"
|
repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -17,10 +14,9 @@ decode-backend-rqrr = ["dep:rqrr"]
|
||||||
decode-backend-zbar = ["dep:keyfork-zbar"]
|
decode-backend-zbar = ["dep:keyfork-zbar"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-bug = { workspace = true }
|
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
|
||||||
keyfork-zbar = { workspace = true, optional = true, features = ["image"] }
|
keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true, registry = "distrust" }
|
||||||
image = { workspace = true, default-features = false, features = ["jpeg"] }
|
image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
|
||||||
rqrr = { version = "0.9.0", optional = true }
|
rqrr = { version = "0.6.0", optional = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.56"
|
||||||
v4l = { workspace = true }
|
v4l = "0.14.0"
|
||||||
cfg-if = "1.0.0"
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
#![allow(missing_docs)]
|
//!
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use keyfork_qrcode::scan_camera;
|
use keyfork_qrcode::scan_camera;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let output = scan_camera(Duration::from_secs(15), 0)?;
|
let output = scan_camera(Duration::from_secs(60 * 10), 0)?;
|
||||||
if let Some(scanned_text) = output {
|
if let Some(scanned_text) = output {
|
||||||
println!("{scanned_text}");
|
println!("{scanned_text}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,23 +2,20 @@
|
||||||
|
|
||||||
use keyfork_bug as bug;
|
use keyfork_bug as bug;
|
||||||
|
|
||||||
use bug::POISONED_MUTEX;
|
use image::io::Reader as ImageReader;
|
||||||
use image::{ImageBuffer, ImageReader, Luma};
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{Cursor, Write},
|
io::{Cursor, Write},
|
||||||
process::{Command, Stdio},
|
|
||||||
sync::{mpsc::channel, Arc, Condvar, Mutex},
|
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
|
process::{Command, Stdio},
|
||||||
};
|
};
|
||||||
use v4l::{
|
use v4l::{
|
||||||
buffer::Type,
|
buffer::Type,
|
||||||
io::{traits::CaptureStream, userptr::Stream},
|
io::{userptr::Stream, traits::CaptureStream},
|
||||||
video::Capture,
|
video::Capture,
|
||||||
Device, FourCC,
|
FourCC,
|
||||||
|
Device,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Image = ImageBuffer<Luma<u8>, Vec<u8>>;
|
|
||||||
|
|
||||||
/// A QR code could not be generated.
|
/// A QR code could not be generated.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum QRGenerationError {
|
pub enum QRGenerationError {
|
||||||
|
@ -37,10 +34,10 @@ pub enum QRCodeScanError {
|
||||||
/// The camera could not load the requested format.
|
/// The camera could not load the requested format.
|
||||||
#[error("Camera could not use {expected} format, instead used {actual}")]
|
#[error("Camera could not use {expected} format, instead used {actual}")]
|
||||||
CameraGaveBadFormat {
|
CameraGaveBadFormat {
|
||||||
/// The expected format, in `FourCC` format.
|
/// The expected format, in FourCC format.
|
||||||
expected: String,
|
expected: String,
|
||||||
|
|
||||||
/// The actual format, in `FourCC` format.
|
/// The actual format, in FourCC format.
|
||||||
actual: String,
|
actual: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -105,188 +102,60 @@ pub fn qrencode(
|
||||||
|
|
||||||
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
|
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
|
||||||
|
|
||||||
trait Scanner {
|
|
||||||
fn scan_image(&mut self, image: Image) -> Option<String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
/// Continuously scan the `index`-th camera for a QR code.
|
||||||
///
|
#[cfg(feature = "decode-backend-rqrr")]
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// The function may return an error if the hardware is unable to scan video or if an image could
|
|
||||||
/// not be decoded.
|
|
||||||
///
|
|
||||||
/// # 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> {
|
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||||
let device = Device::new(index)?;
|
let device = Device::new(index)?;
|
||||||
let mut fmt = device
|
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||||
.format()
|
|
||||||
.unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
|
||||||
fmt.fourcc = FourCC::new(b"MPG1");
|
fmt.fourcc = FourCC::new(b"MPG1");
|
||||||
device.set_format(&fmt)?;
|
device.set_format(&fmt)?;
|
||||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
#[allow(unused)]
|
while Instant::now()
|
||||||
let mut count = 0;
|
.duration_since(start)
|
||||||
|
< timeout
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (buffer, _) = stream.next()?;
|
||||||
let image = ImageReader::new(Cursor::new(buffer))
|
let image = ImageReader::new(Cursor::new(buffer))
|
||||||
.with_guessed_format()?
|
.with_guessed_format()?
|
||||||
.decode()?
|
.decode()?
|
||||||
.to_luma8();
|
.to_luma8();
|
||||||
|
let mut image = rqrr::PreparedImage::prepare(image);
|
||||||
arced
|
for grid in image.detect_grids() {
|
||||||
.0
|
if let Ok((_, content)) = grid.decode() {
|
||||||
.lock()
|
return Ok(Some(content))
|
||||||
.expect(bug::bug!(POISONED_MUTEX))
|
}
|
||||||
.images
|
}
|
||||||
.push(image);
|
}
|
||||||
arced.1.notify_one();
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Continuously scan the `index`-th camera for a QR code.
|
||||||
|
#[cfg(feature = "decode-backend-zbar")]
|
||||||
|
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||||
|
let device = Device::new(index)?;
|
||||||
|
let mut fmt = device.format().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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dbg_elapsed(count, start);
|
|
||||||
|
|
||||||
arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true;
|
|
||||||
arced.1.notify_all();
|
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,10 @@ repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
bindgen = { version = "0.70", default-features = false, features = ["runtime"] }
|
bindgen = { version = "0.68", default-features = false, features = ["runtime"] }
|
||||||
pkg-config = "0.3"
|
pkg-config = "0.3"
|
||||||
|
|
|
@ -45,7 +45,7 @@ fn main() -> Result<()> {
|
||||||
if let Err(e) = generate_bindings_file() {
|
if let Err(e) = generate_bindings_file() {
|
||||||
eprintln!("Building zbar-sys failed: {e}");
|
eprintln!("Building zbar-sys failed: {e}");
|
||||||
eprintln!("Ensure zbar headers, libclang, and pkg-config are installed");
|
eprintln!("Ensure zbar headers, libclang, and pkg-config are installed");
|
||||||
return Err(e);
|
return Err(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
|
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
#![allow(clippy::unreadable_literal, clippy::pub_underscore_fields)]
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-zbar"
|
name = "keyfork-zbar"
|
||||||
version = "0.1.2"
|
version = "0.1.0"
|
||||||
repository = "https://git.distrust.co/public/keyfork"
|
repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -16,9 +13,9 @@ bin = ["image"]
|
||||||
image = ["dep:image"]
|
image = ["dep:image"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-zbar-sys = { workspace = true }
|
keyfork-zbar-sys = { version = "0.1.0", path = "../keyfork-zbar-sys", registry = "distrust" }
|
||||||
image = { workspace = true, default-features = false, optional = true }
|
image = { version = "0.24.7", default-features = false, optional = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.56"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
v4l = { workspace = true }
|
v4l = "0.14.0"
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
|
|
||||||
use keyfork_zbar::{image::Image, image_scanner::ImageScanner};
|
use keyfork_zbar::{image::Image, image_scanner::ImageScanner};
|
||||||
|
|
||||||
use image::ImageReader;
|
use image::io::Reader as ImageReader;
|
||||||
use v4l::{
|
use v4l::{
|
||||||
buffer::Type,
|
buffer::Type,
|
||||||
io::{traits::CaptureStream, userptr::Stream},
|
io::{traits::CaptureStream, userptr::Stream},
|
||||||
|
@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.decode()?,
|
.decode()?,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(symbol) = scanner.scan_image(&image).first() {
|
if let Some(symbol) = scanner.scan_image(&image).get(0) {
|
||||||
println!("{}", String::from_utf8_lossy(symbol.data()));
|
println!("{}", String::from_utf8_lossy(symbol.data()));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,16 @@ impl Image {
|
||||||
|
|
||||||
/// Link: [`sys::zbar_image_set_format`]
|
/// 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
|
/// ```rust,ignore
|
||||||
/// self.set_format(b"Y800")
|
/// self.set_format(b"Y800")
|
||||||
/// ```
|
/// ```
|
||||||
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
|
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
|
||||||
let fourcc = std::os::raw::c_ulong::from(u32::from_le_bytes(*fourcc));
|
let fourcc: u64 = fourcc[0] as u64
|
||||||
|
| ((fourcc[1] as u64) << 8)
|
||||||
|
| ((fourcc[2] as u64) << 16)
|
||||||
|
| ((fourcc[3] as u64) << 24);
|
||||||
unsafe { sys::zbar_image_set_format(self.inner, fourcc) }
|
unsafe { sys::zbar_image_set_format(self.inner, fourcc) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +43,12 @@ impl Image {
|
||||||
/// Accepts raw data in the configured format. See: [`Image::set_format`]
|
/// Accepts raw data in the configured format. See: [`Image::set_format`]
|
||||||
fn set_data(&mut self, data: Vec<u8>) {
|
fn set_data(&mut self, data: Vec<u8>) {
|
||||||
unsafe {
|
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
|
// keep data in self to avoid use after free when data goes out of scope
|
||||||
let _ = self.inner_data.insert(data);
|
let _ = self.inner_data.insert(data);
|
||||||
|
@ -50,7 +58,7 @@ impl Image {
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
mod impls {
|
mod impls {
|
||||||
use super::*;
|
use super::*;
|
||||||
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma};
|
use image::{DynamicImage, GenericImageView};
|
||||||
|
|
||||||
impl From<DynamicImage> for Image {
|
impl From<DynamicImage> for Image {
|
||||||
fn from(value: DynamicImage) -> Self {
|
fn from(value: DynamicImage) -> Self {
|
||||||
|
@ -62,17 +70,6 @@ mod impls {
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ImageBuffer<Luma<u8>, Vec<u8>>> for Image {
|
|
||||||
fn from(value: ImageBuffer<Luma<u8>, Vec<u8>>) -> Self {
|
|
||||||
let mut image = Self::alloc();
|
|
||||||
let (width, height) = value.dimensions();
|
|
||||||
image.set_size(width, height);
|
|
||||||
image.set_format(b"Y800");
|
|
||||||
image.set_data(value.into_raw());
|
|
||||||
image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Image {
|
impl Drop for Image {
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub struct ImageScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageScanner {
|
impl ImageScanner {
|
||||||
/// Create a new `ImageScanner`.
|
/// create a new ImageScanner.
|
||||||
///
|
///
|
||||||
/// Link: [`sys::zbar_image_scanner_create`]
|
/// Link: [`sys::zbar_image_scanner_create`]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
@ -31,7 +31,7 @@ impl ImageScanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a configuration option.
|
/// Set a configuration option for the ImageScanner.
|
||||||
///
|
///
|
||||||
/// Link: [`sys::zbar_image_scanner_set_config`]
|
/// Link: [`sys::zbar_image_scanner_set_config`]
|
||||||
///
|
///
|
||||||
|
@ -58,7 +58,10 @@ impl ImageScanner {
|
||||||
/// Link: [`sys::zbar_scan_image`]
|
/// Link: [`sys::zbar_scan_image`]
|
||||||
///
|
///
|
||||||
/// TODO: return an iterator over scanned values
|
/// 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) };
|
unsafe { sys::zbar_scan_image(self.inner, image.inner) };
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
let mut symbol = unsafe { sys::zbar_image_first_symbol(image.inner) };
|
let mut symbol = unsafe { sys::zbar_image_first_symbol(image.inner) };
|
||||||
|
@ -67,7 +70,7 @@ impl ImageScanner {
|
||||||
let symbol_data = unsafe { sys::zbar_symbol_get_data(symbol) };
|
let symbol_data = unsafe { sys::zbar_symbol_get_data(symbol) };
|
||||||
let symbol_data_len = unsafe { sys::zbar_symbol_get_data_length(symbol) };
|
let symbol_data_len = unsafe { sys::zbar_symbol_get_data_length(symbol) };
|
||||||
let symbol_slice = unsafe {
|
let symbol_slice = unsafe {
|
||||||
std::slice::from_raw_parts(symbol_data.cast::<u8>(), symbol_data_len as usize)
|
std::slice::from_raw_parts(symbol_data as *const u8, symbol_data_len as usize)
|
||||||
};
|
};
|
||||||
result.push(Symbol::new(symbol_type, symbol_slice));
|
result.push(Symbol::new(symbol_type, symbol_slice));
|
||||||
symbol = unsafe { sys::zbar_symbol_next(symbol) };
|
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_modifier_e as Modifier;
|
||||||
pub use sys::zbar_orientation_e as Orientation;
|
pub use sys::zbar_orientation_e as Orientation;
|
||||||
|
|
||||||
pub mod image;
|
|
||||||
pub mod image_scanner;
|
pub mod image_scanner;
|
||||||
|
pub mod image;
|
||||||
pub mod symbol;
|
pub mod symbol;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
//! A Symbol represents some form of encoded data.
|
//!
|
||||||
|
|
||||||
#![allow(clippy::used_underscore_binding)]
|
|
||||||
|
|
||||||
use super::sys;
|
use super::sys;
|
||||||
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
[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"] }
|
|
|
@ -1 +0,0 @@
|
||||||
mod openpgp;
|
|
|
@ -1,56 +0,0 @@
|
||||||
use sequoia_openpgp as openpgp;
|
|
||||||
|
|
||||||
use assert_cmd::Command;
|
|
||||||
use openpgp::{
|
|
||||||
parse::{PacketParser, Parse},
|
|
||||||
policy::StandardPolicy,
|
|
||||||
types::KeyFlags,
|
|
||||||
Cert,
|
|
||||||
};
|
|
||||||
|
|
||||||
const KEYFORK_BIN: &str = "keyfork";
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
let policy = StandardPolicy::new();
|
|
||||||
|
|
||||||
let command_output = Command::cargo_bin(KEYFORK_BIN)
|
|
||||||
.unwrap()
|
|
||||||
.args([
|
|
||||||
"derive",
|
|
||||||
"openpgp",
|
|
||||||
"Ryan Heywood (RyanSquared) <ryan@distrust.co>",
|
|
||||||
])
|
|
||||||
.assert()
|
|
||||||
.success();
|
|
||||||
|
|
||||||
let packets = PacketParser::from_bytes(&command_output.get_output().stdout).unwrap();
|
|
||||||
let cert = Cert::try_from(packets).unwrap();
|
|
||||||
|
|
||||||
// assert the cert contains _any_ secret key data
|
|
||||||
assert!(
|
|
||||||
cert.is_tsk(),
|
|
||||||
"exported key should contain secret key data, indicated by the key being a TSK"
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert the correct keys were added in the correct order
|
|
||||||
let mut key_formats = std::collections::HashSet::from([
|
|
||||||
KeyFlags::empty().set_certification(),
|
|
||||||
KeyFlags::empty().set_signing(),
|
|
||||||
KeyFlags::empty()
|
|
||||||
.set_transport_encryption()
|
|
||||||
.set_storage_encryption(),
|
|
||||||
KeyFlags::empty().set_authentication(),
|
|
||||||
]);
|
|
||||||
let valid_cert = cert.with_policy(&policy, None).unwrap();
|
|
||||||
for key in valid_cert.keys() {
|
|
||||||
let flags = key.key_flags().unwrap();
|
|
||||||
assert!(
|
|
||||||
key_formats.remove(&flags),
|
|
||||||
"could not find key flag set: {flags:?}"
|
|
||||||
);
|
|
||||||
key.alive().expect("is live after being generated");
|
|
||||||
key.parts_into_secret().expect("has secret keys");
|
|
||||||
}
|
|
||||||
assert!(key_formats.is_empty(), "remaining key formats: {key_formats:?}");
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
mod derive;
|
|
|
@ -1,2 +0,0 @@
|
||||||
#[cfg(test)]
|
|
||||||
mod keyfork;
|
|
|
@ -4,12 +4,9 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "1.0.79"
|
||||||
|
|
|
@ -45,7 +45,6 @@ use std::process::ExitCode;
|
||||||
/// A result that may contain any error.
|
/// A result that may contain any error.
|
||||||
pub type ProcessResult<T = ()> = Result<T, Box<dyn std::error::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>) {
|
fn report_err(e: Box<dyn std::error::Error>) {
|
||||||
eprintln!("Unable to run command: {e}");
|
eprintln!("Unable to run command: {e}");
|
||||||
let mut source = e.source();
|
let mut source = e.source();
|
||||||
|
@ -103,13 +102,10 @@ pub trait Bin {
|
||||||
|
|
||||||
/// A Bin that doesn't take any arguments.
|
/// A Bin that doesn't take any arguments.
|
||||||
pub struct ClosureBin<F: Fn() -> ProcessResult> {
|
pub struct ClosureBin<F: Fn() -> ProcessResult> {
|
||||||
closure: F,
|
closure: F
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> ClosureBin<F>
|
impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
|
||||||
where
|
|
||||||
F: Fn() -> ProcessResult,
|
|
||||||
{
|
|
||||||
/// Create a new Bin from a closure.
|
/// Create a new Bin from a closure.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -124,14 +120,13 @@ where
|
||||||
/// bin.main();
|
/// bin.main();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new(closure: F) -> Self {
|
pub fn new(closure: F) -> Self {
|
||||||
Self { closure }
|
Self {
|
||||||
|
closure
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> Bin for ClosureBin<F>
|
impl<F> Bin for ClosureBin<F> where F: Fn() -> ProcessResult {
|
||||||
where
|
|
||||||
F: Fn() -> ProcessResult,
|
|
||||||
{
|
|
||||||
type Args = ();
|
type Args = ();
|
||||||
|
|
||||||
fn validate_args(&self, _args: impl Iterator<Item = String>) -> ProcessResult<Self::Args> {
|
fn validate_args(&self, _args: impl Iterator<Item = String>) -> ProcessResult<Self::Args> {
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-bug"
|
name = "keyfork-bug"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -16,12 +16,6 @@
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ```rust,should_panic
|
//! ```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 std::fs::File;
|
||||||
//! use keyfork_bug as bug;
|
//! use keyfork_bug as bug;
|
||||||
//!
|
//!
|
||||||
|
@ -89,29 +83,6 @@ 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
|
/// 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
|
/// 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.
|
/// argument, so it is suitable for being used with [`Result`] types instead of [`Option`] types.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-crossterm"
|
name = "keyfork-crossterm"
|
||||||
version = "0.27.2"
|
version = "0.27.1"
|
||||||
# authors = ["T. Post"]
|
# authors = ["T. Post"]
|
||||||
authors = ["Ryan Heywood <ryan@distrust.co>"]
|
authors = ["Ryan Heywood <ryan@distrust.co>"]
|
||||||
description = "A crossplatform terminal library for manipulating terminals."
|
description = "A crossplatform terminal library for manipulating terminals."
|
||||||
|
@ -14,9 +14,6 @@ edition = "2021"
|
||||||
rust-version = "1.58.0"
|
rust-version = "1.58.0"
|
||||||
# categories = ["command-line-interface", "command-line-utilities"]
|
# categories = ["command-line-interface", "command-line-utilities"]
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# [lib]
|
# [lib]
|
||||||
# name = "crossterm"
|
# name = "crossterm"
|
||||||
# path = "src/lib.rs"
|
# path = "src/lib.rs"
|
||||||
|
@ -58,16 +55,16 @@ crossterm_winapi = { version = "0.9.1", optional = true }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
signal-hook = { version = "0.3.17", optional = true }
|
signal-hook = { version = "0.3.17", optional = true }
|
||||||
filedescriptor = { version = "0.8", optional = true }
|
filedescriptor = { version = "0.8", optional = true }
|
||||||
mio = { version = "1.0", features = ["os-poll"], optional = true }
|
mio = { version = "0.8", features = ["os-poll"], optional = true }
|
||||||
signal-hook-mio = { version = "0.2.3", features = ["support-v1_0"], optional = true }
|
signal-hook-mio = { version = "0.2.3", features = ["support-v0_8"], optional = true }
|
||||||
|
|
||||||
# Dev dependencies (examples, ...)
|
# Dev dependencies (examples, ...)
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["full"] }
|
tokio = { version = "1.25", features = ["full"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-timer = "3.0"
|
futures-timer = "3.0"
|
||||||
async-std = "1.12"
|
async-std = "1.12"
|
||||||
serde_json = { workspace = true }
|
serde_json = "1.0"
|
||||||
serial_test = "2.0.0"
|
serial_test = "2.0.0"
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#![allow(missing_docs)]
|
//!
|
||||||
|
|
||||||
use keyfork_crossterm::{
|
use keyfork_crossterm::{
|
||||||
execute,
|
execute,
|
||||||
|
|
|
@ -61,7 +61,6 @@ impl Filter for EventFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct InternalEventFilter;
|
pub(crate) struct InternalEventFilter;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{collections::VecDeque, io, time::Duration};
|
use std::{collections::VecDeque, io, time::Duration};
|
||||||
|
|
||||||
use mio::{unix::SourceFd, Events, Interest, Poll, Token};
|
use mio::{unix::SourceFd, Events, Interest, Poll, Token};
|
||||||
use signal_hook_mio::v1_0::Signals;
|
use signal_hook_mio::v0_8::Signals;
|
||||||
|
|
||||||
#[cfg(feature = "event-stream")]
|
#[cfg(feature = "event-stream")]
|
||||||
use crate::event::sys::Waker;
|
use crate::event::sys::Waker;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#![allow(missing_docs, clippy::missing_errors_doc, clippy::missing_panics_doc)]
|
#![allow(missing_docs, clippy::missing_errors_doc, clippy::missing_panics_doc)]
|
||||||
#![deny(unused_imports, unused_must_use)]
|
#![deny(unused_imports, unused_must_use)]
|
||||||
#![allow(clippy::pedantic, clippy::all, unexpected_cfgs)]
|
|
||||||
|
|
||||||
//! # Cross-platform Terminal Manipulation Library
|
//! # Cross-platform Terminal Manipulation Library
|
||||||
//!
|
//!
|
||||||
|
|
|
@ -140,10 +140,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_attributes_const() {
|
fn test_attributes_const() {
|
||||||
const ATTRIBUTES: Attributes = Attributes::none()
|
const ATTRIBUTES: Attributes = Attributes::none().with(Attribute::Bold).with(Attribute::Italic).without(Attribute::Bold);
|
||||||
.with(Attribute::Bold)
|
|
||||||
.with(Attribute::Italic)
|
|
||||||
.without(Attribute::Bold);
|
|
||||||
assert!(!ATTRIBUTES.has(Attribute::Bold));
|
assert!(!ATTRIBUTES.has(Attribute::Bold));
|
||||||
assert!(ATTRIBUTES.has(Attribute::Italic));
|
assert!(ATTRIBUTES.has(Attribute::Italic));
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,10 +108,7 @@ pub struct FdTerminal {
|
||||||
stored_termios: Option<libc::termios>,
|
stored_termios: Option<libc::termios>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<T> for FdTerminal
|
impl<T> From<T> for FdTerminal where T: os::fd::AsRawFd {
|
||||||
where
|
|
||||||
T: os::fd::AsRawFd,
|
|
||||||
{
|
|
||||||
fn from(value: T) -> Self {
|
fn from(value: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
fd: value.as_raw_fd(),
|
fd: value.as_raw_fd(),
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-entropy"
|
name = "keyfork-entropy"
|
||||||
version = "0.1.2"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -14,5 +11,5 @@ default = ["bin"]
|
||||||
bin = ["smex"]
|
bin = ["smex"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-bug = { workspace = true }
|
keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
|
||||||
smex = { workspace = true, optional = true }
|
smex = { version = "0.1.0", path = "../smex", optional = true, registry = "distrust" }
|
||||||
|
|
|
@ -41,7 +41,7 @@ fn ensure_offline() {
|
||||||
.to_str()
|
.to_str()
|
||||||
.expect(bug!("Unable to decode UTF-8 filepath"))
|
.expect(bug!("Unable to decode UTF-8 filepath"))
|
||||||
.split('/')
|
.split('/')
|
||||||
.next_back()
|
.last()
|
||||||
.expect(bug!("No data in file path"))
|
.expect(bug!("No data in file path"))
|
||||||
== "lo"
|
== "lo"
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! Generate entropy of a given size, encoded as hex.
|
//!
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let bit_size: usize = std::env::args()
|
let bit_size: usize = std::env::args()
|
||||||
|
@ -10,12 +10,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
bit_size % 8 == 0,
|
bit_size % 8 == 0,
|
||||||
"Bit size must be divisible by 8, got: {bit_size}"
|
"Bit size must be divisible by 8, got: {bit_size}"
|
||||||
);
|
);
|
||||||
match bit_size {
|
assert!(
|
||||||
128 | 256 | 512 => {}
|
bit_size <= 256,
|
||||||
_ => {
|
"Maximum supported bit size is 256, got: {bit_size}"
|
||||||
eprintln!("reading entropy of uncommon size: {bit_size}");
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
|
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
|
||||||
println!("{}", smex::encode(entropy));
|
println!("{}", smex::encode(entropy));
|
||||||
|
|
|
@ -4,9 +4,6 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -15,13 +12,13 @@ async = ["dep:tokio"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Included in Rust
|
# Included in Rust
|
||||||
sha2 = { workspace = true }
|
sha2 = "0.10.7"
|
||||||
|
|
||||||
# Personally audited
|
# Personally audited
|
||||||
thiserror = { workspace = true }
|
thiserror = "1.0.47"
|
||||||
|
|
||||||
# Optional, not personally audited
|
# Optional, not personally audited
|
||||||
tokio = { workspace = true, optional = true, features = ["io-util"] }
|
tokio = { version = "1.32.0", optional = true, features = ["io-util"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.31.0"
|
insta = "1.31.0"
|
||||||
|
|
|
@ -1,27 +1,24 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-mnemonic"
|
name = "keyfork-mnemonic-util"
|
||||||
version = "0.4.1"
|
version = "0.2.0"
|
||||||
description = "Utilities to generate and manage seeds based on BIP-0039 mnemonics."
|
description = "Utilities to generate and manage seeds based on BIP-0039 mnemonics."
|
||||||
repository = "https://git.distrust.co/public/keyfork"
|
repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["bin"]
|
default = ["bin"]
|
||||||
bin = ["smex"]
|
bin = ["smex"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
smex = { workspace = true, optional = true }
|
smex = { version = "0.1.0", path = "../smex", optional = true, registry = "distrust" }
|
||||||
keyfork-bug = { workspace = true }
|
keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
|
||||||
|
|
||||||
sha2 = { workspace = true }
|
sha2 = "0.10.7"
|
||||||
hmac = { workspace = true }
|
hmac = "0.12.1"
|
||||||
pbkdf2 = "0.12.2"
|
pbkdf2 = "0.12.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bip39 = "2.0.0"
|
bip39 = "2.0.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
serde_json = { workspace = true }
|
serde_json = "1.0.105"
|
|
@ -1,6 +1,6 @@
|
||||||
//! Generate a mnemonic from hex-encoded input.
|
//!
|
||||||
|
|
||||||
use keyfork_mnemonic::Mnemonic;
|
use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let input = std::io::stdin();
|
let input = std::io::stdin();
|
||||||
|
@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
input.read_line(&mut line)?;
|
input.read_line(&mut line)?;
|
||||||
let decoded = smex::decode(line.trim())?;
|
let decoded = smex::decode(line.trim())?;
|
||||||
|
|
||||||
let mnemonic = Mnemonic::from_raw_bytes(&decoded);
|
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&decoded) };
|
||||||
|
|
||||||
println!("{mnemonic}");
|
println!("{mnemonic}");
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue