Skip to content

Commit

Permalink
Merge pull request #9 from kruserr/i6-pack
Browse files Browse the repository at this point in the history
pack command
  • Loading branch information
kruserr authored Oct 8, 2024
2 parents 445b452 + 18353cd commit f7abe66
Show file tree
Hide file tree
Showing 19 changed files with 1,723 additions and 111 deletions.
1,445 changes: 1,366 additions & 79 deletions Cargo.lock

Large diffs are not rendered by default.

38 changes: 13 additions & 25 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
[package]
name = "i6"
version = "0.1.9" # prepare_release.sh
edition = "2021"
license = "AGPL-3.0"
authors = ["kruserr"]
readme = "README.md"
repository = "https://github.com/kruserr/i6"
description = "A collection of tools"
keywords = ["cli", "terminal", "utility", "tool", "command"]
categories = ["command-line-interface", "command-line-utilities", "development-tools"]
[workspace]
resolver = "2"

[dependencies]
clap = "3"
members = [
"i6-pack",
"i6",

# for http and https commands
tokio = { version = "1", features = ["full"] }
# warp = "0.3"
tracing-subscriber = "0.3"
# for https command
warp = { version = "0.3", features = ["default", "tls"] }
openssl = "0.10"
# Internal
# "examples",
]

# for db command
rapiddb-web = "0.1"

[lints.rust]
[workspace.lints.rust]
unused_parens = "allow"
unused_imports = "allow"

[lints.clippy]
[workspace.lints.clippy]
needless_return = "allow"
implicit_saturating_sub = "allow"
single_component_path_imports = "allow"
5 changes: 5 additions & 0 deletions ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ ci () {
cargo +nightly fmt --all
cargo clippy --all-targets --all-features -- -Dwarnings
cargo test

# cargo audit
# cargo upgrade --verbose
# cargo update --verbose
# cargo +nightly udeps --all-targets
}

ci
18 changes: 18 additions & 0 deletions i6-pack/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "i6-pack"
version = "0.1.10"
edition = "2021"
publish = false

[lints]
workspace = true

[dependencies]
clap = "4"
tar = "0.4"
zstd = "0.13"
aes-gcm = "0.10"
rand = "0.8"
hmac = "0.12"
uuid = {version = "1", features = ["v4"]}
argon2 = "0.5"
46 changes: 46 additions & 0 deletions i6-pack/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use crate::compression;
use crate::encryption;
use crate::utils;

pub fn run(action: &str, target: &str, password: &str) -> std::io::Result<()> {
// Validate and sanitize the target path
let target_path = utils::validate_path(target)
.or_else(|_| utils::sanitize_output_path(target))
.expect("Invalid target path");

let tar_file =
&format!(".{}_{}.tar", target_path.display(), uuid::Uuid::new_v4());
let compressed_file =
&format!(".{}_{}.tar.zst", target_path.display(), uuid::Uuid::new_v4());
let encrypted_file = &format!("{}.i6p", target_path.display());

match action {
"pack" => {
compression::create_tar_archive(target_path.to_str().unwrap(), tar_file)?;
compression::compress_tar_file(tar_file, compressed_file)?;
encryption::encrypt_file(compressed_file, encrypted_file, password)?;
}
"unpack" => {
encryption::decrypt_file(
target_path.to_str().unwrap(),
compressed_file,
password,
)?;
compression::decompress_file(compressed_file, tar_file)?;
compression::extract_tar_archive(
tar_file,
&utils::remove_extension(target_path.to_str().unwrap(), ".i6p"),
)?;
}
_ => {
eprintln!("Invalid action. Use 'pack' or 'unpack'.");
std::process::exit(1);
}
}

// Clean up temporary files
std::fs::remove_file(tar_file)?;
std::fs::remove_file(compressed_file)?;

Ok(())
}
53 changes: 53 additions & 0 deletions i6-pack/src/compression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::fs::File;
use std::io::{self, Write};
use std::path::Path;
use tar::Builder;
use zstd::stream::{decode_all, encode_all};

pub fn create_tar_archive<P: AsRef<Path>>(
folder: P,
tar_file: &str,
) -> io::Result<()> {
let tar_gz = File::create(tar_file)?;
let mut archive = Builder::new(tar_gz);
archive.append_dir_all(".", folder)?;
Ok(())
}

pub fn extract_tar_archive(tar_file: &str, output_dir: &str) -> io::Result<()> {
let tar_gz = File::open(tar_file)?;
let mut archive = tar::Archive::new(tar_gz);

let mut final_output_dir = output_dir.to_string();
if Path::new(output_dir).exists() {
final_output_dir = format!("{}-{}", output_dir, uuid::Uuid::new_v4());
}

std::fs::create_dir_all(&final_output_dir)?;
archive.unpack(&final_output_dir)?;
Ok(())
}

pub fn compress_tar_file(
tar_file: &str,
compressed_file: &str,
) -> io::Result<()> {
let tar = File::open(tar_file)?;
let compressed = File::create(compressed_file)?;
let mut tar_reader = tar;
let mut compressed_writer = compressed;
let compressed_data = encode_all(&mut tar_reader, 0)?;
compressed_writer.write_all(&compressed_data)?;
Ok(())
}

pub fn decompress_file(
compressed_file: &str,
output_file: &str,
) -> io::Result<()> {
let compressed = File::open(compressed_file)?;
let decompressed_data = decode_all(compressed)?;
let mut decompressed = File::create(output_file)?;
decompressed.write_all(&decompressed_data)?;
Ok(())
}
83 changes: 83 additions & 0 deletions i6-pack/src/encryption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key, Nonce,
};
use hmac::digest::{generic_array::GenericArray, typenum};
use rand::RngCore;
use std::fs::File;
use std::io::{self, Write};

const SALT_LEN: usize = 16;
const NONCE_LEN: usize = 12;

fn generate_salt() -> [u8; SALT_LEN] {
let mut salt = [0u8; SALT_LEN];
rand::thread_rng().fill_bytes(&mut salt);
salt
}

fn generate_nonce() -> Nonce<typenum::U12> {
let mut nonce = [0u8; NONCE_LEN];
rand::thread_rng().fill_bytes(&mut nonce);
*Nonce::from_slice(&nonce)
}

fn derive_key_from_password_argon2(password: &str, salt: &[u8]) -> [u8; 32] {
use argon2::{self, password_hash::SaltString, Argon2, PasswordHasher};

let argon2 = Argon2::default();
let salt = SaltString::encode_b64(salt).unwrap();
let password_hash = argon2.hash_password(password.as_bytes(), &salt).unwrap();
let key = password_hash.hash.unwrap();
let mut key_bytes = [0u8; 32];
key_bytes.copy_from_slice(key.as_bytes());
key_bytes
}

pub fn encrypt_file(
input_file: &str,
output_file: &str,
password: &str,
) -> io::Result<()> {
let salt = generate_salt();
let key = derive_key_from_password_argon2(password, &salt);
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
let nonce = generate_nonce();

let file_content = std::fs::read(input_file)?;
let ciphertext = cipher
.encrypt(&nonce, file_content.as_ref())
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Encryption failure"))?;

let mut output = File::create(output_file)?;
output.write_all(&salt)?; // Prepend salt
output.write_all(nonce.as_slice())?; // Prepend nonce
output.write_all(&ciphertext)?;
Ok(())
}

pub fn decrypt_file(
input_file: &str,
output_file: &str,
password: &str,
) -> io::Result<()> {
let file_content = std::fs::read(input_file)?;
let (salt_and_nonce, ciphertext) =
file_content.split_at(SALT_LEN + NONCE_LEN); // Extract salt and nonce
let (salt, nonce) = salt_and_nonce.split_at(SALT_LEN); // Extract salt

let key = derive_key_from_password_argon2(password, salt);
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));

let nonce = GenericArray::from_slice(nonce);
let plaintext = match cipher.decrypt(nonce, ciphertext) {
Ok(pt) => pt,
Err(_) => {
return Err(io::Error::new(io::ErrorKind::Other, "Decryption failure"))
}
};

let mut output = File::create(output_file)?;
output.write_all(&plaintext)?;
Ok(())
}
4 changes: 4 additions & 0 deletions i6-pack/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod cli;
pub mod compression;
pub mod encryption;
pub mod utils;
38 changes: 38 additions & 0 deletions i6-pack/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use i6_pack::cli;

use clap::{Arg, Command as ClapCommand};
use std::io;

fn main() -> io::Result<()> {
let matches = ClapCommand::new("i6-pack")
.version("0.0.1")
.author("kruserr")
.about(
"Compress and encrypt a folder, or decrypt and decompress an archive",
)
.arg(
Arg::new("action")
.help("Action to perform: pack or unpack")
.required(true)
.index(1),
)
.arg(
Arg::new("target")
.help("Folder to compress and encrypt, or to extract to")
.required(true)
.index(2),
)
.arg(
Arg::new("password")
.help("Password for encryption/decryption")
.required(true)
.index(3),
)
.get_matches();

let action = matches.get_one::<String>("action").unwrap();
let target = matches.get_one::<String>("target").unwrap();
let password = matches.get_one::<String>("password").unwrap();

cli::run(action, target, password)
}
28 changes: 28 additions & 0 deletions i6-pack/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::io;
use std::path::{Path, PathBuf};

pub fn remove_extension(filename: &str, extension: &str) -> String {
filename.strip_suffix(extension).unwrap_or(filename).to_owned()
}

pub fn validate_path(path: &str) -> io::Result<PathBuf> {
let path = Path::new(path);
if path.exists() {
Ok(path.to_path_buf())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "Path does not exist"))
}
}

pub fn sanitize_output_path(output_path: &str) -> io::Result<PathBuf> {
let path = Path::new(output_path);
if path.is_absolute()
&& !path
.components()
.any(|comp| matches!(comp, std::path::Component::ParentDir))
{
Ok(path.to_path_buf())
} else {
Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid output path"))
}
}
34 changes: 34 additions & 0 deletions i6/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "i6"
version = "0.1.10" # prepare_release.sh
edition = "2021"
default-run = "i6"
license = "AGPL-3.0"
authors = ["kruserr"]
readme = "README.md"
repository = "https://github.com/kruserr/i6"
description = "A collection of tools"
keywords = ["cli", "terminal", "utility", "tool", "command"]
categories = ["command-line-interface", "command-line-utilities", "development-tools"]

[lints]
workspace = true

[dependencies]
clap = "3"

# for http and https commands
tokio = { version = "1", features = ["full"] }
# warp = "0.3"
tracing-subscriber = "0.3"
# for https command
warp = { version = "0.3", features = ["default", "tls"] }
openssl = "0.10"

i6-pack = { version = "0.1", path = "../i6-pack" }

# for db command
rapiddb-web = "0.1"

# for reader command
rustic-reader = "0.1"
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit f7abe66

Please sign in to comment.