Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split kedqr command into hash and img
- Loading branch information
Showing
6 changed files
with
332 additions
and
163 deletions.
There are no files selected for viewing
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,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(()) | ||
} |
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,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(()) | ||
} |
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,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() | ||
); | ||
} | ||
} |
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,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() | ||
); | ||
} | ||
} |
Oops, something went wrong.