2 Commits

9 changed files with 408 additions and 288 deletions

74
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: Rust CI/CD
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
release:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Install Windows GNU toolchain
run: |
sudo apt-get update
sudo apt-get install -y gcc-mingw-w64-x86-64
- name: Get version
id: get_version
run: echo "VERSION=$(grep '^version =' Cargo.toml | cut -d '"' -f2)" >> $GITHUB_OUTPUT
- name: Build Release for Linux
run: cargo build --release --target x86_64-unknown-linux-gnu
- name: Build Release for Windows
run: |
rustup target add x86_64-pc-windows-gnu
cargo build --release --target x86_64-pc-windows-gnu
- name: Verify binaries and create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
linux_binary="./target/x86_64-unknown-linux-gnu/release/rustcm-cli"
windows_binary="./target/x86_64-pc-windows-gnu/release/rustcm-cli.exe"
if [ ! -f "$linux_binary" ]; then
echo "Error: Linux binary not found at $linux_binary"
exit 1
fi
if [ ! -f "$windows_binary" ]; then
echo "Error: Windows binary not found at $windows_binary"
exit 1
fi
version="${{ steps.get_version.outputs.VERSION }}"
tag_name="v$version"
release_name="Release v$version"
gh release create "$tag_name" \
--title "$release_name" \
--notes "Automated release for version $version" \
--prerelease \
"$linux_binary#rustcm-cli-linux-x86_64" \
"$windows_binary#rustcm-cli-windows-x86_64.exe"

View File

@@ -3,13 +3,7 @@ name = "rustcm-cli"
version = "0.1.3-alpha" version = "0.1.3-alpha"
edition = "2021" edition = "2021"
[lib] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
name = "rustcm_cli"
path = "src/lib.rs"
[[bin]]
name = "rustcm-cli"
path = "src/main.rs"
[dependencies] [dependencies]
orion = "0.17.5" orion = "0.17.5"

BIN
blob/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,72 +0,0 @@
use colored::Colorize;
use orion::{aead, kdf};
use std::process::exit;
use crate::utils::into_match;
pub fn encrypt(plaintext: String, secret_key: orion::kdf::SecretKey) -> Vec<u8> {
let plaintext = plaintext.into_bytes();
match aead::seal(&secret_key, &plaintext) {
Ok(temp) => {
println!("{} Data was encrypted", "Success:".bright_green());
temp
}
Err(_) => {
eprintln!("{} Could not encrypt the data", "Error:".bright_red());
exit(1);
}
}
}
pub fn decrypt(ciphertext: Vec<u8>, secret_key: orion::kdf::SecretKey) -> String {
let plaintext = match aead::open(&secret_key, &ciphertext) {
Ok(temp) => {
println!("{} Data was decrypted", "Success:".bright_green());
temp
}
Err(_) => {
eprintln!(
"{} Failed to decrypt the file, please check the password",
"Error:".bright_red()
);
exit(1);
}
};
match String::from_utf8(plaintext) {
Ok(temp) => temp,
Err(_) => {
eprintln!("{} Could not convert to String", "Error:".bright_red());
exit(1);
}
}
}
pub fn get_secret_key(salt_bytes: [u8; 32], password: String) -> orion::kdf::SecretKey {
let password = into_match(
kdf::Password::from_slice(password.as_bytes()),
"Error: Could not use the password",
);
let salt = into_match(
kdf::Salt::from_slice(&salt_bytes),
"Error: Could not create the salt",
);
into_match(
kdf::derive_key(&password, &salt, 3, 8, 32),
"Error: Could not generate the secret key",
)
}
pub fn get_salt_bytes() -> [u8; 32] {
let mut salt_bytes = [0u8; 32];
match orion::util::secure_rand_bytes(&mut salt_bytes) {
Ok(_) => (),
Err(_) => {
eprintln!(
"{} Could not generate the random bytes for the salt",
"Error:".bright_red()
);
exit(1);
}
};
salt_bytes
}

View File

@@ -1,73 +0,0 @@
use colored::Colorize;
use std::fs::{read_to_string, File};
use std::io::{Read, Write};
use std::process::exit;
use crate::utils::into_match;
pub fn write_plain(path: String, plaintext: String) {
let mut file = into_match(
File::create(&path),
&format!("Error: Could not create {}", path),
);
into_match(
file.write(&plaintext.into_bytes()),
&format!("Error: Could not write the data to {}", path),
);
into_match(
file.flush(),
&format!("Error: Could not flush data to {}", path),
);
}
pub fn write_cipher(path: String, salt_bytes: [u8; 32], ciphertext: Vec<u8>) {
let mut file = into_match(
File::create(&path),
&format!("Error: Could not create {}", path),
);
into_match(
file.write(&salt_bytes),
&format!("Error: Could write the salt to {}", path),
);
into_match(
file.write(&ciphertext),
&format!("Error: Could not write the ciphertext to {}", path),
);
into_match(
file.flush(),
&format!("Error: Could not flush data to {}", path),
);
}
pub fn read_plain(path: String) -> String {
into_match(
read_to_string(&path),
&format!("Error: Could not read {}", path),
)
}
pub fn read_cipher(path: String) -> (Vec<u8>, Vec<u8>) {
let mut file = into_match(
File::open(&path),
&format!("Error: Could not open {}", path),
);
let metadata = into_match(
File::metadata(&file),
&format!("Error: Could not read the metadata off of {}", path),
);
let mut data: Vec<u8> = vec![0u8; metadata.len() as usize];
match file.read(&mut data) {
Ok(_) => (),
Err(_) => {
eprintln!("{} Could not read {}", "Error:".bright_red(), path);
exit(1);
}
};
let mut salt_bytes: Vec<u8> = vec![0u8; 32];
salt_bytes.clone_from_slice(&data[..32]);
let mut cypher: Vec<u8> = vec![0u8; (metadata.len() - 32) as usize];
cypher.clone_from_slice(&data[32..]);
(salt_bytes, cypher)
}

View File

@@ -1,3 +0,0 @@
pub mod crypto;
pub mod io;
pub mod utils;

View File

@@ -1,46 +1,235 @@
use colored::Colorize; // Program: rustcm-cli (0.1.3-alpha)
use std::env::args; // License: GNU GPL version 3
use std::process::exit; // Author: Arkaprabha Chakraborty
//
// Copyright (C) 2023 Arkaprabha Chakraborty
use rustcm_cli::{ use colored::Colorize;
crypto::{decrypt, encrypt, get_salt_bytes, get_secret_key}, use orion::{aead, kdf};
io::{read_cipher, read_plain, write_cipher, write_plain}, use passterm::prompt_password_tty;
utils::{get_password, into_match, to_array}, use std::env::args;
}; use std::fs::{read_to_string, File};
use std::io::{stdout, Read, Write};
use std::process::exit;
const PROGRAM_NAME: &str = "rustcm-cli"; const PROGRAM_NAME: &str = "rustcm-cli";
const PROGRAM_VERSION: &str = "0.1.3-alpha"; const PROGRAM_VERSION: &str = "0.1.3-alpha";
fn main() { fn get_password(prompt: &str) -> String {
let mut args = args(); into_match(
args.next(); stdout().flush(),
format!("{} Could not flush date to stdout", "Error".bright_red()).as_str(),
);
let password = into_match(
prompt_password_tty(Some(prompt)),
format!("{} Could not read the password", "Error".bright_red()).as_str(),
);
//println!();
let arg_str: String = match args.next() { password
Some(temp) => temp, }
None => {
fn encrypt(plaintext: String, secret_key: orion::kdf::SecretKey) -> Vec<u8> {
let plaintext = plaintext.into_bytes();
match aead::seal(&secret_key, &plaintext) {
Ok(temp) => {
println!("{} Data was encrypted", "Success:".bright_green());
temp
}
Err(_) => {
eprintln!("{} Could not encrypt the data", "Error:".bright_red());
exit(1);
}
}
}
fn get_secret_key(salt_bytes: [u8; 32], password: String) -> orion::kdf::SecretKey {
let password = into_match(
kdf::Password::from_slice(password.as_bytes()),
format!("{} Could not use the password", "Error:".bright_red()).as_str(),
);
let salt = into_match(
kdf::Salt::from_slice(&salt_bytes),
format!("{} Could not create the salt", "Error:".bright_red()).as_str(),
);
into_match(
kdf::derive_key(&password, &salt, 3, 8, 32),
format!(
"{} Could not generate the secret key",
"Error:".bright_red()
)
.as_str(),
)
}
fn get_salt_bytes() -> [u8; 32] {
let mut salt_bytes = [0u8; 32];
match orion::util::secure_rand_bytes(&mut salt_bytes) {
Ok(_) => (),
Err(_) => {
eprintln!( eprintln!(
"{} No arguments provided. Use --help for usage information.", "{} Could not generate the random bytes for the salt",
"Error:".bright_red() "Error:".bright_red()
); );
exit(1); exit(1);
} }
}; };
match arg_str.as_str() { salt_bytes
"--help" | "-h" => handle_help(), }
"--version" | "-v" => handle_version(),
"--encrypt" | "-e" => handle_encrypt(&mut args), fn into_match<T, E>(res: Result<T, E>, estr: &str) -> T {
"--decrypt" | "-d" => handle_decrypt(&mut args), let ok_val: T = match res {
_ => { Ok(temp) => temp,
eprintln!("{} Unrecognized argument", "Error:".bright_red()); Err(_) => {
eprintln!("{estr}");
exit(1)
}
};
ok_val
}
fn write_plain(path: String, plaintext: String) {
let mut file = into_match(
File::create(&path),
format!("{} Could not create {path}", "Error".bright_red()).as_str(),
);
into_match(
file.write(&plaintext.into_bytes()),
format!(
"{} Could not write the data to {path}",
"Error".bright_red()
)
.as_str(),
);
into_match(
file.flush(),
format!("{} Could not flush data to {path}", "Error".bright_red()).as_str(),
);
}
fn write_cipher(path: String, salt_bytes: [u8; 32], ciphertext: Vec<u8>) {
let mut file = into_match(
File::create(&path),
format!("{} Could not create {path}", "Error".bright_red()).as_str(),
);
into_match(
file.write(&salt_bytes),
format!("{} Could write the salt to {path}", "Error".bright_red()).as_str(),
);
into_match(
file.write(&ciphertext),
format!(
"{} Could not write the ciphertext to {path}",
"Error:".bright_red()
)
.as_str(),
);
into_match(
file.flush(),
format!("{} Could not flush data to {path}", "Error:".bright_red()).as_str(),
);
}
fn read_plain(path: String) -> String {
into_match(
read_to_string(&path),
format!("{} Could not read {path}", "Error:".bright_red()).as_str(),
)
}
fn read_cipher(path: String) -> (Vec<u8>, Vec<u8>) {
let mut file = into_match(
File::open(&path),
format!("{} Could not open {path}", "Error:".bright_red()).as_str(),
);
let metadata = into_match(
File::metadata(&file),
format!(
"{} Could not read the metadata off of {path}",
"Error:".bright_red()
)
.as_str(),
);
let mut data: Vec<u8> = vec![0u8; metadata.len() as usize];
match file.read(&mut data) {
Ok(_) => (),
Err(_) => {
eprintln!("{} Could not read {path}", "Error:".bright_red());
exit(1); exit(1);
} }
}; };
let mut salt_bytes: Vec<u8> = vec![0u8; 32];
salt_bytes.clone_from_slice(&data[..32]);
let mut cypher: Vec<u8> = vec![0u8; (metadata.len() - 32) as usize];
cypher.clone_from_slice(&data[32..]);
(salt_bytes, cypher)
} }
fn handle_help() { fn decrypt(ciphertext: Vec<u8>, secret_key: orion::kdf::SecretKey) -> String {
println!( let plaintext = match aead::open(&secret_key, &ciphertext) {
"{} {} Ok(temp) => {
println!("{} Data was decrypted", "Success:".bright_green());
temp
}
Err(_) => {
eprintln!(
"{} Failed to decrypt the file, please check the password",
"Error:".bright_red()
);
exit(1);
}
};
match String::from_utf8(plaintext) {
Ok(temp) => temp,
Err(_) => {
eprintln!("{} Could not convert to String", "Error:".bright_red());
exit(1);
}
}
}
fn to_array(v: Vec<u8>) -> [u8; 32] {
let slice = v.as_slice();
let array: [u8; 32] = match slice.try_into() {
Ok(bytes) => bytes,
Err(_) => {
eprintln!(
"{} Expected a vector of length {} but it was {}",
"Error:".bright_red(),
32,
v.len()
);
exit(1);
}
};
array
}
fn main() {
let mut args = args();
while args.next() != None {
let arg_str: String = match args.next() {
Some(temp) => temp,
None => exit(0),
};
match arg_str.as_str() {
"--help" | "-h" => {
match args.next() {
Some(_) => {
eprintln!("{} Too many arguments", "Error:".bright_red());
exit(1);
}
None => (),
};
println!(
"{} {}
Rust Simple Text Cipher Machine. Rust Simple Text Cipher Machine.
USAGE: USAGE:
@@ -58,83 +247,134 @@ COMMAND:
{}, {} {} {} {}, {} {} {}
Runs the program in decryption mode", Runs the program in decryption mode",
PROGRAM_NAME.bright_yellow(), PROGRAM_NAME.bright_yellow(),
PROGRAM_VERSION.bright_blue(), PROGRAM_VERSION.bright_blue(),
PROGRAM_NAME.bright_yellow(), PROGRAM_NAME.bright_yellow(),
"-h".bright_cyan(), "-h".bright_cyan(),
"--help".bright_cyan(), "--help".bright_cyan(),
"-v".bright_cyan(), "-v".bright_cyan(),
"--version".bright_cyan(), "--version".bright_cyan(),
"-e".bright_cyan(), "-e".bright_cyan(),
"--encrypt".bright_cyan(), "--encrypt".bright_cyan(),
"<path-to-input>".bright_magenta(), "<path-to-input>".bright_magenta(),
"<path-to-output>".bright_magenta(), "<path-to-output>".bright_magenta(),
"-d".bright_cyan(), "-d".bright_cyan(),
"--decrypt".bright_cyan(), "--decrypt".bright_cyan(),
"<path-to-input>".bright_magenta(), "<path-to-input>".bright_magenta(),
"<path-to-output>".bright_magenta(), "<path-to-output>".bright_magenta(),
); );
exit(0); exit(0);
} }
fn handle_version() { "--version" | "-v" => {
println!( match args.next() {
"{} ({}) Some(_) => {
eprintln!("{} Too many arguments", "Error:".bright_red());
exit(1);
}
None => (),
};
println!(
"{} ({})
Copyright (C) 2023 Arkaprabha Chakraborty Copyright (C) 2023 Arkaprabha Chakraborty
License GPLv3: GNU GPL version 3 License GPLv3: GNU GPL version 3
This is free software: you are free to change and redistribute it. This is free software: you are free to change and redistribute it.
There is {}, to the extent permitted by law. There is {}, to the extent permitted by law.
Written by Arkaprabha Chakraborty", Written by Arkaprabha Chakraborty",
PROGRAM_NAME.bright_yellow(), PROGRAM_NAME.bright_yellow(),
PROGRAM_VERSION.bright_blue(), PROGRAM_VERSION.bright_blue(),
"NO WARRANTY".bright_red() "NO WARRANTY".bright_red()
); );
exit(0); exit(0);
} }
"--encrypt" | "-e" => {
let path: String = match args.next() {
Some(temp) => temp,
None => {
eprintln!(
"{} Was expecting two arguments but received none",
"Error:".bright_red()
);
exit(1);
}
};
fn handle_encrypt(args: &mut std::env::Args) { let output: String = match args.next() {
let (path, output) = get_args(args); Some(temp) => temp,
let plaintext: String = read_plain(path); None => {
let salt_bytes = get_salt_bytes(); eprintln!(
let password: String = get_password("Password: "); "{} Was expecting two arguments but received one",
let secret_key = get_secret_key(salt_bytes, password); "Error:".bright_red()
let ciphertext = encrypt(plaintext, secret_key); );
write_cipher(output, salt_bytes, ciphertext); exit(1);
println!( }
"{} Encryption completed successfully", };
"Success:".bright_green()
);
exit(0);
}
fn handle_decrypt(args: &mut std::env::Args) { match args.next() {
let (path, output) = get_args(args); Some(_) => {
let (salt_bytes, ciphertext) = read_cipher(path); eprintln!("{} Too many arguments", "Error:".bright_red());
let salt_bytes: [u8; 32] = to_array(salt_bytes); exit(1);
let password: String = get_password("Password: "); }
let secret_key = get_secret_key(salt_bytes, password); None => (),
let plaintext = decrypt(ciphertext, secret_key); };
write_plain(output, plaintext);
println!(
"{} Decryption completed successfully",
"Success:".bright_green()
);
exit(0);
}
fn get_args(args: &mut std::env::Args) -> (String, String) { let plaintext: String = read_plain(path);
let path: String = into_match( let salt_bytes = get_salt_bytes();
Err(args.next()), let password: String = get_password("Password: ");
"Error: Was expecting two arguments but received none", let secret_key = get_secret_key(salt_bytes, password);
); let ciphertext = encrypt(plaintext, secret_key);
let output: String = into_match(
Err(args.next()), write_cipher(output, salt_bytes, ciphertext);
"Error: Was expecting two arguments but received one",
); exit(0);
if args.next().is_some() { }
eprintln!("{} Too many arguments", "Error:".bright_red()); "--decrypt" | "-d" => {
exit(1); let path: String = match args.next() {
Some(temp) => temp,
None => {
eprintln!(
"{} Was expecting two arguments but received none",
"Error:".bright_red()
);
exit(1);
}
};
let output: String = match args.next() {
Some(temp) => temp,
None => {
eprintln!(
"{} Was expecting two arguments but received one",
"Error:".bright_red()
);
exit(1);
}
};
match args.next() {
Some(_) => {
eprintln!("{} Too many arguments", "Error:".bright_red());
exit(1);
}
None => (),
};
let (salt_bytes, ciphertext) = read_cipher(path);
let salt_bytes: [u8; 32] = to_array(salt_bytes);
let password: String = get_password("Password: ");
let secret_key = get_secret_key(salt_bytes, password);
let plaintext = decrypt(ciphertext, secret_key);
write_plain(output, plaintext);
exit(0);
}
_ => {
eprintln!("{} Unrecognized argument", "Error:".bright_red());
exit(1);
}
};
} }
(path, output)
} }

View File

@@ -1,40 +0,0 @@
use colored::Colorize;
use passterm::prompt_password_tty;
use std::io::stdout;
use std::io::Write;
use std::process::exit;
pub fn get_password(prompt: &str) -> String {
into_match(stdout().flush(), "Error: Could not flush date to stdout");
let password = into_match(
prompt_password_tty(Some(prompt)),
"Error: Could not read the password",
);
password
}
pub fn into_match<T, E>(res: Result<T, E>, estr: &str) -> T {
match res {
Ok(temp) => temp,
Err(_) => {
eprintln!("{}", estr.bright_red());
exit(1)
}
}
}
pub fn to_array(v: Vec<u8>) -> [u8; 32] {
let slice = v.as_slice();
match slice.try_into() {
Ok(bytes) => bytes,
Err(_) => {
eprintln!(
"{} Expected a vector of length {} but it was {}",
"Error:".bright_red(),
32,
v.len()
);
exit(1);
}
}
}