com.navnorth.learningregistry.LRSigner.java Source code

Java tutorial

Introduction

Here is the source code for com.navnorth.learningregistry.LRSigner.java

Source

/**
 * 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 com.navnorth.learningregistry;

import com.navnorth.learningregistry.util.StringUtil;

import java.util.Map;

import java.io.InputStream;
import java.io.FileInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.File;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.Security;
import java.security.SignatureException;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.openpgp.*;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;

import org.ardverk.coding.*;

/**
 * Signer for Learning Registry envelopes
 *
 * @author Todd Brown / Navigation North
 *         <br>
 *         Copyright  2011 Navigation North Learning Solutions LLC
 *         <br>
 *         Licensed under the Apache License, Version 2.0 (the "License"); See LICENSE
 *         and README.md files distributed with this work for additional information
 *         regarding copyright ownership.
 * @version 0.1.1
 * @since 2011-12-08
 */
public class LRSigner {
    private static final String pgpRegex = "(?s).*-----BEGIN PGP PRIVATE KEY BLOCK-----.*-----END PGP PRIVATE KEY BLOCK-----.*";
    private static final String signingMethod = "LR-PGP.1.0";

    private String publicKeyLocation;
    private String privateKey;
    private String passPhrase;

    /**
     * Creates a signer, using specified key values
     *
     * @param publicKeyLocation location of the public key to be included in signed envelopes
     * @param privateKey        local location or raw value of private key to be used for encoding
     * @param passPhrase        pass phrase for signing with the included private key
     */
    public LRSigner(String publicKeyLocation, String privateKey, String passPhrase) {
        this.publicKeyLocation = publicKeyLocation;
        this.privateKey = privateKey;
        this.passPhrase = passPhrase;

        passPhrase = StringUtil.nullifyBadInput(passPhrase);
        publicKeyLocation = StringUtil.nullifyBadInput(publicKeyLocation);
        privateKey = StringUtil.nullifyBadInput(privateKey);
    }

    /**
     * Sign the specified envelope with this signer
     *
     * @param envelope envelope to be signed
     * @return signed envelope
     * @throws LRException
     */
    public LREnvelope sign(LREnvelope envelope) throws LRException {
        // Bencode the document
        String bencodedMessage = bencode(envelope.getSignableData());

        // Clear sign the bencoded document
        String clearSignedMessage = signEnvelopeData(bencodedMessage);

        envelope.addSigningData(signingMethod, publicKeyLocation, clearSignedMessage);

        return envelope;
    }

    /**
     * Sign the specified activity (envelope) with this signer
     *
     * @param envelope envelope to be signed
     * @return signed envelope
     * @throws LRException
     */
    public LRActivity sign(LRActivity envelope) throws LRException {
        // Bencode the document
        String bencodedMessage = bencode(envelope.getSignableData());

        // Clear sign the bencoded document
        String clearSignedMessage = signEnvelopeData(bencodedMessage);

        envelope.addSigningData(signingMethod, publicKeyLocation, clearSignedMessage);

        return envelope;
    }

    /**
     * Bencodes document
     *
     * @param doc Document to be bencoded
     * @return Bencoded string of the provided document
     * @throws LRException BENCODE_FAILED if document cannot be bencoded
     */
    private String bencode(Map<String, Object> doc) throws LRException {
        String text = "";
        String encodedString = "";

        // Bencode the provided document

        try {
            ByteArrayOutputStream s = new ByteArrayOutputStream();
            BencodingOutputStream bencoder = new BencodingOutputStream(s);
            bencoder.writeMap(doc);
            bencoder.flush();
            encodedString = s.toString();
            s.close();

            // Hash the bencoded document
            MessageDigest md;

            md = MessageDigest.getInstance("SHA-256");

            md.update(encodedString.getBytes());
            byte[] mdbytes = md.digest();
            StringBuffer hexString = new StringBuffer();
            for (int i = 0; i < mdbytes.length; i++) {
                String hex = Integer.toHexString(0xFF & mdbytes[i]);
                if (hex.length() == 1) {
                    hexString.append('0');
                }
                hexString.append(hex);
            }
            text = hexString.toString();
        } catch (Exception e) {
            throw new LRException(LRException.BENCODE_FAILED);
        }

        return text;
    }

    /**
     * Encodes the provided message with the private key and pass phrase set in configuration
     *
     * @param message Message to encode
     * @return Encoded message
     * @throws LRException SIGNING_FAILED if the document cannot be signed, NO_KEY if the key cannot be obtained
     */
    private String signEnvelopeData(String message) throws LRException {
        // Throw an exception if any of the required fields are null
        if (passPhrase == null || publicKeyLocation == null || privateKey == null) {
            throw new LRException(LRException.NULL_FIELD);
        }

        // Add the provider here so that after signing, we can remove the provider.
        // This allows using this code from multiple separate class loaders while Bouncy Castle is on a separate class loader
        BouncyCastleProvider provider = new BouncyCastleProvider();
        Security.addProvider(provider);

        try {

            // Get an InputStream for the private key
            InputStream privateKeyStream = getPrivateKeyStream(privateKey);

            // Get an OutputStream for the result
            ByteArrayOutputStream result = new ByteArrayOutputStream();
            ArmoredOutputStream aOut = new ArmoredOutputStream(result);

            // Get the pass phrase
            char[] privateKeyPassword = passPhrase.toCharArray();

            try {
                // Get the private key from the InputStream
                PGPSecretKey sk = readSecretKey(privateKeyStream);
                PGPPrivateKey pk = sk.extractPrivateKey(
                        new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build(privateKeyPassword));
                PGPSignatureGenerator sGen = new PGPSignatureGenerator(
                        new JcaPGPContentSignerBuilder(sk.getPublicKey().getAlgorithm(), PGPUtil.SHA256)
                                .setProvider("BC"));
                PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();

                // Clear sign the message
                java.util.Iterator it = sk.getPublicKey().getUserIDs();
                if (it.hasNext()) {
                    spGen.setSignerUserID(false, (String) it.next());
                    sGen.setHashedSubpackets(spGen.generate());
                }
                aOut.beginClearText(PGPUtil.SHA256);
                sGen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, pk);
                byte[] msg = message.getBytes();
                sGen.update(msg, 0, msg.length);
                aOut.write(msg, 0, msg.length);
                BCPGOutputStream bOut = new BCPGOutputStream(aOut);
                aOut.endClearText();
                sGen.generate().encode(bOut);
                aOut.close();

                String strResult = result.toString("utf8");

                // for whatever reason, bouncycastle is failing to put a linebreak before "-----BEGIN PGP SIGNATURE"
                strResult = strResult.replaceAll("([a-z0-9])-----BEGIN PGP SIGNATURE-----",
                        "$1\n-----BEGIN PGP SIGNATURE-----");

                return strResult;
            } catch (Exception e) {
                throw new LRException(LRException.SIGNING_FAILED, e);
            } finally {
                try {
                    if (privateKeyStream != null) {
                        privateKeyStream.close();
                    }

                    result.close();
                } catch (IOException e) {
                    //Could not close the streams
                }
            }
        } finally {
            Security.removeProvider(provider.getName());
        }
    }

    /**
     * Reads private key from the provided InputStream
     *
     * @param input InputStream of the private key
     * @return PGPSecretKey
     * @throws LRException NO_KEY error if the key cannot be obtained from the input stream
     */
    private PGPSecretKey readSecretKey(InputStream input) throws LRException {
        PGPSecretKeyRingCollection pgpSec;

        try {
            pgpSec = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(input));
        } catch (Exception e) {
            throw new LRException(LRException.NO_KEY);
        }

        java.util.Iterator keyRingIter = pgpSec.getKeyRings();
        while (keyRingIter.hasNext()) {
            PGPSecretKeyRing keyRing = (PGPSecretKeyRing) keyRingIter.next();

            java.util.Iterator keyIter = keyRing.getSecretKeys();
            while (keyIter.hasNext()) {
                PGPSecretKey key = (PGPSecretKey) keyIter.next();

                if (key.isSigningKey()) {
                    return key;
                }
            }
        }

        throw new LRException(LRException.NO_KEY);
    }

    /**
     * Converts the local location or text of a private key into an input stream
     *
     * @param privateKey The local location or text of the private key for the digital signature
     * @return Private key stream
     * @throws LRException NO_KEY_STREAM if the private key cannot be turned into a stream
     */
    private InputStream getPrivateKeyStream(String privateKey) throws LRException {
        try {
            // If the private key matches the form of a private key string, treat it as such
            if (privateKey.matches(pgpRegex)) {
                return new ByteArrayInputStream(privateKey.getBytes());
            }
            // Otherwise, treat it as a file location on the local disk
            else {
                return new FileInputStream(new File(privateKey));
            }
        } catch (IOException e) {
            throw new LRException(LRException.NO_KEY_STREAM);
        }
    }
}