Comparing version 1.0.5-alpha to 1.1.0-beta
138
index.js
@@ -11,3 +11,3 @@ 'use strict' | ||
: promise.then((result) => { | ||
return callback(null, result || []) | ||
return callback(null, result) | ||
}).catch((error) => { | ||
@@ -18,64 +18,56 @@ return callback(error) | ||
function processEncryptedChange (decrypted, encrypted, ids = []) { | ||
if (ids.length === 0) return Promise.resolve() | ||
const promises = ids.map((id) => { | ||
return encrypted.get(id).then(({ payload }) => { | ||
return decrypted.decrypt(payload) | ||
}).then((newDoc) => { | ||
return decrypted.get(newDoc._id).then((doc) => { | ||
return isEqual(newDoc, doc) ? undefined : newDoc | ||
}).catch((error) => { | ||
if (error.name === 'not_found') { | ||
return newDoc | ||
module.exports = function (PouchDB) { | ||
// save originals | ||
const destroy = PouchDB.prototype.destroy | ||
const bulkDocs = PouchDB.prototype.bulkDocs | ||
const replicate = PouchDB.replicate | ||
// private helpers | ||
async function processEncryptedChange (decrypted, encrypted, ids = []) { | ||
if (ids.length === 0) { return } | ||
const changes = [] | ||
for (const id of ids) { | ||
const { payload } = await encrypted.get(id) | ||
const newDoc = await decrypted.decrypt(payload) | ||
let change | ||
try { | ||
const doc = decrypted.get(newDoc._id) | ||
change = isEqual(newDoc, doc) ? undefined : newDoc | ||
} catch (err) { | ||
if (err.name === 'not_found') { | ||
change = newDoc | ||
} else { | ||
return Promise.reject(error) | ||
throw err | ||
} | ||
}) | ||
}) | ||
}) | ||
return Promise.all(promises).then((results) => { | ||
const decryptedDocs = results.filter((x) => { return !!x }) | ||
const deletedDocs = decryptedDocs.filter((doc) => { | ||
return doc._deleted | ||
}) | ||
} | ||
changes.push(change) | ||
} | ||
const decryptedDocs = changes.filter(x => !!x) | ||
const deletedDocs = changes.filter(doc => !!doc._deleted) | ||
return Promise.all([ | ||
decrypted.bulkDocs(decryptedDocs, { new_edits: false }), | ||
decrypted.bulkDocs(deletedDocs) | ||
bulkDocs.call(decrypted, decryptedDocs, { new_edits: false }), | ||
bulkDocs.call(decrypted, deletedDocs) | ||
]).then(([results1, results2]) => { | ||
return results1.concat(results2) | ||
return [...results1, ...results2] | ||
}) | ||
}) | ||
} | ||
} | ||
function processDecryptedChange (decrypted, encrypted, ids) { | ||
if (ids.length === 0) return Promise.resolve() | ||
const promises = ids.map((id) => { | ||
return decrypted.get(id).then((doc) => { | ||
return decrypted.encrypt(doc) | ||
}).then((payload) => { | ||
async function processDecryptedChange (decrypted, encrypted, ids) { | ||
if (ids.length === 0) { return } | ||
const encryptedDocs = [] | ||
for (const id of ids) { | ||
const doc = await decrypted.get(id) | ||
const payload = await decrypted.encrypt(doc) | ||
const encryptedDoc = { payload } | ||
return encrypted.changes({ | ||
include_docs: true, | ||
limit: 1 | ||
}).then((result) => { | ||
if (result.results && result.results.length) { | ||
if (payload !== result.results[0].doc.payload) { | ||
return encryptedDoc | ||
} | ||
} else { | ||
return encryptedDoc | ||
const latestChanges = await encrypted.changes({ limit: 1, doc_ids: [id] }) | ||
if (latestChanges.results && latestChanges.results.length) { | ||
const latestChange = latestChanges.results[0].doc | ||
if (payload !== latestChange.payload) { | ||
encryptedDocs.push(encryptedDoc) | ||
} | ||
}) | ||
}) | ||
}) | ||
return Promise.all(promises).then((encryptedDocs) => { | ||
return encrypted.bulkDocs(encryptedDocs) | ||
}) | ||
} | ||
module.exports = function (PouchDB) { | ||
// save originals | ||
const destroy = PouchDB.prototype.destroy | ||
const bulkDocs = PouchDB.prototype.bulkDocs | ||
const replicate = PouchDB.replicate | ||
} else { | ||
encryptedDocs.push(encryptedDoc) | ||
} | ||
} | ||
return bulkDocs.call(encrypted, encryptedDocs) | ||
} | ||
// replication wrapper; handles ComDB instances transparently | ||
@@ -120,13 +112,21 @@ PouchDB.replicate = function (source, target, opts = {}, callback) { | ||
// - encrypts docs and maybe saves them to the encrypted db | ||
PouchDB.prototype.bulkDocs = function (docs, opts = {}, callback) { | ||
PouchDB.prototype.bulkDocs = async function (docs, opts = {}, callback) { | ||
if (typeof opts === 'function') { | ||
opts = {} | ||
callback = opts | ||
} | ||
if (!this._crypt) { | ||
const promise = bulkDocs.call(this, docs, opts) | ||
return cbify(promise, callback) | ||
} | ||
const processChange = processDecryptedChange.bind(null, this, this._encrypted) | ||
const promise = bulkDocs.call(this, docs, opts).then((results) => { | ||
const ids = results.map(({ id }) => { return id }).filter((id) => { | ||
const results = await bulkDocs.call(this, docs, opts) | ||
const ids = results | ||
.map(({ id }) => { return id }) | ||
.filter((id) => { | ||
const d = ((docs && docs.docs) || docs) | ||
return !d.map(({ _id }) => { return _id }).includes(id) | ||
}) | ||
return processChange(ids).then(() => { | ||
return results | ||
}) | ||
}) | ||
await processChange(ids) | ||
const promise = Promise.resolve(results) | ||
return cbify(promise, callback) | ||
@@ -148,7 +148,15 @@ } | ||
PouchDB.prototype.destroy = function (opts = {}, callback) { | ||
const promise = destroy.call(this._encrypted, opts).then(() => { | ||
return destroy.call(this, opts) | ||
}) | ||
let promise | ||
if (!this._encrypted || opts.unencrypted_only) { | ||
promise = destroy.call(this, opts) | ||
} else if (opts.encrypted_only) { | ||
promise = destroy.call(this._encrypted, opts) | ||
} else { | ||
promise = Promise.all([ | ||
destroy.call(this._encrypted, opts), | ||
destroy.call(this, opts) | ||
]) | ||
} | ||
return cbify(promise, callback) | ||
} | ||
} |
{ | ||
"name": "comdb", | ||
"version": "1.0.5-alpha", | ||
"version": "1.1.0-beta", | ||
"description": "A PouchDB plugin that transparently encrypts and decrypts its data.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -163,4 +163,7 @@ # ComDB | ||
ComDB wraps PouchDB's database destruction method so that both the encrypted and decrypted databases are destroyed. | ||
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](https://pouchdb.com/api.html#delete_database) | ||
@@ -167,0 +170,0 @@ |
@@ -24,40 +24,57 @@ /* global describe, it, before, after */ | ||
it('should encrypt writes', function () { | ||
it('should encrypt writes', async function () { | ||
this.timeout(4 * 1000) // sometimes runs long | ||
return this.db.post({ hello: 'world' }).then(({ id }) => { | ||
return this.db.get(id) | ||
}).then((doc) => { | ||
return this.db._encrypted.allDocs({ include_docs: true }).then(({ rows }) => { | ||
const { payload } = rows[0].doc | ||
return this.db.decrypt(payload) | ||
}).then((plainDoc) => { | ||
assert(isEqual(doc, plainDoc), 'Unencrypted and decrypted documents differ.') | ||
}) | ||
}) | ||
const { id } = await this.db.post({ hello: 'world' }) | ||
const doc = await this.db.get(id) | ||
const { rows } = await this.db._encrypted.allDocs({ include_docs: true }) | ||
const { payload } = rows[0].doc | ||
const plainDoc = await this.db.decrypt(payload) | ||
assert(isEqual(doc, plainDoc), 'Unencrypted and decrypted documents differ.') | ||
}) | ||
it('should decrypt and handle deletions', function () { | ||
// this test fails for reasons i do not understand | ||
return this.db.post({ hello: 'galaxy' }).then(({ id }) => { | ||
return this.db.get(id) | ||
}).then((doc) => { | ||
doc._deleted = true | ||
return this.db.encrypt(doc).then((payload) => { | ||
return this.db._encrypted.post({ payload }).then(({ id }) => { | ||
return this.db._encrypted.get(id) | ||
}).then(({ payload }) => { | ||
return this.db.decrypt(payload) | ||
}).then((plainDoc) => { | ||
assert.strictEqual(plainDoc._deleted, true) | ||
}) | ||
}).then(() => { | ||
let caught = false | ||
return this.db.get(doc._id).catch((error) => { | ||
assert.strictEqual(error.name, 'not_found') | ||
caught = true | ||
}).then((doc) => { | ||
assert(caught, 'Document was not deleted!') | ||
}) | ||
}) | ||
it('should decrypt and handle deletions', async function () { | ||
// delete a document by inserting a payload into the encrypted db | ||
const { id } = await this.db.post({ hello: 'galaxy' }) | ||
const doc = await this.db.get(id) | ||
doc._deleted = true | ||
const payload = await this.db.encrypt(doc) | ||
const { id: encryptedId } = await this.db._encrypted.post({ payload }) | ||
const encryptedDoc = await this.db._encrypted.get(encryptedId) | ||
const plainDoc = await this.db.decrypt(encryptedDoc.payload) | ||
assert.strictEqual(plainDoc._deleted, true) | ||
let caught = false | ||
// now test that it was deleted in the decrypted copy | ||
try { | ||
await this.db.get(doc._id) | ||
} catch (error) { | ||
assert.strictEqual(error.name, 'not_found') | ||
caught = true | ||
} | ||
assert(caught, 'Document was not deleted!') | ||
}) | ||
describe('destroy', function () { | ||
before(function () { | ||
this.db_destroyable_1 = new PouchDB('test-destroy-1') | ||
this.db_destroyable_2 = new PouchDB('test-destroy-2') | ||
this.db_destroyable_1.setPassword('goodpassword') | ||
this.db_destroyable_2.setPassword('goodpassword') | ||
}) | ||
after(async function () { | ||
await this.db_destroyable_1._encrypted.destroy() | ||
await this.db_destroyable_2.destroy({ unencrypted_only: true }) | ||
}) | ||
it('should optionally not destroy the encrypted copy', async function () { | ||
await this.db_destroyable_1.destroy({ unencrypted_only: true }) | ||
assert.equal(this.db_destroyable_1._encrypted._destroyed, undefined) | ||
assert.equal(this.db_destroyable_1._destroyed, true) | ||
}) | ||
it('should optionally not destroy the unencrypted copy', async function () { | ||
await this.db_destroyable_2.destroy({ encrypted_only: true }) | ||
assert.equal(this.db_destroyable_2._encrypted._destroyed, true) | ||
assert.equal(this.db_destroyable_2._destroyed, undefined) | ||
}) | ||
}) | ||
@@ -92,2 +109,24 @@ | ||
}) | ||
describe('issues', function () { | ||
it('should handle destruction without a set password', async function () { | ||
const db3 = new PouchDB('.test-destruction') | ||
await db3.destroy() | ||
assert(true) | ||
}) | ||
it('should handle calls to bulkDocs', async function () { | ||
const docs = [] | ||
const k = 10 | ||
for (let i = 0; i < k; i++) { | ||
docs.push({ | ||
_id: String(Math.floor(Math.random() * Date.now())), | ||
a: Math.random(), | ||
b: Math.random() * Date.now() | ||
}) | ||
} | ||
const result = await this.db.bulkDocs({ docs }) | ||
assert.equal(result.length, k) | ||
}) | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
24396
434
188