Skip to content

Commit 8d11294

Browse files
committed
some changes and fixes
1 parent b21a72b commit 8d11294

File tree

10 files changed

+1338
-22
lines changed

10 files changed

+1338
-22
lines changed

ModLoader/Classes/CryptoHelper.cs

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
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

Comments
 (0)