Compare commits

..

7 Commits

8 changed files with 454 additions and 115 deletions

117
Cargo.lock generated
View File

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

View File

@ -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 })

View File

@ -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}");

View File

@ -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(())

View File

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

View File

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

View File

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

View File

@ -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.")?;