using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Web; namespace Tango.Portal.Utils { public static class SimpleCryptoHelper { // ======= CONFIG ======= // Provide a 32-byte (256-bit) master key in Base64 (standard Base64, not URL form). // You can reuse the one we generated earlier or generate your own. private const string MasterKeyBase64 = "1SuLF9QlXTNIAKAzRb4ILuQnfZ40BERQtXiMQKHLbQg="; // ======= PUBLIC API ======= public static string Encrypt(string plainText) { if (plainText == null) throw new ArgumentNullException(nameof(plainText)); var masterKey = Convert.FromBase64String(MasterKeyBase64); if (masterKey.Length != 32) throw new InvalidOperationException("Master key must be 32 bytes (Base64 of 32 bytes)."); // Derive subkeys byte[] encKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("enc"), 32); byte[] macKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("mac"), 32); // Generate IV (16 bytes for AES-CBC) byte[] iv = new byte[16]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(iv); byte[] ciphertext; using (var aes = CreateAes(encKey, iv)) using (var encryptor = aes.CreateEncryptor()) { var pt = Encoding.UTF8.GetBytes(plainText); ciphertext = encryptor.TransformFinalBlock(pt, 0, pt.Length); } // Build payload: version(1) || iv(16) || ciphertext || tag(32) byte version = 1; byte[] versionArr = new[] { version }; byte[] macInput = Concat(versionArr, iv, ciphertext); byte[] tag = HmacSha256(macKey, macInput); byte[] combined = Concat(versionArr, iv, ciphertext, tag); return Base64UrlEncode(combined); // URL-safe & no padding } public static string Decrypt(string encrypted) { if (encrypted == null) throw new ArgumentNullException(nameof(encrypted)); byte[] combined; try { combined = Base64UrlDecode(encrypted); } catch { throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); } if (combined.Length < 1 + 16 + 32) throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); // Parse int offset = 0; byte version = combined[offset++]; if (version != 1) throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); byte[] iv = new byte[16]; Buffer.BlockCopy(combined, offset, iv, 0, iv.Length); offset += iv.Length; int tagLen = 32; int ciphertextLen = combined.Length - offset - tagLen; if (ciphertextLen <= 0) throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); byte[] ciphertext = new byte[ciphertextLen]; Buffer.BlockCopy(combined, offset, ciphertext, 0, ciphertextLen); offset += ciphertextLen; byte[] tag = new byte[tagLen]; Buffer.BlockCopy(combined, offset, tag, 0, tagLen); // Re-derive keys var masterKey = Convert.FromBase64String(MasterKeyBase64); if (masterKey.Length != 32) throw new InvalidOperationException("Master key must be 32 bytes (Base64 of 32 bytes)."); byte[] encKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("enc"), 32); byte[] macKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("mac"), 32); // Verify HMAC (constant-time) byte[] macInput = Concat(new[] { version }, iv, ciphertext); byte[] expectedTag = HmacSha256(macKey, macInput); if (!FixedTimeEquals(tag, expectedTag)) throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); // Decrypt using (var aes = CreateAes(encKey, iv)) using (var decryptor = aes.CreateDecryptor()) { try { byte[] pt = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length); return Encoding.UTF8.GetString(pt); } catch { // Covers padding/format errors throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); } } } // ======= CRYPTO PRIMITIVES ======= private static Aes CreateAes(byte[] key, byte[] iv) { // AesManaged/AesCryptoServiceProvider both exist on .NET 4.6.1; .NET 8 maps to platform crypto. var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.KeySize = 256; aes.BlockSize = 128; aes.Key = key; aes.IV = iv; return aes; } private static byte[] HmacSha256(byte[] key, byte[] data) { using (var h = new HMACSHA256(key)) { return h.ComputeHash(data); } } // Minimal HKDF-SHA256 (RFC 5869): we use only Expand with Extract(salt, IKM) private static byte[] HkdfSha256(byte[] ikm, byte[] salt, byte[] info, int length) { // Extract byte[] prk; using (var h = new HMACSHA256(salt ?? new byte[0])) { prk = h.ComputeHash(ikm); } // Expand int hashLen = 32; int n = (int)Math.Ceiling(length / (double)hashLen); byte[] okm = new byte[length]; byte[] t = new byte[0]; int offset = 0; using (var h = new HMACSHA256(prk)) { for (int i = 1; i <= n; i++) { // T(i) = HMAC-PRK(T(i-1) | info | i) h.Initialize(); h.TransformBlock(t, 0, t.Length, null, 0); if (info != null && info.Length > 0) h.TransformBlock(info, 0, info.Length, null, 0); var ctr = new[] { (byte)i }; h.TransformFinalBlock(ctr, 0, 1); t = h.Hash; h.Initialize(); int toCopy = Math.Min(hashLen, length - offset); Buffer.BlockCopy(t, 0, okm, offset, toCopy); offset += toCopy; } } return okm; } private static bool FixedTimeEquals(byte[] a, byte[] b) { if (a == null || b == null || a.Length != b.Length) return false; int diff = 0; for (int i = 0; i < a.Length; i++) diff |= a[i] ^ b[i]; return diff == 0; } // ======= BASE64URL HELPERS (no padding) ======= private static string Base64UrlEncode(byte[] data) { var s = Convert.ToBase64String(data) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); return s; } private static byte[] Base64UrlDecode(string s) { s = s.Replace('-', '+').Replace('_', '/'); switch (s.Length % 4) { case 2: s += "=="; break; case 3: s += "="; break; } return Convert.FromBase64String(s); } // ======= UTIL ======= private static byte[] Concat(params byte[][] arrays) { int len = 0; for (int i = 0; i < arrays.Length; i++) len += arrays[i].Length; var result = new byte[len]; int pos = 0; for (int i = 0; i < arrays.Length; i++) { Buffer.BlockCopy(arrays[i], 0, result, pos, arrays[i].Length); pos += arrays[i].Length; } return result; } } }