ezcrypt_rust/src/main.rs

274 lines
10 KiB
Rust

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>,
}