Compare commits

..

1 Commits

Author SHA1 Message Date
Anton Livaja 81ca435de1
feat: require at least 128 bits of entropy 2024-04-29 13:39:21 -04:00
31 changed files with 231 additions and 516 deletions

41
Cargo.lock generated
View File

@ -341,12 +341,6 @@ 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"
@ -1503,9 +1497,9 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.60" version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
@ -1578,14 +1572,15 @@ dependencies = [
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.38.0" version = "1.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc" checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc"
dependencies = [ dependencies = [
"console", "console",
"lazy_static", "lazy_static",
"linked-hash-map", "linked-hash-map",
"similar", "similar",
"yaml-rust",
] ]
[[package]] [[package]]
@ -1763,7 +1758,7 @@ dependencies = [
[[package]] [[package]]
name = "keyfork-derive-util" name = "keyfork-derive-util"
version = "0.1.1" version = "0.1.0"
dependencies = [ dependencies = [
"digest", "digest",
"ed25519-dalek", "ed25519-dalek",
@ -1781,7 +1776,7 @@ dependencies = [
[[package]] [[package]]
name = "keyfork-entropy" name = "keyfork-entropy"
version = "0.1.1" version = "0.1.0"
dependencies = [ dependencies = [
"keyfork-bug", "keyfork-bug",
"smex", "smex",
@ -1823,7 +1818,7 @@ dependencies = [
[[package]] [[package]]
name = "keyfork-qrcode" name = "keyfork-qrcode"
version = "0.1.1" version = "0.1.0"
dependencies = [ dependencies = [
"image", "image",
"keyfork-bug", "keyfork-bug",
@ -1835,11 +1830,10 @@ dependencies = [
[[package]] [[package]]
name = "keyfork-shard" name = "keyfork-shard"
version = "0.2.0" version = "0.1.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",
@ -1885,7 +1879,7 @@ dependencies = [
[[package]] [[package]]
name = "keyforkd" name = "keyforkd"
version = "0.1.1" version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"hex-literal", "hex-literal",
@ -2109,9 +2103,9 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@ -2892,7 +2886,7 @@ dependencies = [
"aes", "aes",
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
"base64 0.21.7", "base64",
"block-padding", "block-padding",
"blowfish", "blowfish",
"buffered-reader", "buffered-reader",
@ -3817,6 +3811,15 @@ 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"

View File

@ -234,7 +234,7 @@ impl Client {
} }
let depth = path.len() as u8; let depth = path.len() as u8;
Ok(ExtendedPrivateKey::from_parts( Ok(ExtendedPrivateKey::new_from_parts(
&d.data, &d.data,
depth, depth,
d.chain_code, d.chain_code,

View File

@ -25,9 +25,6 @@ fn secp256k1_test_suite() {
if chain_len < 2 { if chain_len < 2 {
continue; continue;
} }
if chain.iter().take(2).any(|index| !index.is_hardened()) {
continue;
}
// Consistency check: ensure the server and the client can each derive the same // Consistency check: ensure the server and the client can each derive the same
// key using an XPrv, for all but the last XPrv, which is verified after this // key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len { for i in 2..chain_len {

View File

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

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyforkd" name = "keyforkd"
version = "0.1.1" version = "0.1.0"
edition = "2021" edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"

View File

@ -69,18 +69,6 @@ impl Service<Request> for Keyforkd {
return Err(DerivationError::InvalidDerivationLength(len).into()); return Err(DerivationError::InvalidDerivationLength(len).into());
} }
if let Some((i, unhardened_index)) = req
.path()
.iter()
.take(2)
.enumerate()
.find(|(_, index)| {
!index.is_hardened()
})
{
return Err(DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into())
}
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
if let Some(target) = guess_target(req.path()) { if let Some(target) = guess_target(req.path()) {
info!("Deriving path: {target}"); info!("Deriving path: {target}");
@ -122,9 +110,6 @@ mod tests {
if chain.len() < 2 { if chain.len() < 2 {
continue; continue;
} }
if chain.iter().take(2).any(|index| !index.is_hardened()) {
continue;
}
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain); let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response: DerivationResponse = keyforkd let response: DerivationResponse = keyforkd
.ready() .ready()

View File

@ -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");
let result = rt.block_on(async move { rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(1); let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let server_handle = tokio::spawn({ let server_handle = tokio::spawn({
let socket_path = socket_path.clone(); let socket_path = socket_path.clone();
@ -87,13 +87,8 @@ where
let result = test_handle.await; let result = test_handle.await;
server_handle.abort(); server_handle.abort();
result result
}); })
if let Err(e) = result { .expect(bug!("runtime could not join all threads"))
if e.is_panic() {
std::panic::resume_unwind(e.into_panic());
}
}
Ok(())
} }
#[cfg(test)] #[cfg(test)]

View File

@ -59,17 +59,13 @@ pub enum Error {
#[allow(missing_docs)] #[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Create an OpenPGP Cert with private key data, with derived keys from the given derivation /// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
/// response, keys, and User ID. /// ID.
///
/// Certificates are created with a default expiration of one day, but may be configured to expire
/// later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as "15d" (15
/// days), "1m" (one month), or "2y" (two years).
/// ///
/// # Errors /// # Errors
/// The function may error for any condition mentioned in [`Error`]. /// The function may error for any condition mentioned in [`Error`].
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> { pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.first() { let primary_key_flags = match keys.get(0) {
Some(kf) if kf.for_certification() => kf, Some(kf) if kf.for_certification() => kf,
_ => return Err(Error::NotCert), _ => return Err(Error::NotCert),
}; };
@ -113,7 +109,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?; let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
let policy = sequoia_openpgp::policy::StandardPolicy::new(); let policy = sequoia_openpgp::policy::StandardPolicy::new();
// Set certificate expiration to configured expiration or (default) one day // Set certificate expiration to one day
let mut keypair = primary_key.clone().into_keypair()?; let mut keypair = primary_key.clone().into_keypair()?;
let signatures = let signatures =
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?; cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork-derive-util" name = "keyfork-derive-util"
version = "0.1.1" version = "0.1.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View File

@ -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.try_into().expect(bug!( let bytes: [u8; 32] = variable_len_bytes
"unable to parse serialized private key; no support for static len" .try_into()
)); .expect(bug!("unable to parse serialized private key; no support for static len"));
Ok(K::from_bytes(&bytes)) Ok(K::from_bytes(&bytes))
} }
} }
@ -179,20 +179,13 @@ where
.into_bytes(); .into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8); let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
assert!( Self::new_from_parts(
!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 chain_code.try_into().expect(bug!("Invalid chain code length")),
.try_into()
.expect(bug!("Invalid chain code length")),
) )
} }
@ -212,9 +205,9 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code); /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// ``` /// ```
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self { pub fn new_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,
@ -236,7 +229,7 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code); /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// 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 {
@ -269,7 +262,7 @@ where
/// # } /// # }
/// ``` /// ```
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> { pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
ExtendedPublicKey::from_parts(self.public_key(), self.depth, self.chain_code) ExtendedPublicKey::new_from_parts(self.public_key(), self.depth, self.chain_code)
} }
/// Return a public key for the current [`PrivateKey`]. /// Return a public key for the current [`PrivateKey`].
@ -308,7 +301,7 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code); /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.depth(), 4); /// assert_eq!(xprv.depth(), 4);
/// ``` /// ```
pub fn depth(&self) -> u8 { pub fn depth(&self) -> u8 {
@ -328,7 +321,7 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code); /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// 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] {

View File

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

View File

@ -85,7 +85,7 @@ pub trait PrivateKey: Sized {
/// # Errors /// # Errors
/// ///
/// An error may be returned if: /// An error may be returned if:
/// * An all-zero `other` is provided. /// * A nonzero `other` is provided.
/// * An error specific to the given algorithm was encountered. /// * An error specific to the given algorithm was encountered.
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>; fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
@ -102,10 +102,6 @@ pub enum PrivateKeyError {
/// For the given algorithm, the private key must be nonzero. /// For the given algorithm, the private key must be nonzero.
#[error("The provided private key must be nonzero, but is not")] #[error("The provided private key must be nonzero, but is not")]
NonZero, NonZero,
/// A scalar could not be constructed for the given algorithm.
#[error("A scalar could not be constructed for the given algorithm")]
InvalidScalar,
} }
#[cfg(feature = "secp256k1")] #[cfg(feature = "secp256k1")]
@ -134,19 +130,20 @@ impl PrivateKey for k256::SecretKey {
} }
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> { fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
use k256::elliptic_curve::ScalarPrimitive; if other.iter().all(|n| n == &0) {
use k256::{Scalar, Secp256k1}; return Err(PrivateKeyError::NonZero);
}
// Construct a scalar from bytes let other = *other;
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(other.into()); // Checked: See above nonzero check
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar); let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
let scalar = scalar.ok_or(PrivateKeyError::InvalidScalar)?; .expect(bug!("Should have been able to get a NonZeroScalar"));
let scalar = Scalar::from(scalar);
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref(); let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
let nonzero_scalar = Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar)) Ok(
.ok_or(PrivateKeyError::NonZero)?; Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
Ok(Self::from(nonzero_scalar)) .map(Into::into)
.expect(bug!("Should be able to make Key")),
)
} }
} }
@ -183,8 +180,7 @@ impl PrivateKey for ed25519_dalek::SigningKey {
use crate::public_key::TestPublicKey; use crate::public_key::TestPublicKey;
/// A private key that can be used for testing purposes. Does not utilize any significant #[doc(hidden)]
/// cryptographic operations.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct TestPrivateKey { pub struct TestPrivateKey {
key: [u8; 32], key: [u8; 32],
@ -205,7 +201,9 @@ impl PrivateKey for TestPrivateKey {
type Err = PrivateKeyError; type Err = PrivateKeyError;
fn from_bytes(b: &PrivateKeyBytes) -> Self { fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self { key: *b } Self {
key: *b
}
} }
fn to_bytes(&self) -> PrivateKeyBytes { fn to_bytes(&self) -> PrivateKeyBytes {

View File

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

View File

@ -57,7 +57,7 @@ pub enum DerivationAlgorithm {
#[allow(missing_docs)] #[allow(missing_docs)]
Secp256k1, Secp256k1,
#[doc(hidden)] #[doc(hidden)]
TestAlgorithm, Internal,
} }
impl DerivationAlgorithm { impl DerivationAlgorithm {
@ -86,7 +86,7 @@ impl DerivationAlgorithm {
&derived_key, &derived_key,
)) ))
} }
Self::TestAlgorithm => { Self::Internal => {
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed); let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed);
let derived_key = key.derive_path(path)?; let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv( Ok(DerivationResponse::with_algo_and_xprv(
@ -120,7 +120,7 @@ pub trait AsAlgorithm: PrivateKey {
impl AsAlgorithm for TestPrivateKey { impl AsAlgorithm for TestPrivateKey {
fn as_algorithm() -> DerivationAlgorithm { fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::TestAlgorithm DerivationAlgorithm::Internal
} }
} }
@ -144,7 +144,7 @@ impl DerivationRequest {
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm; /// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
@ -169,7 +169,7 @@ impl DerivationRequest {
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm; /// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
@ -199,7 +199,7 @@ impl DerivationRequest {
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
/// # )?; /// # )?;
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm; /// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
@ -228,7 +228,7 @@ impl DerivationRequest {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let algo: DerivationAlgorithm = // /// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::TestAlgorithm; /// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
@ -300,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::from_parts( DerivationAlgorithm::Secp256k1 => Ok(Self::new_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::from_parts( DerivationAlgorithm::Ed25519 => Ok(Self::new_from_parts(
&value.data, &value.data,
value.depth, value.depth,
value.chain_code, value.chain_code,

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork-shard" name = "keyfork-shard"
version = "0.2.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
@ -37,4 +37,3 @@ 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"

View File

@ -7,28 +7,22 @@ use std::{
}; };
use aes_gcm::{ use aes_gcm::{
aead::{consts::U12, Aead}, aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce, Aes256Gcm, KeyInit, Nonce,
}; };
use base64::prelude::{Engine, BASE64_STANDARD};
use 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::{ validators::{mnemonic::MnemonicSetValidator, Validator},
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};
// 32-byte share, 1-byte index, 1-byte threshold, 1-byte version == 36 bytes // 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
// Encrypted, is 52 bytes const ENC_LEN: u8 = 4 * 16;
const PLAINTEXT_LENGTH: u8 = 36;
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
#[cfg(feature = "openpgp")] #[cfg(feature = "openpgp")]
pub mod openpgp; pub mod openpgp;
@ -200,6 +194,7 @@ pub trait Format {
let encrypted_messages = self.parse_shard_file(reader)?; let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH // establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None; let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera // receive remote data via scanning QR code from camera
@ -209,13 +204,12 @@ 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(qrcode_content)) = 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)
{ {
let decoded_data = BASE64_STANDARD let decoded_data = smex::decode(&hex)?;
.decode(qrcode_content) nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
.expect(bug!("qrcode should contain base64 encoded data")); pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
} else { } else {
prompt prompt
.lock() .lock()
@ -225,43 +219,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 their_pubkey = match pubkey_data { let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
Some(pubkey) => pubkey, (Some(nonce), Some(pubkey)) => (nonce, pubkey),
None => { _ => {
let validator = MnemonicValidator { let validator = MnemonicSetValidator {
word_length: Some(WordLength::Count(24)), word_lengths: [9, 24],
}; };
prompt let [nonce_mnemonic, pubkey_mnemonic] = 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.diffie_hellman(&PublicKey::from(their_pubkey)); let shared_secret = our_key
assert!( .diffie_hellman(&PublicKey::from(their_pubkey))
shared_secret.was_contributory(), .to_bytes();
bug!("shared secret might be insecure") let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
); let mut hkdf_output = [0u8; 256 / 8];
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes()); hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
// decrypt a single shard and create the payload // decrypt a single shard and create the payload
let (share, threshold) = let (share, threshold) =
@ -270,47 +264,49 @@ pub trait Format {
payload.insert(0, HUNK_VERSION); payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold); payload.insert(1, threshold);
assert!( assert!(
payload.len() < PLAINTEXT_LENGTH as usize, payload.len() <= ENC_LEN as usize,
"invalid share length (too long, max {PLAINTEXT_LENGTH} bytes)" "invalid share length (too long, max {ENC_LEN} bytes)"
); );
// convert plaintext to static-size payload
#[allow(clippy::assertions_on_constants)]
{
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
}
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
// duplicate mnemonic words. This version does not include that, and instead uses a
// repeated length byte.
#[allow(clippy::cast_possible_truncation)]
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
"previously asserted length must be < {PLAINTEXT_LENGTH}",
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
)); PLAINTEXT_LENGTH as usize];
plaintext_bytes[..payload.len()].clone_from_slice(&payload);
// encrypt data // encrypt data
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?; let nonce = Nonce::<U12>::from_slice(&nonce);
assert_eq!( let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
encrypted_bytes.len(),
ENCRYPTED_LENGTH as usize,
bug!("encrypted bytes size != expected len"),
);
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
let payload_mnemonic = Mnemonic::from_nonstandard_bytes(mnemonic_bytes); // convert data to a static-size payload
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
#[allow(clippy::assertions_on_constants)]
{
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
}
#[allow(clippy::cast_possible_truncation)]
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
assert!(
payload_bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit"
);
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
// difficulty when entering in prompts manually, as one's place could be lost due to
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
// but not including the last byte.
#[allow(clippy::cast_possible_truncation)]
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
.iter_mut()
.enumerate()
{
*byte = (i % u8::MAX as usize) as u8;
}
// safety: size of out_bytes is constant and always % 4 == 0
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
use keyfork_qrcode::{qrencode, ErrorCorrection}; use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes(); let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes()); qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode( if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
prompt prompt
.lock() .lock()
.expect(bug!(POISONED_MUTEX)) .expect(bug!(POISONED_MUTEX))
@ -405,7 +401,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 = 2; pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_OFFSET: usize = 2; pub(crate) const HUNK_OFFSET: usize = 2;
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera."; const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
@ -436,22 +432,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 qrcode_data = key_mnemonic.to_bytes(); let mut qrcode_data = nonce_mnemonic.to_bytes();
if let Ok(qrcode) = qrencode( qrcode_data.extend(key_mnemonic.as_bytes());
&BASE64_STANDARD.encode(qrcode_data), if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
ErrorCorrection::Highest,
) {
pm.prompt_message(PromptMessage::Text(format!( pm.prompt_message(PromptMessage::Text(format!(
concat!( concat!(
"QR code #{iter} will be displayed after this prompt. ", "A QR code will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ", "Send the QR code to only shardholder {iter}. ",
"Only the next shardholder should scan the QR code." "Nobody else should scan this QR code."
), ),
iter = iter iter = iter
)))?; )))?;
@ -461,9 +457,11 @@ 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 the shardholder: ", "Upon request, these words should be sent to shardholder {iter}: ",
"{key_mnemonic}" "{nonce_mnemonic} {key_mnemonic}"
), ),
iter = iter,
nonce_mnemonic = nonce_mnemonic,
key_mnemonic = key_mnemonic, key_mnemonic = key_mnemonic,
)))?; )))?;
@ -473,17 +471,10 @@ 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(qrcode_content)) = if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0) keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
{ {
let decoded_data = BASE64_STANDARD let decoded_data = smex::decode(&hex)?;
.decode(qrcode_content)
.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 {
@ -495,7 +486,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, 39], word_lengths: [24, 48],
}; };
let [pubkey_mnemonic, payload_mnemonic] = pm let [pubkey_mnemonic, payload_mnemonic] = pm
@ -513,28 +504,14 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
} }
}; };
assert_eq!( let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
payload.len(), let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
ENCRYPTED_LENGTH as usize, let mut hkdf_output = [0u8; 256 / 8];
bug!("invalid payload data") hkdf.expand(&[], &mut hkdf_output)?;
); let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)); let payload =
assert!( shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
shared_secret.was_contributory(),
bug!("shared secret might be insecure")
);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version"); assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
match &mut iter_count { match &mut iter_count {
@ -549,8 +526,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
} }
} }
let payload_len = payload.last().expect(bug!("payload should not be empty")); shares.push(payload[HUNK_OFFSET..].to_vec());
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
} }
let shares = shares let shares = shares

View File

@ -194,52 +194,33 @@ 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. /// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// /// from a file, or from files one level deep in a directory.
/// Certificates 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 /// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// unable to load certificates from the file. /// to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> { pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref(); let path = path.as_ref();
let mut pubkeys = std::collections::HashSet::new();
let mut certs = HashMap::new();
if path.is_file() { if path.is_file() {
for maybe_cert in CertParser::from_file(path).map_err(Error::Sequoia)? { let mut vec = vec![];
let cert = maybe_cert.map_err(Error::Sequoia)?; for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
let certfp = cert.fingerprint(); vec.push(cert.map_err(Error::Sequoia)?);
for key in cert.keys() {
let fp = key.fingerprint();
if pubkeys.contains(&fp) {
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
}
pubkeys.insert(fp);
}
certs.insert(certfp, cert);
} }
Ok(vec)
} else { } else {
let mut vec = vec![];
for entry in path for entry in path
.read_dir() .read_dir()
.map_err(Error::Io)? .map_err(Error::Io)?
.filter_map(Result::ok) .filter_map(Result::ok)
.filter(|p| p.path().is_file()) .filter(|p| p.path().is_file())
{ {
let cert = Cert::from_file(entry.path()).map_err(Error::Sequoia)?; vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
let certfp = cert.fingerprint();
for key in cert.keys() {
let fp = key.fingerprint();
if pubkeys.contains(&fp) {
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
} }
pubkeys.insert(fp); Ok(vec)
} }
certs.insert(certfp, cert);
}
}
Ok(certs.into_values().collect())
} }
} }

View File

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

View File

@ -193,23 +193,12 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
aead_algo, aead_algo,
} => {} } => {}
MessageLayer::SignatureGroup { results } => { MessageLayer::SignatureGroup { results } => {
match &results[..] {
[Ok(_)] => {
return Ok(());
}
_ => {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
}
}
/*
for result in results { for result in results {
if let Err(e) = result { if let Err(e) = result {
return Err(anyhow::anyhow!("Invalid signature: {e}")); // FIXME: anyhow leak
return Err(anyhow::anyhow!("Verification error: {}", e.to_string()));
} }
} }
*/
} }
} }
} }
@ -275,8 +264,8 @@ 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 = self let temp_pin =
.pm self.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)?;

View File

@ -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.2.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"], registry = "distrust" } keyfork-shard = { version = "0.1.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"], registry = "distrust" }
smex = { version = "0.1.0", path = "../util/smex", registry = "distrust" } 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"] }

View File

@ -20,12 +20,8 @@ pub enum DeriveSubcommands {
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP /// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
/// ASCII Armor, a format usable by most programs using OpenPGP. /// ASCII Armor, a format usable by most programs using OpenPGP.
/// ///
/// Certificates are created with a default expiration of one day, but may be configured to /// The key is generated with a 24-hour expiration time. The operation to set the expiration
/// expire later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as /// time to a higher value is left to the user to ensure the key is usable by the user.
/// "15d" (15 days), "1m" (one month), or "2y" (two years).
///
/// It is recommended to use the default expiration of one day and to change the expiration
/// using an external utility, to ensure the Certify key is usable.
#[command(name = "openpgp")] #[command(name = "openpgp")]
OpenPGP { OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format. /// Default User ID for the certificate, using the OpenPGP User ID format.

View File

@ -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::{SecurePinValidator, Validator}, validators::{PinValidator, 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(shrd_u32), true)?; let account = DerivationIndex::new(u32::from_be_bytes(pgp_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 = SecurePinValidator { let user_pin_validator = PinValidator {
min_length: Some(6), min_length: Some(6),
..Default::default() ..Default::default()
} }
.to_fn(); .to_fn();
let admin_pin_validator = SecurePinValidator { let admin_pin_validator = PinValidator {
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 as u16) + 1, i + 1,
(index as u16) + 1, index + 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()? {

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork-qrcode" name = "keyfork-qrcode"
version = "0.1.1" version = "0.1.0"
repository = "https://git.distrust.co/public/keyfork" repository = "https://git.distrust.co/public/keyfork"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View File

@ -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, Instant}, time::{Duration, SystemTime},
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use v4l::{ use v4l::{
@ -110,10 +110,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 = Instant::now(); let start = SystemTime::now();
while Instant::now() while SystemTime::now()
.duration_since(start) .duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout < timeout
{ {
let (buffer, _) = stream.next()?; let (buffer, _) = stream.next()?;
@ -140,11 +141,12 @@ 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 = Instant::now(); let start = SystemTime::now();
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new(); let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
while Instant::now() while SystemTime::now()
.duration_since(start) .duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout < timeout
{ {
let (buffer, _) = stream.next()?; let (buffer, _) = stream.next()?;

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork-entropy" name = "keyfork-entropy"
version = "0.1.1" version = "0.1.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View File

@ -10,16 +10,14 @@ 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));

View File

@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
input.read_line(&mut line)?; input.read_line(&mut line)?;
let decoded = smex::decode(line.trim())?; let decoded = smex::decode(line.trim())?;
let mnemonic = Mnemonic::from_raw_bytes(&decoded) ; let mnemonic = unsafe { Mnemonic::from_raw_bytes(&decoded) };
println!("{mnemonic}"); println!("{mnemonic}");

View File

@ -125,13 +125,6 @@ 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> {
@ -283,36 +276,7 @@ where
return Err(MnemonicGenerationError::InvalidByteLength(bit_count)); return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
} }
Ok( Self::from_raw_bytes(bytes) ) Ok(unsafe { 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
@ -328,12 +292,11 @@ 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.
/// ///
/// # Panics /// # Safety
///
/// 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. The /// == 0`. If the assumption is incorrect, code may panic.
/// [`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
@ -352,10 +315,11 @@ 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 fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> { pub unsafe 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,
@ -556,30 +520,12 @@ mod tests {
} }
#[test] #[test]
fn can_do_up_to_8192_bits() { fn can_do_up_to_1024_bits() {
let mut entropy = [0u8; 1024]; let entropy = &mut [0u8; 128];
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 = Mnemonic::from_nonstandard_bytes(entropy); let mnemonic = unsafe { Mnemonic::from_raw_bytes(&entropy[..]) };
let words = mnemonic.words(); let words = mnemonic.words();
assert_eq!(words.len(), 768); assert!(words.len() == 96);
}
#[test]
#[should_panic]
fn fails_over_8192_bits() {
let entropy = &mut [0u8; 1024 + 4];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
}
#[test]
#[should_panic]
fn fails_over_invalid_size() {
let entropy = &mut [0u8; 255];
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
} }
} }

View File

@ -29,84 +29,6 @@ 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.
@ -157,8 +79,8 @@ pub mod mnemonic {
use super::Validator; use super::Validator;
use keyfork_bug::bug;
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)]

View File

@ -33,4 +33,3 @@
- [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)

View File

@ -1,39 +0,0 @@
# 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.