diff --git a/.gitignore b/.gitignore index 3f9c62b..9e1aa36 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ target/ **/*.rs.bk Cargo.lock **/*.swp +.abrute +.abrute.bak +**/.abrute +**/.abrute.bak diff --git a/.travis.yml b/.travis.yml index 32b8828..157ef2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: rust os: - linux rust: -- stable -- beta - nightly before_install: - wget https://www.aescrypt.com/download/v3/linux/aescrypt-3.13.tgz diff --git a/Cargo.toml b/Cargo.toml index 118280a..b4f7556 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "abrute" -version = "0.1.3" +version = "0.1.4" authors = ["Daniel P. Clark <6ftdan@gmail.com>"] description = "AESCrypt Brute force attempter." documentation = "http://danielpclark.github.io/abrute/index.html" @@ -12,8 +12,9 @@ keywords = ["crypto","text","cryptography","security"] categories = ["cryptography"] [dependencies] +array_tool = "^1.0" tempdir = "0.3" clap = "^2.26" -digits = "^0.3.6" +digits = "~1.1.0" num_cpus = "^1.0" rayon = "0.8.2" diff --git a/README.md b/README.md index 890acd2..6fdae4e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ a secondary system. ## Installation +_abrute needs the nightly build of Rust for the TryFrom trait._ + To use the install script you need to have the following commands available on your system `curl wget unzip sudo cc` and possibly other build essentials for C. @@ -58,7 +60,7 @@ cd ../.. && rm -rf aescrypt-3.13 Next you need to have [Rust installed](https://www.rust-lang.org/en-US/install.html). ```bash -curl https://sh.rustup.rs -sSf | sh +curl https://sh.rustup.rs -sSf | sh -s -- --channel=nightly ``` Then you can get and compile abrute. diff --git a/install.sh b/install.sh index 3d4a29c..88d1f1d 100755 --- a/install.sh +++ b/install.sh @@ -13,7 +13,7 @@ else if [ "$Rust_answer" == "y" ]; then # User agreed, proceed with installation - curl https://sh.rustup.rs -sSf | sh + curl https://sh.rustup.rs -sSf | sh -s -- --channel=nightly else # User refused, abort installation exit 1 diff --git a/src/core.rs b/src/core.rs index 2acc11f..1cf8858 100644 --- a/src/core.rs +++ b/src/core.rs @@ -13,6 +13,7 @@ use rayon::prelude::*; use super::result::Error; extern crate num_cpus; extern crate tempdir; +use resume::{ResumeKey,ResumeFile}; use self::tempdir::TempDir; use std::{fs,path,env}; @@ -60,12 +61,12 @@ fn unzip_command(value: &str, target: &str) -> Output { unwrap() } -fn progress_report<'a>(sequencer: &Digits<'a>) { +fn progress_report<'a>(sequencer: &Digits) { print!("{}..", sequencer.to_s()); // Verbose io::stdout().flush().unwrap(); } -fn has_reached_end<'a>(sequencer: &Digits<'a>, max: usize) -> Result<(), Error> { +fn has_reached_end<'a>(sequencer: &Digits, max: usize) -> Result<(), Error> { if sequencer.length() > max { return Err(Error::PasswordNotFound); } @@ -74,12 +75,12 @@ fn has_reached_end<'a>(sequencer: &Digits<'a>, max: usize) -> Result<(), Error> } pub fn aescrypt_core_loop<'a>( + characters: String, max: usize, - mut sequencer: Digits<'a>, + mut sequencer: Digits, target: &str, adj: Option<&str> ) -> Result<(), Error> { - loop { has_reached_end(&sequencer, max)?; progress_report(&sequencer); @@ -111,6 +112,15 @@ pub fn aescrypt_core_loop<'a>( break; } + + ResumeFile::save( + ResumeKey::new( + characters.clone(), + adj.map(str::to_string), + sequencer.clone(), + target.to_string() + ) + ); } Ok(()) @@ -133,12 +143,12 @@ fn any_file_contents(dir: &TempDir, omit: &str) -> bool { } pub fn unzip_core_loop<'a>( + characters: String, max: usize, - mut sequencer: Digits<'a>, + mut sequencer: Digits, target: &str, adj: Option<&str> ) -> Result<(), Error> { - if let Ok(dir) = TempDir::new("abrute") { let cwd = env::current_dir().unwrap(); let working = path::Path::new(&dir.path().as_os_str()).join(&target); @@ -182,6 +192,15 @@ pub fn unzip_core_loop<'a>( if !code.is_empty() { return code.pop().unwrap(); } + + ResumeFile::save( + ResumeKey::new( + characters.clone(), + adj.map(str::to_string), + sequencer.clone(), + target.to_string() + ) + ); } } else { return Err(Error::FailedTempDir); diff --git a/src/main.rs b/src/main.rs index 18d7fe7..22be4a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,12 @@ // http://opensource.org/licenses/MIT>, at your option. This file may not be // copied, modified, or distributed except according to those terms. +#![feature(try_from)] extern crate digits; extern crate rayon; use digits::Digits; use std::io::{self, Write}; +mod resume; mod result; use result::Error; use std::error::Error as StdError; @@ -93,7 +95,8 @@ USE OF THIS BINARY FALLS UNDER THE MIT LICENSE (c) 2017"). validate_start_string(&matches, max)?; let mapping = derive_character_base(matches.value_of("CHARACTERS").unwrap()); - let mut sequencer = Digits::new(&mapping, matches.value_of("start").unwrap_or("").to_string()); + let resume_key_chars = mapping_to_characters(&mapping); + let mut sequencer = Digits::new(mapping, matches.value_of("start").unwrap_or("").to_string()); sequencer.zero_fill(min as usize); let target = matches.value_of("TARGET").unwrap_or(""); @@ -102,10 +105,26 @@ USE OF THIS BINARY FALLS UNDER THE MIT LICENSE (c) 2017"). validate_and_prep_sequencer_adjacent(&mut sequencer, adjacent)?; validate_file_exists(&target)?; + // Begin Resume Feature + let starting = sequencer.to_s(); + use ::resume::{ResumeKey,ResumeFile}; + let cli_key = ResumeKey::new( + resume_key_chars.clone(), + adjacent.map(str::to_string), + sequencer, + target.to_string(), + ); + let latest = cli_key.latest(ResumeFile::load()); + let sequencer = latest.start; + if starting != sequencer.to_s() { + println!("Resuming from last save point: {}", sequencer.to_s()); + } + // End Resume Feature + if matches.is_present("zip") { - unzip_core_loop(max, sequencer, target, adjacent) + unzip_core_loop(resume_key_chars, max, sequencer, target, adjacent) } else { - aescrypt_core_loop(max, sequencer, target, adjacent) + aescrypt_core_loop(resume_key_chars, max, sequencer, target, adjacent) } } diff --git a/src/process_input.rs b/src/process_input.rs index 1035264..b172e6c 100644 --- a/src/process_input.rs +++ b/src/process_input.rs @@ -28,3 +28,10 @@ pub fn derive_character_base(characters: &str) -> BaseCustom { BaseCustom::::new(characters.chars().collect()) } +pub fn mapping_to_characters(m: &BaseCustom) -> String { + let mut crs = String::new(); + for x in 0..m.base as usize { + crs.push_str(&m.gen(x as u64)[..]); + } + crs +} diff --git a/src/result.rs b/src/result.rs index c020068..7dc3f6f 100644 --- a/src/result.rs +++ b/src/result.rs @@ -18,6 +18,7 @@ pub enum Error { InvalidRange, InvalidStringLength, PasswordNotFound, + MalformedResumeKey, UnzipMissing, } @@ -32,6 +33,7 @@ impl fmt::Display for Error { Error::InvalidRange => f.write_str("InvalidRange" ), Error::InvalidStringLength => f.write_str("InvalidStringLength" ), Error::PasswordNotFound => f.write_str("PasswordNotFound" ), + Error::MalformedResumeKey => f.write_str("MalformedResumeKey" ), Error::UnzipMissing => f.write_str("UnzipMissing" ), } } @@ -77,6 +79,11 @@ fn password_not_found() -> &'static str { "Password not found for given length and character set." } +#[inline] +fn malformed_resume_key() -> &'static str { + "The input data was not formatted properly for creating ResumeKey." +} + #[inline] fn unzip_missing() -> &'static str { "unzip does not appear to be installed." @@ -93,6 +100,7 @@ impl StdError for Error { Error::InvalidRange => invalid_range(), Error::InvalidStringLength => invalid_string_length(), Error::PasswordNotFound => password_not_found(), + Error::MalformedResumeKey => malformed_resume_key(), Error::UnzipMissing => unzip_missing(), } } diff --git a/src/resume.rs b/src/resume.rs new file mode 100644 index 0000000..801c4e1 --- /dev/null +++ b/src/resume.rs @@ -0,0 +1,224 @@ +use std::fmt::Display; +use std::fs; +use std::path::Path; +use std::fs::{File,OpenOptions}; +use std::fmt; +use std::io::{Read,Write,BufReader}; +use std::convert::TryFrom; +use ::result::Error::MalformedResumeKey; +extern crate array_tool; +use self::array_tool::vec::Shift; +extern crate digits; +use self::digits::prelude::*; + +pub(crate) struct ResumeKeyDB { + rkeys: Vec, +} + +impl ResumeKeyDB { + fn get(f: String) -> ResumeKeyDB { + let file = File::open(f); + assert!(file.is_ok()); + let mut buf_reader = BufReader::new(file.ok().unwrap()); + let mut contents = String::new(); + buf_reader.read_to_string(&mut contents).ok(); + + let mut db: Vec = vec![]; + for item in contents. + split("\n\n"). + map(|s| s.to_string()). + filter(|s| !s.is_empty()). + collect::>() { + let rk = ResumeKey::try_from(item); + if let Ok(key) = rk { + db.push(key); + } + } + ResumeKeyDB { rkeys: db } + } +} + +#[derive(Debug,Clone)] +pub(crate) struct ResumeKey { + characters: String, + adjacent: Option, + pub(crate) start: Digits, + target: String, +} + +impl PartialEq for ResumeKey { + fn eq(&self, other: &ResumeKey) -> bool { + self.start == other.start + } +} + +impl ResumeKey { + pub fn new(c: String,a: Option,s: Digits,t: String) -> ResumeKey { + ResumeKey { + characters: c, + adjacent: a, + start: s, + target: t, + } + } + + pub(crate) fn latest(self, db: ResumeKeyDB) -> ResumeKey { + db. + rkeys. + into_iter(). + filter(|rk| rk.characters == self.characters). + filter(|rk| rk.target == self.target). + filter(|rk| rk.start >= self.start). + max_by(|ref a, ref b| a.start.partial_cmp(&b.start).unwrap()). + unwrap_or(self) + } +} + +#[test] +fn can_pick_latest_resume_value() { + let b = BaseCustom::::new("asdf".chars().collect()); + let aa = Digits::new(b.clone(), "aaaa".to_string()); + let ab = Digits::new(b.clone(), "ssss".to_string()); + let ac = Digits::new(b.clone(), "dddd".to_string()); + let ad = Digits::new(b.clone(), "ffff".to_string()); + + let ra = ResumeKey { characters: "asdf".to_string(), adjacent: None, start: aa, target: "thing".to_string() }; + let rb = ResumeKey { characters: "asdf".to_string(), adjacent: None, start: ab, target: "thing".to_string() }; + let rc = ResumeKey { characters: "asdf".to_string(), adjacent: None, start: ac, target: "thing".to_string() }; + let rd = ResumeKey { characters: "asdf".to_string(), adjacent: None, start: ad, target: "thing".to_string() }; + + let db = ResumeKeyDB { rkeys: vec![ra,rd,rc] }; + + assert_eq!(rb.latest(db).start.to_s(),"ffff"); +} + +impl TryFrom for ResumeKey { + type Error = ::result::Error; + fn try_from(s: String) -> Result { + let mut values: Vec = s.split("\n").map(|s| s.to_string()).filter(|s| !s.is_empty()).collect(); + let c = values.shift().ok_or_else(|| return MalformedResumeKey)?; + let a = values.shift().ok_or_else(|| return MalformedResumeKey).unwrap(); + let a = match a.parse::() { + Ok(_) => Some(a), + _ => None, + }; + Ok(ResumeKey { + characters: c.clone(), + adjacent: a, + start: Digits::new( + BaseCustom::::new(c.chars().collect()), + values.shift().ok_or_else(|| return MalformedResumeKey)? + ), + target: values.shift().ok_or_else(|| return MalformedResumeKey)?, + }) + } +} + +impl Display for ResumeKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let a: String; + match self.adjacent { + Some(ref v) => a = v.clone(), + _ => a = "None".to_string(), + } + write!(f, + "{}\n{}\n{}\n{}\n\n", + self.characters, + a, + self.start.to_s(), + self.target + ) + } +} + +#[test] +fn input_and_output_for_resume_works() { + let r = ResumeKey { + characters: String::from("asdf"), + adjacent: Some("1".to_string()), + start: Digits::new({BaseCustom::::new("asdf".chars().collect())}, String::from("aaaa")), + target: String::from("thing.aes") + }; + + let mut file = File::create(".example.res").ok().unwrap(); + file.write_all(&format!("{}", r).as_bytes()).ok(); + + let keys = ResumeKeyDB::get(".example.res".to_string()); + let k = keys.rkeys.iter().next(); + assert_eq!(Some(&r), k); + + let _a = fs::remove_file(".example.res"); + assert!(!Path::new(".example.res").exists(), "`.example.res` cleanup failed!"); +} + +#[derive(Debug)] +pub(crate) struct ResumeFile; + +impl ResumeFile { + pub(crate) fn save(rk: ResumeKey) { + let file = OpenOptions::new().append(true).create(true).open(".abrute"); + assert!(file.is_ok(), "Failed to create or open file `.abrute` for resuming work!"); + + if let Ok(mut f) = file { + f.write_all(&format!("{}", rk).as_bytes()).ok(); + let _a = f.sync_data(); + } + + ResumeFile::backup() + } + + fn backup() { + let file = ".abrute"; + let backup = ".abrute.bak"; + fs::copy(file, backup). + expect("Failed to backup `.abrute` to `.abrute.bak`!"); + } + + pub(crate) fn load() -> ResumeKeyDB { + let file = ".abrute"; + if Path::new(file).exists() { + let db = ResumeKeyDB::get(file.to_owned()); + if !db.rkeys.is_empty() { return db; } + } + + let backup = ".abrute.bak"; + if Path::new(backup).exists() { + let db = ResumeKeyDB::get(file.to_owned()); + if !db.rkeys.is_empty() { return db; } + } + + return ResumeKeyDB { rkeys: vec![] }; + } + + #[allow(dead_code)] + pub(crate) fn purge() { + let _a = fs::remove_file(".abrute"); + let _b = fs::remove_file(".abrute.bak"); + } +} + +#[test] +fn it_backup_system_works() { + let bc = BaseCustom::::new("asdf".chars().collect()); + let aa = Digits::new(bc.clone(), "aaaa".to_string()); + let ab = Digits::new(bc.clone(), "ssss".to_string()); + + let ra = ResumeKey { characters: "asdf".to_string(), adjacent: None, start: aa, target: "thing".to_string() }; + let rb = ResumeKey { characters: "asdf".to_string(), adjacent: None, start: ab, target: "thing".to_string() }; + + ResumeFile::save(ra.clone()); + ResumeFile::save(rb.clone()); + + assert!(Path::new(".abrute").exists(), "`.abrute` not found!"); + assert!(Path::new(".abrute.bak").exists(), "`.abrute.bak` not found!"); + + let loaded_db = ResumeFile::load(); + + assert!(loaded_db.rkeys.contains(&ra), "`.abrute` backup did not contain first key backup!"); + assert!(loaded_db.rkeys.contains(&rb), "`.abrute` backup did not contain second key backup!"); + + ResumeFile::purge(); + + assert!(!Path::new(".abrute").exists(), "`.abrute` cleanup failed!"); + assert!(!Path::new(".abrute.bak").exists(), "`.abrute.bak` cleanup failed!"); +} diff --git a/src/validators.rs b/src/validators.rs index 92d322b..cf87334 100644 --- a/src/validators.rs +++ b/src/validators.rs @@ -36,7 +36,7 @@ pub fn validate_start_string(matches: &clap::ArgMatches, max: usize) -> Result<( } pub fn validate_and_prep_sequencer_adjacent<'a>( - sequencer: &mut Digits<'a>, + sequencer: &mut Digits, adjacent: Option<&str> ) -> Result<(), Error> {