Compare commits
7 Commits
6c25cb8f31
...
7eeb494819
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 7eeb494819 | |
Ryan Heywood | b873ef4d5c | |
Ryan Heywood | 55b41a49ef | |
Ryan Heywood | 2670cf63a3 | |
Ryan Heywood | 726670fe96 | |
Ryan Heywood | ddefe1c6b5 | |
Ryan Heywood | 1cdbab1a1d |
|
@ -17,6 +17,41 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes-gcm"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"aes",
|
||||||
|
"cipher",
|
||||||
|
"ctr",
|
||||||
|
"ghash",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
|
@ -341,6 +376,16 @@ dependencies = [
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
@ -481,9 +526,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core 0.6.4",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctr"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
|
@ -783,6 +838,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ghash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
|
||||||
|
dependencies = [
|
||||||
|
"opaque-debug",
|
||||||
|
"polyval",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
|
@ -903,6 +968,15 @@ dependencies = [
|
||||||
"hashbrown 0.14.1",
|
"hashbrown 0.14.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.31.0"
|
version = "1.31.0"
|
||||||
|
@ -1086,11 +1160,13 @@ dependencies = [
|
||||||
name = "keyfork-shard"
|
name = "keyfork-shard"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
"card-backend",
|
"card-backend",
|
||||||
"card-backend-pcsc",
|
"card-backend-pcsc",
|
||||||
"keyfork-derive-openpgp",
|
"keyfork-derive-openpgp",
|
||||||
|
"keyfork-mnemonic-util",
|
||||||
"keyfork-prompt",
|
"keyfork-prompt",
|
||||||
"openpgp-card",
|
"openpgp-card",
|
||||||
"openpgp-card-sequoia",
|
"openpgp-card-sequoia",
|
||||||
|
@ -1099,6 +1175,7 @@ dependencies = [
|
||||||
"sharks",
|
"sharks",
|
||||||
"smex",
|
"smex",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"x25519-dalek",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1426,6 +1503,12 @@ version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpgp-card"
|
name = "openpgp-card"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -1624,6 +1707,18 @@ version = "3.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8"
|
checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polyval"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
@ -2440,6 +2535,16 @@ version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -2693,6 +2798,18 @@ version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x25519-dalek"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"serde",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xxhash-rust"
|
name = "xxhash-rust"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
|
|
@ -149,7 +149,8 @@ impl FromStr for Mnemonic {
|
||||||
None => return Err(MnemonicFromStrError::InvalidWord(index)),
|
None => return Err(MnemonicFromStrError::InvalidWord(index)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ![12, 24].contains(&words.len()) {
|
// 3 words for every 32 bits
|
||||||
|
if words.len() % 3 != 0 {
|
||||||
return Err(MnemonicFromStrError::InvalidWordCount(words.len()));
|
return Err(MnemonicFromStrError::InvalidWordCount(words.len()));
|
||||||
}
|
}
|
||||||
Ok(Mnemonic { words, wordlist })
|
Ok(Mnemonic { words, wordlist })
|
||||||
|
|
|
@ -6,7 +6,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_entropy(&decoded, Default::default())?;
|
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&decoded, Default::default()) };
|
||||||
|
|
||||||
println!("{mnemonic}");
|
println!("{mnemonic}");
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ use keyfork_mnemonic_util::Wordlist;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor,
|
cursor,
|
||||||
event::{read, Event, KeyCode},
|
event::{read, Event, KeyCode, KeyModifiers},
|
||||||
style::{Print, PrintStyledContent, Stylize},
|
style::{Print, PrintStyledContent, Stylize},
|
||||||
terminal,
|
terminal,
|
||||||
tty::IsTty,
|
tty::IsTty,
|
||||||
|
@ -110,6 +110,9 @@ where
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
input.pop();
|
input.pop();
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('w') if k.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
while input.pop().is_some_and(|c| !c.is_whitespace()) {}
|
||||||
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') => {
|
||||||
if !input.chars().rev().next().is_some_and(char::is_whitespace) {
|
if !input.chars().rev().next().is_some_and(char::is_whitespace) {
|
||||||
input.push(' ');
|
input.push(' ');
|
||||||
|
@ -235,6 +238,8 @@ where
|
||||||
let mut terminal = AlternateScreen::new(&mut self.write)?;
|
let mut terminal = AlternateScreen::new(&mut self.write)?;
|
||||||
let mut terminal = RawMode::new(&mut terminal)?;
|
let mut terminal = RawMode::new(&mut terminal)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// TODO: split on word boundaries
|
||||||
terminal
|
terminal
|
||||||
.queue(terminal::Clear(terminal::ClearType::All))?
|
.queue(terminal::Clear(terminal::ClearType::All))?
|
||||||
.queue(cursor::MoveTo(0, 0))?;
|
.queue(cursor::MoveTo(0, 0))?;
|
||||||
|
@ -254,7 +259,6 @@ where
|
||||||
.queue(PrintStyledContent(" OK ".negative()))?
|
.queue(PrintStyledContent(" OK ".negative()))?
|
||||||
.flush()?;
|
.flush()?;
|
||||||
|
|
||||||
loop {
|
|
||||||
match read()? {
|
match read()? {
|
||||||
Event::Key(k) => match k.code {
|
Event::Key(k) => match k.code {
|
||||||
KeyCode::Enter | KeyCode::Char(' ') => break,
|
KeyCode::Enter | KeyCode::Char(' ') => break,
|
||||||
|
@ -262,6 +266,8 @@ where
|
||||||
},
|
},
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
terminal.queue(cursor::EnableBlinking)?.flush()?;
|
terminal.queue(cursor::EnableBlinking)?.flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -26,3 +26,6 @@ sharks = "0.5.0"
|
||||||
smex = { version = "0.1.0", path = "../smex" }
|
smex = { version = "0.1.0", path = "../smex" }
|
||||||
thiserror = "1.0.50"
|
thiserror = "1.0.50"
|
||||||
keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true }
|
keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true }
|
||||||
|
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
|
||||||
|
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
|
||||||
|
aes-gcm = "0.10.3"
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
fs::File,
|
||||||
|
io::{stdin, stdout},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::ExitCode,
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use aes_gcm::{aead::Aead, aes::cipher::consts::U12, Aes256Gcm, KeyInit, Nonce};
|
||||||
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
|
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
|
||||||
|
use keyfork_prompt::PromptManager;
|
||||||
|
use keyfork_shard::openpgp::{decrypt_one, discover_certs, openpgp::Cert, parse_messages};
|
||||||
|
|
||||||
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
fn validate<'a>(
|
||||||
|
messages_file: impl AsRef<Path>,
|
||||||
|
key_discovery: impl Into<Option<&'a str>>,
|
||||||
|
) -> Result<(File, Vec<Cert>)> {
|
||||||
|
let key_discovery = key_discovery.into().map(PathBuf::from);
|
||||||
|
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
||||||
|
|
||||||
|
// Load certs from path
|
||||||
|
let certs = key_discovery
|
||||||
|
.map(discover_certs)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
Ok((File::open(messages_file)?, certs))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<()> {
|
||||||
|
let mut args = env::args();
|
||||||
|
let program_name = args.next().expect("program name");
|
||||||
|
let args = args.collect::<Vec<_>>();
|
||||||
|
let (messages_file, cert_list) = match args.as_slice() {
|
||||||
|
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
|
||||||
|
[messages_file] => validate(messages_file, None)?,
|
||||||
|
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut encrypted_messages = parse_messages(messages_file)?;
|
||||||
|
|
||||||
|
let encrypted_metadata = encrypted_messages
|
||||||
|
.pop_front()
|
||||||
|
.expect("any pgp encrypted message");
|
||||||
|
|
||||||
|
// Receive their key
|
||||||
|
|
||||||
|
let mut pm = PromptManager::new(stdin(), stdout())?;
|
||||||
|
let wordlist = Wordlist::default();
|
||||||
|
let their_words = pm.prompt_wordlist("Their words: ", &wordlist)?;
|
||||||
|
let mut nonce_words = their_words
|
||||||
|
.split_whitespace()
|
||||||
|
.take(9)
|
||||||
|
.peekable();
|
||||||
|
let mut pubkey_words = their_words
|
||||||
|
.split_whitespace()
|
||||||
|
.skip(9)
|
||||||
|
.take(24)
|
||||||
|
.peekable();
|
||||||
|
let mut nonce_mnemonic = String::new();
|
||||||
|
let mut pubkey_mnemonic = String::new();
|
||||||
|
while let Some(word) = nonce_words.next() {
|
||||||
|
nonce_mnemonic.push_str(word);
|
||||||
|
if nonce_words.peek().is_some() {
|
||||||
|
nonce_mnemonic.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while let Some(word) = pubkey_words.next() {
|
||||||
|
pubkey_mnemonic.push_str(word);
|
||||||
|
if pubkey_words.peek().is_some() {
|
||||||
|
pubkey_mnemonic.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let their_key = Mnemonic::from_str(&pubkey_mnemonic)?.entropy();
|
||||||
|
let their_key: [u8; 32] = their_key.try_into().expect("24 words");
|
||||||
|
let their_nonce = Mnemonic::from_str(&nonce_mnemonic)?.entropy();
|
||||||
|
let their_nonce = Nonce::<U12>::from_slice(&their_nonce);
|
||||||
|
|
||||||
|
let our_key = EphemeralSecret::random();
|
||||||
|
let our_mnemonic =
|
||||||
|
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
|
||||||
|
// TODO: Encode using `qrencode -t ansiutf8 -m 2`
|
||||||
|
pm.prompt_message(&format!("Our words: {our_mnemonic}"))?;
|
||||||
|
|
||||||
|
let shared_secret = our_key
|
||||||
|
.diffie_hellman(&PublicKey::from(their_key))
|
||||||
|
.to_bytes();
|
||||||
|
|
||||||
|
let share = decrypt_one(encrypted_messages.into(), &cert_list, encrypted_metadata)?;
|
||||||
|
assert_eq!(share.len(), 65, "non-constant share length");
|
||||||
|
const LEN: u8 = 24 * 3;
|
||||||
|
let mut encrypted_payload = [(LEN - share.len() as u8); LEN as usize];
|
||||||
|
encrypted_payload[..share.len()].copy_from_slice(&share);
|
||||||
|
|
||||||
|
let shared_key = Aes256Gcm::new_from_slice(&shared_secret)?;
|
||||||
|
let bytes = shared_key.encrypt(their_nonce, share.as_slice()).unwrap();
|
||||||
|
|
||||||
|
const ENC_LEN: u8 = 24 * 4;
|
||||||
|
let mut out_bytes = [(ENC_LEN - bytes.len() as u8); ENC_LEN as usize];
|
||||||
|
assert!(bytes.len() < out_bytes.len(), "encrypted payload larger than acceptable limit");
|
||||||
|
out_bytes[..bytes.len()].clone_from_slice(&bytes);
|
||||||
|
|
||||||
|
// safety: size of out_bytes is immutable and always % 32 == 0
|
||||||
|
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
|
||||||
|
|
||||||
|
pm.prompt_message(&format!("Our payload: {mnemonic}"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let result = run();
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("Error: {e}");
|
||||||
|
let mut source = e.source();
|
||||||
|
while let Some(new_error) = source.take() {
|
||||||
|
eprintln!("Source: {new_error}");
|
||||||
|
source = new_error.source();
|
||||||
|
}
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
|
@ -205,6 +205,159 @@ fn get_decryption_keys<'a>(
|
||||||
.secret()
|
.secret()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
|
||||||
|
assert_eq!(
|
||||||
|
SHARD_METADATA_VERSION, buf[0],
|
||||||
|
"Incompatible metadata version"
|
||||||
|
);
|
||||||
|
let threshold = buf[1];
|
||||||
|
|
||||||
|
let mut cert_parser =
|
||||||
|
CertParser::from_bytes(&buf[SHARD_METADATA_OFFSET..]).map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
let root_cert = match cert_parser.next() {
|
||||||
|
Some(Ok(c)) => c,
|
||||||
|
Some(Err(e)) => return Err(Error::Sequoia(e)),
|
||||||
|
None => panic!("No data found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let certs = cert_parser
|
||||||
|
.collect::<openpgp::Result<Vec<_>>>()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
Ok((threshold, root_cert, certs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
|
||||||
|
// single message.
|
||||||
|
fn decrypt_with_manager(
|
||||||
|
threshold: u8,
|
||||||
|
messages: &mut HashMap<KeyID, EncryptedMessage>,
|
||||||
|
certs: &[Cert],
|
||||||
|
policy: NullPolicy,
|
||||||
|
manager: &mut SmartcardManager,
|
||||||
|
) -> Result<HashMap<KeyID, Vec<u8>>> {
|
||||||
|
let mut decrypted_messages = HashMap::new();
|
||||||
|
|
||||||
|
while threshold as usize - decrypted_messages.len() > 0 {
|
||||||
|
// Build list of fingerprints that haven't yet been used for decrypting
|
||||||
|
let mut cert_by_fingerprint = HashMap::new();
|
||||||
|
let mut unused_fingerprints = vec![];
|
||||||
|
for valid_cert in certs
|
||||||
|
.iter()
|
||||||
|
.filter(|cert| !decrypted_messages.contains_key(&cert.keyid()))
|
||||||
|
.map(|cert| cert.with_policy(&policy, None))
|
||||||
|
{
|
||||||
|
let valid_cert = valid_cert.map_err(Error::Sequoia)?;
|
||||||
|
let fp = valid_cert
|
||||||
|
.keys()
|
||||||
|
.for_storage_encryption()
|
||||||
|
.map(|k| k.fingerprint())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for fp in &fp {
|
||||||
|
cert_by_fingerprint.insert(fp.clone(), valid_cert.keyid());
|
||||||
|
}
|
||||||
|
unused_fingerprints.extend(fp.into_iter());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message
|
||||||
|
if let Some(fp) = manager.load_any_fingerprint(unused_fingerprints)? {
|
||||||
|
let cert_keyid = cert_by_fingerprint.get(&fp).unwrap().clone();
|
||||||
|
if let Some(message) = messages.remove(&cert_keyid) {
|
||||||
|
let message = message.decrypt_with(&policy, &mut *manager)?;
|
||||||
|
decrypted_messages.insert(cert_keyid, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(decrypted_messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
|
||||||
|
// decrypt messages with.
|
||||||
|
fn decrypt_with_keyring(
|
||||||
|
messages: &mut HashMap<KeyID, EncryptedMessage>,
|
||||||
|
certs: &[Cert],
|
||||||
|
policy: &NullPolicy,
|
||||||
|
keyring: &mut Keyring,
|
||||||
|
) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
|
||||||
|
let mut decrypted_messages = HashMap::new();
|
||||||
|
|
||||||
|
for valid_cert in certs.iter().map(|cert| cert.with_policy(policy, None)) {
|
||||||
|
let valid_cert = valid_cert.map_err(Error::Sequoia)?;
|
||||||
|
let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let secret_cert = secret_cert
|
||||||
|
.with_policy(policy, None)
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
let keys = get_decryption_keys(&secret_cert).collect::<Vec<_>>();
|
||||||
|
if !keys.is_empty() {
|
||||||
|
if let Some(message) = messages.get_mut(&valid_cert.keyid()) {
|
||||||
|
for (pkesk, key) in message.pkesks.iter_mut().zip(keys) {
|
||||||
|
pkesk.set_recipient(key.keyid());
|
||||||
|
}
|
||||||
|
// we have a pkesk, decrypt via keyring
|
||||||
|
decrypted_messages.insert(
|
||||||
|
valid_cert.keyid(),
|
||||||
|
message.decrypt_with(policy, &mut *keyring)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(decrypted_messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_metadata(
|
||||||
|
message: &EncryptedMessage,
|
||||||
|
policy: &NullPolicy,
|
||||||
|
keyring: &mut Keyring,
|
||||||
|
manager: &mut SmartcardManager,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
Ok(if keyring.is_empty() {
|
||||||
|
manager.load_any_card()?;
|
||||||
|
message.decrypt_with(policy, manager)?
|
||||||
|
} else {
|
||||||
|
message.decrypt_with(policy, keyring)?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_one(
|
||||||
|
messages: Vec<EncryptedMessage>,
|
||||||
|
certs: &[Cert],
|
||||||
|
metadata: EncryptedMessage,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let policy = NullPolicy::new();
|
||||||
|
|
||||||
|
let mut keyring = Keyring::new(certs)?;
|
||||||
|
let mut manager = SmartcardManager::new()?;
|
||||||
|
|
||||||
|
let content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?;
|
||||||
|
|
||||||
|
let (_threshold, root_cert, certs) = decode_metadata_v1(&content)?;
|
||||||
|
|
||||||
|
keyring.set_root_cert(root_cert.clone());
|
||||||
|
manager.set_root_cert(root_cert);
|
||||||
|
|
||||||
|
let mut messages: HashMap<KeyID, EncryptedMessage> =
|
||||||
|
HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages));
|
||||||
|
|
||||||
|
let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
|
||||||
|
|
||||||
|
if let Some(message) = decrypted_messages.into_values().next() {
|
||||||
|
return Ok(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, policy, &mut manager)?;
|
||||||
|
|
||||||
|
if let Some(message) = decrypted_messages.into_values().next() {
|
||||||
|
return Ok(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!("smartcard manager should always decrypt")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn combine(
|
pub fn combine(
|
||||||
certs: Vec<Cert>,
|
certs: Vec<Cert>,
|
||||||
metadata: EncryptedMessage,
|
metadata: EncryptedMessage,
|
||||||
|
@ -217,106 +370,35 @@ pub fn combine(
|
||||||
|
|
||||||
let mut keyring = Keyring::new(certs)?;
|
let mut keyring = Keyring::new(certs)?;
|
||||||
let mut manager = SmartcardManager::new()?;
|
let mut manager = SmartcardManager::new()?;
|
||||||
let content = if keyring.is_empty() {
|
let content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?;
|
||||||
// NOTE: Any card plugged in that can't decrypt, will raise issues.
|
|
||||||
// This should not be used on a system where OpenPGP cards are available that shouldn't be
|
|
||||||
// used, due to the nature of how wildcard decryption works.
|
|
||||||
manager.load_any_card()?;
|
|
||||||
metadata.decrypt_with(&policy, &mut manager)?
|
|
||||||
} else {
|
|
||||||
metadata.decrypt_with(&policy, &mut keyring)?
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
let (threshold, root_cert, certs) = decode_metadata_v1(&content)?;
|
||||||
SHARD_METADATA_VERSION, content[0],
|
|
||||||
"incompatible metadata version"
|
|
||||||
);
|
|
||||||
let threshold = content[1];
|
|
||||||
|
|
||||||
let mut cert_parser =
|
|
||||||
CertParser::from_bytes(&content[SHARD_METADATA_OFFSET..]).map_err(Error::Sequoia)?;
|
|
||||||
let root_cert = match cert_parser.next() {
|
|
||||||
Some(Ok(c)) => c,
|
|
||||||
Some(Err(e)) => panic!("Could not find root (first) certificate: {e}"),
|
|
||||||
None => panic!("No certs found in cert parser"),
|
|
||||||
};
|
|
||||||
let certs = cert_parser
|
|
||||||
.collect::<openpgp::Result<Vec<_>>>()
|
|
||||||
.map_err(Error::Sequoia)?;
|
|
||||||
keyring.set_root_cert(root_cert.clone());
|
keyring.set_root_cert(root_cert.clone());
|
||||||
manager.set_root_cert(root_cert);
|
manager.set_root_cert(root_cert);
|
||||||
|
|
||||||
|
// Generate a controlled binding from certificates to encrypted messages. This is stable
|
||||||
|
// because we control the order packets are encrypted and certificates are stored.
|
||||||
|
|
||||||
let mut messages: HashMap<KeyID, EncryptedMessage> =
|
let mut messages: HashMap<KeyID, EncryptedMessage> =
|
||||||
HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages));
|
HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages));
|
||||||
let mut decrypted_messages: HashMap<KeyID, Vec<u8>> = HashMap::new();
|
|
||||||
|
|
||||||
// NOTE: This is ONLY stable because we control the generation of PKESK packets and
|
let mut decrypted_messages =
|
||||||
// encode the policy to ourselves.
|
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
|
||||||
for valid_cert in certs.iter().map(|cert| cert.with_policy(&policy, None)) {
|
|
||||||
let valid_cert = valid_cert.map_err(Error::Sequoia)?;
|
|
||||||
// get keys from keyring for cert
|
|
||||||
let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let secret_cert = secret_cert
|
|
||||||
.with_policy(&policy, None)
|
|
||||||
.map_err(Error::Sequoia)?;
|
|
||||||
let keys = get_decryption_keys(&secret_cert).collect::<Vec<_>>();
|
|
||||||
if !keys.is_empty() {
|
|
||||||
if let Some(message) = messages.get_mut(&valid_cert.keyid()) {
|
|
||||||
for (pkesk, key) in message.pkesks.iter_mut().zip(keys) {
|
|
||||||
pkesk.set_recipient(key.keyid());
|
|
||||||
}
|
|
||||||
// we have a pkesk, decrypt via keyring
|
|
||||||
decrypted_messages.insert(
|
|
||||||
valid_cert.keyid(),
|
|
||||||
message.decrypt_with(&policy, &mut keyring)?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean decrypted messages from encrypted messages
|
// clean decrypted messages from encrypted messages
|
||||||
messages.retain(|k, _v| !decrypted_messages.contains_key(k));
|
messages.retain(|k, _v| !decrypted_messages.contains_key(k));
|
||||||
|
|
||||||
let left_from_threshold = threshold as usize - decrypted_messages.len();
|
let left_from_threshold = threshold as usize - decrypted_messages.len();
|
||||||
if left_from_threshold > 0 {
|
if left_from_threshold > 0 {
|
||||||
let mut remaining_usable_certs = certs
|
let new_messages = decrypt_with_manager(
|
||||||
.iter()
|
left_from_threshold as u8,
|
||||||
.filter(|cert| messages.contains_key(&cert.keyid()))
|
&mut messages,
|
||||||
.collect::<Vec<_>>();
|
&certs,
|
||||||
|
policy,
|
||||||
while threshold as usize - decrypted_messages.len() > 0 {
|
&mut manager,
|
||||||
remaining_usable_certs.retain(|cert| messages.contains_key(&cert.keyid()));
|
)?;
|
||||||
let mut key_by_fingerprints = HashMap::new();
|
decrypted_messages.extend(new_messages.into_iter());
|
||||||
let mut total_fingerprints = vec![];
|
|
||||||
for valid_cert in remaining_usable_certs
|
|
||||||
.iter()
|
|
||||||
.map(|cert| cert.with_policy(&policy, None))
|
|
||||||
{
|
|
||||||
let valid_cert = valid_cert.map_err(Error::Sequoia)?;
|
|
||||||
let fp = valid_cert
|
|
||||||
.keys()
|
|
||||||
.for_storage_encryption()
|
|
||||||
.map(|k| k.fingerprint())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
for fp in &fp {
|
|
||||||
key_by_fingerprints.insert(fp.clone(), valid_cert.keyid());
|
|
||||||
}
|
|
||||||
total_fingerprints.extend(fp.iter().cloned());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message
|
|
||||||
if let Some(fp) = manager.load_any_fingerprint(total_fingerprints)? {
|
|
||||||
// soundness: `key_by_fingerprints` is extended by the same fps that are then
|
|
||||||
// inserted into `total_fingerprints`
|
|
||||||
let cert_keyid = key_by_fingerprints.get(&fp).unwrap().clone();
|
|
||||||
let message = messages.remove(&cert_keyid);
|
|
||||||
if let Some(message) = message {
|
|
||||||
let message = message.decrypt_with(&policy, &mut manager)?;
|
|
||||||
decrypted_messages.insert(cert_keyid, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let shares = decrypted_messages
|
let shares = decrypted_messages
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{collections::HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use keyfork_prompt::{Error as PromptError, DefaultPromptManager, default_prompt_manager};
|
use keyfork_prompt::{default_prompt_manager, DefaultPromptManager, Error as PromptError};
|
||||||
|
|
||||||
use super::openpgp::{
|
use super::openpgp::{
|
||||||
self,
|
self,
|
||||||
|
@ -65,6 +65,7 @@ pub struct SmartcardManager {
|
||||||
current_card: Option<Card<Open>>,
|
current_card: Option<Card<Open>>,
|
||||||
root: Option<Cert>,
|
root: Option<Cert>,
|
||||||
pm: DefaultPromptManager,
|
pm: DefaultPromptManager,
|
||||||
|
pin_cache: HashMap<Fingerprint, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SmartcardManager {
|
impl SmartcardManager {
|
||||||
|
@ -73,6 +74,7 @@ impl SmartcardManager {
|
||||||
current_card: None,
|
current_card: None,
|
||||||
root: None,
|
root: None,
|
||||||
pm: default_prompt_manager()?,
|
pm: default_prompt_manager()?,
|
||||||
|
pin_cache: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,12 +219,13 @@ impl DecryptionHelper for &mut SmartcardManager {
|
||||||
.application_identifier()
|
.application_identifier()
|
||||||
.context("Could not load application identifier")?
|
.context("Could not load application identifier")?
|
||||||
.ident();
|
.ident();
|
||||||
let mut pin = None;
|
let mut pin = self.pin_cache.get(&fp).cloned();
|
||||||
while transaction
|
while transaction
|
||||||
.pw_status_bytes()
|
.pw_status_bytes()
|
||||||
.map_err(Error::PwStatusBytes)?
|
.map_err(Error::PwStatusBytes)?
|
||||||
.err_count_pw1()
|
.err_count_pw1()
|
||||||
> 0
|
> 0
|
||||||
|
&& pin.is_none()
|
||||||
{
|
{
|
||||||
transaction.reload_ard()?;
|
transaction.reload_ard()?;
|
||||||
let attempts = transaction
|
let attempts = transaction
|
||||||
|
@ -236,12 +239,11 @@ impl DecryptionHelper for &mut SmartcardManager {
|
||||||
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.pm.prompt_passphrase(&message)?;
|
let temp_pin = self.pm.prompt_passphrase(&message)?;
|
||||||
let verification_status =
|
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
|
||||||
transaction.verify_user_pin(temp_pin.as_str().trim());
|
|
||||||
match verification_status {
|
match verification_status {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
self.pin_cache.insert(fp.clone(), temp_pin.clone());
|
||||||
pin.replace(temp_pin);
|
pin.replace(temp_pin);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => {
|
Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => {
|
||||||
self.pm.prompt_message("Invalid PIN length entered.")?;
|
self.pm.prompt_message("Invalid PIN length entered.")?;
|
||||||
|
|
Loading…
Reference in New Issue