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.mscab; 018 019import java.io.Closeable; 020import java.io.File; 021import java.io.IOException; 022import java.nio.ByteBuffer; 023import java.nio.ByteOrder; 024import java.nio.channels.SeekableByteChannel; 025import java.nio.file.Files; 026import java.nio.file.StandardOpenOption; 027import java.security.MessageDigest; 028import java.util.ArrayList; 029import java.util.List; 030 031import org.bouncycastle.asn1.ASN1Encodable; 032import org.bouncycastle.asn1.ASN1InputStream; 033import org.bouncycastle.asn1.ASN1Object; 034import org.bouncycastle.asn1.DERNull; 035import org.bouncycastle.asn1.cms.Attribute; 036import org.bouncycastle.asn1.cms.AttributeTable; 037import org.bouncycastle.asn1.cms.ContentInfo; 038import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 039import org.bouncycastle.asn1.x509.DigestInfo; 040import org.bouncycastle.cms.CMSException; 041import org.bouncycastle.cms.CMSProcessable; 042import org.bouncycastle.cms.CMSSignedData; 043import org.bouncycastle.cms.SignerInformation; 044 045import net.jsign.DigestAlgorithm; 046import net.jsign.Signable; 047import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; 048import net.jsign.asn1.authenticode.SpcAttributeTypeAndOptionalValue; 049import net.jsign.asn1.authenticode.SpcIndirectDataContent; 050import net.jsign.asn1.authenticode.SpcPeImageData; 051 052import static net.jsign.ChannelUtils.*; 053 054/** 055 * Microsoft Cabinet File. 056 * 057 * This class is thread safe. 058 * 059 * @see <a href="http://download.microsoft.com/download/5/0/1/501ED102-E53F-4CE0-AA6B-B0F93629DDC6/Exchange/%5BMS-CAB%5D.pdf">[MS-CAB]: Cabinet File Format</a> 060 * 061 * @author Joseph Lee 062 * @since 4.0 063 */ 064public class MSCabinetFile implements Signable, Closeable { 065 066 private final CFHeader header = new CFHeader(); 067 068 private final SeekableByteChannel channel; 069 070 /** 071 * Tells if the specified file is a MS Cabinet file. 072 * 073 * @param file the file to check 074 * @return <code>true</code> if the file is a MS Cabinet, <code>false</code> otherwise 075 * @throws IOException if an I/O error occurs 076 */ 077 public static boolean isMSCabinetFile(File file) throws IOException { 078 if (!file.exists() || !file.isFile()) { 079 return false; 080 } 081 082 try { 083 MSCabinetFile cabFile = new MSCabinetFile(file); 084 cabFile.close(); 085 return true; 086 } catch (IOException e) { 087 if (e.getMessage().contains("MSCabinet header signature not found") || e.getMessage().contains("MSCabinet file too short")) { 088 return false; 089 } else { 090 throw e; 091 } 092 } 093 } 094 095 /** 096 * Create a MSCabinetFile from the specified file. 097 * 098 * @param file the file to open 099 * @throws IOException if an I/O error occurs 100 */ 101 public MSCabinetFile(File file) throws IOException { 102 this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE)); 103 } 104 105 /** 106 * Create a MSCabinetFile 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 MSCabinetFile(SeekableByteChannel channel) throws IOException { 112 this.channel = channel; 113 114 channel.position(0); 115 header.read(channel); 116 117 if (header.csumHeader != 0) { 118 throw new IOException("MSCabinet file is corrupt: invalid reserved field in the header"); 119 } 120 121 if (header.isReservePresent()) { 122 if (header.cbCFHeader != CABSignature.SIZE) { 123 throw new IOException("MSCabinet file is corrupt: cabinet reserved area size is " + header.cbCFHeader + " instead of " + CABSignature.SIZE); 124 } 125 126 CABSignature cabsig = header.getSignature(); 127 if (cabsig.header != CABSignature.HEADER) { 128 throw new IOException("MSCabinet file is corrupt: signature header is " + cabsig.header); 129 } 130 131 if (cabsig.offset < channel.size() && (cabsig.offset + cabsig.length) > channel.size() || cabsig.offset > channel.size()) { 132 throw new IOException("MSCabinet file is corrupt: signature data (offset=" + cabsig.offset + ", size=" + cabsig.length + ") after the end of the file"); 133 } 134 } 135 } 136 137 @Override 138 public void close() throws IOException { 139 channel.close(); 140 } 141 142 @Override 143 public synchronized byte[] computeDigest(MessageDigest digest) throws IOException { 144 CFHeader modifiedHeader = new CFHeader(header); 145 if (!header.isReservePresent()) { 146 modifiedHeader.cbCFHeader = CABSignature.SIZE; 147 modifiedHeader.cbCabinet += 4 + CABSignature.SIZE; 148 modifiedHeader.coffFiles += 4 + CABSignature.SIZE; 149 modifiedHeader.flags |= CFHeader.FLAG_RESERVE_PRESENT; 150 151 CABSignature cabsig = new CABSignature(); 152 cabsig.offset = (int) modifiedHeader.cbCabinet; 153 154 modifiedHeader.abReserved = cabsig.array(); 155 } 156 modifiedHeader.headerDigestUpdate(digest); 157 158 channel.position(header.getHeaderSize()); 159 160 if (header.hasPreviousCabinet()) { 161 digest.update(readNullTerminatedString(channel)); // szCabinetPrev 162 digest.update(readNullTerminatedString(channel)); // szDiskPrev 163 } 164 165 if (header.hasNextCabinet()) { 166 digest.update(readNullTerminatedString(channel)); // szCabinetNext 167 digest.update(readNullTerminatedString(channel)); // szDiskNext 168 } 169 170 for (int i = 0; i < header.cFolders; i++) { 171 CFFolder folder = CFFolder.read(channel); 172 if (!header.isReservePresent()) { 173 folder.coffCabStart += 4 + CABSignature.SIZE; 174 } 175 folder.digest(digest); 176 } 177 178 long endPosition = header.hasSignature() ? header.getSignature().offset : channel.size(); 179 updateDigest(channel, digest, channel.position(), endPosition); 180 181 return digest.digest(); 182 } 183 184 @Override 185 public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException { 186 AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE); 187 DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm.getMessageDigest())); 188 SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_CAB_DATA_OBJID, new SpcPeImageData()); 189 190 return new SpcIndirectDataContent(data, digestInfo); 191 } 192 193 @Override 194 public synchronized List<CMSSignedData> getSignatures() throws IOException { 195 List<CMSSignedData> signatures = new ArrayList<>(); 196 try { 197 CABSignature cabsig = header.getSignature(); 198 if (cabsig != null && cabsig.offset > 0) { 199 byte[] buffer = new byte[(int) cabsig.length]; 200 channel.position(cabsig.offset); 201 channel.read(ByteBuffer.wrap(buffer)); 202 203 CMSSignedData signedData; 204 signedData = new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(new ASN1InputStream(buffer).readObject())); 205 signatures.add(signedData); 206 207 SignerInformation signerInformation = signedData.getSignerInfos().getSigners().iterator().next(); 208 AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); 209 if (unsignedAttributes != null) { 210 Attribute nestedSignatures = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID); 211 if (nestedSignatures != null) { 212 for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) { 213 signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature))); 214 } 215 } 216 } 217 } 218 } catch (CMSException e) { 219 throw new IOException(e); 220 } 221 return signatures; 222 } 223 224 @Override 225 public synchronized void setSignature(CMSSignedData signature) throws IOException { 226 byte[] content = signature.toASN1Structure().getEncoded("DER"); 227 228 int shift = 0; 229 230 if (!header.isReservePresent()) { 231 shift = 4 + CABSignature.SIZE; 232 insert(channel, CFHeader.BASE_SIZE, new byte[shift]); 233 234 header.cbCFHeader = CABSignature.SIZE; 235 header.cbCabinet += shift; 236 header.coffFiles += shift; 237 header.flags |= CFHeader.FLAG_RESERVE_PRESENT; 238 header.abReserved = new byte[CABSignature.SIZE]; 239 } 240 241 CABSignature cabsig = new CABSignature(header.abReserved); 242 cabsig.header = CABSignature.HEADER; 243 cabsig.offset = (int) header.cbCabinet; 244 cabsig.length = content.length; 245 header.abReserved = cabsig.array(); 246 247 // rewrite the header 248 channel.position(0); 249 ByteBuffer buffer = ByteBuffer.allocate(header.getHeaderSize()).order(ByteOrder.LITTLE_ENDIAN); 250 header.write(buffer); 251 buffer.flip(); 252 channel.write(buffer); 253 254 // skip the previous/next cabinet names 255 if (header.hasPreviousCabinet()) { 256 readNullTerminatedString(channel); // szCabinetPrev 257 readNullTerminatedString(channel); // szDiskPrev 258 } 259 260 if (header.hasNextCabinet()) { 261 readNullTerminatedString(channel); // szCabinetNext 262 readNullTerminatedString(channel); // szDiskNext 263 } 264 265 // shift the start offset of the CFFOLDER structures 266 for (int i = 0; i < header.cFolders; i++) { 267 long position = channel.position(); 268 CFFolder folder = CFFolder.read(channel); 269 folder.coffCabStart += shift; 270 271 channel.position(position); 272 folder.write(channel); 273 } 274 275 // write the signature 276 channel.position(cabsig.offset); 277 channel.write(ByteBuffer.wrap(content)); 278 279 // shrink the file if the new signature is shorter 280 if (channel.position() < channel.size()) { 281 channel.truncate(channel.position()); 282 } 283 } 284 285 @Override 286 public void save() { 287 } 288}