Package com.verhas.licensor

Source Code of com.verhas.licensor.License

package com.verhas.licensor;

import static com.verhas.utils.Sugar.to;
import static com.verhas.utils.Sugar.using;
import static com.verhas.utils.Sugar.in;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.SignatureException;
import java.util.Date;
import java.util.Iterator;
import java.util.Properties;

import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
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.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.jcajce.JcaPGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;

/**
* A License object is (key,value) pair set that can be interpreted arbitrary.
* There is no special meaning how to handle these. The License object however
* can be electronically signed and saved into a file and loaded and verified.
* <p>
* You can use a License object for two purposes:
*
* <ul>
* <li>Create a new license from pure text properties file, from a string or
* build it up programmatically and then sign and save it to a file.
* <li>Load an encoded license, verify it and then query features.
* </ul>
*
* License files in clear text are simple properties text files. Electronic
* signature and coding is applied using the format PGP. The library used to
* handle encryption is BouncyCastle.
*
* @author Peter Verhas
*/
public class License {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }
    protected Properties licenseProperties = null;
    private boolean verified = false;

    /**
     * Set a license feature.
     *
     * @param key the name of the feature
     * @param value the string value of the feature
     */
    public void setFeature(final String key, final String value) {
        if (licenseProperties == null) {
            licenseProperties = new Properties();
        }
        licenseProperties.put(key, value);
    }

    /**
     * Get a license feature.
     *
     * @param key the name of the feature.
     * @return the value of the feature as String
     */
    public String getFeature(final String key) {
        final String feature;
        if (licenseProperties != null && licenseProperties.containsKey(key)) {
            feature = (String) licenseProperties.getProperty(key);
        } else {
            feature = null;
        }
        return feature;
    }

    /**
     * Set the license values from an input stream. The stream has to contain
     * properties.
     *
     * @param is the input stream to read the properties values from
     * @throws IOException when the input stream can not be read
     */
    public void setLicense(final InputStream is) throws IOException {
        verified = false;
        licenseProperties = new Properties();
        licenseProperties.load(is);
    }

    /**
     * Set the license values from a clear text file. The file has to be a
     * properties file.
     *
     * @param file the file to read the license data.
     * @throws IOException when the file can not be read
     */
    public License setLicense(final File file) throws IOException {
        setLicense(new FileInputStream(file));
        return this;
    }

    /**
     * Set the license from clear text format. This text should be a properties
     * format file.
     *
     * @param licenseString the properties content.
     * @throws IOException when the string is badly formatter and therefore the
     * license can not be loaded from the string
     */
    public void setLicense(final String licenseString) throws IOException {
        setLicense(new ByteArrayInputStream(licenseString.getBytes()));
    }

    /**
     * Get the license as clear text. The license has the format that is usual
     * for properties files.
     *
     * @return the license as clear text
     */
    public String getLicenseString() {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            if (licenseProperties != null) {
                licenseProperties.store(baos, "-- license file");
            }
            baos.close();
            return new String(baos.toByteArray());
        } catch (final IOException ex) {
            return "";
        }
    }

    /**
     * Dump the license as clear text into a file.
     *
     * @param fileName the name of the file to dump the license into.
     * @throws IOException
     */
    public void dumpLicense(final String fileName) throws IOException {
        dumpLicense(new File(fileName));
    }

    /**
     * Dump the license as clear text into the file.
     *
     * @param file the file to write the license text.
     * @throws IOException
     */
    public void dumpLicense(final File file) throws IOException {
        dumpLicense(new FileOutputStream(file));
    }

    /**
     * Dump the license as clear text into the output stream.
     *
     * @param os the output stream.
     * @throws IOException
     */
    public void dumpLicense(final OutputStream os) throws IOException {
        if (licenseProperties != null) {
            licenseProperties.store(os, "");
        }
    }

    /**
     * Check if the license was verified. A license verification checks the
     * electronic signature. If this method returns false, your application
     * program should not believe the rights the license may declare.
     *
     * @return true if the license was verified.
     */
    public boolean isVerified() {
        return verified;
    }

    private PGPSecretKey key = null;
    private int hashAlgorithm = PGPUtil.SHA512;

    /**
     * Set the hash algorithm to use to sign the license. The default value is
     * SHA512.
     * <p>
     * Call to this method is needed only when encoding license and only when
     * the default algorithm is not appropriate for some reason or if the caller
     * wants to ensure that the default algorithm is used even when a later
     * version of this library is used.
     * <p>
     * The default algorithm may change in future versions of this class. Note
     * that the default algorithm was SHA1 prior to version 1.0.4
     *
     * @param hashAlgorithm the hash algorithm, a constant from the
     * {@code class org.bouncycastle.openpgp.PGPUtil}. For more information on
     * this class visit <a href=
     * "http://www.bouncycastle.org/docs/pgdocs1.6/org/bouncycastle/openpgp/PGPUtil.html"
     * >Bouncy Castle</a>
     */
    public void setHashAlgorithm(final int hashAlgorithm) {
        this.hashAlgorithm = hashAlgorithm;
    }

    /**
     * Load the secret key to be used to encrypt a license.
     *
     * @param fn the name of the file that contains the key rings.
     * @param keyId the id of the key to be used.
     * @return this
     * @throws java.io.IOException
     * @throws org.bouncycastle.openpgp.PGPException
     */
    public License loadKey(final String fn, final String keyId)
            throws IOException, PGPException {
        loadKey(new File(fn), keyId);
        return this;
    }

    private byte[] publicKeyRing = null;

    /**
     * Load a key ring from a resource file (a file that is packaged into the
     * JAR file). This method invokes
     * {@link #loadKeyRing(InputStream in, byte[] digest)}.
     *
     * @param resourceName the name of the file inside the JAR file with full
     * path.
     * @param digest the SHA512 digest of the key ring.
     * @throws IOException
     */
    public void loadKeyRingFromResource(final String resourceName,
            final byte[] digest) throws IOException {
        loadKeyRing(
                this.getClass().getClassLoader()
                .getResourceAsStream(resourceName), digest);
    }

    /**
     * Load a key ring from a file. This method invokes
     * {@link #loadKeyRing(InputStream in, byte[] digest)}.
     *
     * @param fileName the name of the file
     * @param digest the SHA512 digest of the ring
     * @throws IOException
     * @throws FileNotFoundException
     */
    public License loadKeyRing(final String fileName, final byte[] digest)
            throws FileNotFoundException, IOException {
        loadKeyRing(new File(fileName), digest);
        return this;
    }

    /**
     * Load a key ring from a file. This method invokes
     * {@link #loadKeyRing(InputStream in, byte[] digest)}.
     *
     * @param file the file
     * @param digest the SHA512 digest of the ring
     * @throws IOException
     * @throws FileNotFoundException
     */
    public License loadKeyRing(final File file, final byte[] digest)
            throws FileNotFoundException, IOException {
        loadKeyRing(new FileInputStream(file), digest);
        return this;
    }

    /**
     * Load a key ring from an input stream that can be used to verify a
     * license. The key ring can only be loaded if the it is a proper PGP key
     * ring and the SHA512 digest of the key ring matches the parameter
     * 'digest'. This is a simple mechanism to avoid someone to replace the key
     * ring and have a spoof license to be verified.
     *
     * @param in the input stream where the key ring comes from.
     * @param digest the digest of the key ring. If this parameter is
     * {@code null} then the key ring is loaded no matter of its checksum.
     * @throws IOException
     * @throws IllegalArgumentException when the digest does not match the
     * supplied digest. This means that the JAR file containing the key ring was
     * tampered and the key ring is not the same as the one packaged by the
     * developer. In this case using the key ring is useless to check the
     * authenticity of the license and because of this the key ring is not
     * loaded in such a case.
     */
    public License loadKeyRing(final InputStream in, final byte[] digest)
            throws IOException {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int ch;
        while ((ch = in.read()) >= 0) {
            baos.write(ch);
        }
        publicKeyRing = baos.toByteArray();
        if (digest != null) {
            final byte[] calculatedDigest = calculatePublicKeyRingDigest();
            for (int i = 0; i < calculatedDigest.length; i++) {
                if (calculatedDigest[i] != (digest[i])) {
                    publicKeyRing = null;
                    throw new IllegalArgumentException(
                            "Key ring digest does not match.");
                }
            }
        }
        return this;
    }

    /**
     * Calculate the SHA512 digest of the public key ring that is used to decode
     * the license.
     *
     * @return the digest as a byte array.
     */
    public byte[] calculatePublicKeyRingDigest() {
        final SHA512Digest dig = new SHA512Digest();
        dig.reset();
        dig.update(publicKeyRing, 0, publicKeyRing.length);
        final byte[] digest = new byte[dig.getDigestSize()];
        dig.doFinal(digest, 0);
        return digest;
    }

    private static int convertByteToInt(byte b) {
        return (int) b & 0xff;
    }

    /**
     * Dump the public key ring digest as a Java code fragment. You can copy
     * this string into your licensed code that calls {@code loadKeyRing} to
     * protect the code from key ring replacement.
     *
     * @return the Java program code fragment as string.
     */
    public String dumpPublicKeyRingDigest() {
        final byte[] calculatedDigest = calculatePublicKeyRingDigest();
        String retval = "byte [] digest = new byte[] {\n";
        for (int i = 0; i < calculatedDigest.length; i++) {
            int intVal = convertByteToInt(calculatedDigest[i]);
            retval += String.format("(byte)0x%02X, ", intVal);
            if (i % 8 == 0) {
                retval += "\n";
            }
        }
        retval += "\n};\n";
        return retval;
    }

    /**
     * Load the secret key to be used to encrypt a license. This is a
     * complimentary method that calls
     * {@link #loadKey(InputStream in, String userId)}.
     *
     * @param fin the file that contains the key ring.
     * @param userId the user id of the key to be used.
     * @throws java.io.IOException
     * @throws org.bouncycastle.openpgp.PGPException
     * @return this
     */
    public License loadKey(final File fin, final String userId)
            throws IOException, PGPException {
        loadKey(new FileInputStream(fin), userId);
        return this;
    }

    private boolean keyIsAppropriate(String userId, String keyUserId,
            PGPSecretKey k) {
        return k.isSigningKey() && (userId == null || userId.equals(keyUserId));
    }

    /**
     * Load the secret key to be used to encrypt the license. After the key is
     * loaded it can be used to encrypt license files.
     *
     * @param inputStream input stream of the file containing the key rings
     * @param userId the user id of the key. If this parameter is {@code null}
     * then the first key on the key ring appropriate to sign will be used.
     * @return this
     * @throws java.io.IOException
     * @throws org.bouncycastle.openpgp.PGPException
     */
    @SuppressWarnings("unchecked")
    public License loadKey(final InputStream inputStream, final String userId)
            throws IOException, PGPException {
        final InputStream decoderInputStream = PGPUtil.getDecoderStream(inputStream);

        final PGPSecretKeyRingCollection pgpSec = new JcaPGPSecretKeyRingCollection(decoderInputStream);

        key = null;
        for (final PGPSecretKeyRing kRing : in((Iterator<PGPSecretKeyRing>) pgpSec
                .getKeyRings())) {
            for (final PGPSecretKey k : in((Iterator<PGPSecretKey>) kRing
                    .getSecretKeys())) {
                for (final String keyUserId : in((Iterator<String>) k
                        .getUserIDs())) {
                    if (keyIsAppropriate(userId, keyUserId, k)) {
                        key = k;
                        return this;
                    }
                }
            }
        }

        throw new IllegalArgumentException(
                "Can't find signing key in key ring.");
    }

    private PGPSignatureGenerator createPGPSignatureGenerator(final char[] keyPassPhrase) throws PGPException {
        final PGPContentSignerBuilder csBuilder = new JcaPGPContentSignerBuilder(key
                .getPublicKey().getAlgorithm(), hashAlgorithm);
        final PGPSignatureGenerator generator = new PGPSignatureGenerator(csBuilder);
        final PGPPrivateKey privateKey = extractPGPPrivateKey(keyPassPhrase);

        init(generator, privateKey);
        return generator;
    }

    private PGPPrivateKey extractPGPPrivateKey(final char[] keyPassPhrase) throws PGPException {
        PGPDigestCalculatorProvider calcProvider = new JcaPGPDigestCalculatorProviderBuilder().
                setProvider(BouncyCastleProvider.PROVIDER_NAME).
                build();
        PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder(calcProvider).
                setProvider(BouncyCastleProvider.PROVIDER_NAME).
                build(keyPassPhrase);
        final PGPPrivateKey pgpPrivKey = key.extractPrivateKey(decryptor);
        return pgpPrivKey;
    }

    private void setHashedSubpackets(final PGPSignatureGenerator signatureGenerator) {
        final Iterator<String> it = key.getPublicKey().getUserIDs();
        if (it.hasNext()) {
            final PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator();
            generator.setSignerUserID(false, it.next());
            signatureGenerator.setHashedSubpackets(generator.generate());
        }
    }

    private void init(final PGPSignatureGenerator signatureGenerator, final PGPPrivateKey pgpPrivKey) throws PGPException {
        signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, pgpPrivKey);
        setHashedSubpackets(signatureGenerator);
    }

    private PGPCompressedDataGenerator createZlibCompressedDataGenerator() {
        return new PGPCompressedDataGenerator(PGPCompressedData.ZLIB);
    }

    private void encode(final String licensePlain,
            final PGPSignatureGenerator signatureGenerator,
            OutputStream outputStream) throws UnsupportedEncodingException, IOException {

        final PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
        final OutputStream literalDataStream = literalDataGenerator.open(outputStream, PGPLiteralData.BINARY,
                "licenseFileName-Ignored", new Date(), new byte[1024]);
        final InputStream fIn = new ByteArrayInputStream(
                licensePlain.getBytes("utf-8"));
        int ch = 0;
        while ((ch = fIn.read()) >= 0) {
            literalDataStream.write(ch);
            signatureGenerator.update((byte) ch);
        }
        literalDataGenerator.close();
    }

    /**
     * Encode the currently loaded/created license.
     *
     * @param keyPassPhraseString the pass phrase to the signing key that was
     * loaded.
     * @return the license encoded as ascii string.
     * @throws java.io.IOException
     * @throws java.security.NoSuchAlgorithmException
     * @throws java.security.NoSuchProviderException
     * @throws org.bouncycastle.openpgp.PGPException
     * @throws java.security.SignatureException
     */
    public String encodeLicense(final String keyPassPhraseString)
            throws IOException, NoSuchAlgorithmException,
            NoSuchProviderException, PGPException, SignatureException {

        final String licensePlain = getLicenseString();
        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        final OutputStream armoredOutputStream = new ArmoredOutputStream(byteArrayOutputStream);
        final PGPSignatureGenerator signatureGenerator = createPGPSignatureGenerator(keyPassPhraseString.toCharArray());

        final PGPCompressedDataGenerator generator = createZlibCompressedDataGenerator();

        final BCPGOutputStream outputStream = new BCPGOutputStream(generator.open(armoredOutputStream));

        signatureGenerator.generateOnePassVersion(false).encode(outputStream);
        encode(licensePlain, using(signatureGenerator), to(outputStream));
        signatureGenerator.generate().encode(outputStream);
        generator.close();
        armoredOutputStream.close();
        return new String(byteArrayOutputStream.toByteArray());
    }

    /**
     * Open an encoded license from the string literal.
     *
     * @param licenseStringEncoded the license encoded in string format.
     * @return this
     * @throws PGPException
     * @throws IOException
     * @throws UnsupportedEncodingException
     */
    public License setLicenseEncoded(final String licenseStringEncoded)
            throws UnsupportedEncodingException, IOException, PGPException {
        setLicenseEncoded(new ByteArrayInputStream(
                licenseStringEncoded.getBytes("utf-8")));
        return this;
    }

    /**
     * Open an encoded license from a Java resource. Use this method when the
     * license is inside the JAR file of the shipped code.
     *
     * @param resourceName the name of the resource that contains the license.
     * @return this
     * @throws PGPException
     * @throws IOException
     */
    public License setLicenseEncodedFromResource(final String resourceName)
            throws IOException, PGPException {
        setLicenseEncoded(License.class.getClassLoader().getResourceAsStream(
                resourceName));
        return this;
    }

    /**
     * Open an encoded license from a file.
     *
     * @param fileName the name of the file containing the encoded license.
     * @return this
     * @throws PGPException
     * @throws IOException
     * @throws FileNotFoundException
     */
    public License setLicenseEncodedFromFile(final String fileName)
            throws FileNotFoundException, IOException, PGPException {
        setLicenseEncoded(new File(fileName));
        return this;
    }

    /**
     * Deprecated. Use {@link #setLicenseEncoded(File file)}.
     *
     * @param file
     * @throws PGPException
     * @throws IOException
     * @throws FileNotFoundException
     */
    @Deprecated
    public void setLicenseEncodedFromFile(final File file)
            throws FileNotFoundException, IOException, PGPException {
        setLicenseEncoded(file);
    }

    /**
     * Open an encoded license file.
     *
     * @param file the file where the encoded license is.
     * @throws PGPException
     * @throws IOException
     * @throws FileNotFoundException
     */
    public void setLicenseEncoded(final File file)
            throws FileNotFoundException, IOException, PGPException {
        setLicenseEncoded(new FileInputStream(file));
    }

    private Long decodeKeyId = null;

    /**
     * Get the key id that was used to decode the license. The protected code
     * may need this id to check the key security level. The protection may
     * store different keys in different location. For example the key to
     * generate expiring demo licenses may be stored on the public server to let
     * it automatically generate demo keys.
     * <p>
     * The key used to generate commercial licenses is stored in a bunker, deep
     * down under the ground between steel walls.
     * <p>
     * In such a situation the protected program will not accept a commercial
     * non expiring license (that could have been stolen) signed by the demo
     * license key.
     *
     * @return the key id.
     */
    public Long getDecodeKeyId() {
        return decodeKeyId;
    }

    private static void pgpAssertNotNull(Object o) throws PGPException {
        if (o == null) {
            throw new PGPException("can not decode");
        }
    }

    /**
     * Open an encoded license from input stream, decode and load it. If the
     * file can not be loaded or is not signed properly then the method
     * {@link #isVerified()} will return false.
     * <p>
     * Otherwise the license is loaded and can be used.
     *
     * @param inputStream
     * @throws IOException
     * @throws PGPException
     */
    public void setLicenseEncoded(InputStream inputStream) throws IOException,
            PGPException {
        final ByteArrayInputStream keyIn = new ByteArrayInputStream(
                publicKeyRing);
        final InputStream decoderInputStream = PGPUtil.getDecoderStream(inputStream);

        PGPObjectFactory pgpFact = new JcaPGPObjectFactory(decoderInputStream);
        final PGPCompressedData c1 = (PGPCompressedData) pgpFact.nextObject();
        pgpAssertNotNull(c1);
        pgpFact = new JcaPGPObjectFactory(c1.getDataStream());
        final PGPOnePassSignatureList p1 = (PGPOnePassSignatureList) pgpFact
                .nextObject();

        pgpAssertNotNull(p1);
        final PGPOnePassSignature ops = p1.get(0);
        final PGPLiteralData p2 = (PGPLiteralData) pgpFact.nextObject();

        pgpAssertNotNull(p2);
        final InputStream dIn = p2.getInputStream();
        pgpAssertNotNull(dIn);
        int ch;
        final PGPPublicKeyRingCollection pgpRing = new PGPPublicKeyRingCollection(
                PGPUtil.getDecoderStream(keyIn));
        pgpAssertNotNull(ops);
        decodeKeyId = ops.getKeyID();
        if (decodeKeyId == null) {
            // there is no key in the key ring that can decode the license
            verified = false;
            licenseProperties = null;
        } else {
            final PGPPublicKey decodeKey = pgpRing.getPublicKey(decodeKeyId);
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            try {
                final PGPContentVerifierBuilderProvider cvBuilder = new JcaPGPContentVerifierBuilderProvider();
                ops.init(cvBuilder, decodeKey);
                while ((ch = dIn.read()) >= 0) {
                    ops.update((byte) ch);
                    out.write(ch);
                }
                final PGPSignatureList p3 = (PGPSignatureList) pgpFact
                        .nextObject();

                if (ops.verify(p3.get(0))) {
                    setLicense(new String(out.toByteArray()));
                    verified = true;
                } else {
                    verified = false;
                    licenseProperties = null;
                }
            } catch (final Exception e) {
                verified = false;
                licenseProperties = null;
            }
        }
    }

    public static String fromResource(String resourceName) {
        return License.class.getClassLoader().getResource(resourceName)
                .getFile();
    }
}
TOP

Related Classes of com.verhas.licensor.License

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.