-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from Zeegomo/port-kedqr
port kedqr to catalyst-toolbox
- Loading branch information
Showing
6 changed files
with
308 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
use catalyst_toolbox::kedqr::{KeyQrCode, QrPin}; | ||
use chain_crypto::bech32::Bech32; | ||
use chain_crypto::{Ed25519Extended, SecretKey}; | ||
use std::{ | ||
error::Error, | ||
fs::OpenOptions, | ||
io::{BufRead, BufReader}, | ||
path::PathBuf, | ||
}; | ||
use structopt::StructOpt; | ||
|
||
/// QCode CLI toolkit | ||
#[derive(Debug, PartialEq, StructOpt)] | ||
#[structopt(rename_all = "kebab-case")] | ||
pub struct QrCodeCmd { | ||
/// Path to file containing ed25519extended bech32 value. | ||
#[structopt(short, long, parse(from_os_str))] | ||
input: PathBuf, | ||
/// Path to file to save qr code output, if not provided console output will be attempted. | ||
#[structopt(short, long, parse(from_os_str))] | ||
output: Option<PathBuf>, | ||
/// Pin code. 4-digit number is used on Catalyst. | ||
#[structopt(short, long, parse(try_from_str))] | ||
pin: QrPin, | ||
} | ||
|
||
impl QrCodeCmd { | ||
pub fn exec(self) -> Result<(), Box<dyn Error>> { | ||
let QrCodeCmd { input, output, pin } = self; | ||
// open input key and parse it | ||
let key_file = OpenOptions::new() | ||
.create(false) | ||
.read(true) | ||
.write(false) | ||
.append(false) | ||
.open(&input) | ||
.expect("Could not open input file."); | ||
|
||
let mut reader = BufReader::new(key_file); | ||
let mut key_str = String::new(); | ||
let _key_len = reader | ||
.read_line(&mut key_str) | ||
.expect("Could not read input file."); | ||
let sk = key_str.trim_end().to_string(); | ||
|
||
let secret_key: SecretKey<Ed25519Extended> = | ||
SecretKey::try_from_bech32_str(&sk).expect("Malformed secret key."); | ||
// use parsed pin from args | ||
let pwd = pin.password; | ||
// generate qrcode with key and parsed pin | ||
let qr = KeyQrCode::generate(secret_key, &pwd); | ||
// process output | ||
match output { | ||
Some(path) => { | ||
// save qr code to file, or print to stdout if it fails | ||
let img = qr.to_img(); | ||
if let Err(e) = img.save(path) { | ||
println!("Error: {}", e); | ||
println!(); | ||
println!("{}", qr); | ||
} | ||
} | ||
None => { | ||
// prints qr code to stdout when no path is specified | ||
println!(); | ||
println!("{}", qr); | ||
} | ||
} | ||
Ok(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
use chain_crypto::{Ed25519Extended, SecretKey, SecretKeyError}; | ||
use image::{DynamicImage, ImageBuffer, Luma}; | ||
use qrcode::{ | ||
render::{svg, unicode}, | ||
EcLevel, QrCode, | ||
}; | ||
use std::fmt; | ||
use std::fs::File; | ||
use std::io::{self, prelude::*}; | ||
use std::path::Path; | ||
use std::str::FromStr; | ||
use symmetric_cipher::{decrypt, encrypt, Error as SymmetricCipherError}; | ||
use thiserror::Error; | ||
|
||
pub const PIN_LENGTH: usize = 4; | ||
|
||
pub struct KeyQrCode { | ||
inner: QrCode, | ||
} | ||
|
||
#[derive(Error, Debug)] | ||
pub enum KeyQrCodeError { | ||
#[error("encryption-decryption protocol error")] | ||
SymmetricCipher(#[from] SymmetricCipherError), | ||
#[error("io error")] | ||
Io(#[from] io::Error), | ||
#[error("invalid secret key")] | ||
SecretKey(#[from] SecretKeyError), | ||
#[error("couldn't decode QR code")] | ||
QrDecodeError(#[from] QrDecodeError), | ||
#[error("failed to decode hex")] | ||
HexDecodeError(#[from] hex::FromHexError), | ||
} | ||
|
||
#[derive(Error, Debug)] | ||
pub enum QrDecodeError { | ||
#[error("couldn't decode QR code")] | ||
DecodeError(#[from] quircs::DecodeError), | ||
#[error("couldn't extract QR code")] | ||
ExtractError(#[from] quircs::ExtractError), | ||
#[error("QR code payload is not valid uf8")] | ||
NonUtf8Payload, | ||
} | ||
|
||
impl KeyQrCode { | ||
pub fn generate(key: SecretKey<Ed25519Extended>, password: &[u8]) -> Self { | ||
let secret = key.leak_secret(); | ||
let rng = rand::thread_rng(); | ||
// this won't fail because we already know it's an ed25519extended key, | ||
// so it is safe to unwrap | ||
let enc = encrypt(password, secret.as_ref(), rng).unwrap(); | ||
// Using binary would make the QR codes more compact and probably less | ||
// prone to scanning errors. | ||
let enc_hex = hex::encode(enc); | ||
let inner = QrCode::with_error_correction_level(&enc_hex, EcLevel::H).unwrap(); | ||
|
||
KeyQrCode { inner } | ||
} | ||
|
||
pub fn write_svg(&self, path: impl AsRef<Path>) -> Result<(), KeyQrCodeError> { | ||
let mut out = File::create(path)?; | ||
let svg_file = self | ||
.inner | ||
.render() | ||
.quiet_zone(true) | ||
.dark_color(svg::Color("#000000")) | ||
.light_color(svg::Color("#ffffff")) | ||
.build(); | ||
out.write_all(svg_file.as_bytes())?; | ||
out.flush()?; | ||
Ok(()) | ||
} | ||
|
||
pub fn to_img(&self) -> ImageBuffer<Luma<u8>, Vec<u8>> { | ||
let qr = &self.inner; | ||
let img = qr.render::<Luma<u8>>().build(); | ||
img | ||
} | ||
|
||
pub fn decode( | ||
img: DynamicImage, | ||
password: &[u8], | ||
) -> Result<Vec<SecretKey<Ed25519Extended>>, KeyQrCodeError> { | ||
let mut decoder = quircs::Quirc::default(); | ||
|
||
let img = img.into_luma8(); | ||
|
||
let codes = decoder.identify(img.width() as usize, img.height() as usize, &img); | ||
|
||
codes | ||
.map(|code| -> Result<_, KeyQrCodeError> { | ||
let decoded = code | ||
.map_err(QrDecodeError::ExtractError) | ||
.and_then(|c| c.decode().map_err(QrDecodeError::DecodeError))?; | ||
|
||
// TODO: I actually don't know if this can fail | ||
let h = std::str::from_utf8(&decoded.payload) | ||
.map_err(|_| QrDecodeError::NonUtf8Payload)?; | ||
let encrypted_bytes = hex::decode(h)?; | ||
let key = decrypt(password, &encrypted_bytes)?; | ||
Ok(SecretKey::from_binary(&key)?) | ||
}) | ||
.collect() | ||
} | ||
} | ||
|
||
impl fmt::Display for KeyQrCode { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
let qr_img = self | ||
.inner | ||
.render::<unicode::Dense1x2>() | ||
.quiet_zone(true) | ||
.dark_color(unicode::Dense1x2::Light) | ||
.light_color(unicode::Dense1x2::Dark) | ||
.build(); | ||
write!(f, "{}", qr_img) | ||
} | ||
} | ||
|
||
#[derive(Debug, PartialEq)] | ||
pub struct QrPin { | ||
pub password: [u8; 4], | ||
} | ||
|
||
#[derive(Error, Debug)] | ||
|
||
pub enum BadPinError { | ||
#[error("The PIN must consist of {PIN_LENGTH} digits, found {0}")] | ||
InvalidLength(usize), | ||
#[error("Invalid digit {0}")] | ||
InvalidDigit(char), | ||
} | ||
|
||
impl FromStr for QrPin { | ||
type Err = BadPinError; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
if s.chars().count() != PIN_LENGTH { | ||
return Err(BadPinError::InvalidLength(s.len())); | ||
} | ||
|
||
let mut pwd = [0u8; 4]; | ||
for (i, digit) in s.chars().enumerate() { | ||
pwd[i] = digit.to_digit(10).ok_or(BadPinError::InvalidDigit(digit))? as u8; | ||
} | ||
Ok(QrPin { password: pwd }) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn parse_pin_successfully() { | ||
for (pin, pwd) in &[ | ||
("0000", [0, 0, 0, 0]), | ||
("1123", [1, 1, 2, 3]), | ||
("0002", [0, 0, 0, 2]), | ||
] { | ||
let qr_pin = QrPin::from_str(pin).unwrap(); | ||
assert_eq!(qr_pin, QrPin { password: *pwd }) | ||
} | ||
} | ||
#[test] | ||
fn pins_that_do_not_satisfy_length_reqs_return_error() { | ||
for bad_pin in &["", "1", "11", "111", "11111"] { | ||
let qr_pin = QrPin::from_str(bad_pin); | ||
assert!(qr_pin.is_err(),) | ||
} | ||
} | ||
|
||
#[test] | ||
fn pins_that_do_not_satisfy_content_reqs_return_error() { | ||
for bad_pin in &[" ", " 111", "llll", "000u"] { | ||
let qr_pin = QrPin::from_str(bad_pin); | ||
assert!(qr_pin.is_err(),) | ||
} | ||
} | ||
|
||
// TODO: Improve into an integration test using a temporary directory. | ||
// Leaving here as an example. | ||
#[test] | ||
fn generate_svg() { | ||
const PASSWORD: &[u8] = &[1, 2, 3, 4]; | ||
let sk = SecretKey::generate(rand::thread_rng()); | ||
let qr = KeyQrCode::generate(sk, PASSWORD); | ||
qr.write_svg("qr-code.svg").unwrap(); | ||
} | ||
|
||
#[test] | ||
fn encode_decode() { | ||
const PASSWORD: &[u8] = &[1, 2, 3, 4]; | ||
let sk = SecretKey::generate(rand::thread_rng()); | ||
let qr = KeyQrCode::generate(sk.clone(), PASSWORD); | ||
let img = qr.to_img(); | ||
// img.save("qr.png").unwrap(); | ||
assert_eq!( | ||
sk.leak_secret().as_ref(), | ||
KeyQrCode::decode(DynamicImage::ImageLuma8(img), PASSWORD).unwrap()[0] | ||
.clone() | ||
.leak_secret() | ||
.as_ref() | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
pub mod kedqr; | ||
pub mod logs; | ||
pub mod notifications; | ||
pub mod recovery; | ||
|