Compare commits
28 Commits
81ca435de1
...
9cdd3b5aca
Author | SHA1 | Date |
---|---|---|
Anton Livaja | 9cdd3b5aca | |
Ryan Heywood | e0687434ef | |
Ryan Heywood | 23db50956f | |
Ryan Heywood | 94617722a0 | |
Ryan Heywood | 001fc0bccc | |
Ryan Heywood | 6a265ad203 | |
Ryan Heywood | 5d2309e301 | |
Ryan Heywood | c0b19e2457 | |
Ryan Heywood | cdf401515f | |
Ryan Heywood | f0e5ae9a8b | |
Ryan Heywood | 289cec36ef | |
Ryan Heywood | 0fe5301352 | |
Ryan Heywood | 9f089e723a | |
Ryan Heywood | 1de466cad0 | |
Ryan Heywood | 57354fc714 | |
Ryan Heywood | 61871a77f0 | |
Ryan Heywood | 08a66e2365 | |
Ryan Heywood | 6fa434e89c | |
Ryan Heywood | 68f07f6f02 | |
Ryan Heywood | 9394500f2f | |
Ryan Heywood | 2bca0a1580 | |
Ryan Heywood | 5438f4e111 | |
Ryan Heywood | 71b6e4ed0c | |
Ryan Heywood | 4f4e3cfc65 | |
Ryan Heywood | 194d475d59 | |
Ryan Heywood | 40551a5c26 | |
Ryan Heywood | fa125e7cbe | |
Ryan Heywood | f96ad11422 |
|
@ -341,6 +341,12 @@ version = "0.21.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -1497,9 +1503,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.59"
|
version = "0.1.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
|
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_system_properties",
|
"android_system_properties",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
|
@ -1572,15 +1578,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.34.0"
|
version = "1.38.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc"
|
checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console",
|
"console",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
"similar",
|
"similar",
|
||||||
"yaml-rust",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1758,7 +1763,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-derive-util"
|
name = "keyfork-derive-util"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
@ -1776,7 +1781,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-entropy"
|
name = "keyfork-entropy"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"keyfork-bug",
|
"keyfork-bug",
|
||||||
"smex",
|
"smex",
|
||||||
|
@ -1818,7 +1823,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-qrcode"
|
name = "keyfork-qrcode"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"keyfork-bug",
|
"keyfork-bug",
|
||||||
|
@ -1830,10 +1835,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-shard"
|
name = "keyfork-shard"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64 0.22.0",
|
||||||
"card-backend",
|
"card-backend",
|
||||||
"card-backend-pcsc",
|
"card-backend-pcsc",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
|
@ -1879,7 +1885,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyforkd"
|
name = "keyforkd"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
|
@ -2103,9 +2109,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.10"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
@ -2886,7 +2892,7 @@ dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64 0.21.7",
|
||||||
"block-padding",
|
"block-padding",
|
||||||
"blowfish",
|
"blowfish",
|
||||||
"buffered-reader",
|
"buffered-reader",
|
||||||
|
@ -3811,15 +3817,6 @@ version = "0.8.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61"
|
checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yaml-rust"
|
|
||||||
version = "0.4.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
|
||||||
dependencies = [
|
|
||||||
"linked-hash-map",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.32"
|
version = "0.7.32"
|
||||||
|
|
|
@ -234,7 +234,7 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
let depth = path.len() as u8;
|
let depth = path.len() as u8;
|
||||||
Ok(ExtendedPrivateKey::new_from_parts(
|
Ok(ExtendedPrivateKey::from_parts(
|
||||||
&d.data,
|
&d.data,
|
||||||
depth,
|
depth,
|
||||||
d.chain_code,
|
d.chain_code,
|
||||||
|
|
|
@ -25,6 +25,9 @@ fn secp256k1_test_suite() {
|
||||||
if chain_len < 2 {
|
if chain_len < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if chain.iter().take(2).any(|index| !index.is_hardened()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Consistency check: ensure the server and the client can each derive the same
|
// Consistency check: ensure the server and the client can each derive the same
|
||||||
// key using an XPrv, for all but the last XPrv, which is verified after this
|
// key using an XPrv, for all but the last XPrv, which is verified after this
|
||||||
for i in 2..chain_len {
|
for i in 2..chain_len {
|
||||||
|
|
|
@ -43,6 +43,10 @@ pub enum DerivationError {
|
||||||
#[error("Invalid derivation length: Expected at least 2, actual: {0}")]
|
#[error("Invalid derivation length: Expected at least 2, actual: {0}")]
|
||||||
InvalidDerivationLength(usize),
|
InvalidDerivationLength(usize),
|
||||||
|
|
||||||
|
/// The derivation request did not use hardened derivation on the 2 highest indexes.
|
||||||
|
#[error("Invalid derivation paths: expected index #{0} (1) to be hardened")]
|
||||||
|
InvalidDerivationPath(usize, u32),
|
||||||
|
|
||||||
/// An error occurred while deriving data.
|
/// An error occurred while deriving data.
|
||||||
#[error("Derivation error: {0}")]
|
#[error("Derivation error: {0}")]
|
||||||
Derivation(String),
|
Derivation(String),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyforkd"
|
name = "keyforkd"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,18 @@ impl Service<Request> for Keyforkd {
|
||||||
return Err(DerivationError::InvalidDerivationLength(len).into());
|
return Err(DerivationError::InvalidDerivationLength(len).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some((i, unhardened_index)) = req
|
||||||
|
.path()
|
||||||
|
.iter()
|
||||||
|
.take(2)
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, index)| {
|
||||||
|
!index.is_hardened()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
if let Some(target) = guess_target(req.path()) {
|
if let Some(target) = guess_target(req.path()) {
|
||||||
info!("Deriving path: {target}");
|
info!("Deriving path: {target}");
|
||||||
|
@ -110,6 +122,9 @@ mod tests {
|
||||||
if chain.len() < 2 {
|
if chain.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if chain.iter().take(2).any(|index| !index.is_hardened()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
|
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
|
||||||
let response: DerivationResponse = keyforkd
|
let response: DerivationResponse = keyforkd
|
||||||
.ready()
|
.ready()
|
||||||
|
|
|
@ -61,7 +61,7 @@ where
|
||||||
));
|
));
|
||||||
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
|
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
|
||||||
let socket_path = socket_dir.path().join("keyforkd.sock");
|
let socket_path = socket_dir.path().join("keyforkd.sock");
|
||||||
rt.block_on(async move {
|
let result = rt.block_on(async move {
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
||||||
let server_handle = tokio::spawn({
|
let server_handle = tokio::spawn({
|
||||||
let socket_path = socket_path.clone();
|
let socket_path = socket_path.clone();
|
||||||
|
@ -87,8 +87,13 @@ where
|
||||||
let result = test_handle.await;
|
let result = test_handle.await;
|
||||||
server_handle.abort();
|
server_handle.abort();
|
||||||
result
|
result
|
||||||
})
|
});
|
||||||
.expect(bug!("runtime could not join all threads"))
|
if let Err(e) = result {
|
||||||
|
if e.is_panic() {
|
||||||
|
std::panic::resume_unwind(e.into_panic());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -59,13 +59,17 @@ pub enum Error {
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
/// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
|
/// Create an OpenPGP Cert with private key data, with derived keys from the given derivation
|
||||||
/// ID.
|
/// response, keys, and User ID.
|
||||||
|
///
|
||||||
|
/// Certificates are created with a default expiration of one day, but may be configured to expire
|
||||||
|
/// later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as "15d" (15
|
||||||
|
/// days), "1m" (one month), or "2y" (two years).
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may error for any condition mentioned in [`Error`].
|
/// The function may error for any condition mentioned in [`Error`].
|
||||||
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
let primary_key_flags = match keys.get(0) {
|
let primary_key_flags = match keys.first() {
|
||||||
Some(kf) if kf.for_certification() => kf,
|
Some(kf) if kf.for_certification() => kf,
|
||||||
_ => return Err(Error::NotCert),
|
_ => return Err(Error::NotCert),
|
||||||
};
|
};
|
||||||
|
@ -109,7 +113,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
|
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
|
||||||
let policy = sequoia_openpgp::policy::StandardPolicy::new();
|
let policy = sequoia_openpgp::policy::StandardPolicy::new();
|
||||||
|
|
||||||
// Set certificate expiration to one day
|
// Set certificate expiration to configured expiration or (default) one day
|
||||||
let mut keypair = primary_key.clone().into_keypair()?;
|
let mut keypair = primary_key.clone().into_keypair()?;
|
||||||
let signatures =
|
let signatures =
|
||||||
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;
|
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-derive-util"
|
name = "keyfork-derive-util"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|
|
@ -124,9 +124,9 @@ mod serde_with {
|
||||||
K: PrivateKey + Clone,
|
K: PrivateKey + Clone,
|
||||||
{
|
{
|
||||||
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
|
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
|
||||||
let bytes: [u8; 32] = variable_len_bytes
|
let bytes: [u8; 32] = variable_len_bytes.try_into().expect(bug!(
|
||||||
.try_into()
|
"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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,13 +179,20 @@ where
|
||||||
.into_bytes();
|
.into_bytes();
|
||||||
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
|
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
|
||||||
|
|
||||||
Self::new_from_parts(
|
assert!(
|
||||||
|
!private_key.iter().all(|byte| *byte == 0),
|
||||||
|
bug!("hmac function returned all-zero master key")
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::from_parts(
|
||||||
private_key
|
private_key
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
|
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
|
||||||
0,
|
0,
|
||||||
// Checked: chain_code is always the same length, hash is static size
|
// Checked: chain_code is always the same length, hash is static size
|
||||||
chain_code.try_into().expect(bug!("Invalid chain code length")),
|
chain_code
|
||||||
|
.try_into()
|
||||||
|
.expect(bug!("Invalid chain code length")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,9 +212,9 @@ where
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new_from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self {
|
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
private_key: K::from_bytes(key),
|
private_key: K::from_bytes(key),
|
||||||
depth,
|
depth,
|
||||||
|
@ -229,7 +236,7 @@ where
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
|
||||||
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key));
|
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn private_key(&self) -> &K {
|
pub fn private_key(&self) -> &K {
|
||||||
|
@ -262,7 +269,7 @@ where
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
|
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
|
||||||
ExtendedPublicKey::new_from_parts(self.public_key(), self.depth, self.chain_code)
|
ExtendedPublicKey::from_parts(self.public_key(), self.depth, self.chain_code)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a public key for the current [`PrivateKey`].
|
/// Return a public key for the current [`PrivateKey`].
|
||||||
|
@ -301,7 +308,7 @@ where
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
|
||||||
/// assert_eq!(xprv.depth(), 4);
|
/// assert_eq!(xprv.depth(), 4);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn depth(&self) -> u8 {
|
pub fn depth(&self) -> u8 {
|
||||||
|
@ -321,7 +328,7 @@ where
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
|
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
|
||||||
/// assert_eq!(chain_code, &xprv.chain_code());
|
/// assert_eq!(chain_code, &xprv.chain_code());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn chain_code(&self) -> [u8; 32] {
|
pub fn chain_code(&self) -> [u8; 32] {
|
||||||
|
|
|
@ -11,8 +11,8 @@ const KEY_SIZE: usize = 256;
|
||||||
/// Errors associated with creating or deriving Extended Public Keys.
|
/// Errors associated with creating or deriving Extended Public Keys.
|
||||||
#[derive(Error, Clone, Debug)]
|
#[derive(Error, Clone, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// BIP-0032 does not support deriving public keys from hardened private keys.
|
/// BIP-0032 does not support hardened public key derivation from parent public keys.
|
||||||
#[error("Public keys may not be derived when hardened")]
|
#[error("Hardened child public keys may not be derived from parent public keys")]
|
||||||
HardenedIndex,
|
HardenedIndex,
|
||||||
|
|
||||||
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
|
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
|
||||||
|
@ -60,11 +60,11 @@ where
|
||||||
/// let chain_code: &[u8; 32] = //
|
/// let chain_code: &[u8; 32] = //
|
||||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// let pubkey = PublicKey::from_bytes(key);
|
/// let pubkey = PublicKey::from_bytes(key);
|
||||||
/// let xpub = ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
|
/// let xpub = ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new_from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
|
pub fn from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
|
||||||
Self {
|
Self {
|
||||||
public_key,
|
public_key,
|
||||||
depth,
|
depth,
|
||||||
|
@ -86,7 +86,7 @@ where
|
||||||
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// # let pubkey = PublicKey::from_bytes(key);
|
/// # let pubkey = PublicKey::from_bytes(key);
|
||||||
/// let xpub = //
|
/// let xpub = //
|
||||||
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
|
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
||||||
/// let pubkey = xpub.public_key();
|
/// let pubkey = xpub.public_key();
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
|
@ -121,7 +121,7 @@ where
|
||||||
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
/// # let pubkey = PublicKey::from_bytes(key);
|
/// # let pubkey = PublicKey::from_bytes(key);
|
||||||
/// let xpub = //
|
/// let xpub = //
|
||||||
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
|
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
||||||
/// let index = DerivationIndex::new(0, false)?;
|
/// let index = DerivationIndex::new(0, false)?;
|
||||||
/// let child = xpub.derive_child(&index)?;
|
/// let child = xpub.derive_child(&index)?;
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
|
|
|
@ -85,7 +85,7 @@ pub trait PrivateKey: Sized {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// An error may be returned if:
|
/// An error may be returned if:
|
||||||
/// * A nonzero `other` is provided.
|
/// * An all-zero `other` is provided.
|
||||||
/// * An error specific to the given algorithm was encountered.
|
/// * An error specific to the given algorithm was encountered.
|
||||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
|
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||||
|
|
||||||
|
@ -102,6 +102,10 @@ pub enum PrivateKeyError {
|
||||||
/// For the given algorithm, the private key must be nonzero.
|
/// For the given algorithm, the private key must be nonzero.
|
||||||
#[error("The provided private key must be nonzero, but is not")]
|
#[error("The provided private key must be nonzero, but is not")]
|
||||||
NonZero,
|
NonZero,
|
||||||
|
|
||||||
|
/// A scalar could not be constructed for the given algorithm.
|
||||||
|
#[error("A scalar could not be constructed for the given algorithm")]
|
||||||
|
InvalidScalar,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
|
@ -130,20 +134,19 @@ impl PrivateKey for k256::SecretKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||||
if other.iter().all(|n| n == &0) {
|
use k256::elliptic_curve::ScalarPrimitive;
|
||||||
return Err(PrivateKeyError::NonZero);
|
use k256::{Scalar, Secp256k1};
|
||||||
}
|
|
||||||
let other = *other;
|
// Construct a scalar from bytes
|
||||||
// Checked: See above nonzero check
|
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(other.into());
|
||||||
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
|
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
|
||||||
.expect(bug!("Should have been able to get a NonZeroScalar"));
|
let scalar = scalar.ok_or(PrivateKeyError::InvalidScalar)?;
|
||||||
|
let scalar = Scalar::from(scalar);
|
||||||
|
|
||||||
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
|
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
|
||||||
Ok(
|
let nonzero_scalar = Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
|
||||||
Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
|
.ok_or(PrivateKeyError::NonZero)?;
|
||||||
.map(Into::into)
|
Ok(Self::from(nonzero_scalar))
|
||||||
.expect(bug!("Should be able to make Key")),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +183,8 @@ impl PrivateKey for ed25519_dalek::SigningKey {
|
||||||
|
|
||||||
use crate::public_key::TestPublicKey;
|
use crate::public_key::TestPublicKey;
|
||||||
|
|
||||||
#[doc(hidden)]
|
/// A private key that can be used for testing purposes. Does not utilize any significant
|
||||||
|
/// cryptographic operations.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct TestPrivateKey {
|
pub struct TestPrivateKey {
|
||||||
key: [u8; 32],
|
key: [u8; 32],
|
||||||
|
@ -201,9 +205,7 @@ impl PrivateKey for TestPrivateKey {
|
||||||
type Err = PrivateKeyError;
|
type Err = PrivateKeyError;
|
||||||
|
|
||||||
fn from_bytes(b: &PrivateKeyBytes) -> Self {
|
fn from_bytes(b: &PrivateKeyBytes) -> Self {
|
||||||
Self {
|
Self { key: *b }
|
||||||
key: *b
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_bytes(&self) -> PrivateKeyBytes {
|
fn to_bytes(&self) -> PrivateKeyBytes {
|
||||||
|
|
|
@ -42,7 +42,7 @@ pub trait PublicKey: Sized {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// An error may be returned if:
|
/// An error may be returned if:
|
||||||
/// * A nonzero `other` is provided.
|
/// * An all-zero `other` is provided.
|
||||||
/// * An error specific to the given algorithm was encountered.
|
/// * An error specific to the given algorithm was encountered.
|
||||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
|
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||||
|
|
||||||
|
@ -77,6 +77,10 @@ pub enum PublicKeyError {
|
||||||
#[error("The provided public key must be nonzero, but is not")]
|
#[error("The provided public key must be nonzero, but is not")]
|
||||||
NonZero,
|
NonZero,
|
||||||
|
|
||||||
|
/// A scalar could not be constructed for the given algorithm.
|
||||||
|
#[error("A scalar could not be constructed for the given algorithm")]
|
||||||
|
InvalidScalar,
|
||||||
|
|
||||||
/// Public key derivation is unsupported for this algorithm.
|
/// Public key derivation is unsupported for this algorithm.
|
||||||
#[error("Public key derivation is unsupported for this algorithm")]
|
#[error("Public key derivation is unsupported for this algorithm")]
|
||||||
DerivationUnsupported,
|
DerivationUnsupported,
|
||||||
|
@ -85,7 +89,7 @@ pub enum PublicKeyError {
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
use k256::{
|
use k256::{
|
||||||
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
|
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
|
||||||
AffinePoint, NonZeroScalar,
|
AffinePoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
|
@ -105,14 +109,16 @@ impl PublicKey for k256::PublicKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
|
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||||
if other.iter().all(|n| n == &0) {
|
use k256::elliptic_curve::ScalarPrimitive;
|
||||||
return Err(PublicKeyError::NonZero);
|
use k256::{Secp256k1, Scalar};
|
||||||
}
|
|
||||||
// Checked: See above
|
|
||||||
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
|
|
||||||
.expect(bug!("Should have been able to get a NonZeroScalar"));
|
|
||||||
|
|
||||||
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
|
// Construct a scalar from bytes
|
||||||
|
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into());
|
||||||
|
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
|
||||||
|
let scalar = scalar.ok_or(PublicKeyError::InvalidScalar)?;
|
||||||
|
let scalar = Scalar::from(scalar);
|
||||||
|
|
||||||
|
let point = self.to_projective() + (AffinePoint::generator() * scalar);
|
||||||
Ok(Self::from_affine(point.into())
|
Ok(Self::from_affine(point.into())
|
||||||
.expect(bug!("Could not from_affine after scalar arithmetic")))
|
.expect(bug!("Could not from_affine after scalar arithmetic")))
|
||||||
}
|
}
|
||||||
|
@ -142,14 +148,15 @@ impl PublicKey for VerifyingKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
/// A public key that can be used for testing purposes. Does not utilize any significant
|
||||||
|
/// cryptographic operations.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TestPublicKey {
|
pub struct TestPublicKey {
|
||||||
pub(crate) key: [u8; 33],
|
pub(crate) key: [u8; 33],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestPublicKey {
|
impl TestPublicKey {
|
||||||
#[doc(hidden)]
|
/// Create a new TestPublicKey from the given bytes.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn from_bytes(b: &[u8]) -> Self {
|
pub fn from_bytes(b: &[u8]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
@ -57,7 +57,7 @@ pub enum DerivationAlgorithm {
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
Secp256k1,
|
Secp256k1,
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
Internal,
|
TestAlgorithm,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerivationAlgorithm {
|
impl DerivationAlgorithm {
|
||||||
|
@ -86,7 +86,7 @@ impl DerivationAlgorithm {
|
||||||
&derived_key,
|
&derived_key,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Self::Internal => {
|
Self::TestAlgorithm => {
|
||||||
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed);
|
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed);
|
||||||
let derived_key = key.derive_path(path)?;
|
let derived_key = key.derive_path(path)?;
|
||||||
Ok(DerivationResponse::with_algo_and_xprv(
|
Ok(DerivationResponse::with_algo_and_xprv(
|
||||||
|
@ -120,7 +120,7 @@ pub trait AsAlgorithm: PrivateKey {
|
||||||
|
|
||||||
impl AsAlgorithm for TestPrivateKey {
|
impl AsAlgorithm for TestPrivateKey {
|
||||||
fn as_algorithm() -> DerivationAlgorithm {
|
fn as_algorithm() -> DerivationAlgorithm {
|
||||||
DerivationAlgorithm::Internal
|
DerivationAlgorithm::TestAlgorithm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ impl DerivationRequest {
|
||||||
/// # };
|
/// # };
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let algo: DerivationAlgorithm = //
|
/// let algo: DerivationAlgorithm = //
|
||||||
/// # DerivationAlgorithm::Internal;
|
/// # DerivationAlgorithm::TestAlgorithm;
|
||||||
/// let path: DerivationPath = //
|
/// let path: DerivationPath = //
|
||||||
/// # DerivationPath::default();
|
/// # DerivationPath::default();
|
||||||
/// let request = DerivationRequest::new(algo, &path);
|
/// let request = DerivationRequest::new(algo, &path);
|
||||||
|
@ -169,7 +169,7 @@ impl DerivationRequest {
|
||||||
/// # };
|
/// # };
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let algo: DerivationAlgorithm = //
|
/// let algo: DerivationAlgorithm = //
|
||||||
/// # DerivationAlgorithm::Internal;
|
/// # DerivationAlgorithm::TestAlgorithm;
|
||||||
/// let path: DerivationPath = //
|
/// let path: DerivationPath = //
|
||||||
/// # DerivationPath::default();
|
/// # DerivationPath::default();
|
||||||
/// let request = DerivationRequest::new(algo, &path);
|
/// let request = DerivationRequest::new(algo, &path);
|
||||||
|
@ -199,7 +199,7 @@ impl DerivationRequest {
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
/// # )?;
|
/// # )?;
|
||||||
/// let algo: DerivationAlgorithm = //
|
/// let algo: DerivationAlgorithm = //
|
||||||
/// # DerivationAlgorithm::Internal;
|
/// # DerivationAlgorithm::TestAlgorithm;
|
||||||
/// let path: DerivationPath = //
|
/// let path: DerivationPath = //
|
||||||
/// # DerivationPath::default();
|
/// # DerivationPath::default();
|
||||||
/// let request = DerivationRequest::new(algo, &path);
|
/// let request = DerivationRequest::new(algo, &path);
|
||||||
|
@ -228,7 +228,7 @@ impl DerivationRequest {
|
||||||
/// let seed: &[u8; 64] = //
|
/// let seed: &[u8; 64] = //
|
||||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let algo: DerivationAlgorithm = //
|
/// let algo: DerivationAlgorithm = //
|
||||||
/// # DerivationAlgorithm::Internal;
|
/// # DerivationAlgorithm::TestAlgorithm;
|
||||||
/// let path: DerivationPath = //
|
/// let path: DerivationPath = //
|
||||||
/// # DerivationPath::default();
|
/// # DerivationPath::default();
|
||||||
/// let request = DerivationRequest::new(algo, &path);
|
/// let request = DerivationRequest::new(algo, &path);
|
||||||
|
@ -300,7 +300,7 @@ mod secp256k1 {
|
||||||
|
|
||||||
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
||||||
match value.algorithm {
|
match value.algorithm {
|
||||||
DerivationAlgorithm::Secp256k1 => Ok(Self::new_from_parts(
|
DerivationAlgorithm::Secp256k1 => Ok(Self::from_parts(
|
||||||
&value.data,
|
&value.data,
|
||||||
value.depth,
|
value.depth,
|
||||||
value.chain_code,
|
value.chain_code,
|
||||||
|
@ -335,7 +335,7 @@ mod ed25519 {
|
||||||
|
|
||||||
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
||||||
match value.algorithm {
|
match value.algorithm {
|
||||||
DerivationAlgorithm::Ed25519 => Ok(Self::new_from_parts(
|
DerivationAlgorithm::Ed25519 => Ok(Self::from_parts(
|
||||||
&value.data,
|
&value.data,
|
||||||
value.depth,
|
value.depth,
|
||||||
value.chain_code,
|
value.chain_code,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-shard"
|
name = "keyfork-shard"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
|
@ -37,3 +37,4 @@ 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 }
|
||||||
|
base64 = "0.22.0"
|
||||||
|
|
|
@ -7,22 +7,28 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{consts::U12, Aead, AeadCore, OsRng},
|
aead::{consts::U12, Aead},
|
||||||
Aes256Gcm, KeyInit, Nonce,
|
Aes256Gcm, KeyInit, Nonce,
|
||||||
};
|
};
|
||||||
|
use base64::prelude::{Engine, BASE64_STANDARD};
|
||||||
use hkdf::Hkdf;
|
use hkdf::Hkdf;
|
||||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||||
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, MnemonicValidator, WordLength},
|
||||||
|
Validator,
|
||||||
|
},
|
||||||
Message as PromptMessage, PromptHandler, Terminal,
|
Message as PromptMessage, PromptHandler, Terminal,
|
||||||
};
|
};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use sharks::{Share, Sharks};
|
use sharks::{Share, Sharks};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
|
// 32-byte share, 1-byte index, 1-byte threshold, 1-byte version == 36 bytes
|
||||||
const ENC_LEN: u8 = 4 * 16;
|
// Encrypted, is 52 bytes
|
||||||
|
const PLAINTEXT_LENGTH: u8 = 36;
|
||||||
|
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
||||||
|
|
||||||
#[cfg(feature = "openpgp")]
|
#[cfg(feature = "openpgp")]
|
||||||
pub mod openpgp;
|
pub mod openpgp;
|
||||||
|
@ -194,7 +200,6 @@ pub trait Format {
|
||||||
let encrypted_messages = self.parse_shard_file(reader)?;
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||||
|
|
||||||
// establish AES-256-GCM key via ECDH
|
// establish AES-256-GCM key via ECDH
|
||||||
let mut nonce_data: Option<[u8; 12]> = None;
|
|
||||||
let mut pubkey_data: Option<[u8; 32]> = None;
|
let mut pubkey_data: Option<[u8; 32]> = None;
|
||||||
|
|
||||||
// receive remote data via scanning QR code from camera
|
// receive remote data via scanning QR code from camera
|
||||||
|
@ -204,12 +209,13 @@ pub trait Format {
|
||||||
.lock()
|
.lock()
|
||||||
.expect(bug!(POISONED_MUTEX))
|
.expect(bug!(POISONED_MUTEX))
|
||||||
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
if let Ok(Some(hex)) =
|
if let Ok(Some(qrcode_content)) =
|
||||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
||||||
{
|
{
|
||||||
let decoded_data = smex::decode(&hex)?;
|
let decoded_data = BASE64_STANDARD
|
||||||
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
|
.decode(qrcode_content)
|
||||||
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
|
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||||
|
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
|
||||||
} else {
|
} else {
|
||||||
prompt
|
prompt
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -219,43 +225,43 @@ pub trait Format {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
||||||
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
|
let their_pubkey = match pubkey_data {
|
||||||
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
|
Some(pubkey) => pubkey,
|
||||||
_ => {
|
None => {
|
||||||
let validator = MnemonicSetValidator {
|
let validator = MnemonicValidator {
|
||||||
word_lengths: [9, 24],
|
word_length: Some(WordLength::Count(24)),
|
||||||
};
|
};
|
||||||
let [nonce_mnemonic, pubkey_mnemonic] = prompt
|
prompt
|
||||||
.lock()
|
.lock()
|
||||||
.expect(bug!(POISONED_MUTEX))
|
.expect(bug!(POISONED_MUTEX))
|
||||||
.prompt_validated_wordlist::<English, _>(
|
.prompt_validated_wordlist::<English, _>(
|
||||||
QRCODE_COULDNT_READ,
|
QRCODE_COULDNT_READ,
|
||||||
3,
|
3,
|
||||||
validator.to_fn(),
|
validator.to_fn(),
|
||||||
)?;
|
)?
|
||||||
|
|
||||||
let nonce = nonce_mnemonic
|
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| InvalidData)?;
|
.map_err(|_| InvalidData)?
|
||||||
let pubkey = pubkey_mnemonic
|
|
||||||
.as_bytes()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| InvalidData)?;
|
|
||||||
(nonce, pubkey)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// create our shared key
|
// create our shared key
|
||||||
let our_key = EphemeralSecret::random();
|
let our_key = EphemeralSecret::random();
|
||||||
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
||||||
let shared_secret = our_key
|
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
|
||||||
.diffie_hellman(&PublicKey::from(their_pubkey))
|
assert!(
|
||||||
.to_bytes();
|
shared_secret.was_contributory(),
|
||||||
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
bug!("shared secret might be insecure")
|
||||||
let mut hkdf_output = [0u8; 256 / 8];
|
);
|
||||||
hkdf.expand(&[], &mut hkdf_output)?;
|
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
|
||||||
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
|
||||||
|
let mut shared_key_data = [0u8; 256 / 8];
|
||||||
|
hkdf.expand(b"key", &mut shared_key_data)?;
|
||||||
|
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
|
||||||
|
|
||||||
|
let mut nonce_data = [0u8; 12];
|
||||||
|
hkdf.expand(b"nonce", &mut nonce_data)?;
|
||||||
|
let nonce = Nonce::<U12>::from_slice(&nonce_data);
|
||||||
|
|
||||||
// decrypt a single shard and create the payload
|
// decrypt a single shard and create the payload
|
||||||
let (share, threshold) =
|
let (share, threshold) =
|
||||||
|
@ -264,49 +270,47 @@ pub trait Format {
|
||||||
payload.insert(0, HUNK_VERSION);
|
payload.insert(0, HUNK_VERSION);
|
||||||
payload.insert(1, threshold);
|
payload.insert(1, threshold);
|
||||||
assert!(
|
assert!(
|
||||||
payload.len() <= ENC_LEN as usize,
|
payload.len() < PLAINTEXT_LENGTH as usize,
|
||||||
"invalid share length (too long, max {ENC_LEN} bytes)"
|
"invalid share length (too long, max {PLAINTEXT_LENGTH} bytes)"
|
||||||
);
|
);
|
||||||
|
|
||||||
// encrypt data
|
// convert plaintext to static-size payload
|
||||||
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)]
|
#[allow(clippy::assertions_on_constants)]
|
||||||
{
|
{
|
||||||
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
|
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
|
||||||
|
// duplicate mnemonic words. This version does not include that, and instead uses a
|
||||||
|
// repeated length byte.
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
|
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
|
||||||
assert!(
|
"previously asserted length must be < {PLAINTEXT_LENGTH}",
|
||||||
payload_bytes.len() < out_bytes.len(),
|
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
|
||||||
"encrypted payload larger than acceptable limit"
|
)); PLAINTEXT_LENGTH as usize];
|
||||||
|
plaintext_bytes[..payload.len()].clone_from_slice(&payload);
|
||||||
|
|
||||||
|
// encrypt data
|
||||||
|
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?;
|
||||||
|
assert_eq!(
|
||||||
|
encrypted_bytes.len(),
|
||||||
|
ENCRYPTED_LENGTH as usize,
|
||||||
|
bug!("encrypted bytes size != expected len"),
|
||||||
);
|
);
|
||||||
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
|
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
|
||||||
|
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
|
||||||
|
|
||||||
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
|
let payload_mnemonic = Mnemonic::from_nonstandard_bytes(mnemonic_bytes);
|
||||||
// difficulty when entering in prompts manually, as one's place could be lost due to
|
|
||||||
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
|
|
||||||
// but not including the last byte.
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
|
|
||||||
.iter_mut()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
*byte = (i % u8::MAX as usize) as u8;
|
|
||||||
}
|
|
||||||
|
|
||||||
// safety: size of out_bytes is constant and always % 4 == 0
|
|
||||||
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
|
|
||||||
|
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
||||||
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
||||||
qrcode_data.extend(payload_mnemonic.as_bytes());
|
qrcode_data.extend(payload_mnemonic.as_bytes());
|
||||||
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
if let Ok(qrcode) = qrencode(
|
||||||
|
&BASE64_STANDARD.encode(qrcode_data),
|
||||||
|
ErrorCorrection::Highest,
|
||||||
|
) {
|
||||||
prompt
|
prompt
|
||||||
.lock()
|
.lock()
|
||||||
.expect(bug!(POISONED_MUTEX))
|
.expect(bug!(POISONED_MUTEX))
|
||||||
|
@ -401,7 +405,7 @@ pub struct InvalidData;
|
||||||
/// 1 byte: Version
|
/// 1 byte: Version
|
||||||
/// 1 byte: Threshold
|
/// 1 byte: Threshold
|
||||||
/// Data: &[u8]
|
/// Data: &[u8]
|
||||||
pub(crate) const HUNK_VERSION: u8 = 1;
|
pub(crate) const HUNK_VERSION: u8 = 2;
|
||||||
pub(crate) const HUNK_OFFSET: usize = 2;
|
pub(crate) const HUNK_OFFSET: usize = 2;
|
||||||
|
|
||||||
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
||||||
|
@ -432,22 +436,22 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
|
|
||||||
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
||||||
iter += 1;
|
iter += 1;
|
||||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
|
||||||
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
|
|
||||||
let our_key = EphemeralSecret::random();
|
let our_key = EphemeralSecret::random();
|
||||||
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
||||||
|
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
||||||
let mut qrcode_data = nonce_mnemonic.to_bytes();
|
let qrcode_data = key_mnemonic.to_bytes();
|
||||||
qrcode_data.extend(key_mnemonic.as_bytes());
|
if let Ok(qrcode) = qrencode(
|
||||||
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
&BASE64_STANDARD.encode(qrcode_data),
|
||||||
|
ErrorCorrection::Highest,
|
||||||
|
) {
|
||||||
pm.prompt_message(PromptMessage::Text(format!(
|
pm.prompt_message(PromptMessage::Text(format!(
|
||||||
concat!(
|
concat!(
|
||||||
"A QR code will be displayed after this prompt. ",
|
"QR code #{iter} will be displayed after this prompt. ",
|
||||||
"Send the QR code to only shardholder {iter}. ",
|
"Send the QR code to the next shardholder. ",
|
||||||
"Nobody else should scan this QR code."
|
"Only the next shardholder should scan the QR code."
|
||||||
),
|
),
|
||||||
iter = iter
|
iter = iter
|
||||||
)))?;
|
)))?;
|
||||||
|
@ -457,11 +461,9 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
|
|
||||||
pm.prompt_message(PromptMessage::Text(format!(
|
pm.prompt_message(PromptMessage::Text(format!(
|
||||||
concat!(
|
concat!(
|
||||||
"Upon request, these words should be sent to shardholder {iter}: ",
|
"Upon request, these words should be sent to the shardholder: ",
|
||||||
"{nonce_mnemonic} {key_mnemonic}"
|
"{key_mnemonic}"
|
||||||
),
|
),
|
||||||
iter = iter,
|
|
||||||
nonce_mnemonic = nonce_mnemonic,
|
|
||||||
key_mnemonic = key_mnemonic,
|
key_mnemonic = key_mnemonic,
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
|
@ -471,10 +473,17 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
if let Ok(Some(hex)) =
|
if let Ok(Some(qrcode_content)) =
|
||||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
||||||
{
|
{
|
||||||
let decoded_data = smex::decode(&hex)?;
|
let decoded_data = BASE64_STANDARD
|
||||||
|
.decode(qrcode_content)
|
||||||
|
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||||
|
assert_eq!(
|
||||||
|
decoded_data.len(),
|
||||||
|
ENCRYPTED_LENGTH as usize,
|
||||||
|
bug!("invalid payload data")
|
||||||
|
);
|
||||||
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
||||||
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
||||||
} else {
|
} else {
|
||||||
|
@ -486,7 +495,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
(Some(pubkey), Some(payload)) => (pubkey, payload),
|
(Some(pubkey), Some(payload)) => (pubkey, payload),
|
||||||
_ => {
|
_ => {
|
||||||
let validator = MnemonicSetValidator {
|
let validator = MnemonicSetValidator {
|
||||||
word_lengths: [24, 48],
|
word_lengths: [24, 39],
|
||||||
};
|
};
|
||||||
|
|
||||||
let [pubkey_mnemonic, payload_mnemonic] = pm
|
let [pubkey_mnemonic, payload_mnemonic] = pm
|
||||||
|
@ -504,14 +513,28 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
|
assert_eq!(
|
||||||
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
payload.len(),
|
||||||
let mut hkdf_output = [0u8; 256 / 8];
|
ENCRYPTED_LENGTH as usize,
|
||||||
hkdf.expand(&[], &mut hkdf_output)?;
|
bug!("invalid payload data")
|
||||||
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
);
|
||||||
|
|
||||||
let payload =
|
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
|
||||||
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
|
assert!(
|
||||||
|
shared_secret.was_contributory(),
|
||||||
|
bug!("shared secret might be insecure")
|
||||||
|
);
|
||||||
|
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
|
||||||
|
|
||||||
|
let mut shared_key_data = [0u8; 256 / 8];
|
||||||
|
hkdf.expand(b"key", &mut shared_key_data)?;
|
||||||
|
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
|
||||||
|
|
||||||
|
let mut nonce_data = [0u8; 12];
|
||||||
|
hkdf.expand(b"nonce", &mut nonce_data)?;
|
||||||
|
let nonce = Nonce::<U12>::from_slice(&nonce_data);
|
||||||
|
|
||||||
|
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
|
||||||
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
|
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
|
||||||
|
|
||||||
match &mut iter_count {
|
match &mut iter_count {
|
||||||
|
@ -526,7 +549,8 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shares.push(payload[HUNK_OFFSET..].to_vec());
|
let payload_len = payload.last().expect(bug!("payload should not be empty"));
|
||||||
|
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
let shares = shares
|
let shares = shares
|
||||||
|
|
|
@ -194,33 +194,52 @@ impl<P: PromptHandler> OpenPGP<P> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: PromptHandler> OpenPGP<P> {
|
impl<P: PromptHandler> OpenPGP<P> {
|
||||||
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them.
|
||||||
/// from a file, or from files one level deep in a directory.
|
///
|
||||||
|
/// Certificates are read from a file, or from files one level deep in a directory.
|
||||||
|
/// Certificates with duplicated fingerprints will be discarded.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
|
/// The function may return an error if it is unable to read the directory or if Sequoia is
|
||||||
/// to load certificates from the file.
|
/// unable to load certificates from the file.
|
||||||
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
let mut pubkeys = std::collections::HashSet::new();
|
||||||
|
let mut certs = HashMap::new();
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
let mut vec = vec![];
|
for maybe_cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
|
||||||
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
|
let cert = maybe_cert.map_err(Error::Sequoia)?;
|
||||||
vec.push(cert.map_err(Error::Sequoia)?);
|
let certfp = cert.fingerprint();
|
||||||
|
for key in cert.keys() {
|
||||||
|
let fp = key.fingerprint();
|
||||||
|
if pubkeys.contains(&fp) {
|
||||||
|
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
|
||||||
|
}
|
||||||
|
pubkeys.insert(fp);
|
||||||
|
}
|
||||||
|
certs.insert(certfp, cert);
|
||||||
}
|
}
|
||||||
Ok(vec)
|
|
||||||
} else {
|
} else {
|
||||||
let mut vec = vec![];
|
|
||||||
for entry in path
|
for entry in path
|
||||||
.read_dir()
|
.read_dir()
|
||||||
.map_err(Error::Io)?
|
.map_err(Error::Io)?
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.filter(|p| p.path().is_file())
|
.filter(|p| p.path().is_file())
|
||||||
{
|
{
|
||||||
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
|
let cert = Cert::from_file(entry.path()).map_err(Error::Sequoia)?;
|
||||||
|
let certfp = cert.fingerprint();
|
||||||
|
for key in cert.keys() {
|
||||||
|
let fp = key.fingerprint();
|
||||||
|
if pubkeys.contains(&fp) {
|
||||||
|
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
|
||||||
|
}
|
||||||
|
pubkeys.insert(fp);
|
||||||
|
}
|
||||||
|
certs.insert(certfp, cert);
|
||||||
}
|
}
|
||||||
Ok(vec)
|
|
||||||
}
|
}
|
||||||
|
Ok(certs.into_values().collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,13 +84,23 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
|
||||||
aead_algo,
|
aead_algo,
|
||||||
} => {}
|
} => {}
|
||||||
MessageLayer::SignatureGroup { results } => {
|
MessageLayer::SignatureGroup { results } => {
|
||||||
for result in results {
|
match &results[..] {
|
||||||
if let Err(e) = result {
|
[Ok(_)] => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
||||||
// return Err(e.context("Invalid signature"));
|
// return Err(e.context("Invalid signature"));
|
||||||
|
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
for result in results {
|
||||||
|
if let Err(e) = result {
|
||||||
return Err(anyhow::anyhow!("Invalid signature: {e}"));
|
return Err(anyhow::anyhow!("Invalid signature: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,12 +193,23 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
|
||||||
aead_algo,
|
aead_algo,
|
||||||
} => {}
|
} => {}
|
||||||
MessageLayer::SignatureGroup { results } => {
|
MessageLayer::SignatureGroup { results } => {
|
||||||
for result in results {
|
match &results[..] {
|
||||||
if let Err(e) = result {
|
[Ok(_)] => {
|
||||||
// FIXME: anyhow leak
|
return Ok(());
|
||||||
return Err(anyhow::anyhow!("Verification error: {}", e.to_string()));
|
}
|
||||||
|
_ => {
|
||||||
|
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
||||||
|
// return Err(e.context("Invalid signature"));
|
||||||
|
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
for result in results {
|
||||||
|
if let Err(e) = result {
|
||||||
|
return Err(anyhow::anyhow!("Invalid signature: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -264,11 +275,11 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
|
||||||
} 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 =
|
let temp_pin = self
|
||||||
self.pm
|
.pm
|
||||||
.lock()
|
.lock()
|
||||||
.expect(bug!(POISONED_MUTEX))
|
.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 {
|
||||||
#[allow(clippy::ignored_unit_patterns)]
|
#[allow(clippy::ignored_unit_patterns)]
|
||||||
|
|
|
@ -32,7 +32,7 @@ keyfork-entropy = { version = "0.1.0", path = "../util/keyfork-entropy", registr
|
||||||
keyfork-mnemonic-util = { version = "0.2.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
|
keyfork-mnemonic-util = { version = "0.2.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
|
||||||
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", registry = "distrust" }
|
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", registry = "distrust" }
|
||||||
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false, registry = "distrust" }
|
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false, registry = "distrust" }
|
||||||
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"], registry = "distrust" }
|
keyfork-shard = { version = "0.2.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"], registry = "distrust" }
|
||||||
smex = { version = "0.1.0", path = "../util/smex", 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"] }
|
||||||
|
|
|
@ -20,8 +20,12 @@ pub enum DeriveSubcommands {
|
||||||
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
|
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
|
||||||
/// ASCII Armor, a format usable by most programs using OpenPGP.
|
/// ASCII Armor, a format usable by most programs using OpenPGP.
|
||||||
///
|
///
|
||||||
/// The key is generated with a 24-hour expiration time. The operation to set the expiration
|
/// Certificates are created with a default expiration of one day, but may be configured to
|
||||||
/// time to a higher value is left to the user to ensure the key is usable by the user.
|
/// expire later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as
|
||||||
|
/// "15d" (15 days), "1m" (one month), or "2y" (two years).
|
||||||
|
///
|
||||||
|
/// It is recommended to use the default expiration of one day and to change the expiration
|
||||||
|
/// using an external utility, to ensure the Certify key is usable.
|
||||||
#[command(name = "openpgp")]
|
#[command(name = "openpgp")]
|
||||||
OpenPGP {
|
OpenPGP {
|
||||||
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
||||||
|
|
|
@ -11,7 +11,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::{SecurePinValidator, Validator},
|
||||||
Message, PromptHandler, DefaultTerminal, default_terminal
|
Message, PromptHandler, DefaultTerminal, default_terminal
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
|
||||||
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||||
let mut shrd_u32 = [0u8; 4];
|
let mut shrd_u32 = [0u8; 4];
|
||||||
shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::<Vec<u8>>());
|
shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::<Vec<u8>>());
|
||||||
let account = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
let account = DerivationIndex::new(u32::from_be_bytes(shrd_u32), true)?;
|
||||||
let subkey = DerivationIndex::new(u32::from(index), true)?;
|
let subkey = DerivationIndex::new(u32::from(index), true)?;
|
||||||
let path = DerivationPath::default()
|
let path = DerivationPath::default()
|
||||||
.chain_push(chain)
|
.chain_push(chain)
|
||||||
|
@ -116,12 +116,12 @@ fn generate_shard_secret(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_pin_validator = PinValidator {
|
let user_pin_validator = SecurePinValidator {
|
||||||
min_length: Some(6),
|
min_length: Some(6),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.to_fn();
|
.to_fn();
|
||||||
let admin_pin_validator = PinValidator {
|
let admin_pin_validator = SecurePinValidator {
|
||||||
min_length: Some(8),
|
min_length: Some(8),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -132,8 +132,8 @@ fn generate_shard_secret(
|
||||||
for i in 0..keys_per_shard {
|
for i in 0..keys_per_shard {
|
||||||
pm.prompt_message(Message::Text(format!(
|
pm.prompt_message(Message::Text(format!(
|
||||||
"Please remove all keys and insert key #{} for user #{}",
|
"Please remove all keys and insert key #{} for user #{}",
|
||||||
i + 1,
|
(i as u16) + 1,
|
||||||
index + 1,
|
(index as u16) + 1,
|
||||||
)))?;
|
)))?;
|
||||||
let card_backend = loop {
|
let card_backend = loop {
|
||||||
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
|
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-qrcode"
|
name = "keyfork-qrcode"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
repository = "https://git.distrust.co/public/keyfork"
|
repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
@ -5,7 +5,7 @@ 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},
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, Instant},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
};
|
};
|
||||||
use v4l::{
|
use v4l::{
|
||||||
|
@ -110,11 +110,10 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
|
||||||
fmt.fourcc = FourCC::new(b"MPG1");
|
fmt.fourcc = FourCC::new(b"MPG1");
|
||||||
device.set_format(&fmt)?;
|
device.set_format(&fmt)?;
|
||||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||||
let start = SystemTime::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
while SystemTime::now()
|
while Instant::now()
|
||||||
.duration_since(start)
|
.duration_since(start)
|
||||||
.unwrap_or(Duration::from_secs(0))
|
|
||||||
< timeout
|
< timeout
|
||||||
{
|
{
|
||||||
let (buffer, _) = stream.next()?;
|
let (buffer, _) = stream.next()?;
|
||||||
|
@ -141,12 +140,11 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
|
||||||
fmt.fourcc = FourCC::new(b"MPG1");
|
fmt.fourcc = FourCC::new(b"MPG1");
|
||||||
device.set_format(&fmt)?;
|
device.set_format(&fmt)?;
|
||||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||||
let start = SystemTime::now();
|
let start = Instant::now();
|
||||||
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
|
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
|
||||||
|
|
||||||
while SystemTime::now()
|
while Instant::now()
|
||||||
.duration_since(start)
|
.duration_since(start)
|
||||||
.unwrap_or(Duration::from_secs(0))
|
|
||||||
< timeout
|
< timeout
|
||||||
{
|
{
|
||||||
let (buffer, _) = stream.next()?;
|
let (buffer, _) = stream.next()?;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-entropy"
|
name = "keyfork-entropy"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
bit_size % 8 == 0,
|
bit_size % 8 == 0,
|
||||||
"Bit size must be divisible by 8, got: {bit_size}"
|
"Bit size must be divisible by 8, got: {bit_size}"
|
||||||
);
|
);
|
||||||
assert!(
|
|
||||||
bit_size <= 256,
|
|
||||||
"Maximum supported bit size is 256, got: {bit_size}"
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
bit_size >= 128,
|
bit_size >= 128,
|
||||||
"Minimum supported bit size is 128, got {bit_size}"
|
"Minimum supported bit size is 128, got {bit_size}"
|
||||||
);
|
);
|
||||||
|
match bit_size {
|
||||||
|
128 | 256 | 512 => {}
|
||||||
|
_ => {
|
||||||
|
eprintln!("reading entropy of uncommon size: {bit_size}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
|
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
|
||||||
println!("{}", smex::encode(entropy));
|
println!("{}", smex::encode(entropy));
|
||||||
|
|
|
@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
input.read_line(&mut line)?;
|
input.read_line(&mut line)?;
|
||||||
let decoded = smex::decode(line.trim())?;
|
let decoded = smex::decode(line.trim())?;
|
||||||
|
|
||||||
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&decoded) };
|
let mnemonic = Mnemonic::from_raw_bytes(&decoded) ;
|
||||||
|
|
||||||
println!("{mnemonic}");
|
println!("{mnemonic}");
|
||||||
|
|
||||||
|
|
|
@ -125,6 +125,13 @@ impl Wordlist for English {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AssertValidMnemonicSize<const N: usize>;
|
||||||
|
|
||||||
|
impl<const N: usize> AssertValidMnemonicSize<N> {
|
||||||
|
const OK_CHUNKS: () = assert!(N % 4 == 0, "bytes must be a length divisible by 4");
|
||||||
|
const OK_SIZE: () = assert!(N <= 1024, "bytes must be less-or-equal 1024");
|
||||||
|
}
|
||||||
|
|
||||||
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
|
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct MnemonicBase<W: Wordlist> {
|
pub struct MnemonicBase<W: Wordlist> {
|
||||||
|
@ -276,7 +283,36 @@ where
|
||||||
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
|
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(unsafe { Self::from_raw_bytes(bytes) })
|
Ok( Self::from_raw_bytes(bytes) )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data may be of a size
|
||||||
|
/// of a factor of 4, up to 1024 bytes.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// let data = b"hello world!";
|
||||||
|
/// let mnemonic = Mnemonic::from_nonstandard_bytes(*data);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If an invalid size is requested, the code will fail to compile:
|
||||||
|
///
|
||||||
|
/// ```rust,compile_fail
|
||||||
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// let mnemonic = Mnemonic::from_nonstandard_bytes([0u8; 53]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```rust,compile_fail
|
||||||
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// let mnemonic = Mnemonic::from_nonstandard_bytes([0u8; 1024 + 4]);
|
||||||
|
/// ```
|
||||||
|
pub fn from_nonstandard_bytes<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
|
||||||
|
#[allow(clippy::let_unit_value)]
|
||||||
|
{
|
||||||
|
let () = AssertValidMnemonicSize::<N>::OK_CHUNKS;
|
||||||
|
let () = AssertValidMnemonicSize::<N>::OK_SIZE;
|
||||||
|
}
|
||||||
|
Self::from_raw_bytes(&bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
|
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
|
||||||
|
@ -292,11 +328,12 @@ where
|
||||||
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
|
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
|
||||||
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
|
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Panics
|
||||||
///
|
|
||||||
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
||||||
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
||||||
/// == 0`. If the assumption is incorrect, code may panic.
|
/// == 0`. If the assumption is incorrect, code may panic. The
|
||||||
|
/// [`MnemonicBase::from_nonstandard_bytes`] function may be used to generate entropy if the
|
||||||
|
/// length of the data is known at compile-time.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```rust
|
/// ```rust
|
||||||
|
@ -315,11 +352,10 @@ where
|
||||||
/// // NOTE: Data is of invalid length, 31
|
/// // NOTE: Data is of invalid length, 31
|
||||||
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||||
/// let mnemonic_text = mnemonic.to_string();
|
|
||||||
/// // NOTE: panic happens here
|
|
||||||
/// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
|
|
||||||
/// ```
|
/// ```
|
||||||
pub unsafe fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
|
pub fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
|
||||||
|
assert!(bytes.len() % 4 == 0);
|
||||||
|
assert!(bytes.len() <= 1024);
|
||||||
MnemonicBase {
|
MnemonicBase {
|
||||||
data: bytes.to_vec(),
|
data: bytes.to_vec(),
|
||||||
marker: PhantomData,
|
marker: PhantomData,
|
||||||
|
@ -520,12 +556,30 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_do_up_to_1024_bits() {
|
fn can_do_up_to_8192_bits() {
|
||||||
let entropy = &mut [0u8; 128];
|
let mut entropy = [0u8; 1024];
|
||||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||||
random.read_exact(&mut entropy[..]).unwrap();
|
random.read_exact(&mut entropy[..]).unwrap();
|
||||||
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&entropy[..]) };
|
let mnemonic = Mnemonic::from_nonstandard_bytes(entropy);
|
||||||
let words = mnemonic.words();
|
let words = mnemonic.words();
|
||||||
assert!(words.len() == 96);
|
assert_eq!(words.len(), 768);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn fails_over_8192_bits() {
|
||||||
|
let entropy = &mut [0u8; 1024 + 4];
|
||||||
|
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||||
|
random.read_exact(&mut entropy[..]).unwrap();
|
||||||
|
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn fails_over_invalid_size() {
|
||||||
|
let entropy = &mut [0u8; 255];
|
||||||
|
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||||
|
random.read_exact(&mut entropy[..]).unwrap();
|
||||||
|
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,84 @@ pub enum PinError {
|
||||||
/// The PIN contained invalid characters.
|
/// The PIN contained invalid characters.
|
||||||
#[error("PIN contained invalid characters (found {0} at position {1})")]
|
#[error("PIN contained invalid characters (found {0} at position {1})")]
|
||||||
InvalidCharacters(char, usize),
|
InvalidCharacters(char, usize),
|
||||||
|
|
||||||
|
/// The provided PIN had either too many repeated characters or too many sequential characters.
|
||||||
|
#[error("PIN contained too many repeated or sequential characters")]
|
||||||
|
InsecurePIN,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate that a PIN is of a certain length, matches a range of characters, and does not use
|
||||||
|
/// incrementing or decrementing sequences of characters.
|
||||||
|
///
|
||||||
|
/// The validator determines a score for a passphrase and, if the score is high enough, returns an
|
||||||
|
/// error.
|
||||||
|
///
|
||||||
|
/// Score is calculated based on:
|
||||||
|
/// * how many sequential characters are in the passphrase (ascending or descending)
|
||||||
|
/// * how many repeated characters are in the passphrase
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct SecurePinValidator {
|
||||||
|
/// The minimum length of provided PINs.
|
||||||
|
pub min_length: Option<usize>,
|
||||||
|
|
||||||
|
/// The maximum length of provided PINs.
|
||||||
|
pub max_length: Option<usize>,
|
||||||
|
|
||||||
|
/// The characters allowed by the PIN parser.
|
||||||
|
pub range: Option<RangeInclusive<char>>,
|
||||||
|
|
||||||
|
/// Whether repeated characters count against the PIN.
|
||||||
|
pub ignore_repeated_characters: bool,
|
||||||
|
|
||||||
|
/// Whether sequential characters count against the PIN.
|
||||||
|
pub ignore_sequential_characters: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validator for SecurePinValidator {
|
||||||
|
type Output = String;
|
||||||
|
type Error = PinError;
|
||||||
|
|
||||||
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
|
||||||
|
let min_len = self.min_length.unwrap_or(usize::MIN);
|
||||||
|
let max_len = self.max_length.unwrap_or(usize::MAX);
|
||||||
|
let range = self.range.clone().unwrap_or('0'..='9');
|
||||||
|
let ignore_repeated_characters = self.ignore_repeated_characters;
|
||||||
|
let ignore_sequential_characters = self.ignore_sequential_characters;
|
||||||
|
Box::new(move |mut s: String| {
|
||||||
|
s.truncate(s.trim_end().len());
|
||||||
|
let len = s.len();
|
||||||
|
if len < min_len {
|
||||||
|
return Err(Box::new(PinError::TooShort(len, min_len)));
|
||||||
|
}
|
||||||
|
if len > max_len {
|
||||||
|
return Err(Box::new(PinError::TooLong(len, max_len)));
|
||||||
|
}
|
||||||
|
let mut last_char = 0;
|
||||||
|
let mut score = 0;
|
||||||
|
for (index, ch) in s.chars().enumerate() {
|
||||||
|
if !range.contains(&ch) {
|
||||||
|
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
|
||||||
|
}
|
||||||
|
if [-1, 1].contains(&(ch as i32 - last_char))
|
||||||
|
&& !ignore_sequential_characters
|
||||||
|
{
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
last_char = ch as i32;
|
||||||
|
}
|
||||||
|
let mut chars = s.chars().collect::<Vec<_>>();
|
||||||
|
chars.sort();
|
||||||
|
chars.dedup();
|
||||||
|
if !ignore_repeated_characters {
|
||||||
|
// SAFETY: the amount of characters can't have _increased_ since deduping
|
||||||
|
score += s.chars().count() - chars.len();
|
||||||
|
}
|
||||||
|
if score * 2 > s.chars().count() {
|
||||||
|
return Err(Box::new(PinError::InsecurePIN))
|
||||||
|
}
|
||||||
|
Ok(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate that a PIN is of a certain length and matches a range of characters.
|
/// Validate that a PIN is of a certain length and matches a range of characters.
|
||||||
|
@ -79,8 +157,8 @@ pub mod mnemonic {
|
||||||
|
|
||||||
use super::Validator;
|
use super::Validator;
|
||||||
|
|
||||||
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
|
|
||||||
use keyfork_bug::bug;
|
use keyfork_bug::bug;
|
||||||
|
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
|
||||||
|
|
||||||
/// 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)]
|
||||||
|
|
|
@ -33,3 +33,4 @@
|
||||||
- [Provisioners](./dev-guide/provisioners.md)
|
- [Provisioners](./dev-guide/provisioners.md)
|
||||||
- [Auditing Dependencies](./dev-guide/auditing.md)
|
- [Auditing Dependencies](./dev-guide/auditing.md)
|
||||||
- [Entropy Guide](./dev-guide/entropy.md)
|
- [Entropy Guide](./dev-guide/entropy.md)
|
||||||
|
- [The Shard Protocol](./dev-guide/shard-protocol.md)
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# The Shard Protocol
|
||||||
|
|
||||||
|
Keyfork Shard uses a single-handshake protocol to transfer encrypted shards.
|
||||||
|
The initial payload is generated by the program combining the shards, while the
|
||||||
|
response is generated by the program transport-encrypting the shards.
|
||||||
|
|
||||||
|
## Combiner Payload
|
||||||
|
|
||||||
|
The combiner payload consists of a 12-byte nonce and a 32-byte x25519 public
|
||||||
|
key. The payload is then either encoded to hex and displayed as a QR code, and
|
||||||
|
encoded as a mnemonic and printed on-screen.
|
||||||
|
|
||||||
|
```
|
||||||
|
[12-byte nonce | 32-byte public key]
|
||||||
|
```
|
||||||
|
|
||||||
|
The transporter receives the 12-byte nonce and 32-byte x25519 key and generates
|
||||||
|
their own x25519 key. Using HKDF-Sha256 with no salt on the resulting key
|
||||||
|
generates the AES-256-GCM key used to encrypt the now-decrypted shard, along
|
||||||
|
with the received nonce.
|
||||||
|
|
||||||
|
## Transporter Payload
|
||||||
|
|
||||||
|
The transporter payload consists of a 32-byte x25519 public key and a
|
||||||
|
64-byte-padded encrypted "hunk". The hunk contains a version byte, a threshold
|
||||||
|
byte, and the encrypted shard. The last byte of the 64-byte sequence is the
|
||||||
|
total length of the encrypted hunk.
|
||||||
|
|
||||||
|
```
|
||||||
|
Handshake:
|
||||||
|
[32-byte public key | 63-byte-padded encrypted hunk | 1-byte hunk length ]
|
||||||
|
|
||||||
|
Hunk:
|
||||||
|
[1-byte version | 1-byte threshold | variable-length shard ]
|
||||||
|
```
|
||||||
|
|
||||||
|
The combiner receives the 32-byte x25519 key and the 64-byte hunk, and uses the
|
||||||
|
same key derivation scheme as above to generate the decryption key. The
|
||||||
|
threshold byte is used to determine how many shares (in total) are needed.
|
Loading…
Reference in New Issue