├── .gitignore ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tradle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # level-encrypt 2 | 3 | *Note: bulk of code originates from [modeler-leveldb](https://github.com/carlos8f/modeler-leveldb).* 4 | 5 | Encryption for levelup. Performs necessary hydration/dehydration of JSON objects using [hydration](https://github.com/carlos8f/hydration). 6 | 7 | # Usage 8 | 9 | ```js 10 | var crypto = require('crypto') 11 | var levelup = require('levelup') 12 | var memdown = require('memdown') 13 | var encryption = require('level-encrypt') 14 | 15 | var encryptionOptions = { 16 | // key derivation parameters 17 | saltBytes: 32, 18 | digest: 'sha256', 19 | keyBytes: 32, 20 | // iterations for pbkdf2Sync used to derive the encryption key from the password 21 | iterations: 100000, 22 | // encryption parameters 23 | algorithm:'aes-256-cbc', 24 | ivBytes: 16, 25 | // tip: this password is crap 26 | password: 'oogabooga' 27 | // optionally, pass in key instead of password 28 | // key: myKeyBuffer 29 | } 30 | 31 | // for custom encryption options, encryptionOptions should look like this: 32 | // { 33 | // encrypt: Function, 34 | // decrypt: Function 35 | // } 36 | // 37 | 38 | var dbPath = './encrypted.db' 39 | var baseDB = levelup(dbPath, { 40 | db: memdown 41 | }) 42 | 43 | var db = encryption.toEncrypted(baseDB, encryptionOptions) 44 | var key = 'ho' 45 | var val = { hey: 'ho' } 46 | db.put(key, val, function (err) { 47 | if (err) throw err 48 | 49 | db.get(key, function (err, v) { 50 | if (err) throw err 51 | 52 | console.log('retrieved plaintext: ' + JSON.stringify(v)) // {"hey":ho"} 53 | 54 | // let's see the ciphertext stored: 55 | db.close(function () { 56 | baseDB.get(encryption.keyHashFunction(key), function (err, ciphertext) { 57 | if (err) throw err 58 | 59 | console.log('stored ciphertext (+ salt + iv): ' + ciphertext.toString('base64')) 60 | }) 61 | }) 62 | }) 63 | }) 64 | ``` 65 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto') 2 | var levelup = require('levelup') 3 | var memdown = require('memdown') 4 | var encryption = require('./') 5 | 6 | var encryptionOptions = { 7 | // key derivation parameters 8 | saltBytes: 32, 9 | digest: 'sha256', 10 | keyBytes: 32, 11 | // iterations for pbkdf2Sync used to derive the encryption key from the password 12 | iterations: 100000, 13 | // encryption parameters 14 | algorithm:'aes-256-cbc', 15 | ivBytes: 16, 16 | // tip: this password is crap 17 | password: 'oogabooga' 18 | // optionally, pass in key instead of password 19 | // key: myKeyBuffer 20 | } 21 | 22 | // for custom encryption options, encryptionOptions should look like this: 23 | // { 24 | // encrypt: Function, 25 | // decrypt: Function 26 | // } 27 | // 28 | 29 | var dbPath = './encrypted.db' 30 | var baseDB = levelup(dbPath, { 31 | db: memdown 32 | }) 33 | 34 | var db = encryption.toEncrypted(baseDB, encryptionOptions) 35 | var key = 'ho' 36 | var val = { hey: 'ho' } 37 | db.put(key, val, function (err) { 38 | if (err) throw err 39 | 40 | db.get(key, function (err, v) { 41 | if (err) throw err 42 | 43 | console.log('retrieved plaintext: ' + JSON.stringify(v)) // {"hey":ho"} 44 | 45 | // let's see the ciphertext stored: 46 | db.close(function () { 47 | baseDB.get(encryption.keyHashFunction(key), function (err, ciphertext) { 48 | if (err) throw err 49 | 50 | console.log('stored ciphertext (+ salt + iv): ' + ciphertext.toString('base64')) 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var Buffer = require('buffer').Buffer 3 | var crypto = require('crypto') 4 | var hydration = require('hydration') 5 | var levelup = require('levelup') 6 | var updown = require('level-updown') 7 | 8 | // opts from SQLCipher: https://www.zetetic.net/sqlcipher/design/ 9 | var DEFAULT_PASSWORD_BASED_OPTS = { 10 | // key derivation parameters 11 | password: null, 12 | saltBytes: 32, 13 | salt: null, 14 | digest: 'sha256', 15 | keyBytes: 32, 16 | iterations: 64000, 17 | // encryption parameters 18 | algorithm:'aes-256-cbc', 19 | ivBytes: 16, 20 | } 21 | 22 | var DEFAULT_KEY_BASED_OPTS = { 23 | algorithm:'aes-256-cbc', 24 | ivBytes: 16, 25 | key: null 26 | } 27 | 28 | exports.encrypt = encrypt 29 | exports.decrypt = decrypt 30 | exports.hydration = hydration 31 | exports.dehydrate = dehydrate 32 | exports.hydrate = hydrate 33 | exports.toEncrypted = toEncryptedLevelup 34 | exports.keyHashFunction = sha256 35 | exports._unserialize = unserialize // for testing 36 | 37 | function toEncryptedLevelup (db, opts) { 38 | var kEncoding = db.options.keyEncoding 39 | if (kEncoding !== 'binary') { 40 | throw new Error('expected "binary" keyEncoding') 41 | } 42 | 43 | var vEncoding = db.options.valueEncoding 44 | if (vEncoding !== 'binary' && vEncoding !== 'utf8') { 45 | throw new Error('expected "binary" or "utf8" valueEncoding') 46 | } 47 | 48 | opts = opts || {} 49 | if (!opts.encrypt || !opts.decrypt) { 50 | opts = normalizeOpts(opts) 51 | } 52 | 53 | var encryptValue = opts.encrypt || function (data) { 54 | return encrypt(data, opts) 55 | } 56 | 57 | var decryptValue = opts.decrypt || function (data) { 58 | return decrypt(data, opts) 59 | } 60 | 61 | var rawHashKey = opts.keyHashFunction || exports.keyHashFunction 62 | return levelup({ 63 | keyEncoding: db.options.keyEncoding, 64 | valueEncoding: { 65 | encode: dehydrate, 66 | decode: function identity (val) { 67 | return val 68 | } 69 | }, 70 | db: function () { 71 | var ud = updown(db) 72 | ud.extendWith({ 73 | preGet: preHashKey, 74 | postGet: postGet, 75 | postIterator: postIterator, 76 | preDel: preHashKey, 77 | prePut: prePut, 78 | preBatch: preBatch 79 | }) 80 | 81 | return ud 82 | } 83 | }) 84 | 85 | function hashKey (key) { 86 | var hash = rawHashKey(key) 87 | if (!Buffer.isBuffer(hash)) hash = new Buffer(hash) 88 | 89 | return hash 90 | } 91 | 92 | function postIterator (iterator) { 93 | iterator.extendWith({ 94 | postNext: postNext 95 | }) 96 | 97 | return iterator 98 | } 99 | 100 | function postNext (err, key, value, callback, next) { 101 | if (!err && value) { 102 | try { 103 | value = hydrate(decryptValue(value)) 104 | } catch (e) { 105 | err = e 106 | } 107 | } 108 | 109 | next(err, key, value, callback) 110 | } 111 | 112 | function preHashKey(key, options, callback, next) { 113 | key = hashKey(key) 114 | next(key, options, callback) 115 | } 116 | 117 | function postGet (key, options, err, value, callback, next) { 118 | if (!err) { 119 | try { 120 | value = hydrate(decryptValue(value)) 121 | } catch (e) { 122 | err = e 123 | } 124 | } 125 | 126 | next(key, options, err, value, callback) 127 | } 128 | 129 | function prePut (key, value, options, callback, next) { 130 | key = hashKey(key) 131 | value = encryptValue(value) 132 | next(key, value, options, callback) 133 | } 134 | 135 | function preBatch (array, options, callback, next) { 136 | for (var i = 0; i < array.length; i++) { 137 | var row = array[i] 138 | row.key = hashKey(row.key) 139 | if (row.type == 'put') { 140 | row.value = encryptValue(row.value) 141 | } 142 | } 143 | 144 | next(array, options, callback) 145 | } 146 | } 147 | 148 | function encrypt (data, opts) { 149 | var salt = !opts.key && (opts.salt || crypto.randomBytes(opts.saltBytes)) 150 | var key = opts.key || crypto.pbkdf2Sync(opts.password, salt, opts.iterations, opts.keyBytes, opts.digest) 151 | var iv = opts.iv || crypto.randomBytes(opts.ivBytes) 152 | var cipher = crypto.createCipheriv(opts.algorithm, key, iv) 153 | var ciphertext = Buffer.concat([cipher.update(data), cipher.final()]) 154 | var parts = [ 155 | iv, 156 | ciphertext 157 | ] 158 | 159 | if (salt) parts.push(salt) 160 | 161 | return serialize(parts) 162 | } 163 | 164 | function decrypt (data, opts) { 165 | var parts = unserialize(data) 166 | var iv = parts[0] 167 | var ciphertext = parts[1] 168 | var salt = parts[2] 169 | var key = opts.key 170 | if (!key) { 171 | key = crypto.pbkdf2Sync(opts.password, salt, opts.iterations, opts.keyBytes, opts.digest) 172 | } 173 | 174 | var decipher = crypto.createDecipheriv(opts.algorithm, key, iv) 175 | var m = decipher.update(parts[1]) 176 | data = Buffer.concat([m, decipher.final()]).toString() 177 | return JSON.parse(data) 178 | } 179 | 180 | function hydrate (entity) { 181 | return hydration.hydrate(entity) 182 | } 183 | 184 | function dehydrate (entity) { 185 | // if (Buffer.isBuffer(entity)) return entity 186 | var data = hydration.dehydrate(entity) 187 | return new Buffer(JSON.stringify(data)) 188 | } 189 | 190 | function serialize (buffers) { 191 | var parts = [], idx = 0 192 | buffers.forEach(function (part) { 193 | var len = Buffer(4) 194 | if (typeof part === 'string') part = Buffer(part) 195 | len.writeUInt32BE(part.length, 0) 196 | parts.push(len) 197 | idx += len.length 198 | parts.push(part) 199 | idx += part.length 200 | }) 201 | 202 | return Buffer.concat(parts) 203 | } 204 | 205 | function unserialize (buf) { 206 | var parts = [] 207 | var l = buf.length, idx = 0 208 | while (idx < l) { 209 | var dlen = buf.readUInt32BE(idx) 210 | idx += 4 211 | var start = idx 212 | var end = start + dlen 213 | var part = buf.slice(start, end) 214 | parts.push(part) 215 | idx += part.length 216 | } 217 | 218 | return parts 219 | } 220 | 221 | function assert (statement, errMsg) { 222 | if (!statement) throw new Error(errMsg || 'Assertion failed') 223 | } 224 | 225 | function sha256 (key) { 226 | return crypto.createHash('sha256').update(key).digest() 227 | } 228 | 229 | function normalizeOpts (_opts) { 230 | var opts = {} 231 | var defaults = _opts.key ? DEFAULT_KEY_BASED_OPTS : DEFAULT_PASSWORD_BASED_OPTS 232 | for (var p in defaults) { 233 | opts[p] = p in _opts ? _opts[p] : defaults[p] 234 | } 235 | 236 | assert(typeof opts.algorithm === 'string', 'Expected string "algorithm"') 237 | assert(typeof opts.ivBytes === 'number', 'Expected number "ivBytes"') 238 | 239 | if (!opts.key) { 240 | assert(typeof opts.keyBytes === 'number', 'Expected number "keyBytes"') 241 | assert(typeof opts.iterations === 'number', 'Expected number "iterations"') 242 | assert(typeof opts.password === 'string' || Buffer.isBuffer(opts.password), 'Expected string or Buffer "password"') 243 | assert(typeof opts.digest === 'string', 'Expected string "digest"') 244 | 245 | if (opts.salt) { 246 | assert(Buffer.isBuffer(opts.salt), 'Expected Buffer "salt"') 247 | // if global salt is provided don't recalculate key every time 248 | if (!opts.key) { 249 | opts.key = crypto.pbkdf2Sync(opts.password, opts.salt, opts.iterations, opts.keyBytes, opts.digest) 250 | } 251 | } else { 252 | assert(typeof opts.saltBytes === 'number', 'Expected number "saltBytes"') 253 | } 254 | } 255 | 256 | return opts 257 | } 258 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "level-encrypt", 3 | "version": "3.2.1", 4 | "description": "encryption for levelup", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test" 8 | }, 9 | "author": "Mark Vayngrib (http://github.com/mvayngrib)", 10 | "license": "MIT", 11 | "dependencies": { 12 | "buffer": "^4.9.1", 13 | "hydration": "^1.0.0", 14 | "level-updown": "^2.0.2" 15 | }, 16 | "peerDependencies": { 17 | "levelup": ">= 0.18" 18 | }, 19 | "devDependencies": { 20 | "levelup": "1.3.8", 21 | "memdown": "^1.1.2", 22 | "run-series": "^1.1.4", 23 | "tape": "^4.5.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 2 | var crypto = require('crypto') 3 | var test = require('tape') 4 | var levelup = require('levelup') 5 | var memdown = require('memdown') 6 | var updown = require('level-updown') 7 | var series = require('run-series') 8 | var encryption = require('./') 9 | var DB_COUNTER = 0 10 | 11 | test('encrypt/decrypt', function (t) { 12 | var passwordBased = { 13 | keyBytes: 32, 14 | saltBytes: 32, 15 | ivBytes: 16, 16 | digest: 'sha256', 17 | algorithm: 'aes-256-cbc', 18 | iterations: 10000, 19 | password: 'ooga' 20 | } 21 | 22 | var keyBased = { 23 | key: crypto.randomBytes(32) 24 | } 25 | 26 | var encryptors = [ 27 | passwordBased, 28 | keyBased 29 | ] 30 | 31 | series(encryptors.map(function (encryptionOpts) { 32 | return function (cb) { 33 | var db = newDB() 34 | var encrypted = encryption.toEncrypted(db, encryptionOpts) 35 | var key = 'ho' 36 | var val = { hey: 'ho' } 37 | encrypted.put(key, val, function (err) { 38 | if (err) throw err 39 | 40 | encrypted.get(key, function (err, v) { 41 | if (err) throw err 42 | 43 | t.same(v, val) 44 | db.get(sha256(key), function (err, ciphertext) { 45 | if (err) throw err 46 | 47 | t.ok(ciphertext.length > 16 + 32) // at least bigger than iv + salt 48 | t.notSame(ciphertext, val) 49 | cb() 50 | }) 51 | }) 52 | }) 53 | } 54 | }), function (err) { 55 | if (err) throw err 56 | 57 | t.end() 58 | }) 59 | }) 60 | 61 | test('open / close', function (t) { 62 | var dbPath = 'blah.db' + (DB_COUNTER++) 63 | var db = makeDB() 64 | 65 | var key = 'ho' 66 | var val = { hey: 'ho' } 67 | db.put(key, val, function (err) { 68 | if (err) throw err 69 | 70 | db.close(function () { 71 | db = makeDB() 72 | db.get(key, function (err, val1) { 73 | if (err) throw err 74 | 75 | t.same(val, val1) 76 | t.end() 77 | }) 78 | }) 79 | }) 80 | 81 | function makeDB () { 82 | return encryption.toEncrypted(levelup(dbPath, { 83 | db: memdown, 84 | keyEncoding: 'binary' 85 | }), { 86 | password: 'ooga' 87 | }) 88 | } 89 | }) 90 | 91 | test('global vs per-item salt', function (t) { 92 | t.plan(6) 93 | 94 | var globalSalts = [ 95 | null, 96 | crypto.randomBytes(32) 97 | ] 98 | 99 | globalSalts.forEach(function (globalSalt) { 100 | var dbPath = 'blah' + (DB_COUNTER++) 101 | var rawDB = levelup(dbPath, { 102 | keyEncoding: 'binary', 103 | db: memdown 104 | }) 105 | 106 | var db = encryption.toEncrypted(rawDB, { 107 | salt: globalSalt, 108 | password: 'poop' 109 | }) 110 | 111 | db.put('hey', 'ho', function (err) { 112 | if (err) throw err 113 | 114 | db.close(function () { 115 | var rawDB = levelup(dbPath, { 116 | db: memdown 117 | }) 118 | 119 | rawDB.get('hey', function (err, val1) { 120 | t.ok(err) 121 | }) 122 | 123 | rawDB.get(sha256('hey'), function (err, val1) { 124 | t.error(err) 125 | var unserialized = encryption._unserialize(val1) 126 | t.equal(unserialized.length, globalSalt ? 2 : 3) 127 | }) 128 | }) 129 | }) 130 | }) 131 | }) 132 | 133 | test('basic', function (t) { 134 | var db = newDB() 135 | var encrypted = encryption.toEncrypted(db, { 136 | key: crypto.randomBytes(32) 137 | }) 138 | 139 | var hashedKey 140 | var encryptedVal 141 | db.on('put', function (key, val) { 142 | hashedKey = key 143 | encryptedVal = val 144 | }) 145 | 146 | encrypted.put('hey', 'ho', function (err) { 147 | if (err) throw err 148 | 149 | encrypted.get('hey', function (err, val) { 150 | if (err) throw err 151 | 152 | t.equals(val, 'ho') 153 | db.get('hey', function (err, val) { 154 | t.ok(err) 155 | db.get(hashedKey, function (err, val) { 156 | if (err) throw err 157 | 158 | t.same(val, encryptedVal) 159 | t.end() 160 | }) 161 | }) 162 | }) 163 | }) 164 | }) 165 | 166 | test('stream', function (t) { 167 | var db = newDB() 168 | var encrypted = encryption.toEncrypted(db, { 169 | key: crypto.randomBytes(32) 170 | }) 171 | 172 | encrypted.put('hey', 'ho', function (err) { 173 | if (err) throw err 174 | 175 | var data = [] 176 | encrypted.createValueStream() 177 | .on('data', data.push.bind(data)) 178 | .on('end', function () { 179 | t.same(data, ['ho']) 180 | t.end() 181 | }) 182 | }) 183 | }) 184 | 185 | function newDB (opts) { 186 | opts = opts || { 187 | keyEncoding: 'binary', 188 | valueEncoding: 'binary' 189 | } 190 | 191 | opts.db = opts.db || memdown 192 | return levelup('blah' + (DB_COUNTER++), opts) 193 | } 194 | 195 | function sha256 (key) { 196 | return crypto.createHash('sha256').update(key).digest() 197 | } 198 | --------------------------------------------------------------------------------