1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | (function() {
|
13 | var BAND_PREFIX, BAND_SUFFIX, Crypto, Item, Keychain, Opdata, PROFILE_PREFIX, PROFILE_SUFFIX, fs,
|
14 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
|
15 | __slice = [].slice;
|
16 |
|
17 | fs = require('fs');
|
18 |
|
19 | Crypto = require('./crypto');
|
20 |
|
21 | Opdata = require('./opdata');
|
22 |
|
23 | Item = require('./item');
|
24 |
|
25 | BAND_PREFIX = 'ld(';
|
26 |
|
27 | BAND_SUFFIX = ');';
|
28 |
|
29 | PROFILE_PREFIX = 'var profile=';
|
30 |
|
31 | PROFILE_SUFFIX = ';';
|
32 |
|
33 | |
34 |
|
35 |
|
36 |
|
37 |
|
38 | Keychain = (function() {
|
39 | |
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | Keychain.create = function(password, hint, profileName) {
|
48 | var keychain, keys, raw, superKey, timeNow;
|
49 | if (profileName == null) {
|
50 | profileName = 'default';
|
51 | }
|
52 | timeNow = Math.floor(Date.now() / 1000);
|
53 | keychain = new Keychain({
|
54 | uuid: Crypto.generateUuid(),
|
55 | salt: Crypto.randomBytes(16),
|
56 | createdAt: timeNow,
|
57 | updatedAt: timeNow,
|
58 | iterations: 20000,
|
59 | profileName: profileName,
|
60 | passwordHint: hint != null ? hint : '',
|
61 | lastUpdatedBy: 'Dropbox'
|
62 | });
|
63 | raw = {
|
64 | master: Crypto.randomBytes(256),
|
65 | overview: Crypto.randomBytes(64)
|
66 | };
|
67 | keys = {
|
68 | master: Crypto.hash(raw.master, 'sha512'),
|
69 | overview: Crypto.hash(raw.overview, 'sha512')
|
70 | };
|
71 | superKey = keychain._deriveKeys(password);
|
72 | keychain.encrypted = {
|
73 | masterKey: superKey.encrypt('profileKey', raw.master),
|
74 | overviewKey: superKey.encrypt('profileKey', raw.overview)
|
75 | };
|
76 | keychain.master = {
|
77 | encryption: Crypto.toBuffer(keys.master.slice(0, 64)),
|
78 | hmac: Crypto.toBuffer(keys.master.slice(64))
|
79 | };
|
80 | keychain.overview = {
|
81 | encryption: Crypto.toBuffer(keys.overview.slice(0, 64)),
|
82 | hmac: Crypto.toBuffer(keys.overview.slice(64))
|
83 | };
|
84 | return keychain;
|
85 | };
|
86 |
|
87 | |
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 | Keychain.createItem = Item.create;
|
94 |
|
95 | |
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 | function Keychain(attrs) {
|
103 | this._autolock = __bind(this._autolock, this); this.AUTOLOCK_LENGTH = 1 * 60 * 1000;
|
104 | this.profileName = 'default';
|
105 | this._events = {};
|
106 | this.items = {};
|
107 | if (attrs) {
|
108 | this.loadAttrs(attrs);
|
109 | }
|
110 | }
|
111 |
|
112 | |
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | Keychain.prototype.loadAttrs = function(attrs) {
|
120 | var attr, key;
|
121 | for (key in attrs) {
|
122 | attr = attrs[key];
|
123 | this[key] = attr;
|
124 | }
|
125 | return this;
|
126 | };
|
127 |
|
128 | |
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 | Keychain.prototype._deriveKeys = function(password) {
|
137 | var keys;
|
138 | keys = Crypto.pbkdf2(password, this.salt, this.iterations);
|
139 | this["super"] = {
|
140 | encryption: Crypto.toBuffer(keys.slice(0, 64)),
|
141 | hmac: Crypto.toBuffer(keys.slice(64))
|
142 | };
|
143 | return new Opdata(this["super"].encryption, this["super"].hmac);
|
144 | };
|
145 |
|
146 | |
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 | Keychain.prototype._trigger = function() {
|
156 | var args, event, fn, fnArgs, id, _ref, _results;
|
157 | event = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
|
158 | if (!(event in this._events)) {
|
159 | return;
|
160 | }
|
161 | _ref = this._events[event];
|
162 | _results = [];
|
163 | for (id in _ref) {
|
164 | fn = _ref[id];
|
165 | if (typeof fn !== 'function') {
|
166 | continue;
|
167 | }
|
168 | if (id.slice(0, 3) === "__") {
|
169 | fnArgs = args.slice(0);
|
170 | fnArgs.unshift(id);
|
171 | _results.push(fn.apply(null, fnArgs));
|
172 | } else {
|
173 | _results.push(fn.apply(null, args));
|
174 | }
|
175 | }
|
176 | return _results;
|
177 | };
|
178 |
|
179 | |
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 | Keychain.prototype.on = function(event, id, fn, once) {
|
190 | var _base, _ref,
|
191 | _this = this;
|
192 | if ((_ref = (_base = this._events)[event]) == null) {
|
193 | _base[event] = {
|
194 | index: 0
|
195 | };
|
196 | }
|
197 | if (typeof id === 'function') {
|
198 | fn = id;
|
199 | id = "__" + ++this._events[event].index;
|
200 | }
|
201 | if (once) {
|
202 | this._events[event][id] = function() {
|
203 | var args;
|
204 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
|
205 | fn.apply(null, args);
|
206 | return _this.off(event, id);
|
207 | };
|
208 | } else {
|
209 | this._events[event][id] = fn;
|
210 | }
|
211 | return id;
|
212 | };
|
213 |
|
214 | |
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 | Keychain.prototype.off = function(event, id) {
|
223 | var _results;
|
224 | if (id != null) {
|
225 | return delete this._events[event][id];
|
226 | } else {
|
227 | _results = [];
|
228 | for (id in this._events[event]) {
|
229 | _results.push(delete this._events[event][id]);
|
230 | }
|
231 | return _results;
|
232 | }
|
233 | };
|
234 |
|
235 | |
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | Keychain.prototype.one = function(event, id, fn) {
|
242 | return this.on(event, id, fn, true);
|
243 | };
|
244 |
|
245 | |
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 | Keychain.prototype.load = function(keychainPath) {
|
253 | var attachments, bands, filename, folder, folderContents, folders, profile, _i, _len;
|
254 | this.keychainPath = keychainPath;
|
255 | this.profileFolder = "" + this.keychainPath + "/" + this.profileName;
|
256 | folderContents = fs.readdirSync(this.profileFolder);
|
257 | profile = null;
|
258 | folder = null;
|
259 | bands = [];
|
260 | attachments = [];
|
261 | for (_i = 0, _len = folderContents.length; _i < _len; _i++) {
|
262 | filename = folderContents[_i];
|
263 | if (filename === "profile.js") {
|
264 | profile = "" + this.profileFolder + "/profile.js";
|
265 | } else if (filename === "folders.js") {
|
266 | folders = "" + this.profileFolder + "/folders.js";
|
267 | } else if (filename.match(/^band_[0-9A-F]\.js$/)) {
|
268 | bands.push("" + this.profileFolder + "/" + filename);
|
269 | } else if (filename.match(/^[0-9A-F]{32}_[0-9A-F]{32}\.attachment$/)) {
|
270 | attachments.push(filename);
|
271 | }
|
272 | }
|
273 | if (profile != null) {
|
274 | this.loadProfile(profile);
|
275 | } else {
|
276 | throw new Error('Couldn\'t find profile.js');
|
277 | }
|
278 | if (folders != null) {
|
279 | this.loadFolders(folders);
|
280 | }
|
281 | if (bands.length > 0) {
|
282 | this.loadBands(bands);
|
283 | }
|
284 | if (attachments.length > 0) {
|
285 | this.loadAttachment(attachments);
|
286 | }
|
287 | return this;
|
288 | };
|
289 |
|
290 | |
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 | Keychain.prototype.loadProfile = function(filepath) {
|
297 | var profile;
|
298 | profile = fs.readFileSync(filepath).toString();
|
299 | profile = profile.slice(PROFILE_PREFIX.length, -PROFILE_SUFFIX.length);
|
300 | profile = JSON.parse(profile);
|
301 | this.loadAttrs({
|
302 | uuid: profile.uuid,
|
303 | salt: Crypto.fromBase64(profile.salt),
|
304 | createdAt: profile.createdAt,
|
305 | updatedAt: profile.updatedAt,
|
306 | iterations: profile.iterations,
|
307 | profileName: profile.profileName,
|
308 | passwordHint: profile.passwordHint,
|
309 | lastUpdatedBy: profile.lastUpdatedBy
|
310 | });
|
311 | this.encrypted = {
|
312 | masterKey: Crypto.fromBase64(profile.masterKey),
|
313 | overviewKey: Crypto.fromBase64(profile.overviewKey)
|
314 | };
|
315 | return this;
|
316 | };
|
317 |
|
318 | |
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 | Keychain.prototype.loadFolders = function(filepath) {};
|
325 |
|
326 | |
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 | Keychain.prototype.loadBands = function(bands) {
|
333 | var band, filepath, item, uuid, _i, _len;
|
334 | for (_i = 0, _len = bands.length; _i < _len; _i++) {
|
335 | filepath = bands[_i];
|
336 | band = fs.readFileSync(filepath).toString('utf8');
|
337 | band = band.slice(BAND_PREFIX.length, -BAND_SUFFIX.length);
|
338 | band = JSON.parse(band);
|
339 | for (uuid in band) {
|
340 | item = band[uuid];
|
341 | this.addItem(item);
|
342 | }
|
343 | }
|
344 | return this;
|
345 | };
|
346 |
|
347 | |
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 | Keychain.prototype.loadAttachment = function(attachments) {};
|
354 |
|
355 | |
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 | Keychain.prototype.unlock = function(password) {
|
370 | var master, overview, profileKey,
|
371 | _this = this;
|
372 | profileKey = this._deriveKeys(password);
|
373 | master = profileKey.decrypt('profileKey', this.encrypted.masterKey);
|
374 | if (!master.length) {
|
375 | console.error("Could not decrypt master key");
|
376 | return false;
|
377 | }
|
378 | overview = profileKey.decrypt('profileKey', this.encrypted.overviewKey);
|
379 | if (!overview.length) {
|
380 | console.error("Could not decrypt overview key");
|
381 | return false;
|
382 | }
|
383 | this.master = new Opdata(master[0], master[1]);
|
384 | this.overview = new Opdata(overview[0], overview[1]);
|
385 | this.eachItem(function(item) {
|
386 | return item.decryptOverview(_this.overview);
|
387 | });
|
388 | this.unlocked = true;
|
389 | this.rescheduleAutoLock();
|
390 | setTimeout((function() {
|
391 | return _this._autolock();
|
392 | }), 1000);
|
393 | return this;
|
394 | };
|
395 |
|
396 | |
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
403 | Keychain.prototype.lock = function(autolock) {
|
404 | this._trigger('lock', autolock);
|
405 | this["super"] = void 0;
|
406 | this.master = void 0;
|
407 | this.overview = void 0;
|
408 | this.items = {};
|
409 | return this.unlocked = false;
|
410 | };
|
411 |
|
412 | |
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 | Keychain.prototype.rescheduleAutoLock = function() {
|
421 | return this.autoLockTime = Date.now() + this.AUTOLOCK_LENGTH;
|
422 | };
|
423 |
|
424 | |
425 |
|
426 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 | Keychain.prototype._autolock = function() {
|
432 | var now;
|
433 | if (!this.unlocked) {
|
434 | return;
|
435 | }
|
436 | now = Date.now();
|
437 | if (now < this.autoLockTime) {
|
438 | setTimeout(this._autolock, 1000);
|
439 | return;
|
440 | }
|
441 | return this.lock(true);
|
442 | };
|
443 |
|
444 | |
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 | Keychain.prototype.addItem = function(item) {
|
451 | if (!(item instanceof Item)) {
|
452 | item = new Item().load(item);
|
453 | }
|
454 | this.items[item.uuid] = item;
|
455 | return this;
|
456 | };
|
457 |
|
458 | |
459 |
|
460 |
|
461 |
|
462 |
|
463 |
|
464 |
|
465 | Keychain.prototype.decryptItem = function(uuid) {
|
466 | var item;
|
467 | item = this.getItem(uuid);
|
468 | return item.decryptDetails(this.master);
|
469 | };
|
470 |
|
471 | |
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|
477 | Keychain.prototype.exportProfile = function() {
|
478 | var data;
|
479 | data = {
|
480 | lastUpdatedBy: this.lastUpdatedBy,
|
481 | updatedAt: this.updatedAt,
|
482 | profileName: this.profileName,
|
483 | salt: this.salt.toString('base64'),
|
484 | passwordHint: this.passwordHint,
|
485 | masterKey: this.encrypted.masterKey.toString('base64'),
|
486 | iterations: this.iterations,
|
487 | uuid: this.uuid,
|
488 | overviewKey: this.encrypted.overviewKey.toString('base64'),
|
489 | createdAt: this.createdAt
|
490 | };
|
491 | return PROFILE_PREFIX + JSON.stringify(data) + PROFILE_SUFFIX;
|
492 | };
|
493 |
|
494 | |
495 |
|
496 |
|
497 |
|
498 |
|
499 |
|
500 | Keychain.prototype.exportBands = function() {
|
501 | var bands, data, files, id, item, items, uuid, _i, _len, _ref, _ref1;
|
502 | bands = {};
|
503 | _ref = this.items;
|
504 | for (uuid in _ref) {
|
505 | item = _ref[uuid];
|
506 | id = uuid.slice(0, 1);
|
507 | if ((_ref1 = bands[id]) == null) {
|
508 | bands[id] = [];
|
509 | }
|
510 | bands[id].push(item);
|
511 | }
|
512 | files = {};
|
513 | for (id in bands) {
|
514 | items = bands[id];
|
515 | data = {};
|
516 | for (_i = 0, _len = items.length; _i < _len; _i++) {
|
517 | item = items[_i];
|
518 | data[item.uuid] = item.toJSON();
|
519 | }
|
520 | data = BAND_PREFIX + JSON.stringify(data, null, 2) + BAND_SUFFIX;
|
521 | files["band_" + id + ".js"] = data;
|
522 | }
|
523 | return files;
|
524 | };
|
525 |
|
526 | |
527 |
|
528 |
|
529 |
|
530 |
|
531 |
|
532 |
|
533 | Keychain.prototype.getItem = function(uuid) {
|
534 | return this.items[uuid];
|
535 | };
|
536 |
|
537 | |
538 |
|
539 |
|
540 |
|
541 |
|
542 | Keychain.prototype.findItem = function(query) {
|
543 | var item, uuid, _ref, _results;
|
544 | _ref = this.items;
|
545 | _results = [];
|
546 | for (uuid in _ref) {
|
547 | item = _ref[uuid];
|
548 | if (item.match(query) === null) {
|
549 | continue;
|
550 | }
|
551 | _results.push(item);
|
552 | }
|
553 | return _results;
|
554 | };
|
555 |
|
556 | |
557 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
563 | Keychain.prototype.eachItem = function(fn) {
|
564 | var item, uuid, _ref, _results;
|
565 | _ref = this.items;
|
566 | _results = [];
|
567 | for (uuid in _ref) {
|
568 | item = _ref[uuid];
|
569 | _results.push(fn(item));
|
570 | }
|
571 | return _results;
|
572 | };
|
573 |
|
574 | return Keychain;
|
575 |
|
576 | })();
|
577 |
|
578 | module.exports = Keychain;
|
579 |
|
580 | }).call(this);
|