Skip to content

Latest commit

 

History

History
218 lines (171 loc) · 8.54 KB

field-level-encryption.md

File metadata and controls

218 lines (171 loc) · 8.54 KB

Integrating with MongoDB Client Side Field Level Encryption

Client Side Field Level Encryption, or CSFLE for short, is a tool for storing your data in an encrypted format in MongoDB. For example, instead of storing the name property as a plain-text string, CSFLE means MongoDB will store your document with name as an encrypted buffer. The resulting document will look similar to the following to a client that doesn't have access to decrypt the data.

{
  "_id" : ObjectId("647a3207661e3a3a1bc3e614"),
  "name" : BinData(6,"ASrIv7XfokKwiCUJEjckOdgCG+u6IqavcOWX8hINz29MLvcKDZ4nnjCnPFZG+0ftVxMdWgzu6Vdh7ys1uIK1WiaPN0SqpmmtL2rPoqT9gfhADpGDmI60+vm0bJepXNY1Gv0="),
  "__v" : 0
}

You can read more about CSFLE on the MongoDB CSFLE documentation and this blog post about CSFLE in Node.js.

Note that Mongoose does not currently have any Mongoose-specific APIs for CSFLE. Mongoose defers all CSFLE-related work to the MongoDB Node.js driver, so the autoEncryption option for mongoose.connect() and mongoose.createConnection() is where you put all CSFLE-related configuration. Mongoose schemas currently don't support CSFLE configuration.

Setting Up Field Level Encryption with Mongoose

First, you need to install the mongodb-client-encryption npm package. This is MongoDB's official package for setting up encryption keys.

npm install mongodb-client-encryption

You also need to make sure you've installed mongocryptd. mongocryptd is a separate process from the MongoDB server that you need to run to work with field level encryption. You can either run mongocryptd yourself, or make sure it is on the system PATH and the MongoDB Node.js driver will run it for you. You can read more about mongocryptd here.

Once you've set up and run mongocryptd, first you need to create a new encryption key as follows. Keep in mind that the following example is a simple example to help you get started. The encryption key in the following example is insecure; MongoDB recommends using a KMS.

const { ClientEncryption } = require('mongodb');
const mongoose = require('mongoose');

run().catch(err => console.log(err));

async function run() {
  /* Step 1: Connect to MongoDB and insert a key */

  // Create a very basic key. You're responsible for making
  // your key secure, don't use this in prod :)
  const arr = [];
  for (let i = 0; i < 96; ++i) {
    arr.push(i);
  }
  const key = Buffer.from(arr);

  const keyVaultNamespace = 'client.encryption';
  const kmsProviders = { local: { key } };

  const uri = 'mongodb://127.0.0.1:27017/mongoose_test';
  const conn = await mongoose.createConnection(uri, {
    autoEncryption: {
      keyVaultNamespace,
      kmsProviders
    }
  }).asPromise();
  const encryption = new ClientEncryption(conn.getClient(), {
    keyVaultNamespace,
    kmsProviders,
  });

  const _key = await encryption.createDataKey('local', {
    keyAltNames: ['exampleKeyName'],
  });
}

Once you have an encryption key, you can create a separate Mongoose connection with a schemaMap that defines which fields are encrypted using JSON schema syntax as follows.

/* Step 2: connect using schema map and new key */
await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test', {
  // Configure auto encryption
  autoEncryption: {
    keyVaultNamespace,
    kmsProviders,
    schemaMap: {
      'mongoose_test.tests': {
        bsonType: 'object',
        encryptMetadata: {
          keyId: [_key]
        },
        properties: {
          name: {
            encrypt: {
              bsonType: 'string',
              algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
            }
          }
        }
      }
    }
  }
});

With the above connection, if you create a model named 'Test' that uses the 'tests' collection, any documents will have their name property encrypted.

// 'super secret' will be stored as 'BinData' in the database,
// if you query using the `mongo` shell.
const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
await Model.create({ name: 'super secret' });

Automatic FLE in Mongoose

Mongoose supports the declaration of encrypted schemas - schemas that, when connected to a model, utilize MongoDB's Client Side Field Level Encryption or Queryable Encryption under the hood. Mongoose automatically generates either an encryptedFieldsMap or a schemaMap when instantiating a MongoClient and encrypts fields on write and decrypts fields on reads.

Encryption types

MongoDB has two different automatic encryption implementations: client side field level encryption (CSFLE) and queryable encryption (QE).
See choosing an in-use encryption approach.

Declaring Encrypted Schemas

The following schema declares two properties, name and ssn. ssn is encrypted using queryable encryption, and is configured for equality queries:

const encryptedUserSchema = new Schema({ 
  name: String,
  ssn: { 
    type: String, 
    // 1
    encrypt: { 
      keyId: '<uuid string of key id>',
      queries: 'equality'
    }
  }
  // 2
}, { encryptionType: 'queryableEncryption' });

To declare a field as encrypted, you must:

  1. Annotate the field with encryption metadata in the schema definition
  2. Choose an encryption type for the schema and configure the schema for the encryption type

Not all schematypes are supported for CSFLE and QE. For an overview of valid schema types, refer to MongoDB's documentation.

Registering Models

Encrypted schemas must be registered on a connection, not the Mongoose global:

const connection = mongoose.createConnection();
const UserModel = connection.model('User', encryptedUserSchema);

Connecting and configuring encryption options

CSFLE/QE in Mongoose work by generating the encryption schema that the MongoDB driver expects for each encrypted model on the connection. This happens automatically the model's connection is established.

Queryable encryption and CSFLE requires all the same configuration as outlined in <>, except for the schemaMap or encryptedFieldsMap options.

const keyVaultNamespace = 'client.encryption';
const kmsProviders = { local: { key } };
await connection.openUri(`mongodb://localhost:27017`, {
  // Configure auto encryption
  autoEncryption: {
    keyVaultNamespace: 'datakeys.datakeys',
    kmsProviders
  }
});

Once the connection is established, Mongoose's operations will work as usual. Writes are encrypted automatically by the MongoDB driver prior to sending them to the server and reads are decrypted by the driver after fetching documents from the server.

Discriminators

Discriminators are supported for encrypted models as well:

const connection = createConnection();

const schema = new Schema({
  name: {
    type: String, encrypt: { keyId }
  }
}, {
  encryptionType: 'queryableEncryption'
});

const Model = connection.model('BaseUserModel', schema);
const ModelWithAge = model.discriminator('ModelWithAge', new Schema({
  age: {
    type: Int32, encrypt: { keyId: keyId2 }
  }
}, {
  encryptionType: 'queryableEncryption'
}));

const ModelWithBirthday = model.discriminator('ModelWithBirthday', new Schema({
  dob: {
    type: Int32, encrypt: { keyId: keyId3 }
  }
}, {
  encryptionType: 'queryableEncryption'
}));

When generating encryption schemas, Mongoose merges all discriminators together for the all discriminators declared on the same namespace. As a result, discriminators that declare the same key with different types are not supported. Furthermore, all discriminators must share the same encryption type - it is not possible to configure discriminators on the same model for both CSFLE and QE.