Compare commits

..

8 Commits

43 changed files with 1115 additions and 208 deletions

13
Cargo.lock generated
View File

@ -1700,6 +1700,10 @@ dependencies = [
"anyhow", "anyhow",
] ]
[[package]]
name = "keyfork-bug"
version = "0.1.0"
[[package]] [[package]]
name = "keyfork-crossterm" name = "keyfork-crossterm"
version = "0.27.1" version = "0.27.1"
@ -1761,6 +1765,7 @@ dependencies = [
"hex-literal", "hex-literal",
"hmac", "hmac",
"k256", "k256",
"keyfork-bug",
"keyfork-mnemonic-util", "keyfork-mnemonic-util",
"keyfork-slip10-test-data", "keyfork-slip10-test-data",
"ripemd", "ripemd",
@ -1773,6 +1778,7 @@ dependencies = [
name = "keyfork-entropy" name = "keyfork-entropy"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"keyfork-bug",
"smex", "smex",
] ]
@ -1788,11 +1794,12 @@ dependencies = [
[[package]] [[package]]
name = "keyfork-mnemonic-util" name = "keyfork-mnemonic-util"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"bip39", "bip39",
"hex", "hex",
"hmac", "hmac",
"keyfork-bug",
"pbkdf2", "pbkdf2",
"serde_json", "serde_json",
"sha2", "sha2",
@ -1803,6 +1810,7 @@ dependencies = [
name = "keyfork-prompt" name = "keyfork-prompt"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"keyfork-bug",
"keyfork-crossterm", "keyfork-crossterm",
"keyfork-mnemonic-util", "keyfork-mnemonic-util",
"thiserror", "thiserror",
@ -1813,6 +1821,7 @@ name = "keyfork-qrcode"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"image", "image",
"keyfork-bug",
"keyfork-zbar", "keyfork-zbar",
"rqrr", "rqrr",
"thiserror", "thiserror",
@ -1828,6 +1837,7 @@ dependencies = [
"card-backend", "card-backend",
"card-backend-pcsc", "card-backend-pcsc",
"hkdf", "hkdf",
"keyfork-bug",
"keyfork-derive-openpgp", "keyfork-derive-openpgp",
"keyfork-derive-util", "keyfork-derive-util",
"keyfork-mnemonic-util", "keyfork-mnemonic-util",
@ -1874,6 +1884,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"hex-literal", "hex-literal",
"keyfork-bug",
"keyfork-derive-path-data", "keyfork-derive-path-data",
"keyfork-derive-util", "keyfork-derive-util",
"keyfork-frame", "keyfork-frame",

View File

@ -15,6 +15,7 @@ members = [
"crates/qrcode/keyfork-zbar", "crates/qrcode/keyfork-zbar",
"crates/qrcode/keyfork-zbar-sys", "crates/qrcode/keyfork-zbar-sys",
"crates/util/keyfork-bin", "crates/util/keyfork-bin",
"crates/util/keyfork-bug",
"crates/util/keyfork-crossterm", "crates/util/keyfork-crossterm",
"crates/util/keyfork-entropy", "crates/util/keyfork-entropy",
"crates/util/keyfork-frame", "crates/util/keyfork-frame",

View File

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

View File

@ -12,14 +12,14 @@ ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"]
secp256k1 = ["keyfork-derive-util/secp256k1", "k256"] secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
[dependencies] [dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false } 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" } keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame", registry = "distrust" }
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" } keyforkd-models = { version = "0.1.0", path = "../keyforkd-models", registry = "distrust" }
bincode = "1.3.3" bincode = "1.3.3"
thiserror = "1.0.49" thiserror = "1.0.49"
k256 = { version = "0.13.3", optional = true } k256 = { version = "0.13.3", optional = true }
ed25519-dalek = { version = "2.1.1", optional = true } ed25519-dalek = { version = "2.1.1", optional = true }
[dev-dependencies] [dev-dependencies]
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" } keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data", registry = "distrust" }
keyforkd = { path = "../keyforkd" } keyforkd = { path = "../keyforkd", registry = "distrust" }

View File

@ -7,6 +7,6 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false } keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false, registry = "distrust" }
serde = { version = "1.0.190", features = ["derive"] } serde = { version = "1.0.190", features = ["derive"] }
thiserror = "1.0.50" thiserror = "1.0.50"

View File

@ -12,11 +12,12 @@ tracing = ["tower/tracing", "tokio/tracing", "dep:tracing", "dep:tracing-subscri
multithread = ["tokio/rt-multi-thread"] multithread = ["tokio/rt-multi-thread"]
[dependencies] [dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util" } keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame" } keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", registry = "distrust" }
keyfork-mnemonic-util = { version = "0.1.0", path = "../../util/keyfork-mnemonic-util" } keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame", features = ["async"], registry = "distrust" }
keyfork-derive-path-data = { version = "0.1.0", path = "../../derive/keyfork-derive-path-data" } keyfork-mnemonic-util = { version = "0.2.0", path = "../../util/keyfork-mnemonic-util", registry = "distrust" }
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" } 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" }
# Not personally audited # Not personally audited
bincode = "1.3.3" bincode = "1.3.3"
@ -35,4 +36,4 @@ tempfile = { version = "3.10.0", default-features = false }
[dev-dependencies] [dev-dependencies]
hex-literal = "0.4.1" hex-literal = "0.4.1"
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" } keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data", registry = "distrust" }

View File

@ -7,6 +7,8 @@ use crate::{middleware, Keyforkd, ServiceBuilder, UnixServer};
use tokio::runtime::Builder; use tokio::runtime::Builder;
use keyfork_bug::bug;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[error("This error can never be instantiated")] #[error("This error can never be instantiated")]
#[doc(hidden)] #[doc(hidden)]
@ -54,8 +56,10 @@ where
.worker_threads(2) .worker_threads(2)
.enable_io() .enable_io()
.build() .build()
.expect("tokio threaded IO runtime"); .expect(bug!(
let socket_dir = tempfile::tempdir().expect("can't create tempdir"); "can't make tokio threaded IO runtime, should be enabled via feature flags"
));
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
let socket_path = socket_dir.path().join("keyforkd.sock"); let socket_path = socket_dir.path().join("keyforkd.sock");
rt.block_on(async move { rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(1); let (tx, mut rx) = tokio::sync::mpsc::channel(1);
@ -63,25 +67,28 @@ where
let socket_path = socket_path.clone(); let socket_path = socket_path.clone();
let seed = seed.to_vec(); let seed = seed.to_vec();
async move { async move {
let mut server = UnixServer::bind(&socket_path).expect("can't bind unix socket"); let mut server =
tx.send(()).await.expect("couldn't send server start signal"); UnixServer::bind(&socket_path).expect(bug!("can't bind unix socket"));
tx.send(())
.await
.expect(bug!("couldn't send server start signal"));
let service = ServiceBuilder::new() let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new()) .layer(middleware::BincodeLayer::new())
.service(Keyforkd::new(seed.to_vec())); .service(Keyforkd::new(seed.to_vec()));
server.run(service).await.unwrap(); server.run(service).await.expect(bug!("Unable to start service"));
} }
}); });
rx.recv() rx.recv()
.await .await
.expect("can't receive server start signal from channel"); .expect(bug!("can't receive server start signal from channel"));
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path)); let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
let result = test_handle.await; let result = test_handle.await;
server_handle.abort(); server_handle.abort();
result result
}) })
.expect("runtime could not join all threads") .expect(bug!("runtime could not join all threads"))
} }
#[cfg(test)] #[cfg(test)]

View File

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

View File

@ -6,12 +6,12 @@ license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = [] default = ["bin"]
bin = ["sequoia-openpgp/crypto-nettle"] bin = ["sequoia-openpgp/crypto-nettle"]
[dependencies] [dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] } keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"], registry = "distrust" }
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", default-features = false, features = ["ed25519"] } keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", default-features = false, features = ["ed25519"], registry = "distrust" }
ed25519-dalek = "2.0.0" ed25519-dalek = "2.0.0"
sequoia-openpgp = { version = "1.17.0", default-features = false } sequoia-openpgp = { version = "1.17.0", default-features = false }
anyhow = "1.0.75" anyhow = "1.0.75"

View File

@ -7,4 +7,4 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false } keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, registry = "distrust" }

View File

@ -12,7 +12,8 @@ secp256k1 = ["k256"]
ed25519 = ["ed25519-dalek"] ed25519 = ["ed25519-dalek"]
[dependencies] [dependencies]
keyfork-mnemonic-util = { version = "0.1.0", path = "../../util/keyfork-mnemonic-util" } 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" }
# Included in Rust # Included in Rust
digest = "0.10.7" digest = "0.10.7"
@ -32,4 +33,4 @@ ed25519-dalek = { version = "2.0.0", optional = true }
[dev-dependencies] [dev-dependencies]
hex-literal = "0.4.1" hex-literal = "0.4.1"
keyfork-slip10-test-data = { version = "0.1.0", path = "../../util/keyfork-slip10-test-data" } keyfork-slip10-test-data = { version = "0.1.0", path = "../../util/keyfork-slip10-test-data", registry = "distrust" }

View File

@ -1,5 +1,7 @@
use crate::{DerivationIndex, DerivationPath, ExtendedPublicKey, PrivateKey, PublicKey}; use crate::{DerivationIndex, DerivationPath, ExtendedPublicKey, PrivateKey, PublicKey};
use keyfork_bug::bug;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::Sha512; use sha2::Sha512;
@ -124,7 +126,7 @@ mod serde_with {
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?; let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
let bytes: [u8; 32] = variable_len_bytes let bytes: [u8; 32] = variable_len_bytes
.try_into() .try_into()
.expect("unable to parse serialized private key; no support for static len"); .expect(bug!("unable to parse serialized private key; no support for static len"));
Ok(K::from_bytes(&bytes)) Ok(K::from_bytes(&bytes))
} }
} }
@ -171,7 +173,7 @@ where
fn new_internal(seed: &[u8]) -> Self { fn new_internal(seed: &[u8]) -> Self {
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>()) let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
.expect("HmacSha512 InvalidLength should be infallible") .expect(bug!("HmacSha512 InvalidLength should be infallible"))
.chain_update(seed) .chain_update(seed)
.finalize() .finalize()
.into_bytes(); .into_bytes();
@ -180,10 +182,10 @@ where
Self::new_from_parts( Self::new_from_parts(
private_key private_key
.try_into() .try_into()
.expect("KEY_SIZE / 8 did not give a 32 byte slice"), .expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
0, 0,
// Checked: chain_code is always the same length, hash is static size // Checked: chain_code is always the same length, hash is static size
chain_code.try_into().expect("Invalid chain code length"), chain_code.try_into().expect(bug!("Invalid chain code length")),
) )
} }
@ -405,7 +407,7 @@ where
let depth = self.depth.checked_add(1).ok_or(Error::Depth)?; let depth = self.depth.checked_add(1).ok_or(Error::Depth)?;
let mut hmac = HmacSha512::new_from_slice(&self.chain_code) let mut hmac = HmacSha512::new_from_slice(&self.chain_code)
.expect("HmacSha512 InvalidLength should be infallible"); .expect(bug!("HmacSha512 InvalidLength should be infallible"));
if index.is_hardened() { if index.is_hardened() {
hmac.update(&[0]); hmac.update(&[0]);
hmac.update(&self.private_key.to_bytes()); hmac.update(&self.private_key.to_bytes());
@ -423,7 +425,7 @@ where
.derive_child( .derive_child(
&private_key &private_key
.try_into() .try_into()
.expect("Invalid length for private key"), .expect(bug!("Invalid length for private key")),
) )
.map_err(|_| Error::Derivation)?; .map_err(|_| Error::Derivation)?;
@ -432,7 +434,7 @@ where
depth, depth,
chain_code: chain_code chain_code: chain_code
.try_into() .try_into()
.expect("Invalid length for chain code"), .expect(bug!("Invalid length for chain code")),
}) })
} }
} }

View File

@ -4,6 +4,8 @@ use hmac::{Hmac, Mac};
use sha2::Sha512; use sha2::Sha512;
use thiserror::Error; use thiserror::Error;
use keyfork_bug::bug;
const KEY_SIZE: usize = 256; const KEY_SIZE: usize = 256;
/// Errors associated with creating or deriving Extended Public Keys. /// Errors associated with creating or deriving Extended Public Keys.
@ -142,9 +144,11 @@ where
let (child_key, chain_code) = hmac.split_at(KEY_SIZE / 8); let (child_key, chain_code) = hmac.split_at(KEY_SIZE / 8);
let derived_key = self let derived_key = self
.public_key .public_key
.derive_child(child_key.try_into().expect("Invalid key length")) .derive_child(child_key.try_into().expect(bug!("Invalid key length")))
.map_err(|_| Error::Derivation)?; .map_err(|_| Error::Derivation)?;
let chain_code = chain_code.try_into().expect("Invalid chain code length"); let chain_code = chain_code
.try_into()
.expect(bug!("Invalid chain code length"));
Ok(Self { Ok(Self {
public_key: derived_key, public_key: derived_key,

View File

@ -2,6 +2,8 @@ use crate::PublicKey;
use thiserror::Error; use thiserror::Error;
use keyfork_bug::bug;
pub(crate) type PrivateKeyBytes = [u8; 32]; pub(crate) type PrivateKeyBytes = [u8; 32];
/// Functions required to use an `ExtendedPrivateKey`. /// Functions required to use an `ExtendedPrivateKey`.
@ -115,7 +117,7 @@ impl PrivateKey for k256::SecretKey {
} }
fn from_bytes(b: &PrivateKeyBytes) -> Self { fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self::from_slice(b).expect("Invalid private key bytes") Self::from_slice(b).expect(bug!("Invalid private key bytes"))
} }
fn to_bytes(&self) -> PrivateKeyBytes { fn to_bytes(&self) -> PrivateKeyBytes {
@ -134,13 +136,13 @@ impl PrivateKey for k256::SecretKey {
let other = *other; let other = *other;
// Checked: See above nonzero check // Checked: See above nonzero check
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into())) let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
.expect("Should have been able to get a NonZeroScalar"); .expect(bug!("Should have been able to get a NonZeroScalar"));
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref(); let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
Ok( Ok(
Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar)) Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
.map(Into::into) .map(Into::into)
.expect("Should be able to make Key"), .expect(bug!("Should be able to make Key")),
) )
} }
} }

View File

@ -5,6 +5,8 @@ use ripemd::Ripemd160;
use sha2::Sha256; use sha2::Sha256;
use thiserror::Error; use thiserror::Error;
use keyfork_bug::bug;
pub(crate) type PublicKeyBytes = [u8; 33]; pub(crate) type PublicKeyBytes = [u8; 33];
/// Functions required to use an `ExtendedPublicKey`. /// Functions required to use an `ExtendedPublicKey`.
@ -63,7 +65,7 @@ pub trait PublicKey: Sized {
// Note: Safety assured by type returned from Ripemd160 // Note: Safety assured by type returned from Ripemd160
hash[..4] hash[..4]
.try_into() .try_into()
.expect("Ripemd160 returned too little data") .expect(bug!("Ripemd160 returned too little data"))
} }
} }
@ -108,10 +110,11 @@ impl PublicKey for k256::PublicKey {
} }
// Checked: See above // Checked: See above
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into())) let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
.expect("Should have been able to get a NonZeroScalar"); .expect(bug!("Should have been able to get a NonZeroScalar"));
let point = self.to_projective() + (AffinePoint::generator() * *scalar); let point = self.to_projective() + (AffinePoint::generator() * *scalar);
Ok(Self::from_affine(point.into()).expect("Could not from_affine after scalar arithmetic")) Ok(Self::from_affine(point.into())
.expect(bug!("Could not from_affine after scalar arithmetic")))
} }
} }
@ -150,10 +153,11 @@ impl TestPublicKey {
#[allow(dead_code)] #[allow(dead_code)]
pub fn from_bytes(b: &[u8]) -> Self { pub fn from_bytes(b: &[u8]) -> Self {
Self { Self {
key: b.try_into().unwrap(), key: b
.try_into()
.expect(bug!("invalid size when constructing TestPublicKey")),
} }
} }
} }
impl PublicKey for TestPublicKey { impl PublicKey for TestPublicKey {

View File

@ -197,7 +197,6 @@ impl DerivationRequest {
/// let mnemonic: keyfork_mnemonic_util::Mnemonic = // /// let mnemonic: keyfork_mnemonic_util::Mnemonic = //
/// # keyfork_mnemonic_util::Mnemonic::from_entropy( /// # keyfork_mnemonic_util::Mnemonic::from_entropy(
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
/// # Default::default(),
/// # )?; /// # )?;
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal; /// # DerivationAlgorithm::Internal;

View File

@ -14,22 +14,23 @@ openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "de
qrcode = ["keyfork-qrcode"] qrcode = ["keyfork-qrcode"]
[dependencies] [dependencies]
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] } keyfork-bug = { version = "0.1.0", path = "../util/keyfork-bug", registry = "distrust" }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", optional = true, default-features = false } keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"], registry = "distrust" }
smex = { version = "0.1.0", path = "../util/smex" } 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" sharks = "0.5.0"
thiserror = "1.0.50" thiserror = "1.0.50"
# Remote operator mode # Remote operator mode
keyfork-mnemonic-util = { version = "0.1.0", path = "../util/keyfork-mnemonic-util" } keyfork-mnemonic-util = { version = "0.2.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
x25519-dalek = { version = "2.0.0", features = ["getrandom"] } x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = { version = "0.10.3", features = ["std"] }
hkdf = { version = "0.12.4", features = ["std"] } hkdf = { version = "0.12.4", features = ["std"] }
sha2 = "0.10.8" sha2 = "0.10.8"
# OpenPGP # OpenPGP
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-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 } anyhow = { version = "1.0.79", optional = true }
card-backend = { version = "0.2.0", optional = true } card-backend = { version = "0.2.0", optional = true }
card-backend-pcsc = { version = "0.5.0", optional = true } card-backend-pcsc = { version = "0.5.0", optional = true }

View File

@ -7,6 +7,7 @@ use std::{
process::ExitCode, process::ExitCode,
}; };
use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{openpgp::OpenPGP, Format}; use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -31,8 +32,10 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name} <shard> [key_discovery]"), _ => panic!("Usage: {program_name} <shard> [key_discovery]"),
}; };
let openpgp = OpenPGP; let openpgp = OpenPGP::<DefaultTerminal>::new();
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?; let prompt_handler = default_terminal()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?;
print!("{}", smex::encode(bytes)); print!("{}", smex::encode(bytes));
Ok(()) Ok(())

View File

@ -7,6 +7,7 @@ use std::{
process::ExitCode, process::ExitCode,
}; };
use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{Format, openpgp::OpenPGP}; use keyfork_shard::{Format, openpgp::OpenPGP};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -31,9 +32,10 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name} <shard> [key_discovery]"), _ => panic!("Usage: {program_name} <shard> [key_discovery]"),
}; };
let openpgp = OpenPGP; let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file)?; openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?;
Ok(()) Ok(())
} }

View File

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

View File

@ -1,12 +1,17 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![allow(clippy::expect_fun_call)]
use std::io::{stdin, stdout, Read, Write}; use std::{
io::{stdin, stdout, Read, Write},
sync::{Arc, Mutex},
};
use aes_gcm::{ use aes_gcm::{
aead::{consts::U12, Aead, AeadCore, OsRng}, aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce, Aes256Gcm, KeyInit, Nonce,
}; };
use hkdf::Hkdf; use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_mnemonic_util::{English, Mnemonic}; use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{ use keyfork_prompt::{
@ -169,6 +174,7 @@ pub trait Format {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData], encrypted_messages: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Vec<Share>, u8), Self::Error>; ) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a reaable input. For the current /// Decrypt a single share and associated metadata from a reaable input. For the current
@ -182,6 +188,7 @@ pub trait Format {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
) -> Result<(Share, u8), Self::Error>; ) -> Result<(Share, u8), Self::Error>;
/// Decrypt multiple shares and combine them to recreate a secret. /// Decrypt multiple shares and combine them to recreate a secret.
@ -193,12 +200,17 @@ pub trait Format {
&self, &self,
private_key_discovery: Option<impl KeyDiscovery<Self>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
prompt: impl PromptHandler,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> { ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_discovery let private_keys = private_key_discovery
.map(|p| p.discover_private_keys()) .map(|p| p.discover_private_keys())
.transpose()?; .transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?; let encrypted_messages = self.parse_shard_file(reader)?;
let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?; let (shares, threshold) = self.decrypt_all_shards(
private_keys,
&encrypted_messages,
Arc::new(Mutex::new(prompt)),
)?;
let secret = Sharks(threshold) let secret = Sharks(threshold)
.recover(&shares) .recover(&shares)
@ -218,8 +230,9 @@ pub trait Format {
&self, &self,
private_key_discovery: Option<impl KeyDiscovery<Self>>, private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
prompt: impl PromptHandler,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?; let prompt = Arc::new(Mutex::new(prompt));
// parse input // parse input
let private_keys = private_key_discovery let private_keys = private_key_discovery
@ -234,7 +247,10 @@ pub trait Format {
// receive remote data via scanning QR code from camera // receive remote data via scanning QR code from camera
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?; prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) = if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0) keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
{ {
@ -242,7 +258,10 @@ pub trait Format {
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?); nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?) pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
} else { } else {
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?; prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
}; };
} }
@ -253,7 +272,9 @@ pub trait Format {
let validator = MnemonicSetValidator { let validator = MnemonicSetValidator {
word_lengths: [9, 24], word_lengths: [9, 24],
}; };
let [nonce_mnemonic, pubkey_mnemonic] = pm let [nonce_mnemonic, pubkey_mnemonic] = prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_wordlist::<English, _>( .prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ, QRCODE_COULDNT_READ,
3, 3,
@ -284,7 +305,8 @@ pub trait Format {
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?; let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
// decrypt a single shard and create the payload // decrypt a single shard and create the payload
let (share, threshold) = self.decrypt_one_shard(private_keys, &encrypted_messages)?; let (share, threshold) =
self.decrypt_one_shard(private_keys, &encrypted_messages, prompt.clone())?;
let mut payload = Vec::from(&share); let mut payload = Vec::from(&share);
payload.insert(0, HUNK_VERSION); payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold); payload.insert(1, threshold);
@ -332,7 +354,10 @@ pub trait Format {
let mut qrcode_data = our_pubkey_mnemonic.to_bytes(); let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes()); qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) { if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
pm.prompt_message(PromptMessage::Text( prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(
concat!( concat!(
"A QR code will be displayed after this prompt. ", "A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ", "Send the QR code back to the operator combining the shards. ",
@ -340,11 +365,17 @@ pub trait Format {
) )
.to_string(), .to_string(),
))?; ))?;
pm.prompt_message(PromptMessage::Data(qrcode))?; prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Data(qrcode))?;
} }
} }
pm.prompt_message(PromptMessage::Text(format!( prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}" "Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?; )))?;

View File

@ -0,0 +1,617 @@
#![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

@ -1,16 +1,22 @@
//! OpenPGP Shard functionality. //! OpenPGP Shard functionality.
#![allow(clippy::expect_fun_call)]
use std::{ use std::{
collections::HashMap, collections::HashMap,
io::{Read, Write}, io::{Read, Write},
marker::PhantomData,
path::Path, path::Path,
str::FromStr, str::FromStr,
sync::{Arc, Mutex},
}; };
use keyfork_bug::bug;
use keyfork_derive_openpgp::{ use keyfork_derive_openpgp::{
derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed}, derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed},
XPrv, XPrv,
}; };
use keyfork_prompt::PromptHandler;
use openpgp::{ use openpgp::{
armor::{Kind, Writer}, armor::{Kind, Writer},
cert::{Cert, CertParser, ValidCert}, cert::{Cert, CertParser, ValidCert},
@ -176,9 +182,18 @@ impl EncryptedMessage {
} }
/// ///
pub struct OpenPGP; pub struct OpenPGP<P: PromptHandler> {
p: PhantomData<P>,
}
impl OpenPGP { impl<P: PromptHandler> OpenPGP<P> {
#[allow(clippy::new_without_default, missing_docs)]
pub fn new() -> Self {
Self { p: PhantomData }
}
}
impl<P: PromptHandler> OpenPGP<P> {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read /// 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. /// from a file, or from files one level deep in a directory.
/// ///
@ -209,7 +224,9 @@ impl OpenPGP {
} }
} }
impl Format for OpenPGP { const METADATA_MESSAGE_MISSING: &str = "Metadata message was not found in parsed packets";
impl<P: PromptHandler> Format for OpenPGP<P> {
type Error = Error; type Error = Error;
type PublicKey = Cert; type PublicKey = Cert;
type PrivateKeyData = Vec<Cert>; type PrivateKeyData = Vec<Cert>;
@ -233,16 +250,16 @@ impl Format for OpenPGP {
let seed = VariableLengthSeed::new(seed); let seed = VariableLengthSeed::new(seed);
// build cert to sign encrypted shares // build cert to sign encrypted shares
let userid = UserID::from("keyfork-sss"); let userid = UserID::from("keyfork-sss");
let path = DerivationPath::from_str("m/7366512'/0'").expect("valid derivation path"); let path = DerivationPath::from_str("m/7366512'/0'").expect(bug!("valid derivation path"));
let xprv = XPrv::new(seed) let xprv = XPrv::new(seed)
.derive_path(&path) .derive_path(&path)
.expect("valid derivation"); .expect(bug!("valid derivation"));
keyfork_derive_openpgp::derive( keyfork_derive_openpgp::derive(
xprv, xprv,
&[KeyFlags::empty().set_certification().set_signing()], &[KeyFlags::empty().set_certification().set_signing()],
&userid, &userid,
) )
.expect("valid cert creation") .expect(bug!("valid cert creation"))
} }
fn format_encrypted_header( fn format_encrypted_header(
@ -256,21 +273,26 @@ impl Format for OpenPGP {
// Note: Sequoia does not export private keys on a Cert, only on a TSK // Note: Sequoia does not export private keys on a Cert, only on a TSK
signing_key signing_key
.serialize(&mut pp) .serialize(&mut pp)
.expect("serialize cert into bytes"); .expect(bug!("serialize cert into bytes"));
for cert in key_data { for cert in key_data {
cert.serialize(&mut pp) cert.serialize(&mut pp)
.expect("serialize pubkey into bytes"); .expect(bug!("serialize pubkey into bytes"));
} }
// verify packet pile // verify packet pile
let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..]) let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..])
.expect("should have certs"); .expect(bug!("should have certs"));
let first_cert = iter.next().transpose().ok().flatten().expect("first cert"); let first_cert = iter
.next()
.transpose()
.ok()
.flatten()
.expect(bug!("first cert"));
assert_eq!(signing_key, &first_cert); assert_eq!(signing_key, &first_cert);
for (packet_cert, cert) in iter.zip(key_data) { for (packet_cert, cert) in iter.zip(key_data) {
assert_eq!( assert_eq!(
&packet_cert.expect("parsed packet cert"), &packet_cert.expect(bug!("parsed packet cert")),
cert, cert,
"packet pile could not recreate cert: {}", "packet pile could not recreate cert: {}",
cert.fingerprint(), cert.fingerprint(),
@ -383,7 +405,7 @@ impl Format for OpenPGP {
.map_err(Error::Sequoia)? .map_err(Error::Sequoia)?
.into_iter() .into_iter()
.next() .next()
.expect("serialized message should be parseable"); .expect(bug!("serialized message should be parseable"));
Ok(message) Ok(message)
} }
@ -412,16 +434,20 @@ impl Format for OpenPGP {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
) -> std::result::Result<(Vec<Share>, u8), Self::Error> { ) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting. // Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired. // We don't want to invalidate someone's keys just because the old sig expired.
let policy = NullPolicy::new(); let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?; let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
let mut manager = SmartcardManager::new(prompt.clone())?;
let mut encrypted_messages = encrypted_data.iter(); let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages.next().expect("metdata"); let metadata = encrypted_messages
.next()
.expect(bug!(METADATA_MESSAGE_MISSING));
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?; let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?; let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
@ -469,14 +495,18 @@ impl Format for OpenPGP {
&self, &self,
private_keys: Option<Self::PrivateKeyData>, private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData], encrypted_data: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
) -> std::result::Result<(Share, u8), Self::Error> { ) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new(); let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?; let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
let mut manager = SmartcardManager::new(prompt.clone())?;
let mut encrypted_messages = encrypted_data.iter(); let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages.next().expect("metadata"); let metadata = encrypted_messages
.next()
.expect(bug!(METADATA_MESSAGE_MISSING));
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?; let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?; let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
@ -511,22 +541,22 @@ impl Format for OpenPGP {
} }
} }
impl KeyDiscovery<OpenPGP> for &Path { impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &Path {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> { fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
OpenPGP::discover_certs(self) OpenPGP::<P>::discover_certs(self)
} }
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> { fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
todo!() OpenPGP::<P>::discover_certs(self)
} }
} }
impl KeyDiscovery<OpenPGP> for &[Cert] { impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> { fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
Ok(self.to_vec()) Ok(self.to_vec())
} }
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> { fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
Ok(self.to_vec()) Ok(self.to_vec())
} }
} }
@ -587,12 +617,12 @@ fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a // NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
// single message. // single message.
fn decrypt_with_manager( fn decrypt_with_manager<P: PromptHandler>(
threshold: u8, threshold: u8,
messages: &mut HashMap<KeyID, EncryptedMessage>, messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert], certs: &[Cert],
policy: &dyn Policy, policy: &dyn Policy,
manager: &mut SmartcardManager, manager: &mut SmartcardManager<P>,
) -> Result<HashMap<KeyID, Vec<u8>>> { ) -> Result<HashMap<KeyID, Vec<u8>>> {
let mut decrypted_messages = HashMap::new(); let mut decrypted_messages = HashMap::new();
@ -619,7 +649,12 @@ fn decrypt_with_manager(
// Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message // Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message
if let Some(fp) = manager.load_any_fingerprint(unused_fingerprints)? { if let Some(fp) = manager.load_any_fingerprint(unused_fingerprints)? {
let cert_keyid = cert_by_fingerprint.get(&fp).unwrap().clone(); let cert_keyid = cert_by_fingerprint
.get(&fp)
.expect(bug!(
"manager loaded fingerprint not from unused_fingerprints"
))
.clone();
if let Some(message) = messages.remove(&cert_keyid) { if let Some(message) = messages.remove(&cert_keyid) {
let message = message.decrypt_with(policy, &mut *manager)?; let message = message.decrypt_with(policy, &mut *manager)?;
decrypted_messages.insert(cert_keyid, message); decrypted_messages.insert(cert_keyid, message);
@ -632,11 +667,11 @@ fn decrypt_with_manager(
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to // NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
// decrypt messages with. // decrypt messages with.
fn decrypt_with_keyring( fn decrypt_with_keyring<P: PromptHandler>(
messages: &mut HashMap<KeyID, EncryptedMessage>, messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert], certs: &[Cert],
policy: &NullPolicy, policy: &NullPolicy,
keyring: &mut Keyring, keyring: &mut Keyring<P>,
) -> Result<HashMap<KeyID, Vec<u8>>, Error> { ) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
let mut decrypted_messages = HashMap::new(); let mut decrypted_messages = HashMap::new();
@ -666,11 +701,11 @@ fn decrypt_with_keyring(
Ok(decrypted_messages) Ok(decrypted_messages)
} }
fn decrypt_metadata( fn decrypt_metadata<P: PromptHandler>(
message: &EncryptedMessage, message: &EncryptedMessage,
policy: &NullPolicy, policy: &NullPolicy,
keyring: &mut Keyring, keyring: &mut Keyring<P>,
manager: &mut SmartcardManager, manager: &mut SmartcardManager<P>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
Ok(if keyring.is_empty() { Ok(if keyring.is_empty() {
manager.load_any_card()?; manager.load_any_card()?;

View File

@ -1,4 +1,9 @@
use keyfork_prompt::{Error as PromptError, DefaultTerminal, default_terminal, PromptHandler}; #![allow(clippy::expect_fun_call)]
use std::sync::{Arc, Mutex};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{Error as PromptError, PromptHandler};
use super::openpgp::{ use super::openpgp::{
self, self,
@ -22,18 +27,18 @@ pub enum Error {
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring { pub struct Keyring<P: PromptHandler> {
full_certs: Vec<Cert>, full_certs: Vec<Cert>,
root: Option<Cert>, root: Option<Cert>,
pm: DefaultTerminal, pm: Arc<Mutex<P>>,
} }
impl Keyring { impl<P: PromptHandler> Keyring<P> {
pub fn new(certs: impl AsRef<[Cert]>) -> Result<Self> { pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self { Ok(Self {
full_certs: certs.as_ref().to_vec(), full_certs: certs.as_ref().to_vec(),
root: Default::default(), root: Default::default(),
pm: default_terminal()?, pm: p,
}) })
} }
@ -57,7 +62,7 @@ impl Keyring {
} }
} }
impl VerificationHelper for &mut Keyring { impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> { fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
Ok(ids Ok(ids
.iter() .iter()
@ -93,7 +98,7 @@ impl VerificationHelper for &mut Keyring {
} }
} }
impl DecryptionHelper for &mut Keyring { impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
fn decrypt<D>( fn decrypt<D>(
&mut self, &mut self,
pkesks: &[PKESK], pkesks: &[PKESK],
@ -137,6 +142,8 @@ impl DecryptionHelper for &mut Keyring {
}; };
let passphrase = self let passphrase = self
.pm .pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_passphrase(&message) .prompt_passphrase(&message)
.context("Decryption passphrase")?; .context("Decryption passphrase")?;
secret_key secret_key

View File

@ -1,9 +1,14 @@
use std::collections::{HashMap, HashSet}; #![allow(clippy::expect_fun_call)]
use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{ use keyfork_prompt::{
default_terminal,
validators::{PinValidator, Validator}, validators::{PinValidator, Validator},
DefaultTerminal, Error as PromptError, Message, PromptHandler, Error as PromptError, Message, PromptHandler,
}; };
use super::openpgp::{ use super::openpgp::{
@ -66,19 +71,19 @@ fn format_name(input: impl AsRef<str>) -> String {
} }
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub struct SmartcardManager { pub struct SmartcardManager<P: PromptHandler> {
current_card: Option<Card<Open>>, current_card: Option<Card<Open>>,
root: Option<Cert>, root: Option<Cert>,
pm: DefaultTerminal, pm: Arc<Mutex<P>>,
pin_cache: HashMap<Fingerprint, String>, pin_cache: HashMap<Fingerprint, String>,
} }
impl SmartcardManager { impl<P: PromptHandler> SmartcardManager<P> {
pub fn new() -> Result<Self> { pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self { Ok(Self {
current_card: None, current_card: None,
root: None, root: None,
pm: default_terminal()?, pm: p,
pin_cache: Default::default(), pin_cache: Default::default(),
}) })
} }
@ -96,8 +101,12 @@ impl SmartcardManager {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? { if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c; break c;
} }
self.pm.prompt_message(Message::Text( self.pm
"No smart card was found. Please plug in a smart card and press enter".to_string(), .lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?; ))?;
}; };
let mut card = Card::<Open>::new(card_backend).map_err(Error::OpenSmartCard)?; let mut card = Card::<Open>::new(card_backend).map_err(Error::OpenSmartCard)?;
@ -152,7 +161,10 @@ impl SmartcardManager {
} }
} }
self.pm.prompt_message(Message::Text( self.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(Message::Text(
"Please plug in a smart card and press enter".to_string(), "Please plug in a smart card and press enter".to_string(),
))?; ))?;
} }
@ -161,7 +173,7 @@ impl SmartcardManager {
} }
} }
impl VerificationHelper for &mut SmartcardManager { impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> { fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
#[allow(clippy::flat_map_option)] #[allow(clippy::flat_map_option)]
Ok(ids Ok(ids
@ -194,7 +206,7 @@ impl VerificationHelper for &mut SmartcardManager {
} }
} }
impl DecryptionHelper for &mut SmartcardManager { impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
fn decrypt<D>( fn decrypt<D>(
&mut self, &mut self,
pkesks: &[PKESK], pkesks: &[PKESK],
@ -252,8 +264,10 @@ impl DecryptionHelper for &mut SmartcardManager {
} else { } else {
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ") format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
}; };
let temp_pin = self let temp_pin =
.pm self.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_passphrase(&message, 3, &pin_validator)?; .prompt_validated_passphrase(&message, 3, &pin_validator)?;
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim()); let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
match verification_status { match verification_status {
@ -265,6 +279,8 @@ impl DecryptionHelper for &mut SmartcardManager {
// NOTE: This should not be hit, because of the above validator. // NOTE: This should not be hit, because of the above validator.
Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => { Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => {
self.pm self.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(Message::Text("Invalid PIN length entered.".to_string()))?; .prompt_message(Message::Text("Invalid PIN length entered.".to_string()))?;
} }
Err(_) => {} Err(_) => {}

View File

@ -23,16 +23,17 @@ sequoia-crypto-backend-openssl = ["sequoia-openpgp/crypto-openssl"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
keyforkd = { version = "0.1.0", path = "../daemon/keyforkd", features = ["tracing"] } keyfork-bin = { version = "0.1.0", path = "../util/keyfork-bin", registry = "distrust" }
keyforkd-client = { version = "0.1.0", path = "../daemon/keyforkd-client", default-features = false, features = ["ed25519"] } keyforkd = { version = "0.1.0", path = "../daemon/keyforkd", features = ["tracing"], registry = "distrust" }
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp" } keyforkd-client = { version = "0.1.0", path = "../daemon/keyforkd-client", default-features = false, features = ["ed25519"], registry = "distrust" }
keyfork-derive-util = { version = "0.1.0", path = "../derive/keyfork-derive-util", default-features = false, features = ["ed25519"] } keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp", registry = "distrust" }
keyfork-entropy = { version = "0.1.0", path = "../util/keyfork-entropy" } keyfork-derive-util = { version = "0.1.0", path = "../derive/keyfork-derive-util", default-features = false, features = ["ed25519"], registry = "distrust" }
keyfork-mnemonic-util = { version = "0.1.0", path = "../util/keyfork-mnemonic-util" } keyfork-entropy = { version = "0.1.0", path = "../util/keyfork-entropy", registry = "distrust" }
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt" } keyfork-mnemonic-util = { version = "0.2.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false } keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", registry = "distrust" }
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"] } keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false, registry = "distrust" }
smex = { version = "0.1.0", path = "../util/smex" } 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" }
clap = { version = "4.4.2", features = ["derive", "env", "wrap_help"] } clap = { version = "4.4.2", features = ["derive", "env", "wrap_help"] }
thiserror = "1.0.48" thiserror = "1.0.48"
@ -43,4 +44,3 @@ openpgp-card-sequoia = { version = "0.2.0", default-features = false }
openpgp-card = "0.4.1" openpgp-card = "0.4.1"
clap_complete = { version = "4.4.6", optional = true } clap_complete = { version = "4.4.6", optional = true }
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] } sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] }
keyfork-bin = { version = "0.1.0", path = "../util/keyfork-bin" }

View File

@ -3,6 +3,7 @@ use clap::{Parser, Subcommand};
use std::path::PathBuf; use std::path::PathBuf;
use keyfork_mnemonic_util::{English, Mnemonic}; use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_shard::{remote_decrypt, Format}; use keyfork_shard::{remote_decrypt, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -34,10 +35,14 @@ impl RecoverSubcommands {
} => { } => {
let content = std::fs::read_to_string(shard_file)?; let content = std::fs::read_to_string(shard_file)?;
if content.contains("BEGIN PGP MESSAGE") { if content.contains("BEGIN PGP MESSAGE") {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
// TODO: remove .clone() by making handle() consume self // TODO: remove .clone() by making handle() consume self
let seed = openpgp let seed = openpgp.decrypt_all_shards_to_secret(
.decrypt_all_shards_to_secret(key_discovery.as_deref(), content.as_bytes())?; key_discovery.as_deref(),
content.as_bytes(),
prompt_handler,
)?;
Ok(seed) Ok(seed)
} else { } else {
panic!("unknown format of shard file"); panic!("unknown format of shard file");
@ -50,7 +55,6 @@ impl RecoverSubcommands {
} }
RecoverSubcommands::Mnemonic {} => { RecoverSubcommands::Mnemonic {} => {
use keyfork_prompt::{ use keyfork_prompt::{
default_terminal,
validators::{ validators::{
mnemonic::{MnemonicChoiceValidator, WordLength}, mnemonic::{MnemonicChoiceValidator, WordLength},
Validator, Validator,

View File

@ -1,5 +1,6 @@
use super::Keyfork; use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_shard::Format as _; use keyfork_shard::Format as _;
use std::{ use std::{
io::{stdin, stdout, Read, Write}, io::{stdin, stdout, Read, Write},
@ -63,7 +64,7 @@ impl ShardExec for OpenPGP {
secret: &[u8], secret: &[u8],
output: &mut (impl Write + Send + Sync), output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let opgp = keyfork_shard::openpgp::OpenPGP; let opgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output) opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
} }
@ -72,10 +73,10 @@ impl ShardExec for OpenPGP {
key_discovery: Option<&Path>, key_discovery: Option<&Path>,
input: impl Read + Send + Sync, input: impl Read + Send + Sync,
output: &mut impl Write, output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> ) -> Result<(), Box<dyn std::error::Error>> {
{ let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let openpgp = keyfork_shard::openpgp::OpenPGP; let prompt = default_terminal()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
write!(output, "{}", smex::encode(bytes))?; write!(output, "{}", smex::encode(bytes))?;
Ok(()) Ok(())
@ -85,10 +86,10 @@ impl ShardExec for OpenPGP {
&self, &self,
key_discovery: Option<&Path>, key_discovery: Option<&Path>,
input: impl Read + Send + Sync, input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> ) -> Result<(), Box<dyn std::error::Error>> {
{ let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let openpgp = keyfork_shard::openpgp::OpenPGP; let prompt = default_terminal()?;
openpgp.decrypt_one_shard_for_transport(key_discovery, input)?; openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
Ok(()) Ok(())
} }
} }

View File

@ -12,7 +12,7 @@ use keyfork_derive_openpgp::{
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_prompt::{ use keyfork_prompt::{
validators::{PinValidator, Validator}, validators::{PinValidator, Validator},
Message, PromptHandler, Terminal, Message, PromptHandler, DefaultTerminal, default_terminal
}; };
use keyfork_shard::{Format, openpgp::OpenPGP}; use keyfork_shard::{Format, openpgp::OpenPGP};
@ -105,7 +105,7 @@ fn generate_shard_secret(
output_file: &Option<PathBuf>, output_file: &Option<PathBuf>,
) -> Result<()> { ) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{256 / 8}>()?; let seed = keyfork_entropy::generate_entropy_of_const_size::<{256 / 8}>()?;
let mut pm = Terminal::new(std::io::stdin(), std::io::stderr())?; let mut pm = default_terminal()?;
let mut certs = vec![]; let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new(); let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout(); let stdout = std::io::stdout();
@ -165,7 +165,7 @@ fn generate_shard_secret(
certs.push(cert); certs.push(cert);
} }
let opgp = OpenPGP; let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = output_file { if let Some(output_file) = output_file {
let output = File::create(output_file)?; let output = File::create(output_file)?;

View File

@ -8,14 +8,15 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = [] default = ["bin"]
bin = ["decode-backend-rqrr"] bin = ["decode-backend-rqrr"]
decode-backend-rqrr = ["dep:rqrr"] decode-backend-rqrr = ["dep:rqrr"]
decode-backend-zbar = ["dep:keyfork-zbar"] decode-backend-zbar = ["dep:keyfork-zbar"]
[dependencies] [dependencies]
keyfork-bug = { 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"] } image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true }
rqrr = { version = "0.6.0", optional = true } rqrr = { version = "0.6.0", optional = true }
thiserror = "1.0.56" thiserror = "1.0.56"
v4l = "0.14.0" v4l = "0.14.0"

View File

@ -1,5 +1,7 @@
//! Encoding and decoding QR codes. //! Encoding and decoding QR codes.
use keyfork_bug as bug;
use image::io::Reader as ImageReader; use image::io::Reader as ImageReader;
use std::{ use std::{
io::{Cursor, Write}, io::{Cursor, Write},
@ -98,11 +100,13 @@ pub fn qrencode(
Ok(result) Ok(result)
} }
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
/// Continuously scan the `index`-th camera for a QR code. /// Continuously scan the `index`-th camera for a QR code.
#[cfg(feature = "decode-backend-rqrr")] #[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;
let mut fmt = device.format().expect("Failed to read format"); let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
fmt.fourcc = FourCC::new(b"MPG1"); fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?; device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?; let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
@ -133,7 +137,7 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
#[cfg(feature = "decode-backend-zbar")] #[cfg(feature = "decode-backend-zbar")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> { pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?; let device = Device::new(index)?;
let mut fmt = device.format().expect("Failed to read format"); let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
fmt.fourcc = FourCC::new(b"MPG1"); fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?; device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?; let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;

View File

@ -12,8 +12,8 @@ default = ["image"]
image = ["dep:image"] image = ["dep:image"]
[dependencies] [dependencies]
keyfork-zbar-sys = { version = "0.1.0", path = "../keyfork-zbar-sys", registry = "distrust" }
image = { version = "0.24.7", default-features = false, optional = true } image = { version = "0.24.7", default-features = false, optional = true }
keyfork-zbar-sys = { version = "0.1.0", path = "../keyfork-zbar-sys" }
thiserror = "1.0.56" thiserror = "1.0.56"
[dev-dependencies] [dev-dependencies]

View File

@ -2,6 +2,7 @@
name = "keyfork-bin" name = "keyfork-bin"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

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

View File

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

View File

@ -11,4 +11,5 @@ default = ["bin"]
bin = ["smex"] bin = ["smex"]
[dependencies] [dependencies]
smex = { version = "0.1.0", path = "../smex", optional = true } keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
smex = { version = "0.1.0", path = "../smex", optional = true, registry = "distrust" }

View File

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

View File

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

View File

@ -48,13 +48,9 @@
//! let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap(); //! let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
//! ``` //! ```
use std::{ use std::{error::Error, fmt::Display, marker::PhantomData, str::FromStr, sync::OnceLock};
error::Error,
fmt::Display, use keyfork_bug::bug;
str::FromStr,
sync::OnceLock,
marker::PhantomData,
};
use hmac::Hmac; use hmac::Hmac;
use pbkdf2::pbkdf2; use pbkdf2::pbkdf2;
@ -115,12 +111,11 @@ impl Wordlist for English {
fn get_singleton<'a>() -> &'a Self { fn get_singleton<'a>() -> &'a Self {
ENGLISH.get_or_init(|| { ENGLISH.get_or_init(|| {
let wordlist_file = include_str!("data/wordlist.txt"); let wordlist_file = include_str!("data/wordlist.txt");
let mut words = wordlist_file let mut words = wordlist_file.lines().skip(1).map(|x| x.trim().to_string());
.lines()
.skip(1)
.map(|x| x.trim().to_string());
English { English {
words: std::array::from_fn(|_| words.next().expect("wordlist has 2048 words")), words: std::array::from_fn(|_| {
words.next().expect(bug!("wordlist {} should have 2048 words"))
}),
} }
}) })
} }
@ -247,7 +242,10 @@ where
} }
} }
Ok(MnemonicBase { data, marker: PhantomData }) Ok(MnemonicBase {
data,
marker: PhantomData,
})
} }
} }
@ -390,7 +388,7 @@ where
let mnemonic = self.to_string(); let mnemonic = self.to_string();
let salt = ["mnemonic", passphrase.unwrap_or("")].join(""); let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed) pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.expect("HmacSha512 InvalidLength should be infallible"); .expect(bug!("HmacSha512 InvalidLength should be infallible"));
seed.to_vec() seed.to_vec()
} }
@ -415,13 +413,16 @@ where
} }
// TODO: find a way to not have to collect to vec // TODO: find a way to not have to collect to vec
bits.chunks_exact(11).peekable().map(|chunk| { bits.chunks_exact(11)
.peekable()
.map(|chunk| {
let mut num = 0usize; let mut num = 0usize;
for i in 0..11 { for i in 0..11 {
num += usize::from(chunk[10 - i]) << i; num += usize::from(chunk[10 - i]) << i;
} }
num num
}).collect() })
.collect()
} }
} }

View File

@ -13,6 +13,7 @@ default = ["mnemonic"]
mnemonic = ["keyfork-mnemonic-util"] mnemonic = ["keyfork-mnemonic-util"]
[dependencies] [dependencies]
keyfork-crossterm = { version = "0.27.1", path = "../keyfork-crossterm", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] } keyfork-bug = { version = "0.1.0", path = "../keyfork-bug", registry = "distrust" }
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", optional = true } 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" thiserror = "1.0.51"

View File

@ -13,6 +13,8 @@ use keyfork_crossterm::{
ExecutableCommand, QueueableCommand, ExecutableCommand, QueueableCommand,
}; };
use keyfork_bug::bug;
use crate::{Error, Message, PromptHandler, Wordlist}; use crate::{Error, Message, PromptHandler, Wordlist};
#[allow(missing_docs)] #[allow(missing_docs)]
@ -120,9 +122,15 @@ where
W: Write + AsRawFd, W: Write + AsRawFd,
{ {
fn drop(&mut self) { fn drop(&mut self) {
self.write.execute(DisableBracketedPaste).unwrap(); self.write
self.write.execute(LeaveAlternateScreen).unwrap(); .execute(DisableBracketedPaste)
self.terminal.disable_raw_mode().unwrap(); .expect(bug!("can't restore bracketed paste"));
self.write
.execute(LeaveAlternateScreen)
.expect(bug!("can't leave alternate screen"));
self.terminal
.disable_raw_mode()
.expect(bug!("can't disable raw mode"));
} }
} }
@ -286,6 +294,20 @@ where
} }
KeyCode::Char(c) => { KeyCode::Char(c) => {
input.push(c); input.push(c);
let entry_mode = std::env::var("KEYFORK_PROMPT_MNEMONIC_MODE");
if entry_mode.is_ok_and(|mode| mode.to_ascii_lowercase() == "steel") {
let word = input.split_whitespace().next_back().map(ToOwned::to_owned);
if let Some(steel_word) = word {
if steel_word.len() >= 4 {
for word in words.iter().filter(|word| word.len() >= 4) {
if word[..4] == steel_word {
input.push_str(&word[4..]);
input.push(' ');
}
}
}
}
}
} }
_ => (), _ => (),
}, },

View File

@ -80,6 +80,7 @@ pub mod mnemonic {
use super::Validator; use super::Validator;
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError}; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
use keyfork_bug::bug;
/// A mnemonic could not be validated from the given input. /// A mnemonic could not be validated from the given input.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -237,7 +238,7 @@ pub mod mnemonic {
Ok(output Ok(output
.try_into() .try_into()
.expect("vec with capacity of const N was not filled")) .expect(bug!("vec with capacity of const N was not filled")))
}) })
} }
} }

View File

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