fritzdecode 0.1.0

Tools for working with AVM FritzBox config file exports
Documentation
use std::{
    fs::{File, OpenOptions},
    io::{stdin, stdout, BufReader, Write},
};

use clap::{Parser, Subcommand};
use fritzdecode::{markers::Unverified, ConfigBackup};

#[derive(Parser)]
struct Args {
    #[command(subcommand)]
    operation: Operation,
}

#[derive(Subcommand)]
enum Operation {
    /// Decrypts data in a given config file.
    Decrypt {
        file: String,
        /// Ignore an invalid CRC32 checksum in the input
        #[arg(long)]
        ignore_invalid_crc: bool,
        /// File to write the decrypted config to. Uses standard output if not
        /// specified.
        #[arg(short, long)]
        output: Option<String>,
    },
    /// Verifies the integrity (decryption key, CRC32) of the given config file.
    Verify {
        file: Option<String>,
    },
    /// Generates the correct CRC32 checksum for a given config file.
    #[command(name = "gencrc")]
    GenCrc {
        file: Option<String>,
    },
}

fn load_file(file: Option<&str>) -> color_eyre::Result<ConfigBackup<Unverified>> {
    match file {
        None | Some("-") => {
            let config = ConfigBackup::load(BufReader::new(stdin()))?;
            Ok(config)
        }
        Some(file) => {
            let config = ConfigBackup::load(BufReader::new(File::open(file)?))?;
            Ok(config)
        }
    }
}

fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;

    let args = Args::parse();
    match args.operation {
        Operation::Decrypt {
            file,
            ignore_invalid_crc,
            output,
        } => {
            let config = load_file(Some(&file))?;

            print!("Password: ");
            stdout().flush()?;
            let mut password = String::new();
            stdin().read_line(&mut password)?;
            let password = password.trim_end_matches('\n');

            let decrypted = match config.verify_crc() {
                Ok(config) => config.decrypt(password)?,
                Err((unverified, _e)) if ignore_invalid_crc => unverified.decrypt(password)?,
                Err((_unverified, e)) => return Err(e.into()),
            };

            let decrypted = decrypted.update_crc();
            if let Some(output) = output {
                decrypted.serialize(
                    OpenOptions::new()
                        .write(true)
                        .create(true)
                        .truncate(true)
                        .open(output)?,
                )?;
            } else {
                decrypted.serialize(stdout())?;
            }
        }
        Operation::Verify { file } => {
            let config = load_file(file.as_deref())?;
            config.verify_crc().map_err(|(_unverified, e)| e)?;
        }
        Operation::GenCrc { file } => {
            let config = load_file(file.as_deref())?.update_crc();
            println!("{:08X}", config.crc());
        }
    }

    Ok(())
}