using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; namespace InABox.Core { public static class Encryption { // 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 = 256; // This constant determines the number of iterations for the password bytes generation function. private const int DerivationIterations = 1000; public static string Encrypt(string plainText, string passPhrase, bool simple = false) { if (simple) { var encrypted = new byte[plainText.Length]; for (var i = 0; i < plainText.Length; i++) { var ch = plainText[i]; encrypted[i] = (byte)(ch + passPhrase[i % passPhrase.Length]); } return Convert.ToBase64String(encrypted); } // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text // so that the same Salt and IV values can be used when decrypting. var saltStringBytes = Generate256BitsOfRandomEntropy(); var ivStringBytes = Generate256BitsOfRandomEntropy(); var plainTextBytes = Encoding.UTF8.GetBytes(plainText); using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations)) { var keyBytes = password.GetBytes(Keysize / 8); using (var symmetricKey = new RijndaelManaged()) { symmetricKey.BlockSize = 256; symmetricKey.Mode = CipherMode.CBC; symmetricKey.Padding = PaddingMode.PKCS7; using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes)) { using (var memoryStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); cryptoStream.FlushFinalBlock(); // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes. var cipherTextBytes = saltStringBytes; cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray(); cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray(); memoryStream.Close(); cryptoStream.Close(); return Convert.ToBase64String(cipherTextBytes); } } } } } } public static string Decrypt(string cipherText, string passPhrase, bool simpleEncryption = true) { if (string.IsNullOrWhiteSpace(cipherText)) throw new Exception("Text may not be blank!"); // New Simplified Decryption to handle .Net6 clients // Kenric needs this to get the GenHTTP server running // Needs to be revisited (stronger encryption? TLS/SSL?) when moving to .Net6 overall if (cipherText.Length < 64 || simpleEncryption == true) { var encrypted = Convert.FromBase64String(cipherText); var decrypted = new byte[encrypted.Length]; for (var i = 0; i < encrypted.Length; i++) { var ch = encrypted[i]; decrypted[i] = (byte)(ch - passPhrase[i % passPhrase.Length]); } return Encoding.UTF8.GetString(decrypted); } // Get the complete stream of bytes that represent: // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText] var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText); // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes. var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray(); // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes. var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray(); // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string. var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8 * 2).Take(cipherTextBytesWithSaltAndIv.Length - Keysize / 8 * 2) .ToArray(); using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations)) { var keyBytes = password.GetBytes(Keysize / 8); using (var symmetricKey = new RijndaelManaged()) { symmetricKey.BlockSize = 256; symmetricKey.Mode = CipherMode.CBC; symmetricKey.Padding = PaddingMode.PKCS7; using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes)) { using (var memoryStream = new MemoryStream(cipherTextBytes)) { using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) { var plainTextBytes = new byte[cipherTextBytes.Length]; var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length); memoryStream.Close(); cryptoStream.Close(); return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount); } } } } } } public static bool Decrypt(string cipherText, string passPhrase, out string decrypted) { decrypted = ""; var result = false; try { decrypted = Decrypt(cipherText, passPhrase); result = true; } catch (Exception e) { } return result; } public static byte[] Generate256BitsOfRandomEntropy() { var randomBytes = new byte[32]; // 32 Bytes will give us 256 bits. using (var rngCsp = new RNGCryptoServiceProvider()) { // Fill the array with cryptographically secure random bytes. rngCsp.GetBytes(randomBytes); } return randomBytes; } public static string EncryptV2(string data, byte[] key) { if (TryEncryptV2(data, key, out var result, out var error)) { return result; } throw new Exception(error); } public static bool TryEncryptV2(string data, byte[] key, [NotNullWhen(true)] out string? result, [NotNullWhen(false)] out string? error) { try { using var stream = new MemoryStream(); using (var aes = Aes.Create()) { aes.Key = key; var iv = aes.IV; stream.Write(iv, 0, iv.Length); using var cryptoStream = new CryptoStream(stream, aes.CreateEncryptor(), CryptoStreamMode.Write); using var writer = new StreamWriter(cryptoStream); writer.Write(data); } result = Convert.ToBase64String(stream.ToArray()); error = null; return true; } catch(Exception e) { error = $"Encryption failed! {CoreUtils.FormatException(e)}"; result = null; return false; } } public static string DecryptV2(string data, byte[] key) { if(TryDecryptV2(data, key, out var result, out var error)) { return result; } throw new Exception(error); } public static bool TryDecryptV2(string data, byte[] key, [NotNullWhen(true)] out string? result, [NotNullWhen(false)] out string? error) { try { using var stream = new MemoryStream(Convert.FromBase64String(data)); using var aes = Aes.Create(); var iv = new byte[aes.IV.Length]; if (stream.Read(iv, 0, iv.Length) != iv.Length) { error = $"Decryption failed! Data is too short for IV."; result = null; return false; } using var cryptoStream = new CryptoStream(stream, aes.CreateDecryptor(key, iv), CryptoStreamMode.Read); using var decryptReader = new StreamReader(cryptoStream); var decrypted = decryptReader.ReadToEnd(); result = decrypted; error = null; return true; } catch(Exception e) { error = $"Decryption failed! {CoreUtils.FormatException(e)}"; result = null; return false; } } } }