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

com.artipie.debian.misc.GpgClearsign Maven / Gradle / Ivy

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2020 Artipie
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.artipie.debian.misc;

import com.jcabi.log.Logger;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.security.Security;
import java.util.Iterator;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;

/**
 * Gpg signature.
 * @since 0.4
 * @checkstyle ExecutableStatementCountCheck (500 lines)
 * @checkstyle ClassDataAbstractionCouplingCheck (500 lines)
 * @checkstyle InnerAssignmentCheck (500 lines)
 * @todo #29:30min Main functionality of this class was copy-pasted from
 *  https://github.com/bcgit/bc-java/blob/master/pg/src/main/java/org/bouncycastle/openpgp/examples/ClearSignedFileProcessor.java
 *  Let's refactor this class, step by step, remove not necessary functionality, add javadocs, etc.
 *  The job can be performed in several steps, on the last iteration do not forget to remove all
 *  suppressions.
 */
@SuppressWarnings(
    {"PMD.AvoidDuplicateLiterals", "PMD.AssignmentInOperand", "PMD.ArrayIsStoredDirectly"}
)
public final class GpgClearsign {

    /**
     * Bytes content to sign.
     */
    private final byte[] content;

    /**
     * Ctor.
     * @param content Bytes content to sign
     */
    public GpgClearsign(final byte[] content) {
        this.content = content;
    }

    /**
     * Signs content with GPG clearsign signature and returns it along with the signature.
     * @param key Private key bytes
     * @param pass Password
     * @return File, signed with gpg
     */
    public byte[] signedContent(final byte[] key, final String pass) {
        try {
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            final ArmoredOutputStream armored = new ArmoredOutputStream(out);
            try (
                InputStream input = new BufferedInputStream(
                    new ByteArrayInputStream(this.content)
                );
                ByteArrayOutputStream line = new ByteArrayOutputStream()
            ) {
                final PGPSignatureGenerator sgen = GpgClearsign.prepareGenerator(key, pass);
                armored.beginClearText(PGPUtil.SHA256);
                int ahead = readInputLine(line, input);
                GpgClearsign.processLine(armored, sgen, line.toByteArray());
                if (ahead != -1) {
                    do {
                        ahead = GpgClearsign.readInputLine(line, ahead, input);
                        sgen.update((byte) '\r');
                        sgen.update((byte) '\n');
                        GpgClearsign.processLine(armored, sgen, line.toByteArray());
                    }
                    while (ahead != -1);
                }
                armored.endClearText();
                final BCPGOutputStream bout = new BCPGOutputStream(armored);
                sgen.generate().encode(bout);
                armored.close();
                return out.toByteArray();
            }
        } catch (final PGPException err) {
            Logger.error(this, "Error while generating gpg-signature:\n%s", err.getMessage());
            throw new IllegalStateException(err);
        } catch (final IOException err) {
            Logger.error(this, "IO error while generating gpg-signature:\n%s", err.getMessage());
            throw new UncheckedIOException(err);
        }
    }

    /**
     * Signs content with GPG clearsign signature and returns the signature.
     * @param key Private key bytes
     * @param pass Password
     * @return File, signed with gpg
     */
    public byte[] signature(final byte[] key, final String pass) {
        try {
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            final ArmoredOutputStream armored = new ArmoredOutputStream(out);
            try (
                InputStream input = new BufferedInputStream(
                    new ByteArrayInputStream(this.content)
                )
            ) {
                armored.setHeader(ArmoredOutputStream.VERSION_HDR, null);
                final PGPSignatureGenerator sgen = prepareGenerator(key, pass);
                int sym;
                while ((sym = input.read()) >= 0) {
                    sgen.update((byte) sym);
                }
                final BCPGOutputStream res = new BCPGOutputStream(armored);
                sgen.generate().encode(res);
                armored.close();
                return out.toByteArray();
            }
        } catch (final PGPException err) {
            Logger.error(this, "Error while generating gpg-signature:\n%s", err.getMessage());
            throw new IllegalStateException(err);
        } catch (final IOException err) {
            Logger.error(this, "IO error while generating gpg-signature:\n%s", err.getMessage());
            throw new UncheckedIOException(err);
        }
    }

    /**
     * Prepares signature generator.
     * @param key Private key
     * @param pass Password
     * @return Instance of PGPSignatureGenerator
     * @throws IOException On error
     * @throws PGPException On problems with signing
     */
    private static PGPSignatureGenerator prepareGenerator(final byte[] key, final String pass)
        throws IOException, PGPException {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
        final PGPSecretKey skey = readSecretKey(new ByteArrayInputStream(key));
        final PGPPrivateKey pkey = skey.extractPrivateKey(
            new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build(pass.toCharArray())
        );
        final PGPSignatureGenerator sgen = new PGPSignatureGenerator(
            new JcaPGPContentSignerBuilder(skey.getPublicKey().getAlgorithm(), PGPUtil.SHA256)
                .setProvider("BC")
        );
        final PGPSignatureSubpacketGenerator ssgen = new PGPSignatureSubpacketGenerator();
        sgen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, pkey);
        final Iterator ids = skey.getPublicKey().getUserIDs();
        if (ids.hasNext()) {
            ssgen.setSignerUserID(false, ids.next());
            sgen.setHashedSubpackets(ssgen.generate());
        }
        return sgen;
    }

    /**
     * Reads secret key from provided input stream.
     * @param input Input stream to read stream from
     * @return Instance of PGPSecretKey
     * @throws IOException On IO errors
     * @throws PGPException On Keys errors
     */
    private static PGPSecretKey readSecretKey(final InputStream input)
        throws IOException, PGPException {
        final Iterator keys = new PGPSecretKeyRingCollection(
            PGPUtil.getDecoderStream(input), new JcaKeyFingerprintCalculator()
        ).getKeyRings();
        while (keys.hasNext()) {
            final Iterator skey = keys.next().getSecretKeys();
            while (skey.hasNext()) {
                final PGPSecretKey key = skey.next();
                if (key.isSigningKey()) {
                    return key;
                }
            }
        }
        throw new IllegalArgumentException("Can't find signing key in key ring.");
    }

    /**
     * Process line, trailing white space needs to be removed from the end of each line for
     * signature calculation according to RFC 4880 Section 7.1.
     * @param out Where to write
     * @param sign Signature generator
     * @param line Line to process
     * @throws IOException On error
     */
    private static void processLine(final OutputStream out, final PGPSignatureGenerator sign,
        final byte[] line) throws IOException {
        final int length = getLengthWithoutWhiteSpace(line);
        if (length > 0) {
            sign.update(line, 0, length);
        }
        out.write(line, 0, line.length);
    }

    /**
     * Line length without whitespace.
     * @param line Line to measure
     * @return Length
     */
    private static int getLengthWithoutWhiteSpace(final byte[] line) {
        int end = line.length - 1;
        while (end >= 0 && GpgClearsign.isWhiteSpace(line[end])) {
            end = end - 1;
        }
        return end + 1;
    }

    /**
     * Is symbol a whitespace?
     * @param sym Symbol
     * @return True if it is a whitespace
     */
    private static boolean isWhiteSpace(final byte sym) {
        return GpgClearsign.isLineEnding(sym) || sym == '\t' || sym == ' ';
    }

    /**
     * Is symbol an end of the line?
     * @param sym Symbol
     * @return True if it is
     */
    private static boolean isLineEnding(final byte sym) {
        return sym == '\r' || sym == '\n';
    }

    /**
     * Reads input line.
     * @param out Where to write
     * @param input Where to read from
     * @return Symbols ahead
     * @throws IOException On IO error
     */
    private static int readInputLine(final ByteArrayOutputStream out, final InputStream input)
        throws IOException {
        out.reset();
        int ahead = -1;
        int sym;
        while ((sym = input.read()) >= 0) {
            out.write(sym);
            if (GpgClearsign.isLineEnding((byte) sym)) {
                ahead = GpgClearsign.readPassedEol(out, sym, input);
                break;
            }
        }
        return ahead;
    }

    /**
     * Reads input line.
     * @param out Where to write
     * @param ahead Already read
     * @param input Where to read from
     * @return Symbols ahead
     * @throws IOException On IO error
     */
    private static int readInputLine(final ByteArrayOutputStream out, final int ahead,
        final InputStream input) throws IOException {
        out.reset();
        int cnt = ahead;
        int res = ahead;
        do {
            out.write(cnt);
            if (cnt == '\r' || cnt == '\n') {
                res = GpgClearsign.readPassedEol(out, cnt, input);
                break;
            }
        }
        while ((cnt = input.read()) >= 0);
        if (cnt < 0) {
            res = -1;
        }
        return res;
    }

    /**
     * Reads end of line.
     * @param out Where to write
     * @param last Symbol
     * @param input Where to read from
     * @return Symbols ahead
     * @throws IOException On IO error
     */
    private static int readPassedEol(final ByteArrayOutputStream out, final int last,
        final InputStream input) throws IOException {
        int ahead = input.read();
        if (last == '\r' && ahead == '\n') {
            out.write(ahead);
            ahead = input.read();
        }
        return ahead;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy