001/** 002 * Copyright 2019 Emmanuel Bourg 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign.msi; 018 019import java.io.ByteArrayInputStream; 020import java.io.Closeable; 021import java.io.DataInputStream; 022import java.io.File; 023import java.io.FileInputStream; 024import java.io.FileNotFoundException; 025import java.io.FilterInputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.nio.ByteBuffer; 029import java.nio.channels.Channels; 030import java.nio.channels.SeekableByteChannel; 031import java.security.MessageDigest; 032import java.util.ArrayList; 033import java.util.List; 034import java.util.Map; 035import java.util.TreeMap; 036 037import org.apache.poi.poifs.filesystem.DocumentEntry; 038import org.apache.poi.poifs.filesystem.DocumentInputStream; 039import org.apache.poi.poifs.filesystem.POIFSDocument; 040import org.apache.poi.poifs.filesystem.POIFSFileSystem; 041import org.apache.poi.poifs.property.DirectoryProperty; 042import org.apache.poi.poifs.property.DocumentProperty; 043import org.apache.poi.poifs.property.Property; 044import org.apache.poi.util.IOUtils; 045import org.bouncycastle.asn1.ASN1Encodable; 046import org.bouncycastle.asn1.ASN1InputStream; 047import org.bouncycastle.asn1.ASN1Object; 048import org.bouncycastle.asn1.DERNull; 049import org.bouncycastle.asn1.cms.Attribute; 050import org.bouncycastle.asn1.cms.AttributeTable; 051import org.bouncycastle.asn1.cms.ContentInfo; 052import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 053import org.bouncycastle.asn1.x509.DigestInfo; 054import org.bouncycastle.cms.CMSProcessable; 055import org.bouncycastle.cms.CMSSignedData; 056import org.bouncycastle.cms.SignerInformation; 057 058import net.jsign.DigestAlgorithm; 059import net.jsign.Signable; 060import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; 061import net.jsign.asn1.authenticode.SpcAttributeTypeAndOptionalValue; 062import net.jsign.asn1.authenticode.SpcIndirectDataContent; 063import net.jsign.asn1.authenticode.SpcSipInfo; 064import net.jsign.asn1.authenticode.SpcUuid; 065 066/** 067 * A Microsoft Installer package. 068 * 069 * @author Emmanuel Bourg 070 * @since 3.0 071 */ 072public class MSIFile implements Signable, Closeable { 073 074 private static final long MSI_HEADER = 0xD0CF11E0A1B11AE1L; 075 076 private static final String DIGITAL_SIGNATURE_ENTRY_NAME = "\u0005DigitalSignature"; 077 private static final String MSI_DIGITAL_SIGNATURE_EX_ENTRY_NAME = "\u0005MsiDigitalSignatureEx"; 078 079 /** 080 * The POI filesystem used for reading the file. A separate filesystem has 081 * to be used because POI maps the file in memory in read/write mode and 082 * this leads to OOM errors when the file is parsed. 083 * See https://github.com/ebourg/jsign/issues/82 for more info. 084 */ 085 private final POIFSFileSystem fsRead; 086 087 /** The POI filesystem used for writing to the file */ 088 private final POIFSFileSystem fsWrite; 089 090 /** The channel used for in-memory signing */ 091 private SeekableByteChannel channel; 092 093 /** 094 * Tells if the specified file is a MSI file. 095 * 096 * @param file the file to check 097 * @return <code>true</code> if the file is a Microsoft installer, <code>false</code> otherwise 098 * @throws IOException if an I/O error occurs 099 */ 100 public static boolean isMSIFile(File file) throws IOException { 101 try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { 102 return in.readLong() == MSI_HEADER; 103 } 104 } 105 106 /** 107 * Create a MSIFile from the specified file. 108 * 109 * @param file the file to open 110 * @throws IOException if an I/O error occurs 111 */ 112 public MSIFile(File file) throws IOException { 113 this.fsRead = new POIFSFileSystem(file, true); 114 this.fsWrite = new POIFSFileSystem(file, false); 115 } 116 117 /** 118 * Create a MSIFile from the specified channel. 119 * 120 * @param channel the channel to read the file from 121 * @throws IOException if an I/O error occurs 122 */ 123 public MSIFile(final SeekableByteChannel channel) throws IOException { 124 this.channel = channel; 125 InputStream in = new FilterInputStream(Channels.newInputStream(channel)) { 126 public void close() { } 127 }; 128 this.fsRead = new POIFSFileSystem(in); 129 this.fsWrite = fsRead; 130 } 131 132 /** 133 * Closes the file 134 * 135 * @throws IOException if an I/O error occurs 136 */ 137 public void close() throws IOException { 138 try (POIFSFileSystem fsRead = this.fsRead; POIFSFileSystem fsWrite = this.fsWrite; SeekableByteChannel channel = this.channel) { 139 // do nothing 140 } 141 } 142 143 /** 144 * Tells if the MSI file has an extended signature (MsiDigitalSignatureEx) 145 * containing a hash of the streams metadata (name, size, date). 146 * 147 * @return <code>true</code> if the file has a MsiDigitalSignatureEx stream, <code>false</code> otherwise 148 */ 149 public boolean hasExtendedSignature() { 150 try { 151 fsRead.getRoot().getEntry(MSI_DIGITAL_SIGNATURE_EX_ENTRY_NAME); 152 return true; 153 } catch (FileNotFoundException e) { 154 return false; 155 } 156 } 157 158 private List<Property> getSortedProperties() { 159 List<Property> entries = new ArrayList<>(); 160 161 append(fsRead.getPropertyTable().getRoot(), entries); 162 163 return entries; 164 } 165 166 private void append(DirectoryProperty node, List<Property> entries) { 167 Map<MSIStreamName, Property> sortedEntries = new TreeMap<>(); 168 for (Property entry : node) { 169 sortedEntries.put(new MSIStreamName(entry.getName()), entry); 170 } 171 172 for (Property property : sortedEntries.values()) { 173 if (!property.isDirectory()) { 174 entries.add(property); 175 } else { 176 append((DirectoryProperty) property, entries); 177 } 178 } 179 } 180 181 @Override 182 public byte[] computeDigest(MessageDigest digest) { 183 // hash the entries 184 for (Property property : getSortedProperties()) { 185 String name = new MSIStreamName(property.getName()).decode(); 186 if (name.equals(DIGITAL_SIGNATURE_ENTRY_NAME) || name.equals(MSI_DIGITAL_SIGNATURE_EX_ENTRY_NAME)) { 187 continue; 188 } 189 190 POIFSDocument document = new POIFSDocument((DocumentProperty) property, fsRead); 191 long remaining = document.getSize(); 192 for (ByteBuffer buffer : document) { 193 int size = buffer.remaining(); 194 buffer.limit(buffer.position() + (int) Math.min(remaining, size)); 195 digest.update(buffer); 196 remaining -= size; 197 } 198 } 199 200 // hash the package ClassID, in serialized form 201 byte[] classId = new byte[16]; 202 fsRead.getRoot().getStorageClsid().write(classId, 0); 203 digest.update(classId); 204 205 return digest.digest(); 206 } 207 208 @Override 209 public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) { 210 AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE); 211 DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm.getMessageDigest())); 212 213 SpcUuid uuid = new SpcUuid("F1100C00-0000-0000-C000-000000000046"); 214 SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_SIPINFO_OBJID, new SpcSipInfo(1, uuid)); 215 216 return new SpcIndirectDataContent(data, digestInfo); 217 } 218 219 @Override 220 public List<CMSSignedData> getSignatures() throws IOException { 221 List<CMSSignedData> signatures = new ArrayList<>(); 222 223 try { 224 DocumentEntry digitalSignature = (DocumentEntry) fsRead.getRoot().getEntry(DIGITAL_SIGNATURE_ENTRY_NAME); 225 if (digitalSignature != null) { 226 byte[] signatureBytes = IOUtils.toByteArray(new DocumentInputStream(digitalSignature)); 227 try { 228 CMSSignedData signedData = new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(new ASN1InputStream(signatureBytes).readObject())); 229 signatures.add(signedData); 230 231 // look for nested signatures 232 SignerInformation signerInformation = signedData.getSignerInfos().getSigners().iterator().next(); 233 AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); 234 if (unsignedAttributes != null) { 235 Attribute nestedSignatures = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID); 236 if (nestedSignatures != null) { 237 for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) { 238 signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature))); 239 } 240 } 241 } 242 } catch (UnsupportedOperationException e) { 243 // unsupported type, just skip 244 } catch (Exception e) { 245 e.printStackTrace(); 246 } 247 } 248 } catch (FileNotFoundException e) { 249 } 250 251 return signatures; 252 } 253 254 @Override 255 public void setSignature(CMSSignedData signature) throws IOException { 256 byte[] signatureBytes = signature.toASN1Structure().getEncoded("DER"); 257 fsWrite.getRoot().createOrUpdateDocument(DIGITAL_SIGNATURE_ENTRY_NAME, new ByteArrayInputStream(signatureBytes)); 258 } 259 260 @Override 261 public void save() throws IOException { 262 if (channel == null) { 263 fsWrite.writeFilesystem(); 264 } else { 265 channel.position(0); 266 fsWrite.writeFilesystem(Channels.newOutputStream(channel)); 267 channel.truncate(channel.position()); 268 } 269 } 270}