doby/README.md

259 lines
8.1 KiB
Markdown
Raw Normal View History

2021-06-25 23:01:50 +02:00
# doby
Secure 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.
2021-06-25 23:01:50 +02:00
# Features
2021-06-25 23:01:50 +02:00
2021-06-27 20:35:23 +02:00
* 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)
2021-06-25 23:01:50 +02:00
* [HMAC](https://en.wikipedia.org/wiki/HMAC) ciphertext authentication
* Password brute-force resistance with [Argon2](https://en.wikipedia.org/wiki/Argon2)
2021-08-30 16:06:55 +02:00
* Increase the plaintext size of only 142 bytes
2021-06-25 23:01:50 +02:00
* Encryption from STDIN/STDOUT or from files
* Adjustable performance & secuity parameters
# Disclamer
2021-06-25 23:01:50 +02:00
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
2021-06-25 23:01:50 +02:00
Encryption:
2021-06-25 23:01:50 +02:00
```bash
doby my-super-secret-source-code.rs encrypted.doby
```
Decryption:
2021-06-25 23:01:50 +02:00
```bash
doby encrypted.doby decrypted.rs
```
If you ommit file path or use `-`, doby operates from `stdin/stdout`:
2021-06-25 23:01:50 +02:00
```bash
# 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:
2021-06-25 23:01:50 +02:00
```bash
doby -p "A super very ultra strong passphrase" my-super-secret-document.pdf document.doby
```
Double encryption:
2021-06-25 23:01:50 +02:00
```bash
doby -p "first password" my-super-secret-database.db | doby -f - double-encrypted.doby
```
Increase password brute-force resistance:
2021-06-25 23:01:50 +02:00
```bash
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:
2021-06-27 20:35:23 +02:00
-p, --password <password> Password used to derive encryption keys
2021-07-08 18:16:50 +02:00
-i, --iterations <iterations> Argon2 time cost [default: 10]
2021-06-25 23:01:50 +02:00
-m, --memory-cost <memory cost> Argon2 memory cost (in kilobytes) [default: 4096]
-t, --threads <threads> Argon2 parallelism (between 1 and 255) [default: 4]
2021-07-08 18:16:50 +02:00
-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]
2021-06-25 23:01:50 +02:00
ARGS:
<INPUT> <PATH> | "-" or empty for stdin
<OUTPUT> <PATH> | "-" or empty for stdout
```
# Installation
You can download doby from the "Releases" section in this repo.
2021-06-25 23:01:50 +02:00
All binaries MUST be signed with my PGP key available on keyservers. To import it:
```bash
2021-06-25 23:01:50 +02:00
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>`
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 compute the SHA-256 hash of the binary file you want to verify:
```bash
sha256sum <doby binary file>
```
Compare this output and the hash in the PGP-signed message. __Don't execute the file if the hashes don't match!__
You can make available doby in your `$PATH` by running:
```bash
sudo cp <doby binary file> /usr/local/bin/
```
# Build
You should verify commits before building the binary. Follow the steps in [Installation](#installation) to import my PGP key.
2021-06-25 23:01:50 +02:00
```bash
git clone --depth=1 https://forge.chapril.org/hardcoresushi/doby.git
cd doby
git verify-commit HEAD #you need to import my PGP key to verify the commit signature
2021-06-25 23:01:50 +02:00
cargo build --release #outputs to ./target/release/doby
```
# Cryptographic details
2021-06-25 23:01:50 +02:00
### 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.
```rust
let master_key: [u8; 32] = argon2id(
password,
random_salt,
2021-06-25 23:01:50 +02:00
argon2_time_cost,
argon2_memory_cost,
argon2_parallelism,
);
```
2021-08-30 16:06:55 +02:00
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`.
2021-06-25 23:01:50 +02:00
```rust
let hkdf = Hkdf::new(
random_salt,
2021-06-25 23:01:50 +02:00
master_key, //ikm
blake2b, //hash function
2021-06-25 23:01:50 +02:00
);
2021-08-30 16:06:55 +02:00
let nonce: [u8; 16] = hkdf.expand(b"doby_nonce"); //(16 bytes for AES-CTR, 24 for XChaCha20)
2021-06-25 23:01:50 +02:00
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 [BLAKE2b](https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2) HMAC with `authentication_key` and add all public encryption parameters to it.
2021-06-25 23:01:50 +02:00
```rust
let hmac = Hmac::new(
authentication_key,
blake2b, //hash function
2021-06-25 23:01:50 +02:00
);
hmac.update(random_salt);
//integers are encoded in big-endian
2021-06-25 23:01:50 +02:00
hmac.update(argon2_time_cost);
hmac.update(argon2_memory_cost);
hmac.update(argon2_parallelism);
2021-06-27 20:35:23 +02:00
hmac.update(cipher); //1-byte representation of the symmetric cipher used to encrypt (either AES-CTR or XChaCha20)
2021-06-25 23:01:50 +02:00
```
All this parameters are also written in plain text in the header of the doby output.
2021-08-30 16:06:55 +02:00
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.
2021-06-25 23:01:50 +02:00
```rust
2021-08-30 16:06:55 +02:00
let cipher = Aes256Ctr::new(encryption_key, nonce); //example with AES-CTR
2021-06-25 23:01:50 +02:00
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.
```rust
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>64 bytes</td>
</tr>
</table>
2021-06-25 23:01:50 +02:00
### 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.
```rust
let master_key: [u8; 32] = argon2id(
password,
salt_read_from_input,
2021-06-25 23:01:50 +02:00
argon2_time_cost_read_from_input,
argon2_memory_cost_read_from_input,
argon2_parallelism_read_from_input,
);
```
2021-08-30 16:06:55 +02:00
`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.
2021-06-25 23:01:50 +02:00
Then, doby starts decryption.
```rust
2021-08-30 16:06:55 +02:00
let cipher = XChaCha20::new(encryption_key, nonce); //example with XChaCha20
2021-06-25 23:01:50 +02:00
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.
```rust
hmac.digest() == last_64_bytes_read // the default blake2b output size is 64 bytes
2021-06-25 23:01:50 +02:00
```
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._