Previous Efforts
Back in February, I decided it was time to demystify the black box of secure Internet communications. I have been a professional programmer too long to continue carrying a hazy understanding of how web browsers and servers communicate securely. I wanted to remove HTTPS / SSL / TLS from the realm of magic and place it in the realm of the known and understood– a computer algorithm implemented in C# code. So I researched the topic, learned the underlying mathematics, wrote code to prove the theory actually works in practice, then wrote three blog posts explaining what I had learned, including sharing my code.
- The Math That Enables Asymmetric Key Cryptography
- Encrypting and Decrypting Text
- AES, A Stronger Cipher Than XOR
This Project
My earlier blog posts examine how to create a secure communication channel between a client application and server, such as a web browser and web server. In this blog post, I’ll examine how to encrypt and decrypt files on a local computer. I’ll do this by writing a console application complete with command-line arguments to select various security mechanisms. I’ll include arguments for:
- Input File
- Operation (Encrypt or Decrypt)
- Cipher Algorithm
- Key Derivation Algorithm (technique used to translate user-provided password into a byte array for cipher key)
- Key Derivation Iterations
- Key Length
- Salt Length (protection against pre-computed attacks and reuse of same password on multiple websites).
Boilerplate Code
Let’s start by writing boilerplate code that parses command-line arguments.
The ParseCommandLine
method returns an EncryptedFileHeader
class. We’ll use this class later when writing encryption arguments to an unencrypted header at the beginning of the encrypted file.
Notice the EncryptedFileHeader
class does not contain a Password
or Key
property. The absence of these properties is critical. Neither the user’s password nor the key derived from the password are written into the unencrypted header of the encrypted file. Otherwise, a malicious party could open the encrypted file in a hex editor (or simply Notepad++), view the first few bytes, and try interpreting them using common deserialization libraries (.NET Binary Serializer, Json.NET, etc) until they found a password or key.
Instead, the program writes enough detail into the unencrypted file header so it may, at a later time when the user provides their password, recreate the encryption key and decrypt the file.
Let’s write a factory class that instantiates various cipher algorithms written by Microsoft, all of which inherit from the SymmetricAlgorithm abstract class.
Similarly, let’s write a factory class that instantiates various key derivation algorithms written by Microsoft, all of which inherit from the DeriveBytes abstract class.
Encryption
Now let’s write a method that encrypts a file.
Notice how the code leverages the polymorphism capabilities of C# to assign one of two classes (PasswordDeriveBytes or Rfc2898DeriveBytes) to a variable of type DeriveBytes.
using (DeriveBytes keyDerivation = KeyDerivation.Create(EncryptedFileHeader.KeyDerivationAlgorithm,
password, EncryptedFileHeader.Salt, EncryptedFileHeader.KeyDerivationIterations))
Similarly, it uses polymorphism to assign one of several classes (AesCryptoServiceProvider, AesManaged, AesCng, TripleDESCryptoServiceProvider, TripleDESCng) to a variable of type SymmetricAlgorithm.
using (SymmetricAlgorithm cipher = Cipher.Create(EncryptedFileHeader.CipherAlgorithm))
Unencrypted Header
The above code writes encryption settings to an unencrypted header at the beginning of the encrypted file. The header begins with 4 bytes representing an integer. This integer specifies the length of the header, x
. The next x
bytes, in positions 4
through x + 3
, are the the UTF-8 encoded bytes of a JSON string representing the EncryptedFileHeader
class. The remaining y
bytes, in positions x + 4
through x + y + 3
are the encrypted contents of the y-length file (the encrypted length is identical to the plaintext length). For example, an encrypted file of length 102,400 bytes with a header of length 307 bytes is structured as follows.
Protection | Unencrypted | Encrypted | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
Position | 0 | 1 | 2 | 3 | 4 | 5 | … | 310 | 311 | 312 | … | 102,710 |
Data | Header Length | Header Contents | File Contents | |||||||||
Binary Value | 00000000 | 00000000 | 00000001 | 00110011 | ? | ? | ? | ? | ? | ? | ? | ? |
Run the program, specifying the encrypt operation.
PS C:\Users\Erik\...\Encryptor> dotnet run -c release --
-i "C:\Users\Erik\Documents\Financial\Tax Returns\2018\MarriedFilingJointlyTaxReturn.pdf"
-o encrypt -c aescng -kd rfc2898 -kdi 1000 -kl 32 -sl 16
InputPath is a file.
Enter password: MeetingWithTheBobs1999
Output filename is C:\Users\Erik\Documents\Financial\Tax Returns\2018\MarriedFilingJointlyTaxReturn.encrypted.
Encryption key (derived from password and a random salt) is e1iMt4U78rOW1231AvQPhlMiNm/ffSG07E6IRWcMzLI=.
Cipher initialization vector is 43OgyOI8c6CYDagwSx8gkw==.
Wrote encrypted file to C:\Users\Erik\Documents\Financial\Tax Returns\2018\MarriedFilingJointlyTaxReturn.encrypted.
Encryption took 0.192 seconds.
Decryption
Let’s write a method that decrypts a file by reversing the process. Read the EncryptedFileHeader
from the unencrypted section at the beginning of the encrypted file. Then ask the user for the password. Then generate the encryption key from the password, salt, and key derivation algorithm. Then create a cipher from the key and initialization vector and decrypt the file stream.
Run the program, specifying the decrypt operation.
PS C:\Users\Erik\...\Encryptor> dotnet run -c release --
-i "C:\Users\Erik\Documents\Financial\Tax Returns\2018\MarriedFilingJointlyTaxReturn.encrypted" -o decrypt
Enter password: MeetingWithTheBobs1999
Output filename is C:\Users\Erik\Documents\Financial\Tax Returns\2018\MarriedFilingJointlyTaxReturn.pdf.
Encryption key (derived from password and salt) is e1iMt4U78rOW1231AvQPhlMiNm/ffSG07E6IRWcMzLI=.
Cipher initialization vector is 43OgyOI8c6CYDagwSx8gkw==.
Wrote decrypted file to C:\Users\Erik\Documents\Financial\Tax Returns\2018\MarriedFilingJointlyTaxReturn.pdf.
Decryption took 0.197 seconds.
Double-click the decrypted file and verify it looks identical to the original document. Run the program again, specifying the decrypt operation, however enter an incorrect password. Most likely the program fails with the following error message.
Exception Type = System.Security.Cryptography.CryptographicException Exception Message = Padding is invalid and cannot be removed. Exception StackTrace = at Internal.Cryptography.UniversalCryptoDecryptor.DepadBlock(Byte[] block, ...
If somehow it succeeds, the file contents remain encrypted and unintelligible to the associated application (Adobe Acrobat in my example) or a hex editor. The decryption may have succeeded in transforming the file content bytes, but without the correct password it merely produces gibberish.
You may review the full source code in my Encryptor project in GitHub.