Compare commits
11 Commits
Author | SHA1 | Date |
---|---|---|
Matéo Duparc | 61b8bb5e49 | |
Matéo Duparc | 52c79d28a7 | |
Matéo Duparc | 956aea50ef | |
Matéo Duparc | 2baece0122 | |
Matéo Duparc | dbd4563e77 | |
Matéo Duparc | 89be84860d | |
Matéo Duparc | 10153f6316 | |
Matéo Duparc | 1f50973381 | |
Matéo Duparc | 311677d195 | |
Matéo Duparc | 2c8ab7e8ad | |
Matéo Duparc | 382ce3c389 |
|
@ -1,3 +1,4 @@
|
|||
.DS_Store
|
||||
/target
|
||||
/local
|
||||
/local
|
||||
/.vscode
|
||||
|
|
|
@ -210,7 +210,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "doby"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"argon2",
|
||||
|
@ -220,10 +220,10 @@ dependencies = [
|
|||
"clap",
|
||||
"cpufeatures 0.2.1",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"num_enum",
|
||||
"rand",
|
||||
"rpassword",
|
||||
"subtle",
|
||||
"tempfile",
|
||||
"zeroize",
|
||||
]
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
[package]
|
||||
name = "doby"
|
||||
version = "0.2.0"
|
||||
edition = "2018"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
authors = ["Hardcore Sushi <hardcore.sushi@disroot.org>"]
|
||||
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"
|
||||
repository = "https://forge.chapril.org/hardcoresushi/doby"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
@ -21,7 +22,7 @@ num_enum = "0.5"
|
|||
cpufeatures = "0.2"
|
||||
aes = { version = "0.7", features = ["ctr"] }
|
||||
chacha20 = "0.8"
|
||||
hmac = "0.11"
|
||||
subtle = "2.4"
|
||||
blake2 = "0.9"
|
||||
hkdf = "0.11"
|
||||
argon2 = "0.3"
|
||||
|
|
75
README.md
75
README.md
|
@ -1,19 +1,19 @@
|
|||
# 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
|
||||
|
||||
* 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
|
||||
* Password brute-force resistance with [Argon2](https://en.wikipedia.org/wiki/Argon2)
|
||||
* Increase the plaintext size of only 142 bytes
|
||||
* Increase the plaintext size of only 113 bytes
|
||||
* 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__.
|
||||
|
||||
# Usage
|
||||
|
@ -28,7 +28,7 @@ Decryption:
|
|||
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
|
||||
# Read from stdin and write to stdout
|
||||
cat my-super-secret-music.flac | doby > encrypted.doby
|
||||
|
@ -40,19 +40,19 @@ doby encrypted.doby > decrypted.flac
|
|||
cat my-super-secret-logs-file.log | doby - logs.doby
|
||||
```
|
||||
|
||||
Speicfy password from the command line:
|
||||
Specify password from the command line:
|
||||
```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:
|
||||
```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:
|
||||
```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
|
||||
|
@ -63,14 +63,15 @@ USAGE:
|
|||
|
||||
FLAGS:
|
||||
-f, --force-encrypt Encrypt even if doby format is recognized
|
||||
-i, --interactive Prompt before overwriting files
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-p, --password <password> Password used to derive encryption keys
|
||||
-i, --iterations <iterations> Argon2 time cost [default: 10]
|
||||
-m, --memory-cost <memory cost> Argon2 memory cost (in kilobytes) [default: 4096]
|
||||
-t, --threads <threads> Argon2 parallelism (between 1 and 255) [default: 4]
|
||||
--password <password> Password used to derive encryption keys
|
||||
-t, --time-cost <iterations> Argon2 time cost [default: 10]
|
||||
-m, --memory-cost <memory size> Argon2 memory cost (in kilobytes) [default: 4096]
|
||||
-p, --parallelism <threads> Argon2 parallelism cost [default: 4]
|
||||
-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]
|
||||
|
||||
|
@ -82,7 +83,7 @@ ARGS:
|
|||
# Installation
|
||||
You can download doby from the "Releases" section in this repo.
|
||||
|
||||
All binaries MUST be signed with my PGP key available on keyservers. To import it:
|
||||
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
|
||||
```
|
||||
|
@ -95,15 +96,25 @@ 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:
|
||||
If everything goes fine, you can download the package corresponding to your distribution. To verify it, compute its SHA-256 hash:
|
||||
```bash
|
||||
sha256sum <doby binary file>
|
||||
sha256sum <file>
|
||||
```
|
||||
Compare this output and the hash in the PGP-signed message. __Don't execute the file if the hashes don't match!__
|
||||
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.
|
||||
|
||||
You can make available doby in your `$PATH` by running:
|
||||
On debian:
|
||||
```bash
|
||||
sudo cp <doby binary file> /usr/local/bin/
|
||||
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
|
||||
|
@ -119,6 +130,8 @@ cargo build --release --bin doby #outputs to ./target/release/doby
|
|||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
@ -146,12 +159,14 @@ let encryption_key: [u8; 32] = hkdf.expand(b"doby_encryption_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.
|
||||
|
||||
```rust
|
||||
let hmac = Hmac::new(
|
||||
let hmac = Blake2b::new_keyed(
|
||||
authentication_key,
|
||||
blake2b, //hash function
|
||||
32, //digest size
|
||||
);
|
||||
hmac.update(random_salt);
|
||||
//integers are encoded in big-endian
|
||||
|
@ -214,7 +229,7 @@ So here is what an encrypted file layout looks like:
|
|||
</tr>
|
||||
<tr>
|
||||
<th align="left">HMAC</th>
|
||||
<td>64 bytes</td>
|
||||
<td>32 bytes</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
@ -251,9 +266,19 @@ while n != 0 {
|
|||
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
|
||||
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._
|
||||
_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.
|
|
@ -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
|
|
@ -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 "$@"
|
|
@ -0,0 +1 @@
|
|||
/*.gz
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1,3 @@
|
|||
*.deb
|
||||
*.gz
|
||||
*.pkg.tar.zst
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
../../../completions
|
|
@ -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
|
|
@ -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
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
};
|
||||
use doby::{
|
||||
encrypt, decrypt,
|
||||
crypto::{ArgonParams, EncryptionParams, CipherAlgorithm, DobyCipher}
|
||||
crypto::{EncryptionParams, CipherAlgorithm, DobyCipher}
|
||||
};
|
||||
|
||||
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 output = OpenOptions::new().create(true).truncate(true).write(true).open(&args[2])?;
|
||||
|
||||
let params = EncryptionParams::new(ArgonParams{
|
||||
t_cost: 1,
|
||||
m_cost: 8,
|
||||
parallelism: 1,
|
||||
}, CipherAlgorithm::AesCtr);
|
||||
let params = EncryptionParams::new(
|
||||
argon2::Params::new(8, 1, 1, None).unwrap(),
|
||||
CipherAlgorithm::AesCtr
|
||||
);
|
||||
|
||||
let mut best_encrypt_time = 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 writer = BufWriter::with_capacity(block_size, &output);
|
||||
|
||||
let cipher = DobyCipher::new(PASSWORD.into(), ¶ms).unwrap();
|
||||
let cipher = DobyCipher::new(PASSWORD.as_bytes(), ¶ms);
|
||||
let t_encrypt = Instant::now();
|
||||
encrypt(&mut reader, &mut writer, ¶ms, cipher, block_size, None)?;
|
||||
writer.flush()?;
|
||||
|
@ -59,7 +58,7 @@ fn main() -> io::Result<()> {
|
|||
reset(&mut reader)?;
|
||||
reset(&mut writer)?;
|
||||
|
||||
let cipher = DobyCipher::new(PASSWORD.into(), ¶ms).unwrap();
|
||||
let cipher = DobyCipher::new(PASSWORD.as_bytes(), ¶ms);
|
||||
let t_decrypt = Instant::now();
|
||||
decrypt(&mut reader, &mut writer, cipher, block_size)?;
|
||||
writer.flush()?;
|
||||
|
|
|
@ -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>");
|
||||
}
|
||||
}
|
|
@ -10,15 +10,15 @@ fn main() -> io::Result<()> {
|
|||
if magic_bytes == MAGIC_BYTES {
|
||||
match EncryptionParams::read(&mut file)? {
|
||||
Some(params) => {
|
||||
println!("Argon2 time cost: {}", params.argon2.t_cost);
|
||||
println!("Argon2 memory cost: {}KB", params.argon2.m_cost);
|
||||
println!("Argon2 parallelism: {}", params.argon2.parallelism);
|
||||
println!("Argon2 time cost: {}", params.argon2.t_cost());
|
||||
println!("Argon2 memory cost: {}KB", params.argon2.m_cost());
|
||||
println!("Argon2 parallelism cost: {}", params.argon2.p_cost());
|
||||
println!("Encryption cihpher: {}", params.cipher);
|
||||
}
|
||||
None => eprintln!("Invalid cipher")
|
||||
None => eprintln!("Invalid parameters")
|
||||
}
|
||||
} else {
|
||||
eprintln!("Doby format not recognized.");
|
||||
eprintln!("doby format not recognized.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
117
src/cli.rs
117
src/cli.rs
|
@ -1,48 +1,65 @@
|
|||
use std::{
|
||||
path::Path,
|
||||
fs::File,
|
||||
str::FromStr,
|
||||
io::{stdin, stdout, Read},
|
||||
};
|
||||
use std::{fs::File, io::{self, Read, stdin, stdout}, path::Path, str::FromStr};
|
||||
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");
|
||||
|
||||
pub struct CliArgs {
|
||||
pub password: Password,
|
||||
pub password: WrappedPassword,
|
||||
pub force_encrypt: bool,
|
||||
pub argon2_params: ArgonParams,
|
||||
pub argon2_params: argon2::Params,
|
||||
pub cipher: CipherAlgorithm,
|
||||
pub block_size: usize,
|
||||
pub reader: Box<dyn Read>,
|
||||
pub writer: LazyWriter<String>,
|
||||
pub writer: WrappedWriter<String>,
|
||||
}
|
||||
|
||||
pub fn parse() -> Option<CliArgs> {
|
||||
let app = App::new(crate_name!())
|
||||
pub struct ParseResult {
|
||||
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!())
|
||||
.setting(AppSettings::ColoredHelp)
|
||||
.about("Secure symmetric encryption from the command line.")
|
||||
.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("force-encrypt")
|
||||
Arg::with_name("1_force_encrypt")
|
||||
.short("f")
|
||||
.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::with_name("1_password")
|
||||
.short("p")
|
||||
.long("password")
|
||||
.value_name("password")
|
||||
.help("Password used to derive encryption keys")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("2_t_cost")
|
||||
.short("i")
|
||||
.long("iterations")
|
||||
.short("t")
|
||||
.long("time-cost")
|
||||
.value_name("iterations")
|
||||
.help("Argon2 time cost")
|
||||
.default_value("10")
|
||||
|
@ -51,16 +68,16 @@ pub fn parse() -> Option<CliArgs> {
|
|||
Arg::with_name("3_m_cost")
|
||||
.short("m")
|
||||
.long("memory-cost")
|
||||
.value_name("memory cost")
|
||||
.value_name("memory size")
|
||||
.help("Argon2 memory cost (in kilobytes)")
|
||||
.default_value("4096")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("4_parallelism")
|
||||
.short("t")
|
||||
.long("threads")
|
||||
Arg::with_name("4_p_cost")
|
||||
.short("p")
|
||||
.long("parallelism")
|
||||
.value_name("threads")
|
||||
.help("Argon2 parallelism (between 1 and 255)")
|
||||
.help("Argon2 parallelism cost")
|
||||
.default_value("4")
|
||||
)
|
||||
.arg(
|
||||
|
@ -80,19 +97,24 @@ pub fn parse() -> Option<CliArgs> {
|
|||
.possible_values(&["aes", "xchacha20"])
|
||||
.case_insensitive(true)
|
||||
)
|
||||
.get_matches();
|
||||
}
|
||||
|
||||
pub fn parse() -> Option<ParseResult> {
|
||||
let app = app().get_matches();
|
||||
|
||||
let params = {
|
||||
let t_cost = number(app.value_of("2_t_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 {
|
||||
t_cost,
|
||||
m_cost,
|
||||
parallelism,
|
||||
match argon2::Params::new(m_cost, t_cost, p_cost, None) {
|
||||
Ok(params) => Some(params),
|
||||
Err(e) => {
|
||||
eprintln!("Invalid Argon2 parameters: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
}?;
|
||||
|
||||
let cipher = app
|
||||
.value_of("cipher")
|
||||
|
@ -125,35 +147,44 @@ pub fn parse() -> Option<CliArgs> {
|
|||
None => Box::new(stdin())
|
||||
};
|
||||
|
||||
let output = app
|
||||
let wrapped_writer = match app
|
||||
.value_of("OUTPUT")
|
||||
.and_then(|s| if s == "-" { None } else { Some(s) })
|
||||
.map(|s| {
|
||||
if Path::new(s).exists() {
|
||||
eprintln!("WARNING: {} already exists", s);
|
||||
None
|
||||
} else {
|
||||
Some(LazyWriter::from_path(s.to_owned()))
|
||||
.and_then(|s| if s == "-" { None } else { Some(s) }) {
|
||||
Some(path) => {
|
||||
if {
|
||||
if app.is_present("2_interactive") && Path::new(path).exists() {
|
||||
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 {
|
||||
true
|
||||
}
|
||||
} {
|
||||
WrappedWriter::from_path(path.to_string())
|
||||
} else {
|
||||
return Some(ParseResult::exited())
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| Some(LazyWriter::from_writer(stdout())))?;
|
||||
None => WrappedWriter::from_writer(stdout())
|
||||
};
|
||||
|
||||
Some(CliArgs {
|
||||
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,
|
||||
cipher,
|
||||
block_size,
|
||||
reader: input,
|
||||
writer: output,
|
||||
})
|
||||
writer: wrapped_writer,
|
||||
}.into())
|
||||
}
|
||||
|
||||
fn number<T: FromStr>(val: &str) -> Option<T> {
|
||||
match val.parse::<T>() {
|
||||
Ok(n) => Some(n),
|
||||
Err(_) => {
|
||||
eprintln!("Cannot parse '{}' to '{}'", val, std::any::type_name::<T>());
|
||||
eprintln!("Error: '{}' is not a number", val);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
186
src/crypto.rs
186
src/crypto.rs
|
@ -1,34 +1,20 @@
|
|||
use std::{convert::{TryFrom, TryInto}, fmt::{self, Display, Formatter}, io::{self, Read, Write}};
|
||||
use std::{convert::TryFrom, fmt::{self, Display, Formatter}, io::{self, Read, Write}};
|
||||
use blake2::{Blake2b, VarBlake2b, digest::{Update, VariableOutput}};
|
||||
use num_enum::TryFromPrimitive;
|
||||
use chacha20::XChaCha20;
|
||||
use aes::{Aes256Ctr, cipher::{NewCipher, StreamCipher}};
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use subtle::ConstantTimeEq;
|
||||
use rand::{Rng, rngs::OsRng};
|
||||
use argon2::{Argon2, Version, Algorithm};
|
||||
use hkdf::Hkdf;
|
||||
use zeroize::Zeroize;
|
||||
use crate::Password;
|
||||
|
||||
pub const SALT_LEN: usize = 64;
|
||||
const AES_NONCE_LEN: usize = 16;
|
||||
const XCHACHA20_NONCE_LEN: usize = 24;
|
||||
pub const HASH_LEN: usize = 64;
|
||||
pub const HMAC_LEN: usize = 32;
|
||||
const KEY_LEN: usize = 32;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct ArgonParams {
|
||||
pub t_cost: u32,
|
||||
pub m_cost: u32,
|
||||
pub parallelism: u8,
|
||||
}
|
||||
|
||||
impl TryFrom<ArgonParams> for argon2::Params {
|
||||
type Error = argon2::Error;
|
||||
fn try_from(params: ArgonParams) -> Result<Self, Self::Error> {
|
||||
argon2::Params::new(params.m_cost, params.t_cost, params.parallelism.into(), None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum CipherAlgorithm {
|
||||
|
@ -57,14 +43,14 @@ impl Display for CipherAlgorithm {
|
|||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct EncryptionParams {
|
||||
salt: [u8; SALT_LEN],
|
||||
pub argon2: ArgonParams,
|
||||
pub argon2: argon2::Params,
|
||||
pub cipher: CipherAlgorithm,
|
||||
}
|
||||
|
||||
impl EncryptionParams {
|
||||
pub const LEN: usize = SALT_LEN + 4*2 + 2;
|
||||
pub const LEN: usize = SALT_LEN + 4*3 + 1;
|
||||
|
||||
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];
|
||||
OsRng.fill(&mut salt);
|
||||
EncryptionParams {
|
||||
|
@ -76,9 +62,9 @@ impl EncryptionParams {
|
|||
|
||||
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(&self.salt)?;
|
||||
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.parallelism.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.p_cost().to_be_bytes())?;
|
||||
writer.write_all(&(self.cipher as u8).to_be_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -90,97 +76,75 @@ impl EncryptionParams {
|
|||
reader.read_exact(&mut t_cost)?;
|
||||
let mut m_cost = [0; 4];
|
||||
reader.read_exact(&mut m_cost)?;
|
||||
let mut parallelism = [0; 1];
|
||||
reader.read_exact(&mut parallelism)?;
|
||||
let mut p_cost = [0; 4];
|
||||
reader.read_exact(&mut p_cost)?;
|
||||
let mut cipher_buff = [0; 1];
|
||||
reader.read_exact(&mut cipher_buff)?;
|
||||
match CipherAlgorithm::try_from(cipher_buff[0]) {
|
||||
Ok(cipher) => {
|
||||
let argon2_params = ArgonParams {
|
||||
t_cost: u32::from_be_bytes(t_cost),
|
||||
m_cost: u32::from_be_bytes(m_cost),
|
||||
parallelism: u8::from_be_bytes(parallelism),
|
||||
};
|
||||
|
||||
Ok(Some(EncryptionParams {
|
||||
if let Ok(cipher) = CipherAlgorithm::try_from(cipher_buff[0]) {
|
||||
if let Ok(argon2_params) = argon2::Params::new(
|
||||
u32::from_be_bytes(m_cost),
|
||||
u32::from_be_bytes(t_cost),
|
||||
u32::from_be_bytes(p_cost),
|
||||
None
|
||||
) {
|
||||
return Ok(Some(EncryptionParams {
|
||||
salt,
|
||||
argon2: argon2_params,
|
||||
cipher,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
Err(_) => 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
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DobyCipher {
|
||||
cipher: Box<dyn StreamCipher>,
|
||||
hmac: Hmac<blake2::Blake2b>,
|
||||
hasher: VarBlake2b,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DobyCipher {
|
||||
pub fn new(mut password: Password, params: &EncryptionParams) -> Result<Self, argon2::Error> {
|
||||
match params.argon2.try_into() {
|
||||
Ok(argon2_params) => {
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||
let mut master_key = [0; KEY_LEN];
|
||||
let password = password.unwrap_or_ask();
|
||||
argon2.hash_password_into(password.as_bytes(), ¶ms.salt, &mut master_key).zeroize(password)?;
|
||||
let hkdf = Hkdf::<blake2::Blake2b>::new(Some(¶ms.salt), &master_key);
|
||||
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];
|
||||
hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap();
|
||||
let mut authentication_key = [0; KEY_LEN];
|
||||
hkdf.expand(b"doby_authentication_key", &mut authentication_key).unwrap();
|
||||
master_key.zeroize();
|
||||
pub fn new(password: &[u8], params: &EncryptionParams) -> Self {
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params.argon2.clone());
|
||||
let mut master_key = [0; KEY_LEN];
|
||||
argon2.hash_password_into(password, ¶ms.salt, &mut master_key).unwrap();
|
||||
let hkdf = Hkdf::<Blake2b>::new(Some(¶ms.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];
|
||||
hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap();
|
||||
let mut authentication_key = [0; KEY_LEN];
|
||||
hkdf.expand(b"doby_authentication_key", &mut authentication_key).unwrap();
|
||||
|
||||
let mut encoded_params = Vec::with_capacity(EncryptionParams::LEN);
|
||||
params.write(&mut encoded_params).unwrap();
|
||||
let mut hmac = Hmac::new_from_slice(&authentication_key).unwrap();
|
||||
authentication_key.zeroize();
|
||||
hmac.update(&encoded_params);
|
||||
let mut encoded_params = Vec::with_capacity(EncryptionParams::LEN);
|
||||
params.write(&mut encoded_params).unwrap();
|
||||
let mut hasher = VarBlake2b::new_keyed(&authentication_key, HMAC_LEN);
|
||||
authentication_key.zeroize();
|
||||
hasher.update(&encoded_params);
|
||||
|
||||
let cipher: Box<dyn StreamCipher> = match params.cipher {
|
||||
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &nonce).unwrap()),
|
||||
CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &nonce).unwrap()),
|
||||
};
|
||||
encryption_key.zeroize();
|
||||
let cipher: Box<dyn StreamCipher> = match params.cipher {
|
||||
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &nonce).unwrap()),
|
||||
CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &nonce).unwrap()),
|
||||
};
|
||||
encryption_key.zeroize();
|
||||
|
||||
Ok(Self {
|
||||
cipher,
|
||||
hmac,
|
||||
buffer: Vec::new(),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
password.zeroize();
|
||||
Err(e)
|
||||
}
|
||||
Self {
|
||||
cipher,
|
||||
hasher,
|
||||
buffer: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_chunk<W: Write>(&mut self, buff: &mut [u8], writer: &mut W) -> io::Result<()> {
|
||||
self.cipher.apply_keystream(buff);
|
||||
self.hmac.update(buff);
|
||||
self.hasher.update(&buff);
|
||||
writer.write_all(buff)
|
||||
}
|
||||
|
||||
pub fn write_hmac<W: Write>(self, writer: &mut W) -> io::Result<usize> {
|
||||
let tag = self.hmac.finalize().into_bytes();
|
||||
writer.write(&tag)
|
||||
pub fn write_hmac<W: Write>(self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(&self.hasher.finalize_boxed())
|
||||
}
|
||||
|
||||
//buff size must be > to HASH_LEN
|
||||
|
@ -189,44 +153,43 @@ impl DobyCipher {
|
|||
buff[..buffer_len].clone_from_slice(&self.buffer);
|
||||
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();
|
||||
buffer_len + read - HASH_LEN
|
||||
buffer_len + read - HMAC_LEN
|
||||
} else {
|
||||
0
|
||||
};
|
||||
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]);
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
pub fn verify_hmac(self) -> bool {
|
||||
self.hmac.verify(&self.buffer).is_ok()
|
||||
self.hasher.finalize_boxed().ct_eq(&self.buffer).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ArgonParams, CipherAlgorithm, EncryptionParams, DobyCipher, HASH_LEN};
|
||||
use super::{CipherAlgorithm, EncryptionParams, DobyCipher, HMAC_LEN};
|
||||
#[test]
|
||||
fn encryption_params() {
|
||||
let params = EncryptionParams::new(ArgonParams {
|
||||
t_cost: 1,
|
||||
m_cost: 8,
|
||||
parallelism: 1,
|
||||
}, CipherAlgorithm::XChaCha20);
|
||||
let params = EncryptionParams::new(
|
||||
argon2::Params::new(8, 1, 1, None).unwrap(),
|
||||
CipherAlgorithm::XChaCha20
|
||||
);
|
||||
|
||||
assert_eq!(EncryptionParams::LEN, 74);
|
||||
assert_eq!(EncryptionParams::LEN, 77);
|
||||
|
||||
let mut buff = Vec::with_capacity(74);
|
||||
params.write(&mut buff).unwrap();
|
||||
assert_eq!(buff[..64], params.salt);
|
||||
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[72], 0x01); //parallelism
|
||||
assert_eq!(buff[73], CipherAlgorithm::XChaCha20 as u8);
|
||||
assert_eq!(buff[72..76], vec![0, 0, 0, 0x01]); //p_cost
|
||||
assert_eq!(buff[76], CipherAlgorithm::XChaCha20 as u8);
|
||||
|
||||
let new_params = EncryptionParams::read(&mut buff.as_slice()).unwrap().unwrap();
|
||||
assert_eq!(new_params, params);
|
||||
|
@ -234,25 +197,24 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn doby_cipher() {
|
||||
let params = EncryptionParams::new(ArgonParams {
|
||||
t_cost: 1,
|
||||
m_cost: 8,
|
||||
parallelism: 1,
|
||||
}, CipherAlgorithm::AesCtr);
|
||||
let params = EncryptionParams::new(
|
||||
argon2::Params::new(8, 1, 1, None).unwrap(),
|
||||
CipherAlgorithm::AesCtr
|
||||
);
|
||||
let password = "I like spaghetti";
|
||||
let plaintext = b"but I love so much to listen to HARDCORE music on big subwoofer";
|
||||
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(), ¶ms).unwrap();
|
||||
let mut enc_cipher = DobyCipher::new(password.as_bytes(), ¶ms);
|
||||
enc_cipher.encrypt_chunk(&mut buff, &mut vec).unwrap();
|
||||
assert_ne!(buff, *plaintext);
|
||||
assert_eq!(buff, vec.as_slice());
|
||||
assert_eq!(enc_cipher.write_hmac(&mut vec).unwrap(), HASH_LEN);
|
||||
assert_eq!(vec.len(), buff.len()+HASH_LEN);
|
||||
assert!(enc_cipher.write_hmac(&mut vec).is_ok());
|
||||
assert_eq!(vec.len(), buff.len()+HMAC_LEN);
|
||||
|
||||
let mut dec_cipher = DobyCipher::new(password.into(), ¶ms).unwrap();
|
||||
let mut decrypted = vec![0; buff.len()+HASH_LEN];
|
||||
let mut dec_cipher = DobyCipher::new(password.as_bytes(), ¶ms);
|
||||
let mut decrypted = vec![0; buff.len()+HMAC_LEN];
|
||||
let mut n = dec_cipher.decrypt_chunk(&mut vec.as_slice(), &mut decrypted[..]).unwrap();
|
||||
assert_eq!(n, buff.len());
|
||||
n = dec_cipher.decrypt_chunk(&mut &vec[n..], &mut decrypted[n..]).unwrap();
|
||||
|
|
82
src/lib.rs
82
src/lib.rs
|
@ -1,68 +1,68 @@
|
|||
pub mod cli;
|
||||
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 zeroize::Zeroize;
|
||||
|
||||
pub const MAGIC_BYTES: &[u8; 4] = b"DOBY";
|
||||
|
||||
pub struct Password(Option<String>);
|
||||
pub struct WrappedPassword(Option<String>);
|
||||
|
||||
impl Password {
|
||||
fn unwrap_or_ask(self) -> String {
|
||||
self.0.unwrap_or_else(|| rpassword::read_password_from_tty(Some("Password: ")).unwrap())
|
||||
impl WrappedPassword {
|
||||
pub fn get(self, ask_confirm: bool) -> Option<String> {
|
||||
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 {
|
||||
Self(s.map(String::from))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Password {
|
||||
fn from(s: &str) -> Self {
|
||||
Some(s).into()
|
||||
pub enum WrappedWriter<P: AsRef<Path>> {
|
||||
PATH {
|
||||
path: P
|
||||
},
|
||||
WRITER {
|
||||
writer: Box<dyn Write>
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
impl<P: AsRef<Path> + Display> WrappedWriter<P> {
|
||||
fn from_path(path: P) -> Self {
|
||||
Self {
|
||||
path: Some(path),
|
||||
writer: None,
|
||||
}
|
||||
Self::PATH { path }
|
||||
}
|
||||
|
||||
fn from_writer<T: 'static + Write>(writer: T) -> Self {
|
||||
Self {
|
||||
path: None,
|
||||
writer: Some(Box::new(writer)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> Write for LazyWriter<P> {
|
||||
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)
|
||||
Self::WRITER { writer: Box::new(writer) }
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.as_mut().unwrap().flush()
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
83
src/main.rs
83
src/main.rs
|
@ -1,4 +1,4 @@
|
|||
use std::{process, io::{BufWriter, BufReader, Read}};
|
||||
use std::{process, io::{BufReader, Read}};
|
||||
use doby::{
|
||||
cli,
|
||||
crypto::{EncryptionParams, DobyCipher},
|
||||
|
@ -6,63 +6,72 @@ use doby::{
|
|||
decrypt,
|
||||
encrypt,
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
fn run() -> bool {
|
||||
let mut success = false;
|
||||
if let Some(cli_args) = cli::parse() {
|
||||
let mut reader = BufReader::with_capacity(cli_args.block_size, cli_args.reader);
|
||||
let mut writer = BufWriter::with_capacity(cli_args.block_size, cli_args.writer);
|
||||
if let Some(result) = cli::parse() {
|
||||
if let Some(cli_args) = result.cli_args {
|
||||
let mut reader = BufReader::new(cli_args.reader);
|
||||
|
||||
let mut magic_bytes = vec![0; MAGIC_BYTES.len()];
|
||||
match reader.read(&mut magic_bytes) {
|
||||
Ok(n) => {
|
||||
if magic_bytes == MAGIC_BYTES && !cli_args.force_encrypt { //we probably want to decrypt
|
||||
match EncryptionParams::read(&mut reader) {
|
||||
Ok(params) => {
|
||||
match params {
|
||||
Some(params) => {
|
||||
match DobyCipher::new(cli_args.password, ¶ms) {
|
||||
Ok(cipher) => {
|
||||
let mut magic_bytes = vec![0; MAGIC_BYTES.len()];
|
||||
match reader.read(&mut magic_bytes) {
|
||||
Ok(n) => {
|
||||
if magic_bytes == MAGIC_BYTES && !cli_args.force_encrypt { //we probably want to decrypt
|
||||
match EncryptionParams::read(&mut reader) {
|
||||
Ok(params) => {
|
||||
if let Some(params) = params {
|
||||
if let Some(mut password) = cli_args.password.get(false) {
|
||||
if let Some(mut writer) = cli_args.writer.into_buf_writer() {
|
||||
let cipher = DobyCipher::new(password.as_bytes(), ¶ms);
|
||||
password.zeroize();
|
||||
match decrypt(&mut reader, &mut writer, cipher, cli_args.block_size) {
|
||||
Ok(verified) => {
|
||||
if verified {
|
||||
success = true
|
||||
} 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)
|
||||
}
|
||||
} else {
|
||||
password.zeroize();
|
||||
}
|
||||
Err(e) => eprintln!("Invalid argon2 params: {}", e)
|
||||
}
|
||||
} else {
|
||||
eprintln!("Error: invalid encryption parameters")
|
||||
}
|
||||
None => eprintln!("Invalid cipher")
|
||||
}
|
||||
Err(e) => eprintln!("I/O error while reading headers: {}", e)
|
||||
}
|
||||
} else { //otherwise, encrypt
|
||||
let params = EncryptionParams::new(cli_args.argon2_params, cli_args.cipher);
|
||||
if let Some(mut password) = cli_args.password.get(true) {
|
||||
if let Some(mut writer) = cli_args.writer.into_buf_writer() {
|
||||
let cipher = DobyCipher::new(password.as_bytes(), ¶ms);
|
||||
password.zeroize();
|
||||
match encrypt(
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
¶ms,
|
||||
cipher,
|
||||
cli_args.block_size,
|
||||
Some(&magic_bytes[..n])
|
||||
) {
|
||||
Ok(_) => success = true,
|
||||
Err(e) => eprintln!("I/O error while encrypting: {}", e)
|
||||
}
|
||||
} else {
|
||||
password.zeroize();
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("I/O error while reading headers: {}", e)
|
||||
}
|
||||
} else { //otherwise, encrypt
|
||||
let params = EncryptionParams::new(cli_args.argon2_params, cli_args.cipher);
|
||||
match DobyCipher::new(cli_args.password, ¶ms) {
|
||||
Ok(cipher) => {
|
||||
match encrypt(
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
¶ms,
|
||||
cipher,
|
||||
cli_args.block_size,
|
||||
Some(&magic_bytes[..n])
|
||||
) {
|
||||
Ok(_) => success = true,
|
||||
Err(e) => eprintln!("I/O error while encrypting: {}", e)
|
||||
}
|
||||
}
|
||||
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
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use rand::Rng;
|
||||
use doby::{
|
||||
crypto::{
|
||||
ArgonParams,
|
||||
CipherAlgorithm,
|
||||
EncryptionParams,
|
||||
DobyCipher,
|
||||
|
@ -19,15 +18,14 @@ fn different_elements<T: Eq>(v1: &Vec<T>, v2: &Vec<T>) -> usize {
|
|||
fn authentication() {
|
||||
const BLOCK_SIZE: usize = 65536;
|
||||
const PLAINTEXT: &[u8; 13] = b"the plaintext";
|
||||
const CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+142;
|
||||
const CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+113;
|
||||
const PASSWORD: &str = "the password";
|
||||
let params = EncryptionParams::new(ArgonParams {
|
||||
t_cost: 1,
|
||||
m_cost: 8,
|
||||
parallelism: 1,
|
||||
}, CipherAlgorithm::AesCtr);
|
||||
let params = EncryptionParams::new(
|
||||
argon2::Params::new(8, 1, 1, None).unwrap(),
|
||||
CipherAlgorithm::AesCtr
|
||||
);
|
||||
|
||||
let encrypter = DobyCipher::new(PASSWORD.into(), ¶ms).unwrap();
|
||||
let encrypter = DobyCipher::new(PASSWORD.as_bytes(), ¶ms);
|
||||
let mut ciphertext = Vec::with_capacity(CIPHERTEXT_SIZE);
|
||||
encrypt(&mut &PLAINTEXT[..], &mut ciphertext, ¶ms, encrypter, BLOCK_SIZE, None).unwrap();
|
||||
assert_eq!(ciphertext.len(), CIPHERTEXT_SIZE);
|
||||
|
@ -38,13 +36,13 @@ fn authentication() {
|
|||
compromised[i] = rand::thread_rng().gen();
|
||||
}
|
||||
assert_eq!(different_elements(&compromised, &ciphertext), 1);
|
||||
let decrypter = DobyCipher::new(PASSWORD.into(), ¶ms).unwrap();
|
||||
let decrypter = DobyCipher::new(PASSWORD.as_bytes(), ¶ms);
|
||||
let mut decrypted = Vec::with_capacity(PLAINTEXT.len());
|
||||
let verified = decrypt(&mut &compromised[..], &mut decrypted, decrypter, BLOCK_SIZE).unwrap();
|
||||
assert_eq!(verified, false);
|
||||
}
|
||||
|
||||
let decrypter = DobyCipher::new(PASSWORD.into(), ¶ms).unwrap();
|
||||
let decrypter = DobyCipher::new(PASSWORD.as_bytes(), ¶ms);
|
||||
let mut decrypted = Vec::with_capacity(PLAINTEXT.len());
|
||||
let verified = decrypt(&mut &ciphertext[4+EncryptionParams::LEN..], &mut decrypted, decrypter, BLOCK_SIZE).unwrap();
|
||||
assert_eq!(decrypted, PLAINTEXT);
|
||||
|
|
27
tests/cli.rs
27
tests/cli.rs
|
@ -1,7 +1,7 @@
|
|||
use std::{convert::TryInto, fs::{self, File, create_dir}, io::{self, Read, Write}, path::PathBuf};
|
||||
use assert_cmd::{Command, cargo::{CargoError, cargo_bin}};
|
||||
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 PASSWORD: &str = "the password";
|
||||
|
@ -22,7 +22,7 @@ fn setup_files<'a>() -> io::Result<(PathBuf, PathBuf, PathBuf)> {
|
|||
|
||||
fn doby_cmd() -> Result<Command, CargoError> {
|
||||
let mut cmd = Command::cargo_bin("doby")?;
|
||||
cmd.arg("-p").arg(PASSWORD);
|
||||
cmd.arg("--password").arg(PASSWORD);
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ fn files() -> io::Result<()> {
|
|||
fn stdout() -> io::Result<()> {
|
||||
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("");
|
||||
|
||||
doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT);
|
||||
|
@ -64,10 +64,10 @@ fn stdout() -> io::Result<()> {
|
|||
fn stdin() -> io::Result<()> {
|
||||
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("");
|
||||
|
||||
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);
|
||||
|
||||
Ok(())
|
||||
|
@ -85,7 +85,7 @@ fn force_encrypt() -> io::Result<()> {
|
|||
let buff_ciphertext_2 = fs::read(&tmp_ciphertext_2)?;
|
||||
assert_ne!(buff_ciphertext_1, buff_ciphertext_2);
|
||||
assert_ne!(buff_ciphertext_2, PLAINTEXT);
|
||||
assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+142);
|
||||
assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+113);
|
||||
|
||||
let tmp_decrypted_1 = tmp_path.join("decrypted_1");
|
||||
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("");
|
||||
|
||||
let ciphertext = fs::read(&tmp_ciphertext)?;
|
||||
assert_eq!(ciphertext[4+SALT_LEN+4*2+1], cipher_algorithm as u8);
|
||||
assert_eq!(ciphertext.len(), PLAINTEXT.len()+14+SALT_LEN+HASH_LEN);
|
||||
assert_eq!(ciphertext[4+SALT_LEN+4*3], cipher_algorithm as u8);
|
||||
assert_eq!(ciphertext.len(), PLAINTEXT.len()+17+SALT_LEN+HMAC_LEN);
|
||||
|
||||
doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr("");
|
||||
|
||||
|
@ -127,17 +127,16 @@ fn aes_cipher() -> io::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
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("-m").arg("0").assert().failure().stderr("Invalid argon2 params: memory cost is too small\n");
|
||||
Command::cargo_bin("doby").unwrap().arg("-t").arg("0").assert().failure().stderr("Invalid argon2 params: not enough threads\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 parameters: memory cost is too small\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..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(())
|
||||
}
|
Loading…
Reference in New Issue