using System.Security.Cryptography; using System.Text; namespace SVSim.BattleNode.Wire; /// /// 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. /// public static class NodeCrypto { /// Length of the ASCII key, in chars (AES-256 = 32 bytes = 32 ASCII chars). private const int KeyLength = 32; /// IV length, in chars. The node derives the IV from the first half of the key. private const int IvLength = KeyLength / 2; /// /// Generate a fresh 32-char key for server-initiated encryption. /// Calls 32 times; the result is masked with /// & 0xF 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. /// /// /// Differs from the client's Cryptographer.generateKeyString in input shape: /// the client uses Random.Next(0, 65535).ToString("x") per iteration (1–4 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 synchronize pushes) uses this method. /// public static string GenerateKey(Func randHexDigit) { var sb = new StringBuilder(KeyLength); for (var i = 0; i < KeyLength; i++) { sb.Append((randHexDigit() & 0xF).ToString("x")); } var ascii = Encoding.ASCII.GetBytes(sb.ToString()); return Convert.ToBase64String(ascii).Substring(0, KeyLength); } /// Encrypt: returns key + base64(AES-256-CBC(plain)). public static string EncryptForNode(string plaintext, string key) { if (key.Length != KeyLength) throw new ArgumentException($"Key must be exactly {KeyLength} 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); } /// Decrypt: input[0..32] is key, input[32..] is base64(ciphertext). public static string DecryptForNode(string encrypted) { if (encrypted.Length < KeyLength) throw new ArgumentException($"Encrypted blob is shorter than the {KeyLength}-char key prefix", nameof(encrypted)); var key = encrypted.Substring(0, KeyLength); var cipherBytes = Convert.FromBase64String(encrypted.Substring(KeyLength)); using var aes = BuildAes(key); using var decryptor = aes.CreateDecryptor(); var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); return Encoding.UTF8.GetString(plainBytes); } /// /// Configure an AES-256-CBC instance with the node's IV derivation (first /// chars of the key, UTF-8). Callers own disposal. Assumes /// is the -char ASCII key the encrypt / /// decrypt path has already validated. /// 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, IvLength)); return aes; } }