Files
SVSimServer/SVSim.BattleNode/Wire/NodeCrypto.cs

80 lines
3.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Security.Cryptography;
using System.Text;
namespace SVSim.BattleNode.Wire;
/// <summary>
/// AES-256-CBC encrypt/decrypt for the node socket channel. Port of
/// Cryptographer.EncryptRJ256ForNode / DecryptRJ256ForNode in the decompilation.
/// Key is prepended to ciphertext (cleartext); IV is the first 16 chars of the key.
/// </summary>
public static class NodeCrypto
{
/// <summary>
/// Generate a fresh 32-char key for server-initiated encryption.
/// Calls <paramref name="randHexDigit"/> 32 times; the result is masked with
/// <c>&amp; 0xF</c> so a misbehaving caller that returns a larger int still produces
/// exactly one hex digit per iteration (the internal contract is "32 hex chars").
/// The 32-char ASCII string is then base64-encoded and truncated to 32 chars.
/// </summary>
/// <remarks>
/// Differs from the client's <c>Cryptographer.generateKeyString</c> in input shape:
/// the client uses <c>Random.Next(0, 65535).ToString("x")</c> per iteration (14 hex
/// chars each). The output distribution is therefore different, but both produce a
/// valid 32-char UTF-8 AES-256 key — and the client never validates the server's key
/// since the server is decrypt-only in practice. Server-initiated encryption (e.g.
/// for <c>synchronize</c> pushes) uses this method.
/// </remarks>
public static string GenerateKey(Func<int> randHexDigit)
{
var sb = new StringBuilder(32);
for (var i = 0; i < 32; i++)
{
sb.Append((randHexDigit() & 0xF).ToString("x"));
}
var ascii = Encoding.ASCII.GetBytes(sb.ToString());
return Convert.ToBase64String(ascii).Substring(0, 32);
}
/// <summary>Encrypt: returns key + base64(AES-256-CBC(plain)).</summary>
public static string EncryptForNode(string plaintext, string key)
{
if (key.Length != 32)
throw new ArgumentException($"Key must be exactly 32 chars, got {key.Length}", nameof(key));
using var aes = BuildAes(key);
using var encryptor = aes.CreateEncryptor();
var plainBytes = Encoding.UTF8.GetBytes(plaintext);
var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
return key + Convert.ToBase64String(cipherBytes);
}
/// <summary>Decrypt: input[0..32] is key, input[32..] is base64(ciphertext).</summary>
public static string DecryptForNode(string encrypted)
{
if (encrypted.Length < 32)
throw new ArgumentException("Encrypted blob is shorter than the 32-char key prefix", nameof(encrypted));
var key = encrypted.Substring(0, 32);
var cipherBytes = Convert.FromBase64String(encrypted.Substring(32));
using var aes = BuildAes(key);
using var decryptor = aes.CreateDecryptor();
var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return Encoding.UTF8.GetString(plainBytes);
}
/// <summary>
/// Configure an AES-256-CBC instance with the node's IV derivation (first 16 chars
/// of the key, UTF-8). Callers own disposal. Assumes <paramref name="key"/> is the
/// 32-char ASCII key the encrypt / decrypt path has already validated.
/// </summary>
private static Aes BuildAes(string key)
{
var aes = Aes.Create();
aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = Encoding.UTF8.GetBytes(key);
aes.IV = Encoding.UTF8.GetBytes(key.Substring(0, 16));
return aes;
}
}