Apache Cassandra Thrift API Wrapper mit Verschlüsselung

Es gibt eine Erweiterung für den API Wrapper aus diesem Artikel, um die Verschlüsselung von Daten mittels JCE zu unterstützen.
Dafür gibt es jetzt zusätzlich "sichere" Varianten der Klassen BaseEntity und CassandraClient.

Alle Informationen werden mit AES symmetrische verschlüsselt - nur der symmetrische Schlüssel wird asymmetrisch mit RSA verschlüsselt und in der Datenbank abgelegt. Dies ist wesentlich performanter als alle Daten asymmetrisch zu verschlüsseln. Als SecurityProvider kommt BouncyCastle zum Einstatz.

Die Klasse SecureBaseEntity erweitert die Klasse BaseEntity und enthält ein zusätzliches Attribut "secretKey" für die Verschlüsselungsinformation. Eigene Entity-Klassen sollten dann von dieser Klasse ableiten. Anonsten kann der Schlüssel nicht mit abgespeichert werden.

package de.ronnyfriedland.cassandra.entity;

/**
 * Entity, mit dem byte-Arrays verschlüsselt abgelegt werden können.
 *
 * @author Ronny Friedland
 */
public class SecureBaseEntity extends BaseEntity {

    private byte[] secretKey = null;

    /**
     * Erzeugt eine neue SecureBaseEntity-Instanz.
     */
    protected SecureBaseEntity() {
        super();
    }

    /**
     * Erzeugt eine neue SecureBaseEntity-Instanz.
     *
     * @param aColumnFamily
     * @param aSecretKey
     */
    public SecureBaseEntity(final String aColumnFamily, final byte[] aSecretKey) {
        super(aColumnFamily);
        this.secretKey = aSecretKey;
    }

    public SecureBaseEntity(final String aUuid, final String aColumnFamily, final byte[] aSecretKey) {
        super(aUuid, aColumnFamily);
        this.secretKey = aSecretKey;
    }

    public byte[] getSecretKey() {
        return secretKey;
    }

    /**
     * {@inheritDoc}
     *
     * @see de.ronnyfriedland.cassandra.entity.BaseEntity#toString()
     */
    @Override
    public String toString() {
        StringBuffer sbuf = new StringBuffer(super.toString());
        sbuf.append("[secretKey: ***]");
        return sbuf.toString();
    }
}

Die Client-Klasse SecureCassandraClient enthält im Vergleich zu CassandraClient zusätzlich die Logik zum Ver- und Entschlüsseln der Daten.

package de.ronnyfriedland.cassandra.api

import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;

import java.nio.ByteBuffer

import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec

import org.apache.thrift.transport.TTransport

import de.ronnyfriedland.cassandra.entity.BaseEntity
import de.ronnyfriedland.cassandra.entity.SecureBaseEntity
import de.ronnyfriedland.cassandra.props.CassandraProperties;
import de.ronnyfriedland.cassandra.sec.SecurityUtils
import de.ronnyfriedland.cassandra.sec.ex.SecurityException;
import de.ronnyfriedland.cassandra.wrapper.EntityWrapper

/**
 * Cassandra-Client, welcher die Daten verschlüsselt, bevor sie in der DB abgelegt werden.
 * Beim Abrufen der Daten werden diese wieder entschlüsselt zurück gegeben.
 *
 * Die Daten werden symmetrisch verschlüsselt. Der Schlüssel wiederum wird asymmetrisch verschlüsselt in der DB abgelegt.
 *
 * @see de.ronnyfriedland.cassandra.api.CassandraClient
 *
 * @author Ronny Friedland
 */
class SecureCassandraClient extends CassandraClient {

    final KeyPair keyPair

    final SecurityUtils securityUtils = new SecurityUtils()


    /**
     * Erzeugt eine neue SecureCassandraClient-Instanz.
     * @param host Cassandra-Host
     * @param port Cassandra-Port
     * @param keySpace zu nutzender Keyspace
     */
    public SecureCassandraClient(String host, Integer port, String keySpace) throws SecurityException {
        super(host, port, keySpace)

        KeyStore keystore = securityUtils.loadKeyStore(CassandraProperties.getKeystoreName(), CassandraProperties.getKeystorePassword())
        PrivateKey privateKey = securityUtils.getKeyByAlias(keystore, CassandraProperties.getKeyAlias(), CassandraProperties.getKeyPassword());
        PublicKey publicKey = securityUtils.getPublicKeyByAlias(keystore, CassandraProperties.getKeyAlias());

        keyPair = new KeyPair(publicKey, privateKey)
    }

    /**
     * {@inheritDoc}
     * @see de.ronnyfriedland.cassandra.api.CassandraClient#insert(de.ronnyfriedland.cassandra.entity.BaseEntity)
     */
    public void insert(SecureBaseEntity entity) {
        def secretKey = new SecretKeySpec(entity.secretKey, SecurityUtils.ALGORITHM_SYMMETRIC)

        def resultProperties = EntityWrapper.fromEntity(entity)
        resultProperties.each { prop ->
            if("secretKey" != prop.key && prop.value instanceof byte[]) {
                prop.value = securityUtils.encrypt(secretKey, prop.value)
            }
        }

        // finally encrypt secret key
        resultProperties.secretKey = new String(securityUtils.encryptSecretKey(keyPair.getPublic(), resultProperties.secretKey))

        super.doInsert(resultProperties)
    }

    /**
     * {@inheritDoc}
     * @see de.ronnyfriedland.cassandra.api.CassandraClient#select(java.lang.String, java.lang.String)
     */
    SecureBaseEntity select(String uuid, String cf) {
        def resultProperties = doSelect(uuid, cf)

        // first decrypt secret key
        def x = securityUtils.decryptSecretKey(keyPair.getPrivate(), resultProperties.secretKey.bytes)

        def secretKey = new SecretKeySpec(x, SecurityUtils.ALGORITHM_SYMMETRIC)

        resultProperties.each { prop ->
            if("secretKey" != prop.key && prop.value instanceof byte[]) {
                prop.value = securityUtils.decrypt(secretKey, prop.value)
            }
        }

        BaseEntity entity = EntityWrapper.toEntity(resultProperties)
        return (SecureBaseEntity)entity
    }
}

In der Utility-Klasse SecurityUtils sind die einzelnen Methoden zum Ver- und Entschlüsseln zusammengefasst.

package de.ronnyfriedland.cassandra.sec;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import de.ronnyfriedland.cassandra.sec.ex.SecurityException;

/**
 * Utilityklasse für den Zugriff auf den Keystore, sowie Methoden für die Ver-
 * und Entschlüsselung
 *
 * @author Ronny Friedland
 */
public class SecurityUtils {
    /** symmetrischer Ver-/Entschlüsselungsalgorithmus */
    public static final String ALGORITHM_SYMMETRIC = "AES";

    /** asymmetrischer Ver-/Entschlüsselungsalgorithmus */
    public static final String ALGORITHM_ASYMMETRIC = "RSA";

    /**
     * Erzeugt eine neue {@link SecurityUtils} Instanz.
     */
    public SecurityUtils() {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * Lädt den Keystore.
     *
     * @param path
     *            Pfad zum Keystore
     * @param password
     *            Passwort
     * @return konfigurierter Keystore
     * @throws SecurityException
     *             Fehler beim Laden des Keystores
     */
    public KeyStore loadKeyStore(final String path, final char[] password) throws SecurityException {
        if (null == path) {
            throw new IllegalArgumentException("Path is null.");
        }
        if (null == password) {
            throw new IllegalArgumentException("Password is null.");
        }
        try {
            KeyStore keystore = KeyStore.getInstance("JKS");
            keystore.load(Thread.currentThread().getContextClassLoader().getResourceAsStream(path), password);
            return keystore;
        } catch (GeneralSecurityException e) {
            throw new SecurityException(String.format("Error loading keystore: %1$s.", path), e);
        } catch (IOException e) {
            throw new SecurityException(String.format("Error loading keystore: %1$s.", path), e);
        }
    }

    /**
     * Liefert das Zertifikat aus dem Keystore mit dem angegebenen Alias.
     *
     * @param keystore
     *            Keystore
     * @param alias
     *            Alias
     * @return Zertifikat
     * @throws SecurityException
     *             Fehler beim Laden des Zertifikats
     */
    public PublicKey getPublicKeyByAlias(final KeyStore keystore, final String alias) throws SecurityException {
        if (null == keystore) {
            throw new IllegalArgumentException("Keystore is null.");
        }
        if (null == alias) {
            throw new IllegalArgumentException("Alias is null.");
        }
        PublicKey result;
        try {
            if (!keystore.containsAlias(alias)) {
                throw new SecurityException(String.format("Alias %1$s not found.", alias));
            }
            Certificate cert = keystore.getCertificate(alias);
            result = cert.getPublicKey();
        } catch (GeneralSecurityException e) {
            throw new SecurityException("Error getting cert from keystore.", e);
        }
        return result;
    }

    /**
     * Liefert den privaten Schlüssel aus dem Keystore mit dem angegebenen
     * Alias.
     *
     * @param keystore
     *            Keystore
     * @param alias
     *            Alias
     * @param alias
     *            Passwort
     * @return Key
     * @throws SecurityException
     *             Fehler beim Laden des Zertifikats
     */
    public PrivateKey getKeyByAlias(final KeyStore keystore, final String alias, final char[] password)
            throws SecurityException {
        if (null == keystore) {
            throw new IllegalArgumentException("Keystore is null.");
        }
        if (null == alias) {
            throw new IllegalArgumentException("Alias is null.");
        }
        PrivateKey result;
        try {
            if (!keystore.containsAlias(alias)) {
                throw new SecurityException(String.format("Alias %1$s not found.", alias));
            }
            result = (PrivateKey) keystore.getKey(alias, password);
        } catch (GeneralSecurityException e) {
            throw new SecurityException("Error getting key from keystore.", e);
        }
        return result;
    }

    /**
     * Symmetrische Verschlüsselung der übergebenen Daten. Der Schlüssel wird
     * mit PublicKey verschlüsselt und mit zurück gegeben.
     *
     * @param key
     *            Key zur Verschlüsselung
     * @param data
     *            zu verschlüsselnde Daten
     * @return verschlüsselte Daten
     * @throws SecurityException
     *             Fehler bei der Verschlüsselung
     */
    public byte[] encrypt(final SecretKey key, final byte[] data) throws SecurityException {
        byte[] encrypted;
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM_SYMMETRIC, "BC");

            cipher.init(Cipher.ENCRYPT_MODE, key);
            encrypted = cipher.doFinal(data);
        } catch (GeneralSecurityException e) {
            throw new SecurityException("Error enrypting data", e);
        }
        return encrypted;
    }

    /**
     * Verschlüsselt mit dem übergebenen öffentlichen Schlüssel den
     * symmetrischen Schlüssel.
     *
     * @param publicKey
     *            öffentlicher Schlüssel aus Keystore
     * @param secretKey
     *            symmetrischer Schlüssel
     * @return verschlüsselter symmetrischer Schlüssel
     * @throws SecurityException
     *             Fehler bei der Verschlüsselung
     */
    public byte[] encryptSecretKey(final PublicKey publicKey, final byte[] secretKey) throws SecurityException {
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            return cipher.doFinal(secretKey);
        } catch (GeneralSecurityException e) {
            throw new SecurityException("Error enrypting secret key", e);
        }
    }

    /**
     * Entschlüsselt mit dem übergebenen privaten Schlüssel den symmetrischen
     * Schlüssel.
     *
     * @param privateKey
     *            privater Schlüssel aus Keystore
     * @param secretKey
     *            symmetrischer Schlüssel
     * @return entschlüsselter symmetrischer Schlüssel
     * @throws SecurityException
     *             Fehler bei der Entschlüsselung
     */
    public byte[] decryptSecretKey(final PrivateKey privateKey, final byte[] secretKey) throws SecurityException {
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            return cipher.doFinal(secretKey);
        } catch (GeneralSecurityException e) {
            throw new SecurityException("Error derypting secret key", e);
        }
    }

    /**
     * Entschlüsselt die übergebenen Daten
     *
     * @param key
     *            Key zur Verschlüsselung
     * @param data
     *            verschlüsselte Daten
     * @return entschlüsselte Daten
     * @throws SecurityException
     *             Fehler bei der Entschlüsselung
     */
    public byte[] decrypt(final SecretKey key, final byte[] data) throws SecurityException {
        byte[] decrypted;
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM_SYMMETRIC, "BC");

            cipher.init(Cipher.DECRYPT_MODE, key);
            decrypted = cipher.doFinal(data);
        } catch (GeneralSecurityException e) {
            throw new SecurityException("Error decrypting data", e);
        }
        return decrypted;
    }

    /**
     * Liefert einen neuen AES - {@link SecretKey}.
     *
     * @return new SecretKey
     * @throws SecurityException
     *             Fehler bei der Erzeugung
     */
    public SecretKey getSecretKey() throws SecurityException {
        SecretKey skey;
        try {
            KeyGenerator kgen = KeyGenerator.getInstance(ALGORITHM_SYMMETRIC);
            kgen.init(192, new SecureRandom());

            skey = kgen.generateKey();
        } catch (NoSuchAlgorithmException e) {
            throw new SecurityException("Error creating secret key", e);
        }
        return skey;
    }
}
Das entsprechende Klassendiagramm mit allen Klassen:
image0

Der vollständige Sourcecode steht hier zum Download bereit.