1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use super::payload;
use chain_crypto::{Ed25519Extended, SecretKey, SecretKeyError};
use image::{DynamicImage, ImageBuffer, ImageError, 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::payload::Error),
    #[error(transparent)]
    Image(#[from] ImageError),
}

#[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 = payload::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)?;
                payload::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()
        );
    }
}