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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,7 @@ where
));
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
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 server_handle = tokio::spawn({
let socket_path = socket_path.clone();
@ -87,13 +87,8 @@ where
let result = test_handle.await;
server_handle.abort();
result
});
if let Err(e) = result {
if e.is_panic() {
std::panic::resume_unwind(e.into_panic());
}
}
Ok(())
})
.expect(bug!("runtime could not join all threads"))
}
#[cfg(test)]

View File

@ -59,17 +59,13 @@ pub enum Error {
#[allow(missing_docs)]
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
/// 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).
/// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
/// ID.
///
/// # Errors
/// The function may error for any condition mentioned in [`Error`].
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.first() {
let primary_key_flags = match keys.get(0) {
Some(kf) if kf.for_certification() => kf,
_ => 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 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 signatures =
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;

View File

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

View File

@ -124,9 +124,9 @@ mod serde_with {
K: PrivateKey + Clone,
{
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
let bytes: [u8; 32] = variable_len_bytes.try_into().expect(bug!(
"unable to parse serialized private key; no support for static len"
));
let bytes: [u8; 32] = variable_len_bytes
.try_into()
.expect(bug!("unable to parse serialized private key; no support for static len"));
Ok(K::from_bytes(&bytes))
}
}
@ -179,20 +179,13 @@ where
.into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
assert!(
!private_key.iter().all(|byte| *byte == 0),
bug!("hmac function returned all-zero master key")
);
Self::from_parts(
Self::new_from_parts(
private_key
.try_into()
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
0,
// Checked: chain_code is always the same length, hash is static size
chain_code
.try_into()
.expect(bug!("Invalid chain code length")),
chain_code.try_into().expect(bug!("Invalid chain code length")),
)
}
@ -212,9 +205,9 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # 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 {
private_key: K::from_bytes(key),
depth,
@ -236,7 +229,7 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # 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));
/// ```
pub fn private_key(&self) -> &K {
@ -269,7 +262,7 @@ where
/// # }
/// ```
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`].
@ -308,7 +301,7 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # 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);
/// ```
pub fn depth(&self) -> u8 {
@ -328,7 +321,7 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # 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());
/// ```
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.
#[derive(Error, Clone, Debug)]
pub enum Error {
/// BIP-0032 does not support hardened public key derivation from parent public keys.
#[error("Hardened child public keys may not be derived from parent public keys")]
/// BIP-0032 does not support deriving public keys from hardened private keys.
#[error("Public keys may not be derived when hardened")]
HardenedIndex,
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
@ -60,11 +60,11 @@ where
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let pubkey = PublicKey::from_bytes(key);
/// let xpub = ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// let xpub = ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// # 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 {
public_key,
depth,
@ -86,7 +86,7 @@ where
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// let pubkey = xpub.public_key();
/// # Ok(())
/// # }
@ -121,7 +121,7 @@ where
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// let index = DerivationIndex::new(0, false)?;
/// let child = xpub.derive_child(&index)?;
/// # Ok(())

View File

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

View File

@ -42,7 +42,7 @@ pub trait PublicKey: Sized {
/// # Errors
///
/// 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.
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")]
NonZero,
/// A scalar could not be constructed for the given algorithm.
#[error("A scalar could not be constructed for the given algorithm")]
InvalidScalar,
/// Public key derivation is unsupported for this algorithm.
#[error("Public key derivation is unsupported for this algorithm")]
DerivationUnsupported,
@ -89,7 +85,7 @@ pub enum PublicKeyError {
#[cfg(feature = "secp256k1")]
use k256::{
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
AffinePoint,
AffinePoint, NonZeroScalar,
};
#[cfg(feature = "secp256k1")]
@ -109,16 +105,14 @@ impl PublicKey for k256::PublicKey {
}
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
use k256::elliptic_curve::ScalarPrimitive;
use k256::{Secp256k1, Scalar};
if other.iter().all(|n| n == &0) {
return Err(PublicKeyError::NonZero);
}
// Checked: See above
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
.expect(bug!("Should have been able to get a NonZeroScalar"));
// 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);
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
Ok(Self::from_affine(point.into())
.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
/// cryptographic operations.
#[doc(hidden)]
#[derive(Clone)]
pub struct TestPublicKey {
pub(crate) key: [u8; 33],
}
impl TestPublicKey {
/// Create a new TestPublicKey from the given bytes.
#[doc(hidden)]
#[allow(dead_code)]
pub fn from_bytes(b: &[u8]) -> Self {
Self {

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "keyfork-shard"
version = "0.2.0"
version = "0.1.0"
edition = "2021"
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 = { version = "0.4.0", optional = true }
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::{
aead::{consts::U12, Aead},
aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce,
};
use base64::prelude::{Engine, BASE64_STANDARD};
use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{
validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator,
},
validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, PromptHandler, Terminal,
};
use sha2::Sha256;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
// 32-byte share, 1-byte index, 1-byte threshold, 1-byte version == 36 bytes
// Encrypted, is 52 bytes
const PLAINTEXT_LENGTH: u8 = 36;
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
const ENC_LEN: u8 = 4 * 16;
#[cfg(feature = "openpgp")]
pub mod openpgp;
@ -200,6 +194,7 @@ pub trait Format {
let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera
@ -209,13 +204,12 @@ pub trait Format {
.lock()
.expect(bug!(POISONED_MUTEX))
.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)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
let decoded_data = smex::decode(&hex)?;
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
} else {
prompt
.lock()
@ -225,43 +219,43 @@ pub trait Format {
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let their_pubkey = match pubkey_data {
Some(pubkey) => pubkey,
None => {
let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)),
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
};
prompt
let [nonce_mnemonic, pubkey_mnemonic] = prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?
)?;
let nonce = nonce_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
.map_err(|_| InvalidData)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
(nonce, pubkey)
}
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
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 shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
// decrypt a single shard and create the payload
let (share, threshold) =
@ -270,47 +264,49 @@ pub trait Format {
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
assert!(
payload.len() < PLAINTEXT_LENGTH as usize,
"invalid share length (too long, max {PLAINTEXT_LENGTH} bytes)"
payload.len() <= ENC_LEN as usize,
"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
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"),
);
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
let nonce = Nonce::<U12>::from_slice(&nonce);
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
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")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
@ -405,7 +401,7 @@ pub struct InvalidData;
/// 1 byte: Version
/// 1 byte: Threshold
/// Data: &[u8]
pub(crate) const HUNK_VERSION: u8 = 2;
pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_OFFSET: usize = 2;
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
@ -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) {
iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
let our_key = EphemeralSecret::random();
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let qrcode_data = key_mnemonic.to_bytes();
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
let mut qrcode_data = nonce_mnemonic.to_bytes();
qrcode_data.extend(key_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"QR code #{iter} will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ",
"Only the next shardholder should scan the QR code."
"A QR code will be displayed after this prompt. ",
"Send the QR code to only shardholder {iter}. ",
"Nobody else should scan this QR code."
),
iter = iter
)))?;
@ -461,9 +457,11 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"Upon request, these words should be sent to the shardholder: ",
"{key_mnemonic}"
"Upon request, these words should be sent to shardholder {iter}: ",
"{nonce_mnemonic} {key_mnemonic}"
),
iter = iter,
nonce_mnemonic = nonce_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")]
{
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)
{
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 decoded_data = smex::decode(&hex)?;
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec());
} else {
@ -495,7 +486,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
(Some(pubkey), Some(payload)) => (pubkey, payload),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [24, 39],
word_lengths: [24, 48],
};
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!(
payload.len(),
ENCRYPTED_LENGTH as usize,
bug!("invalid payload data")
);
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
assert!(
shared_secret.was_contributory(),
bug!("shared secret might be insecure")
);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
let payload =
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
match &mut iter_count {
@ -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..usize::from(*payload_len)].to_vec());
shares.push(payload[HUNK_OFFSET..].to_vec());
}
let shares = shares

View File

@ -194,52 +194,33 @@ impl<P: PromptHandler> OpenPGP<P> {
}
impl<P: PromptHandler> OpenPGP<P> {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them.
///
/// Certificates are read from a file, or from files one level deep in a directory.
/// Certificates with duplicated fingerprints will be discarded.
/// 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.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is
/// unable to load certificates from the file.
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
let mut pubkeys = std::collections::HashSet::new();
let mut certs = HashMap::new();
if path.is_file() {
for maybe_cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
let cert = maybe_cert.map_err(Error::Sequoia)?;
let certfp = cert.fingerprint();
for key in cert.keys() {
let fp = key.fingerprint();
if pubkeys.contains(&fp) {
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
}
pubkeys.insert(fp);
}
certs.insert(certfp, cert);
let mut vec = vec![];
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
vec.push(cert.map_err(Error::Sequoia)?);
}
Ok(vec)
} else {
let mut vec = vec![];
for entry in path
.read_dir()
.map_err(Error::Io)?
.filter_map(Result::ok)
.filter(|p| p.path().is_file())
{
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}");
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
}
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,
} => {}
MessageLayer::SignatureGroup { results } => {
match &results[..] {
[Ok(_)] => {
return Ok(());
}
_ => {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
}
}
/*
for result in results {
if let Err(e) = result {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Invalid signature: {e}"));
}
}
*/
}
}
}

View File

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

View File

@ -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-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", registry = "distrust" }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false, registry = "distrust" }
keyfork-shard = { version = "0.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" }
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
/// 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
/// 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.
/// The key is generated with a 24-hour expiration time. The operation to set the expiration
/// time to a higher value is left to the user to ensure the key is usable by the user.
#[command(name = "openpgp")]
OpenPGP {
/// 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_prompt::{
validators::{SecurePinValidator, Validator},
validators::{PinValidator, Validator},
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 mut shrd_u32 = [0u8; 4];
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 path = DerivationPath::default()
.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),
..Default::default()
}
.to_fn();
let admin_pin_validator = SecurePinValidator {
let admin_pin_validator = PinValidator {
min_length: Some(8),
..Default::default()
}
@ -132,8 +132,8 @@ fn generate_shard_secret(
for i in 0..keys_per_shard {
pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
(i as u16) + 1,
(index as u16) + 1,
i + 1,
index + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {

View File

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

View File

@ -5,7 +5,7 @@ use keyfork_bug as bug;
use image::io::Reader as ImageReader;
use std::{
io::{Cursor, Write},
time::{Duration, Instant},
time::{Duration, SystemTime},
process::{Command, Stdio},
};
use v4l::{
@ -110,10 +110,11 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = Instant::now();
let start = SystemTime::now();
while Instant::now()
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout
{
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");
device.set_format(&fmt)?;
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();
while Instant::now()
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout
{
let (buffer, _) = stream.next()?;

View File

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

View File

@ -10,16 +10,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
bit_size % 8 == 0,
"Bit size must be divisible by 8, got: {bit_size}"
);
assert!(
bit_size <= 256,
"Maximum supported bit size is 256, got: {bit_size}"
);
assert!(
bit_size >= 128,
"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)?;
println!("{}", smex::encode(entropy));

View File

@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
input.read_line(&mut line)?;
let decoded = smex::decode(line.trim())?;
let mnemonic = Mnemonic::from_raw_bytes(&decoded) ;
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&decoded) };
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`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MnemonicBase<W: Wordlist> {
@ -283,36 +276,7 @@ where
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
}
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)
Ok(unsafe { Self::from_raw_bytes(bytes) })
}
/// 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
/// 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
/// 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
/// [`MnemonicBase::from_nonstandard_bytes`] function may be used to generate entropy if the
/// length of the data is known at compile-time.
/// == 0`. If the assumption is incorrect, code may panic.
///
/// # Examples
/// ```rust
@ -352,10 +315,11 @@ where
/// // NOTE: Data is of invalid length, 31
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string();
/// // NOTE: panic happens here
/// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
/// ```
pub fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
assert!(bytes.len() % 4 == 0);
assert!(bytes.len() <= 1024);
pub unsafe fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
MnemonicBase {
data: bytes.to_vec(),
marker: PhantomData,
@ -556,30 +520,12 @@ mod tests {
}
#[test]
fn can_do_up_to_8192_bits() {
let mut entropy = [0u8; 1024];
fn can_do_up_to_1024_bits() {
let entropy = &mut [0u8; 128];
let mut random = std::fs::File::open("/dev/urandom").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();
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[..]);
assert!(words.len() == 96);
}
}

View File

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

View File

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

View File

@ -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.