Welcome!

Welcome to the Dexios Book! Here you will find a lot of information about the project, including: how to use it, best practices, performance metrics and much more. Please view the list of pages to find what you're looking for!

What is Dexios?

Dexios is a command-line file encryption utility, suitable for encrypting files before uploading them to a cloud service. It is written entirely in rust and contains no unsafe code (some dependencies may contain unsafe code, but they have received the correct audits and are deemed secure).

It uses XChaCha20-Poly1305 encryption by default, or AES-256-GCM if you specify --aead 2. Deoxys encryption can be enabled with --aead 3, but I urge you to read the Security Notices beforehand.

argon2id is used to generate the encryption key.

Security Notices

The AES-GCM crate has received a security audit by NCC group, with no significant findings. You can view the audit here.

The ChaCha20-Poly1305 crate has received a security audit by NCC group, with no significant findings. You can view the audit here

The deoxys crate does not have a formal audit, and it is still rather experimental. Due to how new Deoxys is, relatively speaking, it's extremely hard to find a good implementation. We opted to use one created by the same RustCrypto Team that almost all of our other cryptographic dependencies rely on. You will experience decreased encryption and decryption performance while using Deoxys.

argon2 is regarded as the most secure password hashing algorithm at the time being, due to how resistant it is to cracking - that's why we use it! It's especially ideal while dealing with low-entropy inputs, such as passwords.

All other cryptographic functions are deemed secure - but they don't protect your data, so any vulnerabilities in them would not have a detrimental effect on the security and integrity of said data.

Tested Operating Systems

OSWorking?
Void LinuxYes
Fedora 35Yes
Fedora 36Yes
Ubuntu 20.04Yes
FreeBSD 13Yes
FreeBSD 14Yes
Windows 11Mostly1

1 The password will not be hidden in the terminal if you enter it manually. Keyfiles and environment variables still work as intended.

Donating

If you like my work, and want to help support the project, feel free to donate! This is not necessary by any means, so please don't feel obliged to do so.

XMR: 84zSGS18aHtT3CZjZUnnWpCsz1wmA5f65G6BXisbrvAiH7PxZpP8GorbdjAQYRtfeiANZywwUPjZcHu8eXJeWdafJQFK46G
BTC: bc1q8x0r7khrfj40qd0zr5xv3t9nl92rz2387pu48u
ETH: 0x9630f95F11dFa8703b71DbF746E5c83A31A3F2DD

Privacy

Dexios will never collect your data. No data is sent to any server, nor are any requests.

Everything is done locally on your hardware and your machine.

I encourage all able users to take a peek at the source code and see for themselves.

Thank you!

I'd like to give a huge "thank you" to the entire RustCrypto Team for their hard work within the Rust cryptography community. This program would not be possible without their countless hours of work and dedication.

Installing

To install Dexios, there are two main options.

Firstly, you can install via cargo with cargo install dexios, or you may download a binary from the release page (make sure you mark it as executable, otherwise it won't run!).

We offer binaries for both Windows and FreeBSD, but our tests run on Ubuntu so Windows-specific issues may not be picked up. Please open a Github issue if you encounter any.

Linux/FreeBSD

To use cargo for installing, ensure you have gcc installed on your system.

You may install via cargo with the command cargo install dexios

Windows

Please use Windows Terminal or another Terminal program, as cmd does not have support for the command-line icons used by Dexios.

You may also use cargo for installing, just run the command:

cargo install dexios

Building Notes

gcc is required for building on Linux and FreeBSD.

Manually setting RUSTFLAGS is no longer required, as the AEAD crates we use from the RustCrypto Team automatically detect, and take advantage of hardware cryptography primitives.

Downloading and running a pre-compiled binary

The Github Releases page contains pre-compiled binaries, generated with Github Actions. These are ideal if you don't want to go through the hassle of building, and they should run on any system (provided the architecture matches). They are the exact same binaries from Github Actions, so we can ensure they haven't been tampered with.

We encourage users to check the hash provided in the GA Workflow, and compare with the file you have downloaded.

Choosing a Key

Use a strong password, if you're planning to use an entered one.

dd can provide a suitable keyfile if you'd prefer a higher level of security:

dd if=/dev/urandom of=keyfile bs=1 count=4096 for a 4kb random keyfile

If you lose your file's key/keyfile, there is no recovering the encrypted data. This is not unique to Dexios - it's the nature of encryption as a whole.

Key Inputs

The priority is as follows:

  1. First, Dexios will check for whether or not you have specified a keyfile (via -k or --keyfile)
  2. If no keyfile is detected, it will look for the DEXIOS_KEY environment variable
  3. If neither of the above are found, you will be shown a prompt to enter a password manually

The priority above is thrown out of the window if -p is specified, as this will prompt you for a password (even if the environment variable is set).

Checksums

Hashing mode uses BLAKE3 for verification, due to it's speed, security and regular updates. (very ideal for this use case).

Checksums are not used by Dexios for anything security related, they are for your peace of mind.

We hash the encrypted file (after encryption and before/during decryption). This is to ensure that your file wasn't tampered with between encrypting it and decrypting it. If the hash isn't the same then something very bad has happened.

This was originally sha3-512 in versions 3.x.x and below, and was KangarooTwelve in 4.x.x (via the tiny_keccak crate) but since v5 it has been changed to BLAKE3 for a number of reasons. We have no plans to change BLAKE3 at this moment in time - it's fast, secure, and does the job very well.

Standalone Hashing Mode

You can use this by running dexios hash test.enc. It can also be ran on any file you'd like - encrypted or not.

You may even hash multiple files at once, with a command such as dexios hash test1.enc test2.enc

Performance

Tests were ran on a system with a Ryzen 7 3700x and 16gb of 3000MHz RAM - running Void Linux. The file used was originally 3.5GiB, and it was stored on a Cruicial MX500 SSD.

Version 6 removed JSON entirely, and dropped base64, which really shows in the performance metrics.

This is using AES-256-GCM.

The time was determined via /usr/bin/time -f "%e"

Version-eHyk-dHykStream?
3.2.844.37s40.91sNo
4.0.023.70s30.43sNo
5.0.022.48s28.66sNo
5.0.220.14s21.26sNo
5.0.919.31s18.92sNo
6.0.011.74s11.59sNo
6.4.85.61s5.35sYes

Environment Variables

Dexios can read your key from an environment variable! Just set DEXIOS_KEY and it will automatically be detected and used. Due to using different salts and nonces for every encryption, there is no inherent risk in reusing keys - although it's not a good security practice.

To just encrypt a file
dexios -e test.txt test.enc

To just decrypt a file
dexios -d test.enc test.txt

To just erase a file
dexios erase test.txt

To list all possible AEADs
dexios list aead

To encrypt a file using AES-256-GCM (note: you do not need to specify -a when decrypting)
dexios -ea2 test.txt test.enc

To encrypt a file, and show the hash of the encrypted (output) file for verification later on
dexios -eH test.txt test.enc

To decrypt a file, and show the hash of the encrypted file (to compare with the hash generated above)
dexios -dH test.enc test.txt

To encrypt a file, and erase the original file
dexios -e --erase test.txt test.enc

To use a keyfile for encryption
dexios -ek keyfile test.txt test.enc

To encrypt all .mp4 files in a directory (using find)
Remove `-maxdepth 1` to make this run recursively

find *.mp4 -type f -maxdepth 1 -exec dexios -eyk keyfile {} {}.enc \;

To decrypt all .mp4.enc files in a directory, and remove the .enc suffix
find . -type f -iname "*.mp4.enc" -exec sh -c 'dexios -dk keyfile "$0" "${0%.enc}"' {} \;

Technical Details

Here you will find all relevant technical details for Dexios.

This page will be updated whenever changes are made and released - if anything looks out of date, please open a Github Issue.

Encryption

Encryption is done by default with XChaCha20-Poly1305, but Dexios also has options for AES-256-GCM and Deoxys-II-256. Encryption uses the key derived from argon2id.

Stream Mode

The XChaCha20-Poly1305 nonce is 20 bytes long, and the AES-256-GCM nonce is 8 bytes long when stored. The stream encryptor dynamically changes the nonce, based on a 31-bit little endian counter and a 1-bit "last block" flag. Those last 32 bits (4 bytes) are appended to the end of the nonce - making the total nonce length 24/12 bytes respectively. This is done by the stream encryptor to ensure that identical data will not be encrypted with the same nonce.

On encryption, the header is serialised and written to the start of the file. Then, Dexios reads the source file in 1MB blocks (1048576 bytes), and encrypts them using an LE31 stream encryptor. Each "block" is read, encrypted, written, and then hashed with BLAKE3 (if the option is selected). The salt and the nonce are always the same size, so we can avoid having to serialise/deserialise the data. This had a lot of performance benefits.

For decryption, the header is is read and deserialised. Next, Dexios reads the remaining part of the encrypted file in 1MB blocks + 16 bytes, to account for the AEAD tag. Each "block" is read, decrypted, written, and then hashed with BLAKE3 (if the option is selected).

Stream mode - Edge Cases

Edge cases regardigng very specific block sizes have been checked and confirmed to be working.

The major concern was when a file contains bytes exactly divisible by 1048576, but this is not an issue.

To test this, I created a file with exactly 3145728 bytes (3x streaming blocks). I then instructed Dexios to show me the read_count when encrypting, and it read the file 4 times - 3 times for each of the 3 blocks, and one containing 0 bytes. The 0 bytes were encrypted using the encrypt_last function, adding 16 bytes to the end of the file.

The total encrypted file size was 3145856 bytes - let's break it down.

  • 64 bytes are for the Dexios header
  • (16*3) are for the AEAD tag, 16 bytes per block

This brought our file size down to 3145744 bytes, which is a difference of 16 bytes compared to our source file. This confirms that the source data is encrypted completely, even in rather specific edge cases.

Using dexios hash, we can confirm that the file hashes are exactly the same before encryption, and after decryption.

Memory Mode

In memory mode, the nonce is static and 24/12 bytes (XChaCha20-Poly1305/AES-256-GCM) long, as the data is encrypted all in one pass.

On encryption, the data is read from the source file and placed into a Vec<u8> before being encrypted. The header is written, and then the encrypted data is written to the file - in that order. If hashing mode is selected, the header and encrypted data will be hashed and the BLAKE3 hash will be printed.

On decryption, the 16 byte salt (used for hashing) and the 24/12 byte nonce are read - in that order. Dexios reads the remainder of the file into a Vec<u8>, before decrypting it and writing the plaintext bytes to the output file. If hashing mode is selected, the salt, nonce and data will be hashed with BLAKE3 before it is decrypted and written - giving the user a prompt to continue with decryption (provided the hash matches).

Obtaining the Key

All keys are wrapped in a Secret<>. This ensures that data does not leak, is not copied and is zeroed on drop. It only stays in memory for as long as it needs to.

The key is checked, once, to ensure that it is not empty.

Once the length has been validated, keys are then safely transported to the argon2id function. See Password Hashing for more information.

Reading from the Terminal

While reading from the terminal, the passwords are stored as Strings. We use termion to handle password entry, this way your input is hidden.

On encryption, where you need to enter the password twice, they are compared. The String used for validation is safely zeroed out, and the original is consumed into a Vec<u8>.

On decryption, the password is only entered once, and it is consumed into a Vec<u8>.

The Vec<u8> containing the key is wrapped into a Secret<Vec>, which takes ownership of the value.

Reading from a Keyfile

The keyfile is opened, and it's contents are read into a Vec<u8>. The Vec<u8> is then wrapped into a Secret<Vec>, which takes ownership of the value.

Reading from Environment Variables

The DEXIOS_KEY environment variable is checked (to see if it exists) - if so, the contents are read into a String. It is then consumed into a Vec<u8>, which finally gets wrapped into a Secret<Vec>. This may seem cumbersome, but there are no copies or clones of the data, meaning it is safe.

Password Hashing

Password hashing is done with argon2id. It uses a 16-byte salt to derive the encryption key. Once a key has been hashed, it is dropped and zeroed out via the zeroize crate to ensure it stays in memory no longer than required.

Handling the Hash

The hash is wrapped in a Secret<>, which means it can only be exposed when called for in the program. It is only ever exposed to the cryptographic ciphers, and it is dropped right after. We use a custom Secret<> wrapper which implements zeroize-on-drop, to ensure that all sensitive information is removed from memory once it is no longer required.

Headers

Structure

The full header is 64 bytes - it contains relevant information about the encrypted data.

V1 Headers (v8.0.0 - v8.2.0):

  • The first two bytes of each header should contain 0xDE - this signifies that it is indeed a Dexios file.
  • The next two bytes signify the header version - as of v8.0.0, this is 0x01
  • The next two bytes contain the encryption algorithm, such as XChaCha20-Poly1305
  • The next two bytes contain the encryption mode, which is either stream or memory
  • The next 16 bytes contain the salt used for argon2id
  • We then have 16 bytes of empty space, this has the potential to store more information in later versions
  • We then have the n byte nonce - this is determined by the encryption algorithm and encryption mode
  • We finally have enough zeroes to reach 64 bytes - this is extra empty space that may be used in later revisions

V2 Headers (v8.3.0):

  • The first two bytes of each header should contain 0xDE - this signifies that it is indeed a Dexios file.
  • The next two bytes signify the header version - as of v8.3.0, this is 0x02
  • The next two bytes contain the encryption algorithm, such as XChaCha20-Poly1305
  • The next two bytes contain the encryption mode, which is either stream or memory
  • The next 16 bytes contain the salt used for argon2id
  • We then have the n byte nonce - this is determined by the encryption algorithm and encryption mode
  • We then pad the headers with zeroes until we reach a total of 48 bytes
  • The last 16 bytes contain a truncated SHA3-512 HMAC signature

V3 Headers (v8.4.0+):

  • The first two bytes of each header should contain 0xDE - this signifies that it is indeed a Dexios file.
  • The next two bytes signify the header version - as of v8.4.0, this is 0x03
  • The next two bytes contain the encryption algorithm, such as XChaCha20-Poly1305
  • The next two bytes contain the encryption mode, which is either stream or memory
  • The next 16 bytes contain the salt used for argon2id
  • We then have 16 bytes of empty space, this has the potential to store more information in later versions
  • We then have the n byte nonce - this is determined by the encryption algorithm and encryption mode
  • We finally have enough zeroes to reach 64 bytes - this is extra empty space that may be used in later revisions

Authenticating the Header with AAD (v8.4.0+)

Headers are calculated as a byte array (including the padding), and provided as AAD (additional authenticated data) to every block in stream mode, and the whole block in memory mode.

On decryption, the whole header is read (including the padding), and it is provided as AAD to every block in stream mode, and the whole block in memory mode. The data will not decrypt if the header does not match, even if an empty byte is altered. This fully protects your file from tampering, as even a single change will result in the user being made aware.

We have decided to use AAD as opposed to SHA3-512 HMAC due to few things:

  • HMAC cluttered up the codebase quite significantly. This is not good for a tool such as Dexios, as it'd become harder and harder to maintain
  • HMAC required two additional dependencies, hmac and sha3. This could potentially increase the attack surface (although unlikely)
  • HMAC required additional computational power - this is something a lot of older systems do not have

On the other hand, AAD requires hardly any further computational power, or cluttering of the codebase - it's already implemented by the AEAD crates that we use.

Stripping

Headers are stripped by zeroing out the first 64 bytes of the file - this is where the header is stored.

Dumping

Headers are dumped by reading the first 64 bytes of the input file, and writing them to the output file.

Restoring

Headers are restored by reading the first 64 bytes of the provided input file, and writing them to the first 64 bytes of the output file.

Signing (v8.3.0 - yanked feature)

Headers are signed using your hashed encryption key.

On encryption, the headers are signed when the ciphers/stream ciphers are initialised, this way we only have to hash your key once. The key is never cloned within memory, and it is zeroed out (using zeroize and our secret wrapper) once the header has been signed.

The signature is generated from the first 48 bytes of the header - this is where all of the important information is enclosed.

Verifying (v8.3.0 - yanked feature)

On decryption, the headers are verified when the ciphers/stream ciphers are initialised. Once again, the key is zeroed out once the header has been verified.

If a single byte of the header changes, decryption will not continue as the signature will not match. This is to prevent a malicious actor from tampering with the header.

If the signature doesn't match, but you're positive your key is correct, it is best to assume the header was tampered with.

Secure Erase

Due to the nature of SSDs and their controllers (wear levelling and such), it's almost impossible to fully erase a file - we can try our best though. It's not recommended to use the erase function on a large file (1gb+) on flash storage, as it will wear the drive down unneccessarily.

First we generate enough random bytes to fill the file, seeded by the system. Those random bytes are then written over the file, covering all of the previous data. This is repeated twice (more if specified) to ensure the data is thoroughly gone. This may panic due to a lack of entropy - there's not much we can do in this case.

Once the random bytes have been overwritten, we then overwrite all of the data one final time with just zeroes, before truncating the file length to 0. Afterwards, we remove the file using the system's methods (unlink on Unix and DeleteFile on Windows).

Secret Wrapper

We have implemented our own Secret<> wrapper/type, that implements zeroize-on-drop. This ensures all sensitive information is securely zeroed-out once it is no longer required, and that the sensitive informatiton is only accessed when explicit calls are made. Our implementation was inspired by the secrecy crate's functionality, so I'd like to provide a huge thanks to the original creators.

Secrets do not implement any copying/cloning, and there are none done manually within the codebase. This is to prevent possible leaks of data.

Secrets are also redcated from debug logs and such, and they will show "[REDACTED]" whenever they are printed. They do not implement fmt::display.

This feature has been deprecated since Version 8.4.0

Deprecated Notice

We have decided to remove this functionality entirely from Dexios. We did this for a few reasons:

  • Pack modes cluttered up the codebase quite heavily
  • Pack modes gave Dexios a larger attack-surface, with vulnerabilities such as zip slipping

It didn't feel right to leave in packing mode, but remove unpacking, so both have been deprecated. As "packing" zipped the files, you may still access your data. Just run dexios decrypt file.enc file.zip and open it as you would with any other zip file.

Directory "Packing"

After using the pack feature to create an archive, it is just a zip file. You may decrypt this as normal and open it for yourself, if you so wish.

Folders encrypted with this mode have all of their information hidden. There is no way for anyone to deduce how large each file was, or their names/contents, once it has been encrypted.

This mode does not preserve metadata, file attributes or permissions (yet).

Creating an Archive

First, the files and directory structure are both indexed. While indexing, if any files/directories match the glob pattern, they will be ignored.

The zip file is then initialised with a random 8 character alphanumeric extension, this is to prevent possible endless-loops (the buffer kept getting filled with the data that was just written).

The directory structure is recreated within the zip file (if in recursive mode, this will include other directories and not just the source one), and all of the files are added.

Stream reading is implemented if the file is larger than 1MB - if not then the file is just read into memory.

Once the zip file has been created, it is encrypted. If the zip file is larger than 1mb, stream encryption will be used (unless specified manually).

We then erase the temporary zip file, using 8 passes of the secure erase feature.

Unpacking an Archive

First, the zip file is decrypted as a temporary file with a random 8 character alphanumeric extension. This will use memory mode if it's less than 1MB, or if specified - otherwise stream mode will be used.

We then open the temporary zip file for reading, recreate the directory structure within the output directory, and extract each file to the correct path within the output directory.

Once all of the files have been successfully extracted, we securely erase the temporary zip file. This again uses 8 passes of the secure erase feature.

Performance Notes

Please note that performance is heavily dependent on your hardware - mostly disk speed. Dexios has had a lot of performance optimisations applied throughout the versions, but we're getting to the point where the main factor is disk speed.

To see this for yourself, run one command without the -b switch, and the very same command with the -b switch.

Using Deoxys-II-256 will result in degraded performance, please view the Security Notices for relevant information.

We will continue to optimise Dexios where possible.

Reporting a Vulnerability

Please report any vulnerabilities as a Github issue - we believe all issues should be known, and they are likely to get resolved very quickly this way. Thank you.

As an alternative, you may contact brxken128@tutanota.com

If you find any vulnerabilities within Dexios, and can provide steps/pointers to reproduce, please report them. You may do this anonymously via the email above. I'm afraid I cannot offer any money in return, but I can add you to the list of contributors (at your request).