Also avoid ReadBytes calls that allocate a buffer by either updating the stream position (if not interested in the bytes), by reusing an input buffer (if interested in the bytes), or using a stackalloc buffer to avoid the allocation (for small reads).
296 lines
8.4 KiB
C#
296 lines
8.4 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
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 };
|
|
|
|
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
|
|
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
|
|
|
|
public static string PublicKeyFingerprint(RSAParameters parameters)
|
|
{
|
|
// Public key fingerprint is defined as the SHA1 of the modulus + exponent bytes
|
|
return SHA1Hash(parameters.Modulus.Append(parameters.Exponent).ToArray());
|
|
}
|
|
|
|
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.ReadUInt8();
|
|
ReadTLVLength(s);
|
|
|
|
// SEQUENCE -> fixed header junk
|
|
s.ReadUInt8();
|
|
var headerLength = ReadTLVLength(s);
|
|
s.Position += headerLength;
|
|
|
|
// SEQUENCE -> BIT_STRING
|
|
s.ReadUInt8();
|
|
ReadTLVLength(s);
|
|
s.ReadUInt8();
|
|
|
|
// SEQUENCE -> BIT_STRING -> SEQUENCE
|
|
s.ReadUInt8();
|
|
ReadTLVLength(s);
|
|
|
|
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus)
|
|
s.ReadUInt8();
|
|
var modulusLength = ReadTLVLength(s);
|
|
s.ReadUInt8();
|
|
var modulus = s.ReadBytes(modulusLength - 1);
|
|
|
|
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent)
|
|
s.ReadUInt8();
|
|
var exponentLength = ReadTLVLength(s);
|
|
s.ReadUInt8();
|
|
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.ReadUInt8();
|
|
if (length < 0x80)
|
|
return length;
|
|
|
|
Span<byte> data = stackalloc byte[4];
|
|
s.ReadBytes(data[..Math.Min(length & 0x7F, 4)]);
|
|
return BitConverter.ToInt32(data);
|
|
}
|
|
|
|
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:");
|
|
Log.Write("debug", e);
|
|
Console.WriteLine("String decryption failed:");
|
|
Console.WriteLine(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");
|
|
Log.Write("debug", e);
|
|
Console.WriteLine("String signing failed:");
|
|
Console.WriteLine(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:");
|
|
Log.Write("debug", e);
|
|
Console.WriteLine("Signature validation failed:");
|
|
Console.WriteLine(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static string SHA1Hash(Stream data)
|
|
{
|
|
using var csp = SHA1.Create();
|
|
return ToHex(csp.ComputeHash(data), true);
|
|
}
|
|
|
|
public static string SHA1Hash(byte[] data)
|
|
{
|
|
using var csp = SHA1.Create();
|
|
return ToHex(csp.ComputeHash(data), true);
|
|
}
|
|
|
|
public static string SHA1Hash(string data)
|
|
{
|
|
return SHA1Hash(Encoding.UTF8.GetBytes(data));
|
|
}
|
|
|
|
public static string ToHex(ReadOnlySpan<byte> source, bool lowerCase = false)
|
|
{
|
|
if (source.Length == 0)
|
|
return string.Empty;
|
|
|
|
// excessively avoid stack overflow if source is too large (considering that we're allocating a new string)
|
|
var buffer = source.Length <= 256 ? stackalloc char[source.Length * 2] : new char[source.Length * 2];
|
|
return ToHexInternal(source, buffer, lowerCase);
|
|
}
|
|
|
|
static string ToHexInternal(ReadOnlySpan<byte> source, Span<char> buffer, bool lowerCase)
|
|
{
|
|
var sourceIndex = 0;
|
|
var alphabet = lowerCase ? HexLowerAlphabet : HexUpperAlphabet;
|
|
|
|
for (var i = 0; i < buffer.Length; i += 2)
|
|
{
|
|
var b = source[sourceIndex++];
|
|
buffer[i] = alphabet[b >> 4];
|
|
buffer[i + 1] = alphabet[b & 0xF];
|
|
}
|
|
|
|
return new string(buffer);
|
|
}
|
|
}
|
|
}
|