diff --git a/VipNetExtract.sln b/VipNetExtract.sln new file mode 100644 index 0000000..487c989 --- /dev/null +++ b/VipNetExtract.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.572 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VipNetExtract", "VipNetExtract2\VipNetExtract.csproj", "{201ACA67-E71C-4058-96A5-F8717C484C01}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {201ACA67-E71C-4058-96A5-F8717C484C01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {201ACA67-E71C-4058-96A5-F8717C484C01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {201ACA67-E71C-4058-96A5-F8717C484C01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {201ACA67-E71C-4058-96A5-F8717C484C01}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {072DCD9B-6565-4D97-B502-D4381A85AC1A} + EndGlobalSection +EndGlobal diff --git a/VipNetExtract2/Export.cs b/VipNetExtract2/Export.cs new file mode 100644 index 0000000..9f00056 --- /dev/null +++ b/VipNetExtract2/Export.cs @@ -0,0 +1,67 @@ +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.CryptoPro; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities.IO.Pem; +using Org.BouncyCastle.X509; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace VipNetExtract +{ + interface IExport + { + void Export(VipNetContainer container, string pin, Stream output); + } + + class PrivateKeyExport : IExport + { + public void Export(VipNetContainer container, string pin, Stream output) + { + var privateKey = EncodePrivateKey(container, pin); + var pemObject = new PemObject("PRIVATE KEY", privateKey.GetDerEncoded()); + using (var sw = new StreamWriter(output)) { + var writer = new PemWriter(sw); + writer.WriteObject(pemObject); + } + } + + private static Asn1Object EncodePrivateKey(VipNetContainer container, string pin) + { + var entry = container.Entries[0]; + var gostParams = Gost3410PublicKeyAlgParameters.GetInstance(entry.KeyInfo.Algorithm.Parameters); + + return new DerSequence( + new DerInteger(0), + new DerSequence( + entry.KeyInfo.Algorithm.Algorithm, + new DerSequence( + gostParams.PublicKeyParamSet, + gostParams.DigestParamSet + ) + ), + new DerOctetString(new DerInteger(entry.GetPrivateKey(pin))) + ); + } + } + + class CertificateExport : IExport + { + public void Export(VipNetContainer container, string pin, Stream output) + { + var cert = container.Entries[0].Certificate; + if (cert == null) + throw new InvalidOperationException("Контейнер не содержит сертификата"); + + var pemObject = new PemObject("CERTIFICATE", cert.GetEncoded()); + using (var sw = new StreamWriter(output)) { + var writer = new PemWriter(sw); + writer.WriteObject(pemObject); + } + } + } +} diff --git a/VipNetExtract2/KeyValidity.cs b/VipNetExtract2/KeyValidity.cs new file mode 100644 index 0000000..29e5369 --- /dev/null +++ b/VipNetExtract2/KeyValidity.cs @@ -0,0 +1,46 @@ +using System; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Utilities; + +namespace VipNetExtract +{ + class KeyValidity : Asn1Encodable + { + public KeyValidity(Asn1Sequence seq) + { + NotBefore = Time.GetInstance(GetTime(seq[0])); + NotAfter = Time.GetInstance(GetTime(seq[1])); + } + + private static Asn1Encodable GetTime(Asn1Encodable time) + { + if (time is Asn1TaggedObject tag) + time = tag.GetObject(); + + if (time is Asn1OctetString str) + time = new DerGeneralizedTime(Strings.FromAsciiByteArray(str.GetOctets())); + + return time; + } + + public Time NotBefore { get; } + public Time NotAfter { get; } + + public override Asn1Object ToAsn1Object() + { + throw new NotImplementedException(); + } + + public static KeyValidity GetInstance(object obj) + { + if (obj is KeyValidity entry) + return entry; + + return new KeyValidity(Asn1Sequence.GetInstance(obj)); + } + + public static KeyValidity GetInstance(Asn1TaggedObject obj, bool explicitly) + => GetInstance(Asn1Sequence.GetInstance(obj, explicitly)); + } +} diff --git a/VipNetExtract2/Program.cs b/VipNetExtract2/Program.cs new file mode 100644 index 0000000..e363a6e --- /dev/null +++ b/VipNetExtract2/Program.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using Mono.Options; +using Org.BouncyCastle.Utilities.Encoders; + +namespace VipNetExtract +{ + class Program + { + enum Mode + { + Private, Certificate + } + + private static OptionSet options; + + static void Main(string[] args) + { + string file = null, pin = null; + Mode mode = Mode.Private; + bool showHelp = false; + + options = new OptionSet { + { "f|file=", "Путь к контейнеру", f => file = f }, + { "private", "Извлечь закрытый ключ (по умолчанию)", p => { if (p != null) mode = Mode.Private; } }, + { "cert", "Извлечь сертификат", c => { if (c != null) mode = Mode.Certificate; } }, + { "p|pin=", "ПИН-код", p => pin = p }, + { "h|help", "Помощь", h => showHelp = h != null} + }; + + try { + options.Parse(args); + } catch (OptionException e) { + Console.Error.WriteLine(e.Message); + return; + } + + if (showHelp || String.IsNullOrEmpty(file)) { + PrintHelp(); + return; + } + + IExport export; + if (mode == Mode.Certificate) { + export = new CertificateExport(); + } else { + export = new PrivateKeyExport(); + } + + try { + var container = VipNetContainer.LoadFromFile(file); + export.Export(container, pin, Console.OpenStandardOutput()); + } catch (Exception e) { + Console.Error.WriteLine(e.Message); + } + } + + static void PrintHelp() + { + Console.WriteLine("Использование: extractpkey {ПАРАМЕТРЫ}"); + Console.WriteLine("Извлечение данных из контейнера VipNet"); + Console.WriteLine(); + Console.WriteLine("Параметры:"); + options.WriteOptionDescriptions(Console.Out); + } + } +} diff --git a/VipNetExtract2/Properties/AssemblyInfo.cs b/VipNetExtract2/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a5c675f --- /dev/null +++ b/VipNetExtract2/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("VipNetExtract")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("VipNetExtract")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("201aca67-e71c-4058-96a5-f8717c484c01")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/VipNetExtract2/VipNetContainer.cs b/VipNetExtract2/VipNetContainer.cs new file mode 100644 index 0000000..b5f874f --- /dev/null +++ b/VipNetExtract2/VipNetContainer.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Org.BouncyCastle.Asn1; + +namespace VipNetExtract +{ + class VipNetContainer + { + private VipNetContainer( + string type, uint version, int headerSize, + byte[] header, IList entries) + { + Type = type; + Version = version; + HeaderSize = headerSize; + Header = header; + Entries = entries; + } + + public string Type { get; } + public uint Version { get; } + public int HeaderSize { get; } + public byte[] Header { get; } + public IList Entries { get; } + + public static VipNetContainer LoadFromStream(Stream strm) + { + using (var reader = new BinaryReader(strm)) { + var type = Encoding.ASCII.GetString(reader.ReadBytes(4)); + if (type != "ITCS" && type != "PKEY" && type != "_CCK" && type != "_LCK") + throw new NotSupportedException($"Неподдерживаемый тип контейнера: {type}."); + + var version = reader.ReadUInt32(); + if (LoWord(version) > 0xFF || HiWord(version) > 2) + throw new NotSupportedException($"Неподдерживаемая версия контейнера: {version}."); + + var headerSize = reader.ReadInt32(); + var header = new byte[headerSize]; + if (headerSize > 0) + header = reader.ReadBytes(headerSize); + + var entries = new List(); + while (strm.Position < strm.Length) { + var entrySize = reader.ReadInt32(); + var entryStartPos = strm.Position; + var entrySeq = (Asn1Sequence)Asn1Object.FromStream(strm); + var keySize = reader.ReadInt32(); + if (keySize < 0 || strm.Position + keySize - entryStartPos != entrySize) + throw new InvalidOperationException($"Некорректный размер блока с ключом: {keySize}."); + var key = reader.ReadBytes(keySize); + entries.Add(new VipNetContainerEntry(entrySeq, key)); + } + + if (entries.Count == 0) + throw new InvalidOperationException("Контейнер не содержит записей."); + + return new VipNetContainer(type, version, headerSize, header, entries); + } + } + + public static VipNetContainer LoadFromFile(string fileName) + { + using (var strm = File.OpenRead(fileName)) + return LoadFromStream(strm); + } + + static uint LoWord(uint x) => x & 0x0000FFFF; + static uint HiWord(uint x) => x >> 16; + } +} diff --git a/VipNetExtract2/VipNetContainerEntry.cs b/VipNetExtract2/VipNetContainerEntry.cs new file mode 100644 index 0000000..c5d82ca --- /dev/null +++ b/VipNetExtract2/VipNetContainerEntry.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.CryptoPro; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities.Encoders; + +namespace VipNetExtract +{ + class VipNetContainerEntry + { + public VipNetContainerEntry(Asn1Sequence seq, byte[] keyBlock) + { + Version = (DerInteger)seq[0]; + KeyInfo = VipNetKeyInfo.GetInstance(seq[1]); + DefenceKeyInfo = VipNetKeyInfo.GetInstance(seq[2]); + KeyBlock = keyBlock; + + for (int i = 3; i < seq.Count; ++i) { + if (seq[i] is Asn1TaggedObject tag) { + switch (tag.TagNo) { + case 0: + Certificate = X509CertificateStructure.GetInstance(tag.GetObject()); + break; + case 1: + PublicKey = (DerOctetString)tag.GetObject(); + break; + } + } + } + } + + public DerInteger Version { get; } + public VipNetKeyInfo KeyInfo { get; } + public VipNetKeyInfo DefenceKeyInfo { get; } + public X509CertificateStructure Certificate { get; } + public DerOctetString PublicKey { get; internal set; } + public byte[] KeyBlock { get; } + + public BigInteger GetPrivateKey(string pin) + { + var cek = KeyBlock.Take(KeyBlock.Length - 12).ToArray(); + var mac = KeyBlock.Skip(cek.Length).Take(4).ToArray(); + var data = cek.Concat(KeyInfo.RawData).ToArray(); + var pinKey = GetDecryptionKey(pin); + + CheckMac(pinKey, cek, data, mac); + + var iv = KeyBlock.Skip(KeyBlock.Length - 8).ToArray(); + var pkeyMasked = DecryptKey(pinKey, cek, iv); + + byte[] privateKey; + if (KeyInfo.KeyClass.Value.And(BigInteger.Three).Equals(BigInteger.Zero)) { + data = pkeyMasked.Take(pkeyMasked.Length / 2).ToArray(); + var unwrappingKey = pkeyMasked.Skip(pkeyMasked.Length / 2).ToArray(); + privateKey = DecryptKey(unwrappingKey, data); + } else { + var wrapped = pkeyMasked.Take(pkeyMasked.Length / 2).Reverse().ToArray(); + var mask = pkeyMasked.Skip(pkeyMasked.Length / 2).Reverse().ToArray(); + + var algParams = Gost3410PublicKeyAlgParameters.GetInstance(KeyInfo.Algorithm.Parameters); + var param = new ECKeyGenerationParameters(algParams.PublicKeyParamSet, new SecureRandom()); + + var x = new BigInteger(1, wrapped); + var y = new BigInteger(1, mask); + var z = x.Multiply(y).Mod(param.DomainParameters.Curve.Order); + + CheckPrivateKey(param, z); + + privateKey = z.ToByteArrayUnsigned(); + } + + return new BigInteger(1, privateKey); + } + + private byte[] GetDecryptionKey(string pin) + { + var digest = new Gost3411Digest(); + var passwordData = Encoding.ASCII.GetBytes(pin ?? ""); + var keyData = new byte[digest.GetDigestSize()]; + var unwrappingKey = new byte[digest.GetDigestSize()]; + + digest.BlockUpdate(passwordData, 0, passwordData.Length); + digest.DoFinal(keyData, 0); + digest.Reset(); + + var secodeData = passwordData.Concat(keyData).ToArray(); + digest.BlockUpdate(secodeData, 0, secodeData.Length); + digest.DoFinal(unwrappingKey, 0); + + var tmp = new int[keyData.Length / 4]; + for (int i = 0; i < keyData.Length; i += 4) + tmp[i / 4] = BitConverter.ToInt32(keyData, i) - BitConverter.ToInt32(unwrappingKey, i); + + return tmp.SelectMany(x => BitConverter.GetBytes(x)).ToArray(); + } + + private static void CheckMac(byte[] key, byte[] cek, byte[] data, byte[] mac) + { + var m = new Gost28147Mac(); + var keyPrm = ParameterUtilities.CreateKeyParameter("GOST", key); + m.Init(keyPrm); + + var cekmac = new byte[4]; + m.BlockUpdate(data, 0, data.Length); + m.DoFinal(cekmac, 0); + + if (!mac.SequenceEqual(cekmac)) + throw new CryptographicException("Неверный ПИН-код"); + } + + private static byte[] DecryptKey(byte[] key, byte[] cek, byte[] iv = null) + { + var cipher = CipherUtilities.GetCipher("GOST/CFB/NOPADDING"); + ICipherParameters prms = ParameterUtilities.CreateKeyParameter("GOST", key); + prms = new ParametersWithSBox(prms, Gost28147Engine.GetSBox("E-A")); + cipher.Init(false, iv == null ? prms : new ParametersWithIV(prms, iv)); + return cipher.ProcessBytes(cek); + } + + private void CheckPrivateKey(ECKeyGenerationParameters param, BigInteger privateKey) + { + var point = param.DomainParameters.G.Multiply(privateKey).Normalize(); + var x = point.AffineXCoord.GetEncoded().Reverse(); + var y = point.AffineYCoord.GetEncoded().Reverse(); + var pub = PublicKey.GetOctets(); + + if (!x.SequenceEqual(pub.Take(pub.Length / 2)) || !y.SequenceEqual(pub.Skip(pub.Length / 2))) + throw new CryptographicException("Закрытый ключ не соответствует открытому ключу."); + } + } +} diff --git a/VipNetExtract2/VipNetExtract.csproj b/VipNetExtract2/VipNetExtract.csproj new file mode 100644 index 0000000..33b41a6 --- /dev/null +++ b/VipNetExtract2/VipNetExtract.csproj @@ -0,0 +1,63 @@ + + + + + Debug + AnyCPU + {201ACA67-E71C-4058-96A5-F8717C484C01} + Exe + VipNetExtract + ExtractPKey + v4.0 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\BouncyCastle.1.8.5\lib\BouncyCastle.Crypto.dll + + + ..\packages\Mono.Options.5.3.0.1\lib\net4-client\Mono.Options.dll + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VipNetExtract2/VipNetKeyInfo.cs b/VipNetExtract2/VipNetKeyInfo.cs new file mode 100644 index 0000000..570fb4b --- /dev/null +++ b/VipNetExtract2/VipNetKeyInfo.cs @@ -0,0 +1,110 @@ +using System; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Crmf; +using Org.BouncyCastle.Asn1.X509; + +namespace VipNetExtract +{ + class VipNetKeyInfo : Asn1Encodable + { + public VipNetKeyInfo(Asn1Sequence seq) + { + RawData = seq.GetEncoded(); + KeyClass = (DerInteger)seq[0]; + KeyType = (DerInteger)seq[1]; + + for (int i = 2; i < seq.Count; ++i) { + if (seq[i] is Asn1TaggedObject tag) { + switch (tag.TagNo) { + case 0: + Algorithm = AlgorithmIdentifier.GetInstance(tag.GetObject()); + break; + case 1: + SerialNumber = Asn1OctetString.GetInstance(tag.GetObject()); + break; + case 2: + AddSerialNumber = Asn1OctetString.GetInstance(tag.GetObject()); + break; + case 3: + CertSerialNumber = Asn1OctetString.GetInstance(tag.GetObject()); + break; + case 4: + SubjectUID = Asn1OctetString.GetInstance(tag.GetObject()); + break; + case 5: + RecipientUID = Asn1OctetString.GetInstance(tag.GetObject()); + break; + case 6: + Validity = KeyValidity.GetInstance(tag.GetObject()); + break; + case 7: + KeyUID = DerBitString.GetInstance(tag.GetObject()); + break; + case 10: + Flags = DerInteger.GetInstance(tag.GetObject()); + break; + } + } + } + } + + internal byte[] RawData { get; } + + public DerInteger KeyClass { get; } + public DerInteger KeyType { get; } + public AlgorithmIdentifier Algorithm { get; } + public Asn1OctetString SerialNumber { get; } + public Asn1OctetString AddSerialNumber { get; } + public Asn1OctetString CertSerialNumber { get; } + public Asn1OctetString SubjectUID { get; } + public Asn1OctetString RecipientUID { get; } + public KeyValidity Validity { get; } + public DerBitString KeyUID { get; } + public DerInteger Flags { get; } + + public override Asn1Object ToAsn1Object() + { + var vec = new Asn1EncodableVector(KeyClass, KeyType); + + if (Algorithm != null) + vec.Add(new DerTaggedObject(0, Algorithm)); + + if (SerialNumber != null) + vec.Add(new DerTaggedObject(1, SerialNumber)); + + if (AddSerialNumber != null) + vec.Add(new DerTaggedObject(2, AddSerialNumber)); + + if (CertSerialNumber != null) + vec.Add(new DerTaggedObject(3, CertSerialNumber)); + + if (SubjectUID != null) + vec.Add(new DerTaggedObject(4, SubjectUID)); + + if (RecipientUID != null) + vec.Add(new DerTaggedObject(5, RecipientUID)); + + if (Validity != null) + vec.Add(new DerTaggedObject(6, Validity)); + + if (KeyUID != null) + vec.Add(new DerTaggedObject(7, KeyUID)); + + if (Flags != null) + vec.Add(new DerTaggedObject(10, Flags)); + + return new DerSequence(vec); + } + + public static VipNetKeyInfo GetInstance(object obj) + { + if (obj is VipNetKeyInfo keyInfo) + return keyInfo; + + return new VipNetKeyInfo(Asn1Sequence.GetInstance(obj)); + } + + public static VipNetKeyInfo GetInstance(Asn1TaggedObject obj, bool explicitly) + => GetInstance(Asn1Sequence.GetInstance(obj, explicitly)); + } +} diff --git a/VipNetExtract2/packages.config b/VipNetExtract2/packages.config new file mode 100644 index 0000000..8cd5fd8 --- /dev/null +++ b/VipNetExtract2/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file