diff --git a/.travis.yml b/.travis.yml index 30a8acc..bc137cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,21 +9,25 @@ matrix: rust: nightly env: TARGET=x86_64-unknown-linux-gnu +addons: + apt: + packages: + - clang-3.9 + - curl + - libclang-3.9-dev + cache: cargo: true directories: - "$HOME/lib" -before_install: - - if [ "$TRAVIS_OS_NAME" == "osx" ]; then brew install libsodium; fi - - if [ "$TRAVIS_OS_NAME" == "linux" ]; then source ./ci/linux-libsodium-path.sh; fi - install: - - if [ "$TRAVIS_OS_NAME" == "linux" ]; then bash ./ci/linux-build-libsodium.sh; fi + - source ./ci/libsodium-build.sh + - source ./ci/libsodium-env.sh script: - cargo build --verbose --target "${TARGET}" - - cargo test --verbose --target "${TARGET}" + - cargo test --verbose before_deploy: - cargo build --release --verbose --target "${TARGET}" diff --git a/Cargo.lock b/Cargo.lock index d39a9a6..f882389 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,6 +526,16 @@ dependencies = [ "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rpassword" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-demangle" version = "0.1.9" @@ -566,6 +576,7 @@ dependencies = [ "log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "quicli 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "sodiumoxide 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "spinners 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -893,6 +904,7 @@ dependencies = [ "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum regex 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5bbbea44c5490a1e84357ff28b7d518b4619a159fed5d25f6c1de2d19cc42814" "checksum regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "747ba3b235651f6e2f67dfa8bcdcd073ddb7c243cb21c442fc12395dfcac212d" +"checksum rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d127299b02abda51634f14025aec43ae87a7aa7a95202b6a868ec852607d1451" "checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" diff --git a/Cargo.toml b/Cargo.toml index a0beacd..e1b2479 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ libflate = "0.1.0" log = "0.4.0" quicli = "0.3.0" rand = "0.5.5" +rpassword = "2.0.0" sodiumoxide = "0.1.0" spinners = "1.0.0" structopt = "0.2.10" diff --git a/README.md b/README.md index 000339f..eccf67d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,50 @@ # sneakercopy -## Requirements +A tool for creating encrypted archives for handling sensitive content. -- [libsodium](https://github.com/jedisct1/libsodium) - - macOS: `brew install libsodium` - - Linux: Tested down to libsodium v1.0.11 (`libsodium18` on Debian 9.5) +Sneakercopy stands on the shoulders of giants such as [tar], +[sodiumoxide] / [libsodium], and [libflate] to pack, compress, +and encrypt sensitive files into a light container called a "tarbox". + +We use the system defined in [RFC2289] to generate short, memorable, +easily writable passwords. `libsodium`'s `scryptsalsa208sha256` is used to derive +a hash to encrypt the compressed data stream with. + +[tar]: https://crates.io/crates/tar +[sodiumoxide]: https://crates.io/crates/sodiumoxide +[libsodium]: https://github.com/jedisct1/libsodium +[libflate]: https://crates.io/crates/libflate +[RFC2289]: https://tools.ietf.org/html/rfc2289 + +## Usage + +### Seal a file/directory + +``` +# Creates `directory.tarbox` in the current directory +λ sneakercopy seal /path/to/directory +⢀⠀ Packing... +secret: FOWL-BON-MEMO-ROSY-HORN + +# Creates `configs.tarbox` in `/var/backups` +λ sneakercopy seal -o /var/backups/configs.tarbox /etc +⢀⠀ Packing... +secret: ROAD-SHIN-TAKE-OLDY-YANK +``` + +### Unseal a tarbox + +``` +# Unseals the contents of `directory.tarbox` into current directory +λ sneakercopy unseal ./directory.tarbox FOWL-BON-MEMO-ROSY-HORN + +# Unseals the contents of `configs.tarbox` into `/etc` +λ sneakercopy unseal -C /etc/ /var/backups/configs.tarbox ROAD-SHIN-TAKE-OLDY-YANK +``` ## Compiling -- Install `libsodium` +- Use `./ci/libsodium-build.sh` to prepare a static `libsodium` installation +- Set up build flags with `./ci/libsodium-env.sh` - `cargo build` - Done! \ No newline at end of file diff --git a/ci/libsodium-build.sh b/ci/libsodium-build.sh new file mode 100644 index 0000000..2945405 --- /dev/null +++ b/ci/libsodium-build.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +LIBSODIUM_VERSION=${LIBSODIUM_VERSION:-1.0.16} + +mkdir -p $HOME/lib/libsodium +curl -sSL -olibsodium.tar.gz https://github.com/jedisct1/libsodium/releases/download/${LIBSODIUM_VERSION}/libsodium-${LIBSODIUM_VERSION}.tar.gz +tar xvfz libsodium.tar.gz --strip-components 1 -C $HOME/lib/libsodium +pushd $HOME/lib/libsodium && \ + ./configure \ + --prefix=$HOME/lib/libsodium \ + --disable-debug \ + --disable-dependency-tracking \ + --disable-shared && \ + make && \ + make install && \ + popd diff --git a/ci/libsodium-env.sh b/ci/libsodium-env.sh new file mode 100644 index 0000000..eba5248 --- /dev/null +++ b/ci/libsodium-env.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +export PKG_CONFIG_PATH=$HOME/lib/libsodium/lib/pkgconfig:$PKG_CONFIG_PATH +export LD_LIBRARY_PATH=$HOME/lib/libsodium/lib:$LD_LIBRARY_PATH + +export SODIUM_STATIC=true +export SODIUM_LIB_DIR=$HOME/lib/libsodium/src/libsodium/.libs +export SODIUM_INC_DIR=$HOME/lib/libsodium/src/libsodium/include \ No newline at end of file diff --git a/ci/linux-build-libsodium.sh b/ci/linux-build-libsodium.sh deleted file mode 100755 index 542f759..0000000 --- a/ci/linux-build-libsodium.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -mkdir -p $HOME/lib -wget https://github.com/jedisct1/libsodium/releases/download/1.0.15/libsodium-1.0.15.tar.gz -tar xvfz libsodium-1.0.15.tar.gz -cd libsodium-1.0.15 && ./configure --prefix=$HOME/lib/libsodium && make && make install && cd .. diff --git a/ci/linux-libsodium-path.sh b/ci/linux-libsodium-path.sh deleted file mode 100644 index f6cb6a3..0000000 --- a/ci/linux-libsodium-path.sh +++ /dev/null @@ -1,2 +0,0 @@ -export PKG_CONFIG_PATH=$HOME/lib/libsodium/lib/pkgconfig:$PKG_CONFIG_PATH -export LD_LIBRARY_PATH=$HOME/lib/libsodium/lib:$LD_LIBRARY_PATH \ No newline at end of file diff --git a/src/bin/sneakercopy.rs b/src/bin/sneakercopy.rs index 29c6c26..4b1d9c1 100644 --- a/src/bin/sneakercopy.rs +++ b/src/bin/sneakercopy.rs @@ -1,6 +1,7 @@ #![recursion_limit = "1024"] #![feature(try_from)] +extern crate rpassword; #[macro_use] extern crate quicli; extern crate sneakercopy; @@ -51,7 +52,7 @@ enum Subcommand { path: PathBuf, #[structopt(help = "Password used for encryption")] - password: String, + password: Option, #[structopt( short = "C", @@ -87,12 +88,12 @@ fn entrypoint(args: Cli) -> sneakercopy::errors::Result<()> { path, output, force, - } => seal_subcmd(&args, path, output, force)?, + } => seal_subcmd(&args, &path.canonicalize().unwrap(), output, force)?, Subcommand::Unseal { path, password, dest, - } => unseal_subcmd(&args, path, dest, password)?, + } => unseal_subcmd(&args, &path.canonicalize().unwrap(), dest, password)?, } Ok(()) @@ -127,12 +128,17 @@ fn unseal_subcmd( _args: &Cli, path: &PathBuf, dest: &Option, - password: &String, + password: &Option, ) -> sneakercopy::errors::Result<()> { check_path(&path)?; + let password = password.clone().unwrap_or_else(|| { + return rpassword::prompt_password_stdout("secret: ") + .expect("can't open tarbox without a secret!"); + }); + let sb = tarbox::TarboxSecretBuilder::new(); - let sb = sb.password(password.clone()); + let sb = sb.password(password); let dest = dest.clone().unwrap_or(path.parent().unwrap().to_path_buf()); unseal_path(&path, &dest, sb)?; diff --git a/src/password.rs b/src/password.rs index 659e640..5873ffd 100644 --- a/src/password.rs +++ b/src/password.rs @@ -1,9 +1,11 @@ use rand::{prng, seq, thread_rng, SeedableRng}; +const PASSWORD_WORD_COUNT: usize = 6; + // generate a reasonable password pub fn generate_password() -> String { let mut rng = prng::chacha::ChaChaRng::from_rng(thread_rng()).unwrap(); - let sample = seq::sample_iter(&mut rng, WORDS.into_iter(), 5).unwrap(); + let sample = seq::sample_iter(&mut rng, WORDS.into_iter(), PASSWORD_WORD_COUNT).unwrap(); sample .into_iter() .map(|x| String::from(*x)) diff --git a/src/tarbox/attributes.rs b/src/tarbox/attributes.rs index fd698ee..d757951 100644 --- a/src/tarbox/attributes.rs +++ b/src/tarbox/attributes.rs @@ -33,6 +33,10 @@ impl Attributes { VERSION } + pub fn attr_block_size() -> usize { + (NONCEBYTES + SALTBYTES) as usize + } + pub fn nonce(&self) -> &NonceBytes { &self.nonce } @@ -42,17 +46,18 @@ impl Attributes { } pub fn from_bytes(source: Vec) -> errors::Result { + let expected: usize = NONCEBYTES + SALTBYTES; + let actual: usize = source.len(); + if actual > expected { + bail!(errors::ErrorKind::SourceTooLarge(expected, actual)); + } + let mut nonce = [0; NONCEBYTES]; nonce.copy_from_slice(&source[..NONCEBYTES]); let mut salt = [0; SALTBYTES]; salt.copy_from_slice(&source[NONCEBYTES..NONCEBYTES + SALTBYTES]); - let remaining = source.len() - (NONCEBYTES + SALTBYTES); - if remaining > 0 { - bail!(errors::ErrorKind::SourceNotFullyDrained(remaining)); - } - Ok(Attributes::new(nonce, salt)) } @@ -131,10 +136,10 @@ mod tests { let res = Attributes::from_bytes(source); assert!(res.is_err()); let err = res.unwrap_err(); - if let errors::Error(errors::ErrorKind::SourceNotFullyDrained(num), _) = err { + if let errors::Error(errors::ErrorKind::SourceTooLarge(_, actual), _) = err { assert_eq!( - 2, num, - "only 2 bytes were expected to be remaining (undrained)" + 58, actual, + "only 56 bytes were expected in attrs source (2 undrained)" ); } else { panic!(format!( diff --git a/src/tarbox/decoder.rs b/src/tarbox/decoder.rs index 24e1d62..8402770 100644 --- a/src/tarbox/decoder.rs +++ b/src/tarbox/decoder.rs @@ -36,14 +36,9 @@ impl Decoder { // Now that we have verified the version of the header, // read all bytes until a `NUL` is encountered. - let attrs_data: Vec<_> = inner - .clone() - .into_iter() - .take_while(|b| *b != 0x0) - .collect(); + let attrs_data: Vec<_> = inner.drain(..Attributes::attr_block_size()).collect(); - let attrs = Attributes::from_bytes(attrs_data.clone())?; - inner.drain(..attrs_data.len()); + let attrs = Attributes::from_bytes(attrs_data)?; // The next byte we read should be a `NUL`. let next = inner.remove(0); @@ -154,8 +149,8 @@ mod tests { assert!(res.is_err()); let err = res.unwrap_err(); - if let errors::Error(errors::ErrorKind::SourceNotFullyDrained(num), _) = err { - assert_eq!(2, num, "expected undrained data to be length of inner file"); + if let errors::Error(errors::ErrorKind::ExpectedNullByte(actual), _) = err { + assert_eq!(0xfa, actual, "expected first byte of payload, not {}", actual); } else { panic!(format!( "expected `SourceNotFullyDrained` error, got: {:?}", diff --git a/src/tarbox/errors.rs b/src/tarbox/errors.rs index 0b75a1a..eaf5cf3 100644 --- a/src/tarbox/errors.rs +++ b/src/tarbox/errors.rs @@ -20,9 +20,9 @@ error_chain! { display("invalid key data: {}", kd), } - SourceNotFullyDrained(size: usize) { - description("source vector was not fully drained"), - display("source vector was not fully drained: {} elements remaining", size), + SourceTooLarge(expected: usize, actual: usize) { + description("source vector is too large"), + display("source vector is too large: {} expected < {} actual", expected, actual), } VersionMismatch(expected: u8, actual: u8) {