ezcrypt 0.5.1

File encryption utility with forgot password functionality
Documentation
use std::{
    env, fs,
    io::{self, Write},
    path::PathBuf,
};

use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, KeyInit};
use anyhow::{bail, format_err, Context as AnyhowContext};
use base64::{engine::general_purpose::STANDARD as base64engine, Engine};
use clap::{Args, Parser, Subcommand};
use ezcrypt::{argon2_with_our_defaults, DecryptError, EncryptedFile, Forgorcode};
use p256::{ecdh, pkcs8::EncodePublicKey};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rpassword::prompt_password;

const BACKDOOR_PRIVATE_KEY_CIPHERED:&str = "eROUaGInD++6JyPFB9jynLy2xVxLD0kt908BQIQI6I+ELj6u3m7Mlh/tkupNYgEIOjkZx0TBwK73KTbwrqj7jw36OC+1FdY5PM/9KAC4zJR/u/cM1r0eHBF4U7VFfnn4JudwRlpRu372pcVPuqSePmxNwOwHnDPSespTb5l9MTjegwsqNIlIykDme+z8o9B7GC5ljs4=";
const BACKDOOR_SALT: &[u8] = "924u8gfjkns".as_bytes();
const NONCE_LEN: usize = 12;
const CONTACT_INSTRUCTIONS: &str =
    "Send this code in our matrix room for consideration: #iforgor_ezcrypt:bark.lgbt";

fn main() {
    let args = App::parse();
    if let Err(e) = match args.subcmd {
        Commands::Encrypt(encrypt_args) => encrypt_command(encrypt_args),
        Commands::Decrypt(decrypt_args) => decrypt_command(decrypt_args),
        Commands::AdminDashboard => admin_dashboard(),
        Commands::GenerateKeys => generate_keys(),
    } {
        eprintln!("Error completing action: {:?}", e)
    }
}
fn generate_keys() -> anyhow::Result<()> {
    let mut rng = thread_rng();
    let secret = p256::SecretKey::random(&mut rng);
    let password = rpassword::prompt_password("Choose a password to encrypt your private key: ")?;
    if rpassword::prompt_password("Confirm password: ")? != password {
        bail!("Passwords don't match")
    }
    let salt_string: String = thread_rng()
        .sample_iter(&Alphanumeric)
        .take(20)
        .map(char::from)
        .collect();
    let salt = salt_string.as_bytes();
    let mut password_hash = [0u8; 32];
    argon2_with_our_defaults()
        .hash_password_into(password.as_bytes(), salt, &mut password_hash)
        .map_err(|e| format_err!("{:?}", e))
        .context("Error hashing password")?;
    let cipher = Aes256Gcm::new_from_slice(&password_hash)
        .map_err(|e| format_err!("{}", e))
        .context("Error creating cipher")?;
    let marshalled = secret
        .to_sec1_der()
        .context("Error marshalling private key")?;
    let nonce = Aes256Gcm::generate_nonce(&mut rng);
    let mut encrypted = cipher
        .encrypt(&nonce, marshalled.as_ref())
        .map_err(|e| format_err!("{}", e))
        .context("Error encrypting private key")?;
    let mut nonce_vec = nonce.to_vec();
    nonce_vec.append(&mut encrypted);
    let marshalled_public = secret
        .public_key()
        .to_public_key_der()
        .context("Error marshalling public key")?;
    let encoded_public = base64engine.encode(marshalled_public.as_bytes());
    let encoded_ciphered = base64engine.encode(nonce_vec);
    println!("const BACKDOOR_PUBLIC_KEY: &str = \"{}\";", encoded_public);
    println!(
        "const BACKDOOR_PRIVATE_KEY_CIPHERED:&str = \"{}\";",
        encoded_ciphered
    );
    println!(
        "const BACKDOOR_SALT: &[u8] = \"{}\".as_bytes();",
        salt_string
    );
    Ok(())
}
fn admin_dashboard() -> anyhow::Result<()> {
    let password = prompt_password("Enter password to decrypt keys: ")?;
    let mut hashed = [0u8; 32];
    argon2_with_our_defaults()
        .hash_password_into(password.as_bytes(), BACKDOOR_SALT, &mut hashed)
        .map_err(|e| format_err!("{:?}", e))
        .context("Error hashing password")?;
    let decoded = base64engine
        .decode(BACKDOOR_PRIVATE_KEY_CIPHERED)
        .context("Error decoding ciphertext")?;
    let nonce = &decoded[..NONCE_LEN];
    let ciphertext = &decoded[NONCE_LEN..];
    let cipher = Aes256Gcm::new_from_slice(&hashed)
        .map_err(|e| format_err!("{:?}", e))
        .context("Error creating cipher")?;
    let decrypted = cipher
        .decrypt(nonce.into(), ciphertext)
        .map_err(|e| format_err!("{:?}", e))
        .context("Error decrypting key")?;
    let secret = p256::SecretKey::from_sec1_der(&decrypted).context("Error parsing key bytes")?;
    loop {
        print!("Input forgor code: ");
        io::stdout().flush()?;
        let mut code = String::new();
        io::stdin().read_line(&mut code)?;
        code = code.trim().to_string();
        let parsed = match Forgorcode::decode(code) {
            Ok(parsed) => parsed,
            Err(e) => {
                eprintln!("Error decoding forgorcode: {}", e);
                continue;
            }
        };
        if parsed.message.verify(parsed.public_key.0).is_err() {
            eprintln!("Message is tampered with")
        }
        if !parsed.message.text.is_empty() {
            match String::from_utf8(parsed.message.text) {
                Ok(message) => println!("A signed message is attached: {}", message),
                Err(_) => eprintln!("A signed message is attached but it is not valid utf8"),
            };
        }
        let shared_secret =
            ecdh::diffie_hellman(secret.to_nonzero_scalar(), parsed.public_key.0.as_affine());
        let encoded =
            "irember-".to_string() + &base64engine.encode(shared_secret.raw_secret_bytes());
        println!("Rembercode: {}", encoded);
    }
}
fn encrypt_command(args: EncryptArgs) -> anyhow::Result<()> {
    let contents = fs::read(args.file_name.clone()).context("Unable to access file")?;
    let password = match args.password {
        None => {
            let password = prompt_password("Choose a password: ")?;
            if prompt_password("Confirm password: ")? != password {
                bail!("Passwords don't match")
            }
            password
        }
        Some(password) => password,
    };

    let mut unencrypted_text = String::new();
    print!("Any additional unencrypted data you want to add: ");
    io::stdout().flush()?;
    io::stdin()
        .read_line(&mut unencrypted_text)
        .context("Error reading line")?;
    unencrypted_text = unencrypted_text.trim().to_string();
    let encrypted_file = EncryptedFile::encrypt(
        contents,
        password,
        unencrypted_text,
        Some(args.file_name.clone()),
    )?;
    fs::write(
        args.file_name.clone() + ".ezcrypt",
        encrypted_file
            .serialize()
            .context("Error encoded encrypted file")?,
    )
    .context("Error writing encrypted file")?;
    fs::remove_file(args.file_name).context("Error removing original file")?;
    Ok(())
}
fn decrypt_command(args: EncryptArgs) -> anyhow::Result<()> {
    let contents = fs::read(args.file_name.clone()).context("Unable to access file")?;
    let encrypted_file =
        EncryptedFile::deserialize(contents).context("Error deserializing encrypted file")?;
    if let Ok(msg) = encrypted_file.get_message() {
        if let Some(text) = msg {
            println!(
                "A signed message is attached:\n{}",
                String::from_utf8(text).context("Error converting to utf8")?
            )
        }
    } else {
        eprintln!("A message is attached but its signature is invalid. Not displaying.")
    }
    for attempt_num in 0..3 {
        let password = match args.password {
            None => rpassword::prompt_password("Enter the password for this file: ")?,
            Some(ref password) => password.to_string(),
        };
        if password == "iforgor" {
            let code = encrypted_file.generate_forgorcode();
            let encoded = code.encode().context("Error generating forgorcode")?;
            println!("Your forgorcode is: {}", encoded);
            println!("{}", CONTACT_INSTRUCTIONS);
            return Ok(());
        }
        let decrypted = match encrypted_file.decrypt(password) {
            Ok(decrypted) => decrypted,
            Err(err) => {
                match err {
                    DecryptError::WrongPassword => {
                        if attempt_num == 0 {
                            eprintln!("Incorrect Password. For help decrypting this file, type \"iforgor\" as your password.");
                        } else {
                            eprintln!("Incorrect password");
                        }
                    }
                    _ => eprintln!("Error decrypting: {}", err),
                }
                // Assume that if a password argument is passed, user does not want to use this in interactive mode
                if args.password.is_some() {
                    return Ok(());
                }
                continue;
            }
        };
        let cloned = args.file_name.clone();
        let could_be = PathBuf::from(
            cloned
                .strip_suffix(".ezcrypt")
                .unwrap_or(&args.file_name)
                .to_string(),
        );
        let new_filename = match encrypted_file.original_file_name {
            None => could_be,
            Some(name) => {
                let our_path = PathBuf::from(name);
                let real_name = our_path
                    .file_name()
                    .context("original file name parameter is not a valid file")?;
                if real_name != could_be {
                    eprintln!(
                        "File written to {}",
                        real_name.to_str().unwrap_or(
                            "a different location than the name of the file without the prefix"
                        )
                    );
                }
                env::current_dir()
                    .context("error getting current directory")?
                    .join(real_name)
            }
        };
        fs::write(new_filename, decrypted).context("Error creating decrypted file")?;
        fs::remove_file(args.file_name).context("Error removing original file")?;
        return Ok(());
    }
    bail!("Too many attempts")
}

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct App {
    #[clap(subcommand)]
    subcmd: Commands,
}
#[derive(Debug, Subcommand)]

enum Commands {
    /// Encrypt files
    #[clap(aliases=["en"])]
    Encrypt(EncryptArgs),
    /// Decrypt files
    #[clap(aliases=["de"])]
    Decrypt(EncryptArgs),
    /// For admins. Generates rembercodes from forgorcodes
    #[clap(aliases=["dashboard","admin"])]
    AdminDashboard,
    /// Generates keys for the admin dashboard
    #[clap(aliases=["gen_keys","keys","gen"])]
    GenerateKeys,
}
#[derive(Args, Debug)]
struct EncryptArgs {
    file_name: String,
    #[arg(short, long)]
    password: Option<String>,
}