Basic CipherSweet Usage

Many of our APIs are async functions and therefore return a Promise (if you don't use await).

Once you have an engine in play, you can start defining encrypted fields and defining one or more blind index to be used for fast search operations.

EncryptedField

This will primarily involve the EncryptedField class (as well as one or more instances of BlindIndex), mostly:

Note: The default configuration for blind indexes is to use a password hashing function. You can specify a faster hash function by passing true to the fourth argument.

For example, the following code encrypts a user's social security number and then creates two blind indexes: One for a literal search, the other only matches the last 4 digits.

const {
    EncryptedField,
    BlindIndex,
    CipherSweet,
    LastFourDigits,
} = require('ciphersweet-js');

/** @var CipherSweet engine */
let ssn = (new EncryptedField(engine, 'contacts', 'ssn'))
    // Add a blind index for the "last 4 of SSN":
    .addBlindIndex(
        new BlindIndex(
            // Name (used in key splitting):
            'contact_ssn_last_four',
            // List of Transforms: 
            [new LastFourDigits()],
            // Bloom filter size (bits)
            16,
            // Fast hash (default: false)
            false
        )
    )
    // Add a blind index for the full SSN:
    .addBlindIndex(
        new BlindIndex(
            'contact_ssn', 
            [],
            32
        )
    );

// Some example parameters:
let contactInfo = {
    'name': 'John Smith',
    'ssn': '123-45-6789',
    'email': 'foo@example.com'
};
ssn.prepareForStorage(contactInfo['ssn']).then(function (ciphertext, indexes) {
    console.log(ciphertext);
    /* nacl:jIRj08YiifK86YlMBfulWXbatpowNYf4_vgjultNT1Tnx2XH9ecs1TqD59MPs67Dp3ui */
    console.log(indexes);
    /* { contact_ssn_last_four: '2acb', contact_ssn: '311314c1' } */
});

Every time you run the above code, the ciphertext will be randomized, but the array of blind indexes will remain the same.

If you want the old "typed" index style, simply call setTypedIndexes(true) on any EncryptedField, EncryptedRow or EncryptedMultiRows object.

ssn.setTypedIndexes(true);
ssn.prepareForStorage(contactInfo['ssn']).then(function (ciphertext, indexes) {
    console.log(ciphertext);
    /* nacl:jIRj08YiifK86YlMBfulWXbatpowNYf4_vgjultNT1Tnx2XH9ecs1TqD59MPs67Dp3ui */
    console.log(indexes);
    /* { contact_ssn_last_four: { type: '3dywyifwujcu2', value: '2acb' },
         contact_ssn: { type: '2iztg3wbd7j5a', value: '311314c1' } } */
});

You can now use these values for inserting/updating records into your database.

To search the database at a later date, use getAllBlindIndexes() or getBlindIndex():

const {
    EncryptedField,
    BlindIndex,
    CipherSweet,
    LastFourDigits,
} = require('ciphersweet-js');

/** @var CipherSweet engine */
let ssn = (new EncryptedField(engine, 'contacts', 'ssn'))
    // Add a blind index for the "last 4 of SSN":
    .addBlindIndex(
        new BlindIndex(
            // Name (used in key splitting):
            'contact_ssn_last_four',
            // List of Transforms: 
            [new LastFourDigits()],
            // Bloom filter size (bits)
            16,
            // Fast hash (default: false)
            false
        )
    )
    // Add a blind index for the full SSN:
    .addBlindIndex(
        new BlindIndex(
            'contact_ssn', 
            [],
            32
        )
    );

// Use these values in search queries:
ssn.getAllBlindIndexes('123-45-6789').then(function (indexes) {
    console.log(indexes);
    /* { contact_ssn_last_four: '2acb', contact_ssn: '311314c1' } */
});
ssn.getBlindIndex('123-45-6789', 'contact_ssn_last_four').then(function (lastFour) {
    console.log(lastFour);
    /* 2acb */
});

EncryptedField with AAD

Both EncryptedField.encryptValue() and EncryptedField.prepareForStorage() allow an optional string to be passed to the second parameter, which will be included in the authentication tag on the ciphertext. It will NOT be stored in the ciphertext.

EncryptedRow

An alternative approach for datasets with multiple encrypted rows and/or encrypted boolean fields is the EncryptedRow API, which looks like this:

const {
    EncryptedRow,
    BlindIndex,
    CipherSweet,
    LastFourDigits,
} = require('ciphersweet-js');

/** @var CipherSweet engine */
let row = (new EncryptedRow(engine, 'contacts'))
    .addTextField('ssn')
    .addBooleanField('hivstatus');

// Add a normal Blind Index on one field:
row.addBlindIndex(
    'ssn',
    new BlindIndex(
        'contact_ssn_last_four',
        [new LastFourDigits()],
        32, // 32 bits = 4 bytes
        // Fast hash (default: false)
        false
    )
);

// Create/add a compound blind index on multiple fields:
row.addCompoundIndex(
    new CompoundIndex(
        'contact_ssnlast4_hivstatus',
        ['ssn', 'hivstatus'],
        32, // 32 bits = 4 bytes
        true // fast hash
    ).addTransform('ssn', new LastFourDigits())
);

// Now process some data:

let exampleInput = {
   'extraneous': true,
   'ssn': '123-45-6789',
   'hivstatus': false
};
row.prepareRowForStorage(exampleInput).then(function(prepared) {
    console.log(prepared);
    /*
    [ { extraneous: true,
        ssn: 'nacl:wVMElYqnHrGB4hU118MTuANZXWHZjbsd0uK2N0Exz72mrV8sLrI_oU94vgsWlWJc84-u',
        hivstatus: 'nacl:ctWDJBn-NgeWc2mqEWfakvxkG7qCmIKfPpnA7jXHdbZ2CPgnZF0Yzwg=' },
      { contact_ssn_last_four: '2acbcd1c',
        contact_ssnlast4_hivstatus: 'cbfd03c0' } ]
     */
});

With the EncryptedRow API, you can encrypt a subset of all of the fields in a row, and create compound blind indexes based on multiple pieces of data in the dataset rather than a single field, without writing a ton of glue code.

If you want the old "typed" index style, simply call setTypedIndexes(true) on any EncryptedField, EncryptedRow or EncryptedMultiRows object.

// Use flat indexes
row.setFlatIndexes(true);
row.prepareRowForStorage(exampleInput).then(function(prepared) {
    console.log(prepared);
    /*
    [ { extraneous: true,
        ssn: 'nacl:wVMElYqnHrGB4hU118MTuANZXWHZjbsd0uK2N0Exz72mrV8sLrI_oU94vgsWlWJc84-u',
        hivstatus: 'nacl:ctWDJBn-NgeWc2mqEWfakvxkG7qCmIKfPpnA7jXHdbZ2CPgnZF0Yzwg=' },
      { contact_ssn_last_four: { type: '3dywyifwujcu2', value: '2acbcd1c' },
        contact_ssnlast4_hivstatus: { type: 'nqtcc56kcf4qg', value: 'cbfd03c0' } } ]
     */
    
});

EncryptedRow with a CompoundIndex using a custom Transform of Multiple Fields

It's possible to quickly create a compound index that uses a transformation that combines multiple fields into one output string.

Following the previous example:

const {
    BlindIndex,
    CompoundIndex,
    LastFourDigits,
    RowTransformation
} = require('ciphersweet-js');

class FirstInitialLastName extends RowTransformation
{
    /**
     * @param {Array<string, string>} input
     * @return {string}
     */
    async invoke(input)
    {
        return await FirstInitialLastName.processArray(input);
    }
    
    /**
     * @param {Array<string, string>|Object<string, string>} input
     * @return {string}
     */
    static async processArray(input)
    {
        let first = input['first_name'].toString();
        let last = input['last_name'].toString();
        return first[0].toLowerCase() + last.toLowerCase();
    }
}

/** @var CipherSweet engine */
let row = (new EncryptedRow(engine, 'contacts'))
    .addTextField('first_name')
    .addTextField('last_name')
    .addTextField('ssn')
    .addBooleanField('hivstatus');

// Add a normal Blind Index on one field:
row.addBlindIndex(
    'ssn',
    new BlindIndex(
        'contact_ssn_last_four',
        [new LastFourDigits()],
        32 // 32 bits = 4 bytes
    )
);
row.addCompoundIndex(
    (
        new CompoundIndex(
            'contact_ssnlast4_hivstatus',
            ['ssn', 'hivstatus'],
            32, // 32 bits = 4 bytes
            true // fast hash
        )
    ).addTransform('ssn', new LastFourDigits())
);

// Notice the ->addRowTransform() method:
row.addCompoundIndex(
    row.createCompoundIndex(
        'contact_first_init_last_name',
        ['first_name', 'last_name'],
        64, // 64 bits = 8 bytes
        true
    ).addRowTransform(new FirstInitialLastName())
);

row.prepareRowForStorage({
    'first_name': 'Jane',
    'last_name': 'Doe',
    'extraneous': true,
    'ssn': '123-45-6789',
    'hivstatus': false
}).then(function() {
    console.log(arguments);
});

The above snippet defines a custom implementation of RowTransformationInterface that appends the first initial and the last name.

Note: You can achieve the same overall effect (but not the same hash output) using the default CompoundIndex.

EncryptedRow with AAD

You can also specify a separate plaintext column (e.g. primary or foreign key) as additional authenticated data.

This binds the ciphertext to a specific row, thereby preventing an attacker capable of replacing ciphertexts and using legitimate app access to decrypt ciphertexts they wouldn't otherwise have access to.

row.setAadSourceField('first_name', 'contactid');

This can also be included during the table instantiation:

const {CipherSweet, EncryptedRow} = require('ciphersweet-js');

/** @var CipherSweet engine */
row = (new EncryptedRow(engine, 'contacts'))
    .addTextField('first_name', 'contact_id');
    /* ... */

EncryptedMultiRows

CipherSweet also provides a multi-row abstraction to make it easier to manage heavily-normalized databases.

When working with EncryptedMultiRows, your arrays should be formatted as follows:

let input = {
    'table1': {
        'column1': 'value',
        'columnB': 123456,
        // ...
    },
    'table2': { /* ... */ },
    // ...
};

For example:

const {
    AlphaCharactersOnly,
    CipherSweet,
    EncryptedMultiRows,
    FirstCharacter,
    FIPSCrypto,
    Lowercase
} = require('ciphersweet-js');

let provider = new StringProvider(
    // Example key, chosen randomly, hex-encoded:
    'a981d3894b5884f6965baea64a09bb5b4b59c10e857008fc814923cf2f2de558'
);
let engine = new CipherSweet(provider, new FIPSCrypto());
let rowSet = (new EncryptedMultiRows(engine))
    .addTextField('contacts', 'first_name')
    .addTextField('contacts', 'last_name')
    .addFloatField('contacts', 'latitude')
    .addFloatField('contacts', 'longitude')
    .addTextField('foobar', 'test');

rowSet.addCompoundIndex(
    'contacts',
    rowset.createCompoundIndex(
        'contacts',
        'contact_first_init_last_name',
        ['first_name', 'last_name'],
        64, // 64 bits = 8 bytes
        true
    )
        .addTransform('first_name', new AlphaCharactersOnly())
        .addTransform('first_name', new Lowercase())
        .addTransform('first_name', new FirstCharacter())
        .addTransform('last_name', new AlphaCharactersOnly())
        .addTransform('last_name', new Lowercase())
);

rowSet.prepareForStorage({
    'contacts': {
        'contactid': 12345,
        'first_name': 'Jane',
        'last_name': 'Doe',
        'latitude': 52.52,
        'longitude': -33.106,
        'extraneous': true
    },
    'foobar': {
        'foobarid': 23,
        'contactid': 12345,
        'test': 'paragonie'
    }
}).then(function(prepared) {
    console.log(prepared);  
})

This will produce something similar to the following output:

[ { contacts: 
     { contactid: 12345,
       first_name: 'fips:8NSLNDWxN4u7OeN_v5ahnt-tgTNqrarsdhPwhMFT4uqtMsELj5L1D7KhukM1OSOKdwtgytiaut3-1kvtP8eSiIH8bQLidw3MwUFQ0JaxvNldI7rzVKeMP3yp4UVSrJZNH89nvQ==',
       last_name: 'fips:uk9FtD5HvXY4Fe8_ibXF32FurmV8WvAUVSWUPVhOcfmHNC-nol7EnNjdQ5vBG2HQmpeRaTjSE5QZNZ9TQGeK-HgaO3V_MCVQDTtN2u9-3HR4ehSFjn8rHbGt31Ygrh4CV6WV',
       latitude: 'fips:HE1PQoMso4FBu_rJWk0adWnp9i6HSBXQbf3QaHp1cw8-tOCDSm3rjiE1zIIrUmKarprPRzCTzb2BxdiXVg3RNsLH8iSko0ZmXSXhTa51XoEByxaH9fvAILpXttIfk8rsSXoIKgvMfcY=',
       longitude: 'fips:4gwnipUOws0kLW9gLmIgUNOM65ba1SVkibxILmJOpCbvw3853v_AaEGD-PO3b0fNwVnD6zbWdpovtHblAlXX2iOUvfqgrnwO21vPcYt8FaFkT706-_ZvbRioooL7NwFBqvJJWpiTnhA=',
       extraneous: true },
    foobar: 
     { foobarid: 23,
       contactid: 12345,
       test: 'fips:vnoJ6rIEBBMLCvXMt4gke8CT6PomgAExNufTZUrpPd3rp9y28jgopmXA7w8reqVe3SfE6KhRvN-lt5GQhzR1miQPVaIVq2V6D1i4eZCSKQDBmJ7PTAYuigNd9DPSL4qW3OAOtvagJ4Lc' } },
  { contacts: { contact_first_init_last_name: '546b1ffd1f83c37a' },
    foobar: {} } ]

EncryptedMultiRows with AAD

You can specify a separate plaintext column (e.g. primary or foreign key) as additional authenticated data.

This binds the ciphertext to a specific row, thereby preventing an attacker capable of replacing ciphertexts and using legitimate app access to decrypt ciphertexts they wouldn't otherwise have access to.

rowSet.setAadSourceField('contacts', 'first_name', 'contactid');

This can also be included during the table instantiation:

const {CipherSweet, EncryptedMultiRows} = require('ciphersweet-js');

/** @var CipherSweet engine */
let rowSet = (new EncryptedMultiRows(engine))
    .addTextField('contacts', 'first_name', 'contactid');
    /* ... */

Next: Blind Index Planning