Comparing version 2.0.0-beta to 3.0.0-beta
10
index.js
@@ -131,10 +131,10 @@ 'use strict' | ||
assert(this._crypt, 'Must set a password with `.setPassword(password)` before encrypting documents.') | ||
return this._crypt.encrypt(doc) | ||
return this._crypt.encrypt(JSON.stringify(doc)) | ||
} | ||
// decryption convenience function; takes the output of .encrypt | ||
PouchDB.prototype.decrypt = function (payload) { | ||
PouchDB.prototype.decrypt = async function (payload) { | ||
assert(this._crypt, 'Must set a password with `.setPassword(password)` before decrypting documents.') | ||
return this._crypt.decrypt(payload).then((plaintext) => { | ||
return JSON.parse(plaintext) | ||
}) | ||
const plaintext = await this._crypt.decrypt(payload) | ||
const doc = JSON.parse(plaintext) | ||
return doc | ||
} | ||
@@ -141,0 +141,0 @@ // destroy wrapper that destroys both the encrypted and decrypted DBs |
'use strict' | ||
const assert = require('assert') | ||
const crypto = require('crypto') | ||
const ALGORITHM_NAME = 'aes-256-gcm' | ||
const ALGORITHM_NONCE_SIZE = 12 | ||
const ALGORITHM_TAG_SIZE = 16 | ||
const ALGORITHM_KEY_SIZE = 32 | ||
const PBKDF2_NAME = 'sha512' | ||
const PBKDF2_SALT_SIZE = 16 | ||
const PBKDF2_ITERATIONS = 10000 | ||
const { secretbox, hash, randomBytes } = require('tweetnacl') | ||
const { decodeUTF8, encodeUTF8, encodeBase64, decodeBase64 } = require('tweetnacl-util') | ||
module.exports = class Crypt { | ||
constructor (password, options = {}) { | ||
constructor (password) { | ||
assert(password, 'A password is required for encryption or decryption.') | ||
this.password = password | ||
this.algorithmName = options.algorithmName || ALGORITHM_NAME | ||
this.algorithmNonceSize = options.algorithmNonceSize || ALGORITHM_NONCE_SIZE | ||
this.algorithmTagSize = options.algorithmTagSize || ALGORITHM_TAG_SIZE | ||
this.algorithmKeySize = options.algorithmKeySize || ALGORITHM_KEY_SIZE | ||
this.pbkdf2Name = options.pbkdf2Name || PBKDF2_NAME | ||
this.pbkdf2SaltSize = options.pbkdf2SaltSize || PBKDF2_SALT_SIZE | ||
this.pbkdf2Iterations = options.pbkdf2Iterations || PBKDF2_ITERATIONS | ||
this._key = hash(decodeUTF8(this.password)).slice(0, secretbox.keyLength) | ||
} | ||
encrypt (plaintext) { | ||
if (typeof plaintext !== 'string') return this.encrypt(JSON.stringify(plaintext)) | ||
const salt = crypto.randomBytes(this.pbkdf2SaltSize) | ||
return this._pbkdf2(salt).then((key) => { | ||
const ciphertextAndNonceAndSalt = Buffer.concat([ | ||
salt, | ||
this._encryptWithKey(Buffer.from(plaintext, 'utf8'), key) | ||
]) | ||
return ciphertextAndNonceAndSalt.toString('base64') | ||
}) | ||
async encrypt (plaintext) { | ||
const nonce = randomBytes(secretbox.nonceLength) | ||
const messageUint8 = decodeUTF8(plaintext) | ||
const box = secretbox(messageUint8, nonce, this._key) | ||
const fullMessage = new Uint8Array(nonce.length + box.length) | ||
fullMessage.set(nonce) | ||
fullMessage.set(box, nonce.length) | ||
const base64FullMessage = encodeBase64(fullMessage) | ||
return base64FullMessage | ||
} | ||
decrypt (base64CiphertextAndNonceAndSalt) { | ||
const ciphertextAndNonceAndSalt = Buffer.from(base64CiphertextAndNonceAndSalt, 'base64') | ||
const salt = ciphertextAndNonceAndSalt.slice(0, this.pbkdf2SaltSize) | ||
const ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(this.pbkdf2SaltSize) | ||
return this._pbkdf2(salt).then((key) => { | ||
return this._decryptWithKey(ciphertextAndNonce, key).toString('utf8') | ||
}) | ||
async decrypt (messageWithNonce) { | ||
const messageWithNonceAsUint8Array = decodeBase64(messageWithNonce) | ||
const nonce = messageWithNonceAsUint8Array.slice(0, secretbox.nonceLength) | ||
const message = messageWithNonceAsUint8Array.slice( | ||
secretbox.nonceLength, | ||
messageWithNonce.length | ||
) | ||
const decrypted = secretbox.open(message, nonce, this._key) | ||
if (!decrypted) { | ||
throw new Error('Could not decrypt!') | ||
} else { | ||
return encodeUTF8(decrypted) | ||
} | ||
} | ||
_pbkdf2 (salt) { | ||
return new Promise((resolve, reject) => { | ||
crypto.pbkdf2( | ||
Buffer.from(this.password, 'utf8'), | ||
salt, | ||
this.pbkdf2Iterations, | ||
this.algorithmKeySize, | ||
this.pbkdf2Name, | ||
(err, key) => { | ||
if (err) return reject(err) | ||
return resolve(key) | ||
}) | ||
}) | ||
} | ||
_encryptWithKey (plaintext, key) { | ||
const nonce = crypto.randomBytes(this.algorithmNonceSize) | ||
const cipher = crypto.createCipheriv(this.algorithmName, key, nonce) | ||
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]) | ||
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]) | ||
} | ||
_decryptWithKey (ciphertextAndNonce, key) { | ||
const nonce = ciphertextAndNonce.slice(0, this.algorithmNonceSize) | ||
const ciphertext = ciphertextAndNonce.slice( | ||
this.algorithmNonceSize, | ||
ciphertextAndNonce.length - this.algorithmTagSize) | ||
const tag = ciphertextAndNonce.slice(ciphertext.length + this.algorithmNonceSize) | ||
const cipher = crypto.createDecipheriv(this.algorithmName, key, nonce) | ||
cipher.setAuthTag(tag) | ||
return Buffer.concat([cipher.update(ciphertext), cipher.final()]) | ||
} | ||
} |
{ | ||
"name": "comdb", | ||
"version": "2.0.0-beta", | ||
"version": "3.0.0-beta", | ||
"description": "A PouchDB plugin that transparently encrypts and decrypts its data.", | ||
@@ -28,3 +28,5 @@ "main": "index.js", | ||
"lodash.isequal": "^4.5.0", | ||
"pouchdb": "^7.2.2" | ||
"pouchdb": "^7.2.2", | ||
"tweetnacl": "^1.0.3", | ||
"tweetnacl-util": "^0.15.1" | ||
}, | ||
@@ -31,0 +33,0 @@ "devDependencies": { |
@@ -9,3 +9,3 @@ # ComDB | ||
A [PouchDB](https://pouchdb.com/) 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. | ||
A [PouchDB](https://pouchdb.com/) 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](https://www.npmjs.com/package/tweetnacl) for cryptography. | ||
@@ -138,10 +138,2 @@ As an example, here's what happens when you replicate data to a [CouchDB](https://couchdb.apache.org/) cluster: | ||
- `opts.opts`: An options object passed to the encrypted database's constructor. Use this to pass any options accepted by [PouchDB's constructor](https://pouchdb.com/api.html#create_database). | ||
- `opts.crypt`: Options for ComDB's crypto tooling. | ||
- `opts.crypt.algorithmName`: Name of the encryption algorithm. | ||
- `opts.crypt.algorithmNonceSize`: Size of generated nonces. | ||
- `opts.crypt.algorithmTagSize`: Size of the auth tags used. | ||
- `opts.crypt.algorithmKeySize`: Size of generated keys. | ||
- `opts.crypt.pbkdf2Name`: Name of the hashing algorithm. | ||
- `opts.crypt.pbkdf2SaltSize`: Size of generates salts. | ||
- `opts.crypt.pbkdf2Iterations`: Number of iterations to hash data. | ||
@@ -148,0 +140,0 @@ ### `db.bulkDocs(docs, [opts], [callback])` |
@@ -7,13 +7,37 @@ /* global describe, it */ | ||
const PLAINTEXT = 'hello world' | ||
const PASSWORD = 'password' | ||
const TEST_LENGTH = 1e4 // note: 1e4 = 1 and 4 zeroes (10,000) | ||
describe('crypt', function () { | ||
it('should do the crypto dance', function () { | ||
const plaintext = 'hello world' | ||
const password = 'password' | ||
const crypt = new Crypt(password) | ||
return crypt.encrypt(plaintext).then((ciphertext) => { | ||
return crypt.decrypt(ciphertext) | ||
}).then((newtext) => { | ||
assert.strictEqual(newtext, plaintext) | ||
}) | ||
it('should do the crypto dance', async function () { | ||
const crypt = new Crypt(PASSWORD) | ||
const ciphertext = await crypt.encrypt(PLAINTEXT) | ||
const decryptext = await crypt.decrypt(ciphertext) | ||
assert.strictEqual(decryptext, PLAINTEXT) | ||
}) | ||
it('should fail to decrypt ok', async function () { | ||
const crypt = new Crypt(PASSWORD) | ||
const crypt2 = new Crypt(PASSWORD + 'a') | ||
const ciphertext = await crypt.encrypt(PLAINTEXT) | ||
let failed = false | ||
try { | ||
await crypt2.decrypt(ciphertext) | ||
} catch (e) { | ||
assert.equal(e.message, 'Could not decrypt!') | ||
failed = true | ||
} | ||
assert(failed) | ||
}) | ||
it(`should do the crypto dance ${TEST_LENGTH} times`, async function () { | ||
this.timeout(TEST_LENGTH) // assume each op will take no more than 1ms | ||
const crypt = new Crypt(PASSWORD) | ||
for (let i = 0; i < TEST_LENGTH; i++) { | ||
const ciphertext = await crypt.encrypt(PLAINTEXT) | ||
const decryptext = await crypt.decrypt(ciphertext) | ||
assert.strictEqual(decryptext, PLAINTEXT) | ||
} | ||
}) | ||
}) |
@@ -75,7 +75,7 @@ /* global describe, it, before, after */ | ||
// 1. write to encrypted db | ||
const payload = await this.crypt.encrypt({ | ||
const payload = await this.crypt.encrypt(JSON.stringify({ | ||
_id: 'hello', | ||
_rev: '1-15f65339921e497348be384867bb940f', | ||
hello: 'world' | ||
}) | ||
})) | ||
await this.dbs.encrypted.post({ payload }) | ||
@@ -82,0 +82,0 @@ // 2. hook up decrypted db to encrypted |
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
27745
4
476
235
+ Addedtweetnacl@^1.0.3
+ Addedtweetnacl-util@^0.15.1
+ Addedtweetnacl@1.0.3(transitive)
+ Addedtweetnacl-util@0.15.1(transitive)