All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.bdeploy.common.security.SecurityHelper Maven / Gradle / Ivy

Go to download

Public API including dependencies, ready to be used for integrations and plugins.

There is a newer version: 7.3.6
Show newest version
package io.bdeploy.common.security;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStore.ProtectionParameter;
import java.security.KeyStore.SecretKeyEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.bdeploy.common.util.JacksonHelper;
import io.bdeploy.common.util.PathHelper;

/**
 * Encapsulates certificate and token handling for mutual authentication.
 */
public class SecurityHelper {

    private static final Logger log = LoggerFactory.getLogger(SecurityHelper.class);
    private static final SecurityHelper INSTANCE = new SecurityHelper();

    public static final String ROOT_ALIAS = "1";
    private static final byte[] DEF_SLT = "@%$&".getBytes(StandardCharsets.UTF_8);
    private static final String TOKEN_ALIAS = "token";
    public static final String CERT_ALIAS = "cert";

    private SecurityHelper() {
    }

    public static SecurityHelper getInstance() {
        return INSTANCE;
    }

    /**
     * @param password the password for the key
     * @return a secret key which can be used for encryption and decryption of passwords
     */
    public static SecretKeySpec createSecretKey(char[] password) throws GeneralSecurityException {
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
        PBEKeySpec keySpec = new PBEKeySpec(password, DEF_SLT, 1024, 256);
        SecretKey keyTmp = keyFactory.generateSecret(keySpec);
        return new SecretKeySpec(keyTmp.getEncoded(), "AES");
    }

    /**
     * @param data the data to encrypt
     * @param key the key to use to encrypt
     * @return the encrypted data
     */
    public static String encrypt(String data, SecretKeySpec key) throws GeneralSecurityException {
        Cipher pbeCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        pbeCipher.init(Cipher.ENCRYPT_MODE, key);
        AlgorithmParameters parameters = pbeCipher.getParameters();
        IvParameterSpec ivParameterSpec = parameters.getParameterSpec(IvParameterSpec.class);
        byte[] cryptoText = pbeCipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        byte[] iv = ivParameterSpec.getIV();
        return encode(iv) + ":" + encode(cryptoText);
    }

    /**
     * @param data the encrypted data
     * @param key the key to use to decrypt the data
     * @return the decrypted data
     */
    public static String decrypt(String data, SecretKeySpec key) throws GeneralSecurityException {
        String iv = data.split(":")[0];
        String property = data.split(":")[1];
        Cipher pbeCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        pbeCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(decode(iv)));
        return new String(pbeCipher.doFinal(decode(property)), StandardCharsets.UTF_8);
    }

    /**
     * Creates a new encoded and signed token for this server.
     * 

* To generate an appropriate self signed certificate in a (PKCS12) keystore, * use this: * *

     *   openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 17800 -out cert.pem
     *   openssl pkcs12 -inkey key.pem  -in cert.pem -export -out certstore.p12
     * 
* * @param payload the payload to sign. will be serialized and encoded in the * final signed token * @param keystore the keystore containing the private key. The keystore must * be in PKCS12 format and contain exactly one entry, which is * the private X.509 certificate. * @param passphrase the passphrase for both the keystore and the certificate * within. * @return an encoded and signed token containing all security relevant * information for a client to connect to this server. */ public String createSignaturePack(T payload, KeyStore keystore, char[] passphrase) throws GeneralSecurityException { SignaturePack pack = new SignaturePack(); pack.t = createToken(payload, keystore, passphrase); pack.c = encode(getCertificate(keystore).getEncoded()); return pack.toString(); } /** * Creates a new encoded and signed token for this server. *

* To generate an appropriate self signed certificate in a (PKCS12) keystore, * use this: * *

     *   openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 17800 -out cert.pem
     *   openssl pkcs12 -inkey key.pem  -in cert.pem -export -out certstore.p12
     * 
* * @param payload the payload to sign. will be serialized and encoded in the * final signed token * @param keystore the keystore containing the private key. The keystore must * be in PKCS12 format and contain exactly one entry, which is * the private X.509 certificate. * @param passphrase the passphrase for both the keystore and the certificate * within. * @return an encoded and signed token containing all security relevant * information for a client to connect to this server. */ public String createSignaturePack(T payload, Path keystore, char[] passphrase) throws GeneralSecurityException, IOException { KeyStore ks = loadPrivateKeyStore(keystore, passphrase); SignaturePack pack = new SignaturePack(); pack.t = createToken(payload, ks, passphrase); pack.c = encode(getCertificate(ks).getEncoded()); return pack.toString(); } /** * Create a valid security token suitable for HTTPS traffic verification. Used * to pass to clients connecting and authorizing to use APIs. * * @param payload the token payload. * @param ks the keystore. * @param passphrase the passphrase. * @return a signed token */ public String createToken(T payload, KeyStore ks, char[] passphrase) { try { PrivateKey pk = getPrivateKey(ks, passphrase); return getSignedToken(payload, pk).toString(); } catch (Exception e) { throw new IllegalStateException(e); } } /** * Accepts a token in {@link String} form, extracts the payload from it (see * {@link #createSignaturePack(Object, Path, char[])}) and verifies that the * enclosed signature is valid for the decoded payload. * * @param token the encoded payload and signature. * @param clazz the {@link Class} of the payload - used for * de-serialization. * @param ks the keystore containing the private key and certificate * @return the signed payload, if the signature is valid. */ public T getVerifiedPayload(String token, Class clazz, KeyStore ks) throws GeneralSecurityException { Certificate cert = getCertificate(ks); SignedPayload t = SignedPayload.parse(token); return doVerifyPayload(clazz, t, cert); } /** * Accepts a token in {@link String} form, extracts the payload from it (see * {@link #createSignaturePack(Object, Path, char[])}) and verifies that the * enclosed signature is valid for the decoded payload using the enclosed public certificate. *

* This does NOT verify that the enclosed signature is valid against a present private key. * * @param token the encoded payload and signature. * @param clazz the {@link Class} of the payload - used for * de-serialization. * @return the signed payload, if the signature is valid. */ public T getSelfVerifiedPayloadFromPack(String token, Class clazz) throws GeneralSecurityException, IOException { SignaturePack sp = SignaturePack.parse(token); SignedPayload t = SignedPayload.parse(sp.t); CertificateFactory cf = CertificateFactory.getInstance("X.509"); Certificate cert; try (ByteArrayInputStream bais = new ByteArrayInputStream(decode(sp.c))) { cert = cf.generateCertificate(bais); } return doVerifyPayload(clazz, t, cert); } /** * Extract the pure token (required for HTTPS authentication) from a signature pack. */ public String getTokenFromPack(String pack) { return SignaturePack.parse(pack).t; } /** * @param clazz the type of the payload to deserialize * @param t the raw encrypted and signed payload * @param cert the certificate to use to check the signature against. * @return the verified payload if checks are OK, null otherwise. */ private T doVerifyPayload(Class clazz, SignedPayload t, Certificate cert) throws GeneralSecurityException { byte[] payloadBytes = decode(t.p); T payload; try { payload = getMapper().readValue(payloadBytes, clazz); } catch (IOException e) { throw new IllegalStateException("Cannot read JSON", e); } Signature sig = getSignatureAlgorithm(); sig.initVerify(cert.getPublicKey()); sig.update(payloadBytes); if (!sig.verify(decode(t.s))) { return null; } return payload; } /** * Accepts an encoded and signed token and imports the enclosed security * relevant information into the given (JCEKS) keystore. The keystore is created * if it does not exist. * * @param pack the token/signature pack in {@link String} form * @param ks the keystore to use. * @param passphrase the passphrase used to decode and encode the keystore. */ public void importSignaturePack(String pack, KeyStore ks, char[] passphrase) throws GeneralSecurityException, IOException { SignaturePack sigs = SignaturePack.parse(pack); if (sigs.c == null || sigs.t == null) { throw new IllegalArgumentException("Given token is not a full authentication pack"); } String aliasCert = CERT_ALIAS; String aliasToken = TOKEN_ALIAS; ProtectionParameter pp = passphrase == null ? null : new KeyStore.PasswordProtection(passphrase); SecretKeyFactory skf = SecretKeyFactory.getInstance("PBE"); SecretKey key = skf.generateSecret(new PBEKeySpec(sigs.t.toCharArray())); ks.setEntry(aliasToken, new KeyStore.SecretKeyEntry(key), pp); CertificateFactory cf = CertificateFactory.getInstance("X.509"); Certificate cert; try (ByteArrayInputStream bais = new ByteArrayInputStream(decode(sigs.c))) { cert = cf.generateCertificate(bais); } ks.setCertificateEntry(aliasCert, cert); } /** * Loads a {@link KeyStore} from the given {@link Path} or creates one if it does not exist, imports the signature pack and * saves the {@link KeyStore} afterwards back to the given {@link Path}. * * @see #importSignaturePack(String, KeyStore, char[]) */ public void importSignaturePack(String pack, Path keystore, char[] passphrase) throws GeneralSecurityException, IOException { KeyStore ks = loadPublicKeyStore(keystore, passphrase); importSignaturePack(pack, ks, passphrase); try (OutputStream os = Files.newOutputStream(keystore)) { ks.store(os, passphrase); } } /** * Retrieve the signed token for authentication against a server using this * helper to decode the token. * * @param ks the public keystore * @param passphrase the passphrase for the keystore * @return an encoded token which can be sent to the server. */ public String getSignedToken(KeyStore ks, char[] passphrase) throws GeneralSecurityException { String aliasToken = TOKEN_ALIAS; if (!ks.containsAlias(aliasToken)) { throw new IllegalStateException("No access token found in keystore"); } ProtectionParameter pp = passphrase == null ? null : new KeyStore.PasswordProtection(passphrase); SecretKeyFactory skf = SecretKeyFactory.getInstance("PBE"); SecretKeyEntry ske = (SecretKeyEntry) ks.getEntry(aliasToken, pp); PBEKeySpec spec = (PBEKeySpec) skf.getKeySpec(ske.getSecretKey(), PBEKeySpec.class); return new String(spec.getPassword()); } /** * Load and return a PKCS12 formatted keystore. */ public KeyStore loadPrivateKeyStore(Path keystore, char[] passphrase) throws GeneralSecurityException, IOException { try (InputStream is = Files.newInputStream(keystore)) { return loadPrivateKeyStore(is, passphrase); } } /** * @see #loadPrivateKeyStore(Path, char[]) */ public KeyStore loadPrivateKeyStore(InputStream is, char[] passphrase) throws GeneralSecurityException, IOException { KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(is, passphrase); return ks; } /** * Load and return (create on demand) a JCEKS formatted keystore. */ public KeyStore loadPublicKeyStore(Path keystore, char[] passphrase) throws GeneralSecurityException, IOException { KeyStore ks = KeyStore.getInstance("JCEKS"); if (PathHelper.exists(keystore)) { try (InputStream is = Files.newInputStream(keystore)) { return loadPublicKeyStore(is, passphrase); } } else { ks.load(null, passphrase); } return ks; } /** * @see #loadPublicKeyStore(Path, char[]) */ public KeyStore loadPublicKeyStore(InputStream is, char[] passphrase) throws GeneralSecurityException, IOException { KeyStore ks = KeyStore.getInstance("JCEKS"); ks.load(is, passphrase); return ks; } private PrivateKey getPrivateKey(KeyStore ks, char[] passphrase) throws GeneralSecurityException { // find the "newest" alias, assume aliases are numbers return (PrivateKey) ks.getKey(ROOT_ALIAS, passphrase); } /** * Load the public certificate from the given {@link KeyStore}. */ private Certificate getCertificate(KeyStore ks) throws KeyStoreException { String alias = ROOT_ALIAS; if (!ks.containsAlias(ROOT_ALIAS)) { alias = CERT_ALIAS; } Certificate cert = ks.getCertificate(alias); if (cert != null) { return cert; } throw new IllegalStateException("KeyStore does not contain a certificate"); } private String getRawSignature(String data, PrivateKey pk) throws GeneralSecurityException { Signature rsa = getSignatureAlgorithm(); rsa.initSign(pk); rsa.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signature = rsa.sign(); return encode(signature); } private Signature getSignatureAlgorithm() throws NoSuchAlgorithmException { return Signature.getInstance("SHA256withRSA"); } private SignedPayload getSignedToken(Object payload, PrivateKey pk) throws GeneralSecurityException, IOException { String toSign = getMapper().writeValueAsString(payload); String signature = getRawSignature(toSign, pk); SignedPayload t = new SignedPayload(); t.p = encode(toSign.getBytes(StandardCharsets.UTF_8)); t.s = signature; return t; } private static ObjectMapper getMapper() { return JacksonHelper.getDefaultJsonObjectMapper(); } private static String encode(byte[] bytes) { return Base64.encodeBase64String(bytes); } private static byte[] decode(String data) { return Base64.decodeBase64(data); } private static class SignedPayload { private String p; // payload private String s; // signature @Override public String toString() { try { return encode(getMapper().writeValueAsBytes(this)); } catch (JsonProcessingException e) { throw new IllegalStateException("Cannot write JSON", e); } } public static SignedPayload parse(String token) { try { return getMapper().readValue(decode(token), SignedPayload.class); } catch (IOException e) { throw new IllegalStateException("Cannot read JSON", e); } } } /** * Encapsulates all required certificates, keys and tokens to be able to * communicate with a given {@link RemoteHive} via SSL/TLS. */ private static class SignaturePack { private String c; // certificate private String t; // token @Override public String toString() { try { return encode(getMapper().writeValueAsBytes(this)); } catch (JsonProcessingException e) { throw new IllegalStateException("Cannot write JSON", e); } } public static SignaturePack parse(String pack) { try { return getMapper().readValue(decode(pack), SignaturePack.class); } catch (IOException e) { log.debug("Invalid token supplied", e); throw new IllegalStateException("Security token invalid."); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy