| /******************************************************************************* |
| * 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; } |
| } |
| } |