ai.susi.server.api.aaa.PublicKeyRegistrationService.java Source code

Java tutorial

Introduction

Here is the source code for ai.susi.server.api.aaa.PublicKeyRegistrationService.java

Source

/**
 *  PublicKeyRegistrationService
 *  Copyright 29.06.2015 by Robert Mader, @treba13
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *  
 *  This library 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
 *  Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program in the file lgpl21.txt
 *  If not, see <http://www.gnu.org/licenses/>.
 */

package ai.susi.server.api.aaa;

import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.bouncycastle.util.io.pem.PemWriter;
import org.eclipse.jetty.util.log.Log;
import org.json.JSONArray;
import org.json.JSONObject;

import ai.susi.DAO;
import ai.susi.json.JsonObjectWithDefault;
import ai.susi.server.*;
import ai.susi.tools.IO;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.*;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.stream.IntStream;

/**
 * This service allows users to register a public key for login.
 * It can either take a public key or create a new key-pair.
 * Users can also be granted the right to register keys for individual other users or whole user roles.
 *
 * To export your own PublikKey from java for registering, call:
 * - String encodedPublicKey = Base64.getEncoder().encodeToString(publicKey.getEncoded());
 *
 * To use the private key as generated in DER format in java, call:
 * - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(encodedPrivateKey));
 * - PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
 *
 * To sign a challenge as given by the login, call:
 * - Signature sig = Signature.getInstance("SHA256withRSA");
 * - sig.initSign(privateKey);
 * - sig.update(challengeString.getBytes());
 * - String result = new String(Base64.getEncoder().encode(sig.sign()));
 *
 * To create a signature with openssl (using a key in pem format), the following command should work:
 * - openssl dgst -sha256 -sign privkey.pem -out response.txt challenge.txt
 * - encode the content of response.in BASE64
 * - if necessary, encode it URL-friendly
 */
public class PublicKeyRegistrationService extends AbstractAPIHandler implements APIHandler {

    private static final long serialVersionUID = 8578478303032749879L;

    private static final int[] allowedKeySizesRSA = { 1024, 2048, 4096 };
    private static final int defaultKeySizeRSA = 2048;
    private static final String[] allowedFormats = { "DER", "PEM" };

    @Override
    public BaseUserRole getMinimalBaseUserRole() {
        return BaseUserRole.ANONYMOUS;
    }

    @Override
    public JSONObject getDefaultPermissions(BaseUserRole baseUserRole) {
        JSONObject result = new JSONObject();

        switch (baseUserRole) {
        case ADMIN:
            result.put("self", true);
            result.put("users", new JSONObject());
            JSONObject userRoles = new JSONObject();
            for (String userRole : DAO.userRoles.getUserRoles().keySet()) {
                userRoles.put(userRole, true);
            }
            result.put("userRoles", userRoles);
            break;
        case PRIVILEGED:
        case USER:
            result.put("self", true);
            result.put("users", new JSONObject());
            result.put("userRoles", new JSONObject());
            break;
        case ANONYMOUS:
        default:
            result.put("self", false);
            result.put("users", new JSONObject());
            result.put("userRoles", new JSONObject());
        }

        return result;
    }

    public String getAPIPath() {
        return "/aaa/pubkey_registration.json";
    }

    @Override
    public JSONObject serviceImpl(Query post, HttpServletResponse response, Authorization authorization,
            final JsonObjectWithDefault permissions) throws APIException {

        if (post.get("register", null) == null && !post.get("create", false) && !post.get("getParameters", false)) {
            throw new APIException(400, "Accepted parameters: 'register', 'create' or 'getParameters'");
        }

        JSONObject result = new JSONObject();

        // return algorithm parameters and users for whom we are allowed to register a key
        if (post.get("getParameters", false)) {
            result.put("self", permissions.getBoolean("self", false));
            result.put("users", permissions.getJSONObject("users"));
            result.put("userRoles", permissions.getJSONObject("userRoles"));

            JSONObject algorithms = new JSONObject();

            JSONObject rsa = new JSONObject();
            JSONArray keySizes = new JSONArray();
            for (int i : allowedKeySizesRSA) {
                keySizes.put(i);
            }
            rsa.put("sizes", keySizes);
            rsa.put("defaultSize", defaultKeySizeRSA);
            algorithms.put("RSA", rsa);
            result.put("algorithms", algorithms);

            JSONArray formats = new JSONArray();
            for (String format : allowedFormats) {
                formats.put(format);
            }
            result.put("formats", formats);

            return result;
        }

        // for which id?
        String id;
        if (post.get("id", null) != null)
            id = post.get("id", null);
        else
            id = authorization.getIdentity().getName();

        // check if we are allowed register a key
        if (!id.equals(authorization.getIdentity().getName())) { // if we don't want to register the key for the current user

            // create Authentication to check if the user id is a registered user
            ClientCredential credential = new ClientCredential(ClientCredential.Type.passwd_login, id);
            Authentication authentication = new Authentication(credential, DAO.authentication);

            if (authentication.getIdentity() == null) { // check if identity is valid
                authentication.delete();
                throw new APIException(400, "Bad request"); // do not leak if user exists or not
            }

            // check if the current user is allowed to create a key for the user in question
            boolean allowed = false;
            // check if the user in question is in 'users'
            if (permissions.getJSONObject("users", null).has(id)
                    && permissions.getJSONObjectWithDefault("users", null).getBoolean(id, false)) {
                allowed = true;
            } else { // check if the user role of the user in question is in 'userRoles'
                Authorization auth = new Authorization(authentication.getIdentity(), DAO.authorization,
                        DAO.userRoles);
                for (String key : permissions.getJSONObject("userRoles").keySet()) {
                    if (key.equals(auth.getUserRole().getName())
                            && permissions.getJSONObject("userRoles").getBoolean(key)) {
                        allowed = true;
                    }
                }
            }
            if (!allowed)
                throw new APIException(400, "Bad request"); // do not leak if user exists or not
        } else { // if we want to register a key for this user, bad are not allowed to (for example anonymous users)
            if (!permissions.getBoolean("self", false))
                throw new APIException(403, "You are not allowed to register a public key");
        }

        // set algorithm. later, we maybe want to support other algorithms as well
        String algorithm = "RSA";
        if (post.get("algorithm", null) != null) {
            algorithm = post.get("algorithm", null);
        }

        if (post.get("create", false)) { // create a new key pair on the server

            if (algorithm.equals("RSA")) {
                int keySize = 2048;
                if (post.get("key-size", null) != null) {
                    int finalKeyLength = post.get("key-size", 0);
                    if (!IntStream.of(allowedKeySizesRSA).anyMatch(x -> x == finalKeyLength)) {
                        throw new APIException(400, "Invalid key size.");
                    }
                    keySize = finalKeyLength;
                }

                KeyPairGenerator keyGen;
                KeyPair keyPair;
                try {
                    keyGen = KeyPairGenerator.getInstance(algorithm);
                    keyGen.initialize(keySize);
                    keyPair = keyGen.genKeyPair();
                } catch (NoSuchAlgorithmException e) {
                    throw new APIException(500, "Server error");
                }

                registerKey(authorization.getIdentity(), keyPair.getPublic());

                String pubkey_pem = null, privkey_pem = null;
                try {
                    StringWriter writer = new StringWriter();
                    PemWriter pemWriter = new PemWriter(writer);
                    pemWriter.writeObject(new PemObject("PUBLIC KEY", keyPair.getPublic().getEncoded()));
                    pemWriter.flush();
                    pemWriter.close();
                    pubkey_pem = writer.toString();
                } catch (IOException e) {
                }
                try {
                    StringWriter writer = new StringWriter();
                    PemWriter pemWriter = new PemWriter(writer);
                    pemWriter.writeObject(new PemObject("PRIVATE KEY", keyPair.getPrivate().getEncoded()));
                    pemWriter.flush();
                    pemWriter.close();
                    privkey_pem = writer.toString();
                } catch (IOException e) {
                }

                result.put("publickey_DER_BASE64",
                        Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
                result.put("privatekey_DER_BASE64",
                        Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
                result.put("publickey_PEM", pubkey_pem);
                result.put("privatekey_PEM", privkey_pem);
                result.put("keyhash", IO.getKeyHash(keyPair.getPublic()));
                try {
                    result.put("keyhash_urlsave", URLEncoder.encode(IO.getKeyHash(keyPair.getPublic()), "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                }
                result.put("key-size", keySize);
                result.put("message",
                        "Successfully created and registered key. Make sure to copy the private key, it won't be saved on the server");

                return result;
            }
            throw new APIException(400, "Unsupported algorithm");
        } else if (post.get("register", null) != null) {

            if (algorithm.equals("RSA")) {
                String type = post.get("type", null);
                if (type == null)
                    type = "DER";

                RSAPublicKey pub;
                String encodedKey;
                try {
                    encodedKey = URLDecoder.decode(post.get("register", null), "UTF-8");
                } catch (Throwable e) {
                    throw new APIException(500, "Server error");
                }
                Log.getLog().info("Key (" + type + "): " + encodedKey);

                if (type.equals("DER")) {
                    try {
                        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(encodedKey));
                        pub = (RSAPublicKey) KeyFactory.getInstance(algorithm).generatePublic(keySpec);
                    } catch (Throwable e) {
                        throw new APIException(400, "Public key not readable (DER)");
                    }
                } else if (type.equals("PEM")) {
                    try {
                        PemReader pemReader = new PemReader(new StringReader(encodedKey));
                        PemObject pem = pemReader.readPemObject();
                        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pem.getContent());
                        pub = (RSAPublicKey) KeyFactory.getInstance(algorithm).generatePublic(keySpec);
                    } catch (Exception e) {
                        throw new APIException(400, "Public key not readable (PEM)");
                    }
                } else {
                    throw new APIException(400, "Invalid value for 'type'.");
                }

                // check key size (not really perfect yet)
                int keySize;
                int bitLength = pub.getModulus().bitLength();
                if (bitLength <= 512) {
                    keySize = 512;
                } else if (bitLength <= 1024) {
                    keySize = 1024;
                } else if (bitLength <= 2048) {
                    keySize = 2048;
                } else if (bitLength <= 4096) {
                    keySize = 4096;
                } else {
                    keySize = 8192;
                }
                if (!IntStream.of(allowedKeySizesRSA).anyMatch(x -> x == keySize)) {
                    throw new APIException(400, "Invalid key length.");
                }

                registerKey(authorization.getIdentity(), pub);

                String pubkey_pem = null;
                try {
                    StringWriter writer = new StringWriter();
                    PemWriter pemWriter = new PemWriter(writer);
                    pemWriter.writeObject(new PemObject("PUBLIC KEY", pub.getEncoded()));
                    pemWriter.flush();
                    pemWriter.close();
                    pubkey_pem = writer.toString();
                } catch (IOException e) {
                }

                result.put("publickey_DER_BASE64", Base64.getEncoder().encodeToString(pub.getEncoded()));
                result.put("publickey_PEM", pubkey_pem);
                result.put("keyhash", IO.getKeyHash(pub));
                try {
                    result.put("keyhash_urlsave", URLEncoder.encode(IO.getKeyHash(pub), "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                }
                result.put("message", "Successfully registered key.");

                return result;
            }
            throw new APIException(400, "Unsupported algorithm");
        }

        throw new APIException(400, "Invalid parameter");
    }

    /**
     * Registers a key for an identity.
     * TODO: different algorithms
     * @param id
     * @param key
      */
    private void registerKey(ClientIdentity id, PublicKey key) throws APIException {
        JSONObject user_obj;

        try {
            user_obj = DAO.login_keys.getJSONObject(id.toString());
        } catch (Throwable e) {
            user_obj = new JSONObject();
            DAO.login_keys.put(id.toString(), user_obj);
        }

        if (user_obj.has(IO.getKeyHash(key)))
            throw new APIException(422, "Key already registered");

        user_obj.put(IO.getKeyHash(key), IO.getKeyAsString(key));

        DAO.login_keys.commit();
    }
}