Question
How can I securely encrypt a string in C# and later decrypt it back to its original value? What encryption approach should I use, and how should I handle the encryption key, nonce, and encoded output?
Short Answer
You will learn that reversible encryption requires a secret key, why authenticated encryption is important, and how to encrypt UTF-8 strings with AES-GCM in modern C#. You will also learn when not to decrypt data, such as when storing user passwords.
Concept
Encryption transforms readable plaintext into unreadable ciphertext using a secret key. Decryption uses the same secret key to restore the original plaintext.
For new C# applications, AES-GCM is a strong default for encrypting small pieces of data such as tokens, private configuration values, or sensitive fields. It provides two protections:
- Confidentiality: people without the key cannot read the ciphertext.
- Integrity and authenticity: changes to the ciphertext are detected during decryption.
AES-GCM is a symmetric encryption algorithm: the same key is used for encryption and decryption. This means protecting the key is as important as protecting the encrypted data.
A secure encryption operation needs:
- A random key: secret and kept outside source code.
- A fresh random nonce for every encryption with the same key. A nonce is not secret, but it must not be reused with that key.
- A ciphertext: the encrypted bytes.
- An authentication tag: data used to detect tampering.
Do not use encryption for passwords. Applications should verify passwords, not recover them. Store passwords with a purpose-built password hashing algorithm such as PBKDF2, bcrypt, scrypt, or Argon2 instead.
Mental Model
Think of encryption as putting a message into a locked security box:
- The plaintext is the message you can read.
- The key is the unique key that locks and unlocks the box.
- The nonce is a one-time packing label. It ensures that encrypting the same message twice does not produce the same result.
- The authentication tag is a tamper-evident seal. If someone changes the box or its contents, the seal no longer verifies.
- The ciphertext is the locked, unreadable message.
You may store the packing label and the seal beside the box. However, anyone who gets the key can open every box encrypted with that key.
Syntax and Examples
AES-GCM works with bytes, so convert strings to UTF-8 bytes before encryption and convert them back after decryption.
using System;
using System.Security.Cryptography;
using System.Text;
byte[] key = RandomNumberGenerator.GetBytes(32); // 256-bit key
byte[] nonce = RandomNumberGenerator.GetBytes(12);
byte[] plaintext = Encoding.UTF8.GetBytes("Hello, C#!");
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[16];
using (var aes = new AesGcm(key, tagSizeInBytes: 16))
{
aes.Encrypt(nonce, plaintext, ciphertext, tag);
}
Console.WriteLine(Convert.ToBase64String(ciphertext));
byte[] decrypted = new byte[ciphertext.Length];
using (var aes = new AesGcm(key, tagSizeInBytes: 16))
{
aes.Decrypt(nonce, ciphertext, tag, decrypted);
}
Console.WriteLine(Encoding.UTF8.GetString(decrypted));
ciphertext, nonce, and tag are binary data. Base64 is commonly used to put binary data into JSON, a database text column, or a configuration file. Base64 is encoding, not encryption.
The example creates a random key only for demonstration. If the program exits, that key is lost and the ciphertext can no longer be decrypted. A real application must load the key from secure key storage.
Step by Step Execution
Consider this small encryption flow:
string message = "Order: 42";
byte[] plaintext = Encoding.UTF8.GetBytes(message);
byte[] nonce = RandomNumberGenerator.GetBytes(12);
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[16];
using var aes = new AesGcm(key, tagSizeInBytes: 16);
aes.Encrypt(nonce, plaintext, ciphertext, tag);
messageis ordinary readable text.Encoding.UTF8.GetBytesconverts the text into bytes because cryptographic APIs operate on bytes.- A new 12-byte random nonce is created. Generate a new one for every call to
Encryptwhen using the same key. ciphertextis allocated with the same length as the plaintext. AES-GCM does not expand this byte array; its separate tag has a fixed length.tagis allocated to hold the authentication data.Encryptfillsciphertextandtag.
To decrypt, provide the same key, nonce, ciphertext, and tag:
byte[] decrypted = new byte[ciphertext.Length];
aes.Decrypt(nonce, ciphertext, tag, decrypted);
string originalMessage = Encoding.UTF8.GetString(decrypted);
If the wrong key, nonce, tag, or ciphertext is supplied, Decrypt throws CryptographicException. Treat that as invalid or tampered data; do not attempt to use partial output.
Real World Use Cases
Encryption is appropriate when an application genuinely needs to recover the original value later.
- Encrypt an API credential before storing it in a database, then decrypt it only when calling the external service.
- Encrypt sensitive application settings at rest.
- Protect a private note or document before saving it to local storage.
- Encrypt a short secret included in a backup export.
- Protect data exchanged between trusted components when an additional application-level encryption layer is required.
For data in transit, use HTTPS/TLS rather than creating custom encryption around every HTTP request. For full-database or disk protection, use the platform or service encryption features as well as appropriate access controls.
Real Codebase Usage
In production code, encryption is usually wrapped in a small service rather than repeated throughout controllers or UI code.
Common practices include:
- Load keys from a secret manager, environment-specific protected configuration, cloud key vault, or hardware-backed key store.
- Store an encryption key identifier with encrypted records so keys can be rotated later.
- Include the nonce and tag with each ciphertext. They are required for decryption but do not need secrecy.
- Use a versioned payload format such as
v1:keyId:base64-dataso the format can evolve safely. - Validate null or empty input before processing it.
- Catch
CryptographicExceptionat an application boundary and return a safe error such as “encrypted value is invalid.” Do not expose key details or plaintext in logs. - Restrict which service accounts can read or use the key.
A typical design stores a record like this:
Version: 1
KeyId: customer-data-2025
Payload: Base64(nonce || tag || ciphertext)
The application gets the correct key using KeyId, then parses and decrypts Payload. This makes key rotation possible without guessing which key was used.
Common Mistakes
Hard-coding a key
// Do not do this.
byte[] key = Encoding.UTF8.GetBytes("my-secret-key-123");
Source code is copied, committed, logged, and shared. Keep keys in a secure secret store. Also, this example may not have a valid AES key length.
Reusing an AES-GCM nonce
// Unsafe: the nonce must not be reused with the same AES-GCM key.
byte[] nonce = new byte[12];
An all-zero nonce is repeated every time. Generate a fresh nonce with RandomNumberGenerator.GetBytes(12) for each encryption.
Omitting the authentication tag
Using an unauthenticated mode or discarding the tag can allow modified ciphertext to be accepted. Store the tag and verify it with Decrypt.
Treating Base64 as security
string encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes("secret"));
Anyone can decode this. Base64 changes representation; it does not hide data.
Encrypting passwords
If an attacker obtains an encryption key, encrypted passwords can be recovered. Use a salted password hash and a password-verification API instead.
Losing the key or nonce
Without the original key, decryption is impossible by design. Without the nonce and tag, an AES-GCM payload cannot be decrypted or authenticated. Store required metadata alongside the ciphertext.
Using Random for cryptographic values
Random is not a cryptographically secure random-number generator. Use RandomNumberGenerator for keys, nonces, salts, and tokens.
Comparisons
| Technique | Can recover original value? | Suitable for secrets at rest? | Main use |
|---|---|---|---|
| AES-GCM encryption | Yes, with the key | Yes | Values that must later be read |
| Base64 encoding | Yes, by anyone | No | Represent binary data as text |
| Hashing | No | For password verification | Compare a supplied value with a stored verifier |
| Password hashing | No | Yes, for passwords | Slow, salted password storage |
| RSA encryption | Usually, with private key | Sometimes | Public-key exchange and small payloads |
| AES-CBC | AES-GCM |
|---|---|
| Provides encryption but needs separate integrity protection, such as HMAC. | Provides encryption and authentication together. |
| Requires careful padding and IV handling. | Does not use padding; requires unique nonces. |
| Easy to use incorrectly in custom code. | Usually preferred for new symmetric-encryption designs. |
AES-GCM is not automatically the right choice for every storage problem. For example, ASP.NET Core Data Protection is often more convenient for application-managed protected values, while password hashes are required for user passwords.
Cheat Sheet
using System.Security.Cryptography;
using System.Text;
byte[] key = RandomNumberGenerator.GetBytes(32); // AES-256 key
byte[] nonce = RandomNumberGenerator.GetBytes(12); // new for every encryption
byte[] tag = new byte[16];
byte[] plaintext = Encoding.UTF8.GetBytes("text");
byte[] ciphertext = new byte[plaintext.Length];
using var aes = new AesGcm(key, tagSizeInBytes: 16);
aes.Encrypt(nonce, plaintext, ciphertext, tag);
- AES key sizes: 16, 24, or 32 bytes. Use 32 bytes for AES-256 when appropriate.
- AES-GCM nonce: commonly 12 bytes; never reuse it with the same key.
- Authentication tag: commonly 16 bytes.
- Save:
nonce + tag + ciphertextand any key/version metadata. - Encode binary payloads with
Convert.ToBase64Stringfor text storage. - Decode with
Convert.FromBase64Stringbefore decryption. - Expect
CryptographicExceptionfor a wrong key or modified payload. - Never hard-code keys, log plaintext secrets, or use reversible encryption for passwords.
AesGcmis available in modern .NET. Ensure the target framework supports it.
FAQ
Can I encrypt a string directly in C#?
Cryptographic APIs encrypt bytes, not .NET strings. Convert the string to UTF-8 bytes before encryption and convert decrypted bytes back with UTF-8.
Can I use the same AES key to encrypt many strings?
Yes, but every AES-GCM encryption using that key must use a new unique nonce. Generate a random nonce for each operation and store it with the ciphertext.
Should the nonce be secret?
No. The nonce is required for decryption and can be stored alongside the ciphertext. Its uniqueness, not secrecy, is the important property.
Can I store encrypted data as a string in a database?
Yes. Combine the nonce, tag, and ciphertext, then Base64-encode the combined bytes. Store key/version metadata separately or in a structured payload.
Why does AES-GCM decryption throw CryptographicException?
The key may be incorrect, or the nonce, tag, or ciphertext may have changed. Treat the payload as invalid rather than trying to recover it.
Is AES encryption appropriate for user passwords?
No. Passwords should be salted and hashed with a password-hashing algorithm so the original password cannot be recovered.
Where should I store the encryption key?
Use a managed secret store or platform protection mechanism, such as a cloud key vault, OS credential store, or protected production configuration. Do not put it in source code or a public configuration file.
Mini Project
Description
Build a small C# console utility that protects a private note with AES-GCM. It creates a payload that contains the nonce, authentication tag, and ciphertext, then decrypts the payload to prove that the original note can be recovered only with the correct key.
Goal
Encrypt a user-provided note into a Base64 payload and decrypt it back to the original text.
Requirements
Use AES-GCM with a randomly generated 32-byte key. Generate a new 12-byte nonce for encryption. Store the nonce, 16-byte tag, and ciphertext in one Base64-encoded payload. Decrypt the payload and display the recovered note. Handle tampered or invalid payloads safely.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.