Kryptor

Technical Information

Updated on the 23rd of December 2020

This information is aimed at developers and anybody interested in the algorithms used by Kryptor. If you have no programming or cryptography experience, then you will probably find this page confusing.

File Encryption

Passwords

  1. The password is converted from a char array to a byte array using Encoding.UTF8.GetBytes().
  2. The password bytes are then hashed using BLAKE2b to get a 512-bit hash.
  3. If a keyfile has been selected alongside a password, then the password is combined with the keyfile as outlined below (see 'Keyfiles' section).
  4. The hashed password bytes (64 bytes) are then passed to Argon2.

Keyfiles

  1. The first 64 bytes (512-bits) of the selected keyfile are read from the file and stored in a byte array.
  2. If a keyfile has been selected alongside a password, then the password bytes and keyfile bytes are combined using BLAKE2b with the keyfile bytes (64 bytes) as the key.
  3. If no password is entered, then the keyfile bytes (64 bytes) are used as the password bytes and hashed using BLAKE2b.

Key Derivation

The following repeats for every file that is encrypted:

  1. A random 128-bit salt is generated per file using libsodium.
  2. Argon2id is then used on the password bytes with the parameters specified in the settings (memory size and iterations) to derive a unique 256-bit encryption key and 512-bit MAC key per file. No keys are ever reused.

For more information about Argon2, please read the Key Derivation and Hashing Algorithms documentation.

File Encryption

  1. A new file is created with the '.kryptor' extension using a FileStream. The Argon2 parameters (memory size and iterations) and salt are written to the beginning of the new file.
  2. Files are read in chunks using a FileStream.
  3. Each plaintext chunk is encrypted using the derived encryption key and a counter as the nonce. The encrypted chunk is then written to the new file.
  4. The counter is incremented after each chunk has been encrypted.
  5. Finally, the BLAKE2b MAC for the encrypted file is calculated using a FileStream and the 512-bit MAC key. The BLAKE2b hash is then appended to the encrypted file.
  6. If the MAC was appended successfully and the 'Overwrite Files' setting is enabled, then the encrypted file is copied to the location of the original, unencrypted file using File.Copy() to overwrite the original file with encrypted data.
  7. The encrypted file is then marked as read-only using File.SetAttributes() to prevent modification.

Kryptor defaults to a 4096 byte buffer. However, when the file is greater than or equal to 1 MiB in size, a buffer of 128 KiB is used. When a file less than 4096 bytes in size is selected (e.g. a small text file), Kryptor uses the file's size as the buffer size.

Encrypted File Structure

The structure of an encrypted file looks like this:

ARGON2 MEMORY SIZE | ARGON2 ITERATIONS | SALT (16 bytes) | ENCRYPTED FILE DATA | *ENCRYPTED ORIGINAL FILE NAME | MAC (64 bytes)

*The original file name is only stored in the file if 'Anonymous Rename' is enabled in the settings.

File Encryption Notes

Libsodium is used as the library for BLAKE2b, Argon2, and XChaCha20.

If 'Memory Encryption' is enabled in the settings, the password bytes, encryption key, and MAC key are encrypted in memory when they do not need to be accessed. However, there are periods of time during the encryption process when these arrays are not accessed but don't get encrypted for performance reasons.

Once an array containing sensitive data no longer needs to be used, Array.Clear() is called to zero out the elements of the array.

File Decryption

  1. The Argon2 parameters and salt are read from the encrypted file.
  2. Key derivation to get an encryption key and MAC key occurs using the parameters and salt read from the file.
  3. The BLAKE2b hash stored at the end of the encrypted file is read using a FileStream. These bytes are then removed from the file using FileStream.SetLength().
  4. The MAC key derived by Argon2 is used to compute the BLAKE2b hash of the encrypted file.
  5. The computed BLAKE2b hash is compared to the stored BLAKE2b hash using Sodium.Utilities.Compare(). This comparison is done in constant time to prevent timing attacks.
  6. If the hashes match, then decryption continues. Otherwise, the BLAKE2b hash gets reappended to the end of the encrypted file and an error message is displayed explaining that either the password/keyfile is wrong or the file has been tampered with.
  7. The encrypted file bytes are iteratively read into a buffer using a FileStream.
  8. Each buffer is decrypted using the derived encryption key and a counter for the nonce.
  9. Decrypted bytes are then written to a new file.
  10. If 'Anonymous Rename' is enabled, then the original file name is retrieved from the end of the decrypted file, and the decrypted file is renamed using File.Move().

Kryptor defaults to a 4096 byte buffer. However, when the file is greater than or equal to 1 MiB in size, a buffer of 128 KiB is used. When a file less than 4096 bytes in size is selected (e.g. a small '.txt' file), Kryptor uses the file's size as the buffer size.

Memory Encryption

When this setting is enabled, sensitive byte arrays are encrypted in memory using ProtectedMemory (which uses the Data Protection API available on Windows) with the MemoryProtectionScope set to 'SameProcess'. These byte arrays are encrypted in place.

Note: There are periods of time when these arrays don't get encrypted for performance reasons.

When running the CLI version of Kryptor or the GUI version on Linux or macOS using Mono, Kryptor uses libsodium Sealed Boxes (Curve25519, XSalsa20-Poly1305) with a random key pair generated using PublicKeyBox.GenerateKeyPair().

Note: This method of memory encryption is not as secure as ProtectedMemory because the encryption keys are stored by Kryptor.

Anonymous Rename

When enabled, random file/folder names are generated using Path.GetRandomFileName(), which is called twice to create longer random file names. The random extensions are removed using String.Replace(".", string.Empty).

Note: Path.GetRandomFileName() uses RNGCryptoServiceProvider internally.

If a folder is selected for file encryption, all of the subdirectories are anonymously renamed first, then the parent directory is anonymously renamed. For each renamed folder, the original folder name is stored in a '.txt' file within the folder. This file then gets encrypted like other files in the folder.

For each selected file, the original file name is appended to the end of the file after a new line (Environment.NewLine). Then a random name is generated (as outlined above) and used as the name for the encrypted file that's created.

Tools

Keyfile Generation

Kryptor uses libsodium to randomly generate 64 bytes (512-bits) that are written to a file using File.WriteAllBytes().

Keyfiles are given a random name in the SaveFileDialog using Path.GetRandomFileName() and use the '.key' extension. Keyfiles are marked as read-only using File.SetAttributes() to prevent modification.

Password Generator

For passwords, a random byte is generated using libsodium and converted to a char. If the char is within any of the selected character sets (lowercase/uppercase/numbers/symbols), then it is added to the password. This process repeats until the specified password length is reached.

For passphrase generation, the EFF's long wordlist (for use with five dice) is used as the word source. However, custom wordlists can be used by either appending to the existing wordlist or replacing the 'wordlist.txt' file in the Kryptor folder.

libsodium is used to generate random line numbers. These lines are then read from the file, and the words are combined to form a passphrase. Each word is separated by a '-' character.

Passphrases require the lowercase and symbols character sets to be selected. If the user selects uppercase, the words are converted to title case using TextInfo.ToTitleCase(). If the user selects numbers, then the length of each word is appended after each word.

Password Sharing

Kryptor uses libsodium Sealed Boxes (Curve25519, XSalsa20-Poly1305) for password sharing. Recipient key pairs are randomly generated using PublicKeyBox.GenerateKeyPair(), which generates two 256-bit keys. The recipient can verify the integrity of the message but can't verify the identity of the sender.