In this article, we will learn how encrypting and decrypting a string is done using C# and the Cryptography package that comes included with .NET.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Types of Encryption Algorithms

We use encryption to obscure a piece of information, for example, a text, turning it into a series of seemingly random bytes. When storing encrypted data or sending it to a recipient, third parties will be unable to make sense of the encrypted information if someone other than the intended recipient gains access to the information.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

We can categorize different encryption algorithms into three main groups: Symmetric or secret key encryption algorithms, which use a single secret password, or key, that we need to share with the recipients of the information so they can decrypt it and gain access to the original message.

On the other hand, Asymmetric or public key encryption is based on public/private key pairs. We are more secure since there’s no secret shared key that we need to transmit and hackers can, potentially, intercept.

Finally, Hashing algorithms provide one-way encryption. These are used, among other things, for digital signing and password protection.

For this article, we will focus on a symmetric encryption algorithm that uses a shared secret key.

Encrypting and Decrypting in C#

We will be using System.Security.Cryptography, a package included in .NET. This package bundles several tools for encryption and decryption including implementations of various algorithms.

Choosing the Right Algorithm for Encrypting and Decrypting a String

Among all supported symmetric-key algorithms, the HMACSHA256 and its variants are only suitable for data verification and cryptographic document signing. That leaves us with AES encryption as our only choice for secret key data protection.

AES stands for “Advanced Encryption Standard” and it is an official international standard (ISO/IEC 18033-3) for symmetric-key encryption. Considering all this, we are going to base our example on the AES algorithm implementation.

Encrypting and Decrypting a String With AES

Using the .NET cryptography package to encrypt a simple string value is not straightforward. We will have to deal with byte arrays, streams, disposable objects, etc. Therefore, for our example, we will try to hide all that complexity into a single class called StringEncryptionService with an easy-to-use interface.

Encryption Key and Initialization Vector

We aim to implement an encryption method that takes a string and a passphrase as input parameters. We will have to use the same passphrase to turn that encrypted data back to its unencrypted version.

The AES encryption algorithm uses the concept of an encryption key used to encrypt and decrypt the data. That aligns with the use we want to make of a user-provided passphrase.

However, we can’t use the passphrase directly since the AES encryption key needs to be either 128, 192, or, 256 bits long. The accepted key length depends on the value of the BlockSize property of the Aes object instance.

To solve that, we will implement a private method in our StringEncryptionService class to transform the user-provided password into a valid encryption key:

private byte[] DeriveKeyFromPassword(string password)
{
    var emptySalt = Array.Empty<byte>();
    var iterations = 1000;
    var desiredKeyLength = 16; // 16 bytes equal 128 bits.
    var hashMethod = HashAlgorithmName.SHA384;
    return Rfc2898DeriveBytes.Pbkdf2(Encoding.Unicode.GetBytes(password),
                                     emptySalt,
                                     iterations,
                                     hashMethod,
                                     desiredKeyLength);
}

Here, we are using a key derivation algorithm called PBKDF2 to turn the user passphrase into a random fixed-length byte array. To achieve this we are using the static method Rfc2898DeriveBytes.Pbkdf2(). Note that we could potentially use a password salt as the second parameter of this call. In this case, however, we are just using an empty salt for simplicity.

Likewise, the AES implementation requires an initialization vector. We will use a 128-bit value called initialization vector or IV to generate more entropy in the resulting encrypted data. This will make it more difficult to identify patterns in it.

Again, for simplicity and because we are dealing with relatively small string values we will use a fixed IV:

private byte[] IV =
{
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
    0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16
};

If we do not provide values for the encryption key and/or the initialization vector AES will generate random values for us. But, in that case, we must store both values somewhere since we will need them to decrypt our data later.

String Encryption Method

Now that we have our IV and a way to generate valid encryption keys, we can proceed to implement our string encryption method:

public async Task<byte[]> EncryptAsync(string clearText, string passphrase)
{
    using Aes aes = Aes.Create();
    aes.Key = DeriveKeyFromPassword(passphrase);
    aes.IV = IV;

    using MemoryStream output = new();
    using CryptoStream cryptoStream = new(output, aes.CreateEncryptor(), CryptoStreamMode.Write);

    await cryptoStream.WriteAsync(Encoding.Unicode.GetBytes(clearText));
    await cryptoStream.FlushFinalBlockAsync();

    return output.ToArray();
}

First, we use the Aes.Create() factory method to obtain an instance of the Aes object that we will later use to obtain an encryptor. We generate our encryption key using the DeriveKeyFromPassword() method discussed earlier and add it to the aes object along with our fixed initialization vector.

Next, we go on and create an output MemoryStream that receives the encrypted data as it gets generated. We also need a CryptoStream wrapping the output stream. Here, we are setting up the CryptoStream with an encryptor and CryptoStreamMode.Write so we can write our input text to it and get encrypted data written to the output stream.

Finally, we write the user-provider clear text to the CryptoStream using the WriteAsync() method. It is important to manually call the FlushFinalBlockAsync() on the cryptoStream object method so all the remaining bytes are written to the output stream before we return its contents.

String Decryption Method

To continue, our decryption method will follow a similar pattern:

public async Task<string> DecryptAsync(byte[] encrypted, string passphrase)
{
    using Aes aes = Aes.Create();
    aes.Key = DeriveKeyFromPassword(passphrase);
    aes.IV = IV;

    using MemoryStream input = new(encrypted);
    using CryptoStream cryptoStream = new(input, aes.CreateDecryptor(), CryptoStreamMode.Read);

    using MemoryStream output = new();
    await cryptoStream.CopyToAsync(output);

    return Encoding.Unicode.GetString(output.ToArray());
}

Just like in our Encrypt() method, we set up the encryption key and IV. It is important to use the same values used during encryption to decrypt successfully.

Then, we set up our CryptoStream, this time, wrapped around a MemoryStream containing the encrypted data. Since this is a decryption operation, we use a decryptor instead of an encryptor and CryptoStreamMode.Read.

Finally, we use CryptoStream.CopyToAsync() to copy the contents of cryptoStream into a newly created MemoryStream. At this point, the encrypted data gets pulled from the input MemoryStream and put through the decrypt transformation.

Testing Encryption and Decryption With AES

Now that we have completed the implementation of our StringEncryptionService class, we can use the EncryptAsync() method to turn any text into a password-protected encrypted byte array:

var encryptionService = new StringEncryptionService();
const string passphrase = "Sup3rS3curePass!";

var encrypted = await encryptionService.EncryptAsync("We use encryption to obscure a piece of information.",
                                                     passphrase);

Console.WriteLine($"Encrypted data: {BitConverter.ToString(encrypted)}");

Upon execution, the previous code should output the generated encrypted bytes:

Encrypted data: 3A-BB-65-D7-50-A9-13-75-D4-96-1F-2A-A3-02-97-A6-51-EA...

In the same fashion, we can use the DecryptAsync() method along with the original password to decrypt those bytes and get the original text back:

var decrypted = await encryptionService.DecryptAsync(encrypted, passphrase);

Console.WriteLine($"Decrypted data: {decrypted}");

The output for this code should return the original text:

Decrypted data: We use encryption to obscure a piece of information.

Conclusion

In this article, we have learned about the different types of encryption algorithms. We talked about the cryptography package in .NET. We also have created a simple class with methods for encrypting and decrypting a string using the symmetric secret-key algorithm AES.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!