crypto-pouch
Advanced tools
Comparing version 3.1.3 to 4.0.0
253
index.js
@@ -1,185 +0,86 @@ | ||
'use strict'; | ||
var pbkdf2 = require('native-crypto/pbkdf2'); | ||
var randomBytes = require('randombytes'); | ||
var configId = '_local/crypto'; | ||
var chachaHelper = require('./chacha'); | ||
var gcmHelper = require('./gcm'); | ||
var defaultDigest = 'sha256'; | ||
var defaultIterations = 100000; | ||
var previousIterations = 1000; | ||
var defaultAlgo = 'aes-gcm'; | ||
var transform = require('transform-pouch').transform; | ||
var uuid = require('uuid'); | ||
function noop(){} | ||
function cryptoInit(password, options) { | ||
var db = this; | ||
var key, cb; | ||
var turnedOff = false; | ||
var ignore = ['_id', '_rev', '_deleted'] | ||
if (!options) { | ||
options = {}; | ||
} | ||
var algo; | ||
if (password && typeof password === 'object') { | ||
options = password; | ||
password = password.password; | ||
delete options.password; | ||
} | ||
if (options.ignore) { | ||
ignore = ignore.concat(options.ignore); | ||
} | ||
if(typeof options.cb === 'function') { | ||
cb = options.cb; | ||
} else { | ||
cb = noop; | ||
} | ||
var pending; | ||
if (options.key && !Buffer.isBuffer(options.key) && options.key instanceof global.Uint8Array) { | ||
options.key = new Buffer(options.key) | ||
} | ||
if (Buffer.isBuffer(options.key) && options.key.length === 32) { | ||
key = options.key; | ||
pending = db.get(configId).catch(function () { | ||
return {}; | ||
}).then(function (doc) { | ||
algo = setAlgo(doc); | ||
}); | ||
} else { | ||
var digest = options.digest || defaultDigest; | ||
var iterations = options.iteration || defaultIterations; | ||
pending = db.get(configId).then(function (doc){ | ||
if (!doc.salt) { | ||
throw { | ||
status: 'invalid', | ||
doc: doc | ||
}; | ||
} | ||
return doc; | ||
}).catch(function (err) { | ||
var doc; | ||
if (err.status === 404) { | ||
doc = { | ||
_id: configId, | ||
salt: randomBytes(16).toString('hex'), | ||
digest: digest, | ||
iterations: iterations, | ||
algo: options.algorithm || defaultAlgo | ||
}; | ||
} else if (err.status === 'invalid' && err.doc) { | ||
doc = err.doc; | ||
doc.salt = randomBytes(16).toString('hex'); | ||
doc.digest = digest; | ||
doc.iterations = iterations; | ||
doc.algo = options.algorithm || defaultAlgo; | ||
} | ||
if (doc) { | ||
return db.put(doc).then(function () { | ||
return doc; | ||
}); | ||
} | ||
throw err; | ||
}).then(function (doc) { | ||
algo = setAlgo(doc); | ||
return pbkdf2(password, new Buffer(doc.salt, 'hex'), doc.iterations || options.iteration || previousIterations, 256 / 8, doc.digest || digest); | ||
}).then(function (_key) { | ||
password = null; | ||
if (turnedOff) { | ||
randomize(key); | ||
} else { | ||
key = _key; | ||
} | ||
process.nextTick(function () { | ||
cb(null, key); | ||
}); | ||
}).catch(function (e) { | ||
process.nextTick(function (){ | ||
cb(e); | ||
}); | ||
throw e; | ||
}); | ||
} | ||
db.transform({ | ||
incoming: function (doc) { | ||
return pending.then(function () { | ||
return encrypt(doc); | ||
}); | ||
}, | ||
outgoing: function (doc) { | ||
return pending.then(function () { | ||
return decrypt(doc); | ||
}); | ||
const Crypt = require('garbados-crypt') | ||
const { transform } = require('transform-pouch') | ||
const LOCAL_ID = '_local/crypto' | ||
const IGNORE = ['_id', '_rev', '_deleted', '_conflicts'] | ||
const NO_COUCH = 'crypto-pouch does not work with pouchdb\'s http adapter. Use a local adapter instead.' | ||
module.exports = { | ||
transform, | ||
crypto: async function (password, options = {}) { | ||
if (this.adapter === 'http') { | ||
throw new Error(NO_COUCH) | ||
} | ||
}); | ||
db.removeCrypto = function () { | ||
if (key) { | ||
randomize(key); | ||
if (typeof password === 'object') { | ||
// handle `db.crypto({ password, ...options })` | ||
options = password | ||
password = password.password | ||
delete options.password | ||
} | ||
turnedOff = true; | ||
}; | ||
function setAlgo(doc) { | ||
if (typeof doc.algo !== 'string') { | ||
return chachaHelper; | ||
} else if (doc.algo.toLowerCase().indexOf('chacha') > -1) { | ||
return chachaHelper; | ||
} else if (doc.algo.toLowerCase().indexOf('gcm') > -1) { | ||
return gcmHelper; | ||
} else { | ||
throw new Error('invalid algo'); | ||
// setup ignore list | ||
this._ignore = IGNORE.concat(options.ignore || []) | ||
// setup crypto helper | ||
const trySetup = async () => { | ||
// try saving credentials to a local doc | ||
try { | ||
// first we try to get saved creds from the local doc | ||
const { exportString } = await this.get(LOCAL_ID) | ||
this._crypt = await Crypt.import(password, exportString) | ||
} catch (err) { | ||
// istanbul ignore else | ||
if (err.status === 404) { | ||
// but if the doc doesn't exist, we do first-time setup | ||
this._crypt = new Crypt(password) | ||
const exportString = await this._crypt.export() | ||
try { | ||
await this.put({ _id: LOCAL_ID, exportString }) | ||
} catch (err2) { | ||
// istanbul ignore else | ||
if (err2.status === 409) { | ||
// if the doc was created while we were setting up, | ||
// try setting up again to retrieve the saved credentials. | ||
await trySetup() | ||
} else { | ||
throw err2 | ||
} | ||
} | ||
} else { | ||
throw err | ||
} | ||
} | ||
} | ||
} | ||
function encrypt(doc) { | ||
var nonce = randomBytes(12) | ||
var outDoc = { | ||
nonce: nonce.toString('hex') | ||
}; | ||
// for loop performs better than .forEach etc | ||
for (var i = 0, len = ignore.length; i < len; i++) { | ||
outDoc[ignore[i]] = doc[ignore[i]] | ||
delete doc[ignore[i]] | ||
} | ||
if (!outDoc._id) { | ||
outDoc._id = uuid.v4() | ||
} | ||
// Encrypting attachments is complicated | ||
// https://github.com/calvinmetcalf/crypto-pouch/pull/18#issuecomment-186402231 | ||
if (doc._attachments) { | ||
throw new Error('Attachments cannot be encrypted. Use {ignore: "_attachments"} option') | ||
} | ||
var data = JSON.stringify(doc); | ||
return algo.encrypt(data, key, nonce, new Buffer(outDoc._id)).then(function (resp) { | ||
outDoc.tag = resp.tag; | ||
outDoc.data = resp.data; | ||
return outDoc; | ||
await trySetup() | ||
// instrument document transforms | ||
this.transform({ | ||
incoming: async (doc) => { | ||
// if no crypt, ex: after .removeCrypto(), just return the doc | ||
if (!this._crypt) { return doc } | ||
if (doc._attachments && !this._ignore.includes('_attachments')) { | ||
throw new Error('Attachments cannot be encrypted. Use {ignore: "_attachments"} option') | ||
} | ||
const encrypted = {} | ||
for (const key of this._ignore) { | ||
encrypted[key] = doc[key] | ||
} | ||
encrypted.payload = await this._crypt.encrypt(JSON.stringify(doc)) | ||
return encrypted | ||
}, | ||
outgoing: async (doc) => { | ||
// if no crypt, ex: after .removeCrypto(), just return the doc | ||
if (!this._crypt) { return doc } | ||
const decryptedString = await this._crypt.decrypt(doc.payload) | ||
const decrypted = JSON.parse(decryptedString) | ||
return decrypted | ||
} | ||
}) | ||
}, | ||
removeCrypto: function () { | ||
delete this._crypt | ||
} | ||
function decrypt(doc) { | ||
if (turnedOff || !doc.nonce || !doc._id || !doc.tag || !doc.data) { | ||
return doc; | ||
} | ||
return algo.decrypt(new Buffer(doc.data, 'hex'), key, new Buffer(doc.nonce, 'hex'), new Buffer(doc._id), new Buffer(doc.tag, 'hex')).then(function (outData) { | ||
var out = JSON.parse(outData); | ||
for (var i = 0, len = ignore.length; i < len; i++) { | ||
out[ignore[i]] = doc[ignore[i]] | ||
} | ||
return out; | ||
}); | ||
} | ||
} | ||
function randomize(buf) { | ||
var len = buf.length; | ||
var data = randomBytes(len); | ||
var i = -1; | ||
while (++i < len) { | ||
buf[i] = data[i]; | ||
} | ||
} | ||
exports.transform = transform; | ||
exports.crypto = cryptoInit; | ||
// istanbul ignore next | ||
if (typeof window !== 'undefined' && window.PouchDB) { | ||
window.PouchDB.plugin(module.exports); | ||
window.PouchDB.plugin(module.exports) | ||
} |
{ | ||
"name": "crypto-pouch", | ||
"version": "3.1.3", | ||
"version": "4.0.0", | ||
"description": "encrypted pouchdb/couchdb database", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "node test.js | tspec", | ||
"build": "browserify ./ > bundle.js", | ||
"min": "browserify ./ | uglifyjs -mc > bundle.min.js" | ||
"test": "standard && dependency-check --unused --no-dev . && mocha", | ||
"cov": "nyc npm test", | ||
"build": "browserify . > bundle.js", | ||
"min": "browserify . | uglifyjs -mc > bundle.min.js" | ||
}, | ||
@@ -27,17 +28,15 @@ "repository": { | ||
"dependencies": { | ||
"chacha": "^2.1.0", | ||
"native-crypto": "^1.5.2", | ||
"pouchdb-promise": "^6.1.0", | ||
"randombytes": "^2.0.3", | ||
"transform-pouch": "^1.1.0", | ||
"uuid": "^3.0.1" | ||
"garbados-crypt": "^3.0.0-beta", | ||
"transform-pouch": "^1.1.5" | ||
}, | ||
"devDependencies": { | ||
"browserify": "^13.0.0", | ||
"memdown": "^1.0.0", | ||
"pouchdb": "^6.1.0", | ||
"tap-spec": "^4.1.1", | ||
"tape": "^4.5.1", | ||
"uglify-js": "^2.6.2" | ||
"browserify": "^17.0.0", | ||
"dependency-check": "^4.1.0", | ||
"memdown": "^6.0.0", | ||
"mocha": "^8.3.2", | ||
"nyc": "^15.1.0", | ||
"pouchdb": "^7.2.2", | ||
"standard": "^16.0.3", | ||
"uglify-js": "^3.13.5" | ||
} | ||
} |
164
readme.md
@@ -1,148 +0,94 @@ | ||
crypto pouch [![Build Status](https://travis-ci.org/calvinmetcalf/crypto-pouch.svg)](https://travis-ci.org/calvinmetcalf/crypto-pouch) | ||
=== | ||
# Crypto-Pouch | ||
Plugin to encrypt a PouchDB/CouchDB database. | ||
[![CI](https://github.com/calvinmetcalf/crypto-pouch/actions/workflows/ci.yaml/badge.svg)](https://github.com/calvinmetcalf/crypto-pouch/actions/workflows/ci.yaml) | ||
[![NPM Version](https://img.shields.io/npm/v/crypto-pouch.svg?style=flat-square)](https://www.npmjs.com/package/crypto-pouch) | ||
[![JS Standard Style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) | ||
Plugin to encrypt a PouchDB database. | ||
```js | ||
var db = new PouchDB('my_db'); | ||
const PouchDB = require('pouchdb') | ||
PouchDB.plugin(require('crypto-pouch')) | ||
db.crypto(password); | ||
// all done, docs should be transparently encrypted/decrypted | ||
const db = new PouchDB('my_db') | ||
db.removeCrypto(); | ||
// will no longer encrypt decrypt your data | ||
// init; after this, docs will be transparently en/decrypted | ||
db.crypto(password).then(() => { | ||
// db will now transparently encrypt writes and decrypt reads | ||
await db.put({ ... }) | ||
// you can disable transparent en/decryption, | ||
// though encrypted docs remain encrypted | ||
db.removeCrypto() | ||
}) | ||
``` | ||
It encrypts with the AES-GCM using [native crypto](https://github.com/calvinmetcalf/native-crypto) which prefers the native version in node or the web crypto version in the browser, falling back to the version from [crypto browserify](https://github.com/crypto-browserify/crypto-browserify) if no native version exists. [Chacha20-Poly1305](https://github.com/calvinmetcalf/chacha20poly1305) is also available and previous versions defaulted to this algorithm. You might consider using this if your app will primarily be used in browsers that don't support the web crypto api (e.g. safari). | ||
Crypto-Pouch encrypts documents using [TweetNaCl.js](https://github.com/dchest/tweetnacl-js), an [audited](https://cure53.de/tweetnacl.pdf) encryption library. It uses the *xsalsa20-poly1305* algorithm. | ||
**Note**: Attachments cannot be encrypted at this point. Use `{ignore: '_attachments'}` to leave attachments unencrypted. Also note that `db.putAttachment` / `db.getAttachment` are not supported. Use `db.put` and `db.get({binary: true, attachment: true})` instead. ([#18](https://github.com/calvinmetcalf/crypto-pouch/issues/13)). | ||
This only encrypts the contents of documents, NOT THE ID (or rev). So if you have a document with the id `plan_to_screw_over_db_admin`, while this plugin will happily encrypt that document, that may not be as helpful as you'd want it to be. | ||
This only encrypts the contents of documents, **not the \_id or \_rev, nor view keys and values**. This means that `_id` values always remain unencrypted, and any keys or values emitted by views are stored unencrypted as well. If you need total encryption at rest, consider using the PouchDB plugin [ComDB](https://github.com/garbados/comdb) instead. | ||
Usage | ||
------- | ||
## Usage | ||
This plugin is hosted on npm. To use in Node.js: | ||
This plugin is hosted on [npm](http://npmjs.com/). To install it in your project: | ||
```bash | ||
npm install crypto-pouch | ||
$ npm install crypto-pouch | ||
``` | ||
If you want to use it in the browser, download [the browserified version from wzrd.in](http://wzrd.in/standalone/crypto-pouch) and then include it after `pouchdb`: | ||
## Usage | ||
```html | ||
<script src="pouchdb.js"></script> | ||
<script src="pouchdb.crypto-pouch.js"></script> | ||
``` | ||
### async db.crypto(password [, options]) | ||
API | ||
-------- | ||
Set up encryption on the database. | ||
- `password`: A string password, used to encrypt documents. Make sure it's good! | ||
- `options.ignore`: Array of strings of properties that will not be encrypted. | ||
### db.crypto(password [, options]) | ||
You may also pass an options object as the first parameter, like so: | ||
Set up encryption on the database. | ||
```javascript | ||
db.crypto({ password, ignore: [...] }).then(() => { | ||
// database will now encrypt writes and decrypt reads | ||
}) | ||
``` | ||
If the second argument is an object: | ||
- `options.ignore` | ||
String or Array of Strings of properties that will not be encrypted. | ||
- `options.digest` | ||
Any of `sha1`, `sha256`, `sha512` (default). | ||
- `options.algorithm` | ||
Valid options are `chacha20` and `aes-gcm` (default). | ||
- `iterations` | ||
How many iterations of pbkdf2 to perform, defaults to 100000 (1000 in older versions). | ||
- `key` | ||
If passed a 32 byte buffer then this will be used as the key instead of it being generated from the password. **Warning** this buffer will be randomized when encryption is removed so pass in a copy of the buffer if that will be a problem. | ||
- `password` | ||
You can pass the options object as the first param if you really want and pass in the password in as an option. | ||
- `cb` | ||
A function you can pass in to get the derived key back called with 2 parameters, an error if there is one and the key if no error. **Warning** this buffer will be randomized when encryption is removed copy it or convert it to a string if that will be a problem. | ||
### db.removeCrypto() | ||
Disables encryption on the database and randomizes the key buffer. | ||
Disables encryption on the database and forgets your password. | ||
Details | ||
=== | ||
## Details | ||
If you replicate to another database, it will decrypt before sending it to | ||
the external one. So make sure that one also has a password set as well if you want | ||
it encrypted too. | ||
If you replicate to another database, Crypto-Pouch will decrypt documents before | ||
sending them to the target database. Documents received through replication will | ||
be encrypted before being saved to disk. | ||
If you change the name of a document, it will throw an error when you try | ||
If you change the ID of a document, Crypto-Pouch will throw an error when you try | ||
to decrypt it. If you manually move a document from one database to another, | ||
it will not decrypt correctly. If you need to decrypt it a file manually | ||
you will find a local doc named `_local/crypto` in the database. This doc has | ||
fields named `salt` which is a hex-encoded buffer, `digest` which is a string, `iterations` which is an integer to use and `algo` which is the encryption algorithm. Run pbkdf2 your password with the | ||
salt, digest and iterations values from that document as the parameters generate | ||
a 32 byte (256 bit) key; that is the key for decoding documents. If digest, iterations, or algo are not on the local document due to it being created with an older version of the library, use 'sha256', 1000, and 'chacha20' respectively. | ||
it will not decrypt correctly. | ||
Each document has 3 relevant fields: `data`, `nonce`, and `tag`. | ||
`nonce` is the initialization vector to give to the encryption algorithm in addition to the key | ||
you generated. Pass the document `_id` as additional authenticated data and the tag | ||
as the auth tag and then decrypt the data. If it throws an error, then you either | ||
screwed up or somebody modified the data. | ||
Encrypted documents have only one custom property, `payload`, which contains the | ||
encrypted contents of the unencrypted document. So, `{ hello: 'world' }` becomes | ||
`{ payload: '...' }`. This `payload` value is produced by [garbados-crypt](https://github.com/garbados/crypt#garbados-crypt); see that library for more details. | ||
Examples | ||
=== | ||
## Development | ||
Derive key from password and salt | ||
--- | ||
First, get the source: | ||
```js | ||
db.get('_local/crypto').then(function (doc) { | ||
return new Promise(function (resolve, reject) { | ||
crypto.pbkdf2(password, doc.salt, doc.iterations, 256/8, doc.digest, function (err, key) { | ||
if (err) { | ||
return reject(err); | ||
} | ||
resolve(key); | ||
}); | ||
}); | ||
}).then(function (key) { | ||
// you have the key | ||
}); | ||
```bash | ||
$ git clone git@github.com:calvinmetcalf/crypto-pouch.git | ||
$ cd crypto-pouch | ||
$ npm i | ||
``` | ||
Decrypt a document encrypted with chacha | ||
--- | ||
Use the test suite: | ||
```js | ||
var chacha = require('chacha'); | ||
db.get(id).then(function (doc) { | ||
var decipher = chacha.createDecipher(key, new Buffer(doc.nonce, 'hex')); | ||
decipher.setAAD(new Buffer(doc._id)); | ||
decipher.setAuthTag(new Buffer(doc.tag, 'hex')); | ||
var out = decipher.update(new Buffer(doc.data, 'hex')).toString(); | ||
decipher.final(); | ||
// parse it AFTER calling final | ||
// you don't want to parse it if it has been manipulated | ||
out = JSON.parse(out); | ||
out._id = doc._id; | ||
out._rev = doc._rev; | ||
return out; | ||
}); | ||
```bash | ||
$ npm test | ||
``` | ||
Decrypt a document encrypted with aes-gcm | ||
--- | ||
*When contributing patches, be a good neighbor and include tests!* | ||
```js | ||
var decrypt = require('native-crypto/decrypt'); | ||
## License | ||
db.get(id).then(function (doc) { | ||
var encryptedData = Buffer.concat([ | ||
new Buffer(doc.data, 'hex'), | ||
new Buffer(doc.tag, 'hex') | ||
]); | ||
return decrypt(key, new Buffer(doc.nonce, 'hex'), encryptedData, new Buffer(doc._id)).then(function (resp) { | ||
var out = JSON.parse(resp.toString()); | ||
out._id = doc._id; | ||
out._rev = doc._rev; | ||
return out; | ||
}); | ||
}); | ||
``` | ||
See [LICENSE](./LICENSE). |
416
test.js
@@ -1,275 +0,167 @@ | ||
var test = require('tape'); | ||
var PouchDB = require('pouchdb'); | ||
var memdown = require('memdown'); | ||
var Promise = require('pouchdb-promise'); | ||
PouchDB.plugin(require('./')); | ||
test('basic', function (t) { | ||
t.plan(4); | ||
var dbName = 'one'; | ||
var db = new PouchDB(dbName, {db: memdown}); | ||
db.crypto('password'); | ||
db.put({foo: 'bar', _id: 'baz'}).then(function () { | ||
return db.get('baz'); | ||
}).then(function (resp) { | ||
t.equals(resp.foo, 'bar', 'decrypts data'); | ||
db.removeCrypto(); | ||
return db.get('baz'); | ||
}).then(function (doc) { | ||
t.ok(doc.nonce, 'has nonce'); | ||
t.ok(doc.tag, 'has tag'); | ||
t.ok(doc.data, 'has data'); | ||
}).catch(function (e) { | ||
t.error(e); | ||
}); | ||
}); | ||
test('reopen', function (t) { | ||
t.plan(1); | ||
var dbName = 'one'; | ||
var db = new PouchDB(dbName, {db: memdown}); | ||
db.crypto('password'); | ||
db.get('baz').then(function (resp) { | ||
t.equals(resp.foo, 'bar', 'decrypts data'); | ||
}); | ||
}); | ||
test('changes', function (t) { | ||
t.plan(7); | ||
var dbName = 'five'; | ||
var db = new PouchDB(dbName, {db: memdown}); | ||
db.changes({ live: true, include_docs: true}).on('change', function () { | ||
t.ok(true, 'changes called'); | ||
/* global describe it beforeEach afterEach emit */ | ||
const assert = require('assert').strict | ||
const memdown = require('memdown') | ||
const PouchDB = require('pouchdb') | ||
PouchDB.plugin(require('.')) | ||
const PASSWORD = 'hello world' | ||
const BAD_PASS = 'goodbye sol' | ||
const NAME = 'crypto-pouch-testing' | ||
const DOCS = [ | ||
{ _id: 'a', hello: 'world' }, | ||
{ _id: 'b', hello: 'sol' }, | ||
{ _id: 'c', hello: 'galaxy' } | ||
] | ||
const ATTACHMENTS = { | ||
'att.txt': { | ||
content_type: 'text/plain', | ||
data: 'TGVnZW5kYXJ5IGhlYXJ0cywgdGVhciB1cyBhbGwgYXBhcnQKTWFrZSBvdXIgZW1vdGlvbnMgYmxlZWQsIGNyeWluZyBvdXQgaW4gbmVlZA==' | ||
} | ||
} | ||
describe('crypto-pouch', function () { | ||
beforeEach(async function () { | ||
this.db = new PouchDB(NAME, { db: memdown }) | ||
await this.db.crypto(PASSWORD) | ||
}) | ||
db.crypto('password'); | ||
db.put({foo: 'bar', _id: 'baz'}).then(function () { | ||
return db.get('baz'); | ||
}).then(function (resp) { | ||
t.equals(resp.foo, 'bar', 'decrypts data'); | ||
return db.post({baz: 'bat'}); | ||
}).then(function (d){ | ||
return new Promise(function (yes) { | ||
setTimeout(function () { | ||
yes(d); | ||
}, 200); | ||
}); | ||
}).then(function(d) { | ||
return db.put({ | ||
_id: d.id, | ||
_rev: d.rev, | ||
once: 'more', | ||
with: 'feeling' | ||
}); | ||
}).then(function () { | ||
return db.allDocs({include_docs: true}); | ||
}).then(function () { | ||
db.removeCrypto(); | ||
return db.get('baz'); | ||
}).then(function (doc) { | ||
t.ok(doc.nonce, 'has nonce'); | ||
t.ok(doc.tag, 'has tag'); | ||
t.ok(doc.data, 'has data'); | ||
}).catch(function (e) { | ||
t.error(e); | ||
}); | ||
}); | ||
test('ignore: _attachments', function (t) { | ||
t.plan(1); | ||
var dbName = 'six'; | ||
var db = new PouchDB(dbName, {db: memdown}); | ||
db.crypto('password', {ignore: '_attachments'}) | ||
db.put({ | ||
_id: 'id-12345678', | ||
_attachments: { | ||
'att.txt': { | ||
content_type: 'text/plain', | ||
data: 'TGVnZW5kYXJ5IGhlYXJ0cywgdGVhciB1cyBhbGwgYXBhcnQKTWFrZS' + | ||
'BvdXIgZW1vdGlvbnMgYmxlZWQsIGNyeWluZyBvdXQgaW4gbmVlZA==' | ||
} | ||
afterEach(async function () { | ||
await this.db.destroy() | ||
}) | ||
it('should encrypt documents', async function () { | ||
const doc = DOCS[0] | ||
await this.db.put(doc) | ||
const decrypted = await this.db.get(doc._id) | ||
assert.equal(decrypted.hello, doc.hello) | ||
// now let's ensure that doc is encrypted at rest | ||
this.db.removeCrypto() | ||
const encrypted = await this.db.get(doc._id) | ||
assert.notEqual(encrypted.hello, doc.hello) | ||
}) | ||
it('should not encrypt documents after crypto is removed', async function () { | ||
const [doc1, doc2] = DOCS.slice(0, 2) | ||
await this.db.put(doc1) | ||
this.db.removeCrypto() | ||
await this.db.put(doc2) | ||
const encrypted = await this.db.get(doc1._id) | ||
assert.notEqual(encrypted.hello, doc1.hello) | ||
const decrypted = await this.db.get(doc2._id) | ||
assert.equal(decrypted.hello, doc2.hello) | ||
}) | ||
it('should fail when using a bad password', async function () { | ||
await this.db.put({ _id: 'a', hello: 'world' }) | ||
this.db.removeCrypto() | ||
await this.db.crypto(BAD_PASS) | ||
try { | ||
await this.db.get('a') | ||
throw new Error('read succeeded but should have failed') | ||
} catch (error) { | ||
assert.equal(error.message, 'Could not decrypt!') | ||
} | ||
}).then(function () { | ||
return db.get('id-12345678', { | ||
attachments: true, | ||
binary: true | ||
}); | ||
}).then(function (doc) { | ||
t.ok(Buffer.isBuffer(doc._attachments['att.txt'].data), 'returns _attachments as Buffers'); | ||
}) | ||
.catch(t.error); | ||
}) | ||
test('throws error when document has attachments', function (t) { | ||
t.plan(1); | ||
var dbName = 'eight'; | ||
var db = new PouchDB(dbName, {db: memdown}); | ||
db.crypto('password') | ||
db.put({ | ||
_id: 'id-12345678', | ||
_attachments: { | ||
'att.txt': { | ||
content_type: 'text/plain', | ||
data: 'TGVnZW5kYXJ5IGhlYXJ0cywgdGVhciB1cyBhbGwgYXBhcnQKTWFrZS' + | ||
'BvdXIgZW1vdGlvbnMgYmxlZWQsIGNyeWluZyBvdXQgaW4gbmVlZA==' | ||
it('should preserve primary index sorting', async function () { | ||
for (const doc of DOCS) { await this.db.put(doc) } | ||
const result = await this.db.allDocs() | ||
for (let i = 0; i < result.rows.length - 1; i++) { | ||
const row = result.rows[i] | ||
const next = result.rows[i + 1] | ||
assert(row.key < next.key) | ||
} | ||
}) | ||
it('should preserve secondary index sorting', async function () { | ||
for (const doc of DOCS) { await this.db.put(doc) } | ||
await this.db.put({ | ||
_id: '_design/test', | ||
views: { | ||
test: { | ||
map: function (doc) { emit(doc.hello) }.toString() | ||
} | ||
} | ||
}) | ||
const result = await this.db.query('test') | ||
const EXPECTED = DOCS.map(({ hello }) => { return hello }) | ||
for (let i = 0; i < result.rows.length - 1; i++) { | ||
const row = result.rows[i] | ||
const next = result.rows[i + 1] | ||
assert(row.key < next.key) | ||
// ensure that keys are not encrypted | ||
assert(EXPECTED.includes(row.key)) | ||
assert(EXPECTED.includes(next.key)) | ||
} | ||
}).then(function () { | ||
t.error('does not throw error'); | ||
}).catch(function (e) { | ||
t.ok(/Attachments cannot be encrypted/.test(e.message), 'throws error'); | ||
}) | ||
}) | ||
test('options.digest with sha512 default', function (t) { | ||
t.plan(2); | ||
var db1 = new PouchDB('ten', {db: memdown}); | ||
var db2 = new PouchDB('eleven', {db: memdown}); | ||
// simulate previously doc created with {digest: sha512} | ||
var docSha256 = { | ||
nonce: '619cf4a32914bc9b5ca26ddf', | ||
data: 'bdc160a9ff46151af37ccd6e20', | ||
tag: '1d082c358bc4cda3e8249bb0bb19eb3e', | ||
_id: 'baz' | ||
}; | ||
var cryptoDoc = { | ||
_id: '_local/crypto', | ||
salt: 'f5c011aea21f25b9e975dbacbe38d235' | ||
}; | ||
db1.bulkDocs([docSha256, cryptoDoc]).then(function () { | ||
db1.crypto('password'); | ||
return db1.get('baz'); | ||
}).then(function (doc) { | ||
t.equals(doc.foo, 'bar', 'returns doc for same write / read digest'); | ||
}); | ||
it('should error on attachments', async function () { | ||
const doc = { ...DOCS[0], _attachments: ATTACHMENTS } | ||
try { | ||
await this.db.put(doc) | ||
throw new Error('write should not have succeeded') | ||
} catch (error) { | ||
assert.equal(error.message, 'Attachments cannot be encrypted. Use {ignore: "_attachments"} option') | ||
} | ||
}) | ||
db2.bulkDocs([docSha256, cryptoDoc]).then(function () { | ||
db2.crypto('password', {digest: 'sha512'}); | ||
return db2.get('baz'); | ||
}).then(function () { | ||
t.error('does not throw error'); | ||
}).catch(function (err) { | ||
t.ok(err, 'throws error for different write / read digest'); | ||
}); | ||
}); | ||
test('put with _deleted: true', function (t) { | ||
t.plan(1); | ||
var dbName = 'twelve'; | ||
var db = new PouchDB(dbName, {db: memdown}); | ||
db.crypto('password') | ||
var doc = {_id: 'baz', foo: 'bar'} | ||
db.put(doc).then(function (result) { | ||
doc._rev = result.rev | ||
doc._deleted = true | ||
return db.put(doc) | ||
}).then(function () { | ||
return db.get('baz') | ||
}).then(function () { | ||
t.error('should not find doc after delete'); | ||
}).catch(function (err) { | ||
t.equal(err.status, 404, 'cannot find doc after delete') | ||
it('should ignore attachments when so instructed', async function () { | ||
this.db.removeCrypto() | ||
await this.db.crypto(PASSWORD, { ignore: '_attachments' }) | ||
const doc = { ...DOCS[0], _attachments: ATTACHMENTS } | ||
await this.db.put(doc) | ||
}) | ||
}) | ||
test('pass key in explicitly', function (t) { | ||
t.plan(1); | ||
var db = new PouchDB('thirteen', {db: memdown}); | ||
var ourDoc = { | ||
nonce: '000000000000000000000000', | ||
data: 'e42581d13a730258fadbe55e0e', | ||
tag: 'b52f7f0f3e2926d7ee43f867d2c597e2', | ||
_id: 'baz' | ||
}; | ||
db.bulkDocs([ourDoc]).then(function () { | ||
db.crypto({key: new Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex')}); | ||
return db.get('baz'); | ||
}).then(function (doc) { | ||
t.equals(doc.foo, 'bar', 'returns doc for same write / read digest'); | ||
}).catch(function (e) { | ||
t.error(e); | ||
}); | ||
}); | ||
test('pass key in explicitly as arr buff', function (t) { | ||
t.plan(1); | ||
var db = new PouchDB('tweentyteen', {db: memdown}); | ||
it('should handle _deleted:true ok', async function () { | ||
const doc = DOCS[0] | ||
const { rev } = await this.db.put(doc) | ||
const deleted = { _id: doc._id, _rev: rev, _deleted: true } | ||
await this.db.put(deleted) | ||
try { | ||
await this.db.get(doc._id) | ||
throw new Error('read should not have succeeded') | ||
} catch (error) { | ||
assert.equal(error.reason, 'deleted') | ||
} | ||
}) | ||
var ourDoc = { | ||
nonce: '000000000000000000000000', | ||
data: 'e42581d13a730258fadbe55e0e', | ||
tag: 'b52f7f0f3e2926d7ee43f867d2c597e2', | ||
_id: 'baz' | ||
}; | ||
db.bulkDocs([ourDoc]).then(function () { | ||
db.crypto({key: new Uint8Array(new Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex'))}); | ||
return db.get('baz'); | ||
}).then(function (doc) { | ||
t.equals(doc.foo, 'bar', 'returns doc for same write / read digest'); | ||
}).catch(function (e) { | ||
t.error(e); | ||
}); | ||
}); | ||
test('wrong password throws error', function (t) { | ||
t.plan(1); | ||
var db = new PouchDB('thirteen', {db: memdown}); | ||
it('should accept crypto params as an object', async function () { | ||
this.db.removeCrypto() | ||
await this.db.crypto({ password: PASSWORD }) | ||
const doc = DOCS[0] | ||
await this.db.put(doc) | ||
const { hello } = await this.db.get(doc._id) | ||
assert.equal(hello, 'world') | ||
}) | ||
var ourDoc = { | ||
nonce: '000000000000000000000000', | ||
data: 'e42581d13a730258fadbe55e0e', | ||
tag: 'b52f7f0f3e2926d7ee43f867d2c597e2', | ||
_id: 'baz' | ||
}; | ||
db.bulkDocs([ourDoc]).then(function () { | ||
db.crypto('broken'); | ||
return db.get('baz'); | ||
}).then(function (doc) { | ||
t.notEqual(doc.foo, 'bar', 'returns doc for same write / read digest'); | ||
}).catch(function (e) { | ||
t.ok(e); | ||
}); | ||
}); | ||
test('plain options object', function (t) { | ||
t.plan(4); | ||
var dbName = 'fourteen'; | ||
var db = new PouchDB(dbName, {db: memdown}); | ||
db.crypto({password: 'password'}); | ||
db.put({_id: 'baz', foo: 'bar'}).then(function () { | ||
return db.get('baz'); | ||
}).then(function (resp) { | ||
t.equals(resp.foo, 'bar', 'decrypts data'); | ||
db.removeCrypto(); | ||
return db.get('baz'); | ||
}).then(function (doc) { | ||
t.ok(doc.nonce, 'has nonce'); | ||
t.ok(doc.tag, 'has tag'); | ||
t.ok(doc.data, 'has data'); | ||
}).catch(function (e) { | ||
t.error(e); | ||
}); | ||
}); | ||
test('get key explicitly', function (t) { | ||
t.plan(3); | ||
var db = new PouchDB('fifteen', {db: memdown}); | ||
var key; | ||
var docs = [ | ||
{ _id: '_local/crypto', | ||
salt: '0dac47a196e46680a359c9c18da0bc83', | ||
digest: 'sha256', | ||
iterations: 100000} | ||
]; | ||
db.bulkDocs(docs).then(function () { | ||
db.crypto('password', { | ||
cb: function (err, resp) { | ||
t.error(err); | ||
key = resp.toString('base64'); | ||
t.equals(key, 'jr9j3Krslfck3UkxjiCNYI4hoKQWesoquw11yypC528='); | ||
} | ||
}); | ||
return db.put({ | ||
_id: 'baz', | ||
foo: 'bar' | ||
}); | ||
}).then(function () { | ||
db.removeCrypto(); | ||
db.crypto({key: new Buffer(key, 'base64')}); | ||
return db.get('baz'); | ||
it('should fail to init with http adapter', async function () { | ||
const db = new PouchDB('http://localhost:5984') | ||
assert.rejects( | ||
async () => { await db.crypto(PASSWORD) }, | ||
new Error('crypto-pouch does not work with pouchdb\'s http adapter. Use a local adapter instead.') | ||
) | ||
}) | ||
.then(function (doc) { | ||
t.equals(doc.foo, 'bar', 'decrypts data'); | ||
}).catch(function (e) { | ||
t.error(e); | ||
}); | ||
}); | ||
describe('concurrency', function () { | ||
beforeEach(async function () { | ||
this.db1 = new PouchDB(NAME) | ||
this.db2 = new PouchDB(NAME) | ||
}) | ||
afterEach(async function () { | ||
await this.db1.destroy() // also destroys db2 THANKS | ||
}) | ||
it('should handle concurrent crypt instances ok', async function () { | ||
this.timeout(10 * 1000) | ||
await Promise.all([ | ||
this.db1.crypto(PASSWORD), | ||
this.db2.crypto(PASSWORD) | ||
]) | ||
await this.db1.put(DOCS[0]) | ||
const doc = await this.db2.get(DOCS[0]._id) | ||
assert.equal(DOCS[0].hello, doc.hello) | ||
}) | ||
}) | ||
}) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
2
0
0
14353
8
6
232
95
+ Addedgarbados-crypt@^3.0.0-beta
+ Addedgarbados-crypt@3.0.0-beta(transitive)
+ Addedhash-wasm@4.12.0(transitive)
+ Addedtweetnacl@1.0.3(transitive)
+ Addedtweetnacl-util@0.15.1(transitive)
- Removedchacha@^2.1.0
- Removednative-crypto@^1.5.2
- Removedpouchdb-promise@^6.1.0
- Removedrandombytes@^2.0.3
- Removeduuid@^3.0.1
- Removedasn1.js@4.10.1(transitive)
- Removedbindings@1.5.0(transitive)
- Removedbn.js@4.12.15.2.1(transitive)
- Removedbrorand@1.1.0(transitive)
- Removedbrowserify-aes@1.2.0(transitive)
- Removedbrowserify-rsa@4.1.1(transitive)
- Removedbrowserify-sign@4.2.3(transitive)
- Removedbuffer-equal-constant-time@1.0.1(transitive)
- Removedbuffer-xor@1.0.3(transitive)
- Removedchacha@2.1.0(transitive)
- Removedchacha-native@2.0.3(transitive)
- Removedcipher-base@1.0.6(transitive)
- Removedcreate-ecdh@4.0.4(transitive)
- Removedcreate-hash@1.2.0(transitive)
- Removedcreate-hmac@1.1.7(transitive)
- Removeddebug@2.6.9(transitive)
- Removedecdsa-sig-formatter@1.0.11(transitive)
- Removedelliptic@6.6.1(transitive)
- Removedevp_bytestokey@1.0.3(transitive)
- Removedfile-uri-to-path@1.0.0(transitive)
- Removedhash-base@3.0.5(transitive)
- Removedhash.js@1.1.7(transitive)
- Removedhmac-drbg@1.0.1(transitive)
- Removedinherits@2.0.4(transitive)
- Removedisarray@1.0.0(transitive)
- Removedjwk-to-pem@1.2.6(transitive)
- Removedlie@3.1.1(transitive)
- Removedmd5.js@1.3.5(transitive)
- Removedmiller-rabin@4.0.1(transitive)
- Removedminimalistic-assert@1.0.1(transitive)
- Removedminimalistic-crypto-utils@1.0.1(transitive)
- Removedms@2.0.0(transitive)
- Removednan@2.22.02.3.5(transitive)
- Removednative-crypto@1.8.1(transitive)
- Removedparse-asn1@5.1.7(transitive)
- Removedpbkdf2@3.1.2(transitive)
- Removedpemstrip@0.0.1(transitive)
- Removedpouchdb-promise@6.4.3(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedpublic-encrypt@4.0.3(transitive)
- Removedrandombytes@2.1.0(transitive)
- Removedraw-ecdsa@1.1.1(transitive)
- Removedreadable-stream@1.1.142.3.8(transitive)
- Removedripemd160@2.0.2(transitive)
- Removedrsa-keygen@1.0.6(transitive)
- Removedsafe-buffer@5.1.25.2.1(transitive)
- Removedsha.js@2.4.11(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedutil-deprecate@1.0.2(transitive)
- Removeduuid@3.4.0(transitive)
Updatedtransform-pouch@^1.1.5