001/*
002 * Copyright 2023 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.navx;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.ByteBuffer;
022import java.nio.ByteOrder;
023import java.nio.channels.SeekableByteChannel;
024import java.nio.file.Files;
025import java.nio.file.StandardOpenOption;
026import java.security.MessageDigest;
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.SpcSipInfo;
042import net.jsign.asn1.authenticode.SpcUuid;
043
044import static net.jsign.ChannelUtils.*;
045
046/**
047 * Microsoft Dynamics 365 extension package (NAVX)
048 *
049 * @author Emmanuel Bourg
050 * @since 6.0
051 */
052public class NAVXFile implements Signable {
053
054    /** The channel used for in-memory signing */
055    private final SeekableByteChannel channel;
056
057    /** The underlying file */
058    private File file;
059
060    /** The file header */
061    private final NAVXHeader header = new NAVXHeader();
062
063    /**
064     * Tells if the specified file is a NAVX file.
065     *
066     * @param file the file to check
067     * @return <code>true</code> if the file is a NAVX file, <code>false</code> otherwise
068     * @throws IOException if an I/O error occurs
069     */
070    public static boolean isNAVXFile(File file) throws IOException {
071        if (file.length() < NAVXHeader.SIZE) {
072            return false;
073        }
074
075        // read the signature
076        try (SeekableByteChannel channel = Files.newByteChannel(file.toPath(), StandardOpenOption.READ)) {
077            ByteBuffer buffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
078            channel.read(buffer);
079            buffer.flip();
080            return buffer.getInt() == NAVXHeader.SIGNATURE;
081        }
082    }
083
084    /**
085     * Create a NAVXFile from the specified file.
086     *
087     * @param file the file to open
088     * @throws IOException if an I/O error occurs
089     */
090    public NAVXFile(File file) throws IOException {
091        this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE));
092    }
093
094    /**
095     * Create a NAVXFile from the specified channel.
096     *
097     * @param channel the channel to read the file from
098     * @throws IOException if an I/O error occurs
099     */
100    public NAVXFile(SeekableByteChannel channel) throws IOException {
101        this.channel = channel;
102
103        channel.position(0);
104        header.read(channel);
105
106        if (header.contentSize + NAVXHeader.SIZE > channel.size()) {
107            throw new IOException("NAVX file is corrupt: invalid size in the header");
108        }
109    }
110
111    @Override
112    public byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException {
113        MessageDigest digest = digestAlgorithm.getMessageDigest();
114        updateDigest(channel, digest, 0, getSignatureOffset());
115        return digest.digest();
116    }
117
118    @Override
119    public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException {
120        AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE);
121        DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm));
122
123        SpcUuid uuid  = new SpcUuid("12341234-F804-0000-781D-123412341234");
124        SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_SIPINFO_OBJID, new SpcSipInfo(1, uuid));
125
126        return new SpcIndirectDataContent(data, digestInfo);
127    }
128
129    private int getSignatureOffset() {
130        return NAVXHeader.SIZE + header.contentSize;
131    }
132
133    @Override
134    public List<CMSSignedData> getSignatures() throws IOException {
135        channel.position(getSignatureOffset());
136        NAVXSignatureBlock signatureBlock = new NAVXSignatureBlock();
137        signatureBlock.read(channel);
138
139        return SignatureUtils.getSignatures(signatureBlock.signedData);
140    }
141
142    @Override
143    public void setSignature(CMSSignedData signature) throws IOException {
144        NAVXSignatureBlock signatureBlock = new NAVXSignatureBlock();
145        signatureBlock.signedData = signature;
146
147        channel.position(getSignatureOffset());
148        signatureBlock.write(channel);
149        channel.truncate(channel.position());
150    }
151
152    @Override
153    public void save() throws IOException {
154    }
155
156    @Override
157    public void close() throws IOException {
158        channel.close();
159    }
160}