org.structr.util.StructrLicenseVerifier.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.util.StructrLicenseVerifier.java

Source

/**
 * Copyright (C) 2010-2018 Structr GmbH
 *
 * This file is part of Structr <http://structr.org>.
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Structr.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.structr.util;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 */
public class StructrLicenseVerifier {

    private static final Logger logger = LoggerFactory.getLogger(StructrLicenseVerifier.class);

    private static final Pattern HostIdPattern = Pattern.compile("[a-f0-9]{32}");
    private static final Pattern DatePattern = Pattern.compile("[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}");
    private static final Pattern NamePattern = Pattern.compile("[a-zA-Z0-9 \\-]+");

    private Gson gson = null;
    private Signature signer = null;
    private KeyStore keyStore = null;
    private Cipher streamCipher = null;
    private Cipher blockCipher = null;
    private Key key = null;

    public static void main(final String[] args) {

        if (args.length < 2) {

            System.out.println("Parameters: keystoreFileName password");
            System.exit(0);
        }

        final String keystoreFileName = args[0];
        final String password = args[1];

        new StructrLicenseVerifier(keystoreFileName, password).run();
    }

    private StructrLicenseVerifier(final String keystoreFileName, final String password) {

        logger.info("Starting license server..");

        try {

            logger.info("Loading key store, initializing ciphers..");

            this.gson = new GsonBuilder().setPrettyPrinting().create();
            this.keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            this.blockCipher = Cipher.getInstance(StructrLicenseManager.KeyEncryptionAlgorithm);
            this.streamCipher = Cipher.getInstance(StructrLicenseManager.DataEncryptionAlgorithm);
            this.signer = Signature.getInstance(StructrLicenseManager.SignatureAlgorithm);

            try (final InputStream is = new FileInputStream(keystoreFileName)) {

                keyStore.load(is, password.toCharArray());

                this.key = keyStore.getKey("structr", password.toCharArray());

                blockCipher.init(Cipher.DECRYPT_MODE, key);
            }

        } catch (Throwable t) {
            logger.warn("Unable to initialize key store or ciphers: {}", t.getMessage());
        }
    }

    private void run() {

        try {

            logger.info("Listening on port {}", StructrLicenseManager.ServerPort);

            final ServerSocket serverSocket = new ServerSocket(StructrLicenseManager.ServerPort);

            serverSocket.setReuseAddress(true);

            // validation loop
            while (true) {

                try (final Socket socket = serverSocket.accept()) {

                    logger.info("##### New connection from {}", socket.getInetAddress().getHostAddress());

                    final InputStream is = socket.getInputStream();
                    final int bufSize = 4096;

                    socket.setSoTimeout(2000);

                    // decrypt AES stream key using RSA block cipher
                    final byte[] sessionKey = blockCipher.doFinal(IOUtils.readFully(is, 256));
                    final byte[] ivSpec = blockCipher.doFinal(IOUtils.readFully(is, 256));
                    final byte[] buf = new byte[bufSize];
                    int count = 0;

                    // initialize cipher using stream key
                    streamCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sessionKey, "AES"),
                            new IvParameterSpec(ivSpec));

                    // we want to be able to control the number of bytes AND the timeout
                    // of the underlying socket, so that we read the available amount of
                    // data until the socket times out or we have read all the data.
                    try {

                        count = is.read(buf, 0, bufSize);

                    } catch (IOException ioex) {
                    }

                    final byte[] decrypted = streamCipher.doFinal(buf, 0, count);
                    final String data = new String(decrypted, "utf-8");

                    // transform decrypted data into a Map<String, String>
                    final List<Pair> pairs = split(data).stream().map(StructrLicenseVerifier::keyValue)
                            .collect(Collectors.toList());
                    final Map<String, String> map = pairs.stream().filter(Objects::nonNull)
                            .collect(Collectors.toMap(Pair::getLeft, Pair::getRight));

                    // validate data against customer database
                    if (isValid(map)) {

                        // send signatur of name field back to client
                        final String name = (String) map.get(StructrLicenseManager.NameKey);
                        final byte[] response = name.getBytes("utf-8");

                        // respond with the signature of the data sent to us
                        socket.getOutputStream().write(sign(response));
                        socket.getOutputStream().flush();

                    } else {

                        logger.info("License verification failed.");
                    }

                    socket.getOutputStream().close();

                } catch (Throwable t) {
                    logger.warn("Unable to verify license: {}", t.getMessage());
                }
            }

        } catch (Throwable t) {
            logger.warn("Unable to verify license: {}", t.getMessage());
        }
    }

    private boolean isValid(final Map<String, String> map) {

        final String toValidate = StructrLicenseManager.collectLicenseFieldsForSignature(map);
        final String signature = map.get(StructrLicenseManager.SignatureKey);
        final String startDate = map.get(StructrLicenseManager.StartKey);
        final String licensee = map.get(StructrLicenseManager.NameKey);
        final String hostId = map.get(StructrLicenseManager.HostIdKey);
        boolean valid = false;

        if (StringUtils.isNotBlank(toValidate) && StringUtils.isNotBlank(signature)) {

            try {

                final byte[] signedData = toValidate.getBytes("utf-8");
                final byte[] signatureData = Hex.decodeHex(signature.toCharArray());

                signer.initVerify(keyStore.getCertificate("structr"));
                signer.update(signedData);

                // verify signature of license data sent to us
                if (!signer.verify(signatureData)) {

                    logger.info("Client signature not valid.");

                    return false;
                }

            } catch (Throwable t) {

                logger.warn("Unable to verify client signature: {}", t.getMessage());

                return false;
            }

            // verify license contents
            if (StringUtils.isNotBlank(startDate) && StringUtils.isNotBlank(licensee)
                    && StringUtils.isNotBlank(hostId)) {

                // make sure that date and licensee do not contain illegal characters
                if (DatePattern.matcher(startDate).matches() && NamePattern.matcher(licensee).matches()
                        && HostIdPattern.matcher(hostId).matches()) {

                    logger.info("License request for {}, start date {}, host ID {}", licensee, startDate, hostId);

                    final String cleanedName = licensee.replace(" ", "-");
                    final String configName = startDate + "-" + cleanedName + ".json";

                    logger.info("Loading license configuration from {}..", configName);

                    // load config file or create new one
                    Map<String, Object> config = readConfig(configName);
                    if (config == null) {

                        config = new HashMap<>();
                    }

                    // load count
                    final Map<String, Object> hostIdMapping = getMapValue(config,
                            StructrLicenseManager.HostIdMappingKey);
                    final int limit = getIntValue(config, StructrLicenseManager.LimitKey, -1);
                    final int hostIdCount = getIntValue(hostIdMapping, hostId, 0);
                    final int count = hostIdMapping.size();

                    if (limit == -1) {

                        // no numerical limit found in config, check for "*" value
                        valid = "*".equals(getStringValue(config, StructrLicenseManager.LimitKey, null));

                        logger.info("count: {}, unlimited license", count);

                    } else {

                        valid = count <= limit;

                        logger.info("count: {}, limit: {}", count, limit);
                    }

                    // update host ID count
                    hostIdMapping.put(hostId, hostIdCount + 1);

                    // store config
                    writeConfig(configName, config);

                } else {

                    logger.info("Client request malformed: {} {} {}", startDate, licensee, hostId);
                }

            } else {

                logger.info("Client request incomplete, missing startDate, licensee or hostId.");
            }

        } else {

            logger.info("Client request incomplete, missing data or signature.");
        }

        return valid;
    }

    private byte[] sign(final byte[] data)
            throws SignatureException, InvalidKeyException, NoSuchAlgorithmException {

        signer.initSign((PrivateKey) key);
        signer.update(data);

        return signer.sign();
    }

    private List<String> split(final String src) {
        return Arrays.asList(src.split("\n"));
    }

    private Map<String, Object> readConfig(final String name) {

        try (final FileReader reader = new FileReader(name)) {

            return gson.fromJson(reader, Map.class);

        } catch (IOException ioex) {
            logger.warn("Unable to open license config {}: {}", name, ioex.getMessage());
        }

        return null;
    }

    private void writeConfig(final String name, final Map<String, Object> config) {

        try (final FileWriter writer = new FileWriter(name)) {

            gson.toJson(config, writer);

        } catch (IOException ioex) {
            logger.warn("Unable to store license config {}: {}", name, ioex.getMessage());
        }
    }

    private int getIntValue(final Map<String, Object> data, final String key, final int defaultValue) {

        final Object src = data.get(key);
        if (src instanceof Number) {

            return ((Number) src).intValue();
        }

        return defaultValue;
    }

    private String getStringValue(final Map<String, Object> data, final String key, final String defaultValue) {

        final Object src = data.get(key);
        if (src instanceof String) {

            return (String) src;
        }

        return defaultValue;
    }

    private Map<String, Object> getMapValue(final Map<String, Object> data, final String key) {

        final Object src = data.get(key);
        if (src instanceof Map) {

            return (Map<String, Object>) src;
        }

        // create empty map
        final Map<String, Object> newMap = new HashMap<>();

        // store map in data object
        data.put(key, newMap);

        return newMap;

    }

    // ----- private static members -----
    private static Pair keyValue(final String line) {

        final String[] parts = line.split("=", 2);

        if (parts.length == 2) {

            final String key = parts[0].trim();
            final String value = parts[1].trim();

            return new Pair(key, value);
        }

        // ignore invalid lines
        return null;
    }

    private static class Pair {

        private String left = null;
        private String right = null;

        public Pair(final String left, final String right) {
            this.left = left;
            this.right = right;
        }

        public String getLeft() {
            return left;
        }

        public String getRight() {
            return right;
        }
    }
}