otptool 0.1.0

Simple tool to help you migrate your OTP secrets from Google Authenticator to another app.
Documentation
use crate::otp::OtpParameters;

pub fn decode_from_link(link: &str) -> anyhow::Result<Vec<OtpParameters>> {
    const MIGRATION_SCHEMA: &str = "otpauth-migration";
    const MIGRATION_HOST: &str = "offline";
    const MIGRATION_QUERY_KEY: &str = "data";

    let parsed_link = url::Url::parse(link).unwrap();

    if parsed_link.scheme() != MIGRATION_SCHEMA {
        return Err(anyhow::anyhow!("schema is not correct"));
    }

    if !parsed_link.has_host() || parsed_link.host_str().unwrap() != MIGRATION_HOST {
        return Err(anyhow::anyhow!("host is not correct"));
    }

    let value = match parsed_link
        .query_pairs()
        .find(|(k, _)| return k == MIGRATION_QUERY_KEY)
    {
        Some((_, val)) => val,
        None => {
            return Err(anyhow::anyhow!("query data not found"));
        }
    };

    let fixed_value = value.replace(" ", "+");
    return OtpParameters::from_base64(fixed_value.as_bytes());
}

pub fn decode_from_image(image_path: std::path::PathBuf) -> anyhow::Result<Vec<OtpParameters>> {
    let img = image::open(image_path)?;

    let decoder = bardecoder::default_decoder();
    let results = decoder.decode(&img);

    if results.len() > 1 {
        return Err(anyhow::anyhow!("too much data to decode OTP data"));
    }

    let maybe_result = results.first().unwrap();
    let result = maybe_result.as_ref().unwrap();

    return decode_from_link(result);
}

mod tests {
    #[test]
    fn test_decode_link() {
        let link = "otpauth-migration://offline?data=CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC";
        let res = super::decode_from_link(link);

        assert!(res.is_ok(), "decode_from_link failed: {}", res.unwrap_err());

        let params = res.unwrap();
        assert_eq!(params.len(), 1);

        let param = params.first().unwrap();
        assert_eq!(param.name, "Example:alice@google.com");
        assert_eq!(param.issuer, "Example");
        assert_eq!(param.secret, "JBSWY3DPEHPK3PXP")
    }

    #[test]
    fn test_decode_qr_image() {
        let img_path = std::path::PathBuf::from("src/testdata/qr-test.jpeg");
        let res = super::decode_from_image(img_path);

        assert!(
            res.is_ok(),
            "decode_from_image failed: {}",
            res.unwrap_err()
        );

        let params = res.unwrap();
        assert_eq!(params.len(), 1);

        let param = params.first().unwrap();
        assert_eq!(param.name, "This_is_an_Example:email@email.com");
        assert_eq!(param.issuer, "Example_Website");
        assert_eq!(param.secret, "S6HZS7XT6KQ6AFSR7XEVO")
    }
}