CipherSweet
Cross-platform, searchable field-level database encryptionBasic 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:
-
encryptedField.prepareForStorage()
-
encryptedField.getBlindIndex()
-
encryptedField.getAllBlindIndexes()
-
encryptedField.encryptValue()
-
encryptedField.decryptValue()
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