531 lines
19 KiB
C#
531 lines
19 KiB
C#
//**********************************************************************************
|
|
//
|
|
//OpenSSLKey
|
|
// .NET 2.0 OpenSSL Public & Private Key Parser
|
|
//
|
|
// Copyright (C) 2008 JavaScience Consulting
|
|
//
|
|
//***********************************************************************************
|
|
//
|
|
// opensslkey.cs
|
|
//
|
|
// Reads and parses:
|
|
// (1) OpenSSL PEM or DER public keys
|
|
// (2) OpenSSL PEM or DER traditional SSLeay private keys (encrypted and unencrypted)
|
|
// (3) PKCS #8 PEM or DER encoded private keys (encrypted and unencrypted)
|
|
// Keys in PEM format must have headers/footers .
|
|
// Encrypted Private Key in SSLEay format not supported in DER
|
|
// Removes header/footer lines.
|
|
// For traditional SSLEAY PEM private keys, checks for encrypted format and
|
|
// uses PBE to extract 3DES key.
|
|
// For SSLEAY format, only supports encryption format: DES-EDE3-CBC
|
|
// For PKCS #8, only supports PKCS#5 v2.0 3des.
|
|
// Parses private and public key components and returns .NET RSA object.
|
|
// Creates dummy unsigned certificate linked to private keypair and
|
|
// optionally exports to pkcs #12
|
|
//
|
|
// See also:
|
|
// http://www.openssl.org/docs/crypto/pem.html#PEM_ENCRYPTION_FORMAT
|
|
//**************************************************************************************
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
|
|
namespace DearFTP.Utils
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct CRYPT_KEY_PROV_INFO
|
|
{
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string pwszContainerName;
|
|
[MarshalAs(UnmanagedType.LPWStr)] public string pwszProvName;
|
|
public uint dwProvType;
|
|
public uint dwFlags;
|
|
public uint cProvParam;
|
|
public IntPtr rgProvParam;
|
|
public uint dwKeySpec;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct CERT_NAME_BLOB
|
|
{
|
|
public int cbData;
|
|
public IntPtr pbData;
|
|
}
|
|
|
|
public class OpenSslKey
|
|
{
|
|
//-------- Get the binary PKCS #8 PRIVATE key --------
|
|
public static byte[] DecodePkcs8PrivateKey(string instr)
|
|
{
|
|
const string pemp8header = "-----BEGIN PRIVATE KEY-----";
|
|
const string pemp8footer = "-----END PRIVATE KEY-----";
|
|
string pemstr = instr.Trim();
|
|
byte[] binkey;
|
|
if (!pemstr.StartsWith(pemp8header) || !pemstr.EndsWith(pemp8footer))
|
|
return null;
|
|
StringBuilder sb = new StringBuilder(pemstr);
|
|
sb.Replace(pemp8header, ""); //remove headers/footers, if present
|
|
sb.Replace(pemp8footer, "");
|
|
|
|
string pubstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace
|
|
|
|
try
|
|
{
|
|
binkey = Convert.FromBase64String(pubstr);
|
|
}
|
|
catch (FormatException)
|
|
{ //if can't b64 decode, data is not valid
|
|
return null;
|
|
}
|
|
return binkey;
|
|
}
|
|
|
|
//------- Parses binary asn.1 PKCS #8 PrivateKeyInfo; returns RSACryptoServiceProvider ---
|
|
public static RSACryptoServiceProvider DecodePrivateKeyInfo(byte[] pkcs8)
|
|
{
|
|
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
|
|
// this byte[] includes the sequence byte and terminal encoded null
|
|
byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
|
|
byte[] seq;
|
|
|
|
// --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------
|
|
MemoryStream mem = new MemoryStream(pkcs8);
|
|
int lenstream = (int)mem.Length;
|
|
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
|
|
byte bt;
|
|
ushort twobytes;
|
|
|
|
try
|
|
{
|
|
|
|
twobytes = binr.ReadUInt16();
|
|
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
|
|
binr.ReadByte(); //advance 1 byte
|
|
else if (twobytes == 0x8230)
|
|
binr.ReadInt16(); //advance 2 bytes
|
|
else
|
|
return null;
|
|
|
|
|
|
bt = binr.ReadByte();
|
|
if (bt != 0x02)
|
|
return null;
|
|
|
|
twobytes = binr.ReadUInt16();
|
|
|
|
if (twobytes != 0x0001)
|
|
return null;
|
|
|
|
seq = binr.ReadBytes(15); //read the Sequence OID
|
|
if (!CompareBytearrays(seq, SeqOID)) //make sure Sequence for OID is correct
|
|
return null;
|
|
|
|
bt = binr.ReadByte();
|
|
if (bt != 0x04) //expect an Octet string
|
|
return null;
|
|
|
|
bt = binr.ReadByte(); //read next byte, or next 2 bytes is 0x81 or 0x82; otherwise bt is the byte count
|
|
if (bt == 0x81)
|
|
binr.ReadByte();
|
|
else
|
|
if (bt == 0x82)
|
|
binr.ReadUInt16();
|
|
//------ at this stage, the remaining sequence should be the RSA private key
|
|
|
|
byte[] rsaprivkey = binr.ReadBytes((int)(lenstream - mem.Position));
|
|
RSACryptoServiceProvider rsacsp = DecodeRSAPrivateKey(rsaprivkey);
|
|
return rsacsp;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
binr.Close();
|
|
}
|
|
}
|
|
|
|
//-------- Get the binary PKCS #8 Encrypted PRIVATE key --------
|
|
public static byte[] DecodePkcs8EncPrivateKey(string instr)
|
|
{
|
|
const string pemp8encheader = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
|
|
const string pemp8encfooter = "-----END ENCRYPTED PRIVATE KEY-----";
|
|
string pemstr = instr.Trim();
|
|
byte[] binkey;
|
|
if (!pemstr.StartsWith(pemp8encheader) || !pemstr.EndsWith(pemp8encfooter))
|
|
return null;
|
|
StringBuilder sb = new StringBuilder(pemstr);
|
|
sb.Replace(pemp8encheader, ""); //remove headers/footers, if present
|
|
sb.Replace(pemp8encfooter, "");
|
|
|
|
string pubstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace
|
|
|
|
try
|
|
{
|
|
binkey = Convert.FromBase64String(pubstr);
|
|
}
|
|
catch (System.FormatException)
|
|
{ //if can't b64 decode, data is not valid
|
|
return null;
|
|
}
|
|
return binkey;
|
|
}
|
|
|
|
//------- Parses binary asn.1 EncryptedPrivateKeyInfo; returns RSACryptoServiceProvider ---
|
|
public static RSACryptoServiceProvider DecodeEncryptedPrivateKeyInfo(byte[] encpkcs8)
|
|
{
|
|
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
|
|
// this byte[] includes the sequence byte and terminal encoded null
|
|
byte[] OIDpkcs5PBES2 = { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x0D };
|
|
byte[] OIDpkcs5PBKDF2 = { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x0C };
|
|
byte[] OIDdesEDE3CBC = { 0x06, 0x08, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x03, 0x07 };
|
|
byte[] seqdes;
|
|
byte[] seq;
|
|
byte[] salt;
|
|
byte[] IV;
|
|
byte[] encryptedpkcs8;
|
|
byte[] pkcs8;
|
|
|
|
int saltsize, ivsize, encblobsize;
|
|
int iterations;
|
|
|
|
// --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------
|
|
MemoryStream mem = new MemoryStream(encpkcs8);
|
|
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
|
|
byte bt;
|
|
ushort twobytes;
|
|
|
|
try
|
|
{
|
|
|
|
twobytes = binr.ReadUInt16();
|
|
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
|
|
binr.ReadByte(); //advance 1 byte
|
|
else if (twobytes == 0x8230)
|
|
binr.ReadInt16(); //advance 2 bytes
|
|
else
|
|
return null;
|
|
|
|
twobytes = binr.ReadUInt16(); //inner sequence
|
|
if (twobytes == 0x8130)
|
|
binr.ReadByte();
|
|
else if (twobytes == 0x8230)
|
|
binr.ReadInt16();
|
|
|
|
|
|
seq = binr.ReadBytes(11); //read the Sequence OID
|
|
if (!CompareBytearrays(seq, OIDpkcs5PBES2)) //is it a OIDpkcs5PBES2 ?
|
|
return null;
|
|
|
|
twobytes = binr.ReadUInt16(); //inner sequence for pswd salt
|
|
if (twobytes == 0x8130)
|
|
binr.ReadByte();
|
|
else if (twobytes == 0x8230)
|
|
binr.ReadInt16();
|
|
|
|
twobytes = binr.ReadUInt16(); //inner sequence for pswd salt
|
|
if (twobytes == 0x8130)
|
|
binr.ReadByte();
|
|
else if (twobytes == 0x8230)
|
|
binr.ReadInt16();
|
|
|
|
seq = binr.ReadBytes(11); //read the Sequence OID
|
|
if (!CompareBytearrays(seq, OIDpkcs5PBKDF2)) //is it a OIDpkcs5PBKDF2 ?
|
|
return null;
|
|
|
|
twobytes = binr.ReadUInt16();
|
|
if (twobytes == 0x8130)
|
|
binr.ReadByte();
|
|
else if (twobytes == 0x8230)
|
|
binr.ReadInt16();
|
|
|
|
bt = binr.ReadByte();
|
|
if (bt != 0x04) //expect octet string for salt
|
|
return null;
|
|
saltsize = binr.ReadByte();
|
|
salt = binr.ReadBytes(saltsize);
|
|
|
|
bt = binr.ReadByte();
|
|
if (bt != 0x02) //expect an integer for PBKF2 interation count
|
|
return null;
|
|
|
|
int itbytes = binr.ReadByte(); //PBKD2 iterations should fit in 2 bytes.
|
|
if (itbytes == 1)
|
|
iterations = binr.ReadByte();
|
|
else if (itbytes == 2)
|
|
iterations = 256 * binr.ReadByte() + binr.ReadByte();
|
|
else
|
|
return null;
|
|
|
|
twobytes = binr.ReadUInt16();
|
|
if (twobytes == 0x8130)
|
|
binr.ReadByte();
|
|
else if (twobytes == 0x8230)
|
|
binr.ReadInt16();
|
|
|
|
seqdes = binr.ReadBytes(10); //read the Sequence OID
|
|
if (!CompareBytearrays(seqdes, OIDdesEDE3CBC)) //is it a OIDdes-EDE3-CBC ?
|
|
return null;
|
|
|
|
bt = binr.ReadByte();
|
|
if (bt != 0x04) //expect octet string for IV
|
|
return null;
|
|
|
|
ivsize = binr.ReadByte(); // IV byte size should fit in one byte (24 expected for 3DES)
|
|
IV = binr.ReadBytes(ivsize);
|
|
|
|
bt = binr.ReadByte();
|
|
if (bt != 0x04) // expect octet string for encrypted PKCS8 data
|
|
return null;
|
|
|
|
bt = binr.ReadByte();
|
|
|
|
if (bt == 0x81)
|
|
encblobsize = binr.ReadByte(); // data size in next byte
|
|
else if (bt == 0x82)
|
|
encblobsize = 256 * binr.ReadByte() + binr.ReadByte();
|
|
else
|
|
encblobsize = bt; // we already have the data size
|
|
|
|
encryptedpkcs8 = binr.ReadBytes(encblobsize);
|
|
|
|
SecureString secpswd = GetSecPswd("Enter password for Encrypted PKCS #8 ==>");
|
|
pkcs8 = DecryptPBDK2(encryptedpkcs8, salt, IV, secpswd, iterations);
|
|
|
|
if (pkcs8 == null) // probably a bad pswd entered.
|
|
return null;
|
|
|
|
//----- With a decrypted pkcs #8 PrivateKeyInfo blob, decode it to an RSA ---
|
|
RSACryptoServiceProvider rsa = DecodePrivateKeyInfo(pkcs8);
|
|
|
|
return rsa;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
binr.Close();
|
|
}
|
|
}
|
|
|
|
// ------ Uses PBKD2 to derive a 3DES key and decrypts data --------
|
|
public static byte[] DecryptPBDK2(byte[] edata, byte[] salt, byte[] IV, SecureString secpswd, int iterations)
|
|
{
|
|
byte[] psbytes = new byte[secpswd.Length];
|
|
IntPtr unmanagedPswd = Marshal.SecureStringToGlobalAllocAnsi(secpswd);
|
|
Marshal.Copy(unmanagedPswd, psbytes, 0, psbytes.Length);
|
|
Marshal.ZeroFreeGlobalAllocAnsi(unmanagedPswd);
|
|
|
|
try
|
|
{
|
|
Rfc2898DeriveBytes kd = new Rfc2898DeriveBytes(psbytes, salt, iterations);
|
|
TripleDES decAlg = TripleDES.Create();
|
|
decAlg.Key = kd.GetBytes(24);
|
|
decAlg.IV = IV;
|
|
MemoryStream memstr = new MemoryStream();
|
|
CryptoStream decrypt = new CryptoStream(memstr, decAlg.CreateDecryptor(), CryptoStreamMode.Write);
|
|
decrypt.Write(edata, 0, edata.Length);
|
|
decrypt.Flush();
|
|
decrypt.Close(); // this is REQUIRED.
|
|
byte[] cleartext = memstr.ToArray();
|
|
return cleartext;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine("Problem decrypting: {0}", e.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
//------- Parses binary ans.1 RSA private key; returns RSACryptoServiceProvider ---
|
|
public static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey)
|
|
{
|
|
byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;
|
|
|
|
// --------- Set up stream to decode the asn.1 encoded RSA private key ------
|
|
MemoryStream mem = new MemoryStream(privkey);
|
|
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
|
|
byte bt;
|
|
ushort twobytes;
|
|
int elems;
|
|
|
|
try
|
|
{
|
|
twobytes = binr.ReadUInt16();
|
|
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
|
|
binr.ReadByte(); //advance 1 byte
|
|
else if (twobytes == 0x8230)
|
|
binr.ReadInt16(); //advance 2 bytes
|
|
else
|
|
return null;
|
|
|
|
twobytes = binr.ReadUInt16();
|
|
if (twobytes != 0x0102) //version number
|
|
return null;
|
|
bt = binr.ReadByte();
|
|
if (bt != 0x00)
|
|
return null;
|
|
|
|
|
|
//------ all private key components are Integer sequences ----
|
|
elems = GetIntegerSize(binr);
|
|
MODULUS = binr.ReadBytes(elems);
|
|
|
|
elems = GetIntegerSize(binr);
|
|
E = binr.ReadBytes(elems);
|
|
|
|
elems = GetIntegerSize(binr);
|
|
D = binr.ReadBytes(elems);
|
|
|
|
elems = GetIntegerSize(binr);
|
|
P = binr.ReadBytes(elems);
|
|
|
|
elems = GetIntegerSize(binr);
|
|
Q = binr.ReadBytes(elems);
|
|
|
|
elems = GetIntegerSize(binr);
|
|
DP = binr.ReadBytes(elems);
|
|
|
|
elems = GetIntegerSize(binr);
|
|
DQ = binr.ReadBytes(elems);
|
|
|
|
elems = GetIntegerSize(binr);
|
|
IQ = binr.ReadBytes(elems);
|
|
|
|
// ------- create RSACryptoServiceProvider instance and initialize with public key -----
|
|
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
|
|
RSAParameters RSAparams = new RSAParameters
|
|
{
|
|
Modulus = MODULUS,
|
|
Exponent = E,
|
|
D = D,
|
|
P = P,
|
|
Q = Q,
|
|
DP = DP,
|
|
DQ = DQ,
|
|
InverseQ = IQ
|
|
};
|
|
|
|
RSA.ImportParameters(RSAparams);
|
|
|
|
return RSA;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
finally { binr.Close(); }
|
|
}
|
|
|
|
private static int GetIntegerSize(BinaryReader binr)
|
|
{
|
|
byte bt = binr.ReadByte();
|
|
|
|
if (bt != 0x02) //expect integer
|
|
|
|
return 0;
|
|
bt = binr.ReadByte();
|
|
|
|
int count;
|
|
if (bt == 0x81)
|
|
count = binr.ReadByte(); // data size in next byte
|
|
|
|
else if (bt == 0x82)
|
|
{
|
|
byte highbyte = binr.ReadByte();
|
|
byte lowbyte = binr.ReadByte();
|
|
byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
|
|
count = BitConverter.ToInt32(modint, 0);
|
|
}
|
|
else
|
|
{
|
|
count = bt; // we already have the data size
|
|
}
|
|
|
|
while (binr.ReadByte() == 0x00)
|
|
{ //remove high order zeros in data
|
|
count -= 1;
|
|
}
|
|
|
|
binr.BaseStream.Seek(-1, SeekOrigin.Current); //last ReadByte wasn't a removed zero, so back up a byte
|
|
return count;
|
|
}
|
|
|
|
private static SecureString GetSecPswd(string prompt)
|
|
{
|
|
SecureString password = new SecureString();
|
|
|
|
Console.ForegroundColor = ConsoleColor.Gray;
|
|
Console.Write(prompt);
|
|
Console.ForegroundColor = ConsoleColor.Magenta;
|
|
|
|
while (true)
|
|
{
|
|
ConsoleKeyInfo cki = Console.ReadKey(true);
|
|
if (cki.Key == ConsoleKey.Enter)
|
|
{
|
|
Console.ForegroundColor = ConsoleColor.Gray;
|
|
Console.WriteLine();
|
|
return password;
|
|
}
|
|
else if (cki.Key == ConsoleKey.Backspace)
|
|
{
|
|
// remove the last asterisk from the screen...
|
|
if (password.Length > 0)
|
|
{
|
|
Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
|
|
Console.Write(" ");
|
|
Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
|
|
password.RemoveAt(password.Length - 1);
|
|
}
|
|
}
|
|
else if (cki.Key == ConsoleKey.Escape)
|
|
{
|
|
Console.ForegroundColor = ConsoleColor.Gray;
|
|
Console.WriteLine();
|
|
return password;
|
|
}
|
|
else if (char.IsLetterOrDigit(cki.KeyChar) || char.IsSymbol(cki.KeyChar))
|
|
{
|
|
if (password.Length < 20)
|
|
{
|
|
password.AppendChar(cki.KeyChar);
|
|
Console.Write("*");
|
|
}
|
|
else
|
|
{
|
|
Console.Beep();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.Beep();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool CompareBytearrays(byte[] a, byte[] b)
|
|
{
|
|
if (a.Length != b.Length)
|
|
return false;
|
|
|
|
int i = 0;
|
|
|
|
foreach (byte c in a)
|
|
{
|
|
if (c != b[i])
|
|
return false;
|
|
i++;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
} |