UNPKG

16.4 kBJavaScriptView Raw
1
2/**
3 * @fileoverview Read and write 1Password 4 Cloud Keychain files. Based on the
4 * documentation at {@link http://learn.agilebits.com/1Password4/Security/keychain-design.html Agile Bits}
5 * and {@link https://github.com/Roguelazer/onepasswordpy OnePasswordPy}
6 *
7 * @author George Czabania
8 * @version 0.1
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 * @class The Keychain stores all the items and profile data.
35 */
36
37
38 Keychain = (function() {
39 /**
40 * Create a new keychain
41 * @param {String} password The master password for the keychain.
42 * @param {String} [hint] The master password hint.
43 * @param {String} [profileName=default] The name of the keychain profile.
44 * @return {Keychain} Returns a new Keychain object
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 * Expose Item.create so you only have to include this one file
89 * @type {Function}
90 */
91
92
93 Keychain.createItem = Item.create;
94
95 /**
96 * Constructs a new Keychain
97 * @constructor
98 * @param {Object} [items={}] Load items
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 * Easy way to load data into a keychain
114 * @param {Object} attrs The attributes you want to load
115 * @return {this}
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 * Derive super keys from password using PBKDF2
130 * @private
131 * @param {String} password The master password.
132 * @return {Opdata} The derived keys as an opdata object.
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 * Trigger an event.
148 * @private
149 * @param {String} event The event name
150 * @param {Splat} [args] Any optional arguments you want to send with the
151 * event.
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 * Listen for an event, and run a function when it is triggered.
181 * @param {String} event The event name
182 * @param {String} [id] The id of the listener
183 * @param {Function} fn The function to run when the event is triggered
184 * @param {Boolean} [once] Run once, and then remove the listener
185 * @return {String} The event id
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 * Unbind an event listener
216 * @param {String} event The event name
217 * @param {String} [id] The id of the event. If left blank, then all events
218 * will be removed
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 * Listen to an event, but only fire the listener once
237 * @see this.on()
238 */
239
240
241 Keychain.prototype.one = function(event, id, fn) {
242 return this.on(event, id, fn, true);
243 };
244
245 /**
246 * Load data from a .cloudKeychain folder
247 * @param {String} filepath The filepath of the .cloudKeychain file
248 * @throws {Error} If profile.js can't be found
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 * Load data from profile.js into keychain.
292 * @param {String} filepath The path to the profile.js file.
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 * Load folders
320 * @param {String} filepath The path to the folders.js file.
321 */
322
323
324 Keychain.prototype.loadFolders = function(filepath) {};
325
326 /**
327 * This loads the item data from a band file into the keychain.
328 * @param {Array} bands An array of filepaths to each band file
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 * Load attachments
349 * @param {Array} attachments An array of filepaths to each attachment file
350 */
351
352
353 Keychain.prototype.loadAttachment = function(attachments) {};
354
355 /**
356 * Runs the master password through PBKDF2 to derive the super keys, and then
357 * decrypt the masterKey and overviewKey. The master password and super keys
358 * are then forgotten as they are no longer needed and keeping them in memory
359 * will only be a security risk.
360 *
361 * @param {String} password The master password to unlock the keychain
362 * with.
363 * @return {Boolean} Whether or not the keychain was unlocked successfully.
364 * Which is an easy way to see if the master password was
365 * correct.
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 * Lock the keychain. This discards all currently decrypted keys, overview
398 * data and any decrypted item details.
399 * @param {Boolean} autolock Whether the keychain was locked automatically.
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 * Reschedule when the keychain is locked. Should be called only when the
414 * user performs an important action, such as unlocking the keychain,
415 * selecting an item or copying a password, so that it doesn't lock when
416 * they are using it.
417 */
418
419
420 Keychain.prototype.rescheduleAutoLock = function() {
421 return this.autoLockTime = Date.now() + this.AUTOLOCK_LENGTH;
422 };
423
424 /**
425 * This is run every second, to check to see if the timer has expired. If it
426 * has it then locks the keychain.
427 * @private
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 * Add an item to the keychain
446 * @param {Object} item The item to add to the keychain
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 * Decrypt an item's details. The details are not saved to the item.
460 * @param {String} uuid The item UUID
461 * @return {Object} The items details
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 * Generate the profile.js file
473 * @return {String} The profile.js file
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 * This exports all the items currently in the keychain into band files.
496 * @return {Object} The band files
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 * This returns an item with the matching UUID
528 * @param {String} uuid The UUID to find the Item of
529 * @return {Item} The item matching the UUID
530 */
531
532
533 Keychain.prototype.getItem = function(uuid) {
534 return this.items[uuid];
535 };
536
537 /**
538 * Search through all items
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 * Loop through all the items in the keychain, and pass each one to a
558 * function.
559 * @param {Function} fn The function to pass each item to
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);