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}