274 lines
10 KiB
Rust
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>,
|
|
}
|