All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.jsign.script.SignableScript Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2019 Bj?rn Kautler
 * Copyright 2019 Emmanuel Bourg
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.jsign.script;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.ByteOrderMark;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BOMInputStream;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.DigestInfo;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;

import net.jsign.DigestAlgorithm;
import net.jsign.Signable;
import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers;
import net.jsign.asn1.authenticode.SpcAttributeTypeAndOptionalValue;
import net.jsign.asn1.authenticode.SpcIndirectDataContent;

import static java.lang.Math.min;
import static java.nio.charset.StandardCharsets.*;

/**
 * A script (text file) that can be signed.
 *
 * @author Bj?rn Kautler
 * @author Emmanuel Bourg
 * @since 3.0
 */
abstract class SignableScript implements Signable {

    private File file;
    private String content;
    private Charset encoding;
    private byte[] bom;

    /**
     * Create a script.
     * The encoding is assumed to be UTF-8.
     */
    public SignableScript() {
        this.encoding = StandardCharsets.UTF_8;
    }

    /**
     * Create a script from the specified file and load its content.
     * If the file has no byte order mark the encoding is assumed to be UTF-8.
     * 
     * @param file the script
     * @throws IOException if an I/O error occurs
     */
    public SignableScript(File file) throws IOException {
        this(file, StandardCharsets.UTF_8);
    }

    /**
     * Create a script from the specified file and load its content.
     * 
     * @param file     the script
     * @param encoding the encoding of the script if there is no byte order mark (if null UTF-8 is used by default)
     * @throws IOException if an I/O error occurs
     */
    public SignableScript(File file, Charset encoding) throws IOException {
        this.file = file;
        this.encoding = encoding != null ? encoding : StandardCharsets.UTF_8;

        ByteOrderMark[] supportedBOMs = new ByteOrderMark[] { ByteOrderMark.UTF_8, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_16LE };
        try (BOMInputStream in = new BOMInputStream(new BufferedInputStream(new FileInputStream(file)), isByteOrderMarkSigned(), supportedBOMs)) {
            if (in.hasBOM()) {
                this.encoding = Charset.forName(in.getBOMCharsetName());
                if (!isByteOrderMarkSigned()) {
                    bom = in.getBOM().getBytes();
                }
            } else if (StandardCharsets.UTF_8.equals(encoding) && !isUTF8AutoDetected()) {
                // .vbs, .js and .ps1xml files are signed as ISO-8859-1 even when encoded in UTF-8
                this.encoding = StandardCharsets.ISO_8859_1;
            }

            setContent(new String(IOUtils.toByteArray(in), this.encoding));
        }
    }

    /**
     * Tells if the byte order mark (BOM) should be hashed when creating the signature.
     */
    abstract boolean isByteOrderMarkSigned();

    /**
     * Tells if Windows automatically detects bom-less UTF-8 encoded files of this type.
     */
    boolean isUTF8AutoDetected() {
        return true;
    }

    /**
     * Returns the content of the script.
     * 
     * @return the content of the script
     */
    public String getContent() {
        return content;
    }

    /**
     * Sets the content of the script.
     * 
     * @param content the content of the script
     */
    public void setContent(String content) {
        this.content = content;
    }

    abstract String getSignatureStart();

    abstract String getSignatureEnd();

    abstract String getLineCommentStart();

    abstract String getLineCommentEnd();

    abstract ASN1Object getSpcSipInfo();

    private Pattern getSignatureBlockPattern() {
        return Pattern.compile("(?s)" +
                "\\r\\n" +
                getSignatureStart() + "\\r\\n" +
                "(?.*)" +
                getSignatureEnd() + "\\r\\n");
    }

    private Pattern getSignatureBlockRemovalPattern() {
        /** Pattern for removing signatures, even if the file EOL was converted to LF */
        return Pattern.compile("(?s)" +
                "\\r?\\n" +
                getSignatureStart() + "\\r?\\n" +
                ".*" +
                getSignatureEnd() + "\\r?\\n");
    }

    @Override
    public List getSignatures() {
        List signatures = new ArrayList<>();
        
        try {
            CMSSignedData signedData = decodeSignatureBlock();
            if (signedData != null) {
                signatures.add(signedData);

                // look for nested signatures
                SignerInformation signerInformation = signedData.getSignerInfos().getSigners().iterator().next();
                AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
                if (unsignedAttributes != null) {
                    Attribute nestedSignatures = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID);
                    if (nestedSignatures != null) {
                        for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) {
                            signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature)));
                        }
                    }
                }
            }
        } catch (UnsupportedOperationException e) {
            // unsupported type, just skip
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return signatures;
    }

    /**
     * Returns the signature block.
     * 
     * @return the signature block
     */
    private String getSignatureBlock() {
        Matcher matcher = getSignatureBlockPattern().matcher(getContent());
        if (!matcher.find()) {
            return null;
        }
        
        return matcher.group("signatureBlock");
    }

    private CMSSignedData decodeSignatureBlock() throws CMSException {
        String signatureBlock = getSignatureBlock();
        if (signatureBlock == null) {
            return null;
        }
        
        signatureBlock = signatureBlock.replace(getLineCommentStart(), "");
        signatureBlock = signatureBlock.replace(getLineCommentEnd(), "");
        signatureBlock = signatureBlock.replaceAll("\r|\n", "");

        byte[] signatureBytes = Base64.getDecoder().decode(signatureBlock);

        try {
            return new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(new ASN1InputStream(signatureBytes).readObject()));
        } catch (IOException e) {
            throw new IllegalArgumentException("Failed to construct ContentInfo from byte[]: ", e);
        }
    }

    @Override
    public void setSignature(CMSSignedData signature) throws IOException {
        // build the signed script content
        String content = getContentWithoutSignatureBlock();
        int pos = getSignatureInsertionPoint(content);
        
        this.content = content.substring(0, pos) + createSignatureBlock(signature) + content.substring(pos);
    }

    private String createSignatureBlock(CMSSignedData signature) throws IOException {
        // base64 encode the signature blob
        byte[] signatureBytes = signature.toASN1Structure().getEncoded("DER");
        String signatureBlob = Base64.getEncoder().encodeToString(signatureBytes);
        
        StringBuilder signatureBlock = new StringBuilder();
        signatureBlock.append("\r\n");
        signatureBlock.append(getSignatureStart() + "\r\n");
        for (int start = 0, blobLength = signatureBlob.length(); start < blobLength; start += 64) {
            signatureBlock.append(getLineCommentStart());
            signatureBlock.append(signatureBlob, start, min(blobLength, start + 64));
            signatureBlock.append(getLineCommentEnd());
            signatureBlock.append("\r\n");
        }
        signatureBlock.append(getSignatureEnd() + "\r\n");
        
        return signatureBlock.toString();
    }

    protected int getSignatureInsertionPoint(String content) {
        return content.length();
    }

    /**
     * Returns the content stripped from the signature block.
     * 
     * @return the content without the signature
     */
    protected String getContentWithoutSignatureBlock() {
        return getSignatureBlockRemovalPattern().matcher(getContent()).replaceFirst("");
    }

    @Override
    public byte[] computeDigest(MessageDigest digest) {
        digest.update(getContentWithoutSignatureBlock().getBytes(UTF_16LE));
        return digest.digest();
    }

    @Override
    public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException {
        AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE);
        DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm.getMessageDigest()));
        
        SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_SIPINFO_OBJID, getSpcSipInfo());
        
        return new SpcIndirectDataContent(data, digestInfo);
    }

    @Override
    public void save() throws IOException {
        if (file != null) {
            save(file);
        }
    }

    /**
     * Save the script to the specified file.
     * 
     * @param file the file to write
     * @throws IOException if an I/O error occurs
     */
    public void save(File file) throws IOException {
        try (FileOutputStream out = new FileOutputStream(file)) {
            if (bom != null) {
                out.write(bom);
            }
            out.write(getContent().getBytes(encoding));
            out.flush();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy