Compare commits

...

18 Commits

25 changed files with 801 additions and 362 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store .DS_Store
/target /target
/local /local
/.vscode

68
Cargo.lock generated
View File

@ -10,7 +10,7 @@ checksum = "495ee669413bfbe9e8cace80f4d3d78e6d8c8d99579f97fb93bde351b185f2d4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cipher", "cipher",
"cpufeatures", "cpufeatures 0.1.5",
"ctr", "ctr",
"opaque-debug", "opaque-debug",
] ]
@ -26,19 +26,20 @@ dependencies = [
[[package]] [[package]]
name = "argon2" name = "argon2"
version = "0.2.1" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d60f5f3113c903294dc81dd8cf0012963ed4dda8bc931c864e12e175356ff98b" checksum = "d805bb12b532be9ce066df7913311f43716b41d8d780e9322113e8a6ae7c41ab"
dependencies = [ dependencies = [
"base64ct",
"blake2", "blake2",
"password-hash", "password-hash",
] ]
[[package]] [[package]]
name = "assert_cmd" name = "assert_cmd"
version = "1.0.6" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2dfc8228c6260bf620fc5a341afa8e27edcde388b19ffc5732320bfe657eb2" checksum = "54f002ce7d0c5e809ebb02be78fd503aeed4a511fd0fcaff6e6914cbdabbfa33"
dependencies = [ dependencies = [
"bstr", "bstr",
"doc-comment", "doc-comment",
@ -101,13 +102,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chacha20" name = "chacha20"
version = "0.7.1" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cipher", "cipher",
"cpufeatures", "cpufeatures 0.2.1",
] ]
[[package]] [[package]]
@ -143,6 +144,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "cpufeatures"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crypto-mac" name = "crypto-mac"
version = "0.8.0" version = "0.8.0"
@ -184,10 +194,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "difference" name = "difflib"
version = "2.0.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]] [[package]]
name = "digest" name = "digest"
@ -200,7 +210,7 @@ dependencies = [
[[package]] [[package]]
name = "doby" name = "doby"
version = "0.1.0" version = "0.3.0"
dependencies = [ dependencies = [
"aes", "aes",
"argon2", "argon2",
@ -208,12 +218,12 @@ dependencies = [
"blake2", "blake2",
"chacha20", "chacha20",
"clap", "clap",
"cpufeatures", "cpufeatures 0.2.1",
"hkdf", "hkdf",
"hmac",
"num_enum", "num_enum",
"rand", "rand",
"rpassword", "rpassword",
"subtle",
"tempfile", "tempfile",
"zeroize", "zeroize",
] ]
@ -224,6 +234,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.4" version = "0.14.4"
@ -274,6 +290,15 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "itertools"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
dependencies = [
"either",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -282,9 +307,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.97" version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
[[package]] [[package]]
name = "memchr" name = "memchr"
@ -322,9 +347,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "password-hash" name = "password-hash"
version = "0.2.1" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1a5d4e9c205d2c1ae73b84aab6240e98218c0e72e63b50422cfb2d1ca952282" checksum = "7ad7268ef9bc463fddde8361d358fbfae1aeeb1fb62eca111cd8c763bf1c5891"
dependencies = [ dependencies = [
"base64ct", "base64ct",
"rand_core", "rand_core",
@ -339,11 +364,12 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]] [[package]]
name = "predicates" name = "predicates"
version = "1.0.8" version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df" checksum = "c143348f141cc87aab5b950021bac6145d0e5ae754b0591de23244cee42c9308"
dependencies = [ dependencies = [
"difference", "difflib",
"itertools",
"predicates-core", "predicates-core",
] ]

View File

@ -1,11 +1,12 @@
[package] [package]
name = "doby" name = "doby"
version = "0.1.0" version = "0.3.0"
edition = "2018" edition = "2021"
authors = ["Hardcore Sushi <hardcore.sushi@disroot.org>"] authors = ["Hardcore Sushi <hardcore.sushi@disroot.org>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
description = "Secure symmetric encryption from the command line" description = "Simple, secure and lightweight symmetric encryption from the command line"
readme = "README.md" readme = "README.md"
repository = "https://forge.chapril.org/hardcoresushi/doby"
[profile.release] [profile.release]
lto = true lto = true
@ -18,16 +19,16 @@ opt-level = 3
clap = "2.33" clap = "2.33"
rand = "0.8" rand = "0.8"
num_enum = "0.5" num_enum = "0.5"
cpufeatures = "0.1" cpufeatures = "0.2"
aes = { version = "0.7", features = ["ctr"] } aes = { version = "0.7", features = ["ctr"] }
chacha20 = "0.7" chacha20 = "0.8"
hmac = "0.11" subtle = "2.4"
blake2 = "0.9" blake2 = "0.9"
hkdf = "0.11" hkdf = "0.11"
argon2 = "0.2" argon2 = "0.3"
rpassword = "5.0" rpassword = "5.0"
zeroize = "1.3" zeroize = "1.3"
[dev-dependencies] [dev-dependencies]
assert_cmd = "1.0" assert_cmd = "2.0"
tempfile = "3.0" tempfile = "3.0"

154
README.md
View File

@ -1,37 +1,34 @@
# doby # doby
Secure symmetric encryption from the command line. Simple, secure and lightweight symmetric encryption from the command line.
doby started as a fork of [aef](https://github.com/wyhaya/aef) by [wyhaya](https://github.com/wyhaya). It aims to replace the [ccrypt](http://ccrypt.sourceforge.net) tool which is a bit old and not very secure. doby started as a fork of [aef](https://github.com/wyhaya/aef) by [wyhaya](https://github.com/wyhaya) with the goal of becoming a simple, fast and lightweight CLI utility for symmetric encryption. It aims to be an alternative to the old [ccrypt](http://ccrypt.sourceforge.net) tool by using modern cryptography and authenticated encryption.
# Features # Features
* Fast: written in [rust](https://www.rust-lang.org), encrypts with [AES-256-CTR](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)) or [XChaCha20](https://en.wikipedia.org/wiki/Salsa20#XChaCha) * Fast: written in [rust](https://www.rust-lang.org), encrypts with [AES-256-CTR](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)) or [XChaCha20](https://en.wikipedia.org/wiki/Salsa20#XChaCha)
* [HMAC](https://en.wikipedia.org/wiki/HMAC) ciphertext authentication * [HMAC](https://en.wikipedia.org/wiki/HMAC) ciphertext authentication
* Password brute-force resistance with [Argon2](https://en.wikipedia.org/wiki/Argon2) * Password brute-force resistance with [Argon2](https://en.wikipedia.org/wiki/Argon2)
* Increase the plaintext size of only 158 bytes * Increase the plaintext size of only 113 bytes
* Encryption from STDIN/STDOUT or from files * Encryption from STDIN/STDOUT or from files
* Adjustable performance & secuity parameters * Adjustable performance & security parameters
# Disclamer # Disclaimer
doby is provided "as is", without any warranty of any kind. I'm not a professional cryptographer. This program didn't receive any security audit and therefore __shouldn't be considered fully secure__. doby is provided "as is", without any warranty of any kind. I'm not a professional cryptographer. This program didn't receive any security audit and therefore __shouldn't be considered fully secure__.
# Usage # Usage
Encryption Encryption:
```bash ```bash
doby my-super-secret-source-code.rs encrypted.doby doby my-super-secret-source-code.rs encrypted.doby
``` ```
Decryption Decryption:
```bash ```bash
doby encrypted.doby decrypted.rs doby encrypted.doby decrypted.rs
``` ```
If you ommit file path or use `-`, doby operates from `stdin/stdout` If you omit file path or use `-`, doby operates from `stdin/stdout`:
```bash ```bash
# Read from stdin and write to stdout # Read from stdin and write to stdout
cat my-super-secret-music.flac | doby > encrypted.doby cat my-super-secret-music.flac | doby > encrypted.doby
@ -43,22 +40,19 @@ doby encrypted.doby > decrypted.flac
cat my-super-secret-logs-file.log | doby - logs.doby cat my-super-secret-logs-file.log | doby - logs.doby
``` ```
Speicfy password from the command line Specify password from the command line:
```bash ```bash
doby -p "A super very ultra strong passphrase" my-super-secret-document.pdf document.doby doby --password "A super very ultra strong passphrase" my-super-secret-document.pdf document.doby
``` ```
Double encryption Double encryption:
```bash ```bash
doby -p "first password" my-super-secret-database.db | doby -f - double-encrypted.doby doby --password "first password" my-super-secret-database.db | doby -f - double-encrypted.doby
``` ```
Increase password brute-force resistance Increase password brute-force resistance:
```bash ```bash
echo "you-will-never-break-this" | doby --memory-cost 524288 --threads 16 --iterations 40 > my-super-secret-password.doby echo "you-will-never-break-this" | doby --memory-cost 524288 --parallelism 16 --time-cost 40 > my-super-secret-data.doby
``` ```
## Full Options ## Full Options
@ -69,14 +63,15 @@ USAGE:
FLAGS: FLAGS:
-f, --force-encrypt Encrypt even if doby format is recognized -f, --force-encrypt Encrypt even if doby format is recognized
-i, --interactive Prompt before overwriting files
-h, --help Prints help information -h, --help Prints help information
-V, --version Prints version information -V, --version Prints version information
OPTIONS: OPTIONS:
-p, --password <password> Password used to derive encryption keys --password <password> Password used to derive encryption keys
-i, --iterations <iterations> Argon2 time cost [default: 10] -t, --time-cost <iterations> Argon2 time cost [default: 10]
-m, --memory-cost <memory cost> Argon2 memory cost (in kilobytes) [default: 4096] -m, --memory-cost <memory size> Argon2 memory cost (in kilobytes) [default: 4096]
-t, --threads <threads> Argon2 parallelism (between 1 and 255) [default: 4] -p, --parallelism <threads> Argon2 parallelism cost [default: 4]
-b, --block-size <blocksize> Size of the I/O buffer (in bytes) [default: 65536] -b, --block-size <blocksize> Size of the I/O buffer (in bytes) [default: 65536]
-c, --cipher <cipher> Encryption cipher to use [possible values: aes, xchacha20] -c, --cipher <cipher> Encryption cipher to use [possible values: aes, xchacha20]
@ -85,27 +80,58 @@ ARGS:
<OUTPUT> <PATH> | "-" or empty for stdout <OUTPUT> <PATH> | "-" or empty for stdout
``` ```
# Build # Installation
You can download doby from the "Releases" section in this repo.
You should verify commits before building the binary. You can download my PGP key from keyservers: All releases MUST be signed with my PGP key available on keyservers. To import it:
```bash
```
gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 007F84120107191E gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 007F84120107191E
``` ```
Fingerprint: `BD56 2147 9E7B 74D3 6A40 5BE8 007F 8412 0107 191E` \ Fingerprint: `BD56 2147 9E7B 74D3 6A40 5BE8 007F 8412 0107 191E` \
Email: `Hardcore Sushi <hardcore.sushi@disroot.org>` Email: `Hardcore Sushi <hardcore.sushi@disroot.org>`
Once imported: Then, save the PGP-signed message to a file and run:
```bash
gpg --verify <the file>
```
__Don't continue if the verification fails!__
If everything goes fine, you can download the package corresponding to your distribution. To verify it, compute its SHA-256 hash:
```bash
sha256sum <file>
```
Compare the output and the hash in the PGP-signed message. If the hashes match, the file is authenticated and you can continue the installation.
On debian:
```bash
sudo dpkg -i doby-*.deb
```
On Arch:
```bash
sudo pacman -U doby-*.pkg.tar.zst
```
On other distros:
```bash
tar -xzf doby-*.tar.gz && sudo doby/install.sh
```
# Build
You should verify commits before building the binary. Follow the steps in [Installation](#installation) to import my PGP key.
```bash ```bash
git clone --depth=1 https://forge.chapril.org/hardcoresushi/doby.git git clone --depth=1 https://forge.chapril.org/hardcoresushi/doby.git
cd doby cd doby
git verify-commit HEAD git verify-commit HEAD #you need to import my PGP key to verify the commit signature
cargo build --release #outputs to ./target/release/doby cargo build --release --bin doby #outputs to ./target/release/doby
``` ```
# Cryptographic details # Cryptographic details
The following explanations are illustrated with pseudo rust code to simplify understanding. If you want to see how it's exactly implemented in doby, you can always check the source code.
### Encryption ### Encryption
doby first derives your password with Argon2 (version 19) in Argon2id mode with a 64 bytes long random salt. A `master_key` of 32 bytes is thus generated. doby first derives your password with Argon2 (version 19) in Argon2id mode with a 64 bytes long random salt. A `master_key` of 32 bytes is thus generated.
@ -120,7 +146,7 @@ let master_key: [u8; 32] = argon2id(
); );
``` ```
Then, doby uses [HKDF](https://en.wikipedia.org/wiki/HKDF) with the previous random salt to compute the `encryption_key` and the `authentication_key`. Then, doby uses [HKDF](https://en.wikipedia.org/wiki/HKDF) with the previous random salt to compute the `nonce`, the `encryption_key` and the `authentication_key`.
```rust ```rust
let hkdf = Hkdf::new( let hkdf = Hkdf::new(
@ -128,31 +154,34 @@ let hkdf = Hkdf::new(
master_key, //ikm master_key, //ikm
blake2b, //hash function blake2b, //hash function
); );
let nonce: [u8; 16] = hkdf.expand(b"doby_nonce"); //(16 bytes for AES-CTR, 24 for XChaCha20)
let encryption_key: [u8; 32] = hkdf.expand(b"doby_encryption_key"); let encryption_key: [u8; 32] = hkdf.expand(b"doby_encryption_key");
let authentication_key: [u8; 32] = hkdf.expand(b"doby_authentication_key"); let authentication_key: [u8; 32] = hkdf.expand(b"doby_authentication_key");
``` ```
NOTE: To reduce the size of the header, the `nonce` is derived from the `master_key` instead of being generated purely at random then stored in the encrypted file.
Next, doby initializes a [BLAKE2b](https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2) HMAC with `authentication_key` and add all public encryption parameters to it. Next, doby initializes a [BLAKE2b](https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2) HMAC with `authentication_key` and add all public encryption parameters to it.
```rust ```rust
let hmac = Hmac::new( let hmac = Blake2b::new_keyed(
authentication_key, authentication_key,
blake2b, //hash function 32, //digest size
); );
hmac.update(random_salt); hmac.update(random_salt);
//integers are encoded in big-endian
hmac.update(argon2_time_cost); hmac.update(argon2_time_cost);
hmac.update(argon2_memory_cost); hmac.update(argon2_memory_cost);
hmac.update(argon2_parallelism); hmac.update(argon2_parallelism);
hmac.update(cipher); //1-byte representation of the symmetric cipher used to encrypt (either AES-CTR or XChaCha20) hmac.update(cipher); //1-byte representation of the symmetric cipher used to encrypt (either AES-CTR or XChaCha20)
hmac.update(random_nonce); //random nonce used for encryption (16 bytes for AES-CTR, 24 for XChaCha20)
``` ```
All this parameters are also written in plain text in the header of the doby output. All this parameters are also written in plain text in the header of the doby output.
Now, doby initializes a symmetric cipher with `encryption_key` and `random_nonce` (either AES-CTR or XChaCha20, based on the `--cipher` option) and starts the actual encryption. It reads chunks from the plaintext (according to the `--block-size` parameter), encrypts them with the cipher and updates the HMAC with the ciphertext. Now, doby initializes a symmetric cipher with `encryption_key` and `nonce` (either AES-CTR or XChaCha20, based on the `--cipher` option) and starts the actual encryption. It reads chunks from the plaintext (according to the `--block-size` parameter), encrypts them with the cipher and updates the HMAC with the ciphertext.
```rust ```rust
let cipher = Aes256Ctr::new(encryption_key, random_nonce); //example with AES-CTR let cipher = Aes256Ctr::new(encryption_key, nonce); //example with AES-CTR
let mut n = 1; let mut n = 1;
let mut chunk: [u8; block_size] = [0; block_size]; let mut chunk: [u8; block_size] = [0; block_size];
while n != 0 { while n != 0 {
@ -169,6 +198,41 @@ Once the whole plaintext is encrypted, doby computes and appends the HMAC to the
output.write(hmac.digest()); output.write(hmac.digest());
``` ```
So here is what an encrypted file layout looks like:
<table>
<tr>
<th align="left">Magic bytes</th>
<td>4 bytes</td>
</tr>
<tr>
<th align="left">Salt</th>
<td>64 bytes</td>
</tr>
<tr>
<th align="left" rowspan="3">Argon2 parameters</th>
<td>Time cost: 4 bytes</td>
</tr>
<tr>
<td>Memory cost: 4 bytes</td>
</tr>
<tr>
<td>Parallelism cost: 1 byte</td>
</tr>
<tr>
<th align="left">Encryption cipher</th>
<td>1 byte</td>
</tr>
<tr>
<th align="left">Ciphertext</th>
<td>Exact same size as the plaintext</td>
</tr>
<tr>
<th align="left">HMAC</th>
<td>32 bytes</td>
</tr>
</table>
### Decryption ### Decryption
doby reads the public encryption values from the input header to get all parameters needed to re-derive the `master_key` from the password with Argon2. doby reads the public encryption values from the input header to get all parameters needed to re-derive the `master_key` from the password with Argon2.
@ -183,12 +247,12 @@ let master_key: [u8; 32] = argon2id(
); );
``` ```
`encryption_key` and `authentication_key` are computed from `master_key` in the same way as during encryption. The HMAC is also initialized and updated with the values read from the header. `nonce`, `encryption_key` and `authentication_key` are computed from `master_key` in the same way as during encryption. The HMAC is also initialized and updated with the values read from the header.
Then, doby starts decryption. Then, doby starts decryption.
```rust ```rust
let cipher = XChaCha20::new(encryption_key, nonce_read_from_input); //example with XChaCha20 let cipher = XChaCha20::new(encryption_key, nonce); //example with XChaCha20
let mut n = 1; let mut n = 1;
let mut chunk: [u8; block_size] = [0; block_size]; let mut chunk: [u8; block_size] = [0; block_size];
while n != 0 { while n != 0 {
@ -202,9 +266,19 @@ while n != 0 {
Once the whole ciphertext is decrypted, doby computes and verifies the HMAC. Once the whole ciphertext is decrypted, doby computes and verifies the HMAC.
```rust ```rust
hmac.digest() == last_64_bytes_read // the default blake2b output size is 64 bytes hmac.digest() == last_32_bytes_read
``` ```
If the verification success, the file is successfully decrypted and authenticated. If the verification success, the file is successfully decrypted and authenticated.
_If you find any weakness or security issue is this protocol, please open an issue._ _If you find any weakness or security issue is this protocol, please open an issue._
## Why not using authenticated encryption such as AES-GCM instead of AES-CTR + HMAC ?
In order to encrypt data larger than memory, we need to split the plaintext into several smaller chunks and encrypt each of these chunks one by one. With authenticated encryption such as AES-GCM, this involves adding an authentication tag to each chunk. As a result, the final ciphertext size would be:
```
ciphertext size = plaintext size + (number of chunks ྾ tag size)
```
For example, a 50MB file encrypted with AES-GCM by chunks of 64KiB would be 12.2KB larger than the original plaintext, just to authenticate the file.
doby solves this problem by performing authentication independently of encryption. By using AES-CTR, the ciphertext remains the same size as the plaintext. The HMAC can be computed incrementally, one chunk at a time. Only one hash needs to be included in the final file. Thus, doby encrypted files are only 142 bytes larger than the plaintext, no matter how big the original file is.

63
completions/bash Normal file
View File

@ -0,0 +1,63 @@
#/usr/bin/env bash
_remove_opts() {
local opt new_opts
for opt in ${available_opts}; do
if [[ $opt != $1 && $opt != $2 ]]; then
new_opts+="$opt "
fi
done
available_opts=$new_opts
}
_doby_completion() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local opts="-f --force-encrypt -i --interactive -h --help -V --version --password -t --time-cost -m --memory-cost -p --parallelism -b --block-size -c --cipher"
if [[ ${cur} == -* ]]; then
local i available_opts=$opts
for i in ${COMP_WORDS[@]}; do
if [[ ${opts[*]} =~ $i ]]; then
case $i in
"-f"|"--force-encrypt")
_remove_opts "-f" "--force-encrypt"
;;
"-i"|"--interactive")
_remove_opts "-i" "--interactive"
;;
"-h"|"--help")
_remove_opts "-h" "--help"
;;
"-V"|"--version")
_remove_opts "-V" "--version"
;;
"--password")
_remove_opts "--password"
;;
"-t"|"--time-cost")
_remove_opts "-t" "--time-cost"
;;
"-m"|"--memory-cost")
_remove_opts "-m" "--memory-cost"
;;
"-p"|"--parallelism")
_remove_opts "-p" "--parallelism"
;;
"-b"|"--block-size")
_remove_opts "-b" "--block-size"
;;
"-c"|"--cipher")
_remove_opts "-c" "--cipher"
;;
esac
fi
done
COMPREPLY=($(compgen -W "${available_opts}" -- "${cur}"))
else
local prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ ${prev} == "-c" || ${prev} == "--cipher" ]]; then
COMPREPLY=($(compgen -W "aes xchacha20" -- "${cur}"))
fi
fi
}
complete -F _doby_completion -o bashdefault -o default doby

19
completions/zsh Normal file
View File

@ -0,0 +1,19 @@
#compdef doby
function _doby {
_arguments \
'(-f --force-encrypt)'{-f,--force-encrypt}'[Encrypt even if doby format is recognized]' \
'(-i --interactive)'{-i,--interactive}'[Prompt before overwriting files]' \
'(: * -)'{-h,--help}'[Prints help information]' \
'(: * -)'{-V,--version}'[Prints version information]' \
'--password=[Password used to derive encryption keys]' \
'(-t --time-cost)'{-t,--time-cost}'[Argon2 time cost]' \
'(-m --memory-cost)'{-m,--memory-cost}'[Argon2 memory cost (in kilobytes)]' \
'(-p --parallelism)'{-p,--parallelism}'[Argon2 parallelism cost]' \
'(-b --block-size)'{-b,--block-size}'[Size of the I/O buffer (in bytes)]' \
'(-c --cipher)'{-c,--cipher}'[Encryption cipher to use]: :(aes xchacha20)' \
':::_files' \
':::_files' \
}
_doby "$@"

1
man/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/*.gz

18
man/compile.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
cargo_toml="../Cargo.toml"
source_md="source.md"
if [ ! -f $cargo_toml ]; then
echo "Error: $cargo_toml not found." >&2;
exit 1;
elif [ ! -f $source_md ]; then
echo "Error: $source_md not found." >&2;
exit 1;
fi
version=$(grep "^version = " $cargo_toml | cut -d "\"" -f 2)
date=$(date +"%B %Y")
pandoc $source_md -s -t man | sed \
"s/^\.TH.*$/\.TH \"DOBY\" \"1\" \"$date\" \"doby v$version\" \"doby v$version\"/; \
s/^\.hy/\.ad l/" | gzip - > doby.1.gz

88
man/source.md Normal file
View File

@ -0,0 +1,88 @@
% DOBY(1)
# NAME
doby - Simple, secure and lightweight symmetric encryption from the command line
# SYNOPSIS
doby [**-fi**] [**\--password** password] [**-t** time_cost] [**-m** memory_cost] [**-p** parallelism] [**-b** block_size] [**-c**] {aes | xchacha20} [INPUT] [OUTPUT]
doby [**-h** | **\--help**]
doby [**-V** | **\--version**]
# DESCRIPTION
doby aims to be a small, fast and user-friendly command line tool for symmetric encryption of single files. It uses modern cryptography and (obviously) it's built in rust.
doby can operate with files larger than memory but also from stdout/stdin. In addition to encrypt files, doby also use HMAC cryptography to authenticate the data. This means that encrypted files can't be tampered. Encryptions keys are derived from the user password using Argon2, an expensive KDF function that slows down a lot brute force attacks. You can find more details about cryptography on the doby's repository: https://forge.chapril.org/hardcoresushi/doby#cryptographic-details
doby will add a header at the beginning of the encrypted files so that it can know whether it is encrypted or not. That's why you don't need to specify which operation should be performed. doby will detect this automatically.
# OPTIONS
**-h**, **\--help**
: Print help.
**-V**, **\--version**
: Print doby version.
**-f**, **\--force-encrypt**
: Perform encryption even if doby format is recognized in the input file.
**-i**, **\--interactive**
: Prompt before overwriting the output file if it already exists.
**\--password** *password*
: Specify the password which will be used to derive encryption keys. If omitted, the password will be prompted in the terminal.
**-t**, **\--time-cost** *iterations*
: Argon2 time cost used to derive the master key. Default: 10
**-m**, **\--memory-cost** *memory size*
: Argon2 memory cost used to derive the master key (in kilobytes). Default: 4096 KB
**-p,** **\--parallelism** *threads*
: Argon2 parallelism cost used to derive the master key. Default: 4
**-b,** **\--block-size** *blocksize*
: Size of the buffer used when reading the file (in bytes). Default: 65536 B
**-c,** **\--cipher** *cipher*
: Encryption cipher to use. Either "aes" or "xchacha20". If not specified, AES will be used if your CPU supports AES native instructions, XChaCha20 otherwise. Ignored when performing decryption.
**INPUT**
: The file doby will read as input. If it's omitted or set to "-", doby will read from stdin.
**OUTPUT**
: The file doby will write to. If it's omitted or set to "-", doby will write to stdout.
# EXAMPLES
doby my-super-secret-source-code.rs encrypted.doby
doby \--block-size 4096 encrypted.doby decrypted.rs
cat my-super-secret-music.flac | doby \--cipher xchacha20 > encrypted.doby
doby \--password "rockyou" encrypted.doby > decrypted.flac
cat my-super-secret-logs-file.log | doby \--interactive - logs.doby
echo "you-will-never-break-this" | doby \--memory-cost 524288 \--parallelism 16 \--time-cost 40 > my-super-secret-data.doby
# EXIT STATUS
**0**
: Success
**1**
: Error
# REPORTING BUGS
You can open an issues on Gitea (https://forge.chapril.org/hardcoresushi/doby) or on GitHub (https://github.com/hardcore-sushi/doby) if you find an issue or if you have any questions/suggestions.
If you prefer, you can also email me at hardcore.sushi@disroot.org. My PGP key is available on keyservers (fingerprint: 0x007F84120107191E).
# AUTHOR
Hardcore Sushi <hardcore.sushi@disroot.org>
# COPYRIGHT
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
# SEE ALSO
**ccrypt**(1), **age**(1), **gocryptfs**(1), **cryfs**(1)

3
packaging/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.deb
*.gz
*.pkg.tar.zst

View File

@ -0,0 +1,7 @@
Package: doby
Version: VERSION
Depends: libc6
Homepage: https://forge.chapril.org/hardcoresushi/doby
Maintainer: Hardcore Sushi <hardcore.sushi@disroot.org>
Architecture: amd64
Description: Simple, secure and lightweight symmetric encryption from the command line

71
packaging/package.sh Executable file
View File

@ -0,0 +1,71 @@
#!/bin/bash
set_version() {
cp $1 /tmp/$(basename $1).original &&
sed -i "s/VERSION/$version/g" $1
}
restore() {
mv /tmp/$(basename $1).original $1
}
package_deb() {(
mkdir -p deb/doby/usr/bin deb/doby/usr/share/man/man1 \
deb/doby/usr/share/bash-completion/completions \
deb/doby/usr/share/zsh/vendor-completions &&
strip -s ../target/release/doby -o deb/doby/usr/bin/doby &&
cp ../man/doby.1.gz deb/doby/usr/share/man/man1 &&
cp ../completions/bash deb/doby/usr/share/bash-completion/completions/doby &&
cp ../completions/zsh deb/doby/usr/share/zsh/vendor-completions/_doby &&
cd deb && set_version doby/DEBIAN/control && dpkg -b doby &&
restore doby/DEBIAN/control && mv doby.deb ../doby-$version-x86_64.deb &&
rm -r doby/usr
)}
package_pkg() {(
mkdir pkg/src &&
strip -s ../target/release/doby -o pkg/src/doby &&
cp ../man/doby.1.gz pkg/src &&
cp -r ../completions pkg/src &&
cd pkg && set_version PKGBUILD &&
makepkg && restore PKGBUILD &&
mv doby-*.pkg.tar.zst ../doby-$version-x86_64.pkg.tar.zst && rm -r src pkg
)}
package_tarball() {(
strip -s ../target/x86_64-unknown-linux-musl/release/doby -o tarball/doby/doby &&
cp ../man/doby.1.gz tarball/doby &&
cd tarball && tar -chzf ../doby-$version-x86_64.tar.gz doby &&
rm doby/doby*
)}
if [ "$#" -eq 1 ]; then
cargo_toml="../Cargo.toml"
if [ ! -f $cargo_toml ]; then
echo "Error: $cargo_toml not found." >&2;
exit 1;
fi
version=$(grep "^version = " ../Cargo.toml | cut -d "\"" -f 2)
echo "Packaging doby v$version..."
case $1 in
"deb")
package_deb
;;
"pkg")
package_pkg
;;
"tarball")
package_tarball
;;
"all")
package_deb
package_pkg
package_tarball
;;
esac
else
echo "usage: $0 <deb|pkg|tarball|all>" >&2
exit 1;
fi

18
packaging/pkg/PKGBUILD Normal file
View File

@ -0,0 +1,18 @@
# Maintainer: Hardcore Sushi <hardcore.sushi@disroot.org>
pkgname=doby
pkgver=VERSION
pkgrel=0
depends=("glibc")
arch=("x86_64")
pkgdesc="Simple, secure and lightweight symmetric encryption from the command line"
url="https://forge.chapril.org/hardcoresushi/doby"
license=("GPL-3.0-or-later")
package() {
mkdir -p $pkgdir/usr/bin $pkgdir/usr/share/man/man1 \
$pkgdir/usr/share/bash-completion/completions $pkgdir/usr/share/zsh/vendor-completions
cp $srcdir/doby $pkgdir/usr/bin
cp $srcdir/doby.1.gz $pkgdir/usr/share/man/man1
cp $srcdir/completions/bash $pkgdir/usr/share/bash-completion/completions/doby
cp $srcdir/completions/zsh $pkgdir/usr/share/zsh/vendor-completions/_doby
}

View File

@ -0,0 +1 @@
../../../completions

View File

@ -0,0 +1,28 @@
#!/bin/sh
ROOT=$(dirname $0)
if [ $(id -u) -ne 0 ]; then
echo "Error: root access required" >&2
exit 1
elif [ ! -f $ROOT/doby ]; then
echo "Error: doby binary not found in $ROOT" >&2
exit 1
fi
install -v -g 0 -o 0 $ROOT/doby /usr/bin
MAN_FOLDER=/usr/share/man/man1
if [ -d $MAN_FOLDER ]; then
install -v -g 0 -o 0 -m 0644 $ROOT/doby.1.gz $MAN_FOLDER
fi
BASH_COMPLETION_FOLDER=/usr/share/bash-completion/completions
if [ -d $BASH_COMPLETION_FOLDER ]; then
install -v -g 0 -o 0 -m 0644 $ROOT/completions/bash $BASH_COMPLETION_FOLDER/doby
fi
ZSH_COMPLETION_FOLDER=/usr/share/zsh/vendor-completions
if [ -d $ZSH_COMPLETION_FOLDER ]; then
install -v -g 0 -o 0 -m 0644 $ROOT/completions/zsh $ZSH_COMPLETION_FOLDER/_doby
fi

View File

@ -0,0 +1,11 @@
#!/bin/sh
if [ $(id -u) -ne 0 ]; then
echo "Error: root access required" >&2
exit 1
fi
rm -v /usr/bin/doby
rm -v /usr/share/man/man1/doby.1.gz 2>/dev/null
rm -v /usr/share/bash-completion/completions/doby 2>/dev/null
rm -v /usr/share/zsh/vendor-completions/_doby 2>/dev/null

View File

@ -6,7 +6,7 @@ use std::{
}; };
use doby::{ use doby::{
encrypt, decrypt, encrypt, decrypt,
crypto::{ArgonParams, EncryptionParams, CipherAlgorithm, DobyCipher} crypto::{EncryptionParams, CipherAlgorithm, DobyCipher}
}; };
const MAX_BLOCK_SIZE: usize = 1_073_741_824; //1GB const MAX_BLOCK_SIZE: usize = 1_073_741_824; //1GB
@ -33,11 +33,10 @@ fn main() -> io::Result<()> {
let input = File::open(&args[1])?; let input = File::open(&args[1])?;
let output = OpenOptions::new().create(true).truncate(true).write(true).open(&args[2])?; let output = OpenOptions::new().create(true).truncate(true).write(true).open(&args[2])?;
let params = EncryptionParams::new(ArgonParams{ let params = EncryptionParams::new(
t_cost: 1, argon2::Params::new(8, 1, 1, None).unwrap(),
m_cost: 8, CipherAlgorithm::AesCtr
parallelism: 1, );
}, CipherAlgorithm::AesCtr);
let mut best_encrypt_time = None; let mut best_encrypt_time = None;
let mut best_encrypt_block_size = None; let mut best_encrypt_block_size = None;
@ -49,7 +48,7 @@ fn main() -> io::Result<()> {
let mut reader = BufReader::with_capacity(block_size, &input); let mut reader = BufReader::with_capacity(block_size, &input);
let mut writer = BufWriter::with_capacity(block_size, &output); let mut writer = BufWriter::with_capacity(block_size, &output);
let cipher = DobyCipher::new(PASSWORD.into(), &params).unwrap(); let cipher = DobyCipher::new(PASSWORD.as_bytes(), &params);
let t_encrypt = Instant::now(); let t_encrypt = Instant::now();
encrypt(&mut reader, &mut writer, &params, cipher, block_size, None)?; encrypt(&mut reader, &mut writer, &params, cipher, block_size, None)?;
writer.flush()?; writer.flush()?;
@ -59,7 +58,7 @@ fn main() -> io::Result<()> {
reset(&mut reader)?; reset(&mut reader)?;
reset(&mut writer)?; reset(&mut writer)?;
let cipher = DobyCipher::new(PASSWORD.into(), &params).unwrap(); let cipher = DobyCipher::new(PASSWORD.as_bytes(), &params);
let t_decrypt = Instant::now(); let t_decrypt = Instant::now();
decrypt(&mut reader, &mut writer, cipher, block_size)?; decrypt(&mut reader, &mut writer, cipher, block_size)?;
writer.flush()?; writer.flush()?;

17
src/bin/compgen.rs Normal file
View File

@ -0,0 +1,17 @@
use std::{env, io};
use clap::Shell;
use doby::cli;
fn main() {
let mut args = env::args().skip(1);
if let Some(shell) = args.next() {
if let Ok(shell) = shell.parse() {
cli::app().gen_completions_to("doby", shell, &mut io::stdout());
} else {
eprintln!("error: invalid shell: {}", shell);
eprintln!("shell variants: {:?}", Shell::variants());
}
} else {
eprintln!("usage: compgen <shell>");
}
}

View File

@ -10,15 +10,15 @@ fn main() -> io::Result<()> {
if magic_bytes == MAGIC_BYTES { if magic_bytes == MAGIC_BYTES {
match EncryptionParams::read(&mut file)? { match EncryptionParams::read(&mut file)? {
Some(params) => { Some(params) => {
println!("Argon2 time cost: {}", params.argon2.t_cost); println!("Argon2 time cost: {}", params.argon2.t_cost());
println!("Argon2 memory cost: {}KB", params.argon2.m_cost); println!("Argon2 memory cost: {}KB", params.argon2.m_cost());
println!("Argon2 parallelism: {}", params.argon2.parallelism); println!("Argon2 parallelism cost: {}", params.argon2.p_cost());
println!("Encryption cihpher: {}", params.cipher); println!("Encryption cihpher: {}", params.cipher);
} }
None => eprintln!("Invalid cipher") None => eprintln!("Invalid parameters")
} }
} else { } else {
eprintln!("Doby format not recognized."); eprintln!("doby format not recognized.");
} }
Ok(()) Ok(())
} }

View File

@ -1,48 +1,65 @@
use std::{ use std::{fs::File, io::{self, Read, stdin, stdout}, path::Path, str::FromStr};
path::Path,
fs::File,
str::FromStr,
io::{stdin, stdout, Read},
};
use clap::{crate_name, crate_version, App, Arg, AppSettings}; use clap::{crate_name, crate_version, App, Arg, AppSettings};
use crate::{LazyWriter, Password, crypto::{ArgonParams, CipherAlgorithm}}; use crate::{WrappedWriter, WrappedPassword, crypto::CipherAlgorithm};
cpufeatures::new!(aes_ni, "aes"); cpufeatures::new!(aes_ni, "aes");
pub struct CliArgs { pub struct CliArgs {
pub password: Password, pub password: WrappedPassword,
pub force_encrypt: bool, pub force_encrypt: bool,
pub argon2_params: ArgonParams, pub argon2_params: argon2::Params,
pub cipher: CipherAlgorithm, pub cipher: CipherAlgorithm,
pub block_size: usize, pub block_size: usize,
pub reader: Box<dyn Read>, pub reader: Box<dyn Read>,
pub writer: LazyWriter<String>, pub writer: WrappedWriter<String>,
} }
pub fn parse() -> Option<CliArgs> { pub struct ParseResult {
let app = App::new(crate_name!()) pub error: bool,
pub cli_args: Option<CliArgs>,
}
impl ParseResult {
fn exited() -> Self {
Self { error: false, cli_args: None }
}
}
impl From<CliArgs> for ParseResult {
fn from(args: CliArgs) -> Self {
ParseResult { error: false, cli_args: Some(args) }
}
}
pub fn app<'a>() -> App<'a, 'a> {
App::new(crate_name!())
.version(crate_version!()) .version(crate_version!())
.setting(AppSettings::ColoredHelp) .setting(AppSettings::ColoredHelp)
.about("Secure symmetric encryption from the command line.") .about("Secure symmetric encryption from the command line.")
.arg(Arg::with_name("INPUT").help("<PATH> | \"-\" or empty for stdin")) .arg(Arg::with_name("INPUT").help("<PATH> | \"-\" or empty for stdin"))
.arg(Arg::with_name("OUTPUT").help("<PATH> | \"-\" or empty for stdout")) .arg(Arg::with_name("OUTPUT").help("<PATH> | \"-\" or empty for stdout"))
.arg( .arg(
Arg::with_name("force-encrypt") Arg::with_name("1_force_encrypt")
.short("f") .short("f")
.long("force-encrypt") .long("force-encrypt")
.help(&format!("Encrypt even if {} format is recognized", crate_name!())) .help(concat!("Encrypt even if ", crate_name!(), " format is recognized"))
)
.arg(
Arg::with_name("2_interactive")
.short("i")
.long("interactive")
.help("Prompt before overwriting files")
) )
.arg( .arg(
Arg::with_name("1_password") Arg::with_name("1_password")
.short("p")
.long("password") .long("password")
.value_name("password") .value_name("password")
.help("Password used to derive encryption keys") .help("Password used to derive encryption keys")
) )
.arg( .arg(
Arg::with_name("2_t_cost") Arg::with_name("2_t_cost")
.short("i") .short("t")
.long("iterations") .long("time-cost")
.value_name("iterations") .value_name("iterations")
.help("Argon2 time cost") .help("Argon2 time cost")
.default_value("10") .default_value("10")
@ -51,16 +68,16 @@ pub fn parse() -> Option<CliArgs> {
Arg::with_name("3_m_cost") Arg::with_name("3_m_cost")
.short("m") .short("m")
.long("memory-cost") .long("memory-cost")
.value_name("memory cost") .value_name("memory size")
.help("Argon2 memory cost (in kilobytes)") .help("Argon2 memory cost (in kilobytes)")
.default_value("4096") .default_value("4096")
) )
.arg( .arg(
Arg::with_name("4_parallelism") Arg::with_name("4_p_cost")
.short("t") .short("p")
.long("threads") .long("parallelism")
.value_name("threads") .value_name("threads")
.help("Argon2 parallelism (between 1 and 255)") .help("Argon2 parallelism cost")
.default_value("4") .default_value("4")
) )
.arg( .arg(
@ -80,27 +97,32 @@ pub fn parse() -> Option<CliArgs> {
.possible_values(&["aes", "xchacha20"]) .possible_values(&["aes", "xchacha20"])
.case_insensitive(true) .case_insensitive(true)
) )
.get_matches(); }
pub fn parse() -> Option<ParseResult> {
let app = app().get_matches();
let params = { let params = {
let t_cost = number(app.value_of("2_t_cost").unwrap())?; let t_cost = number(app.value_of("2_t_cost").unwrap())?;
let m_cost = number(app.value_of("3_m_cost").unwrap())?; let m_cost = number(app.value_of("3_m_cost").unwrap())?;
let parallelism = number(app.value_of("4_parallelism").unwrap())?; let p_cost = number(app.value_of("4_p_cost").unwrap())?;
ArgonParams { match argon2::Params::new(m_cost, t_cost, p_cost, None) {
t_cost, Ok(params) => Some(params),
m_cost, Err(e) => {
parallelism, eprintln!("Invalid Argon2 parameters: {}", e);
None
} }
}; }
}?;
let cipher = app let cipher = app
.value_of("cipher") .value_of("cipher")
.and_then(|s| Some(if s.to_lowercase() == "aes" { .map(|s| if s.to_lowercase() == "aes" {
CipherAlgorithm::AesCtr CipherAlgorithm::AesCtr
} else { } else {
CipherAlgorithm::XChaCha20 CipherAlgorithm::XChaCha20
}) }
) )
.unwrap_or_else(|| if aes_ni::get() { .unwrap_or_else(|| if aes_ni::get() {
CipherAlgorithm::AesCtr CipherAlgorithm::AesCtr
@ -125,35 +147,44 @@ pub fn parse() -> Option<CliArgs> {
None => Box::new(stdin()) None => Box::new(stdin())
}; };
let output = app let wrapped_writer = match app
.value_of("OUTPUT") .value_of("OUTPUT")
.and_then(|s| if s == "-" { None } else { Some(s) }) .and_then(|s| if s == "-" { None } else { Some(s) }) {
.map(|s| { Some(path) => {
if Path::new(s).exists() { if {
eprintln!("WARNING: {} already exists", s); if app.is_present("2_interactive") && Path::new(path).exists() {
None eprint!("Warning: {} already exists. Overwrite [y/N]? ", path);
let mut c = String::with_capacity(2);
io::stdin().read_line(&mut c).unwrap();
!c.is_empty() && c.chars().nth(0).unwrap() == 'y'
} else { } else {
Some(LazyWriter::from_path(s.to_owned())) true
} }
}) } {
.unwrap_or_else(|| Some(LazyWriter::from_writer(stdout())))?; WrappedWriter::from_path(path.to_string())
} else {
return Some(ParseResult::exited())
}
}
None => WrappedWriter::from_writer(stdout())
};
Some(CliArgs { Some(CliArgs {
password: app.value_of("1_password").into(), password: app.value_of("1_password").into(),
force_encrypt: app.is_present("force-encrypt"), force_encrypt: app.is_present("1_force_encrypt"),
argon2_params: params, argon2_params: params,
cipher, cipher,
block_size, block_size,
reader: input, reader: input,
writer: output, writer: wrapped_writer,
}) }.into())
} }
fn number<T: FromStr>(val: &str) -> Option<T> { fn number<T: FromStr>(val: &str) -> Option<T> {
match val.parse::<T>() { match val.parse::<T>() {
Ok(n) => Some(n), Ok(n) => Some(n),
Err(_) => { Err(_) => {
eprintln!("Cannot parse '{}' to '{}'", val, std::any::type_name::<T>()); eprintln!("Error: '{}' is not a number", val);
None None
} }
} }

View File

@ -1,31 +1,20 @@
use std::{ use std::{convert::TryFrom, fmt::{self, Display, Formatter}, io::{self, Read, Write}};
convert::TryFrom, use blake2::{Blake2b, VarBlake2b, digest::{Update, VariableOutput}};
fmt::{self, Display, Formatter},
io::{self, Read, Write}
};
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use chacha20::XChaCha20; use chacha20::XChaCha20;
use aes::{Aes256Ctr, cipher::{NewCipher, StreamCipher}}; use aes::{Aes256Ctr, cipher::{NewCipher, StreamCipher}};
use hmac::{Hmac, Mac, NewMac}; use subtle::ConstantTimeEq;
use rand::{Rng, rngs::OsRng}; use rand::{Rng, rngs::OsRng};
use argon2::{Argon2, Version, Algorithm}; use argon2::{Argon2, Version, Algorithm};
use hkdf::Hkdf; use hkdf::Hkdf;
use zeroize::Zeroize; use zeroize::Zeroize;
use crate::Password;
pub const SALT_LEN: usize = 64; pub const SALT_LEN: usize = 64;
const AES_NONCE_LEN: usize = 16; const AES_NONCE_LEN: usize = 16;
const XCHACHA20_NONCE_LEN: usize = 24; const XCHACHA20_NONCE_LEN: usize = 24;
pub const HASH_LEN: usize = 64; pub const HMAC_LEN: usize = 32;
const KEY_LEN: usize = 32; const KEY_LEN: usize = 32;
#[derive(Debug, PartialEq, Eq)]
pub struct ArgonParams {
pub t_cost: u32,
pub m_cost: u32,
pub parallelism: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)] #[repr(u8)]
pub enum CipherAlgorithm { pub enum CipherAlgorithm {
@ -54,37 +43,32 @@ impl Display for CipherAlgorithm {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct EncryptionParams { pub struct EncryptionParams {
salt: [u8; SALT_LEN], salt: [u8; SALT_LEN],
pub argon2: ArgonParams, pub argon2: argon2::Params,
nonce: Vec<u8>,
pub cipher: CipherAlgorithm, pub cipher: CipherAlgorithm,
} }
impl EncryptionParams { impl EncryptionParams {
pub fn get_params_len(&self) -> usize { pub const LEN: usize = SALT_LEN + 4*3 + 1;
SALT_LEN + 4*2 + 2 + self.cipher.get_nonce_size()
}
pub fn new(argon2_params: ArgonParams, cipher: CipherAlgorithm) -> EncryptionParams { pub fn new(argon2_params: argon2::Params, cipher: CipherAlgorithm) -> EncryptionParams {
let mut salt = [0; SALT_LEN]; let mut salt = [0; SALT_LEN];
OsRng.fill(&mut salt); OsRng.fill(&mut salt);
let mut nonce = vec![0; cipher.get_nonce_size()];
OsRng.fill(&mut nonce[..]);
EncryptionParams { EncryptionParams {
salt, salt,
argon2: argon2_params, argon2: argon2_params,
nonce,
cipher, cipher,
} }
} }
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> { pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&self.salt)?; writer.write_all(&self.salt)?;
writer.write_all(&self.argon2.t_cost.to_be_bytes())?; writer.write_all(&self.argon2.t_cost().to_be_bytes())?;
writer.write_all(&self.argon2.m_cost.to_be_bytes())?; writer.write_all(&self.argon2.m_cost().to_be_bytes())?;
writer.write_all(&self.argon2.parallelism.to_be_bytes())?; writer.write_all(&self.argon2.p_cost().to_be_bytes())?;
writer.write_all(&(self.cipher as u8).to_be_bytes())?; writer.write_all(&(self.cipher as u8).to_be_bytes())?;
writer.write_all(&self.nonce)?;
Ok(()) Ok(())
} }
pub fn read<R: Read>(reader: &mut R) -> io::Result<Option<Self>> { pub fn read<R: Read>(reader: &mut R) -> io::Result<Option<Self>> {
let mut salt = [0; SALT_LEN]; let mut salt = [0; SALT_LEN];
reader.read_exact(&mut salt)?; reader.read_exact(&mut salt)?;
@ -92,98 +76,75 @@ impl EncryptionParams {
reader.read_exact(&mut t_cost)?; reader.read_exact(&mut t_cost)?;
let mut m_cost = [0; 4]; let mut m_cost = [0; 4];
reader.read_exact(&mut m_cost)?; reader.read_exact(&mut m_cost)?;
let mut parallelism = [0; 1]; let mut p_cost = [0; 4];
reader.read_exact(&mut parallelism)?; reader.read_exact(&mut p_cost)?;
let mut cipher_buff = [0; 1]; let mut cipher_buff = [0; 1];
reader.read_exact(&mut cipher_buff)?; reader.read_exact(&mut cipher_buff)?;
match CipherAlgorithm::try_from(cipher_buff[0]) { if let Ok(cipher) = CipherAlgorithm::try_from(cipher_buff[0]) {
Ok(cipher) => { if let Ok(argon2_params) = argon2::Params::new(
let mut nonce = vec![0; cipher.get_nonce_size()]; u32::from_be_bytes(m_cost),
reader.read_exact(&mut nonce)?; u32::from_be_bytes(t_cost),
u32::from_be_bytes(p_cost),
let argon2_params = ArgonParams { None
t_cost: u32::from_be_bytes(t_cost), ) {
m_cost: u32::from_be_bytes(m_cost), return Ok(Some(EncryptionParams {
parallelism: u8::from_be_bytes(parallelism),
};
Ok(Some(EncryptionParams {
salt, salt,
argon2: argon2_params, argon2: argon2_params,
nonce,
cipher, cipher,
})) }));
}
Err(_) => Ok(None)
} }
} }
} Ok(None)
trait ThenZeroize {
fn zeroize<T: Zeroize>(self, v: T) -> Self;
}
impl<S, E> ThenZeroize for Result<S, E> {
fn zeroize<T: Zeroize>(self, mut v: T) -> Self {
v.zeroize();
self
} }
} }
pub struct DobyCipher { pub struct DobyCipher {
cipher: Box<dyn StreamCipher>, cipher: Box<dyn StreamCipher>,
hmac: Hmac<blake2::Blake2b>, hasher: VarBlake2b,
buffer: Vec<u8>, buffer: Vec<u8>,
} }
impl DobyCipher { impl DobyCipher {
pub fn new(mut password: Password, params: &EncryptionParams) -> Result<Self, argon2::Error> { pub fn new(password: &[u8], params: &EncryptionParams) -> Self {
match Argon2::new(None, params.argon2.t_cost, params.argon2.m_cost, params.argon2.parallelism.into(), Version::V0x13) { let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params.argon2.clone());
Ok(argon2) => {
let mut master_key = [0; KEY_LEN]; let mut master_key = [0; KEY_LEN];
let password = password.unwrap_or_ask(); argon2.hash_password_into(password, &params.salt, &mut master_key).unwrap();
argon2.hash_password_into(Algorithm::Argon2id, password.as_bytes(), &params.salt, &[], &mut master_key).zeroize(password)?; let hkdf = Hkdf::<Blake2b>::new(Some(&params.salt), &master_key);
let hkdf = Hkdf::<blake2::Blake2b>::new(Some(&params.salt), &master_key); master_key.zeroize();
let mut nonce = vec![0; params.cipher.get_nonce_size()];
hkdf.expand(b"doby_nonce", &mut nonce).unwrap();
let mut encryption_key = [0; KEY_LEN]; let mut encryption_key = [0; KEY_LEN];
hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap(); hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap();
let mut authentication_key = [0; KEY_LEN]; let mut authentication_key = [0; KEY_LEN];
hkdf.expand(b"doby_authentication_key", &mut authentication_key).unwrap(); hkdf.expand(b"doby_authentication_key", &mut authentication_key).unwrap();
master_key.zeroize();
let mut encoded_params = Vec::with_capacity(params.get_params_len()); let mut encoded_params = Vec::with_capacity(EncryptionParams::LEN);
params.write(&mut encoded_params).unwrap(); params.write(&mut encoded_params).unwrap();
let mut hmac = Hmac::new_from_slice(&authentication_key).unwrap(); let mut hasher = VarBlake2b::new_keyed(&authentication_key, HMAC_LEN);
authentication_key.zeroize(); authentication_key.zeroize();
hmac.update(&encoded_params); hasher.update(&encoded_params);
let cipher: Box<dyn StreamCipher> = match params.cipher { let cipher: Box<dyn StreamCipher> = match params.cipher {
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &params.nonce).unwrap()), CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &nonce).unwrap()),
CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &params.nonce).unwrap()), CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &nonce).unwrap()),
}; };
encryption_key.zeroize(); encryption_key.zeroize();
Ok(Self { Self {
cipher, cipher,
hmac, hasher,
buffer: Vec::new(), buffer: Vec::new(),
})
}
Err(e) => {
password.zeroize();
Err(e)
}
} }
} }
pub fn encrypt_chunk<W: Write>(&mut self, buff: &mut [u8], writer: &mut W) -> io::Result<()> { pub fn encrypt_chunk<W: Write>(&mut self, buff: &mut [u8], writer: &mut W) -> io::Result<()> {
self.cipher.apply_keystream(buff); self.cipher.apply_keystream(buff);
self.hmac.update(buff); self.hasher.update(&buff);
writer.write_all(buff) writer.write_all(buff)
} }
pub fn write_hmac<W: Write>(self, writer: &mut W) -> io::Result<usize> { pub fn write_hmac<W: Write>(self, writer: &mut W) -> io::Result<()> {
let tag = self.hmac.finalize().into_bytes(); writer.write_all(&self.hasher.finalize_boxed())
writer.write(&tag)
} }
//buff size must be > to HASH_LEN //buff size must be > to HASH_LEN
@ -192,45 +153,43 @@ impl DobyCipher {
buff[..buffer_len].clone_from_slice(&self.buffer); buff[..buffer_len].clone_from_slice(&self.buffer);
let read = reader.read(&mut buff[buffer_len..])?; let read = reader.read(&mut buff[buffer_len..])?;
let n = if buffer_len + read >= HASH_LEN { let n = if buffer_len + read >= HMAC_LEN {
self.buffer.clear(); self.buffer.clear();
buffer_len + read - HASH_LEN buffer_len + read - HMAC_LEN
} else { } else {
0 0
}; };
self.buffer.extend_from_slice(&buff[n..buffer_len+read]); self.buffer.extend_from_slice(&buff[n..buffer_len+read]);
self.hmac.update(&buff[..n]); self.hasher.update(&buff[..n]);
self.cipher.apply_keystream(&mut buff[..n]); self.cipher.apply_keystream(&mut buff[..n]);
Ok(n) Ok(n)
} }
pub fn verify_hmac(self) -> bool { pub fn verify_hmac(self) -> bool {
self.hmac.verify(&self.buffer).is_ok() self.hasher.finalize_boxed().ct_eq(&self.buffer).into()
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ArgonParams, CipherAlgorithm, EncryptionParams, DobyCipher, HASH_LEN}; use super::{CipherAlgorithm, EncryptionParams, DobyCipher, HMAC_LEN};
#[test] #[test]
fn encryption_params() { fn encryption_params() {
let params = EncryptionParams::new(ArgonParams { let params = EncryptionParams::new(
t_cost: 1, argon2::Params::new(8, 1, 1, None).unwrap(),
m_cost: 8, CipherAlgorithm::XChaCha20
parallelism: 1, );
}, CipherAlgorithm::XChaCha20);
assert_eq!(params.get_params_len(), 98); assert_eq!(EncryptionParams::LEN, 77);
let mut buff = Vec::with_capacity(98); let mut buff = Vec::with_capacity(74);
params.write(&mut buff).unwrap(); params.write(&mut buff).unwrap();
assert_eq!(buff[..64], params.salt); assert_eq!(buff[..64], params.salt);
assert_eq!(buff[64..68], vec![0, 0, 0, 0x01]); //t_cost assert_eq!(buff[64..68], vec![0, 0, 0, 0x01]); //t_cost
assert_eq!(buff[68..72], vec![0, 0, 0, 0x08]); //m_cost assert_eq!(buff[68..72], vec![0, 0, 0, 0x08]); //m_cost
assert_eq!(buff[72], 0x01); //parallelism assert_eq!(buff[72..76], vec![0, 0, 0, 0x01]); //p_cost
assert_eq!(buff[73], CipherAlgorithm::XChaCha20 as u8); assert_eq!(buff[76], CipherAlgorithm::XChaCha20 as u8);
assert_eq!(buff[74..], params.nonce);
let new_params = EncryptionParams::read(&mut buff.as_slice()).unwrap().unwrap(); let new_params = EncryptionParams::read(&mut buff.as_slice()).unwrap().unwrap();
assert_eq!(new_params, params); assert_eq!(new_params, params);
@ -238,25 +197,24 @@ mod tests {
#[test] #[test]
fn doby_cipher() { fn doby_cipher() {
let params = EncryptionParams::new(ArgonParams { let params = EncryptionParams::new(
t_cost: 1, argon2::Params::new(8, 1, 1, None).unwrap(),
m_cost: 8, CipherAlgorithm::AesCtr
parallelism: 1, );
}, CipherAlgorithm::AesCtr);
let password = "I like spaghetti"; let password = "I like spaghetti";
let plaintext = b"but I love so much to listen to HARDCORE music on big subwoofer"; let plaintext = b"but I love so much to listen to HARDCORE music on big subwoofer";
let mut buff: [u8; 63] = *plaintext; let mut buff: [u8; 63] = *plaintext;
let mut vec = Vec::with_capacity(buff.len()+HASH_LEN); let mut vec = Vec::with_capacity(buff.len()+HMAC_LEN);
let mut enc_cipher = DobyCipher::new(password.into(), &params).unwrap(); let mut enc_cipher = DobyCipher::new(password.as_bytes(), &params);
enc_cipher.encrypt_chunk(&mut buff, &mut vec).unwrap(); enc_cipher.encrypt_chunk(&mut buff, &mut vec).unwrap();
assert_ne!(buff, *plaintext); assert_ne!(buff, *plaintext);
assert_eq!(buff, vec.as_slice()); assert_eq!(buff, vec.as_slice());
assert_eq!(enc_cipher.write_hmac(&mut vec).unwrap(), HASH_LEN); assert!(enc_cipher.write_hmac(&mut vec).is_ok());
assert_eq!(vec.len(), buff.len()+HASH_LEN); assert_eq!(vec.len(), buff.len()+HMAC_LEN);
let mut dec_cipher = DobyCipher::new(password.into(), &params).unwrap(); let mut dec_cipher = DobyCipher::new(password.as_bytes(), &params);
let mut decrypted = vec![0; buff.len()+HASH_LEN]; let mut decrypted = vec![0; buff.len()+HMAC_LEN];
let mut n = dec_cipher.decrypt_chunk(&mut vec.as_slice(), &mut decrypted[..]).unwrap(); let mut n = dec_cipher.decrypt_chunk(&mut vec.as_slice(), &mut decrypted[..]).unwrap();
assert_eq!(n, buff.len()); assert_eq!(n, buff.len());
n = dec_cipher.decrypt_chunk(&mut &vec[n..], &mut decrypted[n..]).unwrap(); n = dec_cipher.decrypt_chunk(&mut &vec[n..], &mut decrypted[n..]).unwrap();

View File

@ -1,72 +1,72 @@
pub mod cli; pub mod cli;
pub mod crypto; pub mod crypto;
use std::{fs::File, path::Path, io::{self, Read, Write}}; use std::{fmt::Display, fs::OpenOptions, io::{self, BufWriter, Read, Write}, path::Path};
use crypto::{DobyCipher, EncryptionParams}; use crypto::{DobyCipher, EncryptionParams};
use zeroize::Zeroize; use zeroize::Zeroize;
pub const MAGIC_BYTES: &[u8; 4] = b"DOBY"; pub const MAGIC_BYTES: &[u8; 4] = b"DOBY";
pub struct Password(Option<String>); pub struct WrappedPassword(Option<String>);
impl Password { impl WrappedPassword {
fn unwrap_or_ask(self) -> String { pub fn get(self, ask_confirm: bool) -> Option<String> {
self.0.unwrap_or_else(|| rpassword::read_password_from_tty(Some("Password: ")).unwrap()) self.0.or_else(|| {
let mut password = rpassword::read_password_from_tty(Some("Password: ")).ok()?;
if ask_confirm {
let mut password_confirm = rpassword::read_password_from_tty(Some("Password (confirm): ")).ok()?;
if password == password_confirm {
password_confirm.zeroize();
Some(password)
} else {
password.zeroize();
password_confirm.zeroize();
eprintln!("Error: passwords don't match");
None
}
} else {
Some(password)
}
})
} }
} }
impl From<Option<&str>> for Password { impl From<Option<&str>> for WrappedPassword {
fn from(s: Option<&str>) -> Self { fn from(s: Option<&str>) -> Self {
Self(s.map(|s| String::from(s))) Self(s.map(String::from))
}
}
pub enum WrappedWriter<P: AsRef<Path>> {
PATH {
path: P
},
WRITER {
writer: Box<dyn Write>
} }
} }
impl From<&str> for Password { impl<P: AsRef<Path> + Display> WrappedWriter<P> {
fn from(s: &str) -> Self {
Some(s).into()
}
}
impl Zeroize for Password {
fn zeroize(&mut self) {
self.0.zeroize()
}
}
pub struct LazyWriter<P: AsRef<Path>> {
path: Option<P>,
writer: Option<Box<dyn Write>>,
}
impl<P: AsRef<Path>> LazyWriter<P> {
fn from_path(path: P) -> Self { fn from_path(path: P) -> Self {
Self { Self::PATH { path }
path: Some(path),
writer: None,
}
} }
fn from_writer<T: 'static + Write>(writer: T) -> Self { fn from_writer<T: 'static + Write>(writer: T) -> Self {
Self { Self::WRITER { writer: Box::new(writer) }
path: None,
writer: Some(Box::new(writer)),
} }
pub fn into_buf_writer(self) -> Option<BufWriter<Box<dyn Write>>> {
Some(BufWriter::new(match self {
Self::PATH { path } => Box::new(
OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref())
.map_err(|e| eprintln!("{}: {}", path, e))
.ok()?
) as Box<dyn Write>,
Self::WRITER { writer } => writer,
}))
} }
} }
impl<P: AsRef<Path>> Write for LazyWriter<P> { pub fn encrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, params: &EncryptionParams, mut cipher: DobyCipher, block_size: usize, already_read: Option<&[u8]>) -> io::Result<()> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.writer.is_none() {
self.writer = Some(Box::new(File::create(self.path.as_ref().unwrap()).unwrap()));
}
self.writer.as_mut().unwrap().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.as_mut().unwrap().flush()
}
}
pub fn encrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, params: &EncryptionParams, mut cipher: DobyCipher, block_size: usize, already_read: Option<Vec<u8>>) -> io::Result<()> {
writer.write_all(MAGIC_BYTES)?; writer.write_all(MAGIC_BYTES)?;
params.write(writer)?; params.write(writer)?;
let mut buff = vec![0; block_size]; let mut buff = vec![0; block_size];
@ -97,7 +97,7 @@ pub fn decrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, mut cipher: Do
if n == 0 { if n == 0 {
break; break;
} else { } else {
writer.write(&buff[..n])?; writer.write_all(&buff[..n])?;
} }
} }
Ok(cipher.verify_hmac()) Ok(cipher.verify_hmac())

View File

@ -1,4 +1,4 @@
use std::{process, io::{BufWriter, BufReader, Read}}; use std::{process, io::{BufReader, Read}};
use doby::{ use doby::{
cli, cli,
crypto::{EncryptionParams, DobyCipher}, crypto::{EncryptionParams, DobyCipher},
@ -6,67 +6,73 @@ use doby::{
decrypt, decrypt,
encrypt, encrypt,
}; };
use zeroize::Zeroize;
fn run() -> bool { fn run() -> bool {
let mut success = false; let mut success = false;
if let Some(cli_args) = cli::parse() { if let Some(result) = cli::parse() {
let mut reader = BufReader::with_capacity(cli_args.block_size, cli_args.reader); if let Some(cli_args) = result.cli_args {
let mut writer = BufWriter::with_capacity(cli_args.block_size, cli_args.writer); let mut reader = BufReader::new(cli_args.reader);
let mut magic_bytes = vec![0; MAGIC_BYTES.len()]; let mut magic_bytes = vec![0; MAGIC_BYTES.len()];
match reader.read(&mut magic_bytes) { match reader.read(&mut magic_bytes) {
Ok(n) => { Ok(n) => {
if n < magic_bytes.len() {
magic_bytes.truncate(n);
}
if magic_bytes == MAGIC_BYTES && !cli_args.force_encrypt { //we probably want to decrypt if magic_bytes == MAGIC_BYTES && !cli_args.force_encrypt { //we probably want to decrypt
match EncryptionParams::read(&mut reader) { match EncryptionParams::read(&mut reader) {
Ok(params) => { Ok(params) => {
match params { if let Some(params) = params {
Some(params) => { if let Some(mut password) = cli_args.password.get(false) {
match DobyCipher::new(cli_args.password, &params) { if let Some(mut writer) = cli_args.writer.into_buf_writer() {
Ok(cipher) => { let cipher = DobyCipher::new(password.as_bytes(), &params);
password.zeroize();
match decrypt(&mut reader, &mut writer, cipher, cli_args.block_size) { match decrypt(&mut reader, &mut writer, cipher, cli_args.block_size) {
Ok(verified) => { Ok(verified) => {
if verified { if verified {
success = true success = true
} else { } else {
eprintln!("WARNING: HMAC verification failed !\nEither your password is incorrect or the ciphertext has been corrupted.\nBe careful, the data could have been altered by an attacker."); eprintln!("Warning: HMAC verification failed !\nEither your password is incorrect or the ciphertext has been corrupted.\nBe careful, the data could have been altered by an attacker.");
} }
} }
Err(e) => eprintln!("I/O error while decrypting: {}", e) Err(e) => eprintln!("I/O error while decrypting: {}", e)
} }
} } else {
Err(e) => eprintln!("Invalid argon2 params: {}", e) password.zeroize();
} }
} }
None => eprintln!("Invalid cipher") } else {
eprintln!("Error: invalid encryption parameters")
} }
} }
Err(e) => eprintln!("I/O error while reading headers: {}", e) Err(e) => eprintln!("I/O error while reading headers: {}", e)
} }
} else { //otherwise, encrypt } else { //otherwise, encrypt
let params = EncryptionParams::new(cli_args.argon2_params, cli_args.cipher); let params = EncryptionParams::new(cli_args.argon2_params, cli_args.cipher);
match DobyCipher::new(cli_args.password, &params) { if let Some(mut password) = cli_args.password.get(true) {
Ok(cipher) => { if let Some(mut writer) = cli_args.writer.into_buf_writer() {
let cipher = DobyCipher::new(password.as_bytes(), &params);
password.zeroize();
match encrypt( match encrypt(
&mut reader, &mut reader,
&mut writer, &mut writer,
&params, &params,
cipher, cipher,
cli_args.block_size, cli_args.block_size,
Some(magic_bytes) Some(&magic_bytes[..n])
) { ) {
Ok(_) => success = true, Ok(_) => success = true,
Err(e) => eprintln!("I/O error while encrypting: {}", e) Err(e) => eprintln!("I/O error while encrypting: {}", e)
} }
} else {
password.zeroize();
} }
Err(e) => eprintln!("Invalid argon2 params: {}", e)
} }
} }
} }
Err(e) => eprintln!("I/O error while reading magic bytes: {}", e), Err(e) => eprintln!("I/O error while reading magic bytes: {}", e),
} }
} else {
success = !result.error;
}
} }
success success
} }

View File

@ -1,7 +1,6 @@
use rand::Rng; use rand::Rng;
use doby::{ use doby::{
crypto::{ crypto::{
ArgonParams,
CipherAlgorithm, CipherAlgorithm,
EncryptionParams, EncryptionParams,
DobyCipher, DobyCipher,
@ -19,17 +18,17 @@ fn different_elements<T: Eq>(v1: &Vec<T>, v2: &Vec<T>) -> usize {
fn authentication() { fn authentication() {
const BLOCK_SIZE: usize = 65536; const BLOCK_SIZE: usize = 65536;
const PLAINTEXT: &[u8; 13] = b"the plaintext"; const PLAINTEXT: &[u8; 13] = b"the plaintext";
const CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+113;
const PASSWORD: &str = "the password"; const PASSWORD: &str = "the password";
let params = EncryptionParams::new(ArgonParams { let params = EncryptionParams::new(
t_cost: 1, argon2::Params::new(8, 1, 1, None).unwrap(),
m_cost: 8, CipherAlgorithm::AesCtr
parallelism: 1, );
}, CipherAlgorithm::AesCtr);
let encrypter = DobyCipher::new(PASSWORD.into(), &params).unwrap(); let encrypter = DobyCipher::new(PASSWORD.as_bytes(), &params);
let mut ciphertext = Vec::with_capacity(PLAINTEXT.len()+158); let mut ciphertext = Vec::with_capacity(CIPHERTEXT_SIZE);
encrypt(&mut &PLAINTEXT[..], &mut ciphertext, &params, encrypter, BLOCK_SIZE, None).unwrap(); encrypt(&mut &PLAINTEXT[..], &mut ciphertext, &params, encrypter, BLOCK_SIZE, None).unwrap();
assert_eq!(ciphertext.len(), PLAINTEXT.len()+158); assert_eq!(ciphertext.len(), CIPHERTEXT_SIZE);
for i in 0..ciphertext.len() { for i in 0..ciphertext.len() {
let mut compromised = ciphertext.clone(); let mut compromised = ciphertext.clone();
@ -37,15 +36,15 @@ fn authentication() {
compromised[i] = rand::thread_rng().gen(); compromised[i] = rand::thread_rng().gen();
} }
assert_eq!(different_elements(&compromised, &ciphertext), 1); assert_eq!(different_elements(&compromised, &ciphertext), 1);
let decrypter = DobyCipher::new(PASSWORD.into(), &params).unwrap(); let decrypter = DobyCipher::new(PASSWORD.as_bytes(), &params);
let mut decrypted = Vec::with_capacity(PLAINTEXT.len()); let mut decrypted = Vec::with_capacity(PLAINTEXT.len());
let verified = decrypt(&mut &compromised[..], &mut decrypted, decrypter, BLOCK_SIZE).unwrap(); let verified = decrypt(&mut &compromised[..], &mut decrypted, decrypter, BLOCK_SIZE).unwrap();
assert_eq!(verified, false); assert_eq!(verified, false);
} }
let decrypter = DobyCipher::new(PASSWORD.into(), &params).unwrap(); let decrypter = DobyCipher::new(PASSWORD.as_bytes(), &params);
let mut decrypted = Vec::with_capacity(PLAINTEXT.len()); let mut decrypted = Vec::with_capacity(PLAINTEXT.len());
let verified = decrypt(&mut &ciphertext[4+params.get_params_len()..], &mut decrypted, decrypter, BLOCK_SIZE).unwrap(); let verified = decrypt(&mut &ciphertext[4+EncryptionParams::LEN..], &mut decrypted, decrypter, BLOCK_SIZE).unwrap();
assert_eq!(decrypted, PLAINTEXT); assert_eq!(decrypted, PLAINTEXT);
assert_eq!(verified, true); assert_eq!(verified, true);
} }

View File

@ -1,7 +1,7 @@
use std::{convert::TryInto, fs::{self, File, create_dir}, io::{self, Read, Write}, path::PathBuf}; use std::{convert::TryInto, fs::{self, File, create_dir}, io::{self, Read, Write}, path::PathBuf};
use assert_cmd::{Command, cargo::{CargoError, cargo_bin}}; use assert_cmd::{Command, cargo::{CargoError, cargo_bin}};
use tempfile::TempDir; use tempfile::TempDir;
use doby::crypto::{CipherAlgorithm, SALT_LEN, HASH_LEN}; use doby::crypto::{CipherAlgorithm, SALT_LEN, HMAC_LEN};
const PLAINTEXT: &[u8] = b"the plaintext"; const PLAINTEXT: &[u8] = b"the plaintext";
const PASSWORD: &str = "the password"; const PASSWORD: &str = "the password";
@ -22,7 +22,7 @@ fn setup_files<'a>() -> io::Result<(PathBuf, PathBuf, PathBuf)> {
fn doby_cmd() -> Result<Command, CargoError> { fn doby_cmd() -> Result<Command, CargoError> {
let mut cmd = Command::cargo_bin("doby")?; let mut cmd = Command::cargo_bin("doby")?;
cmd.arg("-p").arg(PASSWORD); cmd.arg("--password").arg(PASSWORD);
Ok(cmd) Ok(cmd)
} }
@ -52,7 +52,7 @@ fn files() -> io::Result<()> {
fn stdout() -> io::Result<()> { fn stdout() -> io::Result<()> {
let (_, tmp_plaintext, tmp_ciphertext) = setup_files()?; let (_, tmp_plaintext, tmp_ciphertext) = setup_files()?;
let shell_cmd = format!("{} -p \"{}\" {} > {}", cargo_bin("doby").to_str().unwrap(), PASSWORD, tmp_plaintext.to_str().unwrap(), tmp_ciphertext.to_str().unwrap()); let shell_cmd = format!("{} --password \"{}\" {} > {}", cargo_bin("doby").to_str().unwrap(), PASSWORD, tmp_plaintext.to_str().unwrap(), tmp_ciphertext.to_str().unwrap());
bash_cmd().arg(shell_cmd).assert().success().stdout("").stderr(""); bash_cmd().arg(shell_cmd).assert().success().stdout("").stderr("");
doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT); doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT);
@ -64,10 +64,10 @@ fn stdout() -> io::Result<()> {
fn stdin() -> io::Result<()> { fn stdin() -> io::Result<()> {
let (_, tmp_plaintext, tmp_ciphertext) = setup_files()?; let (_, tmp_plaintext, tmp_ciphertext) = setup_files()?;
let mut shell_cmd = format!("cat {} | {} -p \"{}\" - {}", tmp_plaintext.to_str().unwrap(), cargo_bin("doby").to_str().unwrap(), PASSWORD, tmp_ciphertext.to_str().unwrap()); let mut shell_cmd = format!("cat {} | {} --password \"{}\" - {}", tmp_plaintext.to_str().unwrap(), cargo_bin("doby").to_str().unwrap(), PASSWORD, tmp_ciphertext.to_str().unwrap());
bash_cmd().arg(shell_cmd).assert().success().stdout("").stderr(""); bash_cmd().arg(shell_cmd).assert().success().stdout("").stderr("");
shell_cmd = format!("cat {} | {} -p \"{}\"", tmp_ciphertext.to_str().unwrap(), cargo_bin("doby").to_str().unwrap(), PASSWORD); shell_cmd = format!("cat {} | {} --password \"{}\"", tmp_ciphertext.to_str().unwrap(), cargo_bin("doby").to_str().unwrap(), PASSWORD);
bash_cmd().arg(shell_cmd).assert().success().stdout(PLAINTEXT); bash_cmd().arg(shell_cmd).assert().success().stdout(PLAINTEXT);
Ok(()) Ok(())
@ -85,7 +85,7 @@ fn force_encrypt() -> io::Result<()> {
let buff_ciphertext_2 = fs::read(&tmp_ciphertext_2)?; let buff_ciphertext_2 = fs::read(&tmp_ciphertext_2)?;
assert_ne!(buff_ciphertext_1, buff_ciphertext_2); assert_ne!(buff_ciphertext_1, buff_ciphertext_2);
assert_ne!(buff_ciphertext_2, PLAINTEXT); assert_ne!(buff_ciphertext_2, PLAINTEXT);
assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+158); assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+113);
let tmp_decrypted_1 = tmp_path.join("decrypted_1"); let tmp_decrypted_1 = tmp_path.join("decrypted_1");
doby_cmd().unwrap().arg(tmp_ciphertext_2).arg(&tmp_decrypted_1).assert().success().stdout("").stderr(""); doby_cmd().unwrap().arg(tmp_ciphertext_2).arg(&tmp_decrypted_1).assert().success().stdout("").stderr("");
@ -107,8 +107,8 @@ fn test_cipher(cipher_str: &str, cipher_algorithm: CipherAlgorithm) -> io::Resul
doby_cmd().unwrap().arg("-c").arg(cipher_str).arg(tmp_plaintext).arg(&tmp_ciphertext).assert().success().stdout("").stderr(""); doby_cmd().unwrap().arg("-c").arg(cipher_str).arg(tmp_plaintext).arg(&tmp_ciphertext).assert().success().stdout("").stderr("");
let ciphertext = fs::read(&tmp_ciphertext)?; let ciphertext = fs::read(&tmp_ciphertext)?;
assert_eq!(ciphertext[4+SALT_LEN+4*2+1], cipher_algorithm as u8); assert_eq!(ciphertext[4+SALT_LEN+4*3], cipher_algorithm as u8);
assert_eq!(ciphertext.len(), PLAINTEXT.len()+14+SALT_LEN+HASH_LEN+cipher_algorithm.get_nonce_size()); assert_eq!(ciphertext.len(), PLAINTEXT.len()+17+SALT_LEN+HMAC_LEN);
doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr(""); doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr("");
@ -127,17 +127,16 @@ fn aes_cipher() -> io::Result<()> {
Ok(()) Ok(())
} }
#[test] #[test]
fn argon2_params() -> io::Result<()> { fn argon2_params() -> io::Result<()> {
Command::cargo_bin("doby").unwrap().arg("-i").arg("0").assert().failure().stderr("Invalid argon2 params: time cost is too small\n"); Command::cargo_bin("doby").unwrap().arg("-t").arg("0").assert().failure().stderr("Invalid Argon2 parameters: time cost is too small\n");
Command::cargo_bin("doby").unwrap().arg("-m").arg("0").assert().failure().stderr("Invalid argon2 params: memory cost is too small\n"); Command::cargo_bin("doby").unwrap().arg("-m").arg("0").assert().failure().stderr("Invalid Argon2 parameters: memory cost is too small\n");
Command::cargo_bin("doby").unwrap().arg("-t").arg("0").assert().failure().stderr("Invalid argon2 params: too few lanes\n"); Command::cargo_bin("doby").unwrap().arg("-p").arg("0").assert().failure().stderr("Invalid Argon2 parameters: not enough threads\n");
let ciphertext = doby_cmd().unwrap().arg("-i").arg("8").arg("-m").arg("2048").arg("-t").arg("8").assert().success().stderr("").get_output().stdout.clone(); let ciphertext = doby_cmd().unwrap().arg("-t").arg("8").arg("-m").arg("2048").arg("-p").arg("8").assert().success().stderr("").get_output().stdout.clone();
assert_eq!(u32::from_be_bytes(ciphertext[4+SALT_LEN..4+SALT_LEN+4].try_into().unwrap()), 8); //time cost assert_eq!(u32::from_be_bytes(ciphertext[4+SALT_LEN..4+SALT_LEN+4].try_into().unwrap()), 8); //time cost
assert_eq!(u32::from_be_bytes(ciphertext[4+SALT_LEN+4..4+SALT_LEN+8].try_into().unwrap()), 2048); //memory cost assert_eq!(u32::from_be_bytes(ciphertext[4+SALT_LEN+4..4+SALT_LEN+8].try_into().unwrap()), 2048); //memory cost
assert_eq!(u8::from_be_bytes([ciphertext[4+SALT_LEN+8]]), 8); //parallelism assert_eq!(u32::from_be_bytes(ciphertext[4+SALT_LEN+8..4+SALT_LEN+12].try_into().unwrap()), 8); //parallelism
Ok(()) Ok(())
} }