aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Web/Tango.Portal/Utils/SimpleCryptoHelper.cs
blob: bcd0281f4b1a29620d6351957cd77bdaf66a2325 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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;
        }
    }
}