Kryptor

Technical Information

Updated on the 1st of October 2020

The code snippets are a work in progress. They will be sorted soon.

This information is aimed at developers and anybody interested in the algorithms and key sizes being 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 to a byte array using Encoding.UTF8.GetBytes().
  2. 64 bytes (512-bits) of associated data is calculated by hashing a string which specificies the name of the selected encryption algorithm (e.g. "XChaCha20") using BLAKE2b.
  3. The password bytes are then hashed using BLAKE2b with the associated data (64 bytes) as the key to get a 512-bit hash.
  4. If a keyfile has been selected alongside a password, then the password is combined with the keyfile as outlined below (see 'Keyfiles' section).
  5. The hashed password bytes (64 bytes) are then passed to Argon2.
      public static byte[] GetPasswordBytes(char[] password)
      {
          byte[] passwordBytes = null;
          if (password != null && password.Length > 0)
          {
              passwordBytes = Encoding.UTF8.GetBytes(password);
              passwordBytes = HashPasswordBytes(passwordBytes);
          }
          return passwordBytes;
      }

      private static byte[] HashPasswordBytes(byte[] passwordBytes)
      {
          byte[] associatedData = Generate.AssociatedData();
          // Combine associated data and password bytes
          passwordBytes = HashingAlgorithms.Blake2(passwordBytes, associatedData);
          MemoryEncryption.EncryptByteArray(ref passwordBytes);
          return passwordBytes;
      }

      public static byte[] AssociatedData()
      {
          Enum cipher = (Cipher)Globals.EncryptionAlgorithm;
          string cipherName = Enum.GetName(cipher.GetType(), cipher);
          return HashingAlgorithms.Blake2(cipherName);
      }

      public static byte[] Blake2(string message)
      {
          return GenericHash.Hash(message, (byte[])null, Constants.HashLength);
      }

      public static byte[] Blake2(byte[] message, byte[] key)
      {
          return GenericHash.Hash(message, key, Constants.HashLength);
      }

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 with the associated data as the key.
      public static void StartEncryption(bool encryption, byte[] passwordBytes, BackgroundWorker backgroundWorker)
      {
          // Don't use keyfile bytes when only a keyfile is selected
          if (passwordBytes != null)
          {
              passwordBytes = GetKeyfileBytes(passwordBytes);
          }
          else
          {
              passwordBytes = KeyfileAsPassword();
          }
          GetFilePaths(encryption, passwordBytes, backgroundWorker);
          Utilities.ZeroArray(passwordBytes);
      }

      private static byte[] GetKeyfileBytes(byte[] passwordBytes)
      {
          if (!string.IsNullOrEmpty(Globals.KeyfilePath))
          {
              byte[] keyfileBytes = Keyfiles.ReadKeyfile(Globals.KeyfilePath);
              if (keyfileBytes != null)
              {
                  MemoryEncryption.DecryptByteArray(ref passwordBytes);
                  // Combine password and keyfile bytes
                  passwordBytes = HashingAlgorithms.Blake2(passwordBytes, keyfileBytes);
                  MemoryEncryption.EncryptByteArray(ref passwordBytes);
                  Utilities.ZeroArray(keyfileBytes);
              }
          }
          return passwordBytes;
      }

      private static byte[] KeyfileAsPassword()
      {
          // If only a keyfile was selected, use the keyfile bytes as the password
          byte[] passwordBytes = Keyfiles.ReadKeyfile(Globals.KeyfilePath);
          return HashPasswordBytes(passwordBytes);
      }

Key Derivation

The following repeats for every file that is encrypted:

  1. A random 16 byte (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, iterations) to derive a unique encryption key (32 bytes/256-bits) and MAC key (64 bytes/512-bits) per file. No keys are ever reused.
      public static byte[] Salt()
      {
          return SodiumCore.GetRandomBytes(Constants.SaltLength);
      }

      public static (byte[], byte[]) DeriveKeys(byte[] passwordBytes, byte[] salt, int iterations, int memorySize)
      {
          var argon2id = PasswordHash.ArgonAlgorithm.Argon_2ID13;
          MemoryEncryption.DecryptByteArray(ref passwordBytes);
          // Derive a 96 byte key
          byte[] derivedKey = PasswordHash.ArgonHashBinary(passwordBytes, salt, iterations, memorySize, Constants.Argon2KeySize, argon2id);
          // 256-bit encryption key
          byte[] encryptionKey = new byte[Constants.EncryptionKeySize];
          Array.Copy(derivedKey, encryptionKey, encryptionKey.Length);
          // 512-bit MAC key
          byte[] macKey = new byte[Constants.MACKeySize];
          Array.Copy(derivedKey, encryptionKey.Length, macKey, 0, macKey.Length);
          Utilities.ZeroArray(derivedKey);
          MemoryEncryption.EncryptByteArray(ref passwordBytes);
          MemoryEncryption.EncryptByteArray(ref encryptionKey);
          MemoryEncryption.EncryptByteArray(ref macKey);
          return (encryptionKey, macKey);
      }

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

File Encryption

  1. A random nonce is generated for each file using libsodium. XChaCha20 and XSalsa20 both use a nonce of 192-bits (24 bytes), whereas AES-CBC uses a 128-bit (16 byte) nonce.
  2. A new file is created with the '.kryptor' extension using a FileStream. The Argon2 parameters (memory size and iterations), salt, and nonce are written to the beginning of the new file.
  3. Files are read using a FileStream into a byte array buffer that is encrypted and written to the new file.
  4. Finally, the BLAKE2b MAC for the encrypted file is calculated using a FileStream and the 64 byte (512-bit) MAC key. The BLAKE2b hash is then appended to the encrypted file.
  5. 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.
  6. The encrypted file is then marked as read-only using File.SetAttributes() to prevent modification.
      public static byte[] Nonce()
      {
          if (Globals.EncryptionAlgorithm == (int)Cipher.XChaCha20 || Globals.EncryptionAlgorithm == (int)Cipher.XSalsa20)
          {
              return SodiumCore.GetRandomBytes(Constants.XChaChaNonceLength);
          }
          else
          {
              return SodiumCore.GetRandomBytes(Constants.AesNonceLength);
          }
      }

      public static void InitializeEncryption(string filePath, byte[] passwordBytes, BackgroundWorker bgwEncryption)
      {
          string encryptedFilePath = GetEncryptedFilePath(filePath);
          byte[] salt = Generate.Salt();
          byte[] nonce = Generate.Nonce();
          var keys = KeyDerivation.DeriveKeys(passwordBytes, salt, Globals.Iterations, Globals.MemorySize);
          EncryptFile(filePath, encryptedFilePath, salt, nonce, keys, bgwEncryption);
      }

      private static void EncryptFile(string filePath, string encryptedFilePath, byte[] salt, byte[] nonce, (byte[], byte[]) keys, BackgroundWorker bgwEncryption)
      {
          try
          {
              using (var ciphertext = new FileStream(encryptedFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))
              using (var plaintext = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
              {
                  WriteFileHeaders.WriteHeaders(ciphertext, salt, nonce);
                  // Store headers length to correct percentage calculation
                  long headersLength = ciphertext.Position;
                  byte[] fileBytes = FileHandling.GetBufferSize(plaintext);
                  MemoryEncryption.DecryptByteArray(ref keys.Item1);
                  if (Globals.EncryptionAlgorithm == (int)Cipher.XChaCha20 || Globals.EncryptionAlgorithm == (int)Cipher.XSalsa20)
                  {
                      StreamCiphers.Encrypt(plaintext, ciphertext, headersLength, fileBytes, nonce, keys.Item1, bgwEncryption);
                  }
                  else if (Globals.EncryptionAlgorithm == (int)Cipher.AesCBC)
                  {
                      AesAlgorithms.EncryptAesCBC(plaintext, ciphertext, headersLength, fileBytes, nonce, keys.Item1, bgwEncryption);
                  }
              }
              Utilities.ZeroArray(keys.Item1);
              CompleteEncryption(filePath, encryptedFilePath, keys.Item2);
          }
          catch (Exception ex) when (ExceptionFilters.FileEncryptionExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.High);
              DisplayMessage.ErrorResultsText(filePath, ex.GetType().Name, "Unable to encrypt the file.");
              FileHandling.DeleteFile(encryptedFilePath);
              Utilities.ZeroArray(keys.Item1);
              Utilities.ZeroArray(keys.Item2);
          }
      }

      private static void CompleteEncryption(string filePath, string encryptedFilePath, byte[] macKey)
      {
          // Calculate and append MAC
          bool fileSigned = FileAuthentication.SignFile(encryptedFilePath, macKey);
          Utilities.ZeroArray(macKey);
          if (fileSigned == true && Globals.OverwriteFiles == true)
          {
              FileHandling.OverwriteFile(filePath, encryptedFilePath);
          }
          FileHandling.MakeFileReadOnly(encryptedFilePath);
          GetEncryptionResult(filePath, fileSigned);
      }

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.

      public static byte[] GetBufferSize(FileStream fileStream)
      {
          NullChecks.FileStreams(fileStream);
          int bufferSize = 4096;
          // Use a larger buffer for bigger files
          if (fileStream.Length >= Constants.Mebibyte)
          {
              // 128 KiB
              bufferSize = 131072;
          }
          else if (bufferSize > fileStream.Length)
          {
              // Use file size as buffer for small files
              return new byte[fileStream.Length];
          }
          return new byte[bufferSize];
      }

Encrypted File Structure

The structure of an encrypted file looks like this:

ARGON2 PARAMETERS | SALT (32 bytes) | NONCE (24 or 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, XChaCha20, and XSalsa20, whereas AES-CBC uses the System.Security.Cryptography namespace in .NET.

      public static void Encrypt(FileStream plaintext, FileStream ciphertext, long headersLength, byte[] fileBytes, byte[] nonce, byte[] key, BackgroundWorker bgwEncryption)
      {
          NullChecks.FileEncryption(plaintext, ciphertext, fileBytes, nonce, key);
          int bytesRead;
          while ((bytesRead = plaintext.Read(fileBytes, 0, fileBytes.Length)) > 0)
          {
              byte[] encryptedBytes = EncryptFileBytes(fileBytes, nonce, key);
              ciphertext.Write(encryptedBytes, 0, bytesRead);
              // Report progress if encrypting a single file
              ReportProgress.ReportEncryptionProgress(ciphertext.Position, plaintext.Length + headersLength, bgwEncryption);
          }
      }

      private static byte[] EncryptFileBytes(byte[] fileBytes, byte[] nonce, byte[] key)
      {
          byte[] encryptedBytes = new byte[fileBytes.Length];
          if (Globals.EncryptionAlgorithm == (int)Cipher.XChaCha20)
          {
              encryptedBytes = StreamEncryption.EncryptXChaCha20(fileBytes, nonce, key);
          }
          else if (Globals.EncryptionAlgorithm == (int)Cipher.XSalsa20)
          {
              encryptedBytes = StreamEncryption.Encrypt(fileBytes, nonce, key);
          }
          return encryptedBytes;
      }

      private const CipherMode _cbcMode = CipherMode.CBC;
      private const PaddingMode _pkcs7Padding = PaddingMode.PKCS7;

      public static void EncryptAesCBC(FileStream plaintext, FileStream ciphertext, long headersLength, byte[] fileBytes, byte[] nonce, byte[] key, BackgroundWorker bgwEncryption)
      {
          NullChecks.FileEncryption(plaintext, ciphertext, fileBytes, nonce, key);
          using (var aes = new AesCryptoServiceProvider() { Mode = _cbcMode, Padding = _pkcs7Padding })
          {
              using (var cryptoStream = new CryptoStream(ciphertext, aes.CreateEncryptor(key, nonce), CryptoStreamMode.Write))
              {
                  int bytesRead;
                  while ((bytesRead = plaintext.Read(fileBytes, 0, fileBytes.Length)) > 0)
                  {
                      cryptoStream.Write(fileBytes, 0, bytesRead);
                      // Report progress if encrypting a single file
                      ReportProgress.ReportEncryptionProgress(ciphertext.Position, plaintext.Length + headersLength, bgwEncryption);
                  }
              }
          }
      }

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.

      public static void ZeroArray(byte[] byteArray)
      {
          if (byteArray != null)
          {
              Array.Clear(byteArray, 0, byteArray.Length);
          }
      }

File Decryption

  1. The Argon2 parameters, salt, and nonce 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/selected encryption algorithm is wrong or that the file has been tampered with.
  7. The encrypted file bytes are read into a buffer using a FileStream, and the bytes are decrypted using the derived encryption key and the nonce read from the encrypted file.
  8. Decrypted bytes are then written to a new, unencrypted file.
  9. 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.

      public static bool AuthenticateFile(string filePath, byte[] macKey, out byte[] storedHash)
      {
          try
          {
              bool tampered = true;
              storedHash = ReadStoredHash(filePath);
              if (storedHash != null)
              {
                  byte[] computedHash = new byte[Constants.HashLength];
                  using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
                  {
                      // Remove the stored MAC from the file before computing the MAC
                      fileStream.SetLength(fileStream.Length - computedHash.Length);
                      MemoryEncryption.DecryptByteArray(ref macKey);
                      computedHash = HashingAlgorithms.Blake2(fileStream, macKey);
                      MemoryEncryption.EncryptByteArray(ref macKey);
                  }
                  // Invert result
                  tampered = !Sodium.Utilities.Compare(storedHash, computedHash);
                  if (tampered == true)
                  {
                      // Restore the stored MAC
                      AppendHash(filePath, storedHash);
                  }
              }
              return tampered;
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.High);
              DisplayMessage.ErrorResultsText(filePath, ex.GetType().Name, "Unable to authenticate the file.");
              storedHash = null;
              return true;
          }
      }

      private static byte[] ReadStoredHash(string filePath)
      {
          try
          {
              byte[] storedHash = new byte[Constants.HashLength];
              using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
              {
                  // Read the last 64 bytes of the file
                  fileStream.Seek(fileStream.Length - storedHash.Length, SeekOrigin.Begin);
                  fileStream.Read(storedHash, 0, storedHash.Length);
              }
              return storedHash;
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.High);
              DisplayMessage.ErrorResultsText(filePath, ex.GetType().Name, "Unable to read the MAC stored in the file.");
              return null;
          }
      }

Memory Encryption (Windows)

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.

      public static void EncryptByteArray(ref byte[] byteArray)
      {
          try
          {
              if (Globals.MemoryEncryption == true && byteArray != null)
              {
                  if (Constants.RunningOnMono == false)
                  {
                      // Windows
                      ProtectedMemory.Protect(byteArray, MemoryProtectionScope.SameProcess);
                  }
                  else if (Constants.RunningOnMono == true)
                  {
                      // Linux & macOS
                      byteArray = SealedPublicKeyBox.Create(byteArray, _keyPair.PublicKey);
                  }
              }
          }
          catch (Exception ex) when (ExceptionFilters.MemoryEncryptionExceptions(ex))
          {
              Globals.MemoryEncryption = false;
              Settings.SaveSettings();
              Logging.LogException(ex.ToString(), Logging.Severity.Bug);
              DisplayMessage.ErrorMessageBox(ex.GetType().Name, "Memory encryption has been disabled due to an exception. This is a bug - please report it.");
          }
      }

Memory Encryption (Linux & macOS)

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

NOTE: Memory encryption on Linux and macOS is not as secure as on Windows because the encryption keys are stored by Kryptor.

      // Memory encryption for Mono
      private static readonly KeyPair _keyPair = PublicKeyBox.GenerateKeyPair();

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.

      public static string GetAnonymousFileName(string filePath)
      {
          try
          {
              NullChecks.Strings(filePath);
              string originalFileName = Path.GetFileName(filePath);
              string randomFileName = GenerateRandomFileName();
              string anonymousFilePath = filePath.Replace(originalFileName, randomFileName);
              return anonymousFilePath;
          }
          catch (ArgumentException ex)
          {
              Logging.LogException(ex.ToString(), Logging.Severity.Bug);
              DisplayMessage.ErrorResultsText(filePath, ex.GetType().Name, "Unable to get anonymous file name. This is a bug - please report it.");
              return filePath;
          }
      }

      public static string GenerateRandomFileName()
      {
          // Remove the generated extension
          string randomFileName = Path.GetRandomFileName().Replace(".", string.Empty);
          randomFileName += Path.GetRandomFileName().Replace(".", string.Empty);
          return randomFileName;
      }

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.

      public static string AnonymiseDirectories(bool encryption, string folderPath)
      {
          if (encryption == true && Globals.AnonymousRename == true)
          {
              string[] subdirectories = GetAllDirectories(folderPath);
              if (subdirectories != null)
              {
                  // Anonymise subdirectories - bottom most first
                  for (int i = subdirectories.Length - 1; i >= 0; i--)
                  {
                      AnonymiseDirectoryName(subdirectories[i]);
                  }
              }
              // Anonymise selected directory
              folderPath = AnonymiseDirectoryName(folderPath);
          }
          return folderPath;
      }

      private static string[] GetAllDirectories(string folderPath)
      {
          try
          {
              return Directory.GetDirectories(folderPath, "*", SearchOption.AllDirectories);
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.High);
              DisplayMessage.ErrorResultsText(folderPath, ex.GetType().Name, "Unable to get subdirectories in selected folder.");
              return null;
          }
      }

      private static string AnonymiseDirectoryName(string folderPath)
      {
          try
          {
              string originalDirectoryName = Path.GetFileName(folderPath);
              string anonymisedPath = GetAnonymousFileName(folderPath);
              Directory.Move(folderPath, anonymisedPath);
              // Store the original directory name in a text file inside the directory
              string storageFilePath = Path.Combine(anonymisedPath, $"{Path.GetFileName(anonymisedPath)}.txt");
              File.WriteAllText(storageFilePath, originalDirectoryName);
              return anonymisedPath;
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.High);
              DisplayMessage.ErrorResultsText(folderPath, ex.GetType().Name, "Unable to anonymise directory name.");
              return folderPath;
          }
      }

      public static void DeanonymiseDirectories(bool encryption, string folderPath)
      {
          if (encryption == false && Globals.AnonymousRename == true)
          {
              string[] subdirectories = GetAllDirectories(folderPath);
              if (subdirectories != null)
              {
                  for (int i = subdirectories.Length - 1; i >= 0; i--)
                  {
                      OriginalFileName.RestoreDirectoryName(subdirectories[i]);
                  }
              }
              OriginalFileName.RestoreDirectoryName(folderPath);
          }
      }

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.

      public static bool AppendOriginalFileName(string filePath)
      {
          try
          {
              string fileName = Path.GetFileName(filePath);
              EncodeFileName(filePath, fileName, out byte[] newLineBytes, out byte[] fileNameBytes);
              using (var fileStream = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read))
              {
                  fileStream.Write(newLineBytes, 0, newLineBytes.Length);
                  fileStream.Write(fileNameBytes, 0, fileNameBytes.Length);
              }
              return true;
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex) || ex is EncoderFallbackException)
          {
              Logging.LogException(ex.ToString(), Logging.Severity.High);
              DisplayMessage.ErrorResultsText(filePath, ex.GetType().Name, "Could not store original file name.");
              return false;
          }
      }

      private static void EncodeFileName(string filePath, string fileName, out byte[] newLineBytes, out byte[] fileNameBytes)
      {
          using (var streamReader = new StreamReader(filePath, Encoding.UTF8, true))
          {
              streamReader.Peek();
              var fileEncoding = streamReader.CurrentEncoding;
              newLineBytes = fileEncoding.GetBytes(Environment.NewLine);
              fileNameBytes = fileEncoding.GetBytes(fileName);
          }
      }

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.

      private static bool GenerateKeyfile(string filePath)
      {
          try
          {
              byte[] keyfileBytes = SodiumCore.GetRandomBytes(Constants.MACKeySize);
              File.WriteAllBytes(filePath, keyfileBytes);
              File.SetAttributes(filePath, FileAttributes.ReadOnly);
              Utilities.ZeroArray(keyfileBytes);
              return true;
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.Medium);
              DisplayMessage.ErrorMessageBox(ex.GetType().Name, "Unable to generate keyfile.");
              return false;
          }
      }

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.

      public static char[] GenerateRandomPassword(int length, bool lowercase, bool uppercase, bool numbers, bool symbols)
      {
          const string lowercaseCharacters = "abcdefghijklmnopqrstuvwxyz";
          const string uppercaseCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
          const string numberCharacters = "1234567890";
          const string symbolCharacters = "!#$%&*?@+-=^";
          List password = new List();
          while (password.Count < length)
          {
              bool characterAdded = false;
              byte[] characterByte = SodiumCore.GetRandomBytes(1);
              char character = (char)characterByte[0];
              if (lowercase == true)
              {
                  CheckCharacter(lowercaseCharacters, character, ref password, ref characterAdded);
              }
              if (uppercase == true && characterAdded == false)
              {
                  CheckCharacter(uppercaseCharacters, character, ref password, ref characterAdded);
              }
              if (numbers == true && characterAdded == false)
              {
                  CheckCharacter(numberCharacters, character, ref password, ref characterAdded);
              }
              if (symbols == true && characterAdded == false)
              {
                  CheckCharacter(symbolCharacters, character, ref password, ref characterAdded);
              }
          }
          return password.ToArray();
      }

      private static void CheckCharacter(string characterSet, char character, ref List password, ref bool characterAdded)
      {
          if (characterSet.Contains(character))
          {
              password.Add(character);
              characterAdded = true;
          }
      }

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.

      public static char[] GenerateRandomPassphrase(int wordCount, bool uppercase, bool numbers)
      {
          try
          {
              string wordlistFilePath = Path.Combine(Constants.KryptorDirectory, "wordlist.txt");
              if (File.Exists(wordlistFilePath))
              {
                  List passphrase = new List();
                  int wordlistLength = File.ReadLines(wordlistFilePath).Count();
                  int[] lineNumbers = GenerateLineNumbers(wordlistLength, wordCount);
                  string[] words = GetRandomWords(wordlistFilePath, lineNumbers, wordCount, uppercase, numbers);
                  Array.Clear(lineNumbers, 0, lineNumbers.Length);
                  if (words != null)
                  {
                      FormatPassphrase(words, ref passphrase, wordCount);
                      Array.Clear(words, 0, words.Length);
                  }
                  return passphrase.ToArray();
              }
              else
              {
                  File.WriteAllText(wordlistFilePath, Properties.Resources.wordlist);
                  return GenerateRandomPassphrase(wordCount, uppercase, numbers);
              }
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.Medium);
              DisplayMessage.ErrorMessageBox(ex.GetType().Name, "Unable to generate a random passphrase.");
              return Array.Empty();
          }
      }

      private static int[] GenerateLineNumbers(int wordlistLength, int wordCount)
      {
          int[] lineNumbers = new int[wordCount];
          for (int i = 0; i < wordCount; i++)
          {
              byte[] randomBytes = SodiumCore.GetRandomBytes(4);
              uint max = BitConverter.ToUInt32(randomBytes, 0);
              lineNumbers[i] = (int)(wordlistLength * (max / (double)uint.MaxValue));
          }
          return lineNumbers;
      }

      private static string[] GetRandomWords(string wordListFilePath, int[] lineNumbers, int wordCount, bool upperCase, bool numbers)
      {
          try
          {
              string[] words = new string[wordCount];
              for (int i = 0; i < wordCount; i++)
              {
                  words[i] = File.ReadLines(wordListFilePath).Skip(lineNumbers[i]).Take(1).First();
                  // Remove any numbers/spaces on the line
                  words[i] = Regex.Replace(words[i], @"[\d-]", string.Empty).Trim();
                  if (upperCase == true)
                  {
                      words[i] = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(words[i].ToLower(CultureInfo.CurrentCulture));
                  }
                  if (numbers == true)
                  {
                      words[i] += words[i].Length;
                  }
              }
              return words;
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex) || ex is RegexMatchTimeoutException)
          {
              Logging.LogException(ex.ToString(), Logging.Severity.Medium);
              DisplayMessage.ErrorMessageBox(ex.GetType().Name, "Unable to retrieve words from the wordlist.");
              return null;
          }
      }

      private static void FormatPassphrase(string[] words, ref List passphrase, int wordCount)
      {
          for (int i = 0; i < wordCount; i++)
          {
              foreach (char character in words[i])
              {
                  passphrase.Add(character);
              }
              // Add word separator symbol
              if (i != wordCount - 1)
              {
                  passphrase.Add('-');
              }
          }
      }

Password Sharing

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

      public static (string, string) GenerateKeyPair()
      {
          using (var keyPair = PublicKeyBox.GenerateKeyPair())
          {
              string publicKey = Convert.ToBase64String(keyPair.PublicKey);
              string privateKey = Convert.ToBase64String(keyPair.PrivateKey);
              return (publicKey, privateKey);
          }
      }

      private static char[] EncryptPassword(byte[] plaintext, byte[] publicKey)
      {
          try
          {
              byte[] ciphertext = SealedPublicKeyBox.Create(plaintext, publicKey);
              return Convert.ToBase64String(ciphertext).ToCharArray();
          }
          catch (Exception ex) when (ExceptionFilters.PasswordSharingExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.Low);
              GetErrorMessage(ex, "Encryption");
              return Array.Empty();
          }
      }

      private static char[] DecryptPassword(byte[] ciphertext, byte[] privateKey)
      {
          try
          {
              using (var keyPair = PublicKeyBox.GenerateKeyPair(privateKey))
              {
                  byte[] plaintext = SealedPublicKeyBox.Open(ciphertext, keyPair);
                  return Encoding.UTF8.GetChars(plaintext);
              }
          }
          catch (Exception ex) when (ExceptionFilters.PasswordSharingExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.Low);
              GetErrorMessage(ex, "Decryption");
              return Array.Empty();
          }
      }

Shred Files

  1. 16 KiB erasure causes the first and last 16 KiB of the file to be overwritten with pseudorandom data.
  2. Zero fill overwrites files with zeroes using one pass.
  3. 1 Pass overwrites files with one pass of pseudorandom data.
  4. Encryption erasure causes files to be encrypted with XChaCha20 using a random key and nonce (generated using libsodium), which aren't stored. The encrypted file is then copied using File.Copy() to the location of the original, unencrypted file to overwrite it.
  5. HMG IS5 overwrites files using three passes over the file. The first pass uses ones, the second pass uses zeroes, and the third pass uses pseudorandom data.
  6. 5 Passes overwrites the selected files five times with pseudorandom data.
  7. Once a file has been overwritten using any of the above methods, the file is renamed to a random file name generated using Path.GetRandomFileName().
  8. The file's creation time, last access time, and last write time are replaced with a set date (the date AES was announced by NIST - November 26, 2001).
  9. Finally, the overwritten file is deleted using File.Delete().

Pseudorandom data is generated using libsodium.

Original file sizes are hidden because a FileStream is used with a byte array buffer to overwrite selected files. The original file size is ignored.

      public static void PseudorandomData(string filePath, BackgroundWorker bgwShredFiles)
      {
          try
          {
              using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
              {
                  byte[] randomBytes = FileHandling.GetBufferSize(fileStream);
                  while (fileStream.Position < fileStream.Length)
                  {
                      randomBytes = SodiumCore.GetRandomBytes(randomBytes.Length);
                      fileStream.Write(randomBytes, 0, randomBytes.Length);
                      ReportProgress.ReportEncryptionProgress(fileStream.Position, fileStream.Length, bgwShredFiles);
                  }
              }
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.High);
              DisplayMessage.ErrorResultsText(filePath, ex.GetType().Name, "'1 Pass' erasure failed.");
          }
      }

      private static void DeleteFile(string filePath)
      {
          try
          {
              string anonymisedFilePath = AnonymousRename.MoveFile(filePath, true);
              EraseFileMetadata(anonymisedFilePath);
              File.Delete(anonymisedFilePath);
              Globals.ResultsText += $"{Path.GetFileName(filePath)}: File erasure successful." + Environment.NewLine;
              Globals.SuccessfulCount += 1;
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.High);
              DisplayMessage.ErrorResultsText(filePath, ex.GetType().Name, "Unable to delete the file.");
          }
      }

      public static void EraseFileMetadata(string filePath)
      {
          try
          {
              var eraseDate = new DateTime(2001, 11, 26, 12, 0, 0);
              File.SetCreationTime(filePath, eraseDate);
              File.SetLastWriteTime(filePath, eraseDate);
          }
          catch (Exception ex) when (ExceptionFilters.FileAccessExceptions(ex))
          {
              Logging.LogException(ex.ToString(), Logging.Severity.Medium);
              DisplayMessage.ErrorResultsText(filePath, ex.GetType().Name, "Erasure of file metadata failed.");
          }
      }