keyfork-shard: add keyfork-qrcode

This commit is contained in:
Ryan Heywood 2024-01-11 19:49:56 -05:00
parent b8c1fc1a93
commit 2220faf865
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
10 changed files with 506 additions and 107 deletions

258
Cargo.lock generated
View File

@ -58,6 +58,18 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.2" version = "1.1.2"
@ -343,6 +355,29 @@ dependencies = [
"serde", "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]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.68.1" version = "0.68.1"
@ -360,7 +395,7 @@ dependencies = [
"regex", "regex",
"rustc-hash", "rustc-hash",
"shlex", "shlex",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -453,6 +488,12 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytemuck"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -598,7 +639,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -607,6 +648,12 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.0" version = "1.0.0"
@ -742,7 +789,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1091,7 +1138,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1130,6 +1177,34 @@ dependencies = [
"slab", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@ -1205,7 +1280,16 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
dependencies = [ 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]] [[package]]
@ -1262,6 +1346,15 @@ dependencies = [
"digest", "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]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.59" version = "0.1.59"
@ -1295,6 +1388,20 @@ dependencies = [
"unicode-normalization", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.1.0" version = "2.1.0"
@ -1382,6 +1489,12 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.66" version = "0.3.66"
@ -1539,6 +1652,16 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "keyfork-qrcode"
version = "0.1.0"
dependencies = [
"image",
"rqrr",
"thiserror",
"v4l",
]
[[package]] [[package]]
name = "keyfork-shard" name = "keyfork-shard"
version = "0.1.0" version = "0.1.0"
@ -1551,6 +1674,7 @@ dependencies = [
"keyfork-derive-openpgp", "keyfork-derive-openpgp",
"keyfork-mnemonic-util", "keyfork-mnemonic-util",
"keyfork-prompt", "keyfork-prompt",
"keyfork-qrcode",
"openpgp-card", "openpgp-card",
"openpgp-card-sequoia", "openpgp-card-sequoia",
"sequoia-openpgp", "sequoia-openpgp",
@ -1735,6 +1859,15 @@ dependencies = [
"value-bag", "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]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -1802,7 +1935,7 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b495053a10a19a80e3a26bf1212e92e29350797b5f5bdc58268c3f3f818e66ec" checksum = "b495053a10a19a80e3a26bf1212e92e29350797b5f5bdc58268c3f3f818e66ec"
dependencies = [ dependencies = [
"bindgen", "bindgen 0.68.1",
"cc", "cc",
"libc", "libc",
"pkg-config", "pkg-config",
@ -1874,6 +2007,17 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.17" version = "0.2.17"
@ -2060,7 +2204,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2184,6 +2328,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.76" version = "1.0.76"
@ -2357,6 +2511,17 @@ dependencies = [
"digest", "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]] [[package]]
name = "rsa" name = "rsa"
version = "0.8.2" version = "0.8.2"
@ -2511,7 +2676,7 @@ checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2547,7 +2712,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2739,6 +2904,17 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 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]] [[package]]
name = "syn" name = "syn"
version = "2.0.48" version = "2.0.48"
@ -2811,7 +2987,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2876,7 +3052,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2927,7 +3103,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3034,6 +3210,26 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 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]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"
@ -3091,7 +3287,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3125,7 +3321,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3146,6 +3342,18 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -3336,6 +3544,26 @@ dependencies = [
"linked-hash-map", "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]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.7.0" version = "1.7.0"
@ -3353,5 +3581,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]

View File

@ -15,8 +15,14 @@ members = [
"keyfork-plumbing", "keyfork-plumbing",
"keyfork-shard", "keyfork-shard",
"keyfork-slip10-test-data", "keyfork-slip10-test-data",
"keyfork-qrcode",
"keyforkd", "keyforkd",
"keyforkd-client", "keyforkd-client",
"keyforkd-models", "keyforkd-models",
"smex", "smex",
] ]
[profile.dev.package.keyfork-qrcode]
opt-level = 3
debug = true

View File

@ -9,9 +9,8 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["mnemonic", "qrencode"] default = ["mnemonic"]
mnemonic = ["keyfork-mnemonic-util"] mnemonic = ["keyfork-mnemonic-util"]
qrencode = []
[dependencies] [dependencies]
keyfork-crossterm = { version = "0.27.1", path = "../keyfork-crossterm", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] } keyfork-crossterm = { version = "0.27.1", path = "../keyfork-crossterm", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] }

View File

@ -7,9 +7,6 @@ pub mod terminal;
pub mod validators; pub mod validators;
pub use terminal::{Terminal, DefaultTerminal, default_terminal}; pub use terminal::{Terminal, DefaultTerminal, default_terminal};
#[cfg(feature = "qrencode")]
pub mod qrencode;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
#[error("The given handler is not a TTY")] #[error("The given handler is not a TTY")]

View File

@ -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<String, QrGenerationError> {
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)
}

12
keyfork-qrcode/Cargo.toml Normal file
View File

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

123
keyfork-qrcode/src/lib.rs Normal file
View File

@ -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<Option<ErrorCorrection>>,
) -> Result<String, QRGenerationError> {
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<Option<String>, 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)
}

View File

@ -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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["openpgp", "openpgp-card"] default = ["openpgp", "openpgp-card", "qrcode"]
openpgp = ["sequoia-openpgp", "prompt", "anyhow"] openpgp = ["sequoia-openpgp", "anyhow"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
prompt = ["keyfork-prompt"] qrcode = ["keyfork-qrcode"]
[dependencies] [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" } smex = { version = "0.1.0", path = "../smex" }
sharks = "0.5.0" sharks = "0.5.0"

View File

@ -7,9 +7,8 @@ use aes_gcm::{
use hkdf::Hkdf; use hkdf::Hkdf;
use keyfork_mnemonic_util::{Mnemonic, Wordlist}; use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use keyfork_prompt::{ use keyfork_prompt::{
qrencode,
validators::{mnemonic::MnemonicSetValidator, Validator}, validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, Terminal, PromptHandler Message as PromptMessage, PromptHandler, Terminal,
}; };
use sha2::Sha256; use sha2::Sha256;
use sharks::{Share, Sharks}; use sharks::{Share, Sharks};
@ -28,8 +27,8 @@ pub enum SharksError {
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[error("Mnemonic did not store enough data")] #[error("Mnemonic or QR code did not store enough data")]
pub struct InvalidMnemonicData; pub struct InvalidData;
/// Decrypt hunk version 1: /// Decrypt hunk version 1:
/// 1 byte: Version /// 1 byte: Version
@ -58,36 +57,66 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let key_mnemonic = let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let combined_mnemonic = format!("{nonce_mnemonic} {key_mnemonic}");
pm.prompt_message(PromptMessage::Text(format!(
"Our words: {combined_mnemonic}"
)))?;
if let Ok(qrcode) = qrencode::qrencode(&combined_mnemonic) { #[cfg(feature = "qrcode")]
pm.prompt_message(PromptMessage::Data(qrcode))?; {
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = nonce_mnemonic.entropy();
qrcode_data.extend(key_mnemonic.entropy());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Medium) {
pm.prompt_message(PromptMessage::Data(qrcode))?;
}
} }
let validator = MnemonicSetValidator { pm.prompt_message(PromptMessage::Text(format!(
word_lengths: [24, 48], "Our words: {nonce_mnemonic} {key_mnemonic}"
)))?;
let mut pubkey_data: Option<[u8; 32]> = 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] = let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
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 hkdf = Hkdf::<Sha256>::new(None, &shared_secret); let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8]; let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?; hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?; let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let payload = payload_mnemonic.entropy();
let payload = let payload =
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?; shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version"); assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");

View File

@ -17,9 +17,8 @@ use keyfork_derive_openpgp::derive_util::{
}; };
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist}; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist};
use keyfork_prompt::{ use keyfork_prompt::{
qrencode,
validators::{mnemonic::MnemonicSetValidator, Validator}, validators::{mnemonic::MnemonicSetValidator, Validator},
Error as PromptError, Message as PromptMessage, Terminal, PromptHandler, Error as PromptError, Message as PromptMessage, PromptHandler, Terminal,
}; };
use openpgp::{ use openpgp::{
armor::{Kind, Writer}, armor::{Kind, Writer},
@ -55,7 +54,7 @@ use smartcard::SmartcardManager;
const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_VERSION: u8 = 1;
const SHARD_METADATA_OFFSET: usize = 2; 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 // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
const ENC_LEN: u8 = 4 * 16; const ENC_LEN: u8 = 4 * 16;
@ -99,7 +98,7 @@ pub enum Error {
MnemonicFromStr(#[from] MnemonicFromStrError), MnemonicFromStr(#[from] MnemonicFromStrError),
#[error("{0}")] #[error("{0}")]
InvalidMnemonicData(#[from] InvalidMnemonicData), InvalidMnemonicData(#[from] InvalidData),
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[source] std::io::Error), Io(#[source] std::io::Error),
@ -110,6 +109,9 @@ pub enum Error {
#[error("Derivation request: {0}")] #[error("Derivation request: {0}")]
DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError), DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError),
#[error("Unable to decode hex: {0}")]
HexDecode(#[from] smex::DecodeError),
#[error("Keyfork OpenPGP: {0}")] #[error("Keyfork OpenPGP: {0}")]
KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error), KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error),
} }
@ -411,26 +413,54 @@ pub fn decrypt(
) -> Result<()> { ) -> Result<()> {
let mut pm = Terminal::new(stdin(), stdout())?; let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default(); 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 let mut nonce_data: Option<[u8; 12]> = None;
.entropy() let mut pubkey_data: Option<[u8; 32]> = None;
.try_into()
.map_err(|_| InvalidMnemonicData)?; #[cfg(feature = "qrcode")]
let their_nonce = nonce_mnemonic.entropy(); {
let their_nonce = Nonce::<U12>::from_slice(&their_nonce); 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::<U12>::from_slice(&nonce);
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let our_mnemonic = let our_pubkey_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let shared_secret = our_key let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
.diffie_hellman(&PublicKey::from(their_key))
.to_bytes();
let (mut share, threshold, ..) = decrypt_one(encrypted_messages.to_vec(), certs, metadata)?; let (mut share, threshold, ..) = decrypt_one(encrypted_messages.to_vec(), certs, metadata)?;
share.insert(0, HUNK_VERSION); share.insert(0, HUNK_VERSION);
@ -445,8 +475,8 @@ pub fn decrypt(
hkdf.expand(&[], &mut hkdf_output)?; hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?; let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let bytes = shared_key.encrypt(their_nonce, share.as_slice())?; let bytes = shared_key.encrypt(nonce, share.as_slice())?;
shared_key.decrypt(their_nonce, &bytes[..])?; shared_key.decrypt(nonce, &bytes[..])?;
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX // 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 // 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 // safety: size of out_bytes is constant and always % 4 == 0
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; let payload_mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
let combined_mnemonic = format!("{our_mnemonic} {mnemonic}");
#[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!( 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(()) Ok(())
} }