Compare commits
8 Commits
1b30b17691
...
960f098b95
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 960f098b95 | |
Ryan Heywood | 752138bd35 | |
Ryan Heywood | 59c710a114 | |
Ryan Heywood | 076bc3a1f5 | |
Ryan Heywood | f206cd5db1 | |
Ryan Heywood | 1699975b57 | |
Ryan Heywood | 472d0288f9 | |
Ryan Heywood | 354eae5a6a |
|
@ -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,7 +1837,9 @@ 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-mnemonic-util",
|
"keyfork-mnemonic-util",
|
||||||
"keyfork-prompt",
|
"keyfork-prompt",
|
||||||
"keyfork-qrcode",
|
"keyfork-qrcode",
|
||||||
|
@ -1873,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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -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/
|
||||||
|
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -78,9 +78,7 @@ fn validate(
|
||||||
subkey_format: &str,
|
subkey_format: &str,
|
||||||
default_userid: &str,
|
default_userid: &str,
|
||||||
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
|
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
|
||||||
let mut pgp_u32 = [0u8; 4];
|
let index = DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true)?;
|
||||||
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
|
|
||||||
let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
|
||||||
|
|
||||||
let path = DerivationPath::from_str(path)?;
|
let path = DerivationPath::from_str(path)?;
|
||||||
assert_eq!(2, path.len(), "Expected path of m/{index}/account_id'");
|
assert_eq!(2, path.len(), "Expected path of m/{index}/account_id'");
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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")),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -14,25 +14,27 @@ 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 }
|
||||||
openpgp-card-sequoia = { version = "0.2.0", optional = true, default-features = false }
|
openpgp-card-sequoia = { version = "0.2.0", optional = true, default-features = false }
|
||||||
openpgp-card = { version = "0.4.0", optional = true }
|
openpgp-card = { version = "0.4.0", optional = true }
|
||||||
sequoia-openpgp = { version = "1.17.0", optional = true, default-features = false }
|
sequoia-openpgp = { version = "1.17.0", optional = true, default-features = false }
|
||||||
|
keyfork-derive-util = { version = "0.1.0", path = "../derive/keyfork-derive-util", default-features = false }
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
#![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_mnemonic_util::{English, Mnemonic};
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
validators::{mnemonic::MnemonicSetValidator, Validator},
|
validators::{mnemonic::MnemonicSetValidator, Validator},
|
||||||
|
@ -60,6 +66,52 @@ pub trait Format {
|
||||||
/// A type representing the parsed, but encrypted, Shard data.
|
/// A type representing the parsed, but encrypted, Shard data.
|
||||||
type EncryptedData;
|
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
|
/// Derive a signer
|
||||||
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
|
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
|
||||||
|
|
||||||
|
@ -122,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
|
||||||
|
@ -135,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.
|
||||||
|
@ -146,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)
|
||||||
|
@ -171,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
|
||||||
|
@ -187,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)
|
||||||
{
|
{
|
||||||
|
@ -195,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()))?;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,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,
|
||||||
|
@ -237,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);
|
||||||
|
@ -285,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. ",
|
||||||
|
@ -293,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}"
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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::{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,28 +224,42 @@ 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>;
|
||||||
type SigningKey = Cert;
|
type SigningKey = Cert;
|
||||||
type EncryptedData = EncryptedMessage;
|
type EncryptedData = EncryptedMessage;
|
||||||
|
|
||||||
|
fn provision_shardholder_key(
|
||||||
|
&self,
|
||||||
|
derivation_path: DerivationPath,
|
||||||
|
seed: &[u8],
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn purpose_derivation_index(&self) -> DerivationIndex {
|
||||||
|
DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// Derive an OpenPGP Shard certificate from the given seed.
|
/// Derive an OpenPGP Shard certificate from the given seed.
|
||||||
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
|
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
|
||||||
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(
|
||||||
|
@ -244,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(),
|
||||||
|
@ -371,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)
|
||||||
}
|
}
|
||||||
|
@ -400,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)?;
|
||||||
|
@ -457,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)?;
|
||||||
|
@ -499,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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -575,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();
|
||||||
|
|
||||||
|
@ -607,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);
|
||||||
|
@ -620,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();
|
||||||
|
|
||||||
|
@ -654,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()?;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(_) => {}
|
||||||
|
|
|
@ -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" }
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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]
|
|
@ -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)
|
||||||
|
}};
|
||||||
|
}
|
|
@ -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" }
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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")))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
Loading…
Reference in New Issue