Compare commits

..

73 Commits

Author SHA1 Message Date
Ryan Heywood d7bf3d16e1
keyfork-shard: move to blahaj 2024-11-21 17:24:06 -05:00
Ryan Heywood 9e4d5649d9
audits: add audit from NCC and Cure53 following release of `blahaj` 2024-11-21 17:21:23 -05:00
Ryan Heywood 6a3244df01
Cargo.lock: bump g2p, remove syn 1.x 2024-08-14 14:31:03 -04:00
Ryan Heywood be6d562b33
keyfork-qrcode: use image::ImageReader over image::io::Reader (deprecated) 2024-08-14 13:50:48 -04:00
Ryan Heywood 6317cc964f
Cargo.lock: bump deps, dupe generic-array :( 2024-08-12 01:07:43 -04:00
Ryan Heywood 305e070b93
Cargo.lock: bump multiple deps to deduplicate 2024-08-12 00:31:18 -04:00
Ryan Heywood 7e5c7ea8fb
Cargo.lock: bump lalrpop to remove duplicate regex-syntax 2024-08-12 00:11:25 -04:00
Ryan Heywood 63b4677b19
deny.toml: update to not use deprecated keys 2024-08-11 23:48:44 -04:00
Ryan Heywood 1d68dd19d9
fry up some bacon 2024-08-11 23:19:39 -04:00
Ryan Heywood 4ab1e8afa6
add docs to make clippy extra happy 2024-08-11 19:38:18 -04:00
Ryan Heywood a8b2814b17
make clippy happy 2024-08-11 19:25:25 -04:00
Ryan Heywood c36fe0a1b1
keyfork-shard: re-enable standard policy, alive check still disabled, add check for encryption keys when discovering certs 2024-08-11 18:57:43 -04:00
Ryan Heywood c25c11d1a0
release keyfork v0.2.4 2024-08-11 17:33:41 -04:00
Ryan Heywood e1f8ce9a97
cleanup workspace duplicated dependencies 2024-08-11 16:54:06 -04:00
Ryan Heywood b254ba7c56
cleanup post-merge 2024-08-11 14:56:36 -04:00
Ryan Heywood 58d3c34b61
Merge branch 'main' into ryansquared/staging-since-latest 2024-08-11 14:54:02 -04:00
Ryan Heywood ba64db8f00
update Cargo.toml and Cargo.lock 2024-08-08 00:56:40 -04:00
Ryan Heywood dd4354ffc1
keyfork: bump keyfork-shard 2024-08-08 00:53:15 -04:00
Ryan Heywood fa84a2ae5f
keyfork-shard: Be less strict about keys
Rationale: Keyfork Shard runs on Airgap systems. The biggest impact of
using StandardPolicy and checking whether keys are "alive" is the drift
between different Airgap systems where the keys may not be valid at the
same time. Because of this, it is impossible to shard a secret to all
keys at once using a StandardPolicy.

However, we consider these keys to be a trusted input, whether created
by a previous system or generated directly by Keyfork. Because of this,
we can use a NullPolicy to blanketly permit all keys, the same way we
blanketly permit all keys when reconstituting the sharded secret, and
disable the check for whether keys are alive (though, we are still
denying revoked keys).
2024-08-08 00:48:33 -04:00
Ryan Heywood 35f57fcc41
Merge branch 'ryansquared/keyfork-mnemonic-refactors' 2024-08-05 18:00:10 -04:00
Ryan Heywood a2eb5fda11
bump dependencies with listed vulnerabilities (not affected) 2024-08-05 17:48:19 -04:00
Ryan Heywood 5219c5a99f
keyfork: enum-trait-ify choose-your-own commands 2024-08-05 17:43:22 -04:00
Ryan Heywood b26f296a75
keyfork-derive-path-data: move all pathcrafting here 2024-08-01 11:05:46 -04:00
Ryan Heywood 35ab5e65a4
keyfork-mnemonic-util => keyfork-mnemonic 2024-08-01 09:50:30 -04:00
Ryan Heywood 3ee81b6a82
keyfork-mnemonic-util: impl as_slice to_vec into_vec 2024-08-01 09:35:04 -04:00
Ryan Heywood f5627e5bd9
keyfork-mnemonic-util: impl try_from_slice and from_array 2024-08-01 09:29:03 -04:00
Ryan Heywood 02e5b545a4
keyfork-mnemonic-util::generate_seed: return const size array 2024-08-01 09:19:07 -04:00
Ryan Heywood 536e6da5ad
keyforkd{,-client}: lots of documentationings 2024-08-01 08:59:01 -04:00
Ryan Heywood bac762f5be
release keyfork v0.2.2 and keyfork-derive-openpgp 0.1.2 2024-08-01 01:37:18 -04:00
Ryan Heywood c868afedbf
scripts/generate-dependency-queue.py: doc how to run 2024-08-01 01:30:56 -04:00
Ryan Heywood 8d40d2630c
keyfork: add `bottoms-up` wizard 2024-08-01 01:30:54 -04:00
Ryan Heywood 142bea3b9f
keyfork-shard: verify QR code length correctly 2024-05-29 16:16:55 -04:00
Ryan Heywood c65ddbf119
scripts/generate-dependency-queue.py: rewrite 2024-05-16 14:56:31 -04:00
Ryan Heywood d759982853
scripts: add publishing scripts 2024-05-16 02:01:10 -04:00
Ryan Heywood 491d19469a
crates: bump versions 2024-05-16 00:29:28 -04:00
Ryan Heywood 756be9b9d7
Merge remote-tracking branch 'origin/anton/require-min-entropy' 2024-05-05 14:49:12 -04:00
Ryan Heywood ad329131de
Merge remote-tracking branch 'origin/anton/chore/update-readme' 2024-05-05 14:49:01 -04:00
Anton Livaja bcfcc8711f
keyforkd: add warning when loading seed with less than 128 bits 2024-05-05 14:27:10 -04:00
Ryan Heywood de4e98ae07
keyfork-derive-util: black-box checking all zeroes 2024-05-03 23:28:45 -04:00
Ryan Heywood 48ccd7c68f
keyfork-derive-util: add note about potential side-channel when verifying keys 2024-05-03 23:20:53 -04:00
Ryan Heywood d04989ef30
keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct 2024-05-03 23:20:50 -04:00
Ryan Heywood 1a036a0b5f
keyfork-shard: clean up documentation for encrypted shard padding 2024-05-03 22:41:38 -04:00
Anton Livaja fc0350a098
fix: specify OpenPGP 2024-04-29 17:57:05 -04:00
Anton Livaja a18ea7ba0f
chore: make docs regarding factory reset more specific 2024-04-29 13:20:46 -04:00
Ryan Heywood e0687434ef
keyfork-shard: display error message on duplicate key fingerprints found 2024-04-24 13:29:32 -04:00
Ryan Heywood 23db50956f
keyfork-shard: improve wording for counting shardholders 2024-04-24 13:13:48 -04:00
Ryan Heywood 94617722a0
keyfork-shard: ignore duplicate certificate entries 2024-04-22 17:06:13 -04:00
Ryan Heywood 001fc0bccc
remove trailing hitespace :( 2024-04-19 00:30:38 -04:00
Ryan Heywood 6a265ad203
keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes 2024-04-18 23:53:59 -04:00
Ryan Heywood 5d2309e301
keyfork-prompt: add SecurePinValidator for making new, secure, PINs 2024-04-18 23:01:03 -04:00
Ryan Heywood c0b19e2457
keyfork-shard: assert shared secrets are contributory 2024-04-17 15:36:42 -04:00
Ryan Heywood cdf401515f
keyfork wizard: use correct derivation path for re-deriving shard decryption keys 2024-04-17 15:25:22 -04:00
Ryan Heywood f0e5ae9a8b
keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE 2024-04-17 15:25:20 -04:00
Ryan Heywood 289cec36ef
keyfork wizard: upcast i and index to avoid wrapping add 2024-04-17 15:25:19 -04:00
Ryan Heywood 0fe5301352
keyfork-shard: add in bug messages 2024-04-17 15:25:18 -04:00
Ryan Heywood 9f089e723a
keyfork-derive-openpgp: use .first() in place of .get(0) 2024-04-17 15:25:15 -04:00
Ryan Heywood 1de466cad0
keyfork-derive-util: allow zeroable input for non-master-key derivation 2024-04-17 15:25:02 -04:00
Ryan Heywood 57354fc714
Cargo.lock: bump insta, remove unmaintained yaml-rust 2024-04-14 21:27:57 -04:00
Ryan Heywood 61871a77f0
keyfork-derive-util: make private and public test keys more visible 2024-04-14 21:26:44 -04:00
Ryan Heywood 08a66e2365
keyfork-shard: base64 encode content instead of base16 2024-04-14 21:19:57 -04:00
Ryan Heywood 6fa434e89c
keyfork-shard: shorten length and pad inside encrypted block 2024-04-14 21:19:56 -04:00
Ryan Heywood 68f07f6f02
bump mio and iana-time-zone 2024-04-14 21:19:54 -04:00
Ryan Heywood 9394500f2f
keyfork-shard: generate nonce using hkdf 2024-04-14 21:19:52 -04:00
Ryan Heywood 2bca0a1580
keyfork-derive-util: make Test{Public,Private}Key public, rename Internal algorithm 2024-04-12 16:23:24 -04:00
Ryan Heywood 5438f4e111
keyfork-entropy: downgrade entropy size limit to warning 2024-04-12 16:14:41 -04:00
Ryan Heywood 71b6e4ed0c
Merge branch 'ryan/use-instant-time-qrcode' 2024-04-10 15:35:50 -04:00
Ryan Heywood 4f4e3cfc65
Merge branch 'ryan/harden-derivation-on-highest-level-keys' 2024-04-10 15:35:40 -04:00
Ryan Heywood 194d475d59
keyfork-shard: validate signatures using shard-specific validation requirements 2024-04-10 15:17:30 -04:00
Ryan Heywood 40551a5c26
keyforkd: require hardened derivation on two highest indexes 2024-04-09 20:14:59 -04:00
Ryan Heywood fa125e7cbe
keyfork-qrcode: prefer Instant over SystemTime for infallible time comparison 2024-04-09 19:54:11 -04:00
Ryan Heywood f96ad11422
docs: add basic documentation on shard remote-decrypt protocol 2024-04-08 14:44:26 -04:00
Anton Livaja 089021a302
chore: add link to airgapOS repo 2024-03-28 20:01:50 -04:00
Ryan Heywood 0c768690db
.cargo/config.toml: add registry configuration :) 2024-03-24 22:52:27 -04:00
84 changed files with 2917 additions and 2205 deletions

2
.cargo/config.toml Normal file
View File

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

1
.gitattributes vendored Normal file
View File

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

277
CHANGELOG.md Normal file
View File

@ -0,0 +1,277 @@
# 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`

1723
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

BIN
audits/DIS-01-report.v2.pdf Normal file

Binary file not shown.

92
bacon.toml Normal file
View File

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

View File

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

View File

@ -1,8 +1,10 @@
//! # The Keyforkd Client
//!
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". The server is operated
//! on a UNIX socket with messages sent using the Keyfork Frame format.
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". This allows
//! enforcement of policies, such as requiring at least two leves of a derivation path (for
//! instance, `m/0'` would not be allowed, but `m/0'/0'` would). The server is operated on a UNIX
//! socket with messages sent using the Keyfork Frame format.
//!
//! Programs using the Keyfork Client should ensure they are built against a compatible version of
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
@ -10,18 +12,23 @@
//! 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`.
//!
//! Presently, the Keyfork server only supports the following requests:
//! The Keyfork Client documentation makes extensive use of the `keyforkd::test_util` module.
//! This provides testing infrastructure to set up a temporary Keyfork Daemon. In
//! your code, you should assume the daemon has already been initialized, whether by another
//! process, on another terminal, or some other instance. At no point should a program deriving an
//! "endpoint" key have control over a mnemonic or a seed.
//!
//! * Derive Key
//! ## Server Requests
//!
//! ## Extended Private Keys
//! 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:
//!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
//! will be derived similarly between the server and the client.
//! ### 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.
//!
//! # Examples
//! ```rust
//! use std::str::FromStr;
//!
@ -31,17 +38,121 @@
//! // 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| {
//! # 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();
//! 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(())
//! }
//! ```
//!
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used.
//! ---
//!
//! Request objects are typically handled by the Keyfork Client library (such as with
//! [`Client::request_xprv`]). While unadvised, clients can also attempt to handle their own
//! requests, using [`Client::request`].
//!
//! ## Extended Private Keys
//!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
//! will be derived similarly between the server and the client.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::{DerivationIndex, DerivationPath};
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey as PrivateKey;
//! # fn check_wallet<T>(_: T) {}
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Index(#[from] keyfork_derive_util::IndexError),
//!
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! PrivateKey(#[from] keyfork_derive_util::PrivateKeyError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'/0'/0")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! // scan first 20 wallets
//! for index in 0..20 {
//! // use non-hardened derivation
//! let new_xprv = xprv.derive_child(&DerivationIndex::new(index, false)?);
//! check_wallet(new_xprv)
//! }
//! # Ok::<_, Error>(())
//! # })?;
//! Ok(())
//! }
//! ```
//!
//! ## Testing Infrastructure
//!
//! In tests, the `keyforkd::test_util` module and TestPrivateKeys can be used. These provide
//! useful utilities for writing tests that interact with the Keyfork Server without needing to
//! manually create the server for the purpose of the test. The `run_test` method can be used to
//! run a test, which can handle both returning errors and correctly translating panics (though,
//! the panics definitely won't look tidy).
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! Ok::<_, Error>(())
//! })?;
//! Ok(())
//! }
//! ```
//!
//! If you would rather write tests to panic rather than error, or would rather not deal with error
//! types, the Panicable type should be used, which will handle the Error type for the closure.
//!
//! ```rust
//! use std::str::FromStr;
@ -52,11 +163,10 @@
//!
//! 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(())
//! keyforkd::test_util::Panicable::Ok(())
//! }).unwrap();
//! ```
@ -108,6 +218,10 @@ pub enum Error {
/// An error encountered in Keyforkd.
#[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError),
/// An invalid key was returned.
#[error("Invalid key returned")]
InvalidKey,
}
#[allow(missing_docs)]
@ -161,10 +275,9 @@ impl Client {
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let mut socket = get_socket().unwrap();
/// let mut socket = get_socket()?;
/// let mut client = Client::new(socket);
/// # keyforkd::test_util::Infallible::Ok(())
/// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap();
/// ```
pub fn new(socket: UnixStream) -> Self {
@ -183,9 +296,8 @@ impl Client {
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let mut client = Client::discover_socket().unwrap();
/// # keyforkd::test_util::Infallible::Ok(())
/// let mut client = Client::discover_socket()?;
/// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap();
/// ```
pub fn discover_socket() -> Result<Self> {
@ -213,11 +325,10 @@ impl Client {
///
/// # 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(())
/// # keyforkd::test_util::Panicable::Ok(())
/// # }).unwrap();
/// ```
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
@ -234,24 +345,26 @@ impl Client {
}
let depth = path.len() as u8;
Ok(ExtendedPrivateKey::new_from_parts(
&d.data,
depth,
d.chain_code,
))
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code)
.map_err(|_| Error::InvalidKey)
}
_ => Err(Error::InvalidResponse),
}
}
}
impl Client {
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
///
/// This function does not properly assert the association between a request type and a
/// response type, and does not perform any serialization of native objects into Request or
/// Response types, and should only be used when absolutely necessary.
///
/// # Errors
/// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd.
#[doc(hidden)]
pub fn request(&mut self, req: &Request) -> Result<Response> {
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
let resp = try_decode_from(&mut self.socket)?;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,20 +12,21 @@ use keyfork_bug::bug;
#[derive(Debug, thiserror::Error)]
#[error("This error can never be instantiated")]
#[doc(hidden)]
pub struct InfallibleError {
protected: (),
}
pub enum UninstantiableError {}
/// An infallible result. This type can be used to represent a function that should never error.
/// A panicable result. This type can be used when a closure chooses to panic instead of
/// returning an error. This doesn't necessarily mean a closure _has_ to panic, and its absence
/// doesn't imply a closure _can't_ panic, but this is a useful utility function for writing tests,
/// to avoid the necessity of making custom error types.
///
/// ```rust
/// use keyforkd::test_util::Infallible;
/// use keyforkd::test_util::Panicable;
/// let closure = || {
/// Infallible::Ok(())
/// Panicable::Ok(())
/// };
/// assert!(closure().is_ok());
/// ```
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
@ -39,6 +40,8 @@ pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// runtime.
///
/// # Examples
/// The test utility provides a socket that can be connected to for deriving keys.
///
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
@ -46,6 +49,18 @@ pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
///
/// The `keyforkd-client` crate uses the `KEYFORKD_SOCKET_PATH` variable to determine the default
/// socket path. The test will export the environment variable so it may be used by default.
///
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
/// assert_eq!(std::env::var_os("KEYFORKD_SOCKET_PATH").unwrap(), path.as_os_str());
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
#[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where
@ -61,7 +76,7 @@ where
));
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
let socket_path = socket_dir.path().join("keyforkd.sock");
rt.block_on(async move {
let result = rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let server_handle = tokio::spawn({
let socket_path = socket_path.clone();
@ -82,13 +97,19 @@ where
rx.recv()
.await
.expect(bug!("can't receive server start signal from channel"));
std::env::set_var("KEYFORKD_SOCKET_PATH", &socket_path);
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
let result = test_handle.await;
server_handle.abort();
result
})
.expect(bug!("runtime could not join all threads"))
});
if let Err(e) = result {
if let Ok(reason) = e.try_into_panic() {
std::panic::resume_unwind(reason);
}
}
Ok(())
}
#[cfg(test)]
@ -98,6 +119,6 @@ mod tests {
#[test]
fn test_run_test() {
let seed = b"beefbeef";
run_test(seed, |_path| Infallible::Ok(())).expect("infallible");
run_test(seed, |_path| Panicable::Ok(())).expect("infallible");
}
}

View File

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

View File

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

View File

@ -1,13 +1,13 @@
[package]
name = "keyfork-derive-key"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", registry = "distrust" }
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", registry = "distrust" }
smex = { version = "0.1.0", path = "../../util/smex", registry = "distrust" }
thiserror = "1.0.48"
keyfork-derive-util = { workspace = true }
keyforkd-client = { workspace = true }
smex = { workspace = true }
thiserror = { workspace = true }

View File

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

View File

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

View File

@ -19,8 +19,14 @@ use sequoia_openpgp::{
Cert, Packet,
};
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
// something like Prv or PrvKey.
/// The private key type used with OpenPGP.
pub type XPrvKey = SigningKey;
pub type XPrv = ExtendedPrivateKey<SigningKey>;
/// The extended private key type used with OpenPGP.
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
/// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)]
@ -59,13 +65,17 @@ pub enum Error {
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
/// ID.
/// Create an OpenPGP Cert with private key data, with derived keys from the given derivation
/// response, keys, and User ID.
///
/// Certificates are created with a default expiration of one day, but may be configured to expire
/// later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as "15d" (15
/// days), "1m" (one month), or "2y" (two years).
///
/// # Errors
/// The function may error for any condition mentioned in [`Error`].
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.get(0) {
let primary_key_flags = match keys.first() {
Some(kf) if kf.for_certification() => kf,
_ => return Err(Error::NotCert),
};
@ -109,7 +119,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 policy = sequoia_openpgp::policy::StandardPolicy::new();
// Set certificate expiration to one day
// Set certificate expiration to configured expiration or (default) one day
let mut keypair = primary_key.clone().into_keypair()?;
let signatures =
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,10 @@ pub enum Error {
/// The given slice was of an inappropriate size to create a Private Key.
#[error("The given slice was of an inappropriate size to create a Private Key")]
InvalidSliceError(#[from] std::array::TryFromSliceError),
/// The given data was not a valid key for the chosen key type.
#[error("The given data was not a valid key for the chosen key type")]
InvalidKey,
}
type Result<T, E = Error> = std::result::Result<T, E>;
@ -124,10 +128,10 @@ mod serde_with {
K: PrivateKey + Clone,
{
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
let bytes: [u8; 32] = variable_len_bytes
.try_into()
.expect(bug!("unable to parse serialized private key; no support for static len"));
Ok(K::from_bytes(&bytes))
let bytes: [u8; 32] = variable_len_bytes.try_into().expect(bug!(
"unable to parse serialized private key; no support for static len"
));
Ok(K::from_bytes(&bytes).expect(bug!("could not deserialize key with invalid scalar")))
}
}
@ -148,13 +152,9 @@ where
/// Generate a new [`ExtendedPrivateKey`] from a seed, ideally from a 12-word or 24-word
/// mnemonic, but may take 16-byte seeds.
///
/// # Panics
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
///
/// # Errors
/// An error may be returned if:
/// * The given seed had an incorrect length.
/// * A `HmacSha512` can't be constructed.
/// The function may return an error if the derived master key could not be parsed as a key for
/// the given algorithm (such as exceeding values on the secp256k1 curve).
///
/// # Examples
/// ```rust
@ -167,11 +167,11 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// ```
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())
}
fn new_internal(seed: &[u8]) -> Self {
fn new_internal(seed: &[u8]) -> Result<Self> {
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
.expect(bug!("HmacSha512 InvalidLength should be infallible"))
.chain_update(seed)
@ -179,13 +179,27 @@ where
.into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
Self::new_from_parts(
// Verify the master key is nonzero, hopefully avoiding side-channel attacks.
let mut has_any_nonzero = false;
// deoptimize arithmetic smartness
for byte in private_key.iter().map(std::hint::black_box) {
if *byte != 0 {
// deoptimize break
has_any_nonzero = std::hint::black_box(true);
}
}
assert!(has_any_nonzero, bug!("hmac function returned all-zero master key"));
Self::from_parts(
private_key
.try_into()
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
0,
// Checked: chain_code is always the same length, hash is static size
chain_code.try_into().expect(bug!("Invalid chain code length")),
chain_code
.try_into()
.expect(bug!("Invalid chain code length")),
)
}
@ -205,13 +219,18 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
/// ```
pub fn new_from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self {
Self {
private_key: K::from_bytes(key),
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
match K::from_bytes(key) {
Ok(key) => {
Ok(Self {
private_key: key,
depth,
chain_code,
})
}
Err(_) => Err(Error::InvalidKey),
}
}
@ -225,12 +244,15 @@ where
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key));
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key)?);
/// # Ok(())
/// # }
/// ```
pub fn private_key(&self) -> &K {
&self.private_key
@ -255,14 +277,14 @@ where
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
/// # ];
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let xpub = xprv.extended_public_key();
/// assert_eq!(known_key, xpub.public_key().to_bytes());
/// # Ok(())
/// # }
/// ```
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
ExtendedPublicKey::new_from_parts(self.public_key(), self.depth, self.chain_code)
ExtendedPublicKey::from_parts(self.public_key(), self.depth, self.chain_code)
}
/// Return a public key for the current [`PrivateKey`].
@ -279,7 +301,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let pubkey = xprv.public_key();
/// # Ok(())
/// # }
@ -297,12 +319,15 @@ where
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(xprv.depth(), 4);
/// # Ok(())
/// # }
/// ```
pub fn depth(&self) -> u8 {
self.depth
@ -317,12 +342,15 @@ where
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(chain_code, &xprv.chain_code());
/// # Ok(())
/// # }
/// ```
pub fn chain_code(&self) -> [u8; 32] {
self.chain_code
@ -344,7 +372,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let path = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
@ -390,7 +418,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let bip44_wallet = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)

View File

@ -11,8 +11,8 @@ const KEY_SIZE: usize = 256;
/// Errors associated with creating or deriving Extended Public Keys.
#[derive(Error, Clone, Debug)]
pub enum Error {
/// BIP-0032 does not support deriving public keys from hardened private keys.
#[error("Public keys may not be derived when hardened")]
/// BIP-0032 does not support hardened public key derivation from parent public keys.
#[error("Hardened child public keys may not be derived from parent public keys")]
HardenedIndex,
/// 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] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let pubkey = PublicKey::from_bytes(key);
/// let xpub = ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// let xpub = ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// # Ok(())
/// # }
/// ```
pub fn new_from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
pub fn from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
Self {
public_key,
depth,
@ -86,7 +86,7 @@ where
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// let pubkey = xpub.public_key();
/// # Ok(())
/// # }
@ -121,7 +121,7 @@ where
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// let index = DerivationIndex::new(0, false)?;
/// let child = xpub.derive_child(&index)?;
/// # Ok(())

View File

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

View File

@ -30,7 +30,7 @@ pub trait PublicKey: Sized {
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// let public_key_bytes = private_key.public_key().to_bytes();
/// ```
fn to_bytes(&self) -> PublicKeyBytes;
@ -42,7 +42,7 @@ pub trait PublicKey: Sized {
/// # Errors
///
/// An error may be returned if:
/// * A nonzero `other` is provided.
/// * An all-zero `other` is provided.
/// * An error specific to the given algorithm was encountered.
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
@ -56,7 +56,7 @@ pub trait PublicKey: Sized {
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// let fingerprint = private_key.public_key().fingerprint();
/// ```
fn fingerprint(&self) -> [u8; 4] {
@ -77,6 +77,10 @@ pub enum PublicKeyError {
#[error("The provided public key must be nonzero, but is not")]
NonZero,
/// A scalar could not be constructed for the given algorithm.
#[error("A scalar could not be constructed for the given algorithm")]
InvalidScalar,
/// Public key derivation is unsupported for this algorithm.
#[error("Public key derivation is unsupported for this algorithm")]
DerivationUnsupported,
@ -85,7 +89,7 @@ pub enum PublicKeyError {
#[cfg(feature = "secp256k1")]
use k256::{
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
AffinePoint, NonZeroScalar,
AffinePoint,
};
#[cfg(feature = "secp256k1")]
@ -105,14 +109,16 @@ impl PublicKey for k256::PublicKey {
}
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
if other.iter().all(|n| n == &0) {
return Err(PublicKeyError::NonZero);
}
// Checked: See above
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
.expect(bug!("Should have been able to get a NonZeroScalar"));
use k256::elliptic_curve::ScalarPrimitive;
use k256::{Secp256k1, Scalar};
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
// Construct a scalar from bytes
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into());
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
let scalar = scalar.ok_or(PublicKeyError::InvalidScalar)?;
let scalar = Scalar::from(scalar);
let point = self.to_projective() + (AffinePoint::generator() * scalar);
Ok(Self::from_affine(point.into())
.expect(bug!("Could not from_affine after scalar arithmetic")))
}
@ -142,14 +148,15 @@ impl PublicKey for VerifyingKey {
}
}
#[doc(hidden)]
/// A public key that can be used for testing purposes. Does not utilize any significant
/// cryptographic operations.
#[derive(Clone)]
pub struct TestPublicKey {
pub(crate) key: [u8; 33],
}
impl TestPublicKey {
#[doc(hidden)]
/// Create a new TestPublicKey from the given bytes.
#[allow(dead_code)]
pub fn from_bytes(b: &[u8]) -> Self {
Self {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,23 +7,30 @@ use std::{
};
use aes_gcm::{
aead::{consts::U12, Aead, AeadCore, OsRng},
aead::{consts::U12, Aead},
Aes256Gcm, KeyInit, Nonce,
};
use base64::prelude::{Engine, BASE64_STANDARD};
use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{
validators::{mnemonic::MnemonicSetValidator, Validator},
validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator,
},
Message as PromptMessage, PromptHandler, Terminal,
};
use sha2::Sha256;
use sharks::{Share, Sharks};
use blahaj::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
const ENC_LEN: u8 = 4 * 16;
const PLAINTEXT_LENGTH: u8 = 32 // shard
+ 1 // index
+ 1 // threshold
+ 1 // version
+ 1; // length;
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
#[cfg(feature = "openpgp")]
pub mod openpgp;
@ -66,52 +73,6 @@ pub trait Format {
/// A type representing the parsed, but encrypted, Shard data.
type EncryptedData;
/// Provision hardware with a deterministic key based on a shardholder's DerivationIndex.
///
/// The derivation path for provisioned shardholder keys is built using the following template:
/// `m / purpose ' / shard_index ' / shardholder_index '`.
///
/// Purpose is defined by the Format, and can be a four-byte sequence transformed into a u32
/// using `u32::from_be_bytes(*purpose)`. For OpenPGP, for legacy reasons, this purpose is
/// "\x00pgp". The purpose can be _any_ sequence of four bytes so long as the _first_ byte is
/// not higher than 0x80 (meaning, all ASCII / 7-bit characters are allowed).
///
/// The shard index is provided by Keyfork, and is equivalent to b"shrd".
///
/// The shardholder index is how Keyfork is able to recreate keys for specific shardholders -
/// the only necessary information is which shardholder is not accounted for. Shardholders are
/// encouraged to mark hardware with the shardholder number so shardholders can verify their
/// index.
fn provision_shardholder_key(
&self,
derivation_path: DerivationPath,
seed: &[u8],
) -> Result<(), Self::Error>;
/// Return a DerivationIndex for the Format.
///
/// The derivation path for provisioned shardholder keys is built using the following template:
/// `m / purpose ' / shard_index ' / shardholder_index '`.
///
/// Purpose is defined by the Format, and can be a four-byte sequence transformed into a u32
/// using `u32::from_be_bytes(*purpose)`. For OpenPGP, for legacy reasons, this purpose is
/// "\x00pgp". The purpose can be _any_ sequence of four bytes so long as the _first_ byte is
/// not higher than 0x80 (meaning, all ASCII / 7-bit characters are allowed).
fn purpose_derivation_index(&self) -> DerivationIndex;
/// Create a shardholder derivation path for the given format.
///
/// The derivation path for provisioned shardholder keys is built using the following template:
/// `m / purpose ' / shard_index ' / shardholder_index '`.
fn create_derivation_path(&self, shardholder_index: DerivationIndex) -> DerivationPath {
let purpose = self.purpose_derivation_index();
let shard_index = DerivationIndex::new(u32::from_be_bytes(*b"shrd"), true).unwrap();
DerivationPath::default()
.chain_push(purpose)
.chain_push(shard_index)
.chain_push(shardholder_index)
}
/// Derive a signer
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
@ -241,7 +202,6 @@ pub trait Format {
let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera
@ -251,12 +211,13 @@ pub trait Format {
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
{
let decoded_data = smex::decode(&hex)?;
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
} else {
prompt
.lock()
@ -266,43 +227,43 @@ pub trait Format {
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
let their_pubkey = match pubkey_data {
Some(pubkey) => pubkey,
None => {
let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)),
};
let [nonce_mnemonic, pubkey_mnemonic] = prompt
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
(nonce, pubkey)
.map_err(|_| InvalidData)?
}
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
assert!(
shared_secret.was_contributory(),
bug!("shared secret might be insecure")
);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
// decrypt a single shard and create the payload
let (share, threshold) =
@ -311,49 +272,47 @@ pub trait Format {
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
assert!(
payload.len() <= ENC_LEN as usize,
"invalid share length (too long, max {ENC_LEN} bytes)"
payload.len() < PLAINTEXT_LENGTH as usize,
"invalid share length (too long, must be less than {PLAINTEXT_LENGTH} bytes)"
);
// encrypt data
let nonce = Nonce::<U12>::from_slice(&nonce);
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
// convert data to a static-size payload
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
// convert plaintext to static-size payload
#[allow(clippy::assertions_on_constants)]
{
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
}
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
// duplicate mnemonic words. This version does not include that, and instead uses a
// repeated length byte.
#[allow(clippy::cast_possible_truncation)]
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
assert!(
payload_bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit"
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
"previously asserted length must be < {PLAINTEXT_LENGTH}",
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
)); PLAINTEXT_LENGTH as usize];
plaintext_bytes[..payload.len()].clone_from_slice(&payload);
// encrypt data
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?;
assert_eq!(
encrypted_bytes.len(),
ENCRYPTED_LENGTH as usize,
bug!("encrypted bytes size != expected len"),
);
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
mnemonic_bytes.copy_from_slice(&encrypted_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) };
let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes);
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
@ -448,7 +407,7 @@ pub struct InvalidData;
/// 1 byte: Version
/// 1 byte: Threshold
/// Data: &[u8]
pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_VERSION: u8 = 2;
pub(crate) const HUNK_OFFSET: usize = 2;
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
@ -479,22 +438,22 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
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 key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = nonce_mnemonic.to_bytes();
qrcode_data.extend(key_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
let qrcode_data = key_mnemonic.to_bytes();
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code to only shardholder {iter}. ",
"Nobody else should scan this QR code."
"QR code #{iter} will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ",
"Only the next shardholder should scan the QR code."
),
iter = iter
)))?;
@ -504,11 +463,9 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"Upon request, these words should be sent to shardholder {iter}: ",
"{nonce_mnemonic} {key_mnemonic}"
"Upon request, these words should be sent to the shardholder: ",
"{key_mnemonic}"
),
iter = iter,
nonce_mnemonic = nonce_mnemonic,
key_mnemonic = key_mnemonic,
)))?;
@ -518,10 +475,18 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
{
let decoded_data = smex::decode(&hex)?;
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
assert_eq!(
decoded_data.len(),
// Include length of public key
ENCRYPTED_LENGTH as usize + 32,
bug!("invalid payload data")
);
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec());
} else {
@ -533,7 +498,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
(Some(pubkey), Some(payload)) => (pubkey, payload),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [24, 48],
word_lengths: [24, 39],
};
let [pubkey_mnemonic, payload_mnemonic] = pm
@ -551,14 +516,28 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
}
};
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
assert_eq!(
payload.len(),
ENCRYPTED_LENGTH as usize,
bug!("invalid payload data")
);
let payload =
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
assert!(
shared_secret.was_contributory(),
bug!("shared secret might be insecure")
);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
match &mut iter_count {
@ -573,7 +552,8 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
}
}
shares.push(payload[HUNK_OFFSET..].to_vec());
let payload_len = payload.last().expect(bug!("payload should not be empty"));
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
}
let shares = shares

View File

@ -1,617 +0,0 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::expect_fun_call)]
use std::{
io::{stdin, stdout, Read, Write},
sync::{Arc, Mutex},
};
use aes_gcm::{
aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce,
};
use hkdf::Hkdf;
<<<<<<< HEAD
use keyfork_bug::{bug, POISONED_MUTEX};
||||||| parent of 1b30b17 (keyfork-shard: begin work on (re)provisioning shardholder keys)
=======
use keyfork_derive_util::{DerivationIndex, DerivationPath};
>>>>>>> 1b30b17 (keyfork-shard: begin work on (re)provisioning shardholder keys)
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{
validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, PromptHandler, Terminal,
};
use sha2::Sha256;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
const ENC_LEN: u8 = 4 * 16;
#[cfg(feature = "openpgp")]
pub mod openpgp;
/// A trait to specify where keys can be discovered from, such as a Rust-native type or a path on
/// the filesystem that keys may be read from.
pub trait KeyDiscovery<F: Format + ?Sized> {
/// Discover public keys for the associated format.
///
/// # Errors
/// The method may return an error if public keys could not be loaded from the given discovery
/// mechanism. A discovery mechanism _must_ be able to detect public keys.
fn discover_public_keys(&self) -> Result<Vec<F::PublicKey>, F::Error>;
/// Discover private keys for the associated format.
///
/// # Errors
/// The method may return an error if private keys could not be loaded from the given
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
}
/// A format to use for splitting and combining secrets.
pub trait Format {
/// The error type returned from any failed operations.
type Error: std::error::Error + 'static;
/// A type encapsulating a single public key recipient.
type PublicKey;
/// A type encapsulating the private key recipients of shards.
type PrivateKeyData;
/// A type representing a Signer derived from the secret.
type SigningKey;
/// A type representing the parsed, but encrypted, Shard data.
type EncryptedData;
/// Provision hardware with a deterministic key based on a shardholder's DerivationIndex.
///
/// The derivation path for provisioned shardholder keys is built using the following template:
/// `m / purpose ' / shard_index ' / shardholder_index '`.
///
/// Purpose is defined by the Format, and can be a four-byte sequence transformed into a u32
/// using `u32::from_be_bytes(*purpose)`. For OpenPGP, for legacy reasons, this purpose is
/// "\x00pgp". The purpose can be _any_ sequence of four bytes so long as the _first_ byte is
/// not higher than 0x80 (meaning, all ASCII / 7-bit characters are allowed).
///
/// The shard index is provided by Keyfork, and is equivalent to b"shrd".
///
/// The shardholder index is how Keyfork is able to recreate keys for specific shardholders -
/// the only necessary information is which shardholder is not accounted for. Shardholders are
/// encouraged to mark hardware with the shardholder number so shardholders can verify their
/// index.
fn provision_shardholder_key(
&self,
derivation_path: DerivationPath,
seed: &[u8],
) -> Result<(), Self::Error>;
/// Return a DerivationIndex for the Format.
///
/// The derivation path for provisioned shardholder keys is built using the following template:
/// `m / purpose ' / shard_index ' / shardholder_index '`.
///
/// Purpose is defined by the Format, and can be a four-byte sequence transformed into a u32
/// using `u32::from_be_bytes(*purpose)`. For OpenPGP, for legacy reasons, this purpose is
/// "\x00pgp". The purpose can be _any_ sequence of four bytes so long as the _first_ byte is
/// not higher than 0x80 (meaning, all ASCII / 7-bit characters are allowed).
fn purpose_derivation_index(&self) -> DerivationIndex;
/// Create a shardholder derivation path for the given format.
///
/// The derivation path for provisioned shardholder keys is built using the following template:
/// `m / purpose ' / shard_index ' / shardholder_index '`.
fn create_derivation_path(&self, shardholder_index: DerivationIndex) -> DerivationPath {
let purpose = self.purpose_derivation_index();
let shard_index = DerivationIndex::new(u32::from_be_bytes(*b"shrd"), true).unwrap();
DerivationPath::default()
.chain_push(purpose)
.chain_push(shard_index)
.chain_push(shardholder_index)
}
/// Derive a signer
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
/// will use the same key_data for both, ensuring an iteration of this method will match with
/// iterations in methods called later.
///
/// # Errors
/// The method may return an error if encryption to any of the public keys fails.
fn format_encrypted_header(
&self,
signing_key: &Self::SigningKey,
key_data: &[Self::PublicKey],
threshold: u8,
) -> Result<Self::EncryptedData, Self::Error>;
/// Format a shard encrypted to the given public key, signing with the private key.
///
/// # Errors
/// The method may return an error if the public key used to encrypt the shard is unsuitable
/// for encryption, or if an error occurs while encrypting.
fn encrypt_shard(
&self,
shard: &[u8],
public_key: &Self::PublicKey,
signing_key: &mut Self::SigningKey,
) -> Result<Self::EncryptedData, Self::Error>;
/// Parse the Shard file into a processable type.
///
/// # Errors
/// The method may return an error if the Shard file could not be read from or if the Shard
/// file could not be properly parsed.
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Vec<Self::EncryptedData>, Self::Error>;
/// Write the Shard data to a Shard file.
///
/// # Errors
/// The method may return an error if the Shard data could not be properly serialized or if the
/// Shard file could not be written to.
fn format_shard_file(
&self,
encrypted_data: &[Self::EncryptedData],
shard_file: impl Write + Send + Sync,
) -> Result<(), Self::Error>;
/// Decrypt shares and associated metadata from a readable input. For the current version of
/// Keyfork, the only associated metadata is a u8 representing the threshold to combine
/// secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if all shards
/// could not be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a reaable input. For the current
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
/// combine secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if a shard could not
/// be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Share, u8), Self::Error>;
/// Decrypt multiple shares and combine them to recreate a secret.
///
/// # Errors
/// The method may return an error if the shares can't be decrypted or if the shares can't
/// be combined into a secret.
fn decrypt_all_shards_to_secret(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: impl PromptHandler,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
let (shares, threshold) = self.decrypt_all_shards(
private_keys,
&encrypted_messages,
Arc::new(Mutex::new(prompt)),
)?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
Ok(secret)
}
/// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the
/// shard to the AES key.
///
/// # Errors
/// The method may return an error if a share can't be decrypted. The method will not return an
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
/// QR code; instead, a mnemonic prompt will be used.
fn decrypt_one_shard_for_transport(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: impl PromptHandler,
) -> Result<(), Box<dyn std::error::Error>> {
let prompt = Arc::new(Mutex::new(prompt));
// parse input
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera
#[cfg(feature = "qrcode")]
{
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
{
let decoded_data = smex::decode(&hex)?;
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
} else {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
};
let [nonce_mnemonic, pubkey_mnemonic] = prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
(nonce, pubkey)
}
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
// decrypt a single shard and create the payload
let (share, threshold) =
self.decrypt_one_shard(private_keys, &encrypted_messages, prompt.clone())?;
let mut payload = Vec::from(&share);
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
assert!(
payload.len() <= ENC_LEN as usize,
"invalid share length (too long, max {ENC_LEN} bytes)"
);
// encrypt data
let nonce = Nonce::<U12>::from_slice(&nonce);
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
// 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")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ",
"Nobody else should scan this QR code."
)
.to_string(),
))?;
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Data(qrcode))?;
}
}
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?;
Ok(())
}
/// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing
/// threshold.
///
/// # Errors
/// The method may return an error if the shares can't be encrypted.
fn shard_and_encrypt(
&self,
threshold: u8,
max: u8,
secret: &[u8],
public_key_discovery: impl KeyDiscovery<Self>,
writer: impl Write + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut signing_key = self.derive_signing_key(secret);
let sharks = Sharks(threshold);
let dealer = sharks.dealer(secret);
let public_keys = public_key_discovery.discover_public_keys()?;
assert!(
public_keys.len() < u8::MAX as usize,
"must have less than u8::MAX public keys"
);
assert_eq!(
max,
public_keys.len() as u8,
"max must be equal to amount of public keys"
);
let max = public_keys.len() as u8;
assert!(max >= threshold, "threshold must not exceed max keys");
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
let mut messages = vec![header];
for (pk, share) in public_keys.iter().zip(dealer) {
let shard = Vec::from(&share);
messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?);
}
self.format_shard_file(&messages, writer)?;
Ok(())
}
}
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
#[derive(thiserror::Error, Debug)]
pub enum SharksError {
/// A Shamir Share could not be created.
#[error("Error creating share: {0}")]
Share(String),
/// The Shamir shares could not be combined.
#[error("Error combining shares: {0}")]
CombineShare(String),
}
/// The mnemonic or QR code used to transport an encrypted shard did not store the correct amount
/// of data.
#[derive(thiserror::Error, Debug)]
#[error("Mnemonic or QR code did not store enough data")]
pub struct InvalidData;
/// Decrypt hunk version 1:
/// 1 byte: Version
/// 1 byte: Threshold
/// Data: &[u8]
pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_OFFSET: usize = 2;
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
const QRCODE_TIMEOUT: u64 = 60; // One minute
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry.";
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
/// shares, and combine them.
///
/// # Errors
/// The function may error if:
/// * Prompting for transport-encrypted shards fails.
/// * Decrypting shards fails.
/// * Combining shards fails.
///
/// # Panics
/// The function may panic if it is given payloads generated using a version of Keyfork that is
/// incompatible with the currently running version.
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?;
let mut iter_count = None;
let mut shares = vec![];
let mut threshold = 0;
let mut iter = 0;
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
let our_key = EphemeralSecret::random();
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = nonce_mnemonic.to_bytes();
qrcode_data.extend(key_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code to only shardholder {iter}. ",
"Nobody else should scan this QR code."
),
iter = iter
)))?;
pm.prompt_message(PromptMessage::Data(qrcode))?;
}
}
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"Upon request, these words should be sent to shardholder {iter}: ",
"{nonce_mnemonic} {key_mnemonic}"
),
iter = iter,
nonce_mnemonic = nonce_mnemonic,
key_mnemonic = key_mnemonic,
)))?;
let mut pubkey_data: Option<[u8; 32]> = None;
let mut payload_data = None;
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
{
let decoded_data = smex::decode(&hex)?;
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec());
} else {
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
}
let (pubkey, payload) = match (pubkey_data, payload_data) {
(Some(pubkey), Some(payload)) => (pubkey, payload),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [24, 48],
};
let [pubkey_mnemonic, payload_mnemonic] = pm
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let payload = payload_mnemonic.to_bytes();
(pubkey, payload)
}
};
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let payload =
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
match &mut iter_count {
Some(n) => {
// Must be > 0 to start loop, can't go lower
*n -= 1;
}
None => {
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
threshold = payload[1];
let _ = iter_count.insert(threshold - 1);
}
}
shares.push(payload[HUNK_OFFSET..].to_vec());
}
let shares = shares
.into_iter()
.map(|s| Share::try_from(s.as_slice()))
.collect::<Result<Vec<_>, &str>>()
.map_err(|e| SharksError::Share(e.to_string()))?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
/*
* Verification would take up too much size, mnemonic would be very large
let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str("m/7366512'/0'")?,
)
.derive_with_master_seed(secret.to_vec())?;
let derived_cert = keyfork_derive_openpgp::derive(
kdr,
&[KeyFlags::empty().set_certification().set_signing()],
userid,
)?;
// NOTE: Signatures on certs will be different. Compare fingerprints instead.
let derived_fp = derived_cert.fingerprint();
let expected_fp = root_cert.fingerprint();
if derived_fp != expected_fp {
return Err(Error::InvalidSecret(derived_fp, expected_fp));
}
*/
w.write_all(&secret)?;
Ok(())
}

View File

@ -13,7 +13,7 @@ use std::{
use keyfork_bug::bug;
use keyfork_derive_openpgp::{
derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed},
derive_util::{DerivationPath, VariableLengthSeed},
XPrv,
};
use keyfork_prompt::PromptHandler;
@ -25,7 +25,7 @@ use openpgp::{
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
Parse,
},
policy::{NullPolicy, Policy, StandardPolicy},
policy::{NullPolicy, StandardPolicy, Policy},
serialize::{
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal,
@ -34,7 +34,7 @@ use openpgp::{
KeyID, PacketPile,
};
pub use sequoia_openpgp as openpgp;
use sharks::Share;
use blahaj::Share;
mod keyring;
use keyring::Keyring;
@ -77,6 +77,10 @@ pub enum Error {
/// An IO error occurred.
#[error("IO error: {0}")]
Io(#[source] std::io::Error),
/// No valid keys were found for the given recipient.
#[error("No valid keys were found for the recipient {0}")]
NoValidKeys(KeyID),
}
#[allow(missing_docs)]
@ -181,7 +185,7 @@ impl EncryptedMessage {
}
}
///
/// Encoding and decoding shards using OpenPGP.
pub struct OpenPGP<P: PromptHandler> {
p: PhantomData<P>,
}
@ -194,33 +198,59 @@ impl<P: PromptHandler> OpenPGP<P> {
}
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.
/// 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.
/// Certificates with duplicated fingerprints will be discarded.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
/// The function may return an error if it is unable to read the directory or if Sequoia is
/// unable to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
let mut pubkeys = std::collections::HashSet::new();
let mut certs = HashMap::new();
if path.is_file() {
let mut vec = vec![];
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
vec.push(cert.map_err(Error::Sequoia)?);
for maybe_cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
let cert = maybe_cert.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);
}
certs.insert(certfp, cert);
}
Ok(vec)
} else {
let mut vec = vec![];
for entry in path
.read_dir()
.map_err(Error::Io)?
.filter_map(Result::ok)
.filter(|p| p.path().is_file())
{
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
let cert = 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}");
}
Ok(vec)
pubkeys.insert(fp);
}
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())
}
}
@ -233,18 +263,6 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
type SigningKey = Cert;
type EncryptedData = EncryptedMessage;
fn provision_shardholder_key(
&self,
derivation_path: DerivationPath,
seed: &[u8],
) -> Result<(), Self::Error> {
todo!()
}
fn purpose_derivation_index(&self) -> DerivationIndex {
DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true).unwrap()
}
/// Derive an OpenPGP Shard certificate from the given seed.
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
let seed = VariableLengthSeed::new(seed);
@ -252,6 +270,7 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
let userid = UserID::from("keyfork-sss");
let path = DerivationPath::from_str("m/7366512'/0'").expect(bug!("valid derivation path"));
let xprv = XPrv::new(seed)
.expect(bug!("could not create XPrv from key"))
.derive_path(&path)
.expect(bug!("valid derivation"));
keyfork_derive_openpgp::derive(
@ -569,7 +588,8 @@ fn get_encryption_keys<'a>(
openpgp::packet::key::UnspecifiedRole,
> {
cert.keys()
.alive()
// NOTE: this causes complications on Airgap systems
// .alive()
.revoked(false)
.supported()
.for_storage_encryption()

View File

@ -84,13 +84,23 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
aead_algo,
} => {}
MessageLayer::SignatureGroup { results } => {
for result in results {
if let Err(e) = result {
match &results[..] {
[Ok(_)] => {
return Ok(());
}
_ => {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
}
}
/*
for result in results {
if let Err(e) = result {
return Err(anyhow::anyhow!("Invalid signature: {e}"));
}
}
*/
}
}
}

View File

@ -193,12 +193,23 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
aead_algo,
} => {}
MessageLayer::SignatureGroup { results } => {
match &results[..] {
[Ok(_)] => {
return Ok(());
}
_ => {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
}
}
/*
for result in results {
if let Err(e) = result {
// FIXME: anyhow leak
return Err(anyhow::anyhow!("Verification error: {}", e.to_string()));
return Err(anyhow::anyhow!("Invalid signature: {e}"));
}
}
*/
}
}
}
@ -264,8 +275,8 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
} else {
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
};
let temp_pin =
self.pm
let temp_pin = self
.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_passphrase(&message, 3, &pin_validator)?;

View File

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

View File

@ -1,5 +1,5 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use clap::{Args, Parser, Subcommand};
use keyfork_derive_openpgp::{
openpgp::{
@ -10,7 +10,8 @@ use keyfork_derive_openpgp::{
},
XPrvKey,
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_derive_util::DerivationIndex;
use keyfork_derive_path_data::paths;
use keyforkd_client::Client;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -20,25 +21,33 @@ pub enum DeriveSubcommands {
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
/// ASCII Armor, a format usable by most programs using OpenPGP.
///
/// The key is generated with a 24-hour expiration time. The operation to set the expiration
/// time to a higher value is left to the user to ensure the key is usable by the user.
/// Certificates are created with a default expiration of one day, but may be configured to
/// expire later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as
/// "15d" (15 days), "1m" (one month), or "2y" (two years).
///
/// It is recommended to use the default expiration of one day and to change the expiration
/// using an external utility, to ensure the Certify key is usable.
#[command(name = "openpgp")]
OpenPGP {
OpenPGP(OpenPGP)
}
#[derive(Args, Clone, Debug)]
pub struct OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String,
},
}
impl DeriveSubcommands {
fn handle(&self, account: DerivationIndex) -> Result<()> {
match self {
DeriveSubcommands::OpenPGP { user_id } => {
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let path = DerivationPath::default()
.chain_push(chain)
.chain_push(account);
DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account),
}
}
}
impl OpenPGP {
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
let path = paths::OPENPGP.clone().chain_push(account);
// TODO: should this be customizable?
let subkeys = vec![
KeyFlags::empty().set_certification(),
@ -49,19 +58,16 @@ impl DeriveSubcommands {
KeyFlags::empty().set_authentication(),
];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(user_id.as_str());
let default_userid = UserID::from(self.user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
for packet in cert.into_packets() {
for packet in cert.into_packets2() {
packet.serialize(&mut w)?;
}
w.finalize()?;
}
}
Ok(())
}
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-qrcode"
version = "0.1.0"
version = "0.1.1"
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
@ -14,9 +14,9 @@ decode-backend-rqrr = ["dep:rqrr"]
decode-backend-zbar = ["dep:keyfork-zbar"]
[dependencies]
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true, registry = "distrust" }
image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
rqrr = { version = "0.6.0", optional = true }
thiserror = "1.0.56"
v4l = "0.14.0"
keyfork-bug = { workspace = true }
keyfork-zbar = { workspace = true, optional = true }
image = { workspace = true, default-features = false, features = ["jpeg"] }
rqrr = { version = "0.7.0", optional = true }
thiserror = { workspace = true }
v4l = { workspace = true }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,17 +3,17 @@
//! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase:
//!
//! ```rust
//! use keyfork_mnemonic_util::Mnemonic;
//! use keyfork_mnemonic::Mnemonic;
//! let data = b"Hello, world! I am a mnemonic :)";
//! assert_eq!(data.len(), 32);
//! let mnemonic = Mnemonic::from_bytes(data).unwrap();
//! let mnemonic = Mnemonic::try_from_slice(data).unwrap();
//! println!("Our mnemonic is: {mnemonic}");
//! ```
//!
//! A mnemonic can also be parsed from a string:
//!
//! ```rust
//! use keyfork_mnemonic_util::Mnemonic;
//! use keyfork_mnemonic::Mnemonic;
//! use std::str::FromStr;
//!
//! let data = b"Hello, world! I am a mnemonic :)";
@ -28,7 +28,7 @@
//! verified to be safe:
//!
//! ```rust
//! use keyfork_mnemonic_util::Mnemonic;
//! use keyfork_mnemonic::Mnemonic;
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
//! let mnemonic_text = mnemonic.to_string();
@ -37,7 +37,7 @@
//! If given an invalid length, undefined behavior may follow, or code may panic.
//!
//! ```rust,should_panic
//! use keyfork_mnemonic_util::Mnemonic;
//! use keyfork_mnemonic::Mnemonic;
//! use std::str::FromStr;
//!
//! // NOTE: Data is of invalid length, 31
@ -125,6 +125,13 @@ impl Wordlist for English {
}
}
struct AssertValidMnemonicSize<const N: usize>;
impl<const N: usize> AssertValidMnemonicSize<N> {
const OK_CHUNKS: () = assert!(N % 4 == 0, "bytes must be a length divisible by 4");
const OK_SIZE: () = assert!(N <= 1024, "bytes must be less-or-equal 1024");
}
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MnemonicBase<W: Wordlist> {
@ -261,11 +268,11 @@ where
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic_util::Mnemonic;
/// use keyfork_mnemonic::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = Mnemonic::from_bytes(data.as_slice()).unwrap();
/// let mnemonic = Mnemonic::try_from_slice(data.as_slice()).unwrap();
/// ```
pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
pub fn try_from_slice(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
let bit_count = bytes.len() * 8;
if bit_count % 32 != 0 {
@ -276,31 +283,51 @@ where
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
}
Ok(unsafe { Self::from_raw_bytes(bytes) })
Ok( Self::from_raw_bytes(bytes) )
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data may be of a size
/// of a factor of 4, up to 1024 bytes.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::from_bytes"]
pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::from_bytes(bytes)
/// ```rust
/// use keyfork_mnemonic::Mnemonic;
/// let data = b"hello world!";
/// let mnemonic = Mnemonic::from_array(*data);
/// ```
///
/// If an invalid size is requested, the code will fail to compile:
///
/// ```rust,compile_fail
/// use keyfork_mnemonic::Mnemonic;
/// let mnemonic = Mnemonic::from_array([0u8; 53]);
/// ```
///
/// ```rust,compile_fail
/// use keyfork_mnemonic::Mnemonic;
/// let mnemonic = Mnemonic::from_array([0u8; 1024 + 4]);
/// ```
pub fn from_array<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
#[allow(clippy::let_unit_value)]
{
let () = AssertValidMnemonicSize::<N>::OK_CHUNKS;
let () = AssertValidMnemonicSize::<N>::OK_SIZE;
}
Self::from_raw_bytes(&bytes)
}
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
///
/// # Safety
///
/// # Panics
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`. If the assumption is incorrect, code may panic.
/// == 0`. If the assumption is incorrect, code may panic. The
/// [`MnemonicBase::from_array`] function may be used to generate entropy if the length of the
/// data is known at compile-time.
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic_util::Mnemonic;
/// use keyfork_mnemonic::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string();
@ -309,17 +336,16 @@ where
/// If given an invalid length, undefined behavior may follow, or code may panic.
///
/// ```rust,should_panic
/// use keyfork_mnemonic_util::Mnemonic;
/// use keyfork_mnemonic::Mnemonic;
/// use std::str::FromStr;
///
/// // NOTE: Data is of invalid length, 31
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string();
/// // NOTE: panic happens here
/// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
/// ```
pub unsafe fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
pub fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
assert!(bytes.len() % 4 == 0);
assert!(bytes.len() <= 1024);
MnemonicBase {
data: bytes.to_vec(),
marker: PhantomData,
@ -347,16 +373,31 @@ where
&self.data
}
/// A view to internal representation of the decoded data.
pub fn as_slice(&self) -> &[u8] {
&self.data
}
/// A clone of the internal representation of the decoded data.
pub fn to_bytes(&self) -> Vec<u8> {
self.data.to_vec()
}
/// A clone of the internal representation of the decoded data.
pub fn to_vec(&self) -> Vec<u8> {
self.data.to_vec()
}
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_bytes(self) -> Vec<u8> {
self.data
}
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_vec(self) -> Vec<u8> {
self.data
}
/// Clone the existing data.
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> {
@ -372,7 +413,7 @@ where
&self,
passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> {
Ok(self.generate_seed(passphrase))
Ok(self.generate_seed(passphrase).to_vec())
}
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
@ -380,8 +421,7 @@ where
/// # Panics
/// The function may panic if the HmacSha512 function returns an error. The only error the
/// HmacSha512 function should return is an invalid length, which should not be possible.
///
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> Vec<u8> {
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> [u8; 64] {
let passphrase = passphrase.into();
let mut seed = [0u8; 64];
@ -389,7 +429,7 @@ where
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.expect(bug!("HmacSha512 InvalidLength should be infallible"));
seed.to_vec()
seed
}
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
@ -426,6 +466,39 @@ where
}
}
impl<W> MnemonicBase<W>
where
W: Wordlist,
{
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::try_from_slice"]
pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::try_from_slice(bytes)
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::try_from_slice"]
pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::try_from_slice(bytes)
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data may be of a size
/// of a factor of 4, up to 1024 bytes.
///
#[deprecated = "Use Mnemonic::from_array"]
pub fn from_nonstandard_bytes<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
MnemonicBase::from_array(bytes)
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, fs::File, io::Read};
@ -442,7 +515,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let mnemonic = super::Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let mnemonic = super::Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let new_entropy = mnemonic.as_bytes();
assert_eq!(new_entropy, entropy);
}
@ -458,7 +531,7 @@ mod tests {
};
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
let mnemonic = Mnemonic::from_bytes(&hex).unwrap();
let mnemonic = Mnemonic::try_from_slice(&hex).unwrap();
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
}
@ -469,7 +542,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let my_mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let my_mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
@ -493,7 +566,7 @@ mod tests {
for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
let words = mnemonic.words();
hs.clear();
hs.extend(words);
@ -520,12 +593,30 @@ mod tests {
}
#[test]
fn can_do_up_to_1024_bits() {
let entropy = &mut [0u8; 128];
fn can_do_up_to_8192_bits() {
let mut entropy = [0u8; 1024];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&entropy[..]) };
let mnemonic = Mnemonic::from_array(entropy);
let words = mnemonic.words();
assert!(words.len() == 96);
assert_eq!(words.len(), 768);
}
#[test]
#[should_panic]
fn fails_over_8192_bits() {
let entropy = &mut [0u8; 1024 + 4];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
}
#[test]
#[should_panic]
fn fails_over_invalid_size() {
let entropy = &mut [0u8; 255];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
}
}

View File

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

View File

@ -1,4 +1,4 @@
//!
#![allow(missing_docs)]
use std::io::{stdin, stdout};
@ -7,7 +7,7 @@ use keyfork_prompt::{
Terminal, PromptHandler,
};
use keyfork_mnemonic_util::English;
use keyfork_mnemonic::English;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut mgr = Terminal::new(stdin(), stdout())?;

View File

@ -3,7 +3,7 @@
use std::borrow::Borrow;
#[cfg(feature = "mnemonic")]
use keyfork_mnemonic_util::Wordlist;
use keyfork_mnemonic::Wordlist;
///
pub mod terminal;

View File

@ -1,3 +1,9 @@
//! A terminal prompt handler.
//!
//! This prompt handler uses a raw terminal device to read inputs and uses ANSI escape codes to
//! provide formatting for prompts. Because of these reasons, it is not intended to be
//! machine-readable.
use std::{
borrow::Borrow,
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},

View File

@ -29,6 +29,84 @@ pub enum PinError {
/// The PIN contained invalid characters.
#[error("PIN contained invalid characters (found {0} at position {1})")]
InvalidCharacters(char, usize),
/// The provided PIN had either too many repeated characters or too many sequential characters.
#[error("PIN contained too many repeated or sequential characters")]
InsecurePIN,
}
/// Validate that a PIN is of a certain length, matches a range of characters, and does not use
/// incrementing or decrementing sequences of characters.
///
/// The validator determines a score for a passphrase and, if the score is high enough, returns an
/// error.
///
/// Score is calculated based on:
/// * how many sequential characters are in the passphrase (ascending or descending)
/// * how many repeated characters are in the passphrase
#[derive(Default, Clone)]
pub struct SecurePinValidator {
/// The minimum length of provided PINs.
pub min_length: Option<usize>,
/// The maximum length of provided PINs.
pub max_length: Option<usize>,
/// The characters allowed by the PIN parser.
pub range: Option<RangeInclusive<char>>,
/// Whether repeated characters count against the PIN.
pub ignore_repeated_characters: bool,
/// Whether sequential characters count against the PIN.
pub ignore_sequential_characters: bool,
}
impl Validator for SecurePinValidator {
type Output = String;
type Error = PinError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
let min_len = self.min_length.unwrap_or(usize::MIN);
let max_len = self.max_length.unwrap_or(usize::MAX);
let range = self.range.clone().unwrap_or('0'..='9');
let ignore_repeated_characters = self.ignore_repeated_characters;
let ignore_sequential_characters = self.ignore_sequential_characters;
Box::new(move |mut s: String| {
s.truncate(s.trim_end().len());
let len = s.len();
if len < min_len {
return Err(Box::new(PinError::TooShort(len, min_len)));
}
if len > max_len {
return Err(Box::new(PinError::TooLong(len, max_len)));
}
let mut last_char = 0;
let mut score = 0;
for (index, ch) in s.chars().enumerate() {
if !range.contains(&ch) {
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
}
if [-1, 1].contains(&(ch as i32 - last_char))
&& !ignore_sequential_characters
{
score += 1;
}
last_char = ch as i32;
}
let mut chars = s.chars().collect::<Vec<_>>();
chars.sort();
chars.dedup();
if !ignore_repeated_characters {
// SAFETY: the amount of characters can't have _increased_ since deduping
score += s.chars().count() - chars.len();
}
if score * 2 > s.chars().count() {
return Err(Box::new(PinError::InsecurePIN))
}
Ok(s)
})
}
}
/// Validate that a PIN is of a certain length and matches a range of characters.
@ -79,8 +157,8 @@ pub mod mnemonic {
use super::Validator;
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
use keyfork_bug::bug;
use keyfork_mnemonic::{Mnemonic, MnemonicFromStrError};
/// A mnemonic could not be validated from the given input.
#[derive(thiserror::Error, Debug)]

View File

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

101
deny.toml
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,37 @@
import json
import sys
# pipe `cargo metadata --format-version=1` into this
priority_queue = []
packages = json.load(sys.stdin)["packages"]
def sort_graph(unsorted):
sorted = []
while unsorted:
deadlock = True
for node, edges in list(unsorted.items()):
for edge in edges:
if edge in unsorted:
break
else:
del unsorted[node]
sorted.append((node, edges))
deadlock = False
if deadlock:
raise Exception("deadlock")
return sorted
packages_dict = {
package["name"]: [
dep["name"] for dep in package["dependencies"]
if dep["kind"] is None
]
for package in packages if package["source"] is None
}
# iter_packages("keyfork")
priority_queue = sort_graph(packages_dict.copy())
for key, _ in priority_queue:
version = next(p["version"] for p in packages if p["name"] == key)
print(" ".join([key, version]))

View File

@ -0,0 +1,21 @@
set -eu
set -o pipefail
LAST_REF="$1"
CURRENT_REF="${2:-HEAD}"
IGNORE="${3:-ABCDEFG}"
cargo metadata --format-version=1 | \
jq -r '.packages[] | select(.source == null) | .name + " " + .manifest_path' | \
while read crate manifest_path; do
crate_path="$(dirname $manifest_path)"
git_log="$(git log --format='%h %s' "$LAST_REF".."$CURRENT_REF" "$crate_path" | { grep -v "$IGNORE" || true; })"
if test ! -z "$git_log"; then
echo "### Changes in $crate:"
echo ""
echo "\`\`\`"
echo "$git_log"
echo "\`\`\`"
echo ""
fi
done

15
scripts/publish.sh Normal file
View File

@ -0,0 +1,15 @@
set -eu
set -o pipefail
scripts_dir="$(dirname $0)"
python_script="$scripts_dir/generate-dependency-queue.py"
registry_url="https://git.distrust.co/api/packages/public/cargo"
search_url="${registry_url}/api/v1/crates"
cargo metadata --format-version=1 | python3 "$python_script" | while read crate version; do
# Verify the package does not exist
if ! curl "${search_url}?q=${crate}" 2>/dev/null | jq -e "$(printf '.crates | .[] | select(.name == "%s" and .max_version == "%s")' "$crate" "$version")" >/dev/null; then
cargo publish --registry distrust -p "$crate"
fi
done

View File

@ -0,0 +1,29 @@
set -eu
set -o pipefail
LAST_REF="$1"
CURRENT_REF="${2:-HEAD}"
temp_file="$(mktemp)"
cargo metadata --format-version=1 | jq -r '.packages[] | select(.source == null) | .name + " " + .manifest_path + " " + .version' > "$temp_file"
while read crate manifest_path version <&3; do
crate_path="$(dirname $manifest_path)"
git_log="$(git log --format='%h %s' "$LAST_REF".."$CURRENT_REF" "$crate_path")"
git_tag="$(git tag --list "$crate-v${version}")"
if test ! -z "$git_log" -a -z "$git_tag"; then
{
echo "${crate} v${version}"
echo ""
echo "\`\`\`"
echo "$git_log"
echo "\`\`\`"
echo ""
echo "# Crate: ${crate} ${version}"
} | git tag --sign "${crate}-v${version}" -F - -e
echo "Making new tag: ${crate}-v${version}"
fi
done 3<"$temp_file"
rm "$temp_file"