diff --git a/Cargo.lock b/Cargo.lock index ef81d5a..d6e8d20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,18 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" +[[package]] +name = "ahash" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -343,6 +355,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.48", + "which", +] + [[package]] name = "bindgen" version = "0.68.1" @@ -360,7 +395,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.48", ] [[package]] @@ -453,6 +488,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.5.0" @@ -598,7 +639,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -607,6 +648,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.0" @@ -742,7 +789,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -1091,7 +1138,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -1130,6 +1177,34 @@ dependencies = [ "slab", ] +[[package]] +name = "g2gen" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2c7625b2fc250dd90b63f7887a6bb0f7ec1d714c8278415bea2669ef20820e" +dependencies = [ + "g2poly", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "g2p" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc36d9bdc3d2da057775a9f4fa7d7b09edab3e0eda7a92cc353358fa63b8519e" +dependencies = [ + "g2gen", + "g2poly", +] + +[[package]] +name = "g2poly" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af6a86e750338603ea2c14b1c0bfe58cd61f87ca67a0021d9334996024608e12" + [[package]] name = "generic-array" version = "0.14.7" @@ -1205,7 +1280,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" dependencies = [ - "ahash", + "ahash 0.4.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.7", ] [[package]] @@ -1262,6 +1346,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "iana-time-zone" version = "0.1.59" @@ -1295,6 +1388,20 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "jpeg-decoder", + "num-rational", + "num-traits", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -1382,6 +1489,12 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + [[package]] name = "js-sys" version = "0.3.66" @@ -1539,6 +1652,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "keyfork-qrcode" +version = "0.1.0" +dependencies = [ + "image", + "rqrr", + "thiserror", + "v4l", +] + [[package]] name = "keyfork-shard" version = "0.1.0" @@ -1551,6 +1674,7 @@ dependencies = [ "keyfork-derive-openpgp", "keyfork-mnemonic-util", "keyfork-prompt", + "keyfork-qrcode", "openpgp-card", "openpgp-card-sequoia", "sequoia-openpgp", @@ -1735,6 +1859,15 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" +dependencies = [ + "hashbrown 0.13.2", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1802,7 +1935,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b495053a10a19a80e3a26bf1212e92e29350797b5f5bdc58268c3f3f818e66ec" dependencies = [ - "bindgen", + "bindgen 0.68.1", "cc", "libc", "pkg-config", @@ -1874,6 +2007,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -2060,7 +2204,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -2184,6 +2328,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn 2.0.48", +] + [[package]] name = "proc-macro2" version = "1.0.76" @@ -2357,6 +2511,17 @@ dependencies = [ "digest", ] +[[package]] +name = "rqrr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a8b87d1f9f69bb1a6c77e20fd303f9617b2b68dcff87cd9bcbfff2ced4b8a0b" +dependencies = [ + "g2p", + "image", + "lru", +] + [[package]] name = "rsa" version = "0.8.2" @@ -2511,7 +2676,7 @@ checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -2547,7 +2712,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -2739,6 +2904,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.48" @@ -2811,7 +2987,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -2876,7 +3052,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -2927,7 +3103,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -3034,6 +3210,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "v4l" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" +dependencies = [ + "bitflags 1.3.2", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" +dependencies = [ + "bindgen 0.65.1", +] + [[package]] name = "valuable" version = "0.1.0" @@ -3091,7 +3287,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -3125,7 +3321,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3146,6 +3342,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.28", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3336,6 +3544,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "zeroize" version = "1.7.0" @@ -3353,5 +3581,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] diff --git a/Cargo.toml b/Cargo.toml index 4c12b52..e69d0af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,14 @@ members = [ "keyfork-plumbing", "keyfork-shard", "keyfork-slip10-test-data", + "keyfork-qrcode", "keyforkd", "keyforkd-client", "keyforkd-models", "smex", ] + +[profile.dev.package.keyfork-qrcode] +opt-level = 3 +debug = true + diff --git a/keyfork-prompt/Cargo.toml b/keyfork-prompt/Cargo.toml index d859fc0..4bd1a07 100644 --- a/keyfork-prompt/Cargo.toml +++ b/keyfork-prompt/Cargo.toml @@ -9,9 +9,8 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["mnemonic", "qrencode"] +default = ["mnemonic"] mnemonic = ["keyfork-mnemonic-util"] -qrencode = [] [dependencies] keyfork-crossterm = { version = "0.27.1", path = "../keyfork-crossterm", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] } diff --git a/keyfork-prompt/src/lib.rs b/keyfork-prompt/src/lib.rs index 102eae4..c14a4ae 100644 --- a/keyfork-prompt/src/lib.rs +++ b/keyfork-prompt/src/lib.rs @@ -7,9 +7,6 @@ pub mod terminal; pub mod validators; pub use terminal::{Terminal, DefaultTerminal, default_terminal}; -#[cfg(feature = "qrencode")] -pub mod qrencode; - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("The given handler is not a TTY")] diff --git a/keyfork-prompt/src/qrencode.rs b/keyfork-prompt/src/qrencode.rs deleted file mode 100644 index eedfd9c..0000000 --- a/keyfork-prompt/src/qrencode.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::{ - io::Write, - process::{Command, Stdio}, -}; - -#[derive(thiserror::Error, Debug)] -pub enum QrGenerationError { - #[error("{0}")] - Io(#[from] std::io::Error), - - #[error("{0}")] - StringParse(#[from] std::string::FromUtf8Error) -} - -/// Generate a terminal-printable QR code for a given string. Uses the `qrencode` CLI utility. -pub fn qrencode(text: &str) -> Result { - let mut qrencode = Command::new("qrencode") - .arg("-t") - .arg("ansiutf8") - .arg("-m") - .arg("2") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn()?; - if let Some(stdin) = qrencode.stdin.as_mut() { - stdin.write_all(text.as_bytes())?; - } - let output = qrencode.wait_with_output()?; - let result = String::from_utf8(output.stdout)?; - Ok(result) -} diff --git a/keyfork-qrcode/Cargo.toml b/keyfork-qrcode/Cargo.toml new file mode 100644 index 0000000..a6c768a --- /dev/null +++ b/keyfork-qrcode/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "keyfork-qrcode" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +image = { version = "0.24.7", default-features = false, features = ["jpeg"] } +rqrr = "0.6.0" +thiserror = "1.0.56" +v4l = "0.14.0" diff --git a/keyfork-qrcode/src/lib.rs b/keyfork-qrcode/src/lib.rs new file mode 100644 index 0000000..c6edd37 --- /dev/null +++ b/keyfork-qrcode/src/lib.rs @@ -0,0 +1,123 @@ +use image::io::Reader as ImageReader; +use rqrr::PreparedImage; +use std::{ + io::{Cursor, Write}, + time::{Duration, SystemTime}, + process::{Command, Stdio}, +}; +use v4l::{ + buffer::Type, + io::{mmap::Stream, traits::CaptureStream}, + video::Capture, + Device, FourCC, +}; + +static MJPEG: &[u8; 4] = b"MJPG"; + + +#[derive(thiserror::Error, Debug)] +pub enum QRGenerationError { + #[error("{0}")] + Io(#[from] std::io::Error), + + #[error("Could not decode output of qrencode (this is a bug!): {0}")] + StringParse(#[from] std::string::FromUtf8Error), +} + +#[derive(thiserror::Error, Debug)] +pub enum QRCodeScanError { + #[error("Camera could not use {expected} format, instead used {actual}")] + CameraGaveBadFormat { + expected: String, + actual: String, + }, + + #[error("Unable to interface with camera: {0}")] + CameraIO(#[from] std::io::Error), + + #[error("Could not decode image: {0}")] + ImageDecode(#[from] image::ImageError), + + #[error("Could not format FourCC as string (this is a bug!): {0}")] + FourCC(#[from] std::string::FromUtf8Error), +} + +#[derive(Default)] +pub enum ErrorCorrection { + #[default] + Lowest, + Medium, + Quartile, + Highest, +} + +/// Generate a terminal-printable QR code for a given string. Uses the `qrencode` CLI utility. +pub fn qrencode( + text: &str, + error_correction: impl Into>, +) -> Result { + let error_correction_arg = match error_correction.into().unwrap_or_default() { + ErrorCorrection::Lowest => "L", + ErrorCorrection::Medium => "M", + ErrorCorrection::Quartile => "Q", + ErrorCorrection::Highest => "H", + }; + + let mut qrencode = Command::new("qrencode") + .arg("-t") + .arg("ansiutf8") + .arg("-m") + .arg("2") + .arg("-l") + .arg(error_correction_arg) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + if let Some(stdin) = qrencode.stdin.as_mut() { + stdin.write_all(text.as_bytes())?; + } + let output = qrencode.wait_with_output()?; + let result = String::from_utf8(output.stdout)?; + Ok(result) +} + +pub fn scan_camera(timeout: Duration, index: usize) -> Result, QRCodeScanError> { + let device = Device::new(index)?; + + let mut format = device.format()?; + format.width = 1280; + format.height = 720; + format.fourcc = FourCC::new(MJPEG); + let format = device.set_format(&format)?; + + if MJPEG != &format.fourcc.repr { + return Err(QRCodeScanError::CameraGaveBadFormat { + expected: String::from_utf8(MJPEG.to_vec())?, + actual: String::from_utf8(format.fourcc.repr.to_vec())?, + }) + } + + let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?; + + let start = SystemTime::now(); + + while SystemTime::now() + .duration_since(start) + .unwrap_or(Duration::from_secs(0)) + < timeout + { + let (buffer, _) = stream.next()?; + let image = ImageReader::new(Cursor::new(buffer)) + .with_guessed_format()? + .decode()? + .to_luma8(); + let mut image = PreparedImage::prepare(image); + for grid in image.detect_grids() { + if let Ok((_, content)) = grid.decode() { + return Ok(Some(content)) + } + } + } + + Ok(None) +} diff --git a/keyfork-shard/Cargo.toml b/keyfork-shard/Cargo.toml index ca96a7b..f50f6be 100644 --- a/keyfork-shard/Cargo.toml +++ b/keyfork-shard/Cargo.toml @@ -7,13 +7,14 @@ license = "AGPL-3.0-only" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["openpgp", "openpgp-card"] -openpgp = ["sequoia-openpgp", "prompt", "anyhow"] +default = ["openpgp", "openpgp-card", "qrcode"] +openpgp = ["sequoia-openpgp", "anyhow"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] -prompt = ["keyfork-prompt"] +qrcode = ["keyfork-qrcode"] [dependencies] -keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true } +keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", default-features = false, features = ["mnemonic"] } +keyfork-qrcode = { version = "0.1.0", path = "../keyfork-qrcode", optional = true } smex = { version = "0.1.0", path = "../smex" } sharks = "0.5.0" diff --git a/keyfork-shard/src/lib.rs b/keyfork-shard/src/lib.rs index 16a1774..78bf936 100644 --- a/keyfork-shard/src/lib.rs +++ b/keyfork-shard/src/lib.rs @@ -7,9 +7,8 @@ use aes_gcm::{ use hkdf::Hkdf; use keyfork_mnemonic_util::{Mnemonic, Wordlist}; use keyfork_prompt::{ - qrencode, validators::{mnemonic::MnemonicSetValidator, Validator}, - Message as PromptMessage, Terminal, PromptHandler + Message as PromptMessage, PromptHandler, Terminal, }; use sha2::Sha256; use sharks::{Share, Sharks}; @@ -28,8 +27,8 @@ pub enum SharksError { } #[derive(thiserror::Error, Debug)] -#[error("Mnemonic did not store enough data")] -pub struct InvalidMnemonicData; +#[error("Mnemonic or QR code did not store enough data")] +pub struct InvalidData; /// Decrypt hunk version 1: /// 1 byte: Version @@ -58,36 +57,66 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box = None; + let mut payload_data = None; + + #[cfg(feature = "qrcode")] + { + pm.prompt_message(PromptMessage::Text( + "Press enter, then present QR code to camera".to_string(), + ))?; + if let Ok(Some(hex)) = + keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0) + { + 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 { + pm.prompt_message(PromptMessage::Text( + "Unable to detect QR code, falling back to text".to_string(), + ))?; + }; + } + + let (pubkey, payload) = match (pubkey_data, payload_data) { + (Some(pubkey), Some(payload)) => (pubkey, payload), + _ => { + let validator = MnemonicSetValidator { + word_lengths: [24, 48], + }; + + let [pubkey_mnemonic, payload_mnemonic] = + pm.prompt_validated_wordlist("Their words: ", &wordlist, 3, validator.to_fn())?; + let pubkey = pubkey_mnemonic + .entropy() + .try_into() + .map_err(|_| InvalidData)?; + let payload = payload_mnemonic.entropy(); + (pubkey, payload) + } }; - let [pubkey_mnemonic, payload_mnemonic] = - pm.prompt_validated_wordlist("Their words: ", &wordlist, 3, validator.to_fn())?; - - let their_key: [u8; 32] = pubkey_mnemonic - .entropy() - .try_into() - .map_err(|_| InvalidMnemonicData)?; - - let shared_secret = our_key - .diffie_hellman(&PublicKey::from(their_key)) - .to_bytes(); + let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes(); let hkdf = Hkdf::::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 payload = payload_mnemonic.entropy(); let payload = shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?; assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version"); diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs index 0fa088c..4cc1375 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -17,9 +17,8 @@ use keyfork_derive_openpgp::derive_util::{ }; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist}; use keyfork_prompt::{ - qrencode, validators::{mnemonic::MnemonicSetValidator, Validator}, - Error as PromptError, Message as PromptMessage, Terminal, PromptHandler, + Error as PromptError, Message as PromptMessage, PromptHandler, Terminal, }; use openpgp::{ armor::{Kind, Writer}, @@ -55,7 +54,7 @@ use smartcard::SmartcardManager; const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; -use super::{InvalidMnemonicData, SharksError, HUNK_VERSION}; +use super::{InvalidData, SharksError, HUNK_VERSION}; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding const ENC_LEN: u8 = 4 * 16; @@ -99,7 +98,7 @@ pub enum Error { MnemonicFromStr(#[from] MnemonicFromStrError), #[error("{0}")] - InvalidMnemonicData(#[from] InvalidMnemonicData), + InvalidMnemonicData(#[from] InvalidData), #[error("IO error: {0}")] Io(#[source] std::io::Error), @@ -110,6 +109,9 @@ pub enum Error { #[error("Derivation request: {0}")] DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError), + #[error("Unable to decode hex: {0}")] + HexDecode(#[from] smex::DecodeError), + #[error("Keyfork OpenPGP: {0}")] KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error), } @@ -411,26 +413,54 @@ pub fn decrypt( ) -> Result<()> { let mut pm = Terminal::new(stdin(), stdout())?; let wordlist = Wordlist::default(); - let validator = MnemonicSetValidator { - word_lengths: [9, 24], - }; - let [nonce_mnemonic, pubkey_mnemonic] = - pm.prompt_validated_wordlist("Their words: ", &wordlist, 3, validator.to_fn())?; - let their_key: [u8; 32] = pubkey_mnemonic - .entropy() - .try_into() - .map_err(|_| InvalidMnemonicData)?; - let their_nonce = nonce_mnemonic.entropy(); - let their_nonce = Nonce::::from_slice(&their_nonce); + let mut nonce_data: Option<[u8; 12]> = None; + let mut pubkey_data: Option<[u8; 32]> = None; + + #[cfg(feature = "qrcode")] + { + pm.prompt_message(PromptMessage::Text( + "Press enter, then present QR code to camera".to_string(), + ))?; + if let Ok(Some(hex)) = keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0) { + let decoded_data = smex::decode(&hex)?; + let _ = nonce_data.insert(decoded_data[..12].try_into().map_err(|_| InvalidData)?); + let _ = pubkey_data.insert(decoded_data[12..].try_into().map_err(|_| InvalidData)?); + } else { + pm.prompt_message(PromptMessage::Text( + "Unable to detect QR code, falling back to text".to_string(), + ))?; + }; + } + + let (nonce, pubkey) = match (nonce_data, pubkey_data) { + (Some(nonce), Some(pubkey)) => (nonce, pubkey), + _ => { + let validator = MnemonicSetValidator { + word_lengths: [9, 24], + }; + let [nonce_mnemonic, pubkey_mnemonic] = + pm.prompt_validated_wordlist("Their words: ", &wordlist, 3, validator.to_fn())?; + + let nonce = nonce_mnemonic + .entropy() + .try_into() + .map_err(|_| InvalidData)?; + let pubkey = pubkey_mnemonic + .entropy() + .try_into() + .map_err(|_| InvalidData)?; + (nonce, pubkey) + } + }; + + let nonce = Nonce::::from_slice(&nonce); let our_key = EphemeralSecret::random(); - let our_mnemonic = + let our_pubkey_mnemonic = Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; - let shared_secret = our_key - .diffie_hellman(&PublicKey::from(their_key)) - .to_bytes(); + let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes(); let (mut share, threshold, ..) = decrypt_one(encrypted_messages.to_vec(), certs, metadata)?; share.insert(0, HUNK_VERSION); @@ -445,8 +475,8 @@ pub fn decrypt( hkdf.expand(&[], &mut hkdf_output)?; let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?; - let bytes = shared_key.encrypt(their_nonce, share.as_slice())?; - shared_key.decrypt(their_nonce, &bytes[..])?; + let bytes = shared_key.encrypt(nonce, share.as_slice())?; + shared_key.decrypt(nonce, &bytes[..])?; // NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX // NOTE: This previously used a single value as the padding byte, but resulted in @@ -473,17 +503,22 @@ pub fn decrypt( } // safety: size of out_bytes is constant and always % 4 == 0 - let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; - let combined_mnemonic = format!("{our_mnemonic} {mnemonic}"); + let payload_mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; + + #[cfg(feature = "qrcode")] + { + use keyfork_qrcode::{qrencode, ErrorCorrection}; + let mut qrcode_data = our_pubkey_mnemonic.entropy(); + qrcode_data.extend(payload_mnemonic.entropy()); + if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Lowest) { + pm.prompt_message(PromptMessage::Data(qrcode))?; + } + } pm.prompt_message(PromptMessage::Text(format!( - "Our words: {combined_mnemonic}" + "Our words: {our_pubkey_mnemonic} {payload_mnemonic}" )))?; - if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) { - pm.prompt_message(PromptMessage::Data(qrcode))?; - } - Ok(()) }