| /******************************************************************************* | |
| * You may amend and distribute as you like, but don't remove this header! | |
| * | |
| * EPPlus provides server-side generation of Excel 2007/2010 spreadsheets. | |
| * See http://www.codeplex.com/EPPlus for details. | |
| * | |
| * Copyright (C) 2011 Jan Källman | |
| * | |
| * This library is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU Lesser General Public | |
| * License as published by the Free Software Foundation; either | |
| * version 2.1 of the License, or (at your option) any later version. | |
| * This library is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
| * See the GNU Lesser General Public License for more details. | |
| * | |
| * The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php | |
| * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html | |
| * | |
| * All code and executables are provided "as is" with no warranty either express or implied. | |
| * The author accepts no liability for any damage or loss of business that this product may cause. | |
| * | |
| * Code change notes: | |
| * | |
| * Author Change Date | |
| ******************************************************************************* | |
| * Jan Källman Added 26-MAR-2012 | |
| *******************************************************************************/ | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Text; | |
| using System.Security.Cryptography.X509Certificates; | |
| using System.Security.Cryptography.Pkcs; | |
| using OfficeOpenXml.Utils; | |
| using System.IO; | |
| namespace OfficeOpenXml.VBA | |
| { | |
| /// <summary> | |
| /// The code signature properties of the project | |
| /// </summary> | |
| public class ExcelVbaSignature | |
| { | |
| const string schemaRelVbaSignature = "http://schemas.microsoft.com/office/2006/relationships/vbaProjectSignature"; | |
| Packaging.ZipPackagePart _vbaPart = null; | |
| internal ExcelVbaSignature(Packaging.ZipPackagePart vbaPart) | |
| { | |
| _vbaPart = vbaPart; | |
| GetSignature(); | |
| } | |
| private void GetSignature() | |
| { | |
| if (_vbaPart == null) return; | |
| var rel = _vbaPart.GetRelationshipsByType(schemaRelVbaSignature).FirstOrDefault(); | |
| if (rel != null) | |
| { | |
| Uri = UriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri); | |
| Part = _vbaPart.Package.GetPart(Uri); | |
| var stream = Part.GetStream(); | |
| BinaryReader br = new BinaryReader(stream); | |
| uint cbSignature = br.ReadUInt32(); | |
| uint signatureOffset = br.ReadUInt32(); //44 ?? | |
| uint cbSigningCertStore = br.ReadUInt32(); | |
| uint certStoreOffset = br.ReadUInt32(); | |
| uint cbProjectName = br.ReadUInt32(); | |
| uint projectNameOffset = br.ReadUInt32(); | |
| uint fTimestamp = br.ReadUInt32(); | |
| uint cbTimestampUrl = br.ReadUInt32(); | |
| uint timestampUrlOffset = br.ReadUInt32(); | |
| byte[] signature = br.ReadBytes((int)cbSignature); | |
| uint version = br.ReadUInt32(); | |
| uint fileType = br.ReadUInt32(); | |
| uint id = br.ReadUInt32(); | |
| while (id != 0) | |
| { | |
| uint encodingType = br.ReadUInt32(); | |
| uint length = br.ReadUInt32(); | |
| if (length > 0) | |
| { | |
| byte[] value = br.ReadBytes((int)length); | |
| switch (id) | |
| { | |
| //Add property values here... | |
| case 0x20: | |
| Certificate = new X509Certificate2(value); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| id = br.ReadUInt32(); | |
| } | |
| uint endel1 = br.ReadUInt32(); //0 | |
| uint endel2 = br.ReadUInt32(); //0 | |
| ushort rgchProjectNameBuffer = br.ReadUInt16(); | |
| ushort rgchTimestampBuffer = br.ReadUInt16(); | |
| Verifier = new SignedCms(); | |
| Verifier.Decode(signature); | |
| } | |
| else | |
| { | |
| Certificate = null; | |
| Verifier = null; | |
| } | |
| } | |
| //Create Oid from a bytearray | |
| //private string ReadHash(byte[] content) | |
| //{ | |
| // StringBuilder builder = new StringBuilder(); | |
| // int offset = 0x6; | |
| // if (0 < (content.Length)) | |
| // { | |
| // byte num = content[offset]; | |
| // byte num2 = (byte)(num / 40); | |
| // builder.Append(num2.ToString(null, null)); | |
| // builder.Append("."); | |
| // num2 = (byte)(num % 40); | |
| // builder.Append(num2.ToString(null, null)); | |
| // ulong num3 = 0L; | |
| // for (int i = offset + 1; i < content.Length; i++) | |
| // { | |
| // num2 = content[i]; | |
| // num3 = (ulong)(ulong)(num3 << 7) + ((byte)(num2 & 0x7f)); | |
| // if ((num2 & 0x80) == 0) | |
| // { | |
| // builder.Append("."); | |
| // builder.Append(num3.ToString(null, null)); | |
| // num3 = 0L; | |
| // } | |
| // //1.2.840.113549.2.5 | |
| // } | |
| // } | |
| // string oId = builder.ToString(); | |
| // return oId; | |
| //} | |
| internal void Save(ExcelVbaProject proj) | |
| { | |
| if (Certificate == null) | |
| { | |
| return; | |
| } | |
| if (Certificate.HasPrivateKey==false) //No signature. Remove any Signature part | |
| { | |
| var storeCert = GetCertFromStore(StoreLocation.CurrentUser); | |
| if (storeCert == null) | |
| { | |
| storeCert = GetCertFromStore(StoreLocation.LocalMachine); | |
| } | |
| if (storeCert != null && storeCert.HasPrivateKey == true) | |
| { | |
| Certificate = storeCert; | |
| } | |
| else | |
| { | |
| foreach (var r in Part.GetRelationships()) | |
| { | |
| Part.DeleteRelationship(r.Id); | |
| } | |
| Part.Package.DeletePart(Part.Uri); | |
| return; | |
| } | |
| } | |
| var ms = new MemoryStream(); | |
| var bw = new BinaryWriter(ms); | |
| byte[] certStore = GetCertStore(); | |
| byte[] cert = SignProject(proj); | |
| bw.Write((uint)cert.Length); | |
| bw.Write((uint)44); //?? 36 ref inside cert ?? | |
| bw.Write((uint)certStore.Length); //cbSigningCertStore | |
| bw.Write((uint)(cert.Length + 44)); //certStoreOffset | |
| bw.Write((uint)0); //cbProjectName | |
| bw.Write((uint)(cert.Length + certStore.Length + 44)); //projectNameOffset | |
| bw.Write((uint)0); //fTimestamp | |
| bw.Write((uint)0); //cbTimestampUrl | |
| bw.Write((uint)(cert.Length + certStore.Length + 44 + 2)); //timestampUrlOffset | |
| bw.Write(cert); | |
| bw.Write(certStore); | |
| bw.Write((ushort)0);//rgchProjectNameBuffer | |
| bw.Write((ushort)0);//rgchTimestampBuffer | |
| bw.Write((ushort)0); | |
| bw.Flush(); | |
| var rel = proj.Part.GetRelationshipsByType(schemaRelVbaSignature).FirstOrDefault(); | |
| if (Part == null) | |
| { | |
| if (rel != null) | |
| { | |
| Uri = rel.TargetUri; | |
| Part = proj._pck.GetPart(rel.TargetUri); | |
| } | |
| else | |
| { | |
| Uri = new Uri("/xl/vbaProjectSignature.bin", UriKind.Relative); | |
| Part = proj._pck.CreatePart(Uri, ExcelPackage.schemaVBASignature); | |
| } | |
| } | |
| if (rel == null) | |
| { | |
| proj.Part.CreateRelationship(UriHelper.ResolvePartUri(proj.Uri, Uri), Packaging.TargetMode.Internal, schemaRelVbaSignature); | |
| } | |
| var b = ms.ToArray(); | |
| Part.GetStream(FileMode.Create).Write(b, 0, b.Length); | |
| } | |
| private X509Certificate2 GetCertFromStore(StoreLocation loc) | |
| { | |
| try | |
| { | |
| X509Store store = new X509Store(loc); | |
| store.Open(OpenFlags.ReadOnly); | |
| try | |
| { | |
| var storeCert = store.Certificates.Find( | |
| X509FindType.FindByThumbprint, | |
| Certificate.Thumbprint, | |
| true | |
| ).OfType<X509Certificate2>().FirstOrDefault(); | |
| return storeCert; | |
| } | |
| finally | |
| { | |
| store.Close(); | |
| } | |
| } | |
| catch | |
| { | |
| return null; | |
| } | |
| } | |
| private byte[] GetCertStore() | |
| { | |
| var ms = new MemoryStream(); | |
| var bw = new BinaryWriter(ms); | |
| bw.Write((uint)0); //Version | |
| bw.Write((uint)0x54524543); //fileType | |
| //SerializedCertificateEntry | |
| var certData = Certificate.RawData; | |
| bw.Write((uint)0x20); | |
| bw.Write((uint)1); | |
| bw.Write((uint)certData.Length); | |
| bw.Write(certData); | |
| //EndElementMarkerEntry | |
| bw.Write((uint)0); | |
| bw.Write((ulong)0); | |
| bw.Flush(); | |
| return ms.ToArray(); | |
| } | |
| private void WriteProp(BinaryWriter bw, int id, byte[] data) | |
| { | |
| bw.Write((uint)id); | |
| bw.Write((uint)1); | |
| bw.Write((uint)data.Length); | |
| bw.Write(data); | |
| } | |
| internal byte[] SignProject(ExcelVbaProject proj) | |
| { | |
| if (!Certificate.HasPrivateKey) | |
| { | |
| //throw (new InvalidOperationException("The certificate doesn't have a private key")); | |
| Certificate = null; | |
| return null; | |
| } | |
| var hash = GetContentHash(proj); | |
| BinaryWriter bw = new BinaryWriter(new MemoryStream()); | |
| bw.Write((byte)0x30); //Constructed Type | |
| bw.Write((byte)0x32); //Total length | |
| bw.Write((byte)0x30); //Constructed Type | |
| bw.Write((byte)0x0E); //Length SpcIndirectDataContent | |
| bw.Write((byte)0x06); //Oid Tag Indentifier | |
| bw.Write((byte)0x0A); //Lenght OId | |
| bw.Write(new byte[] { 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x1D }); //Encoded Oid 1.3.6.1.4.1.311.2.1.29 | |
| bw.Write((byte)0x04); //Octet String Tag Identifier | |
| bw.Write((byte)0x00); //Zero length | |
| bw.Write((byte)0x30); //Constructed Type (DigestInfo) | |
| bw.Write((byte)0x20); //Length DigestInfo | |
| bw.Write((byte)0x30); //Constructed Type (Algorithm) | |
| bw.Write((byte)0x0C); //length AlgorithmIdentifier | |
| bw.Write((byte)0x06); //Oid Tag Indentifier | |
| bw.Write((byte)0x08); //Lenght OId | |
| bw.Write(new byte[] { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x02, 0x05 }); //Encoded Oid for 1.2.840.113549.2.5 (AlgorithmIdentifier MD5) | |
| bw.Write((byte)0x05); //Null type identifier | |
| bw.Write((byte)0x00); //Null length | |
| bw.Write((byte)0x04); //Octet String Identifier | |
| bw.Write((byte)hash.Length); //Hash length | |
| bw.Write(hash); //Content hash | |
| ContentInfo contentInfo = new ContentInfo(((MemoryStream)bw.BaseStream).ToArray()); | |
| contentInfo.ContentType.Value = "1.3.6.1.4.1.311.2.1.4"; | |
| Verifier = new SignedCms(contentInfo); | |
| var signer = new CmsSigner(Certificate); | |
| Verifier.ComputeSignature(signer, false); | |
| return Verifier.Encode(); | |
| } | |
| private byte[] GetContentHash(ExcelVbaProject proj) | |
| { | |
| //MS-OVBA 2.4.2 | |
| var enc = System.Text.Encoding.GetEncoding(proj.CodePage); | |
| BinaryWriter bw = new BinaryWriter(new MemoryStream()); | |
| bw.Write(enc.GetBytes(proj.Name)); | |
| bw.Write(enc.GetBytes(proj.Constants)); | |
| foreach (var reference in proj.References) | |
| { | |
| if (reference.ReferenceRecordID == 0x0D) | |
| { | |
| bw.Write((byte)0x7B); | |
| } | |
| if (reference.ReferenceRecordID == 0x0E) | |
| { | |
| //var r = (ExcelVbaReferenceProject)reference; | |
| //BinaryWriter bwTemp = new BinaryWriter(new MemoryStream()); | |
| //bwTemp.Write((uint)r.Libid.Length); | |
| //bwTemp.Write(enc.GetBytes(r.Libid)); | |
| //bwTemp.Write((uint)r.LibIdRelative.Length); | |
| //bwTemp.Write(enc.GetBytes(r.LibIdRelative)); | |
| //bwTemp.Write(r.MajorVersion); | |
| //bwTemp.Write(r.MinorVersion); | |
| foreach (byte b in BitConverter.GetBytes((uint)reference.Libid.Length)) //Length will never be an UInt with 4 bytes that aren't 0 (> 0x00FFFFFF), so no need for the rest of the properties. | |
| { | |
| if (b != 0) | |
| { | |
| bw.Write(b); | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| foreach (var module in proj.Modules) | |
| { | |
| var lines = module.Code.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); | |
| foreach (var line in lines) | |
| { | |
| if (!line.StartsWith("attribute", true, null)) | |
| { | |
| bw.Write(enc.GetBytes(line)); | |
| } | |
| } | |
| } | |
| var buffer = (bw.BaseStream as MemoryStream).ToArray(); | |
| var hp = System.Security.Cryptography.MD5CryptoServiceProvider.Create(); | |
| return hp.ComputeHash(buffer); | |
| } | |
| /// <summary> | |
| /// The certificate to sign the VBA project. | |
| /// <remarks> | |
| /// This certificate must have a private key. | |
| /// There is no validation that the certificate is valid for codesigning, so make sure it's valid to sign Excel files (Excel 2010 is more strict that prior versions). | |
| /// </remarks> | |
| /// </summary> | |
| public X509Certificate2 Certificate { get; set; } | |
| /// <summary> | |
| /// The verifier | |
| /// </summary> | |
| public SignedCms Verifier { get; internal set; } | |
| #if !MONO | |
| internal CompoundDocument Signature { get; set; } | |
| #endif | |
| internal Packaging.ZipPackagePart Part { get; set; } | |
| internal Uri Uri { get; private set; } | |
| } | |
| } |