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.File; 020import java.io.IOException; 021import java.nio.ByteBuffer; 022import java.nio.channels.SeekableByteChannel; 023import java.nio.file.Files; 024import java.nio.file.StandardOpenOption; 025import java.security.MessageDigest; 026import java.util.ArrayList; 027import java.util.List; 028 029import org.bouncycastle.asn1.ASN1Object; 030import org.bouncycastle.asn1.DERNull; 031import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 032import org.bouncycastle.asn1.x509.DigestInfo; 033import org.bouncycastle.cms.CMSSignedData; 034 035import net.jsign.DigestAlgorithm; 036import net.jsign.Signable; 037import net.jsign.SignatureUtils; 038import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; 039import net.jsign.asn1.authenticode.SpcAttributeTypeAndOptionalValue; 040import net.jsign.asn1.authenticode.SpcIndirectDataContent; 041import net.jsign.asn1.authenticode.SpcPeImageData; 042 043import static net.jsign.ChannelUtils.*; 044 045/** 046 * Microsoft Cabinet File. 047 * 048 * This class is thread safe. 049 * 050 * @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> 051 * 052 * @author Joseph Lee 053 * @since 4.0 054 */ 055public class MSCabinetFile implements Signable { 056 057 private final CFHeader header = new CFHeader(); 058 059 private final SeekableByteChannel channel; 060 061 /** 062 * Tells if the specified file is a MS Cabinet file. 063 * 064 * @param file the file to check 065 * @return <code>true</code> if the file is a MS Cabinet, <code>false</code> otherwise 066 * @throws IOException if an I/O error occurs 067 */ 068 public static boolean isMSCabinetFile(File file) throws IOException { 069 if (!file.exists() || !file.isFile()) { 070 return false; 071 } 072 073 try { 074 MSCabinetFile cabFile = new MSCabinetFile(file); 075 cabFile.close(); 076 return true; 077 } catch (IOException e) { 078 if (e.getMessage().contains("Invalid MSCabinet header signature") || e.getMessage().contains("MSCabinet file too short")) { 079 return false; 080 } else { 081 throw e; 082 } 083 } 084 } 085 086 /** 087 * Create a MSCabinetFile from the specified file. 088 * 089 * @param file the file to open 090 * @throws IOException if an I/O error occurs 091 */ 092 public MSCabinetFile(File file) throws IOException { 093 this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE)); 094 } 095 096 /** 097 * Create a MSCabinetFile from the specified channel. 098 * 099 * @param channel the channel to read the file from 100 * @throws IOException if an I/O error occurs 101 */ 102 public MSCabinetFile(SeekableByteChannel channel) throws IOException { 103 this.channel = channel; 104 105 try { 106 channel.position(0); 107 header.read(channel); 108 109 if (header.hasSignature() && header.reserve.structure2.length == CABSignature.SIZE) { 110 CABSignature cabsig = new CABSignature(header.reserve.structure2); 111 if (cabsig.offset < channel.size() && (cabsig.offset + cabsig.length) > channel.size() || cabsig.offset > channel.size()) { 112 throw new IOException("MSCabinet file is corrupt: signature data (offset=" + cabsig.offset + ", size=" + cabsig.length + ") after the end of the file"); 113 } 114 115 if (header.cbCabinet != cabsig.offset) { 116 throw new IOException("MSCabinet file is corrupt: the declared size of the file (" + header.cbCabinet + ") doesn't match the offset of the signature (" + cabsig.offset + ")"); 117 } 118 119 if (header.cbCabinet + cabsig.length != channel.size()) { 120 throw new IOException("MSCabinet file is corrupt: the declared size of the file (" + header.cbCabinet + ") and the size of the signature (" + cabsig.length + ") are inconsistent with the actual size of the file (" + channel.size() + ")"); 121 } 122 } 123 } catch (IOException e) { 124 channel.close(); 125 throw e; 126 } 127 } 128 129 @Override 130 public void close() throws IOException { 131 channel.close(); 132 } 133 134 @Override 135 public synchronized byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException { 136 MessageDigest digest = digestAlgorithm.getMessageDigest(); 137 138 CFReserve modifiedReserve = new CFReserve(); 139 modifiedReserve.minSize = header.cbCFHeader; 140 if (header.reserve != null) { 141 modifiedReserve.structure1 = header.reserve.structure1; 142 } 143 modifiedReserve.structure2 = new byte[CABSignature.SIZE]; 144 145 CFHeader modifiedHeader = new CFHeader(header); 146 modifiedHeader.setReserve(modifiedReserve); 147 modifiedHeader.headerDigestUpdate(digest); 148 149 int shift = modifiedHeader.getHeaderSize() - header.getHeaderSize(); 150 151 channel.position(header.getHeaderSize()); 152 153 for (int i = 0; i < header.cFolders; i++) { 154 CFFolder folder = CFFolder.read(channel); 155 folder.coffCabStart += shift; 156 folder.digest(digest); 157 updateDigest(channel, digest, channel.position(), channel.position() + header.cbCFFolder); 158 } 159 160 long endPosition = header.cbCabinet; 161 updateDigest(channel, digest, channel.position(), endPosition); 162 163 return digest.digest(); 164 } 165 166 @Override 167 public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException { 168 AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE); 169 DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm)); 170 SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_CAB_DATA_OBJID, new SpcPeImageData()); 171 172 return new SpcIndirectDataContent(data, digestInfo); 173 } 174 175 @Override 176 public synchronized List<CMSSignedData> getSignatures() throws IOException { 177 if (header.hasSignature()) { 178 if (header.reserve.structure2.length == CABSignature.SIZE) { 179 CABSignature cabsig = new CABSignature(header.reserve.structure2); 180 if (cabsig.offset > 0 && cabsig.length > 0 && cabsig.length < channel.size()) { 181 byte[] buffer = new byte[(int) cabsig.length]; 182 channel.position(cabsig.offset); 183 channel.read(ByteBuffer.wrap(buffer)); 184 185 return SignatureUtils.getSignatures(buffer); 186 } 187 } else { 188 return SignatureUtils.getSignatures(header.reserve.structure2); 189 } 190 } 191 192 return new ArrayList<>(); 193 } 194 195 @Override 196 public synchronized void setSignature(CMSSignedData signature) throws IOException { 197 if (signature == null && !header.hasSignature()) { 198 return; 199 } 200 201 byte[] content = signature != null ? signature.toASN1Structure().getEncoded("DER") : new byte[0]; 202 203 int previousSize = header.getHeaderSize(); 204 205 CFReserve reserve = new CFReserve(); 206 reserve.minSize = header.cbCFHeader; 207 if (header.reserve != null) { 208 reserve.structure1 = header.reserve.structure1; 209 } 210 211 if (content.length > 0) { 212 reserve.structure2 = new byte[CABSignature.SIZE]; 213 214 header.setReserve(reserve); 215 216 CABSignature cabsig = new CABSignature(); 217 cabsig.offset = header.cbCabinet; 218 cabsig.length = content.length; 219 220 reserve.structure2 = cabsig.array(); 221 } else { 222 reserve.structure2 = new byte[0]; 223 224 header.setReserve(reserve); 225 } 226 227 int currentSize = header.getHeaderSize(); 228 int shift = currentSize - previousSize; 229 230 if (shift > 0) { 231 insert(channel, previousSize, new byte[shift]); 232 } else if (shift < 0) { 233 delete(channel, previousSize + shift, -shift); 234 } 235 236 // rewrite the header 237 header.write(channel); 238 239 if (shift != 0) { 240 // shift the start offset of the CFFOLDER structures 241 for (int i = 0; i < header.cFolders; i++) { 242 long position = channel.position(); 243 CFFolder folder = CFFolder.read(channel); 244 folder.coffCabStart += shift; 245 246 channel.position(position); 247 folder.write(channel); 248 } 249 } 250 251 // write the signature 252 channel.position(header.cbCabinet); 253 channel.write(ByteBuffer.wrap(content)); 254 255 // shrink the file if the new signature is shorter 256 if (channel.position() < channel.size()) { 257 channel.truncate(channel.position()); 258 } 259 } 260 261 @Override 262 public void save() { 263 } 264}