001/*
002 * Copyright 2022 Emmanuel Bourg and contributors
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.cat;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.ByteBuffer;
022import java.nio.channels.Channels;
023import java.nio.channels.SeekableByteChannel;
024import java.nio.file.Files;
025import java.nio.file.StandardOpenOption;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.List;
029
030import org.bouncycastle.asn1.ASN1Object;
031import org.bouncycastle.cms.CMSException;
032import org.bouncycastle.cms.CMSSignedData;
033import org.bouncycastle.cms.CMSTypedData;
034import org.bouncycastle.cms.SignerInformationStore;
035import org.bouncycastle.util.CollectionStore;
036
037import net.jsign.DigestAlgorithm;
038import net.jsign.Signable;
039import net.jsign.SignatureUtils;
040
041/**
042 * Windows Catalog file.
043 *
044 * @see <a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/install/catalog-files">Windows Drivers - Catalog Files and Digital Signatures</a>
045 * @since 4.2
046 */
047public class CatalogFile implements Signable {
048
049    private final SeekableByteChannel channel;
050
051    private CMSSignedData signedData;
052
053    /**
054     * Tells if the specified file is a Windows catalog file.
055     *
056     * @param file the file to check
057     * @return <code>true</code> if the file is a Windows catalog, <code>false</code> otherwise
058     */
059    public static boolean isCatalogFile(File file) {
060        if (!file.exists() || !file.isFile()) {
061            return false;
062        }
063
064        try {
065            CatalogFile catFile = new CatalogFile(file);
066            catFile.close();
067            return true;
068        } catch (IOException e) {
069            return false;
070        }
071    }
072
073    /**
074     * Create a Windows catalog from the specified file.
075     *
076     * @param file the file to open
077     * @throws IOException if an I/O error occurs
078     */
079    public CatalogFile(File file) throws IOException {
080        this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE));
081    }
082
083    /**
084     * Create a Windows catalog from the specified channel.
085     *
086     * @param channel the channel to read the file from
087     * @throws IOException if an I/O error occurs
088     */
089    public CatalogFile(SeekableByteChannel channel) throws IOException {
090        this.channel = channel;
091
092        channel.position(0);
093
094        try {
095            signedData = new CMSSignedData(Channels.newInputStream(channel));
096        } catch (CMSException e) {
097            channel.close();
098            throw new IOException("Catalog file format error", e);
099        }
100    }
101
102    @Override
103    public void close() throws IOException {
104        channel.close();
105    }
106
107    @Override
108    public CMSTypedData createSignedContent(DigestAlgorithm digestAlgorithm) {
109        return signedData.getSignedContent();
110    }
111
112    @Override
113    public byte[] computeDigest(DigestAlgorithm digest) {
114        throw new UnsupportedOperationException();
115    }
116
117    @Override
118    public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) {
119        throw new UnsupportedOperationException();
120    }
121
122    @Override
123    public List<CMSSignedData> getSignatures() throws IOException {
124        if (signedData.getSignerInfos().size() > 0) {
125            return SignatureUtils.getSignatures(signedData);
126        } else {
127            return new ArrayList<>();
128        }
129    }
130
131    @Override
132    public void setSignature(CMSSignedData signature) {
133        if (signature != null) {
134            signedData = signature;
135        } else {
136            // remove the signatures and the certificates
137            try {
138                signedData = CMSSignedData.replaceSigners(signedData, new SignerInformationStore(Collections.emptyList()));
139                CollectionStore<?> emptyStore = new CollectionStore<>(Collections.emptyList());
140                signedData = CMSSignedData.replaceCertificatesAndCRLs(signedData, emptyStore, emptyStore, emptyStore);
141            } catch (CMSException e) {
142                throw new RuntimeException(e);
143            }
144        }
145    }
146
147    @Override
148    public void save() throws IOException {
149        channel.position(0);
150        channel.truncate(0);
151        channel.write(ByteBuffer.wrap(signedData.getEncoded("DER")));
152    }
153}