From 096546414838d9196831bac0e7064b37cc7a67a5 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Thu, 8 Sep 2016 17:45:04 +0100 Subject: [PATCH] Add crypto helpers for working with RSA keys. --- OpenRA.Game/CryptoUtil.cs | 217 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/OpenRA.Game/CryptoUtil.cs b/OpenRA.Game/CryptoUtil.cs index 219adca331..fb5304c12e 100644 --- a/OpenRA.Game/CryptoUtil.cs +++ b/OpenRA.Game/CryptoUtil.cs @@ -9,6 +9,7 @@ */ #endregion +using System; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -18,6 +19,222 @@ namespace OpenRA { public static class CryptoUtil { + // Fixed byte pattern for the OID header + static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 }; + + public static string EncodePEMPublicKey(RSAParameters parameters) + { + var data = Convert.ToBase64String(EncodePublicKey(parameters)); + var output = new StringBuilder(); + output.AppendLine("-----BEGIN PUBLIC KEY-----"); + for (var i = 0; i < data.Length; i += 64) + output.AppendLine(data.Substring(i, Math.Min(64, data.Length - i))); + output.Append("-----END PUBLIC KEY-----"); + + return output.ToString(); + } + + public static RSAParameters DecodePEMPublicKey(string key) + { + try + { + // Reconstruct original key data + var lines = key.Split('\n'); + var data = Convert.FromBase64String(lines.Skip(1).Take(lines.Length - 2).JoinWith("")); + + // Pull the modulus and exponent bytes out of the ASN.1 tree + // Expect this to blow up if the key is not correctly formatted + using (var s = new MemoryStream(data)) + { + // SEQUENCE + s.ReadByte(); + ReadTLVLength(s); + + // SEQUENCE -> fixed header junk + s.ReadByte(); + var headerLength = ReadTLVLength(s); + s.Position += headerLength; + + // SEQUENCE -> BIT_STRING + s.ReadByte(); + ReadTLVLength(s); + s.ReadByte(); + + // SEQUENCE -> BIT_STRING -> SEQUENCE + s.ReadByte(); + ReadTLVLength(s); + + // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus) + s.ReadByte(); + var modulusLength = ReadTLVLength(s); + s.ReadByte(); + var modulus = s.ReadBytes(modulusLength - 1); + + // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent) + s.ReadByte(); + var exponentLength = ReadTLVLength(s); + s.ReadByte(); + var exponent = s.ReadBytes(exponentLength - 1); + + return new RSAParameters + { + Modulus = modulus, + Exponent = exponent + }; + } + } + catch (Exception e) + { + throw new InvalidDataException("Invalid PEM public key", e); + } + } + + static byte[] EncodePublicKey(RSAParameters parameters) + { + using (var stream = new MemoryStream()) + { + var writer = new BinaryWriter(stream); + + var modExpLength = TripletFullLength(parameters.Modulus.Length + 1) + TripletFullLength(parameters.Exponent.Length + 1); + var bitStringLength = TripletFullLength(modExpLength + 1); + var sequenceLength = TripletFullLength(bitStringLength + OIDHeader.Length); + + // SEQUENCE + writer.Write((byte)0x30); + WriteTLVLength(writer, sequenceLength); + + // SEQUENCE -> fixed header junk + writer.Write(OIDHeader); + + // SEQUENCE -> BIT_STRING + writer.Write((byte)0x03); + WriteTLVLength(writer, bitStringLength); + writer.Write((byte)0x00); + + // SEQUENCE -> BIT_STRING -> SEQUENCE + writer.Write((byte)0x30); + WriteTLVLength(writer, modExpLength); + + // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER + // Modulus is padded with a zero to avoid issues with the sign bit + writer.Write((byte)0x02); + WriteTLVLength(writer, parameters.Modulus.Length + 1); + writer.Write((byte)0); + writer.Write(parameters.Modulus); + + // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER + // Exponent is padded with a zero to avoid issues with the sign bit + writer.Write((byte)0x02); + WriteTLVLength(writer, parameters.Exponent.Length + 1); + writer.Write((byte)0); + writer.Write(parameters.Exponent); + + return stream.ToArray(); + } + } + + static void WriteTLVLength(BinaryWriter writer, int length) + { + if (length < 0x80) + { + // Length < 128 is stored in a single byte + writer.Write((byte)length); + } + else + { + // If 128 <= length < 256**128 first byte encodes number of bytes required to hold the length + // High-bit is set as a flag to use this long-form encoding + var lengthBytes = BitConverter.GetBytes(length).Reverse().SkipWhile(b => b == 0).ToArray(); + writer.Write((byte)(0x80 | lengthBytes.Length)); + writer.Write(lengthBytes); + } + } + + static int ReadTLVLength(Stream s) + { + var length = s.ReadByte(); + if (length < 0x80) + return length; + + var data = new byte[4]; + s.ReadBytes(data, 0, Math.Min(length & 0x7F, 4)); + return BitConverter.ToInt32(data.ToArray(), 0); + } + + static int TripletFullLength(int dataLength) + { + if (dataLength < 0x80) + return 2 + dataLength; + + return 2 + dataLength + BitConverter.GetBytes(dataLength).Reverse().SkipWhile(b => b == 0).Count(); + } + + public static string DecryptString(RSAParameters parameters, string data) + { + try + { + using (var rsa = new RSACryptoServiceProvider()) + { + rsa.ImportParameters(parameters); + return Encoding.UTF8.GetString(rsa.Decrypt(Convert.FromBase64String(data), false)); + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to decrypt string with exception: {0}", e); + Console.WriteLine("String decryption failed: {0}", e); + return null; + } + } + + public static string Sign(RSAParameters parameters, string data) + { + return Sign(parameters, Encoding.UTF8.GetBytes(data)); + } + + public static string Sign(RSAParameters parameters, byte[] data) + { + try + { + using (var rsa = new RSACryptoServiceProvider()) + { + rsa.ImportParameters(parameters); + using (var csp = SHA1.Create()) + return Convert.ToBase64String(rsa.SignHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"))); + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to sign string with exception: {0}", e); + Console.WriteLine("String signing failed: {0}", e); + return null; + } + } + + public static bool VerifySignature(RSAParameters parameters, string data, string signature) + { + return VerifySignature(parameters, Encoding.UTF8.GetBytes(data), signature); + } + + public static bool VerifySignature(RSAParameters parameters, byte[] data, string signature) + { + try + { + using (var rsa = new RSACryptoServiceProvider()) + { + rsa.ImportParameters(parameters); + using (var csp = SHA1.Create()) + return rsa.VerifyHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature)); + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to verify signature with exception: {0}", e); + Console.WriteLine("Signature validation failed: {0}", e); + return false; + } + } + public static string SHA1Hash(Stream data) { using (var csp = SHA1.Create())