Compare commits

...

7 Commits

9 changed files with 136 additions and 23 deletions

View File

@ -1,3 +1,43 @@
# Keyfork v0.3.3
This release introduces a checksum verification mechanism for Remote Shard.
### Changes in keyfork-prompt:
```
e7be91b keyfork-{shard,prompt}: add Yes/No prompt for verifying QR codes
```
### Changes in keyfork-shard:
```
e7be91b keyfork-{shard,prompt}: add Yes/No prompt for verifying QR codes
739921d WIP: add checksum to shard
```
# Keyfork v0.3.2
This is another bugfix release, allowing the derivation of Shard keys.
### Changes in keyfork:
```
6ffcdc3 add derivation path for Shard keys
```
# Keyfork v0.3.1
This is a bugfix release, resolving an issue with Keyfork Shard not having a
exit condition for when a valid QR code was scanned.
### Changes in keyfork-shard:
```
d0019a9 keyfork-shard: break loop when receiving valid QR code
```
# Keyfork v0.3.0 # Keyfork v0.3.0
The Wizard is Dead. Long Live the Mnemonic Generator. The Wizard is Dead. Long Live the Mnemonic Generator.

6
Cargo.lock generated
View File

@ -1797,7 +1797,7 @@ dependencies = [
[[package]] [[package]]
name = "keyfork" name = "keyfork"
version = "0.3.0" version = "0.3.3"
dependencies = [ dependencies = [
"base64", "base64",
"card-backend-pcsc", "card-backend-pcsc",
@ -1944,7 +1944,7 @@ dependencies = [
[[package]] [[package]]
name = "keyfork-prompt" name = "keyfork-prompt"
version = "0.2.2" version = "0.2.3"
dependencies = [ dependencies = [
"keyfork-bug", "keyfork-bug",
"keyfork-crossterm", "keyfork-crossterm",
@ -1967,7 +1967,7 @@ dependencies = [
[[package]] [[package]]
name = "keyfork-shard" name = "keyfork-shard"
version = "0.3.2" version = "0.3.4"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",

View File

@ -35,17 +35,17 @@ keyforkd-models = { version = "0.2.0", path = "crates/daemon/keyforkd-models", r
keyfork-derive-openpgp = { version = "0.1.2", path = "crates/derive/keyfork-derive-openpgp", registry = "distrust", default-features = false } keyfork-derive-openpgp = { version = "0.1.2", path = "crates/derive/keyfork-derive-openpgp", registry = "distrust", default-features = false }
keyfork-derive-path-data = { version = "0.1.1", path = "crates/derive/keyfork-derive-path-data", registry = "distrust", default-features = false } keyfork-derive-path-data = { version = "0.1.1", path = "crates/derive/keyfork-derive-path-data", registry = "distrust", default-features = false }
keyfork-derive-util = { version = "0.2.0", path = "crates/derive/keyfork-derive-util", registry = "distrust", default-features = false } keyfork-derive-util = { version = "0.2.0", path = "crates/derive/keyfork-derive-util", registry = "distrust", default-features = false }
keyfork-shard = { version = "0.3.0", path = "crates/keyfork-shard", registry = "distrust", default-features = false } keyfork-shard = { version = "0.3.4", path = "crates/keyfork-shard", registry = "distrust", default-features = false }
keyfork-qrcode = { version = "0.1.1", path = "crates/qrcode/keyfork-qrcode", registry = "distrust", default-features = false } keyfork-qrcode = { version = "0.1.1", path = "crates/qrcode/keyfork-qrcode", registry = "distrust", default-features = false }
keyfork-zbar = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar", registry = "distrust", default-features = false } keyfork-zbar = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar", registry = "distrust", default-features = false }
keyfork-zbar-sys = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar-sys", registry = "distrust", default-features = false } keyfork-zbar-sys = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar-sys", registry = "distrust", default-features = false }
keyfork-bin = { version = "0.1.0", path = "crates/util/keyfork-bin", registry = "distrust", default-features = false } keyfork-bin = { version = "0.1.0", path = "crates/util/keyfork-bin", registry = "distrust", default-features = false }
keyfork-bug = { version = "0.1.0", path = "crates/util/keyfork-bug", registry = "distrust", default-features = false } keyfork-bug = { version = "0.1.1", path = "crates/util/keyfork-bug", registry = "distrust", default-features = false }
keyfork-crossterm = { version = "0.27.1", path = "crates/util/keyfork-crossterm", registry = "distrust", default-features = false } keyfork-crossterm = { version = "0.27.1", path = "crates/util/keyfork-crossterm", registry = "distrust", default-features = false }
keyfork-entropy = { version = "0.1.1", path = "crates/util/keyfork-entropy", registry = "distrust", default-features = false } keyfork-entropy = { version = "0.1.1", path = "crates/util/keyfork-entropy", registry = "distrust", default-features = false }
keyfork-frame = { version = "0.1.0", path = "crates/util/keyfork-frame", registry = "distrust", default-features = false } keyfork-frame = { version = "0.1.0", path = "crates/util/keyfork-frame", registry = "distrust", default-features = false }
keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false } keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false }
keyfork-prompt = { version = "0.2.0", path = "crates/util/keyfork-prompt", registry = "distrust", default-features = false } keyfork-prompt = { version = "0.2.3", path = "crates/util/keyfork-prompt", registry = "distrust", default-features = false }
keyfork-slip10-test-data = { version = "0.1.0", path = "crates/util/keyfork-slip10-test-data", registry = "distrust", default-features = false } keyfork-slip10-test-data = { version = "0.1.0", path = "crates/util/keyfork-slip10-test-data", registry = "distrust", default-features = false }
smex = { version = "0.1.0", path = "crates/util/smex", registry = "distrust", default-features = false } smex = { version = "0.1.0", path = "crates/util/smex", registry = "distrust", default-features = false }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork-shard" name = "keyfork-shard"
version = "0.3.2" version = "0.3.4"
edition = "2021" edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"

View File

@ -23,9 +23,9 @@ use keyfork_prompt::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength}, mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator, Validator,
}, },
Message as PromptMessage, PromptHandler, Message as PromptMessage, PromptHandler, YesNo,
}; };
use sha2::Sha256; use sha2::{Digest, Sha256};
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
const PLAINTEXT_LENGTH: u8 = 32 // shard const PLAINTEXT_LENGTH: u8 = 32 // shard
@ -59,6 +59,21 @@ impl std::fmt::Display for RetryScanMnemonic {
} }
} }
fn calculate_checksum(slice: &[u8]) -> Vec<u8> {
// generate a verification checksum
// this checksum should be expensive to calculate
let mut payload = vec![];
for _ in 0..1_000_000 {
payload.extend(slice);
let mut hasher = Sha256::new();
hasher.update(&payload);
let result = hasher.finalize();
payload.clear();
payload.extend(result);
}
payload
}
#[cfg(feature = "openpgp")] #[cfg(feature = "openpgp")]
pub mod openpgp; pub mod openpgp;
@ -273,14 +288,33 @@ pub trait Format {
.expect(bug!(POISONED_MUTEX)) .expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?; .prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
loop { loop {
if let Ok(Some(qrcode_content)) = keyfork_qrcode::scan_camera( if let Ok(Some(qrcode_content)) =
std::time::Duration::from_secs(*QRCODE_TIMEOUT), keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
0, {
) {
let decoded_data = BASE64_STANDARD let decoded_data = BASE64_STANDARD
.decode(qrcode_content) .decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data")); .expect(bug!("qrcode should contain base64 encoded data"));
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?); let data: [u8; 32] = decoded_data.try_into().map_err(|_| InvalidData)?;
let checksum = calculate_checksum(&data);
let small_sum = &checksum[..8];
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
let question =
format!("Do these words match the expected words? {small_mnemonic}");
let response = keyfork_prompt::prompt_choice(
&mut **prompt,
&question,
&[YesNo::No, YesNo::Yes],
)?;
if response == YesNo::No {
prompt.prompt_message(PromptMessage::Text(String::from(
"Could not establish secure channel, exiting.",
)))?;
std::process::exit(1);
}
pubkey_data = Some(data);
break; break;
} else { } else {
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX)); let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
@ -535,15 +569,21 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
&BASE64_STANDARD.encode(qrcode_data), &BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest, ErrorCorrection::Highest,
) { ) {
let checksum = calculate_checksum(key_mnemonic.as_bytes());
let small_sum = &checksum[..8];
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
pm.prompt_message(PromptMessage::Text(format!( pm.prompt_message(PromptMessage::Text(format!(
concat!( concat!(
"QR code #{iter} will be displayed after this prompt. ", "QR code #{iter} will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ", "Send the QR code to the next shardholder. ",
"Only the next shardholder should scan the QR code." "Only the next shardholder should scan the QR code. ",
), ),
iter = iter iter = iter,
)))?; )))?;
pm.prompt_message(PromptMessage::Data(qrcode))?; pm.prompt_message(PromptMessage::Data(qrcode))?;
pm.prompt_message(PromptMessage::Text(format!(
"The following should be sent to verify the QR code: {small_mnemonic}"
)))?;
} }
} }
@ -562,10 +602,9 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
{ {
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?; pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
loop { loop {
if let Ok(Some(qrcode_content)) = keyfork_qrcode::scan_camera( if let Ok(Some(qrcode_content)) =
std::time::Duration::from_secs(*QRCODE_TIMEOUT), keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
0, {
) {
let decoded_data = BASE64_STANDARD let decoded_data = BASE64_STANDARD
.decode(qrcode_content) .decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data")); .expect(bug!("qrcode should contain base64 encoded data"));
@ -578,6 +617,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
let _ = let _ =
pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?); 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());
break;
} else { } else {
let choice = keyfork_prompt::prompt_choice( let choice = keyfork_prompt::prompt_choice(
&mut *pm, &mut *pm,

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork" name = "keyfork"
version = "0.3.0" version = "0.3.3"
edition = "2021" edition = "2021"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
@ -8,7 +8,7 @@ license = "AGPL-3.0-only"
default = [ default = [
"completion", "completion",
"qrcode-decode-backend-rqrr", "qrcode-decode-backend-rqrr",
"sequoia-crypto-backend-nettle", "sequoia-crypto-backend-nettle",
] ]
completion = ["dep:clap_complete"] completion = ["dep:clap_complete"]

View File

@ -58,6 +58,9 @@ pub enum Path {
/// The Disaster Recovery index. /// The Disaster Recovery index.
DisasterRecovery, DisasterRecovery,
/// The Shard index.
Shard,
} }
impl std::fmt::Display for Path { impl std::fmt::Display for Path {
@ -71,6 +74,7 @@ impl Path {
match self { match self {
Path::Default => "default", Path::Default => "default",
Path::DisasterRecovery => "disaster-recovery", Path::DisasterRecovery => "disaster-recovery",
Path::Shard => "shard",
} }
} }
@ -78,6 +82,7 @@ impl Path {
match self { match self {
Self::Default => paths::OPENPGP.clone(), Self::Default => paths::OPENPGP.clone(),
Self::DisasterRecovery => paths::OPENPGP_DISASTER_RECOVERY.clone(), Self::DisasterRecovery => paths::OPENPGP_DISASTER_RECOVERY.clone(),
Self::Shard => paths::OPENPGP_SHARD.clone(),
} }
} }
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "keyfork-prompt" name = "keyfork-prompt"
version = "0.2.2" version = "0.2.3"
description = "Prompt management utilities for Keyfork" description = "Prompt management utilities for Keyfork"
repository = "https://git.distrust.co/public/keyfork" repository = "https://git.distrust.co/public/keyfork"
edition = "2021" edition = "2021"

View File

@ -83,6 +83,34 @@ impl<T: Choice> Choice for &T {
} }
} }
/// A Yes/No Choice.
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum YesNo {
/// Yes.
Yes,
/// No.
No,
}
impl std::fmt::Display for YesNo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
YesNo::Yes => f.write_str("Yes"),
YesNo::No => f.write_str("No"),
}
}
}
impl Choice for YesNo {
fn identifier(&self) -> Option<char> {
match self {
YesNo::Yes => Some('y'),
YesNo::No => Some('n'),
}
}
}
#[doc(hidden)] #[doc(hidden)]
pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>; pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>;