Secure symmetric encryption from the command line
Go to file
2021-07-05 12:25:52 +02:00
src Create output file only when writing 2021-07-04 19:24:20 +02:00
tests Add --force-encrypt integration test 2021-07-05 12:25:52 +02:00
.gitignore Genesis 2021-06-26 16:50:54 +02:00
Cargo.lock Add integration test 2021-07-04 14:38:51 +02:00
Cargo.toml Add integration test 2021-07-04 14:38:51 +02:00
LICENSE Genesis 2021-06-26 16:50:54 +02:00
README.md Add stdin/stdout integration tests 2021-07-05 12:09:31 +02:00

doby

Secure symmetric encryption from the command line.

doby started as a fork of aef by wyhaya. It aims to replace the ccrypt tool which is a bit old and not very secure.

Features

  • Fast: written in rust, encrypts with AES-256-CTR or XChaCha20
  • HMAC ciphertext authentication
  • Password brute-force resistance with Argon2
  • Increase the plaintext size of only 190 bytes
  • Encryption from STDIN/STDOUT or from files
  • Adjustable performance & secuity parameters

Disclamer

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

Encryption

doby my-super-secret-source-code.rs encrypted.doby

Decryption

doby encrypted.doby decrypted.rs

If you ommit file path or use -, doby operates from stdin/stdout

# Read from stdin and write to stdout
cat my-super-secret-music.flac | doby > encrypted.doby

# Read from a file and output to stdout
doby encrypted.doby > decrypted.flac

# Read from stdin and save to a file
cat my-super-secret-logs-file.log | doby - logs.doby

Speicfy password from the command line

doby -p "A super very ultra strong passphrase" my-super-secret-document.pdf document.doby

Double encryption

doby -p "first password" my-super-secret-database.db | doby -f - double-encrypted.doby

Increase password brute-force resistance

echo "you-will-never-break-this" | doby --memory-cost 524288 --threads 16 --iterations 40 > my-super-secret-password.doby

Full Options

USAGE:
    doby [FLAGS] [OPTIONS] [ARGS]

FLAGS:
    -f, --force-encrypt    Encrypt even if doby format is recognized
    -h, --help             Prints help information
    -V, --version          Prints version information

OPTIONS:
    -p, --password <password>          Password used to derive encryption keys
    -b, --block-size <blocksize>       Size of file chunk (in bytes) [default: 65536]
    -c, --cipher <cipher>              Encryption cipher to use [possible values: aes, xchacha20]
    -m, --memory-cost <memory cost>    Argon2 memory cost (in kilobytes) [default: 4096]
    -t, --threads <threads>            Argon2 parallelism (between 1 and 255) [default: 4]
    -i, --iterations <iterations>      Argon2 time cost [default: 10]

ARGS:
    <INPUT>     <PATH> | "-" or empty for stdin
    <OUTPUT>    <PATH> | "-" or empty for stdout

Build

You should verify commits before building the binary. You can download my PGP key from keyservers:

gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 007F84120107191E

Fingerprint: BD56 2147 9E7B 74D3 6A40 5BE8 007F 8412 0107 191E
Email: Hardcore Sushi <hardcore.sushi@disroot.org>

Once imported:

git clone --depth=1 https://forge.chapril.org/hardcoresushi/doby.git
cd doby
git verify-commit HEAD
cargo build --release #outputs to ./target/release/doby

Cryptographic details

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.

let master_key: [u8; 32] = argon2id(
    password,
    random_password_salt,
    argon2_time_cost,
    argon2_memory_cost,
    argon2_parallelism,
);

Then, doby uses HKDF with a new random salt to compute the encryption_key and the authentication_key.

let hkdf = Hkdf::new(
    random_hkdf_salt,
    master_key, //ikm
    blake3, //hash function
);
let encryption_key: [u8; 32] = hkdf.expand(b"doby_encryption_key");
let authentication_key: [u8; 32] = hkdf.expand(b"doby_authentication_key");

Next, doby initializes a BLAKE3 HMAC with authentication_key and add all public encryption parameters to it.

let hmac = Hmac::new(
    authentication_key,
    blake3, //hash function
);
hmac.update(random_password_salt);
hmac.update(argon2_time_cost);
hmac.update(argon2_memory_cost);
hmac.update(argon2_parallelism);
hmac.update(random_hkdf_salt);
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.

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.

let cipher = Aes256Ctr::new(encryption_key, random_nonce); //example with AES-CTR
let mut n = 1;
let mut chunk: [u8; block_size] = [0; block_size];
while n != 0 {
    n = input.read(&mut chunk); //read plaintext
    cipher.apply_keystream(&mut chunk[..n]); //encrypt
    hmac.update(chunk[..n]);
    output.write(chunk[..n]); //write ciphertext
}

Once the whole plaintext is encrypted, doby computes and appends the HMAC to the ciphertext.

output.write(hmac.digest());

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.

let master_key: [u8; 32] = argon2id(
    password,
    password_salt_read_from_input,
    argon2_time_cost_read_from_input,
    argon2_memory_cost_read_from_input,
    argon2_parallelism_read_from_input,
);

encryption_key and authentication_key are computed from master_key and the HKDF salt 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.

let cipher = XChaCha20::new(encryption_key, nonce_read_from_input); //example with XChaCha20
let mut n = 1;
let mut chunk: [u8; block_size] = [0; block_size];
while n != 0 {
    n = input.read(&mut chunk); //read ciphertext
    hmac.update(chunk[..n]);
    cipher.apply_keystream(&mut chunk[..n]); //decrypt
    output.write(chunk[..n]); //write plaintext
}

Once the whole ciphertext is decrypted, doby computes and verifies the HMAC.

hmac.digest() == last_32_bytes_read

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.