1 | /**
|
2 | * @file lokiCryptedFileAdapter.js
|
3 | * @author Hans Klunder <Hans.Klunder@bigfoot.com>
|
4 | */
|
5 |
|
6 | /*
|
7 | * The default Loki File adapter uses plain text JSON files. This adapter crypts the database string and wraps the result
|
8 | * in a JSON including enough info to be able to decrypt it (except for the 'secret' of course !)
|
9 | *
|
10 | * The idea is that the 'secret' does not reside in your source code but is supplied by some other source (e.g. the user in node-webkit)
|
11 | *
|
12 | * The idea + encrypt/decrypt routines are borrowed from https://github.com/mmoulton/krypt/blob/develop/lib/krypt.js
|
13 | * not using the krypt module to avoid third party dependencies
|
14 | */
|
15 |
|
16 |
|
17 | /**
|
18 | * require libs
|
19 | * @ignore
|
20 | */
|
21 | var fs = require('fs');
|
22 | var cryptoLib = require('crypto');
|
23 | var isError = require('util').isError;
|
24 |
|
25 | /*
|
26 | * sensible defaults
|
27 | */
|
28 | var CIPHER = 'aes-256-cbc',
|
29 | KEY_DERIVATION = 'pbkdf2',
|
30 | KEY_LENGTH = 256,
|
31 | ITERATIONS = 64000;
|
32 |
|
33 | /**
|
34 | * encrypt() - encrypt a string
|
35 | * @private
|
36 | * @param {string} input - the serialized JSON object to decrypt.
|
37 | * @param {string} secret - the secret to use for encryption
|
38 | */
|
39 | function encrypt(input, secret) {
|
40 | if (!secret) {
|
41 | return new Error('A \'secret\' is required to encrypt');
|
42 | }
|
43 |
|
44 |
|
45 | var salt = cryptoLib.randomBytes(KEY_LENGTH / 8),
|
46 | iv = cryptoLib.randomBytes(16);
|
47 |
|
48 | try {
|
49 |
|
50 | var key = cryptoLib.pbkdf2Sync(secret, salt, ITERATIONS, KEY_LENGTH / 8, 'sha1'),
|
51 | cipher = cryptoLib.createCipheriv(CIPHER, key, iv);
|
52 |
|
53 | var encryptedValue = cipher.update(input, 'utf8', 'base64');
|
54 | encryptedValue += cipher.final('base64');
|
55 |
|
56 | var result = {
|
57 | cipher: CIPHER,
|
58 | keyDerivation: KEY_DERIVATION,
|
59 | keyLength: KEY_LENGTH,
|
60 | iterations: ITERATIONS,
|
61 | iv: iv.toString('base64'),
|
62 | salt: salt.toString('base64'),
|
63 | value: encryptedValue
|
64 | };
|
65 | return result;
|
66 |
|
67 | } catch (err) {
|
68 | return new Error('Unable to encrypt value due to: ' + err);
|
69 | }
|
70 | }
|
71 |
|
72 | /**
|
73 | * decrypt() - Decrypt a serialized JSON object
|
74 | * @private
|
75 | * @param {string} input - the serialized JSON object to decrypt.
|
76 | * @param {string} secret - the secret to use for decryption
|
77 | */
|
78 | function decrypt(input, secret) {
|
79 | // Ensure we have something to decrypt
|
80 | if (!input) {
|
81 | return new Error('You must provide a value to decrypt');
|
82 | }
|
83 | // Ensure we have the secret used to encrypt this value
|
84 | if (!secret) {
|
85 | return new Error('A \'secret\' is required to decrypt');
|
86 | }
|
87 |
|
88 | // turn string into an object
|
89 | try {
|
90 | input = JSON.parse(input);
|
91 | } catch (err) {
|
92 | return new Error('Unable to parse string input as JSON');
|
93 | }
|
94 |
|
95 | // Ensure our input is a valid object with 'iv', 'salt', and 'value'
|
96 | if (!input.iv || !input.salt || !input.value) {
|
97 | return new Error('Input must be a valid object with \'iv\', \'salt\', and \'value\' properties');
|
98 | }
|
99 |
|
100 | var salt = new Buffer(input.salt, 'base64'),
|
101 | iv = new Buffer(input.iv, 'base64'),
|
102 | keyLength = input.keyLength,
|
103 | iterations = input.iterations;
|
104 |
|
105 | try {
|
106 |
|
107 | var key = cryptoLib.pbkdf2Sync(secret, salt, iterations, keyLength / 8, 'sha1'),
|
108 | decipher = cryptoLib.createDecipheriv(CIPHER, key, iv);
|
109 |
|
110 | var decryptedValue = decipher.update(input.value, 'base64', 'utf8');
|
111 | decryptedValue += decipher.final('utf8');
|
112 |
|
113 | return decryptedValue;
|
114 |
|
115 | } catch (err) {
|
116 | return new Error('Unable to decrypt value due to: ' + err);
|
117 | }
|
118 | }
|
119 |
|
120 | /**
|
121 | * The constructor is automatically called on `require` , see examples below
|
122 | * @constructor
|
123 | */
|
124 | function lokiCryptedFileAdapter() {}
|
125 |
|
126 | /**
|
127 | * setSecret() - set the secret to be used during encryption and decryption
|
128 | *
|
129 | * @param {string} secret - the secret to be used
|
130 | */
|
131 | lokiCryptedFileAdapter.prototype.setSecret = function setSecret(secret) {
|
132 | this.secret = secret;
|
133 | };
|
134 |
|
135 | /**
|
136 | * loadDatabase() - Retrieves a serialized db string from the catalog.
|
137 | *
|
138 | * @example
|
139 | // LOAD
|
140 | var cryptedFileAdapter = require('./lokiCryptedFileAdapter');
|
141 | cryptedFileAdapter.setSecret('mySecret'); // you should change 'mySecret' to something supplied by the user
|
142 | var db = new loki('test.crypted', { adapter: cryptedFileAdapter }); //you can use any name, not just '*.crypted'
|
143 | db.loadDatabase(function(result) {
|
144 | console.log('done');
|
145 | });
|
146 | *
|
147 | * @param {string} dbname - the name of the database to retrieve.
|
148 | * @param {function} callback - callback should accept string param containing serialized db string.
|
149 | */
|
150 | lokiCryptedFileAdapter.prototype.loadDatabase = function loadDatabase(dbname, callback) {
|
151 | var secret = this.secret;
|
152 | var cFun = callback || console.log;
|
153 |
|
154 | fs.readFile(dbname,'utf8',function(err,data){
|
155 | var decrypted = err || decrypt(data, secret);
|
156 | cFun(decrypted);
|
157 | });
|
158 | };
|
159 |
|
160 | /**
|
161 | *
|
162 | @example
|
163 | // SAVE : will save database in 'test.crypted'
|
164 | var cryptedFileAdapter = require('./lokiCryptedFileAdapter');
|
165 | cryptedFileAdapter.setSecret('mySecret'); // you should change 'mySecret' to something supplied by the user
|
166 | var loki=require('lokijs');
|
167 | var db = new loki('test.crypted',{ adapter: cryptedFileAdapter }); //you can use any name, not just '*.crypted'
|
168 | var coll = db.addCollection('testColl');
|
169 | coll.insert({test: 'val'});
|
170 | db.saveDatabase(); // could pass callback if needed for async complete
|
171 |
|
172 | @example
|
173 | // if you have the krypt module installed you can use:
|
174 | krypt --decrypt test.crypted --secret mySecret
|
175 | to view the contents of the database
|
176 |
|
177 | * saveDatabase() - Saves a serialized db to the catalog.
|
178 | *
|
179 | * @param {string} dbname - the name to give the serialized database within the catalog.
|
180 | * @param {string} dbstring - the serialized db string to save.
|
181 | * @param {function} callback - (Optional) callback passed obj.success with true or false
|
182 | */
|
183 | lokiCryptedFileAdapter.prototype.saveDatabase = function saveDatabase(dbname, dbstring, callback) {
|
184 | var cFun = callback || function (){};
|
185 | var encrypted = encrypt(dbstring, this.secret);
|
186 | if (! isError(encrypted)){
|
187 | fs.writeFile(dbname,
|
188 | JSON.stringify(encrypted, null, ' '),
|
189 | 'utf8',cFun);
|
190 | }
|
191 | else { // Error !
|
192 | cFun(encrypted);
|
193 | }
|
194 | };
|
195 |
|
196 | module.exports = new lokiCryptedFileAdapter();
|
197 | exports.lokiCryptedFileAdapter = lokiCryptedFileAdapter;
|