'use strict'; var PromiseA = require('bluebird'); var path = require('path'); var fs = PromiseA.promisifyAll(require('fs')); var jwt = require('jsonwebtoken'); var crypto = require('crypto'); module.exports.create = function (deps, conf) { var hrIds = require('human-readable-ids').humanReadableIds; var scmp = require('scmp'); var storageDir = path.join(__dirname, '..', 'var'); function read(fileName) { return fs.readFileAsync(path.join(storageDir, fileName)) .then(JSON.parse, function (err) { if (err.code === 'ENOENT') { return {}; } throw err; }); } function write(fileName, obj) { return fs.mkdirAsync(storageDir).catch(function (err) { if (err.code !== 'EEXIST') { console.error('failed to mkdir', storageDir, err.toString()); } }).then(function () { return fs.writeFileAsync(path.join(storageDir, fileName), JSON.stringify(obj), 'utf8'); }); } var owners = { _filename: 'owners.json' , all: function () { return read(this._filename).then(function (owners) { return Object.keys(owners).map(function (id) { var owner = owners[id]; owner.id = id; return owner; }); }); } , get: function (id) { // While we could directly read the owners file and access the id directly from // the resulting object I'm not sure of the details of how the object key lookup // works or whether that would expose us to timing attacks. // See https://codahale.com/a-lesson-in-timing-attacks/ return this.all().then(function (owners) { return owners.filter(function (owner) { return scmp(id, owner.id); })[0]; }); } , exists: function (id) { return this.get(id).then(function (owner) { return !!owner; }); } , set: function (id, obj) { var self = this; return read(self._filename).then(function (owners) { obj.id = id; owners[id] = obj; return write(self._filename, owners); }); } }; var confCb; var config = { save: function (changes) { deps.messenger.send({ type: 'com.daplie.goldilocks/config' , changes: changes }); return new deps.PromiseA(function (resolve, reject) { var timeoutId = setTimeout(function () { reject(new Error('Did not receive config update from main process in a reasonable time')); confCb = null; }, 15*1000); confCb = function (config) { confCb = null; clearTimeout(timeoutId); resolve(config); }; }); } }; function updateConf(config) { if (confCb) { confCb(config); } } var userTokens = { _filename: 'user-tokens.json' , _cache: {} , _convertToken: function convertToken(id, token) { // convert the token into something that looks more like what OAuth3 uses internally // as sessions so we can use it with OAuth3. We don't use OAuth3's internal session // storage because it effectively only supports storing tokens based on provider URI. // We also use the token as the `access_token` instead of `refresh_token` because the // refresh functionality is closely tied to the storage. var decoded = jwt.decode(token); if (!decoded) { return null; } return { id: id , access_token: token , token: decoded , provider_uri: decoded.iss || decoded.issuer || decoded.provider_uri , client_uri: decoded.azp , scope: decoded.scp || decoded.scope || decoded.grants }; } , all: function allUserTokens() { var self = this; if (self._cacheComplete) { return deps.PromiseA.resolve(Object.values(self._cache)); } return read(self._filename).then(function (tokens) { // We will read every single token into our cache, so it will be complete once we finish // creating the result (it's set out of order so we can directly return the result). self._cacheComplete = true; return Object.keys(tokens).map(function (id) { self._cache[id] = self._convertToken(id, tokens[id]); return self._cache[id]; }); }); } , get: function getUserToken(id) { var self = this; if (self._cache.hasOwnProperty(id) || self._cacheComplete) { return deps.PromiseA.resolve(self._cache[id] || null); } return read(self._filename).then(function (tokens) { self._cache[id] = self._convertToken(id, tokens[id]); return self._cache[id]; }); } , save: function saveUserToken(newToken) { var self = this; return read(self._filename).then(function (tokens) { var rawToken; if (typeof newToken === 'string') { rawToken = newToken; } else { rawToken = newToken.refresh_token || newToken.access_token; } if (typeof rawToken !== 'string') { throw new Error('cannot save invalid session: missing refresh_token and access_token'); } var decoded = jwt.decode(rawToken); var idHash = crypto.createHash('sha256'); idHash.update(decoded.sub || decoded.ppid || decoded.appScopedId || ''); idHash.update(decoded.iss || decoded.issuer || ''); idHash.update(decoded.aud || decoded.audience || ''); var scope = decoded.scope || decoded.scp || decoded.grants || ''; idHash.update(scope.split(/[,\s]+/mg).sort().join(',')); var id = idHash.digest('hex'); tokens[id] = rawToken; return write(self._filename, tokens).then(function () { // Delete the current cache so that if this is an update it will refresh // the cache once we read the ID. delete self._cache[id]; return self.get(id); }); }); } , remove: function removeUserToken(id) { var self = this; return read(self._filename).then(function (tokens) { var present = delete tokens[id]; if (!present) { return present; } return write(self._filename, tokens).then(function () { delete self._cache[id]; return true; }); }); } }; var mdnsId = { _filename: 'mdns-id' , get: function () { var self = this; return read("mdns-id").then(function (result) { if (typeof result !== 'string') { throw new Error('mDNS ID not present'); } return result; }).catch(function () { return self.set(hrIds.random()); }); } , set: function (value) { var self = this; return write(self._filename, value).then(function () { return self.get(); }); } }; return { owners: owners , config: config , updateConf: updateConf , tokens: userTokens , mdnsId: mdnsId }; };