001/*
002 * Copyright 2007-2018 The jdeb developers.
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 org.vafer.jdeb.signing;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.InputStreamReader;
023import java.io.OutputStream;
024import java.nio.charset.Charset;
025import java.security.GeneralSecurityException;
026import java.util.Iterator;
027
028import org.apache.commons.io.LineIterator;
029import org.bouncycastle.bcpg.ArmoredOutputStream;
030import org.bouncycastle.bcpg.BCPGOutputStream;
031import org.bouncycastle.openpgp.PGPException;
032import org.bouncycastle.openpgp.PGPPrivateKey;
033import org.bouncycastle.openpgp.PGPSecretKey;
034import org.bouncycastle.openpgp.PGPSecretKeyRing;
035import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
036import org.bouncycastle.openpgp.PGPSignature;
037import org.bouncycastle.openpgp.PGPSignatureGenerator;
038import org.bouncycastle.openpgp.PGPUtil;
039import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
040import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
041import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
042import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
043
044/**
045 * Signing with OpenPGP.
046 */
047public class PGPSigner {
048
049    private static final byte[] EOL = "\n".getBytes(Charset.forName("UTF-8"));
050
051    private PGPSecretKey secretKey;
052    private PGPPrivateKey privateKey;
053    private int digest;
054
055    public PGPSigner(InputStream keyring, String keyId, String passphrase, int digest) throws IOException, PGPException {
056        secretKey = getSecretKey(keyring, keyId);
057        if(secretKey == null)
058        {
059            throw new PGPException(String.format("Specified key %s does not exist in key ring %s", keyId, keyring));
060        }
061        privateKey = secretKey.extractPrivateKey(new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()).build(passphrase.toCharArray()));
062        this.digest = digest;
063    }
064
065    /**
066     * Creates a clear sign signature over the input data. (Not detached)
067     *
068     * @param input      the content to be signed
069     * @param output     the output destination of the signature
070     */
071    public void clearSign(String input, OutputStream output) throws IOException, PGPException, GeneralSecurityException {
072        clearSign(new ByteArrayInputStream(input.getBytes("UTF-8")), output);
073    }
074
075    /**
076     * Creates a clear sign signature over the input data. (Not detached)
077     *
078     * @param input      the content to be signed
079     * @param output     the output destination of the signature
080     */
081    public void clearSign(InputStream input, OutputStream output) throws IOException, PGPException, GeneralSecurityException {
082
083        PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(new BcPGPContentSignerBuilder(privateKey.getPublicKeyPacket().getAlgorithm(), digest));
084        signatureGenerator.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, privateKey);
085
086        ArmoredOutputStream armoredOutput = new ArmoredOutputStream(output);
087        armoredOutput.beginClearText(digest);
088
089        LineIterator iterator = new LineIterator(new InputStreamReader(input));
090
091        while (iterator.hasNext()) {
092            String line = iterator.nextLine();
093
094            // trailing spaces must be removed for signature calculation (see http://tools.ietf.org/html/rfc4880#section-7.1)
095            byte[] data = trim(line).getBytes("UTF-8");
096
097            armoredOutput.write(data);
098            armoredOutput.write(EOL);
099
100            signatureGenerator.update(data);
101            if (iterator.hasNext()) {
102                signatureGenerator.update(EOL);
103            }
104        }
105
106        armoredOutput.endClearText();
107
108        PGPSignature signature = signatureGenerator.generate();
109        signature.encode(new BCPGOutputStream(armoredOutput));
110
111        armoredOutput.close();
112    }
113
114    /**
115     * Returns the secret key.
116     */
117    public PGPSecretKey getSecretKey()
118    {
119        return secretKey;
120    }
121
122    /**
123     * Returns the private key.
124     */
125    public PGPPrivateKey getPrivateKey()
126    {
127        return privateKey;
128    }
129
130    /**
131     * Returns the secret key matching the specified identifier.
132     *
133     * @param input the input stream containing the keyring collection
134     * @param keyId the 4 bytes identifier of the key
135     */
136    private PGPSecretKey getSecretKey(InputStream input, String keyId) throws IOException, PGPException {
137        PGPSecretKeyRingCollection keyrings = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(input), new JcaKeyFingerprintCalculator());
138
139        Iterator rIt = keyrings.getKeyRings();
140
141        while (rIt.hasNext()) {
142            PGPSecretKeyRing kRing = (PGPSecretKeyRing) rIt.next();
143            Iterator kIt = kRing.getSecretKeys();
144
145            while (kIt.hasNext()) {
146                PGPSecretKey key = (PGPSecretKey) kIt.next();
147
148                if (key.isSigningKey() && String.format("%08x", key.getKeyID() & 0xFFFFFFFFL).equals(keyId.toLowerCase())) {
149                    return key;
150                }
151            }
152        }
153
154        return null;
155    }
156
157    /**
158     * Trim the trailing spaces.
159     *
160     * @param line
161     */
162    private String trim(String line) {
163        char[] chars = line.toCharArray();
164        int len = chars.length;
165
166        while (len > 0) {
167            if (!Character.isWhitespace(chars[len - 1])) {
168                break;
169            }
170            len--;
171        }
172
173        return line.substring(0, len);
174    }
175}