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}