Skip to content

Commit

Permalink
Merge pull request #17 from Zeegomo/port-kedqr
Browse files Browse the repository at this point in the history
port kedqr to catalyst-toolbox
  • Loading branch information
Mikhail Zabaluev committed Jul 20, 2021
2 parents 31d23b9 + caa4471 commit 6ade8c0
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 0 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Expand Up @@ -38,6 +38,11 @@ serde_yaml = "0.8.17"
sscanf = "0.1"
thiserror = "1.0"
url = "2.2"
hex = "0.4"
image = "0.23.12"
qrcode = "0.12"
quircs = "0.10.0"
symmetric-cipher = { git = "https://github.com/input-output-hk/chain-wallet-libs.git", branch = "master" }

[dev-dependencies]
rand_chacha = "0.3"
Expand Down
71 changes: 71 additions & 0 deletions src/bin/cli/kedqr/mod.rs
@@ -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(())
}
}
4 changes: 4 additions & 0 deletions src/bin/cli/mod.rs
@@ -1,3 +1,4 @@
mod kedqr;
mod logs;
mod notifications;
mod recovery;
Expand Down Expand Up @@ -34,6 +35,8 @@ pub enum CatalystCommand {
Recover(recovery::Recover),
/// Download, compare and get stats from sentry and persistent fragment logs
Logs(logs::Logs),
/// Generate qr codes
QrCode(kedqr::QrCodeCmd),
}

impl Cli {
Expand All @@ -60,6 +63,7 @@ impl CatalystCommand {
PushNotification(notifications) => notifications.exec()?,
Recover(recover) => recover.exec()?,
Logs(logs) => logs.exec()?,
QrCode(kedqr) => kedqr.exec()?,
};
Ok(())
}
Expand Down
206 changes: 206 additions & 0 deletions src/kedqr/mod.rs
@@ -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()
);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
@@ -1,3 +1,4 @@
pub mod kedqr;
pub mod logs;
pub mod notifications;
pub mod recovery;
Expand Down

0 comments on commit 6ade8c0

Please sign in to comment.