Skip to content

Commit

Permalink
Split kedqr command into hash and img
Browse files Browse the repository at this point in the history
  • Loading branch information
dkijania authored and zeegomo committed Oct 11, 2021
1 parent ef44f5b commit b3f6217
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 163 deletions.
54 changes: 54 additions & 0 deletions src/bin/cli/kedqr/hash.rs
@@ -0,0 +1,54 @@
use catalyst_toolbox::kedqr::generate;
use catalyst_toolbox::kedqr::QrPin;
use chain_crypto::bech32::Bech32;
use chain_crypto::{Ed25519Extended, SecretKey};
use std::fs::File;
use std::io::Write;
use std::{
error::Error,
fs::OpenOptions,
io::{BufRead, BufReader},
path::PathBuf,
};

pub fn generate_hash(
input: PathBuf,
output: Option<PathBuf>,
pin: QrPin,
) -> Result<(), Box<dyn Error>> {
// 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 = generate(secret_key, &pwd);
// process output
match output {
Some(path) => {
// save qr code to file, or print to stdout if it fails
let mut file = File::create(path)?;
file.write_all(qr.as_bytes())?;
}
None => {
// prints qr code to stdout when no path is specified
println!("{}", qr);
}
}
Ok(())
}
56 changes: 56 additions & 0 deletions src/bin/cli/kedqr/img.rs
@@ -0,0 +1,56 @@
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,
};

pub fn generate_qr(
input: PathBuf,
output: Option<PathBuf>,
pin: QrPin,
) -> Result<(), Box<dyn Error>> {
// 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();
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(())
}
70 changes: 21 additions & 49 deletions src/bin/cli/kedqr/mod.rs
@@ -1,12 +1,11 @@
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,
};
mod hash;
mod img;

use catalyst_toolbox::kedqr::QrPin;
pub use hash::generate_hash;
pub use img::generate_qr;
use std::error::Error;
use std::path::PathBuf;
use structopt::StructOpt;

/// QCode CLI toolkit
Expand All @@ -22,50 +21,23 @@ pub struct QrCodeCmd {
/// Pin code. 4-digit number is used on Catalyst.
#[structopt(short, long, parse(try_from_str))]
pin: QrPin,

#[structopt(flatten)]
opts: QrCodeOpts,
}

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);
}
match self.opts {
QrCodeOpts::Hash => generate_hash(self.input, self.output, self.pin),
QrCodeOpts::Img => generate_qr(self.input, self.output, self.pin),
}
Ok(())
}
}

#[derive(Debug, PartialEq, StructOpt)]
#[structopt(rename_all = "kebab-case")]
pub enum QrCodeOpts {
Img,
Hash,
}
52 changes: 52 additions & 0 deletions src/kedqr/hash.rs
@@ -0,0 +1,52 @@
use chain_crypto::{Ed25519Extended, SecretKey, SecretKeyError};
use std::io;
use symmetric_cipher::{decrypt, encrypt, Error as SymmetricCipherError};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
#[error("encryption-decryption protocol error")]
SymmetricCipher(#[from] SymmetricCipherError),
#[error("io error")]
Io(#[from] io::Error),
#[error("invalid secret key")]
SecretKey(#[from] SecretKeyError),
#[error("failed to decode hex")]
HexDecode(#[from] hex::FromHexError),
}

pub fn generate(key: SecretKey<Ed25519Extended>, password: &[u8]) -> String {
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.
hex::encode(enc)
}

pub fn decode<S: Into<String>>(
payload: S,
password: &[u8],
) -> Result<SecretKey<Ed25519Extended>, Error> {
let encrypted_bytes = hex::decode(payload.into())?;
let key = decrypt(password, &encrypted_bytes)?;
Ok(SecretKey::from_binary(&key)?)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn encode_decode() {
const PASSWORD: &[u8] = &[1, 2, 3, 4];
let sk = SecretKey::generate(rand::thread_rng());
let hash = generate(sk.clone(), PASSWORD);
assert_eq!(
sk.leak_secret().as_ref(),
decode(hash, PASSWORD).unwrap().leak_secret().as_ref()
);
}
}
142 changes: 142 additions & 0 deletions src/kedqr/img.rs
@@ -0,0 +1,142 @@
use super::hash;
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 symmetric_cipher::Error as SymmetricCipherError;
use thiserror::Error;

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),
#[error("failed to decode hex")]
QrCodeHashError(#[from] super::hash::Error),
}

#[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 enc_hex = hash::generate(key, password);
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)?;
hash::decode(h, password).map_err(Into::into)
})
.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)
}
}

#[cfg(test)]
mod tests {
use super::*;

// TODO: Improve into an integration test using a temporary directory.
// Leaving here as an example.
#[test]
#[ignore]
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]
#[ignore]
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()
);
}
}

0 comments on commit b3f6217

Please sign in to comment.