Field Level Encryption

  • Enterprise
    +
    This is an Enterprise Edition feature.
    Community Edition
    • The push replicator will detect encryptable values inside a document. It will fail to replicate unencrypted encryptable values with a crypto error.

    • The pull replicator will not detect encryptable values inside pulled documents. The document will be saved as it was received. This may include Server SDK encrypted fields

    Overview

    Couchbase Lite for C 3.0.0 supports client-side, field-Level encryption on replications, allowing applications to encrypt/decrypt sensitive fields in documents using an encryption framework of choice.

    Using the new client-side encryption capability, Couchbase Lite C applications can designate selected fields for encryption and have the client automatically handle the encryption and-or decryption at property level during the replication.

    Only clients with access to the correct encryption keys can decrypt and read the protected data.

    The client-side encryption is compatible with the Couchbase server SDK field-level encryption format — see, for example, https://docs.couchbase.com/python-sdk/current/concept-docs/encryption.html#format

    Encryptable Type

    The API includes a CBLEncryptable type, representing an encryptable value to be automatically encrypted/decrypted the replicator — see Example 1.

    You declare properties requiring encryption by the replicator, as a dictionary with the structure shown in Example 1.

    Example 1. Encryptable-type Dictionary Structure
    {
      "@type": "encryptable",
      “value” : <Decrypted Value>
    } (1)
    
    {
      "@type": "encryptable",
      “ciphertext”: <Encrypted Value in BASE64 String>
    } (2)

    The key for the value can be either of the following:

    1 Use value as the key when storing a decrypted value, which can be any type (string, number, boolean, dictionary, array or null).
    2 Use cyphertext as the key when storing a encrypted value, which must be a BASE-64 String.

    Replicator Decryption

    Pull replication detects encryptable values before saving them to the local database and decrypts them using the property decryption callback function — see: Decryption for more on the decryption callback function.

    On successful decryption the replicator saves the property in encryptable dictionary format (removing the encrypted$ prefix) — see: Example 1.

    Replicator Encryption

    Push replication detects encryptable values before pushing them to the remote database and encrypts them using the property encryption callback function — see: Encryption.

    On successful encryption the replicator transforms the property into a format compatible with Couchbase Server SDK — see: Example 3.

    Server SDK compatibility

    Couchbase Lite’s replicator ensures compatibility with Server SDK’s field level. Both Push and Pull replication transform Couchbase Lite’s encryptable dictionary to-and-from Server SDK Encrypted Field dictionary structure — see: Example 3.

    Push replicator
    • Adds the prefix encrypted$ to the key.

    • Sets the SDK'` alg either to a user-specified algorithm name or the default CB_MOBILE_CUSTOM.

    • Set the value’s key as ciphertext

    • Stores the encrypted value as a BASE64 string.

      Example 2. Stored Encryptable
      {
          “alg”: <User-Specified or CB_MOBILE_CUSTOM>,
          “ciphertext” : <Encrypted Value in BASE64 String>
      }
    Pull replicator
    • Detects encrypted encryptable values by looking for dictionary keys prefixed with “encrypted$”.

    • Transforms the dictionary by:

      • Removing alg

      • Replacing the ciphertext key with the value

      • Storing the decrypted key/value in CBLEncryptable format

    Example 3. Server SDK Encrypted Field Structure

    Server SDK’s field (property) level encryption uses key mangling, by add a prefix to the field name (encrypted$). Its directory structure differs CBL’s encryptable dictionary when serializing the encrypted fields; as shown here.

    {
        encrypted$mykey: {
            “alg” : “AEAD_AES_256_CBC_HMAC_SHA512”, (1)
            “kid” : “my-key-id”,
            “ciphertext”: “<BASE64-TEXT>”
        }
    }
    1 Here alg identifies the encryption algorithm and is the only required field.

    Callback Definition

    Encryption

    Provide an encryption callback function CBLPropertyEncryptor to encrypt encryptable properties during replication.

    After encryption the FLSliceResult is released and the returned value zeroed. See Example 4 for an example encryptor callback function.

    If you return a null slice the replicator will fail and log a crypto error message.

    Decryption

    Provide a decryption callback function CBLPropertyDecryptor to decrypt any encryptable properties. After decryption the FLSliceResult is released and the returned value zeroed. See Example 4 for an example decryptor callback function.

    If you return a null slice without an error the replicator skips and saves the property as received.

    If you return a null slice with an error the replicator logs the error and does not replicate the document.

    Example 4. Simple Encryption-Decryption Callback
    // Purpose: Declare property-level encryptor callback functions
    static FLSliceResult my_cipher_function(FLSlice input) {
        FLSliceResult result = FLSliceResult_New(input.size);
        for(int i = 0; i < input.size; ++i) {
            ((uint8_t*)(result.buf))[i] = ((uint8_t*)input.buf)[i] ^ 'K';}
        return result;
    }
    
    
    static FLSliceResult property_encryptor(void* context, FLString docID, FLDict props, FLString path,
                                            FLSlice input, FLStringResult* algorithm, FLStringResult* keyID, CBLError* error) {
        *algorithm = FLSlice_Copy(FLSTR("MyEnc"));
        return my_cipher_function(input);
    }
    
    
    static FLSliceResult property_decryptor(void* context, FLString documentID, FLDict properties, FLString keyPath,
                                            FLSlice input, FLString algorithm, FLString keyID, CBLError* error) {
        return my_cipher_function(input);
    }

    Callback Configuration

    You register the callback function for use by declaring them in the replicator configuration using propertyEncryptor() and-or propertyDecryptor() — see: Example 5

    If you do not provide an encryption callback:

    • The push replicator always detects encrypted encryptable values in a document and will fail the document replication, flagging a crypto error.

    • The pull replicator does not detect encrypted encryptables in pulled documents and will save documents as received; this could include SDK encrypted field dictionaries.

    Example 5. Simple Callback Replicator Configuration
    // Purpose: Declare property-level encryptor callback functions
    static FLSliceResult my_cipher_function(FLSlice input) {
        FLSliceResult result = FLSliceResult_New(input.size);
        for(int i = 0; i < input.size; ++i) {
            uint8_t*)(result.buf[i] = uint8_t*)input.buf)[i] ^ 'K';}     return result; }   static FLSliceResult property_encryptor(void* context, FLString docID, FLDict props, FLString path,                                         FLSlice input, FLStringResult* algorithm, FLStringResult* keyID, CBLError* error) {     *algorithm = FLSlice_Copy(FLSTR("MyEnc";
        return my_cipher_function(input);
    }
    
    
    static FLSliceResult property_decryptor(void* context, FLString documentID, FLDict properties, FLString keyPath,
                                            FLSlice input, FLString algorithm, FLString keyID, CBLError* error) {
        return my_cipher_function(input);
    }
    
        // Purpose: Show how to declare en(de)cryptors in replicator config
        // NOTE: No error handling, for brevity (see getting started)
    
        CBLError err;
        FLString url = FLSTR("ws://localhost:4984/db");
        CBLEndpoint* target = CBLEndpoint_CreateWithURL(url, &err);
    
        CBLReplicatorConfiguration config;
        memset(&config, 0, sizeof(CBLReplicatorConfiguration));
    
        config.database = db;
        config.endpoint = target;
        config.propertyEncryptor = property_encryptor; (1)
        config.propertyDecryptor = property_decryptor; (2)
    
        CBLReplicator* replicator = CBLReplicator_Create(&config, &err);
        CBLEndpoint_Free(target);
    
        CBLReplicator_Start(replicator, false);

    Querying Encryptables

    Encrypted values can be queried — see Example 6. The query result of an encryptable value is CBLEncryptable

    CBLEncryptable exposes a value property for query purposes. If this value is encrypted the query will return MISSING.

    Example 6. A Simple encryptable Query
    SELECT  ssn,  (1)
            ssn.value  (2)
    FROM db WHERE ssn.value = "123-45-6789"
    1 The returned ssn column is in the form of an encryptable dictionary
    2 The returned ssn.value column is the actual value, unless it is still encrypted in which case it returns MISSING

    Constraints

    Nesting

    In the case of nested encryptable types, the replicator only encrypts the outer encryptable.

    Arrays

    For compatibility with Server SDKS, encryptables are not supported within arrays.

    The push replicator should detect and report an error if an encrypted property is found in an array.

    Blobs

    Encrypting blob’s content is not supported.

    Where a Blob as a Fleece dictionary is specified in the encrypted property value, only the dictionary is encrypted; not the blob’s content.

    Delta Sync

    Delta Sync will be disabled and a warning message logged when propertyEncryption is configured.

    Brute-Force Susceptibility

    Any document with simple encrypted fields (for example, fields containing a subset of values) may be brute-force computed with all possible values using the document revId. This will be fixed in a future release. In the meantime, adding an encrypted field including a nonce or random value to the document can mitigate against such brute-force computation — as shown in Example 7.

    Example 7. Sample brute-force mitigation code
    void secureRandomize(void *bytes, size_t count) {
        // This sample code uses Apple’s Common Crypto API to generate a secure random bytes.
        CCRandomGenerateBytes(bytes, count);
    }
    …
    
    auto doc = CBLDocument_CreateWithID("doc1"_sl);
    FLMutableDict props = CBLDocument_MutableProperties(doc);
    
    // Create a random bytes in base64:
    uint8_t nonceBuf[64];
    secureRandomize(nonceBuf, sizeof(nonceBuf));
    FLValue nonceValue = FLValue_NewData({nonceBuf, sizeof(nonceBuf)});
    FLSliceResult nonceBase64 = FLValue_ToJSON(nonceValue);
    FLValue_Release(nonceValue);
    
    // Create an encryptable value from the random bytes and add to the document’s property:
    auto nonce = CBLEncryptable_CreateWithString({nonceBase64.buf, nonceBase64.size});
    FLMutableDict_SetEncryptableValue(props, "nonce"_sl, nonce);
    
    …
    
    // Save doc:
    CBLError error;
    CHECK(CBLDatabase_SaveDocument(db, doc, &error));
    
    // Release:
    CBLDocument_Release(doc);
    FLSliceResult_Release(nonceBase64);
    CBLEncryptable_Release(nonce);