Field Level Encryption from the Java SDK

    +
    The Field Level Encryption library enables encryption and decryption of JSON fields.
    The Field-Level Encryption library for Couchbase Java SDK is currently a pre-release. It is not supported, but is available for development and experiment. It is planned that a supported, full release version will be available later this year. Refer to the GitHub repo for the latest updates.

    Packaging

    The Couchbase Java SDK uses the java-couchbase-encryption library to provide support for encryption and decryption of JSON fields. It includes cryptographic algorithms and keyrings you can use out of the box, and provides a framework for implementing your own crypto components.

    This separation of the encryption library ensures that the SDK does not have a dependency upon an encryption library in general use — but it does mean you have to explicitly include this external dependency in your project configuration. Refer to the dependencies section.

    Version 3.0.0-pre.1 of this library requires Couchbase Java SDK version 3.0.5 or later.

    Maven Coordinates

    <dependency>
        <groupId>com.couchbase.client</groupId>
        <artifactId>couchbase-encryption</artifactId>
        <version>${version}</version>
    </dependency>

    Optional Dependencies

    To reduce the footprint of this library, some of its dependencies are optional. Using certain features requires adding additional dependencies to your project.

    HashiCorp Vault Transit integration requires Spring Vault:

    <dependency>
        <groupId>org.springframework.vault</groupId>
        <artifactId>spring-vault-core</artifactId>
        <version>2.2.2.RELEASE</version>
    </dependency>

    Configuration

    To enable Field-Level Encryption, supply a CryptoManager when configuring the Java SDK’s ClusterEnvironment.

    KeyStore javaKeyStore = KeyStore.getInstance("MyKeyStoreType");
    FileInputStream fis = new java.io.FileInputStream("keyStoreName");
    char[] password = {'a', 'b', 'c'};
    javaKeyStore.load(fis, password);
    Keyring keyring = new KeyStoreKeyring(javaKeyStore, keyName -> "swordfish");
    
    // AES-256 authenticated with HMAC SHA-512. Requires a 64-byte key.
    AeadAes256CbcHmacSha512Provider provider = AeadAes256CbcHmacSha512Provider.builder()
        .keyring(keyring)
        .build();
    
    CryptoManager cryptoManager = DefaultCryptoManager.builder()
        .decrypter(provider.decrypter())
        .defaultEncrypter(provider.encrypterForKey("myKey"))
        .build();
    
    ClusterEnvironment env = ClusterEnvironment.builder()
        .cryptoManager(cryptoManager)
        .build();
    
    Cluster cluster = Cluster.connect("localhost",
        ClusterOptions.clusterOptions("username", "password")
            .environment(env));

    Usage

    Two modes of operation are available:

    • Transparent encryption/decryption during Jackson data binding.

    • Manual field editing using JsonObjectCrypto.

    Data Binding Example

    Sensitive fields of your POJOs can be annotated with @Encrypted. Let’s use this class as an example:

    public class Employee {
      @Encrypted
      private boolean replicant;
    
      // alternatively you could annotate the getter or setter
      public boolean isReplicant() {
        return replicant;
      }
    
      public void setReplicant(boolean replicant) {
        this.replicant = replicant;
      }
    }

    Now let’s create an employee record:

    Collection collection = cluster.bucket("myBucket")
        .defaultCollection();
    
    Employee employee = new Employee();
    employee.setReplicant(true);
    collection.upsert("employee:1234", employee);

    You can get the document as a JsonObject to verify the field was encrypted:

    JsonObject encrypted = collection.get("employee:1234")
        .contentAsObject();
    
    System.out.println(encrypted);

    Because contentAsObject() does not decrypt anything, the expected output is something like:

    {
      "encrypted$replicant": {
        "alg": "AEAD_AES_256_CBC_HMAC_SHA512",
        "ciphertext": "xwcxyUyZ.....",
        "kid": "myKey"
      }
    }

    Now let’s read the employee record using data binding:

    Employee readItBack = collection.get("employee:1234")
        .contentAs(Employee.class);
    
    System.out.println(readItBack.isReplicant());

    This prints true.

    Using a custom ObjectMapper

    The code that enables encryption/decryption during data binding is packaged as a Jackson module called EncryptionModule. You can register this module with any Jackson ObjectMapper.

    You’ll need to do this if you want to supply your own customized ObjectMapper for the Java SDK to use when serializing documents. Here’s how to configure the cluster environment to use a custom JSON serializer backed by your own ObjectMapper with support for Field-Level Encryption:

    // CryptoManager cryptoManager = createMyCryptoManager();
    KeyStore javaKeyStore = KeyStore.getInstance("MyKeyStoreType");
    FileInputStream fis = new java.io.FileInputStream("keyStoreName");
    char[] ksPassword = {'a', 'b', 'c'};
    javaKeyStore.load(fis, ksPassword);
    Keyring keyring = new KeyStoreKeyring(javaKeyStore, keyName -> "swordfish");
    
    // AES-256 authenticated with HMAC SHA-512. Requires a 64-byte key.
    AeadAes256CbcHmacSha512Provider provider = AeadAes256CbcHmacSha512Provider.builder()
        .keyring(keyring)
        .build();
    CryptoManager cryptoManager = DefaultCryptoManager.builder()
        .decrypter(provider.decrypter())
        .defaultEncrypter(provider.encrypterForKey("myKey"))
        .build();
    
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JsonValueModule()); // for JsonObject
    mapper.registerModule(new EncryptionModule(cryptoManager));
    
    // Here you can register more modules, add mixins, enable features, etc.
    
    ClusterEnvironment env = ClusterEnvironment.builder()
        .cryptoManager(cryptoManager)
        .jsonSerializer(JacksonJsonSerializer.create(mapper))
        .build();
    
    Cluster cluster = Cluster.connect(connectionString,
        ClusterOptions.clusterOptions(username, password)
            .environment(env));

    JsonObjectCrypto

    If you need more control of which fields get decrypted, or if you prefer working with the Couchbase JsonObject tree model, you can use a JsonObjectCrypto instance to read and write encrypted field values of a JsonObject.

    Collection collection = cluster.bucket("myBucket").defaultCollection();
    
    JsonObject document = JsonObject.create();
    JsonObjectCrypto crypto = document.crypto(collection);
    
    crypto.put("locationOfBuriedTreasure", "Between palm trees");
    
    // This displays the encrypted form of the field
    System.out.println(document);
    
    collection.upsert("treasureMap", document);
    
    JsonObject readItBack = collection.get("treasureMap").contentAsObject();
    JsonObjectCrypto readItBackCrypto = crypto.withObject(readItBack);
    System.out.println(readItBackCrypto.getString("locationOfBuriedTreasure"));

    Creating Encryption Keys

    The AEAD_AES_256_CBC_HMAC_SHA512 algorithm included in this library uses encryption keys that are 64 bytes long.

    Here’s an example that shows how to create a Java key store file containing a suitable encryption key:

    KeyStore keyStore = KeyStore.getInstance("JCEKS");
    keyStore.load(null); // initialize new empty key store
    
    // Generate 64 random bytes
    SecureRandom random = new SecureRandom();
    byte[] keyBytes = new byte[64];
    random.nextBytes(keyBytes);
    
    // Add a new key called "my-key" to the key store
    KeyStoreKeyring.setSecretKey(keyStore, "my-key", keyBytes,
        "protection-password".toCharArray());
    
    // Write the key store to disk
    try (OutputStream os = new FileOutputStream("MyKeystoreFile.jceks")) {
      keyStore.store(os, "integrity-password".toCharArray());
    }

    And here’s how to use that file to create a Keyring for use with Couchbase Field-Level Encryption:

    KeyStore keyStore = KeyStore.getInstance("JCEKS");
    try (InputStream is = new FileInputStream("MyKeystoreFile.jceks")) {
      keyStore.load(is, "integrity-password".toCharArray());
    }
    
    KeyStoreKeyring keyring = new KeyStoreKeyring(
        keyStore, keyName -> "protection-password");

    Migrating from SDK 2

    If you were previously using Field-Level Encryption with Java SDK 2, a few extra configuration steps are required.

    Changing the field name prefix

    In SDK 2, the default prefix for encrypted field names was __crypt_. This caused problems for Couchbase Sync Gateway, which does not like field names to begin with an underscore. In SDK 3, the default prefix is encrypted$.

    In order to decrypt fields written by SDK 2, you can configure the CryptoManager to use the old __crypt_ prefix:

    CryptoManager cryptoManager = DefaultCryptoManager.builder()
        .encryptedFieldNamePrefix("__crypt_")
        // other config...
        .build();
    In SDK 2, only top-level fields could be encrypted. SDK 3 allows encrypting fields at any depth. If you decide to rename the existing fields, make sure to do so before writing any encrypted fields below the top level, otherwise it may be difficult to rename the nested fields using a generic N1QL statement.

    Enabling decrypters for legacy algorithms

    The encryption algorithms used by SDK 2 are deprecated, and are no longer used for encrypting new data. To decrypt fields written by SDK 2, enable the legacy decrypters when configuring the CryptoManager:

    CryptoManager cryptoManager = DefaultCryptoManager.builder()
        .legacyAesDecrypters(keyring, encryptionKeyName -> "MySigningKeyName")
        .legacyRsaDecrypter(keyring, publicKeyName -> "MyPrivateKeyName")
        // other config...
        .build();
    The legacy decrypters require a mapping function. For AES, this function accepts an encryption key name and returns the corresponding signing key name. For RSA, this function accepts a public key name and returns the corresponding private key name.