ComDB
A PouchDB plugin that transparently encrypts and decrypts its data so that only encrypted data is sent during replication, while encrypted data that you receive is automatically decrypted. Uses TweetNaCl for cryptography.
As an example, here's what happens when you replicate data to a CouchDB cluster:
const PouchDB = require('pouchdb')
PouchDB.plugin(require('comdb'))
const password = 'extremely secure value'
const db = new PouchDB(POUCH_PATH)
db.setPassword(password).then(async () => {
await db.post({
_id: 'gay-agenda',
type: 'queerspiracy',
agenda: ['be gay', 'do crimes']
})
await db.replicate.to(`${COUCH_URL}/FALGSC`)
})
Now you can check the CouchDB for the encrypted information:
$ curl "$COUCH_URL/FALGSC/_all_docs?include_docs=true" | jq .
{
"total_rows": 1,
"offset": 0,
"rows": [
{
"id": "...",
"key": "...",
"value": {
"rev": "1-[...]"
},
"doc": {
"_id": "...",
"_rev": "1-[...]",
"payload": "...",
}
}
}
ComDB can also restore encrypted data that it doesn't already have
using your password.
const db = new PouchDB(`${POUCH_PATH}-2`)
db.setPassword(password, { name: `${COUCH_URL}/FALGSC` })
return db.loadEncrypted().then(async () => {
return db.allDocs({ include_docs: true })
}).then(({ rows }) => {
const { doc } = rows[0].doc
console.log(doc)
})
----
{ _id: 'gay-agenda',
_rev: '1-[...]',
type: 'queerspiracy',
agenda: [ 'be gay', 'do crimes' ] }
This way, the server can't (easily) know anything about your data, but you can still maintain query indexes.
In the above example, we replicated data from a local encrypted copy of our data, but you can use a CouchDB instance as your encrypted copy. That way, your documents will be automatically backed up to the remote instance.
const db = new PouchDB(POUCH_PATH)
await db.setPassword(password, { name: COUCH_URL })
You can also set up encryption on another device by using db.exportComDB()
and db.importComDB()
.
This is useful when you want to maintain a separate encrypted copy of your data, for example
because you want that separate copy to live on another device, while retaining the ability to
replicate with the original encrypted copy.
const key = await db.exportComDB()
const db = new PouchDB(POUCH_PATH)
await db.importComDB(password, key)
await db.replicate.from(COUCH_URL)
Now you can give your data to strangers with confidence!
For more examples, check out the /examples
folder.
Install
You can get ComDB with npm:
$ npm i comdb
Now you can require()
it in your node.js projects:
const PouchDB = require('pouchdb')
PouchDB.plugin(require('comdb'))
const db = new PouchDB(...)
await db.setPassword(...)
You can also use PouchDB and ComDB in the browser using browserify.
Usage
ComDB adds and extends several methods to PouchDB and any instances of it:
PouchDB.replicate(source, target, [opts], [callback])
ComDB wraps PouchDB's replicator to check if either the source or the target have an _encrypted
attribute, reflecting that they are ComDB instances. If it finds the attribute, it changes the parameter to use the encrypted database rather than its decrypted one. If neither the source or target is a ComDB instance, the replicator behaves as normal.
You can also disable this functionality by passing comdb: false
in the opts
parameter:
PouchDB.replicate(db1, db2, { comdb: false })
The instance methods db.replicate.to
and db.replicate.from
automatically use PouchDB.replicate
so that wrapping the static method causes the instance methods to exhibit the same behavior.
Original: PouchDB.replicate
async db.setPassword(password, [opts])
Mutates the instance with crypto tooling so that it can encrypt and decrypt documents.
await db.setPassword('hello world')
password
: A string used to encrypt and decrypt documents.opts.name
: A name or connection string for the encrypted database.opts.opts
: An options object passed to the encrypted database's constructor. Use this to pass any options accepted by PouchDB's constructor.
async db.destroy([opts], callback)
ComDB wraps PouchDB's database destruction method so that both the encrypted and decrypted databases are destroyed. ComDB adds two options to the method:
encrypted_only
: Destroy only the encrypted database. This is useful when a backup has become compromised and you need to burn it.unencrypted_only
: Destroy only the unencrypted database. This is useful if you are using a remote encrypted backup and want to burn the local device so you can restore from backup on a fresh one.
Original: db.destroy()
async db.exportComDB()
Export the encryption key specific to your database's encrypted copy. This is necessary to creating new encrypted copies that can still replicate with the original, for example if you're creating an encrypted copy on a phone by replicating down from a server.
const db1 = new PouchDB('device-1')
await db1.setPassword(password)
const key = await db1.exportComDB()
const db2 = new PouchDB('device-2')
await db2.importComDB(password, key)
await PouchDB.sync(db1, db2)
async db.importComDB(password, encryptionKey)
Set up ComDB, like db.setPassword()
, but rather than generating a new encryption key for your encrypted copy, ComDB will use the given one. This allows you to replicate with other encrypted databases using the same password and encryption key.
const db1 = new PouchDB('device-1')
await db1.setPassword(password)
const key = await db1.exportComDB()
const db2 = new PouchDB('device-2')
await db2.importComDB(password, key)
await PouchDB.sync(db1, db2)
async db.loadEncrypted(opts = {})
Load changes from the encrypted database into the decrypted one. Useful if you are restoring from backup:
const db = new PouchDB('local', { adapter: 'memory' })
db.setPassword(PASSWORD, { name: REMOTE_URL }).then(async () => {
await db.loadEncrypted()
})
Accepts the same options as PouchDB.replicate().
async db.loadDecrypted(opts = {})
Load changes from the decrypted database into the encrypted one. Useful if you are instrumenting encryption onto a database that already exists.
const db = new PouchDB('local')
db.setPassword(PASSWORD).then(async () => {
await db.loadDecrypted()
})
Accepts the same options as db.changes().
Recipe: End-to-End Encryption
ComDB can instrument end-to-end encryption of application data using pouchdb-adapter-memory, so that documents are only decrypted in memory while everything on disk remains encrypted.
Consider this setup:
const db = new PouchDB('local', { adapter: 'memory' })
db.setPassword(PASSWORD).then(async () => {
await db.loadEncrypted()
})
You can then replicate your encrypted database with a remote CouchDB installation to ensure you can restore your data even if your device is compromised:
const remoteDb = 'https://...'
const sync = PouchDB.sync(db, remoteDb, { live: true, retry: true })
Now you'll have three copies of your data:
- One in local memory, decrypted.
- One on local disk, encrypted.
- One on remote disk, encrypted.
The user syncs local disk with remote disk to have a remote encrypted backup, so the user can restore their info when switching devices. The local disk populates the in-memory database on startup, so that the only data that remains on disk remains encrypted. The user retains all their information locally, so they do not require network connectivity to use the app normally.
Development
To hack on ComDB, check out the issues page. To submit a patch, submit a pull request.
To run the test suite, use npm test
in the source directory:
$ git clone garbados/comdb
$ cd comdb
$ npm i
$ npm test
A formal code of conduct is forthcoming. Pending it, contributions will be moderated at the maintainers' discretion.
License
Apache-2.0