Skip to content

Commit 9e1f999

Browse files
committed
Initial commit: rustcube multi-password encryption tool
0 parents  commit 9e1f999

File tree

1,823 files changed

+67755
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,823 files changed

+67755
-0
lines changed

Cargo.lock

Lines changed: 653 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "rustcube"
3+
version = "0.1.1"
4+
edition = "2021"
5+
description = "Multi-password, order-dependent, streaming encryption/decryption tool with secure memory handling."
6+
authors = ["Joshua Wink <Joshua@orchestrate.solutions>"]
7+
license = "MIT"
8+
9+
10+
[dependencies]
11+
libaes = "0.7"
12+
pbkdf2 = "0.12"
13+
rand = "0.8"
14+
zeroize = "1.6"
15+
clap = { version = "4.5", features = ["derive"] }
16+
tar = "0.4"
17+
hex = "0.4"
18+
sha2 = "0.10"
19+
rpassword = "7.3"

README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# rustcube
2+
3+
Multi-password, order-dependent, streaming encryption/decryption tool with secure memory handling.
4+
5+
## Features
6+
- **Multi-password, order-dependent**: Any number of passwords, order matters, case sensitive. Only the correct sequence unlocks the data everything else returns non-sensical output.
7+
- **Streaming encryption/decryption**: Handles large files/folders efficiently.
8+
- **Salted key derivation**: Each encryption is unique, even with the same passwords.
9+
- **No error on wrong password**: Decryption always produces output; wrong passwords yield unusable (garbled) data.
10+
- **Secure memory handling**: Uses the `zeroize` crate to clear sensitive data from memory.
11+
- **Tar-based archiving**: Folders are packed/unpacked using tar for cross-platform compatibility.
12+
- **CLI interface**: Easy to use, scriptable, and ready for automation.
13+
14+
## Usage
15+
16+
17+
### Install from crates.io
18+
19+
```
20+
cargo install rustcube
21+
```
22+
23+
### Build from source
24+
25+
```
26+
cargo build --release
27+
```
28+
29+
### Encrypt a Folder
30+
31+
```
32+
cargo run --release -- encrypt --folder <FOLDER_TO_ENCRYPT> --output <OUTPUT_FILE>
33+
```
34+
- You will be prompted for passwords (one per line, empty line to finish).
35+
- The output file will contain the salt, IV, and encrypted data.
36+
37+
### Decrypt a File
38+
39+
```
40+
cargo run --release -- decrypt --input <ENCRYPTED_FILE> --output <OUTPUT_FOLDER>
41+
```
42+
- You will be prompted for passwords (one per line, empty line to finish).
43+
- If the passwords and order are correct, the folder will be restored.
44+
- If not, the output will be garbled (no error is shown).
45+
46+
## How It Works
47+
48+
## Multi-Password Usability Advantage
49+
50+
Instead of relying on a single massive password, rustcube lets you use several smaller, memorable passwords in a strict order. This makes it easier to remember and type, while still providing extremely strong security. The number of possible combinations grows rapidly with each additional password and the order in which they are entered, making brute-force attacks much harder.
51+
52+
For example, 4 passwords of 6 characters each (with order sensitivity) can be as strong or stronger than a single 24-character password, depending on the entropy of each password and the total number of possible combinations.
53+
54+
**Tip:** Use unique, non-trivial passwords and avoid common words or sequences for best results.
55+
56+
## Planned Feature: Brute-Force Calculator
57+
58+
In a future version, rustcube will include a brute-force calculator. When you set your passwords, it will estimate how long it would take to brute-force your chosen combination using:
59+
60+
- A high-spec modern machine or cluster (e.g., billions of guesses per second)
61+
- A hypothetical quantum computer (using Grover's algorithm, which can halve the effective keyspace)
62+
63+
This feedback will help you choose a password sequence that balances usability and security for your needs.
64+
65+
- **Key Derivation**: Each password is used to mutate the key state (like turning a Rubik's cube). The final key is used for AES-256 encryption/decryption.
66+
- **Salt**: A random salt is generated for each encryption and stored with the ciphertext. This ensures uniqueness and prevents rainbow table attacks.
67+
- **IV**: A random IV is generated for each encryption and stored with the ciphertext.
68+
- **Memory Security**: All sensitive data (passwords, keys, intermediate states) are zeroed from memory after use.
69+
- **No Feedback on Failure**: Decryption always produces output. If the key is wrong, the output is just random data.
70+
71+
## Security Notes
72+
- Passwords and their order are never stored.
73+
- The salt and IV are not secrets; they are stored with the encrypted file.
74+
- For maximum security, use strong, unique passwords and keep them safe.
75+
- The tool is designed for local, user-controlled encryption and decryption. You can use it in the cloud if you wish, but its primary intent is to let a user keep private, secured data alongside public data (such as in a repo), so you can pull your repo from anywhere and always access your own secure data—while keeping it inaccessible to others. You control if, when, and how you share your secrets. For cloud or multi-user scenarios, review your threat model and use at your own discretion.
76+
77+
## Dependencies
78+
- [aes](https://crates.io/crates/aes)
79+
- [block-modes](https://crates.io/crates/block-modes)
80+
- [block-padding](https://crates.io/crates/block-padding)
81+
- [pbkdf2](https://crates.io/crates/pbkdf2)
82+
- [rand](https://crates.io/crates/rand)
83+
- [zeroize](https://crates.io/crates/zeroize)
84+
- [clap](https://crates.io/crates/clap)
85+
- [tar](https://crates.io/crates/tar)
86+
- [hex](https://crates.io/crates/hex)
87+
88+
## Example
89+
90+
```
91+
# Encrypt
92+
cargo run --release -- encrypt --folder secrets --output secrets.enc
93+
94+
# Decrypt
95+
cargo run --release -- decrypt --input secrets.enc --output secrets_restored
96+
```
97+
98+
## License
99+
MIT
100+
101+
---
102+
103+
God willing, this tool will help you keep your secrets safe and your workflow efficient.

src/estimate.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use std::io::{self, Write};
2+
3+
fn get_charset_size(pw: &str) -> usize {
4+
let mut has_lower = false;
5+
let mut has_upper = false;
6+
let mut has_digit = false;
7+
let mut has_symbol = false;
8+
for c in pw.chars() {
9+
if c.is_ascii_lowercase() { has_lower = true; }
10+
else if c.is_ascii_uppercase() { has_upper = true; }
11+
else if c.is_ascii_digit() { has_digit = true; }
12+
else { has_symbol = true; }
13+
}
14+
let mut size = 0;
15+
if has_lower { size += 26; }
16+
if has_upper { size += 26; }
17+
if has_digit { size += 10; }
18+
if has_symbol { size += 33; }
19+
size
20+
}
21+
22+
fn estimate_entropy(pw: &str, is_dict: bool) -> f64 {
23+
if is_dict {
24+
// Penalize dictionary words: assume 10 bits
25+
10.0
26+
} else {
27+
let charset = get_charset_size(pw);
28+
(pw.len() as f64) * (charset as f64).log2()
29+
}
30+
}
31+
32+
pub fn estimate_passwords() {
33+
println!("Enter passwords (one per line, empty line to finish):");
34+
let mut passwords = Vec::new();
35+
let mut entropies = Vec::new();
36+
loop {
37+
print!("Password: "); io::stdout().flush().unwrap();
38+
let mut pw = String::new();
39+
io::stdin().read_line(&mut pw).unwrap();
40+
let pw = pw.trim().to_string();
41+
if pw.is_empty() { break; }
42+
print!("Is this a dictionary word? (y/N): "); io::stdout().flush().unwrap();
43+
let mut dict = String::new();
44+
io::stdin().read_line(&mut dict).unwrap();
45+
let is_dict = dict.trim().to_lowercase() == "y";
46+
let entropy = estimate_entropy(&pw, is_dict);
47+
passwords.push(pw);
48+
entropies.push(entropy);
49+
}
50+
if passwords.is_empty() {
51+
println!("No passwords entered.");
52+
return;
53+
}
54+
let total_entropy: f64 = entropies.iter().sum();
55+
let total_keyspace = 2f64.powf(total_entropy);
56+
let guesses_per_sec = 1e11; // 100 billion/sec
57+
let classical_seconds = total_keyspace / guesses_per_sec;
58+
let quantum_seconds = total_keyspace.sqrt() / guesses_per_sec;
59+
fn human_time(secs: f64) -> String {
60+
if secs < 60.0 {
61+
format!("{:.2} seconds", secs)
62+
} else if secs < 3600.0 {
63+
format!("{:.2} minutes", secs/60.0)
64+
} else if secs < 86400.0 {
65+
format!("{:.2} hours", secs/3600.0)
66+
} else if secs < 31_536_000.0 {
67+
format!("{:.2} days", secs/86400.0)
68+
} else {
69+
format!("{:.2} years", secs/31_536_000.0)
70+
}
71+
}
72+
println!("\n--- Brute-Force Estimate ---");
73+
println!("Total entropy: {:.2} bits", total_entropy);
74+
println!("Total keyspace: ~{:.2e}", total_keyspace);
75+
println!("Classical brute-force: {}", human_time(classical_seconds));
76+
println!("Quantum brute-force: {}", human_time(quantum_seconds));
77+
if total_entropy < 60.0 {
78+
println!("Warning: Your passwords are weak. Consider using longer or more complex passwords.");
79+
} else if total_entropy < 80.0 {
80+
println!("Caution: Your passwords are moderate. For strong security, aim for 80+ bits of entropy.");
81+
} else {
82+
println!("Good: Your passwords are strong.");
83+
}
84+
}

src/main.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//! rustcube: Multi-password, order-dependent, streaming encryption/decryption tool
2+
//! - Each password mutates the key state (like a Rubik's cube)
3+
//! - Salt is stored with ciphertext
4+
//! - No error on wrong password: just garbled output
5+
//! - Secure memory zeroing with zeroize
6+
7+
use libaes::Cipher;
8+
use pbkdf2::pbkdf2_hmac;
9+
use rand::RngCore;
10+
use rand::rngs::OsRng;
11+
use sha2::Sha256;
12+
use zeroize::Zeroize;
13+
use clap::{Parser, Subcommand};
14+
mod estimate;
15+
use std::fs::{File};
16+
use std::io::{Read, Write};
17+
use tar::{Builder, Archive};
18+
19+
const SALT_LEN: usize = 16;
20+
const IV_LEN: usize = 16;
21+
const PBKDF2_ITER: u32 = 100_000;
22+
23+
// No longer needed: type Aes256Cbc = Cbc<Aes256, Pkcs7>;
24+
25+
#[derive(Parser)]
26+
#[command(author, version, about)]
27+
struct Cli {
28+
#[command(subcommand)]
29+
command: Commands,
30+
}
31+
32+
#[derive(Subcommand)]
33+
enum Commands {
34+
Encrypt {
35+
#[arg(short, long)]
36+
folder: String,
37+
#[arg(short, long)]
38+
output: String,
39+
},
40+
Estimate,
41+
Decrypt {
42+
#[arg(short, long)]
43+
input: String,
44+
#[arg(short, long)]
45+
output: String,
46+
},
47+
}
48+
49+
fn derive_key(passwords: &[String], salt: &[u8]) -> [u8; 32] {
50+
let mut key = [0u8; 32];
51+
let mut state = Vec::new();
52+
for pw in passwords {
53+
let mut k = [0u8; 32];
54+
pbkdf2_hmac::<Sha256>(pw.as_bytes(), salt, PBKDF2_ITER, &mut k);
55+
if state.is_empty() {
56+
state.extend_from_slice(&k);
57+
} else {
58+
let state_len = state.len();
59+
for (i, b) in k.iter().enumerate() {
60+
let idx = i % state_len;
61+
state[idx] ^= b;
62+
}
63+
}
64+
k.zeroize();
65+
}
66+
key.copy_from_slice(&state[..32]);
67+
state.zeroize();
68+
key
69+
}
70+
71+
fn encrypt_folder(folder: &str, output: &str, passwords: &[String]) -> std::io::Result<()> {
72+
let mut salt = [0u8; SALT_LEN];
73+
OsRng.fill_bytes(&mut salt);
74+
let mut iv = [0u8; IV_LEN];
75+
OsRng.fill_bytes(&mut iv);
76+
let key = derive_key(passwords, &salt);
77+
let tar_path = format!("{}.tar", output);
78+
{
79+
let tar_gz = File::create(&tar_path)?;
80+
let mut builder = Builder::new(tar_gz);
81+
builder.append_dir_all(".", folder)?;
82+
}
83+
let mut tar_data = Vec::new();
84+
{
85+
let mut f = File::open(&tar_path)?;
86+
f.read_to_end(&mut tar_data)?;
87+
}
88+
let cipher = Cipher::new_256(&key);
89+
let ciphertext = cipher.cbc_encrypt(&iv, &tar_data);
90+
let mut out = File::create(output)?;
91+
out.write_all(&salt)?;
92+
out.write_all(&iv)?;
93+
out.write_all(&ciphertext)?;
94+
std::fs::remove_file(&tar_path)?;
95+
Ok(())
96+
}
97+
98+
fn decrypt_folder(input: &str, output: &str, passwords: &[String]) -> std::io::Result<()> {
99+
let mut f = File::open(input)?;
100+
let mut salt = [0u8; SALT_LEN];
101+
let mut iv = [0u8; IV_LEN];
102+
f.read_exact(&mut salt)?;
103+
f.read_exact(&mut iv)?;
104+
let mut ciphertext = Vec::new();
105+
f.read_to_end(&mut ciphertext)?;
106+
let key = derive_key(passwords, &salt);
107+
let cipher = Cipher::new_256(&key);
108+
let decrypted = cipher.cbc_decrypt(&iv, &ciphertext);
109+
let tar_path = format!("{}.tar", input);
110+
{
111+
let mut tar_file = File::create(&tar_path)?;
112+
tar_file.write_all(&decrypted)?;
113+
}
114+
let tar_gz = File::open(&tar_path)?;
115+
let mut archive = Archive::new(tar_gz);
116+
archive.unpack(output)?;
117+
std::fs::remove_file(&tar_path)?;
118+
Ok(())
119+
}
120+
121+
fn main() {
122+
let cli = Cli::parse();
123+
match cli.command {
124+
Commands::Encrypt { folder, output } => {
125+
println!("Enter passwords (one per line, empty line to finish):");
126+
let mut passwords = Vec::new();
127+
loop {
128+
let pw = rpassword::prompt_password("Password: ").unwrap();
129+
if pw.is_empty() { break; }
130+
passwords.push(pw);
131+
}
132+
encrypt_folder(&folder, &output, &passwords).expect("Encryption failed");
133+
println!("Encrypted to {}", output);
134+
}
135+
Commands::Decrypt { input, output } => {
136+
println!("Enter passwords (one per line, empty line to finish):");
137+
let mut passwords = Vec::new();
138+
loop {
139+
let pw = rpassword::prompt_password("Password: ").unwrap();
140+
if pw.is_empty() { break; }
141+
passwords.push(pw);
142+
}
143+
decrypt_folder(&input, &output, &passwords).expect("Decryption failed");
144+
println!("Decryption attempted. If passwords were wrong, output will be garbled.");
145+
}
146+
Commands::Estimate => {
147+
estimate::estimate_passwords();
148+
}
149+
}
150+
}

target/.rustc_info.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"rustc_fingerprint":16784359761484475572,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.88.0 (6b00bc388 2025-06-23) (Homebrew)\nbinary: rustc\ncommit-hash: 6b00bc3880198600130e1cf62b8f8a93494488cc\ncommit-date: 2025-06-23\nhost: aarch64-apple-darwin\nrelease: 1.88.0\nLLVM version: 20.1.7\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/opt/homebrew/Cellar/rust/1.88.0\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}

target/CACHEDIR.TAG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Signature: 8a477f597d28d172789f06886806bc55
2+
# This file is a cache directory tag created by cargo.
3+
# For information about cache directory tags see https://bford.info/cachedir/
9.93 KB
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"git": {
3+
"sha1": "b28dbfc201bdda98c339d809bc861aa67f2d1ff1"
4+
},
5+
"path_in_vcs": ".secure/rustcube"
6+
}

0 commit comments

Comments
 (0)