Cryptography
The cryptography module has one static class, Security. It provides symmetric string encryption and decryption (AES-256 in CBC mode, with a key derived from a password) and string hashing with MD5, SHA-1, SHA-2 and, on .NET 8 or later, SHA-3.
These helpers exist for compatibility with data produced by earlier versions of the library. Read Security notes before you use them for new data.
| Package | CrispyWaffle |
| Namespace | CrispyWaffle.Cryptography |
How it works
Encryption and decryption
Encrypt and Decrypt are extension methods on string. Both take the same three secrets: passwordHash, saltKey and viKey.
| Step | Detail |
|---|---|
| Key derivation | new Rfc2898DeriveBytes(passwordHash, Encoding.ASCII.GetBytes(saltKey)).GetBytes(32). This is PBKDF2 with the .NET defaults for this constructor: HMAC-SHA1 and 1,000 iterations. The password is UTF-8 encoded by Rfc2898DeriveBytes; the salt is ASCII encoded (non-ASCII characters become ?). The result is a 256-bit key. |
| Initialization vector | Encoding.ASCII.GetBytes(viKey), used as is. It must be exactly 16 ASCII characters (one 128-bit block). Any other length throws ArgumentException ("Specified initialization vector (IV) does not match the block size for this algorithm."). |
| Cipher | RijndaelManaged with its default 128-bit block size, CipherMode.CBC. With a 256-bit key this is AES-256-CBC. |
| Padding | Encrypt uses PaddingMode.Zeros. Decrypt uses PaddingMode.None and then removes all trailing '\0' characters from the decoded text. |
| Plain text encoding | UTF-8. |
| Cipher text encoding | Base64 of the raw cipher text. The output contains only the cipher text: no salt, IV, version or authentication tag. |
Encryption is deterministic: the same plain text with the same three secrets always gives the same output.
Hashing
Security.Hash(value, type) UTF-8 encodes value, computes the hash with the selected algorithm and converts the bytes to a string. The string format depends on the algorithm:
| Algorithm | Output format |
|---|---|
Md5 |
Lowercase hexadecimal, two characters per byte (32 characters). This is the usual MD5 hex string. |
| All others | Each byte as an unpadded decimal number (0 to 255), concatenated with no separator. For example, bytes 0x05 0xA0 become "5160". |
The non-MD5 format is not the standard hex encoding, so it does not match the output of other tools (sha256sum, Convert.ToHexString), and it has variable length. Different hashes can also produce the same string, because the byte boundaries are lost. Compare these values only with other values produced by Security.Hash.
The hash algorithm objects are created once, in a static field, and shared by all calls.
Getting started
using System;
using CrispyWaffle.Cryptography;
const string password = "123456";
const string salt = "12345678";
const string iv = "HZEM7|Ne2YGS/F41"; // exactly 16 ASCII characters
var encrypted = "PlainText".Encrypt(password, salt, iv); // "vBWW26e4f9QMRPjz4pXoEQ=="
var decrypted = encrypted.Decrypt(password, salt, iv); // "PlainText"
var md5 = Security.Hash("PlainText", HashAlgorithmType.Md5); // "b7ebbf7f254ef646928dd58f62383a85"
Console.WriteLine($"{encrypted} {decrypted} {md5}");
Reference
Security
public static class Security — string encryption, decryption and hashing helpers.
| Member | Description |
|---|---|
Encrypt(this string plainText, string passwordHash, string saltKey, string viKey) |
Encrypts plainText with AES-256-CBC and zero padding, as described in How it works. Returns the cipher text as Base64. Throws ArgumentException when viKey is not 16 ASCII bytes. Throws ArgumentNullException when any argument is null. |
Decrypt(this string encryptedText, string passwordHash, string saltKey, string viKey) |
Decodes Base64 and decrypts with the same parameters. Returns the UTF-8 text with trailing '\0' characters removed. Throws FormatException for invalid Base64, ArgumentException for an invalid IV length, and CryptographicException when the cipher text length is not a multiple of 16 bytes. Wrong secrets do not throw; they return unreadable text. |
Hash(string value, HashAlgorithmType type) |
Returns the hash of value in the format described in Hashing. Throws ArgumentOutOfRangeException (Invalid algorithm type) for a type that is not supported on the current target framework. Throws ArgumentNullException when value is null. |
HashAlgorithmType
public enum HashAlgorithmType — algorithm selector for Security.Hash.
| Value | Description |
|---|---|
Md5 |
MD5 (128-bit). Output is lowercase hex. |
Sha1 |
SHA-1 (160-bit). |
Sha256 |
SHA-256. |
Sha384 |
SHA-384. |
Sha512 |
SHA-512. |
Sha3_256 |
SHA3-256. Only defined when the library is compiled for .NET 8 or later. |
Sha3_384 |
SHA3-384. Only defined when the library is compiled for .NET 8 or later. |
Sha3_512 |
SHA3-512. Only defined when the library is compiled for .NET 8 or later. |
Examples
Hash with each algorithm
using System;
using CrispyWaffle.Cryptography;
foreach (var type in new[] { HashAlgorithmType.Md5, HashAlgorithmType.Sha1, HashAlgorithmType.Sha256 })
{
Console.WriteLine($"{type}: {Security.Hash("PlainText", type)}");
}
// Md5: b7ebbf7f254ef646928dd58f62383a85
// Sha1: 5022439192242211661341747135888248197192199115101
// Sha256: 43167247186156224140100585014234839723255150225212687216120565623113068401650
Keep secrets out of code
Read the three secrets from configuration instead of constants. The IV still has to be 16 ASCII characters.
using System;
using CrispyWaffle.Cryptography;
var password = Environment.GetEnvironmentVariable("APP_CRYPTO_PASSWORD");
var salt = Environment.GetEnvironmentVariable("APP_CRYPTO_SALT");
var iv = Environment.GetEnvironmentVariable("APP_CRYPTO_IV");
var token = "customer-42".Encrypt(password, salt, iv);
var original = token.Decrypt(password, salt, iv);
Security notes
These points come from the code in Security.cs:
- No integrity protection. The output is unauthenticated AES-CBC. An attacker who can change the cipher text can change the decrypted plain text in predictable ways, and
Decryptwill not detect it. Do not use this for data that crosses a trust boundary without a separate MAC or signature. - Fixed IV. The IV comes from a caller-supplied string and is reused for every message. Equal plain texts produce equal cipher texts, and messages that share a prefix produce cipher texts that share a prefix. Each message should have a random IV.
- Fixed salt. The salt is also a caller-supplied constant, so the derived key is the same for every message.
- Weak key derivation. PBKDF2 uses the
Rfc2898DeriveBytesconstructor defaults: HMAC-SHA1 with 1,000 iterations, far below current recommendations. TreatpasswordHashas a high-entropy secret, not a human password. - Zero padding. Plain text that ends with
'\0'characters loses them on decryption. - Obsolete APIs.
RijndaelManagedand theRfc2898DeriveBytes(string, byte[])constructor are marked obsolete in recent .NET versions (SYSLIB0022 and SYSLIB0041). - Weak hash algorithms.
Md5andSha1are broken for collision resistance. Do not use them for signatures or integrity checks. None of the hash options is suitable for password storage: they are fast, unsalted hashes. Use a password hashing function (PBKDF2 with a high iteration count, bcrypt, scrypt or Argon2) for passwords. - Thread safety.
Hashreuses oneHashAlgorithminstance per algorithm, stored in a static dictionary.HashAlgorithm.ComputeHashis not thread-safe, so concurrent calls with the same algorithm can return wrong results or throw. Synchronize calls if you hash from several threads. - SHA-3 availability. On .NET 8 or later, the SHA-3 instances are created in the same static initializer as the others. .NET throws
PlatformNotSupportedExceptionfromSHA3_256.Create()and its siblings on platforms without SHA-3 support, which would surface as aTypeInitializationExceptionthe first timeHashruns on such a platform.