Improve StringCipher code and correctness
MODIFY
WalletWasabi.Tests/UnitTests/Crypto/StringCipherTests.cs
WalletWasabi.Tests/UnitTests/Crypto/StringCipherTests.cs
using System.Security.Cryptography;
using System.Text;
using WalletWasabi.Crypto;
-using WalletWasabi.Logging;
using Xunit;
namespace WalletWasabi.Tests.UnitTests.Crypto

MODIFY
WalletWasabi/Crypto/StringCipher.cs
WalletWasabi/Crypto/StringCipher.cs
-using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
@@ -6,109 +5,111 @@ using WalletWasabi.Crypto.Randomness;
namespace WalletWasabi.Crypto
{
- // https://stackoverflow.com/a/10177020/2061103
public static class StringCipher
{
// This constant is used to determine the keysize of the encryption algorithm in bits.
// We divide this by 8 within the code below to get the equivalent number of bytes.
- private const int KeySize = 128;
+ private const int KeyByteSize = 128 / 8;
+ private const int AesBlockByteSize = 128 / 8;
+ private const int PasswordSaltByteSize = 128 / 8;
+ private const int SignatureByteSize = 256 / 8;
// This constant determines the number of iterations for the password bytes generation function.
private const int DerivationIterations = 1000;
+ private const int MinimumEncryptedMessageByteSize =
+ PasswordSaltByteSize + // Auth salt
+ PasswordSaltByteSize + // Key salt
+ AesBlockByteSize + // IV
+ AesBlockByteSize + // Cipher text min length
+ SignatureByteSize; // Signature tag
+
+ private static readonly SecureRandom SecureRandom = new();
+
public static string Encrypt(string plainText, string passPhrase)
{
// Salt is randomly generated each time, but is prepended to encrypted cipher text
// so that the same Salt value can be used when decrypting.
- byte[] salt = Generate128BitsOfRandomEntropy();
- byte[] cipherTextBytes;
- var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
-
- byte[] key = DerivateKey(passPhrase, salt);
- using var aes = CreateAES();
- aes.GenerateIV();
- byte[] iv = aes.IV;
- using var encryptor = aes.CreateEncryptor(key, iv);
- using var memoryStreamEncryptor = new MemoryStream();
- using (var cryptoStream = new CryptoStream(memoryStreamEncryptor, encryptor, CryptoStreamMode.Write))
- {
- cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
- cryptoStream.FlushFinalBlock();
- }
- cipherTextBytes = memoryStreamEncryptor.ToArray();
-
- using var memoryStream = new MemoryStream();
- using var writer = new BinaryWriter(memoryStream);
- writer.Write(salt);
- writer.Write(iv);
- using var hmac = new HMACSHA256(key);
- var authenticationCode = hmac.ComputeHash(iv.Concat(cipherTextBytes).ToArray());
- writer.Write(authenticationCode);
- writer.Write(cipherTextBytes);
- writer.Flush();
-
- var cipherTextWithAuthBytes = memoryStream.ToArray();
- return Convert.ToBase64String(cipherTextWithAuthBytes);
+ byte[] iv = GenerateRandomEntropy(AesBlockByteSize);
+ byte[] encryptionKeySalt = GenerateRandomEntropy(PasswordSaltByteSize);
+ byte[] encryptionKey = DerivateKey(passPhrase, encryptionKeySalt);
+
+ // Encrpyt the plain text
+ using var aes = CreateAES(encryptionKey);
+ byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
+ byte[] cipherTextBytes = aes.EncryptCbc(plainTextBytes, iv);
+
+ // Authenticate
+ byte[] authKeySalt = GenerateRandomEntropy(PasswordSaltByteSize);
+ byte[] authKey = DerivateKey(passPhrase, authKeySalt);
+ byte[] result = MergeArrays(additionalCapacity: SignatureByteSize, encryptionKeySalt, iv, authKeySalt, cipherTextBytes);
+
+ byte[] authCode = HMACSHA256.HashData(authKey, result[..^SignatureByteSize]);
+ authCode.CopyTo(result, result.Length - SignatureByteSize);
+
+ return Convert.ToBase64String(result);
}
- public static string Decrypt(string cipherText, string passPhrase)
- {
- var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
- byte[] key;
- byte[] iv;
-
- using var memoryStream = new MemoryStream(cipherTextBytesWithSaltAndIv);
- int cipherLength;
- int cipherStartIndex;
- using (var reader = new BinaryReader(memoryStream, Encoding.UTF8, true))
- {
- var salt = reader.ReadBytes(KeySize / 8);
- iv = reader.ReadBytes(KeySize / 8);
- var authenticationCode = reader.ReadBytes(32);
- cipherStartIndex = (int)memoryStream.Position;
- cipherLength = (int)(memoryStream.Length - memoryStream.Position);
- var cipher = reader.ReadBytes(cipherLength);
- key = DerivateKey(passPhrase, salt);
-
- using var hmac = new HMACSHA256(key);
- var calculatedAuthenticationCode = hmac.ComputeHash(iv.Concat(cipher).ToArray());
- for (var i = 0; i < calculatedAuthenticationCode.Length; i++)
+ public static string Decrypt(string encryptedString, string passPhrase)
{
- if (calculatedAuthenticationCode[i] != authenticationCode[i])
+ byte[] encryptedData = Convert.FromBase64String(encryptedString);
+
+ if (encryptedData is null || encryptedData.Length < MinimumEncryptedMessageByteSize)
{
- throw new CryptographicException("Message Authentication failed. Message has been modified or wrong password");
- }
+ throw new ArgumentException("Invalid length of encrypted data.");
}
+
+ Span<byte> encryptionKeySalt = encryptedData.AsSpan(0, PasswordSaltByteSize);
+ Span<byte> iv = encryptedData.AsSpan(PasswordSaltByteSize, AesBlockByteSize);
+ Span<byte> authKeySalt = encryptedData.AsSpan(PasswordSaltByteSize + AesBlockByteSize, PasswordSaltByteSize);
+ Span<byte> authCode = encryptedData.AsSpan(encryptedData.Length - SignatureByteSize, SignatureByteSize);
+
+ // Authenticate
+ byte[] authKey = DerivateKey(passPhrase, authKeySalt);
+
+ byte[] expectedAuthCode = HMACSHA256.HashData(authKey, encryptedData[..^SignatureByteSize]);
+
+ if (!authCode.SequenceEqual(expectedAuthCode))
+ {
+ throw new CryptographicException("Message Authentication failed. Message has been modified or wrong password.");
}
- using var aes = CreateAES();
- using var decryptor = aes.CreateDecryptor(key, iv);
- byte[] plainTextBytes = decryptor.TransformFinalBlock(cipherTextBytesWithSaltAndIv, cipherStartIndex, cipherLength);
+ // Decrypt.
+ int cipherTextIndex = authKeySalt.Length + encryptionKeySalt.Length + iv.Length;
+ int cipherTextLength = encryptedData.Length - cipherTextIndex - authCode.Length;
+
+ byte[] encryptionKey = DerivateKey(passPhrase, encryptionKeySalt);
+ using var aes = CreateAES(encryptionKey);
+ byte[] plainTextBytes = aes.DecryptCbc(encryptedData.AsSpan(cipherTextIndex, cipherTextLength), iv);
return Encoding.UTF8.GetString(plainTextBytes);
}
- private static byte[] DerivateKey(string passPhrase, byte[] salt)
- {
- using var password = new Rfc2898DeriveBytes(passPhrase, salt, DerivationIterations);
- return password.GetBytes(KeySize / 8);
- }
+ private static byte[] DerivateKey(string passPhrase, Span<byte> salt) =>
+ Rfc2898DeriveBytes.Pbkdf2(passPhrase, salt, DerivationIterations, HashAlgorithmName.SHA256, KeyByteSize);
- private static Aes CreateAES()
+ private static Aes CreateAES(byte[] encryptionKey)
{
Aes aes = Aes.Create();
- aes.BlockSize = 128;
- aes.Mode = CipherMode.CBC;
- aes.Padding = PaddingMode.PKCS7;
-
+ aes.Key = encryptionKey;
return aes;
}
- private static byte[] Generate128BitsOfRandomEntropy()
+ private static byte[] GenerateRandomEntropy(int numberOfBytes) =>
+ SecureRandom.GetBytes(numberOfBytes);
+
+ private static byte[] MergeArrays(int additionalCapacity, params byte[][] arrays)
+ {
+ byte[] merged = new byte[arrays.Sum(a => a.Length) + additionalCapacity];
+ int mergeIndex = 0;
+
+ for (int i = 0; i < arrays.GetLength(0); i++)
{
- using var secureRandom = new SecureRandom();
- var randomBytes = secureRandom.GetBytes(16); // 16 Bytes will give us 128 bits.
- return randomBytes;
+ arrays[i].CopyTo(merged, mergeIndex);
+ mergeIndex += arrays[i].Length;
+ }
+
+ return merged;
}
}
}

Improve StringCipher code and correctness