|
| 1 | +using ModLoader; |
| 2 | +using System; |
| 3 | +using System.Diagnostics; |
| 4 | +using System.Reflection.Metadata; |
| 5 | +using System.Security.Cryptography; |
| 6 | +using System.Text; |
| 7 | +using System.Text.Json; |
| 8 | + |
| 9 | +namespace DCTS.Classes |
| 10 | +{ |
| 11 | + public class CryptoHelper |
| 12 | + { |
| 13 | + public static CryptoHelper instance { get; private set; } |
| 14 | + public CryptoHelper() |
| 15 | + { |
| 16 | + instance = this; |
| 17 | + } |
| 18 | + |
| 19 | + /* for curious people: |
| 20 | + RSA-2048 (OAEP-SHA1) encrypts the key |
| 21 | + AES-256-GCM encrypts the message |
| 22 | + PBKDF2-SHA256 makes a key from a password |
| 23 | + RSA-SHA256-PKCS1 signs and verifies data |
| 24 | + */ |
| 25 | + |
| 26 | + |
| 27 | + private readonly string KeyFilePath = Path.Combine(Form1.appPath, "privatekey.json"); |
| 28 | + |
| 29 | + public (string PrivateKey, string PublicKey) EnsureKeyPair() |
| 30 | + { |
| 31 | + if (File.Exists(KeyFilePath)) |
| 32 | + { |
| 33 | + var json = File.ReadAllText(KeyFilePath); |
| 34 | + var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json); |
| 35 | + if (data != null && data.ContainsKey("PrivateKey")) |
| 36 | + { |
| 37 | + using var rsa = RSA.Create(); |
| 38 | + rsa.ImportFromPem(data["PrivateKey"].AsSpan()); |
| 39 | + string pub = ExportPublicKey(rsa); |
| 40 | + return (data["PrivateKey"], pub); |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + using var newRsa = RSA.Create(2048); |
| 45 | + string privatePem = ExportPrivateKey(newRsa); |
| 46 | + string publicPem = ExportPublicKey(newRsa); |
| 47 | + File.WriteAllText(KeyFilePath, JsonSerializer.Serialize(new { PrivateKey = privatePem }, new JsonSerializerOptions { WriteIndented = true })); |
| 48 | + return (privatePem, publicPem); |
| 49 | + } |
| 50 | + |
| 51 | + public string EncodeBase64(string value) |
| 52 | + { |
| 53 | + var valueBytes = Encoding.UTF8.GetBytes(value); |
| 54 | + return Convert.ToBase64String(valueBytes); |
| 55 | + } |
| 56 | + |
| 57 | + public string DecodeBase64(string value) |
| 58 | + { |
| 59 | + var valueBytes = Convert.FromBase64String(value); |
| 60 | + return Encoding.UTF8.GetString(valueBytes); |
| 61 | + } |
| 62 | + |
| 63 | + public string EncodeURIComponent(string value) |
| 64 | + { |
| 65 | + return Uri.EscapeDataString(value); |
| 66 | + } |
| 67 | + |
| 68 | + public string DecodeURIComponent(string value) |
| 69 | + { |
| 70 | + return Uri.UnescapeDataString(value); |
| 71 | + } |
| 72 | + |
| 73 | + public string EncodeToBase64(string value) |
| 74 | + { |
| 75 | + return EncodeBase64(EncodeURIComponent(value)); |
| 76 | + } |
| 77 | + |
| 78 | + public string DecodeFromBase64(string value) |
| 79 | + { |
| 80 | + return DecodeURIComponent(DecodeBase64(value)); |
| 81 | + } |
| 82 | + |
| 83 | + public string GetPrivateKey() |
| 84 | + { |
| 85 | + var pair = EnsureKeyPair(); |
| 86 | + return pair.PrivateKey; |
| 87 | + } |
| 88 | + |
| 89 | + public string GetPublicKey() |
| 90 | + { |
| 91 | + var pair = EnsureKeyPair(); |
| 92 | + return pair.PublicKey; |
| 93 | + } |
| 94 | + |
| 95 | + private static string ExportPrivateKey(RSA rsa) |
| 96 | + { |
| 97 | + var priv = rsa.ExportPkcs8PrivateKey(); |
| 98 | + return PemEncode("PRIVATE KEY", priv); |
| 99 | + } |
| 100 | + |
| 101 | + private static string ExportPublicKey(RSA rsa) |
| 102 | + { |
| 103 | + var pub = rsa.ExportSubjectPublicKeyInfo(); |
| 104 | + return PemEncode("PUBLIC KEY", pub); |
| 105 | + } |
| 106 | + |
| 107 | + private static string PemEncode(string label, byte[] data) |
| 108 | + { |
| 109 | + string base64 = Convert.ToBase64String(data, Base64FormattingOptions.InsertLineBreaks); |
| 110 | + return $"-----BEGIN {label}-----\n{base64}\n-----END {label}-----"; |
| 111 | + } |
| 112 | + |
| 113 | + public string EncryptEnvelope(string plaintext, string recipientPemOrPassword) |
| 114 | + { |
| 115 | + byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext); |
| 116 | + byte[] aesKey; |
| 117 | + string result; |
| 118 | + |
| 119 | + if (recipientPemOrPassword.Contains("BEGIN PUBLIC KEY")) |
| 120 | + { |
| 121 | + aesKey = RandomBytes(32); |
| 122 | + |
| 123 | + using var rsa = RSA.Create(); |
| 124 | + rsa.ImportFromPem(recipientPemOrPassword.AsSpan()); |
| 125 | + byte[] encKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA1); |
| 126 | + |
| 127 | + var cipherBytes = EncryptAes(plainBytes, aesKey, out var iv, out var tag); |
| 128 | + |
| 129 | + result = string.Join("|", |
| 130 | + "rsa", |
| 131 | + Convert.ToBase64String(encKey), |
| 132 | + "", |
| 133 | + Convert.ToBase64String(cipherBytes), |
| 134 | + Convert.ToBase64String(iv), |
| 135 | + Convert.ToBase64String(tag) |
| 136 | + ); |
| 137 | + } |
| 138 | + else |
| 139 | + { |
| 140 | + byte[] salt = RandomBytes(16); |
| 141 | + using var derive = new Rfc2898DeriveBytes(recipientPemOrPassword, salt, 100000, HashAlgorithmName.SHA256); |
| 142 | + aesKey = derive.GetBytes(32); |
| 143 | + |
| 144 | + var cipherBytes = EncryptAes(plainBytes, aesKey, out var iv, out var tag); |
| 145 | + |
| 146 | + // method|""|salt|cipher|iv|tag |
| 147 | + result = string.Join("|", |
| 148 | + "password", |
| 149 | + "", |
| 150 | + Convert.ToBase64String(salt), |
| 151 | + Convert.ToBase64String(cipherBytes), |
| 152 | + Convert.ToBase64String(iv), |
| 153 | + Convert.ToBase64String(tag) |
| 154 | + ); |
| 155 | + } |
| 156 | + |
| 157 | + return result; |
| 158 | + } |
| 159 | + |
| 160 | + |
| 161 | + public string DecryptEnvelope(string method, string encKey, string iv, string tag, string ciphertext, string privateKeyPem) |
| 162 | + { |
| 163 | + try |
| 164 | + { |
| 165 | + byte[] aesKey; |
| 166 | + |
| 167 | + if (method == "rsa") |
| 168 | + { |
| 169 | + byte[] encKeyBytes = Convert.FromBase64String(encKey); |
| 170 | + using var rsa = RSA.Create(); |
| 171 | + rsa.ImportFromPem(privateKeyPem.AsSpan()); |
| 172 | + aesKey = rsa.Decrypt(encKeyBytes, RSAEncryptionPadding.OaepSHA1); |
| 173 | + } |
| 174 | + else if (method == "password") |
| 175 | + { |
| 176 | + throw new Exception("password mode not supported in this overload"); |
| 177 | + } |
| 178 | + else throw new Exception("unsupported method"); |
| 179 | + |
| 180 | + byte[] ivBytes = Convert.FromBase64String(iv); |
| 181 | + byte[] tagBytes = Convert.FromBase64String(tag); |
| 182 | + byte[] cipherBytes = Convert.FromBase64String(ciphertext); |
| 183 | + |
| 184 | + byte[] plainBytes = new byte[cipherBytes.Length]; |
| 185 | + using var aes = new AesGcm(aesKey); |
| 186 | + aes.Decrypt(ivBytes, cipherBytes, tagBytes, plainBytes); |
| 187 | + |
| 188 | + return Encoding.UTF8.GetString(plainBytes); |
| 189 | + } |
| 190 | + catch (Exception ex) |
| 191 | + { |
| 192 | + Logger.Log("Cant decrypt data"); |
| 193 | + Logger.Log(ex.Message); |
| 194 | + return ""; |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + |
| 199 | + private static byte[] EncryptAes(byte[] plaintext, byte[] key, out byte[] iv, out byte[] tag) |
| 200 | + { |
| 201 | + iv = RandomBytes(12); |
| 202 | + tag = new byte[16]; |
| 203 | + byte[] cipher = new byte[plaintext.Length]; |
| 204 | + using var aes = new AesGcm(key); |
| 205 | + aes.Encrypt(iv, plaintext, cipher, tag); |
| 206 | + return cipher; |
| 207 | + } |
| 208 | + |
| 209 | + private static byte[] DecryptAes(byte[] ciphertext, byte[] key, byte[] iv, byte[] tag) |
| 210 | + { |
| 211 | + byte[] plain = new byte[ciphertext.Length]; |
| 212 | + using var aes = new AesGcm(key); |
| 213 | + aes.Decrypt(iv, ciphertext, tag, plain); |
| 214 | + return plain; |
| 215 | + } |
| 216 | + |
| 217 | + private static byte[] RandomBytes(int len) |
| 218 | + { |
| 219 | + byte[] b = new byte[len]; |
| 220 | + RandomNumberGenerator.Fill(b); |
| 221 | + return b; |
| 222 | + } |
| 223 | + |
| 224 | + |
| 225 | + public bool VerifyJson(string json, string publicKeyPem) |
| 226 | + { |
| 227 | + using var doc = JsonDocument.Parse(json); |
| 228 | + var element = doc.RootElement; |
| 229 | + if (element.ValueKind != JsonValueKind.Object) return false; |
| 230 | + |
| 231 | + var dict = element.EnumerateObject() |
| 232 | + .ToDictionary(p => p.Name, p => Canonicalize(p.Value)); |
| 233 | + |
| 234 | + if (!dict.ContainsKey("sig")) return false; |
| 235 | + string sig = dict["sig"]?.ToString() ?? ""; |
| 236 | + dict.Remove("sig"); |
| 237 | + |
| 238 | + return VerifyData(dict, sig, publicKeyPem); |
| 239 | + } |
| 240 | + |
| 241 | + public object Canonicalize(JsonElement element) |
| 242 | + { |
| 243 | + return element.ValueKind switch |
| 244 | + { |
| 245 | + JsonValueKind.Object => element.EnumerateObject() |
| 246 | + .OrderBy(p => p.Name) |
| 247 | + .ToDictionary(p => p.Name, p => Canonicalize(p.Value)), |
| 248 | + JsonValueKind.Array => element.EnumerateArray().Select(Canonicalize).ToList(), |
| 249 | + JsonValueKind.String => element.GetString(), |
| 250 | + JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(), |
| 251 | + JsonValueKind.True => true, |
| 252 | + JsonValueKind.False => false, |
| 253 | + JsonValueKind.Null => null, |
| 254 | + _ => element.ToString() |
| 255 | + }; |
| 256 | + } |
| 257 | + |
| 258 | + public string StableStringify(object data) |
| 259 | + { |
| 260 | + string json = data is string s ? s : JsonSerializer.Serialize(data); |
| 261 | + using var doc = JsonDocument.Parse(json); |
| 262 | + var canonical = Canonicalize(doc.RootElement); |
| 263 | + return JsonSerializer.Serialize(canonical, new JsonSerializerOptions |
| 264 | + { |
| 265 | + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping |
| 266 | + }); |
| 267 | + } |
| 268 | + |
| 269 | + public string SignData(object data, string privateKeyPem) |
| 270 | + { |
| 271 | + string payload = StableStringify(data); |
| 272 | + using var rsa = RSA.Create(); |
| 273 | + rsa.ImportFromPem(privateKeyPem.AsSpan()); |
| 274 | + byte[] bytes = Encoding.UTF8.GetBytes(payload); |
| 275 | + byte[] sig = rsa.SignData(bytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); |
| 276 | + return Convert.ToBase64String(sig); |
| 277 | + } |
| 278 | + |
| 279 | + public bool VerifyData(object data, string signatureBase64, string publicKeyPem) |
| 280 | + { |
| 281 | + string payload = StableStringify(data); |
| 282 | + using var rsa = RSA.Create(); |
| 283 | + rsa.ImportFromPem(publicKeyPem.AsSpan()); |
| 284 | + byte[] bytes = Encoding.UTF8.GetBytes(payload); |
| 285 | + byte[] sig = Convert.FromBase64String(signatureBase64); |
| 286 | + return rsa.VerifyData(bytes, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); |
| 287 | + } |
| 288 | + |
| 289 | + public string SignString(string text, string key) |
| 290 | + { |
| 291 | + if (!Form1.HandleArgs(text)) return ""; |
| 292 | + |
| 293 | + if (key == null) |
| 294 | + { |
| 295 | + Logger.Log($"Cant sign string {text} because no key supplied"); |
| 296 | + Debug.WriteLine($"Cant sign string {text} because no key supplied"); |
| 297 | + return ""; |
| 298 | + } |
| 299 | + |
| 300 | + using var rsa = RSA.Create(); |
| 301 | + rsa.ImportFromPem(key.AsSpan()); |
| 302 | + byte[] bytes = Encoding.UTF8.GetBytes(text); |
| 303 | + byte[] sig = rsa.SignData(bytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); |
| 304 | + return Convert.ToBase64String(sig); |
| 305 | + } |
| 306 | + |
| 307 | + public bool VerifyString(string text, string signatureBase64, string publicKeyPem) |
| 308 | + { |
| 309 | + if (!Form1.HandleArgs(text, signatureBase64, publicKeyPem)) return false; |
| 310 | + |
| 311 | + using var rsa = RSA.Create(); |
| 312 | + rsa.ImportFromPem(publicKeyPem.AsSpan()); |
| 313 | + byte[] bytes = Encoding.UTF8.GetBytes(text); |
| 314 | + byte[] sig = Convert.FromBase64String(signatureBase64); |
| 315 | + return rsa.VerifyData(bytes, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); |
| 316 | + } |
| 317 | + |
| 318 | + |
| 319 | + public string SignJson(string json, string privateKeyPem = null) |
| 320 | + { |
| 321 | + if (!Form1.HandleArgs(json)) return ""; |
| 322 | + |
| 323 | + if (privateKeyPem == null) |
| 324 | + { |
| 325 | + var pair = EnsureKeyPair(); |
| 326 | + privateKeyPem = pair.PrivateKey; |
| 327 | + } |
| 328 | + |
| 329 | + using var doc = JsonDocument.Parse(json); |
| 330 | + var canonical = Canonicalize(doc.RootElement); |
| 331 | + |
| 332 | + var canonicalJson = JsonSerializer.Serialize(canonical, new JsonSerializerOptions |
| 333 | + { |
| 334 | + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
| 335 | + WriteIndented = false |
| 336 | + }); |
| 337 | + |
| 338 | + var sig = SignData(canonicalJson, privateKeyPem); |
| 339 | + |
| 340 | + using var original = JsonDocument.Parse(json); |
| 341 | + var dict = original.RootElement.EnumerateObject() |
| 342 | + .ToDictionary(p => p.Name, p => Canonicalize(p.Value)); |
| 343 | + |
| 344 | + dict["sig"] = sig; |
| 345 | + return JsonSerializer.Serialize(dict, new JsonSerializerOptions |
| 346 | + { |
| 347 | + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
| 348 | + WriteIndented = false |
| 349 | + }); |
| 350 | + } |
| 351 | + } |
| 352 | +} |
0 commit comments