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 private final POIFSFileSystem fs; 080 private SeekableByteChannel channel; 081 082 /** 083 * Tells if the specified file is a MSI file. 084 * 085 * @param file the file to check 086 * @return <code>true</code> if the file is a Microsoft installer, <code>false</code> otherwise 087 * @throws IOException if an I/O error occurs 088 */ 089 public static boolean isMSIFile(File file) throws IOException { 090 try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { 091 return in.readLong() == MSI_HEADER; 092 } 093 } 094 095 /** 096 * Create a MSIFile from the specified file. 097 * 098 * @param file the file to open 099 * @throws IOException if an I/O error occurs 100 */ 101 public MSIFile(File file) throws IOException { 102 this.fs = new POIFSFileSystem(file, false); 103 } 104 105 /** 106 * Create a MSIFile from the specified channel. 107 * 108 * @param channel the channel to read the file from 109 * @throws IOException if an I/O error occurs 110 */ 111 public MSIFile(final SeekableByteChannel channel) throws IOException { 112 this.channel = channel; 113 InputStream in = new FilterInputStream(Channels.newInputStream(channel)) { 114 public void close() { } 115 }; 116 this.fs = new POIFSFileSystem(in); 117 } 118 119 /** 120 * Closes the file 121 * 122 * @throws IOException if an I/O error occurs 123 */ 124 public void close() throws IOException { 125 fs.close(); 126 if (channel != null) { 127 channel.close(); 128 } 129 } 130 131 /** 132 * Tells if the MSI file has an extended signature (MsiDigitalSignatureEx) 133 * containing a hash of the streams metadata (name, size, date). 134 * 135 * @return <code>true</code> if the file has a MsiDigitalSignatureEx stream, <code>false</code> otherwise 136 */ 137 public boolean hasExtendedSignature() { 138 try { 139 fs.getRoot().getEntry(MSI_DIGITAL_SIGNATURE_EX_ENTRY_NAME); 140 return true; 141 } catch (FileNotFoundException e) { 142 return false; 143 } 144 } 145 146 private List<Property> getSortedProperties() { 147 List<Property> entries = new ArrayList<>(); 148 149 append(fs.getPropertyTable().getRoot(), entries); 150 151 return entries; 152 } 153 154 private void append(DirectoryProperty node, List<Property> entries) { 155 Map<MSIStreamName, Property> sortedEntries = new TreeMap<>(); 156 for (Property entry : node) { 157 sortedEntries.put(new MSIStreamName(entry.getName()), entry); 158 } 159 160 for (Property property : sortedEntries.values()) { 161 if (!property.isDirectory()) { 162 entries.add(property); 163 } else { 164 append((DirectoryProperty) property, entries); 165 } 166 } 167 } 168 169 @Override 170 public byte[] computeDigest(MessageDigest digest) throws IOException { 171 // hash the entries 172 for (Property property : getSortedProperties()) { 173 String name = new MSIStreamName(property.getName()).decode(); 174 if (name.equals(DIGITAL_SIGNATURE_ENTRY_NAME) || name.equals(MSI_DIGITAL_SIGNATURE_EX_ENTRY_NAME)) { 175 continue; 176 } 177 178 POIFSDocument document = new POIFSDocument((DocumentProperty) property, fs); 179 long remaining = document.getSize(); 180 for (ByteBuffer buffer : document) { 181 int size = buffer.remaining(); 182 buffer.limit(buffer.position() + (int) Math.min(remaining, size)); 183 digest.update(buffer); 184 remaining -= size; 185 } 186 } 187 188 // hash the package ClassID, in serialized form 189 byte[] classId = new byte[16]; 190 fs.getRoot().getStorageClsid().write(classId, 0); 191 digest.update(classId); 192 193 return digest.digest(); 194 } 195 196 @Override 197 public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException { 198 AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE); 199 DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm.getMessageDigest())); 200 201 SpcUuid uuid = new SpcUuid("F1100C00-0000-0000-C000-000000000046"); 202 SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_SIPINFO_OBJID, new SpcSipInfo(1, uuid)); 203 204 return new SpcIndirectDataContent(data, digestInfo); 205 } 206 207 @Override 208 public List<CMSSignedData> getSignatures() throws IOException { 209 List<CMSSignedData> signatures = new ArrayList<>(); 210 211 try { 212 DocumentEntry digitalSignature = (DocumentEntry) fs.getRoot().getEntry(DIGITAL_SIGNATURE_ENTRY_NAME); 213 if (digitalSignature != null) { 214 byte[] signatureBytes = IOUtils.toByteArray(new DocumentInputStream(digitalSignature)); 215 try { 216 CMSSignedData signedData = new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(new ASN1InputStream(signatureBytes).readObject())); 217 signatures.add(signedData); 218 219 // look for nested signatures 220 SignerInformation signerInformation = signedData.getSignerInfos().getSigners().iterator().next(); 221 AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); 222 if (unsignedAttributes != null) { 223 Attribute nestedSignatures = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID); 224 if (nestedSignatures != null) { 225 for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) { 226 signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature))); 227 } 228 } 229 } 230 } catch (UnsupportedOperationException e) { 231 // unsupported type, just skip 232 } catch (Exception e) { 233 e.printStackTrace(); 234 } 235 } 236 } catch (FileNotFoundException e) { 237 } 238 239 return signatures; 240 } 241 242 @Override 243 public void setSignature(CMSSignedData signature) throws IOException { 244 byte[] signatureBytes = signature.toASN1Structure().getEncoded("DER"); 245 fs.getRoot().createOrUpdateDocument(DIGITAL_SIGNATURE_ENTRY_NAME, new ByteArrayInputStream(signatureBytes)); 246 } 247 248 @Override 249 public void save() throws IOException { 250 if (channel == null) { 251 fs.writeFilesystem(); 252 } else { 253 channel.position(0); 254 fs.writeFilesystem(Channels.newOutputStream(channel)); 255 channel.truncate(channel.position()); 256 } 257 } 258}