diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..8b7e824 --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "plugins": [ + "transform-flow-strip-types", + "transform-es2015-modules-amd" + ], + "ignore": [ + "./login/frontend/static/js/bad_password_bloom_data.js" + ] +} diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000..4a58bdc --- /dev/null +++ b/.flowconfig @@ -0,0 +1,7 @@ +[ignore] + +[include] + +[libs] + +[options] diff --git a/.gitignore b/.gitignore index bcf39f1..729d77a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ *.pyc node_modules +build/ +yarn-error.log +.vscode/settings.json diff --git a/FLOW.md b/FLOW.md new file mode 100644 index 0000000..77a9258 --- /dev/null +++ b/FLOW.md @@ -0,0 +1,21 @@ +# Flow typing the code base + +This is an ongoing process. You will need [flow](https://flow.org/) and yarn. To get started: + +``` +yarn install +node_modules/.bin/babel login/ --watch --out-dir build +``` + +# Contributing + +To start we're trying to type the code base without making any large +feature changes. + +This is best done by adding a `// @flow` line to the beginning of a +file, followed by fixing the resulting fallout of the `flow` command. + +# random + +* mark external libraries with TODO and EXTERNAL +* `nix-shell -p python2 --command "yarn install"` \ No newline at end of file diff --git a/api/js/cli/assert.js b/api/js/cli/assert.js new file mode 100644 index 0000000..cd60dca --- /dev/null +++ b/api/js/cli/assert.js @@ -0,0 +1,5 @@ +export function assert(expression: bool) { + if (!expression) { + throw new Error('Assertion failed'); + } +} diff --git a/api/js/cli/crypto.js b/api/js/cli/crypto.js index 4873d05..bb3a9a9 100644 --- a/api/js/cli/crypto.js +++ b/api/js/cli/crypto.js @@ -12,7 +12,7 @@ * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, + * This program is distributed in the hope that ist will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. @@ -52,15 +52,11 @@ Decrypting: /** @suppress{duplicate} */ var mitro = mitro || {}; -/** @suppress{duplicate} */ -var keyczar = keyczar || require('keyczarjs'); + +// TODO(flow): require keyczar + (function() { mitro.crypto = {}; -if (typeof module !== 'undefined' && module.exports) { - // define node.js module - mitro.cache = require('./lru_cache.js'); - module.exports = mitro.crypto; -} var crypto = mitro.crypto; /** Only for type checking. TODO: Use this? Make it an interface? @@ -162,7 +158,7 @@ function _makeKeyNoMemo(encryptionKey, signingKey) { } return memoResult; }; - + key.verify = function(message, signature) { return signingKey.verify(message, signature); }; @@ -204,7 +200,7 @@ function _makeKeyNoMemo(encryptionKey, signingKey) { var memoKey = mitro.cache.makeKey('decrypt', message); var memoResult = cache.getItem(memoKey); if (memoResult) { - + } else { memoResult = key.decryptNoMemo(message); cache.setItem(memoKey, memoResult); diff --git a/api/js/cli/kew.js b/api/js/cli/kew.js index d7286ac..91a3545 100644 --- a/api/js/cli/kew.js +++ b/api/js/cli/kew.js @@ -26,6 +26,8 @@ // Original source: https://github.com/Obvious/kew // License: MIT +// Now: https://github.com/Medium/kew +// TODO(tom): EXTERNAL library // Export all variables in the kew namespace in the browser var kew = kew || {}; @@ -567,7 +569,7 @@ kew.all = function(promises) { var promise = new kew.Promise(); var counter = promises.length; - /*The below functions have been pulled out of the for loop to prevent + /*The below functions have been pulled out of the for loop to prevent function creation in a loop*/ var decrement = function decrementAllCounter() { counter--; @@ -624,7 +626,7 @@ function allSettled(promises) { var resolution = function(){ if ((--counter) === 0) promise.resolve(outputs); }; - + for (var i = 0; i < promises.length; i += 1) { if (!promises[i] || !isPromiseLike(promises[i])) { replaceElFulfilled(outputs, i, promises[i]); diff --git a/api/js/cli/keycache.js b/api/js/cli/keycache.js index 66a4f31..fe16d57 100644 --- a/api/js/cli/keycache.js +++ b/api/js/cli/keycache.js @@ -1,3 +1,4 @@ +// @flow /* * ***************************************************************************** * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. @@ -24,57 +25,29 @@ * ***************************************************************************** */ -/** @suppress{duplicate} */ -var forge = forge || require('node-forge'); -/** @suppress{duplicate} */ -var mitro = mitro || {}; - -// Cache for crypto keys. Includes code to fill it in the background. -(function() { -"use strict"; -mitro.keycache = {}; - -var _Worker = null; -// define window.mitro.keycache for the browser -if(typeof(window) !== 'undefined') { - _Worker = (typeof(unsafeWindow) !== 'undefined') ? unsafeWindow.Worker : Worker; -} -// define node.js module -else if(typeof(module) !== 'undefined' && module.exports) { - mitro.crappycrypto = require('./crappycrypto.js'); - mitro.crypto = require('./crypto.js'); - module.exports = mitro.keycache; - try { - _Worker = require('webworker-threads').Worker; - } catch (e) { - // failed to load webworkers; ignore if DISABLE_WEBWORKERS env var is set - if (!process.env.DISABLE_WEBWORKERS) { - throw e; - } - } -} +import * as crypto from "./crypto"; +import * as forge from "../../../node_modules/node-forge/js/forge"; +import * as background_api from "../../../login/common/background_api"; // ideally keep the cache fill with this many keys -mitro.keycache.CACHE_TARGET = 2; +const CACHE_TARGET = 2; -// TODO: Define this in one place; maybe here and not in mitro_lib? -var crypto = mitro.crypto; -mitro.keycache.useCrappyCrypto = function() { - if (!mitro.crappycrypto) { - throw new Error('crappycrypto does not exist?'); +type Cache = { + push: (key: string) => void; + setPopListener: (listener: any) => void; + size: () => number; + getNewRSAKeysAsync: (numKeys: number, onSuccess: any, onError: any) => void; } - crypto = mitro.crappycrypto; -}; - -mitro.keycache.MakeKeyCache = function() { +// TODO(tom): migrate to class +export const MakeKeyCache = function(): Cache { var keys = []; var pushListener = null; var popListener = null; - var keyLimit = mitro.keycache.CACHE_TARGET; + var keyLimit = CACHE_TARGET; var keyLimitReachedCallback = null; - var cache = { + var cache: any = { size: function() { return keys.length; }, @@ -87,7 +60,7 @@ mitro.keycache.MakeKeyCache = function() { keyLimitReachedCallback(); keyLimitReachedCallback = null; } - keyLimit = mitro.keycache.CACHE_TARGET; + keyLimit = CACHE_TARGET; } if (pushListener !== null) { pushListener(cache); @@ -144,12 +117,11 @@ mitro.keycache.MakeKeyCache = function() { keyLimit = count; keyLimitReachedCallback = onSuccess; for (var i = cache.size(); i < count; ++i) { - popListener(); + if (popListener !== null) popListener(cache); } } }; - cache.getNewRSAKeysAsync = function(numKeys, onSuccess, onError) { try { var rval = []; @@ -176,27 +148,26 @@ mitro.keycache.MakeKeyCache = function() { /** Returns a string with random bytes from forge.random.getBytesSync. */ // TODO: Remove this -mitro.keycache.getEntropyBytes = function(numBytes) { +export const getEntropyBytes = function(numBytes: number) { return forge.random.seedFileSync(numBytes); }; - - -mitro.keycache.startFiller = function(cache) { +export const startFiller = function(cache: Cache) { // start a worker: get it to fill the cache - var background_worker = new _Worker('keycache_webworker.js'); - - var type = 'keyczar'; - if (crypto == mitro.crappycrypto) { - type = 'crappy'; - } + const background_worker = new Worker('keycache_webworker.js'); + const type = 'keyczar'; var hasStrongRandom = (type != 'crappy' && typeof window != 'undefined' && window.crypto && window.crypto.getRandomValues); - background_worker.addEventListener('message', function(e) { + + background_worker.onmessage = function(e: MessageEvent) { + if (!e.data || typeof e.data !== "object") { + return; + } if (e.data.key) { console.log('got key from worker'); + // $FlowFixMe var k = crypto.loadFromJson(e.data.key); cache.push(k); } else if (e.data.request == 'log') { @@ -206,14 +177,14 @@ mitro.keycache.startFiller = function(cache) { } else { console.log('unhandled worker message', e.data); } - }); + } var postGenerateKey = function() { - var request = {request: 'generate'}; + var request: any = {request: 'generate'}; if (hasStrongRandom) { // each time we generate a key, add some additional entropy from the browser // TODO: Is this a sufficient amount? - request.seed = mitro.keycache.getEntropyBytes(32); + request.seed = getEntropyBytes(32); } background_worker.postMessage(request); }; @@ -230,13 +201,13 @@ mitro.keycache.startFiller = function(cache) { if (hasStrongRandom) { // seed the random number generator with good entropy, if we have it // 32 pools that each want 32 bytes of entropy - var seed = mitro.keycache.getEntropyBytes(32 * 32); + var seed = getEntropyBytes(32 * 32); background_worker.postMessage({request: 'seed', seed: seed}); } // Post one message to the background for each key var count = cache.size(); - while (count < mitro.keycache.CACHE_TARGET) { + while (count < CACHE_TARGET) { postGenerateKey(); count += 1; } @@ -247,6 +218,3 @@ mitro.keycache.startFiller = function(cache) { } }; }; - - -})(); diff --git a/api/js/cli/logging.js b/api/js/cli/logging.js index fbd763d..b73bcd0 100644 --- a/api/js/cli/logging.js +++ b/api/js/cli/logging.js @@ -1,3 +1,4 @@ +// @flow /* * ***************************************************************************** * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. @@ -24,29 +25,11 @@ * ***************************************************************************** */ -var mitro = mitro || {}; - -/** @suppress{duplicate} */ -var debugMode; -(function() { -// define mitro -if(typeof(window) !== 'undefined') { - if (typeof(mitro) === 'undefined') { - mitro = {}; - } - mitro.log = {}; -} -// define node.js module -else if(typeof(module) !== 'undefined' && module.exports) { - mitro = {}; - module.exports = mitro.log = {}; -} - -var makeCircularBuffer = function(size) { - var loc = 0; - var data = []; - var obj = { - get : function(k){ +export const makeCircularBuffer = function(size: number) { + let loc = 0; + let data = []; + let obj = { + get: function(k: number) { if (k < 0 || data.length <= k) { return undefined; } @@ -54,69 +37,61 @@ var makeCircularBuffer = function(size) { // the "oldest" entry in the array is the one that loc points at return data[(loc + k) % data.length]; }, - push : function(){ + push: function() { var dat = Array.prototype.slice.call(arguments); data[loc] = dat; loc = (loc + 1) % size; }, size : function() { return data.length; - } - }; - - obj.toArray = function() { - var rval = []; - for (var i=0; i < obj.size(); ++i) { - rval.push(obj.get(i)); - } - return rval; - }; + }, + toArray: function() { + var rval = []; + for (var i=0; i < obj.size(); ++i) { + rval.push(obj.get(i)); + } + return rval; + }, + toString: function() { + var outputArray = obj.toArray(); + var output = ''; + for (var i = 0; i < outputArray.length; i++) { + // don't modify the actual data in the buffer + // $FlowFixMe + var rowArrayCopy = outputArray[i].slice(); + for (var j = 0; j < rowArrayCopy.length; j++) { + var element = rowArrayCopy[j]; + if (element instanceof Error) { + // Browser's default JSON.stringify doesn't convert native Error instances + element = { + name: element.name, + message: element.message, + stack: element.stack + }; + } - obj.toString = function() { - var outputArray = obj.toArray(); - var output = ''; - for (var i = 0; i < outputArray.length; i++) { - // don't modify the actual data in the buffer - var rowArrayCopy = outputArray[i].slice(); - for (var j = 0; j < rowArrayCopy.length; j++) { - var element = rowArrayCopy[j]; - if (element instanceof Error) { - // Browser's default JSON.stringify doesn't convert native Error instances - element = { - name: element.name, - message: element.message, - stack: element.stack - }; + // recursively converts arrays and objects + if (typeof element == 'object') { + rowArrayCopy[j] = JSON.stringify(element); + } } - // recursively converts arrays and objects - if (typeof element == 'object') { - rowArrayCopy[j] = JSON.stringify(element); - } + output += rowArrayCopy.join(' '); + output += '\n'; } - - output += rowArrayCopy.join(' '); - output += '\n'; + return output; } - return output; }; - return obj; }; -var logBuffer = makeCircularBuffer(500); -mitro.log.makeCircularBuffer = makeCircularBuffer; -mitro.log.logBuffer = logBuffer; -var oldLog = console.log; -mitro.log.captureLogsToBuffer = function() { +export const logBuffer = makeCircularBuffer(500); +const oldLog = console.log; +export const captureLogsToBuffer = function() { + // $FlowFixMe console.log = logBuffer.push; }; -mitro.log.stopCapturingLogsToBuffer = function() { +export const stopCapturingLogsToBuffer = function() { + // $FlowFixMe console.log = oldLog; }; - -if (!debugMode) { - mitro.log.captureLogsToBuffer(); -} - -})(); diff --git a/api/js/cli/lru_cache.js b/api/js/cli/lru_cache.js index ec3a20e..dcc8e62 100644 --- a/api/js/cli/lru_cache.js +++ b/api/js/cli/lru_cache.js @@ -24,20 +24,7 @@ * ***************************************************************************** */ -/** @suppress{duplicate} */ -var mitro = mitro || {}; -(function() { -mitro.cache = {}; -if (typeof module !== 'undefined' && module.exports) { - module.exports = mitro.cache; -} -var cache = mitro.cache; - -var assert = function(expression) { - if (!expression) { - throw new Error('Assertion failed'); - } -}; +import { assert } from "./assert"; /* MIT LICENSE @@ -68,7 +55,7 @@ OTHER DEALINGS IN THE SOFTWARE. // **************************************************************************** // LRUCachePriority ENUM // An easier way to refer to the priority of a cache item -var LRUCachePriority = { +export const LRUCachePriority = { Low: 1, Normal: 2, High: 4 @@ -83,7 +70,7 @@ var LRUCachePriority = { /** @constructor @param {number} maxSize */ -function LRUCache(maxSize) { +export function LRUCache(maxSize) { this.items = {}; this.count = 0; if (maxSize === null) @@ -287,7 +274,7 @@ LRUCache.prototype.toHtmlString = function() { }; var KEY_SEPARATOR = '\u009E'; // some random UTF-8 control character -var makeKey = function() { +function makeKey() { var args = Array.prototype.slice.call(arguments); var key = ''; for (var i = 0; i < args.length; ++i) { @@ -302,11 +289,8 @@ var makeKey = function() { // Decent hash function // TODO: think about security implications of using this in keys. -var hash = function(s){ +function hash(s: string){ return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a;},0); }; -cache.LRUCache = LRUCache; -cache.makeKey = makeKey; -cache.hash = hash; -})(); +export { makeKey, hash }; diff --git a/api/js/cli/mitro.js b/api/js/cli/mitro.js index a0b7124..05ca568 100644 --- a/api/js/cli/mitro.js +++ b/api/js/cli/mitro.js @@ -24,14 +24,12 @@ * ***************************************************************************** */ -//#!/usr/bin/env node - -var optimist = require('optimist'); -var assert = require('assert'); -var lib = require('./mitro_lib.js'); -var rpc = require('./rpc'); -var fe = require('./mitro_fe'); -var fs = require('fs'); +//var optimist = require('optimist'); +//var assert = require('assert'); +//var lib = require('./mitro_lib.js'); +//var rpc = require('./rpc'); +//var fe = require('./mitro_fe'); +//var fs = require('fs'); // TODO(flow) var allowableCommands = { 'getpub' : lib.GetPublicKey, diff --git a/api/js/cli/mitro_fe.js b/api/js/cli/mitro_fe.js index 4e20dcd..1740b46 100644 --- a/api/js/cli/mitro_fe.js +++ b/api/js/cli/mitro_fe.js @@ -24,53 +24,16 @@ * ***************************************************************************** */ -/** @suppress{duplicate} */ -var keyczar = keyczar || require('keyczarjs'); -/** @suppress{duplicate} */ -var mitro = mitro || {}; -/** @suppress{duplicate} */ -var forge = forge || require('node-forge'); -(function() { -mitro.fe = {}; -var keyCache = null; -if(typeof(module) !== 'undefined' && module.exports) { - /// NODE module - mitro = { - lib: require('./mitro_lib.js'), - keycache: require('./keycache'), - fs : require('fs'), - log : require('./logging') - }; - - var mitroClientModule = require('./mitroclient'); - mitro.Client = mitroClientModule.Client; - - module.exports = mitro.fe = {}; +let keyCache = null; +let FAILOVER_MITRO_HOST = null; +let FAILOVER_MITRO_PORT = null; +let DEVICE_ID = null; - mitro.fe.initCacheFromFile = function(cacheFileName) { - try { - keyCache = mitro.keycache.MakeKeyCache(); - keyCache.loadFromJson(mitro.fs.readFileSync(cacheFileName)); - console.log('loaded ' + keyCache.size() + ' keys'); - } catch (e) { - console.log('could not read from key cache file '+ cacheFileName+'... using default'); - console.log(e); - console.log(e.stack); - } - }; - - mitro.fe.startCacheFiller = function() { - keyCache = mitro.keycache.MakeKeyCache(); - mitro.keycache.startFiller(keyCache); - }; +import * as crypto from "./crypto"; +import * as mitro_lib from "./mitro_lib"; +import * as mitro_legacyapi from "./mitro_legacyapi"; - mitro.fe.initCacheFromJson = function(json) { - keyCache = mitro.keycache.MakeKeyCache(); - keyCache.loadFromJson(json); - }; -} - -mitro.fe.getRandomness = function(onSuccess, onError) { +export function getRandomness(onSuccess, onError) { // This runs in a webworker and can't touch window.crypto (because of slow JS in Firefox) // worker.js replaces this as a "proxy" to post a message to the background extension page. // background_api.js implements the "real" version that gets entropy. @@ -81,24 +44,13 @@ mitro.fe.getRandomness = function(onSuccess, onError) { onSuccess({seed: ''}); }; -mitro.fe.setKeyCache = function(kc) { +export function setKeyCache(kc) { keyCache = kc; }; - -var fe = mitro.fe; -var assert = function(expression) { - if (!expression) { - throw new Error('Assertion failed'); - } -}; - -var crypto = function() {return mitro.lib.getCrypto();}; - - /** -Computes the difference between oldList and newList (treating them as sets), -putting the result in added and deleted. The lists will be sorted afterwards. +Computes the difference between oldList and newList (treating them as sets), +putting the result in added and deleted. The lists will be sorted afterwards. Only works for numeric types. @param {!Array.} oldList @@ -107,6 +59,7 @@ Only works for numeric types. @param {!Array.} deletedList @return {boolean} true iff the lists are different */ +// TODO(tom_: use underscore; function bidirectionalSetDiff(oldList, newList, addedList, deletedList) { return bidrectionalSetDiffWithComparator(oldList, newList, addedList, deletedList, function(a, b) { @@ -115,8 +68,8 @@ function bidirectionalSetDiff(oldList, newList, addedList, deletedList) { } /** -Computes the difference between oldList and newList (treating them as sets), -putting the result in added and deleted. The lists will be sorted afterwards. +Computes the difference between oldList and newList (treating them as sets), +putting the result in added and deleted. The lists will be sorted afterwards. Supply a comparator if you want non-string compare. @@ -158,7 +111,7 @@ function bidrectionalSetDiffWithComparator(oldList, newList, addedList, deletedL } /** Converts the response from ListMySecretsAndGroups to the format expected by the extension. */ -mitro.fe.convertListSitesToExtension = function(response) { +export function convertListSitesToExtension(response) { var output = []; for (var secretId in response.secretToPath) { var obj = {}; @@ -192,16 +145,12 @@ mitro.fe.convertListSitesToExtension = function(response) { return output; }; -var FAILOVER_MITRO_HOST = null; -var FAILOVER_MITRO_PORT = null; -var DEVICE_ID = null; - /** * Wraps an operation and retries it entirely if it fails * due to a cancelled transaction that should be retried. * **/ - var wrapWithTransactionCancelledRetry = function(wrappedFcn) { +var wrapWithTransactionCancelledRetry = function(wrappedFcn) { var MIN_RETRY_DELAY = 1000; var MAX_RETRY_DELAY = 3000; // some of the write functions modify arguments, so we need to keep a deep @@ -221,7 +170,7 @@ var DEVICE_ID = null; // do this double copy to be sure that the default implementation actually works wrappedFcnArgs = _copy(originalwrappedFcnArgs); - + var newOnError = function(e) { try { if (e.exceptionType === 'RetryTransactionException') { @@ -240,7 +189,7 @@ var DEVICE_ID = null; oldOnError(e); } } catch (e2) { - oldOnError(mitro.lib.makeLocalException(e2)); + oldOnError(mitro_lib.makeLocalException(e2)); } }; wrappedFcnArgs.push(oldOnSuccess); @@ -301,7 +250,7 @@ var wrapWithFailover = function(wrappedFcn) { /* Creates a new identity. onSuccess gets called with the MitroIdentity. */ -function createIdentity(email, password, analyticsId, host, port, onSuccess, onError) { +export function createIdentity(email, password, analyticsId, host, port, onSuccess, onError) { var onSuccessWrapper = function(result) { try { @@ -312,7 +261,7 @@ function createIdentity(email, password, analyticsId, host, port, onSuccess, onE } catch (e) { console.log(e); - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; var args = { @@ -325,7 +274,7 @@ function createIdentity(email, password, analyticsId, host, port, onSuccess, onE _createPrivateGroup : true, deviceId : DEVICE_ID }; - mitro.lib.AddIdentity(args, onSuccessWrapper, onError); + mitro_lib.AddIdentity(args, onSuccessWrapper, onError); } /* Accesses an existing identity. onSuccess gets called with the MitroIdentity. */ @@ -338,7 +287,7 @@ function loginWithToken(email, password, token, host, port, tfaCode, onSuccess, } function loginWithTokenAndLocalKey(email, password, token, host, port, locallyEncryptedKey, tfaCode, onSuccess, onError) { - mitro.lib.clearCaches(); + mitro_lib.clearCaches(); if (!token) { token = {}; } @@ -353,8 +302,8 @@ function loginWithTokenAndLocalKey(email, password, token, host, port, locallyEn deviceId : DEVICE_ID, automatic: !!locallyEncryptedKey }; - - (wrapWithFailover(mitro.lib.GetPrivateKey))(args, function(response) { + + (wrapWithFailover(mitro_lib.GetPrivateKey))(args, function(response) { try { var privateKey = null; // in case we have an AES key, and a locally stored key, and no password. @@ -385,7 +334,7 @@ function loginWithTokenAndLocalKey(email, password, token, host, port, locallyEn }, onError); } catch (e) { console.log(e); - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }, onError); } @@ -394,7 +343,7 @@ function addIssue(args, host, port, onSuccess, onError) { args.server_host = host; args.server_port = port; - mitro.lib.AddIssue(args, onSuccess, onError); + mitro_lib.AddIssue(args, onSuccess, onError); } /** Provides Closure-compiled code access to existing APIs. @@ -411,7 +360,7 @@ LegacyAPIImplementation.prototype.getPublicKeys = function( var args = this.makeArgs(transaction); args.addMissingUsers = true; identities.sort(); - mitro.lib.GetPublicKeys(args, identities, function(result) { + mitro_lib.GetPublicKeys(args, identities, function(result) { if (result.missingUsers.length !== 0) { onError(new Error('LegacyAPIImplementation.getPublicKeys: missing users: ' + result.missingUsers.length)); @@ -428,7 +377,7 @@ LegacyAPIImplementation.prototype.cryptoLoadFromJson = function(jsonString) { LegacyAPIImplementation.prototype.postSigned = function( path, request, transaction, onSuccess, onError) { var args = this.makeArgs(transaction); - mitro.lib.PostToMitro(request, args, path, onSuccess, onError); + mitro_lib.PostToMitro(request, args, path, onSuccess, onError); }; LegacyAPIImplementation.prototype.getNewRSAKeysAsync = function(count, onSuccess, onError) { @@ -438,7 +387,7 @@ LegacyAPIImplementation.prototype.getNewRSAKeysAsync = function(count, onSuccess LegacyAPIImplementation.prototype.getGroup = function(groupId, transaction, onSuccess, onError) { var args = this.makeArgs(transaction); args.gid = groupId; - mitro.lib.GetGroup(args, onSuccess, onError); + mitro_lib.GetGroup(args, onSuccess, onError); }; LegacyAPIImplementation.prototype.getIdentity = function() { @@ -485,7 +434,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port var legacyApi = new LegacyAPIImplementation(_makeArgs); obj.mitroclient = new mitro.Client(legacyApi); var deviceSpecificKey = null; - + obj.getPrivateKeyStringForLocalDisk = function() { return deviceSpecificKey ? privateKey.encryptWith(deviceSpecificKey) : null; }; @@ -498,21 +447,20 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port onSuccess(deviceSpecificKey ? privateKey.encryptWith(deviceSpecificKey) : null); }; - obj.holdingTransaction.retrieveDeviceSpecificKey = function(transactionSpecificData, onSuccess, onError) { try { var args = _makeArgs(transactionSpecificData); - wrapWithFailover(mitro.lib.RetrieveDeviceSpecificKey)(args, + wrapWithFailover(mitro_lib.RetrieveDeviceSpecificKey)(args, function (response) { try { obj.setDeviceSpecificKeyFromString(response.deviceKeyString); onSuccess(); } catch(e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }, onError); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; @@ -525,7 +473,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port try { onSuccess(privateKey.sign(msg)); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; @@ -575,7 +523,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.mutateSecret(transactionSpecificData, secretId, secret.serverData, secret.clientData, secretData, onSuccess, onError); } catch(e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }, onError); }; @@ -600,7 +548,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port toRun.push([obj.holdingTransaction.editServerSecret, [transactionSpecificData, updatedServerData]]); } - mitro.lib.batch(toRun, + mitro_lib.batch(toRun, function(rvals) { try { var secret = rvals[0]; @@ -626,12 +574,12 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port groupIdToEncryptedData : groupIdToEncryptedData }; transactionSpecificData.implicitEndTransaction = true; - mitro.lib.PostToMitro(data, _makeArgs(transactionSpecificData), + mitro_lib.PostToMitro(data, _makeArgs(transactionSpecificData), '/mitro-core/api/EditSecretContent', onSuccess, onError); } catch (e) { console.log(e.message); console.log(e.stack); - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }, onError); }; @@ -639,10 +587,10 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.removePendingGroups = function(transactionSpecificData, scope, onSuccess, onError) { try { var rpc = {scope: scope}; - mitro.lib.PostToMitro(rpc, _makeArgs(transactionSpecificData), '/mitro-core/api/RemovePendingGroupApprovals', onSuccess, onError); + mitro_lib.PostToMitro(rpc, _makeArgs(transactionSpecificData), '/mitro-core/api/RemovePendingGroupApprovals', onSuccess, onError); } catch (e) { console.log(e.stack); - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; @@ -652,20 +600,20 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port assert (args.isViewable !== undefined); data.isViewable = args.isViewable; data.secretId = args.secretId; - mitro.lib.PostToMitro(data, _makeArgs(transactionSpecificData), + mitro_lib.PostToMitro(data, _makeArgs(transactionSpecificData), '/mitro-core/api/EditSecret', onSuccess, onError); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; obj.holdingTransaction.getPendingGroups = function(transactionSpecificData, scope, onSuccess, onError) { try { - mitro.lib.PostToMitro({scope:scope}, _makeArgs(transactionSpecificData), + mitro_lib.PostToMitro({scope:scope}, _makeArgs(transactionSpecificData), '/mitro-core/api/GetPendingGroups', onSuccess, onError); } catch (e) { console.log('error getting groups:', e.stack); - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; @@ -674,13 +622,13 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port // PostToMitro has a different parameter order than wrapWithFailover - (wrapWithFailover(mitro.lib.GetOrganizationState))(_makeArgs(transactionSpecificData), + (wrapWithFailover(mitro_lib.GetOrganizationState))(_makeArgs(transactionSpecificData), {orgId:orgId}, function(resp) { var secrets = resp.orgSecretsToPath; for (var i in resp.orgSecretsToPath) { // if the secret has encrypted data, decrypt it. TODO: Fix server to ALWAYS send this if (secrets[i].encryptedClientData) { - mitro.lib.decryptSecretWithGroups(secrets[i], resp.groups, privateKey); + mitro_lib.decryptSecretWithGroups(secrets[i], resp.groups, privateKey); secrets[i].clientData = JSON.parse(secrets[i].clientData); } secrets[i].hints = {}; @@ -692,7 +640,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port } catch (e) { console.log('error getting org state:', e.stack); - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; @@ -753,7 +701,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port } // mutate the organization as requested. - var mutateRequest = new mitro.MutateOrganizationClientRequest(); + var mutateRequest = new mitro_legacyapi.MutateOrganizationClientRequest(); mutateRequest.orgId = approvals.orgId; mutateRequest.newMembers = approvals.newOrgMembers; if (options.removeUsersFromOrg) { @@ -764,13 +712,13 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port toRun.push([obj.holdingTransaction.mutateOrganization, [transactionSpecificData, mutateRequest]]); } - mitro.lib.series(toRun, onSuccess, onError); + mitro_lib.series(toRun, onSuccess, onError); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }, onError); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; @@ -779,9 +727,9 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port args.loginToken = token.loginToken, args.loginTokenSignature = token.loginTokenSignature; try { - mitro.lib.checkTwoFactor(args, function(d) { onSuccess(d.twoFactorUrl); }, onError); + mitro_lib.checkTwoFactor(args, function(d) { onSuccess(d.twoFactorUrl); }, onError); } catch(e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; @@ -790,12 +738,12 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port args.loginToken = token.loginToken, args.loginTokenSignature = token.loginTokenSignature; - mitro.lib.GetPrivateKey(args, function(privateKeyResponse) { + mitro_lib.GetPrivateKey(args, function(privateKeyResponse) { try { // refetch the private key. var privateKey = crypto().loadFromJson(privateKeyResponse.encryptedPrivateKey, oldPassword); var newEncryptedPrivateKey = privateKey.toJsonEncrypted(newPassword); - mitro.lib.EditEncryptedPrivateKey(_makeArgs(transactionSpecificData), up, newEncryptedPrivateKey, + mitro_lib.EditEncryptedPrivateKey(_makeArgs(transactionSpecificData), up, newEncryptedPrivateKey, function(resp) { changePwd = false; onSuccess(resp); @@ -803,7 +751,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }); }; @@ -811,13 +759,13 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.mutatePrivateKeyPasswordWithoutOldPassword = function(transactionSpecificData, args, onSuccess, onError) { try { var newEncryptedPrivateKey = privateKey.toJsonEncrypted(args.newPassword); - mitro.lib.EditEncryptedPrivateKey(_makeArgs(transactionSpecificData), args.up, newEncryptedPrivateKey, + mitro_lib.EditEncryptedPrivateKey(_makeArgs(transactionSpecificData), args.up, newEncryptedPrivateKey, function(resp) { changePwd = false; onSuccess(resp); }, onError); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; @@ -828,7 +776,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port var args = _makeArgs(transactionSpecificData); args.gid = privateNonOrgGroupId; args._ = [null, loginUrl, JSON.stringify(clientData), JSON.stringify(secretData)]; - mitro.lib.AddSecret(args, function (response) { + mitro_lib.AddSecret(args, function (response) { onSuccess(response.secretId); }, onError); }; @@ -838,7 +786,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port data.clientData = JSON.stringify(data.clientData); data.criticalData = JSON.stringify(data.criticalData); console.log('mitro_fe addSecrets', data.groupIds); - mitro.lib.AddSecrets(args, data, onSuccess, onError); + mitro_lib.AddSecrets(args, data, onSuccess, onError); }; var _makeClientAndSecretData = function(loginUrl, username, password, @@ -863,17 +811,17 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port return obj.holdingTransaction.addSecret(transactionSpecificData, loginUrl, clientAndSecretData[0], clientAndSecretData[1], onSuccess, onError); }; - obj.holdingTransaction.shareSite = function(transactionSpecificData, secretId, newGroupIdList, + obj.holdingTransaction.shareSite = function(transactionSpecificData, secretId, newGroupIdList, newIdentityList, onSuccess, onError) { - return obj.holdingTransaction.shareSiteAndOptionallySetOrg(transactionSpecificData, secretId, newGroupIdList, newIdentityList, + return obj.holdingTransaction.shareSiteAndOptionallySetOrg(transactionSpecificData, secretId, newGroupIdList, newIdentityList, null, onSuccess, onError); }; - obj.holdingTransaction.shareSiteAndOptionallySetOrg = function(transactionSpecificData, secretId, newGroupIdList, + obj.holdingTransaction.shareSiteAndOptionallySetOrg = function(transactionSpecificData, secretId, newGroupIdList, newIdentityList, orgGroupId, onSuccess, onError) { // 1. list groups and secrets and get existing secret assert(secretId); - mitro.lib.parallel([ + mitro_lib.parallel([ [obj.holdingTransaction.getSiteSecretData, [transactionSpecificData, secretId]] ], @@ -943,7 +891,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port newOrgList = [orgGroupId]; } - // TODO: this should actually edit the group with the modified user list instead of + // TODO: this should actually edit the group with the modified user list instead of // recreating from scratch. var hasDifferentUsers = bidrectionalSetDiffWithComparator(rval[0].users, newIdentityList, [], []); if (rval[0].owningOrgId !== orgGroupId || hasDifferentUsers) { @@ -982,7 +930,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port bidirectionalSetDiff(oldgroups, newGroupIdList, toadd, todel); - toRun.push([mitro.lib.AddSecrets, [_makeArgs(transactionSpecificData), + toRun.push([mitro_lib.AddSecrets, [_makeArgs(transactionSpecificData), {groupIds: toadd, secretId : secretId}]]); @@ -991,15 +939,15 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port delArgs.secretId = secretId; delArgs._ = [null, secretId]; delArgs.gid = todel[i]; - toRun.push([mitro.lib.RemoveSecret, [delArgs]]); + toRun.push([mitro_lib.RemoveSecret, [delArgs]]); } newNumberOfGroups += oldgroups.length - todel.length + toadd.length; - + // Boo JS has no XOR. should continue to have groups IFF we are not deleting the secret. assert ((newNumberOfGroups > 0) === (!deleteSecret)); - mitro.lib.series(toRun, onSuccess, onError); + mitro_lib.series(toRun, onSuccess, onError); }, onError ); @@ -1008,7 +956,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.getGroup = function(transactionSpecificData, groupId, onSuccess, onError) { var args = _makeArgs(transactionSpecificData); args.gid = groupId; - mitro.lib.GetGroup(args, onSuccess, onError); + mitro_lib.GetGroup(args, onSuccess, onError); }; @@ -1034,10 +982,10 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port // fetch all the users' public keys. - mitro.lib.GetUserAndGroupPublicKeys(args, /* add missing users*/ true, newIdentityList, newGroupIdList, function(keysResponse) { + mitro_lib.GetUserAndGroupPublicKeys(args, /* add missing users*/ true, newIdentityList, newGroupIdList, function(keysResponse) { args.gid = groupId; args.orgId = newGroupIdList !== null ? newGroupIdList[0] : null; - mitro.lib.MutateMembership(args, function(group, unencryptedGroupKey, response) { + mitro_lib.MutateMembership(args, function(group, unencryptedGroupKey, response) { var i; var regenerateGroupKey = null; @@ -1127,20 +1075,20 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port // close GetPublicKeys }, onError); - + } catch(e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; obj.holdingTransaction.getPublicKeys = function(transactionSpecificData, userIds, onSuccess, onError) { - mitro.lib.GetPublicKeys(_makeArgs(transactionSpecificData), userIds, onSuccess, onError); + mitro_lib.GetPublicKeys(_makeArgs(transactionSpecificData), userIds, onSuccess, onError); }; - + obj.holdingTransaction._addGroup = function(transactionSpecificData, groupName, autoDelete, onSuccess, onError) { return obj.holdingTransaction.addGroupWithScope(transactionSpecificData, groupName, null, autoDelete, onSuccess, onError); }; - + obj.holdingTransaction.addGroupWithScope = function(transactionSpecificData, groupName, groupScope, autoDelete, onSuccess, onError) { try { console.log('in addgroup with args: ', transactionSpecificData, groupName, groupScope, autoDelete); @@ -1157,7 +1105,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port args.scope = groupScope; args._ = [null, groupName]; args.autoDelete = autoDelete; - mitro.lib.AddGroup(args, function(response) { + mitro_lib.AddGroup(args, function(response) { // if we are creating the initial empty group, save the id if (privateNonOrgGroupId === null) { @@ -1178,7 +1126,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.deleteSecret = function(transactionSpecificData, secretId, onSuccess, onError) { try { var rpc = {secretId: secretId, groupId:null}; - mitro.lib.PostToMitro(rpc, _makeArgs(transactionSpecificData), '/mitro-core/api/RemoveSecret', onSuccess, onError); + mitro_lib.PostToMitro(rpc, _makeArgs(transactionSpecificData), '/mitro-core/api/RemoveSecret', onSuccess, onError); } catch (e) { console.log(e.stack); onError({local_exception : e}); @@ -1188,16 +1136,16 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.removeGroup = function(transactionSpecificData, groupId, onSuccess, onError) { try { var rpc = {groupId: groupId}; - mitro.lib.PostToMitro(rpc, _makeArgs(transactionSpecificData), '/mitro-core/api/DeleteGroup', onSuccess, onError); + mitro_lib.PostToMitro(rpc, _makeArgs(transactionSpecificData), '/mitro-core/api/DeleteGroup', onSuccess, onError); } catch (e) { console.log(e.stack); - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; var processListSites = function(response) { - var output = mitro.fe.convertListSitesToExtension(response); + var output = convertListSitesToExtension(response); // find our private group if we haven't already done it if (privateNonOrgGroupId === null) { @@ -1218,12 +1166,12 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.listSites = function(transactionSpecificData, onSuccess, onError) { var args = _makeArgs(transactionSpecificData); - (wrapWithFailover(mitro.lib.ListGroupsAndSecrets))(args, function(response) { + (wrapWithFailover(mitro_lib.ListGroupsAndSecrets))(args, function(response) { try { onSuccess(processListSites(response)); } catch (e) { console.log('Error:', e); - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }, onError); }; @@ -1294,7 +1242,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.listGroups = function(transactionSpecificData, onSuccess, onError) { var args = _makeArgs(transactionSpecificData); - (wrapWithFailover(mitro.lib.ListGroupsAndSecrets))(args, function(response) { + (wrapWithFailover(mitro_lib.ListGroupsAndSecrets))(args, function(response) { onSuccess(processListGroups(response)); }, onError); }; @@ -1306,14 +1254,14 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.listUsers = function(transactionSpecificData, onSuccess, onError) { var args = _makeArgs(transactionSpecificData); - (wrapWithFailover(mitro.lib.ListGroupsAndSecrets))(args, function(response) { + (wrapWithFailover(mitro_lib.ListGroupsAndSecrets))(args, function(response) { onSuccess(processListUsers(response)); }, onError); }; obj.holdingTransaction.listUsersGroupsAndSecrets = function(transactionSpecificData, onSuccess, onError) { var args = _makeArgs(transactionSpecificData); - (wrapWithFailover(mitro.lib.ListGroupsAndSecrets))(args, function(response) { + (wrapWithFailover(mitro_lib.ListGroupsAndSecrets))(args, function(response) { try { var out = { users: processListUsers(response), @@ -1323,7 +1271,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port }; onSuccess(out); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }, onError); }; @@ -1343,21 +1291,21 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.holdingTransaction.shareSiteAndOptionallySetOrg(transactionSpecificData, secretId, newGroupIds, secret.users, orgGroupId, onSuccess, onError); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } - + }, onError); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; obj.holdingTransaction.getSiteSecretData = function(transactionSpecificData, secretId, onSuccess, onError) { return obj.holdingTransaction.getSiteData(transactionSpecificData, secretId, 'true', onSuccess, onError); }; - + obj.holdingTransaction.getSiteSecretDataForDisplay = function(transactionSpecificData, secretId, onSuccess, onError) { return obj.holdingTransaction.getSiteData(transactionSpecificData, secretId, 'display', onSuccess, onError); }; @@ -1367,11 +1315,11 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port // includeCriticalData is now a string "enum", not a boolean includeCriticalData = includeCriticalData.toString(); assert (includeCriticalData in {'true':1, 'false':1, 'display':1}); - + var args = _makeArgs(transactionSpecificData); args._ = [null, secretId]; args.includeCriticalData = includeCriticalData; - (wrapWithFailover(mitro.lib.GetSecret))(args, function(response) { + (wrapWithFailover(mitro_lib.GetSecret))(args, function(response) { var secret = {}; secret.secretId = response.secretId; secret.clientData = JSON.parse(response.clientData); @@ -1433,7 +1381,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port onSuccess(auditResponse); }; - mitro.lib.GetAuditLog(args, function (auditResponse) { + mitro_lib.GetAuditLog(args, function (auditResponse) { if (data.orgId === null) { obj.holdingTransaction.listSites(transactionSpecificData, function(secrets) { obj.holdingTransaction.listGroups(transactionSpecificData, function (groups) { @@ -1463,7 +1411,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port obj.mitroclient.createOrganization( request.name, request.owners, request.members, function() { - mitro.lib.clearCaches(); + mitro_lib.clearCaches(); var oldArgs = Array.prototype.slice.call(arguments); // This list groups command refreshes the user's org id. Do not delete this line! obj.holdingTransaction.listGroups(transactionSpecificData, function() { @@ -1518,9 +1466,9 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port oldCriticalData: oldCriticalData }; - mitro.lib.PostToMitroAgent(agentRequest, '/ChangePassword', onSuccess, onError); + mitro_lib.PostToMitroAgent(agentRequest, '/ChangePassword', onSuccess, onError); } catch(e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }, onError); }; @@ -1560,7 +1508,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port assert (isFunction(oldOnSuccess)); var mitroArgs = _makeArgs(); - + assert (mitroArgs.transactionSpecificData === undefined); var transactionSpecificData = {id:null, isWriteOperation: isWriteOperation, @@ -1569,8 +1517,8 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port var onSuccess = function(successResponse) { var afterEndTxn = function() { if (isWriteOperation) { - mitro.lib.clearCaches(); - mitro.lib.postEndTransaction(transactionSpecificData); + mitro_lib.clearCaches(); + mitro_lib.postEndTransaction(transactionSpecificData); } oldOnSuccess(successResponse); }; @@ -1579,7 +1527,7 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port if (transactionSpecificData.id) { if (!transactionSpecificData.implicitEndTransaction) { console.log('trying to close transaction with data:', transactionSpecificData); - mitro.lib.PostToMitro({}, _makeArgs(transactionSpecificData), '/mitro-core/api/EndTransaction', afterEndTxn, onError); + mitro_lib.PostToMitro({}, _makeArgs(transactionSpecificData), '/mitro-core/api/EndTransaction', afterEndTxn, onError); } else { console.log('transaction already closed: ', transactionSpecificData); afterEndTxn(); @@ -1589,15 +1537,15 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port var onError = function(e) { console.log('ERROR IN TRANSACTION CODE:', e); // TODO: rollback transaction - mitro.lib.clearCaches(); - mitro.lib.postEndTransaction(transactionSpecificData); + mitro_lib.clearCaches(); + mitro_lib.postEndTransaction(transactionSpecificData); // attempt to send log to server. var a = _makeArgs(); a.type = 'exception'; a.description = JSON.stringify(e); a.email = email; a.url = ""; - //mitro.lib.AddIssue(a, function() {}, function() {}); + //mitro_lib.AddIssue(a, function() {}, function() {}); oldOnError(e); }; wrappedFcnArgs.unshift(transactionSpecificData); @@ -1614,23 +1562,23 @@ function _make(email, verified, unsignedToken, privateKey, changePwd, host, port return obj; } -fe.setFailover = function(host, port) { +export function setFailover(host, port) { FAILOVER_MITRO_HOST = host; FAILOVER_MITRO_PORT = port; -}; -fe.setDeviceId = function(deviceId) { +} + +export function setDeviceId(deviceId) { DEVICE_ID = deviceId; -}; +} -fe.getDeviceId = function() { +export function getDeviceId() { return DEVICE_ID; }; -fe.getDeviceIdAsync = function(onSuccess) { +export function getDeviceIdAsync(onSuccess) { onSuccess(DEVICE_ID); }; - /** * This function wraps login and create calls and returns only clone-able objects. * The key is retained in the identities object and is never sent back to the client. @@ -1650,9 +1598,9 @@ var wrapLogin = function(wrappedFcn) { var token = JSON.stringify({'email' : identity.uid, 'nonce' : Math.random().toString(36).substring(7)}); var signature = identity.signMessage(token); - var url_path = "/mitro-core/TwoFactorAuth/TFAPreferences?user=" + - encodeURIComponent(identity.getUid()) + "&token=" + - encodeURIComponent(token) + "&signature=" + + var url_path = "/mitro-core/TwoFactorAuth/TFAPreferences?user=" + + encodeURIComponent(identity.getUid()) + "&token=" + + encodeURIComponent(token) + "&signature=" + encodeURIComponent(signature); @@ -1667,13 +1615,13 @@ var wrapLogin = function(wrappedFcn) { wrappedFcnArgs.push(oldOnError); wrappedFcn.apply(undefined, wrappedFcnArgs); } catch (e) { - oldOnError(mitro.lib.makeLocalException(e)); + oldOnError(mitro_lib.makeLocalException(e)); } }; }; -fe.workerInvokeOnIdentity = function() { +export function workerInvokeOnIdentity() { var args = Array.prototype.slice.call(arguments); var onError = args[args.length-1]; try { @@ -1683,7 +1631,7 @@ fe.workerInvokeOnIdentity = function() { // Unnecessary to initialize to undefined var appliedThisPointer; identities[identityId.identityId][operation].apply(appliedThisPointer, args); - fe.getRandomness(function(data) { + getRandomness(function(data) { forge.random.collect(data.seed); console.log('added randomness...'); }, function() { @@ -1691,26 +1639,21 @@ fe.workerInvokeOnIdentity = function() { }); } catch (e) { - onError(mitro.lib.makeLocalException(e)); + onError(mitro_lib.makeLocalException(e)); } }; -fe.workerLogout = function(identityObject) { +export function workerLogout(identityObject) { delete identities[identityObject.identityId]; }; -fe.createIdentity = createIdentity; -fe.workerCreateIdentity = wrapLogin(createIdentity); -fe.workerLogin = wrapLogin(login); -fe.workerLoginWithToken = wrapLogin(loginWithToken); -fe.workerLoginWithTokenAndLocalKey = wrapLogin(loginWithTokenAndLocalKey); - -fe.login = login; - -fe.loginWithToken = loginWithToken; -fe.loginWithTokenAndLocalKey = loginWithTokenAndLocalKey; -fe.addIssue = addIssue; -fe.keyCache = keyCache; -fe.clearCaches = mitro.lib.clearCaches; -fe.bidirectionalSetDiff = bidirectionalSetDiff; -})(); + +const workerCreateIdentity = wrapLogin(createIdentity); +const workerLogin = wrapLogin(login); +const workerLoginWithToken = wrapLogin(loginWithToken); +const workerLoginWithTokenAndLocalKey = wrapLogin(loginWithTokenAndLocalKey); + +export { workerCreateIdentity, workerLogin, workerLoginWithToken, + workerLoginWithTokenAndLocalKey, login, loginWithToken, + loginWithTokenAndLocalKey, addIssue, keyCache, clearCaches, + bidirectionalSetDiff } diff --git a/api/js/cli/mitro_legacyapi.js b/api/js/cli/mitro_legacyapi.js index 6ef017c..6d0a036 100644 --- a/api/js/cli/mitro_legacyapi.js +++ b/api/js/cli/mitro_legacyapi.js @@ -1,160 +1,124 @@ +// @flow /* - * ***************************************************************************** - * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. - * Authors: - * Vijay Pandurangan (vijayp@mitro.co) - * Evan Jones (ej@mitro.co) - * Adam Hilss (ahilss@mitro.co) - * - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * You can contact the authors at inbound@mitro.co. - * ***************************************************************************** - */ - -/** @suppress{duplicate} */ -var mitro = mitro || {}; - -/** -@interface -*/ -mitro.CryptoKey = function() {}; -/** -@return {string} -*/ -mitro.CryptoKey.prototype.toJson = function() {}; -/** -@param {string} data -@return {string} -*/ -mitro.CryptoKey.prototype.encrypt = function(data) {}; -/** -@return {mitro.CryptoKey} +* ***************************************************************************** +* Copyright (c) 2012, 2013, 2014 Lectorius, Inc. +* Authors: +* Vijay Pandurangan (vijayp@mitro.co) +* Evan Jones (ej@mitro.co) +* Adam Hilss (ahilss@mitro.co) +* +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* You can contact the authors at inbound@mitro.co. +* ***************************************************************************** */ -mitro.CryptoKey.prototype.exportPublicKey = function() {}; + +export interface CryptoKey { + toJson(): string; + encrypt(data: string): string; + exportPublicKey(): CryptoKey; +} /** Opaque object representing the current transaction. Do not look inside. -@interface */ -mitro.Transaction = function() {}; +export class Transaction {}; /** @constructor @struct */ -mitro.AclRpc = function() { - /** @type{string} */ - this.level = ''; - /** @type{string} */ - this.groupKeyEncryptedForMe = ''; - /** @type{string} */ - this.myPublicKey = ''; - /** @type {?number} */ - this.memberGroup = null; - /** @type {?string} */ - this.memberIdentity = null; -}; +export class AclRpc { + level: string; + groupKeyEncryptedForMe: string; + myPublicKey: string; + memberGroup: ?number; + memberIdentity: ?string; +} /** Actually called AddGroupRequest in Java; seems like a bad name? -@constructor -@struct */ -mitro.GroupRpc = function() { - /** @type{string} */ - this.name = ''; - /** @type{string} */ - this.publicKey = ''; - /** @type{string} */ - this.signatureString = ''; - /** @type{string} */ - this.scope = ''; - /** @type{boolean} */ - this.autoDelete = false; - /** @type {!Array.} */ - this.acls = []; +export class GroupRpc { + name: string; + publicKey: string; + signatureString: string; + scope: string; + autoDelete: boolean; + acls: Array; }; /** JavaScript client arguments to mutate organization. -@constructor -@struct */ -mitro.MutateOrganizationClientRequest = function() { - /** id of the organization to modify. @type {number} */ - this.orgId = 0; - /** New or existing members who will get admin priviledges. - @type {!Array.} */ - this.membersToPromote = []; +export class MutateOrganizationClientRequest { + orgId: number; + // New or existing members who will get admin priviledges. + membersToPromote: Array; /** New members to be added. Must not be members already. @type {!Array.} */ - this.newMembers = []; + newMembers: Array; /** Admins who will get admin privledges removed. @type {!Array.} */ - this.adminsToDemote = []; + adminsToDemote: Array; /** Members to be removed. @type {!Array.} */ - this.membersToRemove = []; + membersToRemove: Array; }; /** Performs operations with the old API. @interface */ -mitro.LegacyAPI = function() {}; - -/** -Gets public keys for users. -@param {!Array.} identities email addresses of users to fetch. -@param {mitro.Transaction} transaction transaction this request belongs to (or null). -@param {function(!Object.)} onSuccess called with address -> key mappings. -@param {function(!Error)} onError called with an error if anything fails. -*/ -mitro.LegacyAPI.prototype.getPublicKeys = function(identities, transaction, onSuccess, onError) {}; -/** -@param {string} jsonString -@return {!mitro.CryptoKey} -*/ -mitro.LegacyAPI.prototype.cryptoLoadFromJson = function(jsonString) {}; -/** -@param {string} path -@param {!Object} request -@param {mitro.Transaction} transaction transaction this request belongs to (or null). -@param {function(!Object)} onSuccess -@param {function(!Error)} onError -*/ -mitro.LegacyAPI.prototype.postSigned = function(path, request, transaction, onSuccess, onError) {}; -/** -@param {number} count -@param {function(!Array.)} onSuccess -@param {function(!Error)} onError -*/ -mitro.LegacyAPI.prototype.getNewRSAKeysAsync = function(count, onSuccess, onError) {}; -/** -@param {number} groupId -@param {mitro.Transaction} transaction transaction this request belongs to (or null). -@param {function(mitro.GroupRpc)} onSuccess -@param {function(!Error)} onError -*/ -mitro.LegacyAPI.prototype.getGroup = function(groupId, transaction, onSuccess, onError) {}; -/** -@return {string} -*/ -mitro.LegacyAPI.prototype.getIdentity = function() {}; -/** -@param {string} ciphertext -@return {string} -*/ -mitro.LegacyAPI.prototype.decrypt = function(ciphertext) {}; - -if (typeof module !== 'undefined' && module.exports) { - module.exports = mitro; -} +export interface LegacyAPI { + /** + Gets public keys for users. + @param {!Array.} identities email addresses of users to fetch. + @param {mitro.Transaction} transaction transaction this request belongs to (or null). + @param {function(!Object.)} onSuccess called with address -> key mappings. + @param {function(!Error)} onError called with an error if anything fails. + */ + getPublicKeys(identities: Array, transaction: Transaction, onSuccess: any, onError: any): void; + /** + @param {string} jsonString + @return {!mitro.CryptoKey} + */ + cryptoLoadFromJson(jsonString: string): CryptoKey; + /** + @param {string} path + @param {!Object} request + @param {mitro.Transaction} transaction transaction this request belongs to (or null). + @param {function(!Object)} onSuccess + @param {function(!Error)} onError + */ + postSigned(path: string, request: Object, transaction: Transaction, onSuccess: any, onError: any): void; + /** + @param {number} count + @param {function(!Array.)} onSuccess + @param {function(!Error)} onError + */ + getNewRSAKeysAsync(count: number, onSuccess: (Array) => void, onError: any): void; + /** + @param {number} groupId + @param {mitro.Transaction} transaction transaction this request belongs to (or null). + @param {function(mitro.GroupRpc)} onSuccess + @param {function(!Error)} onError + */ + getGroup(groupId: number, transaction: Transaction, onSuccess: any, onError: any): any; + /** + @return {string} + */ + getIdentity(): string; + /** + @param {string} ciphertext + @return {string} + */ + decrypt(ciphertext: string): string; +} \ No newline at end of file diff --git a/api/js/cli/mitro_lib.js b/api/js/cli/mitro_lib.js index e46cbdd..5fd9756 100644 --- a/api/js/cli/mitro_lib.js +++ b/api/js/cli/mitro_lib.js @@ -1,3 +1,4 @@ +// @flow /* * ***************************************************************************** * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. @@ -24,105 +25,79 @@ * ***************************************************************************** */ -(function() { -// define mitro -if(typeof(window) !== 'undefined') { - if (typeof(mitro) === 'undefined') {mitro = window.mitro = window.mitro || {};} - mitro.lib = {}; -} -// define node.js module -else if(typeof(module) !== 'undefined' && module.exports) { - getExtensionId = function() { return 'node_extension_id';}; - mitro = { - crypto : require('./crypto.js'), - crappycrypto : require('./crappycrypto.js'), - keycache: require('./keycache'), - rpc : require('./rpc.js'), - cache: require('./lru_cache.js'), - log: require('./logging.js') - }; - module.exports = mitro.lib = {}; -} +import { assert } from "./assert"; +import * as lru_cache from "./lru_cache"; +import * as crypto from "./crypto"; +import * as rpc from "./rpc"; +import * as config from "../../../login/chrome/config/config"; +import { getExtensionId } from "../../../login/common/worker"; +import * as KeyCache from "./keycache.js"; +import { GroupInfo, AuditAction } from "../../../login/frontend/static/js/background_interface"; -var globalDecryptionCache = new mitro.cache.LRUCache(1024); -var txnSpecificCaches = {}; +const globalDecryptionCache = new lru_cache.LRUCache(1024); +const txnSpecificCaches = {}; -var makeLocalException = function (e) { +function makeLocalException (e: Error) { try { console.log('local exception:', e, e.stack); } catch (ee) {} - var output = {status: -1, - userVisibleError: 'Unknown local error', - exceptionType: 'JavascriptException', - local_exception: e}; + var output = { + status: -1, + userVisibleError: 'Unknown local error', + exceptionType: 'JavascriptException', + local_exception: e + }; if (e.userVisibleError) { - output.userVisibleError = e.userVisibleError; + output.userVisibleError = "" + (e.userVisibleError : any); } return output; }; // cache things for ~ 1 minute. -var CACHE_TIME_MS = 1000*60*1; - -var _getCache = function(args) { +const CACHE_TIME_MS = 1000*60*1; +const GENERAL_TRANSACTION = 'general-transaction' +function _getCache(args) { if (args._transactionSpecificData && args._transactionSpecificData.isWriteOperation && args._transactionSpecificData.id) { if (!(args._transactionSpecificData.id in txnSpecificCaches)) { - txnSpecificCaches[args._transactionSpecificData.id] = new mitro.cache.LRUCache(); + txnSpecificCaches[args._transactionSpecificData.id] = new lru_cache.LRUCache(); } return txnSpecificCaches[args._transactionSpecificData.id]; } else { - if (txnSpecificCaches[null] === undefined) { - txnSpecificCaches[null] = new mitro.cache.LRUCache(1024); + if (txnSpecificCaches[GENERAL_TRANSACTION] === undefined) { + txnSpecificCaches[GENERAL_TRANSACTION] = new lru_cache.LRUCache(1024); } - return txnSpecificCaches[null]; + return txnSpecificCaches[GENERAL_TRANSACTION]; } }; -var postEndTransaction = function(transactionSpecificData) { +type TransactionSpecificData = any // todo + +function postEndTransaction(transactionSpecificData: TransactionSpecificData) { delete txnSpecificCaches[transactionSpecificData.id]; }; -var clearCacheAndCall = function(f) { - return function() { +var clearCacheAndCall = function(f: any) { + return function(...rest: any) { console.log('mitro_lib: clearing global cache'); - delete txnSpecificCaches[null]; + delete txnSpecificCaches[GENERAL_TRANSACTION]; f.apply(null, Array.prototype.slice.call(arguments)); }; }; -var crypto = mitro.crypto; -var initForTest = function() { - if (!mitro.crappycrypto) { - throw new Error('crappycrypto does not exist?'); - } - crypto = mitro.crappycrypto; - mitro.keycache.useCrappyCrypto(); -}; - -var getCrypto = function() { - return crypto; -}; - -var lib = mitro.lib; -var assert = function(expression) { - if (!expression) { - throw new Error('Assertion failed'); - } -}; // General code for all modules -var PostToMitro = function(outdict, args, path, onSuccess, onError) { - +function PostToMitro(outdict: Object, args: Object, path: string, onSuccess: any, onError: any) { + // include the device id in the signed portion of the request outdict.deviceId = args.deviceId; - var message = { + const message: any = { 'identity': args.uid, 'request': JSON.stringify(outdict) }; if (args._transactionSpecificData) { - message.operationName = args._transactionSpecificData.operationName; + message.operationName = args._transactionSpecificData.operationName; message.transactionId = args._transactionSpecificData.id; message.implicitEndTransaction = args._transactionSpecificData.implicitEndTransaction; if (args._transactionSpecificData.implicitBeginTransaction) { @@ -136,7 +111,7 @@ var PostToMitro = function(outdict, args, path, onSuccess, onError) { message.signature = args._privateKey.sign(message.request); } - return mitro.rpc._PostToMitro(message, args, path, function(resp) { + return rpc._PostToMitro(message, args, path, function(resp) { if (args._transactionSpecificData && !args._transactionSpecificData.id) { args._transactionSpecificData.id = resp.transactionId; } @@ -144,15 +119,16 @@ var PostToMitro = function(outdict, args, path, onSuccess, onError) { }, onError); }; -var PostToMitroAgent = function(request, path, onSuccess, onError) { +// TODO(tom): we can probably do better for types of onSuccess and onError +function PostToMitroAgent(request: Object, path: string, onSuccess: any, onError: any) { var args = { - server_host: MITRO_AGENT_HOST, - server_port: MITRO_AGENT_PORT + server_host: config.MITRO_AGENT_HOST, + server_port: config.MITRO_AGENT_PORT, }; - return mitro.rpc._PostToMitro(request, args, path, onSuccess, onError); + return rpc._PostToMitro(request, args, path, onSuccess, onError); }; -var setPostToMitroForTest = function(replacementFunction) { +function setPostToMitroForTest(replacementFunction: any) { PostToMitro = replacementFunction; }; @@ -161,34 +137,38 @@ var setPostToMitroForTest = function(replacementFunction) { // takes > 1 year to crack, so that seems like a reasonable rule? // TODO: Enforce mixed-case, numeric, or special char rules? // http://blog.agilebits.com/2013/04/16/1password-hashcat-strong-master-passwords/ -var MIN_PASSWORD_LENGTH = 8; +const MIN_PASSWORD_LENGTH = 8; + +type EncryptedPrivateKey = any // TODO -var EditEncryptedPrivateKey = function(args, up, newEncryptedPrivateKey, onSuccess, onError) { +function EditEncryptedPrivateKey(args: any, up: any, newEncryptedPrivateKey: EncryptedPrivateKey, onSuccess: any, onError: any) { try { - var request = { - userId: args.uid, encryptedPrivateKey: newEncryptedPrivateKey, tfaToken: up.token, tfaSignature: up.token_signature - }; - assert (newEncryptedPrivateKey); - PostToMitro(request, - args, '/mitro-core/api/EditEncryptedPrivateKey', onSuccess, onError); + var request = { + userId: args.uid, encryptedPrivateKey: newEncryptedPrivateKey, tfaToken: up.token, tfaSignature: up.token_signature + }; + assert (newEncryptedPrivateKey); + PostToMitro(request, + args, '/mitro-core/api/EditEncryptedPrivateKey', onSuccess, onError); } catch(e) { onError(makeLocalException(e)); } -}; +} +type OnSuccess = any +type OnError = any -var checkTwoFactor = function (args, onSuccess, onError) { - try { - var request = { - userId: args.uid, - extensionId: getExtensionId() - }; +function checkTwoFactor(args: any, onSuccess: OnSuccess, onError: OnError) { + try { + var request = { + userId: args.uid, + extensionId: getExtensionId() + }; PostToMitro(request, args, '/mitro-core/api/ChangePwdTwoFactorRequired', onSuccess, onError); - } - catch (e) { - onError(makeLocalException(e)); - } -}; + } + catch (e) { + onError(makeLocalException(e)); + } +} /** @@ -200,7 +180,7 @@ var checkTwoFactor = function (args, onSuccess, onError) { * callback to call; response contains privateKey and transaction id * */ -var AddIdentity = clearCacheAndCall(function(args, onSuccess, onError) { +const AddIdentity = clearCacheAndCall(function(args, onSuccess, onError) { try { console.log('>>Add Identity'); // uid must be email, password must be long enough @@ -216,7 +196,7 @@ var AddIdentity = clearCacheAndCall(function(args, onSuccess, onError) { var privateKey = keys[0]; var groupKey = keys[1]; // generate a key; encrypt it - var request = { + var request: any = { userId: args.uid, publicKey: privateKey.exportPublicKey().toJson(), encryptedPrivateKey: privateKey.toJsonEncrypted(args.password), @@ -247,7 +227,7 @@ var AddIdentity = clearCacheAndCall(function(args, onSuccess, onError) { /** * AddGroup -- add a new group to the DB, and add me to it. * Args: - * args: + * args: * { uid : user id of the actor (string) * _privateKey: an initialized Private Key object from crypto. * '_' : [ name (string)]}, @@ -255,7 +235,7 @@ var AddIdentity = clearCacheAndCall(function(args, onSuccess, onError) { * onSuccess: function(response) * callback to call; response = {groupId:int}; * - */ + */ var AddGroup = clearCacheAndCall(function(args, onSuccess, onError) { try { args._keyCache.getNewRSAKeysAsync(1, function(keys) { @@ -294,11 +274,11 @@ var AddGroup = clearCacheAndCall(function(args, onSuccess, onError) { /** * TODO: this is broken and should use a different field for the actor and the public key - * + * * GetPublicKey -- get a specific user's public key * Args: - * args: - * + * args: + * * { uid : user id of the calling user. * target_uid: user id whose public key you want (string) * _privateKey: an initialized Private Key object from crypto. @@ -306,39 +286,41 @@ var AddGroup = clearCacheAndCall(function(args, onSuccess, onError) { * onSuccess: function(response) * callback to call; response = {myUserId: int, publicKey: string} * - */ -var GetPublicKey = function(args, onSuccess, onError) { + */ +function GetPublicKey(args: Object, onSuccess: OnSuccess, onError: OnError) { var uids = [args.target_uid]; GetPublicKeys(args, uids, - function(response) { - // TODO: this is kind of ugly. We should fix this code eventually - response.publicKey = response.userIdToPublicKey[uids[0]]; - response.myUserId = uids[0]; - onSuccess(response); - }, onError); -}; + function(response) { + // TODO: this is kind of ugly. We should fix this code eventually + response.publicKey = response.userIdToPublicKey[uids[0]]; + response.myUserId = uids[0]; + onSuccess(response); + }, onError); +} + +type Identities = any // TODO -var GetPublicKeys = function(args, uids, onSuccess, onError) { +function GetPublicKeys(args: Object, uids: Identities, onSuccess: OnSuccess, onError: OnError) { return GetUserAndGroupPublicKeys(args, args.addMissingUsers, uids, null, onSuccess, onError); }; -var GetUserAndGroupPublicKeys = function(args, addMissingUsers, uids, gids, onSuccess, onError) { +function GetUserAndGroupPublicKeys(args: Object, addMissingUsers: bool, uids: Identities, gids: ?Array, onSuccess: OnSuccess, onError: OnError) { try { console.log('>>Get public key'); var request = {userIds : uids, addMissingUsers: addMissingUsers, groupIds: gids}; - PostToMitro(request, args, '/mitro-core/api/GetPublicKeyForIdentity', - function(r) { - //TODO: pass this back so we can prompt users - assert (!r.missingUsers || r.missingUsers.length === 0); - onSuccess(r); - }, onError); - } catch (e) { + PostToMitro(request, args, '/mitro-core/api/GetPublicKeyForIdentity', + function(r) { + //TODO: pass this back so we can prompt users + assert (!r.missingUsers || r.missingUsers.length === 0); + onSuccess(r); + }, onError); + } catch (e) { console.log(e.stack); onError(makeLocalException(e)); } -}; +} -var RetrieveDeviceSpecificKey = function(args, onSuccess, onError) { +function RetrieveDeviceSpecificKey(args: Object, onSuccess: OnSuccess, onError: OnError) { try { console.log('>>Retrieve Device key'); assert(args.uid); @@ -357,7 +339,7 @@ var RetrieveDeviceSpecificKey = function(args, onSuccess, onError) { * args {uid: user id} * calls onSuccess with object from RPC.java */ -var GetPrivateKey = function(args, onSuccess, onError) { +function GetPrivateKey(args: Object, onSuccess: OnSuccess, onError: OnError) { try { console.log('>>Get private key'); assert(args.uid); @@ -385,10 +367,10 @@ var decryptSecretWithKeyString = function(secret, keyString, privateKeyObject) { return secret; }; -var decryptSecretWithGroups = function(secret, groups, previousUnencryptedKey) { +function decryptSecretWithGroups(secret: SecretToPath, groups: Array, previousUnencryptedKey) { assert(secret); var path = secret.groupIdPath; - for (var pathId in path) { + for (let pathId of path) { var groupId = path[pathId]; if (!groups[groupId]) { console.log("ERROR: could not get group info for (group)", groupId); @@ -420,9 +402,9 @@ var _decryptSecret = function(secretId, listGroupAndSecretsResp, userPrivateKey) * args {uid: userId, '_':['secret_id'], _privateKey: key object} * calls onSuccess with a secret object, with .criticalData and .clientData set */ -var GetSecret = function(args, onSuccess, onError) { +function GetSecret(args: Object, onSuccess: OnSuccess, onError: OnError) { try { - // This is a bit complicated. First we have to list groups and secrets, then decrypt the + // This is a bit complicated. First we have to list groups and secrets, then decrypt the // chain of group keys, then decrypt the secret, by re-requesting it, requesting the critical data. // TODO: cache this. assert(onSuccess); @@ -432,7 +414,7 @@ var GetSecret = function(args, onSuccess, onError) { } var cacheKey = null; if (!includeCriticalData) { - cacheKey = mitro.cache.makeKey('GetSecret', args.uid, args._[1]); + cacheKey = lru_cache.makeKey('GetSecret', args.uid, args._[1]); var resp = _getCache(args).getItem(cacheKey); if (resp) { console.log('mitro_lib GetSecret: Found response in cache'); @@ -482,19 +464,19 @@ var GetSecret = function(args, onSuccess, onError) { }; /** - * ListGroupsAndSecrets - * + * ListGroupsAndSecrets + * * args {uid: userId, _privateKey: key object} - * + * * calls onSuccess with object in RPC.java, but with unencrypted .clientData * added to every secret. */ -var ListGroupsAndSecrets = function(args, onSuccess, onError) { +function ListGroupsAndSecrets(args: Object, onSuccess: OnSuccess, onError: OnError) { try { - var resp = _getCache(args).getItem(mitro.cache.makeKey('ListGroupsAndSecrets', args.uid)); + var resp = _getCache(args).getItem(lru_cache.makeKey('ListGroupsAndSecrets', args.uid)); if (resp) { console.log('mitro_lib ListGroupsAndSecrets: Found response in cache'); - (onSuccess || mitro.rpc.DefaultResponseHandler)(JSON.parse(resp)); + (onSuccess || rpc.DefaultResponseHandler)(JSON.parse(resp)); return; // IMPORTANT DO NOT REMOVE } console.log('>>Get List Groups and Secrets'); @@ -502,15 +484,15 @@ var ListGroupsAndSecrets = function(args, onSuccess, onError) { var request = {myUserId : args.uid}; PostToMitro(request, args, '/mitro-core/api/ListMySecretsAndGroupKeys', function(resp) { var secretIds = Object.keys(resp.secretToPath); - for (var i in secretIds) { - _decryptSecret(secretIds[i], resp, args._privateKey); + for (let x of secretIds) { + _decryptSecret(x, resp, args._privateKey); } // Cache for one minute - _getCache(args).setItem(mitro.cache.makeKey('ListGroupsAndSecrets', args.uid), + _getCache(args).setItem(lru_cache.makeKey('ListGroupsAndSecrets', args.uid), // TODO: this should actually be a deep copy but I have no idea how to do that in JS. JSON.stringify(resp), {expirationAbsolute: new Date(new Date().getTime() + CACHE_TIME_MS)}); - (onSuccess || mitro.rpc.DefaultResponseHandler)(resp); + (onSuccess || rpc.DefaultResponseHandler)(resp); }, onError ); @@ -522,14 +504,14 @@ var ListGroupsAndSecrets = function(args, onSuccess, onError) { }; -var GetOrganizationState = function(args, postArgs, onSuccess, onError) { +function GetOrganizationState(args: Object, postArgs: Object, onSuccess: OnSuccess, onError: OnError) { var path = '/mitro-core/api/GetOrganizationState'; - var key = mitro.cache.makeKey(path, postArgs.orgId); + var key = lru_cache.makeKey(path, postArgs.orgId); var resp = _getCache(args).getItem(key); if (resp) { onSuccess(JSON.parse(resp)); } else { - mitro.lib.PostToMitro(postArgs, args, path, function(resp) { + PostToMitro(postArgs, args, path, function(resp) { _getCache(args).setItem(key, JSON.stringify(resp), {expirationAbsolute: new Date(new Date().getTime() + CACHE_TIME_MS)}); onSuccess(resp); @@ -539,7 +521,8 @@ var GetOrganizationState = function(args, postArgs, onSuccess, onError) { // mutationFunction MUST RETURN TRUE if secrets are to be re-encrypted with // a new key -var MutateMembership = clearCacheAndCall(function(args, mutationFunction, onSuccess, onError) { +const MutateMembership = clearCacheAndCall( + function(args, mutationFunction, onSuccess: OnSuccess, onError: OnError) { try { args.orgId = parseInt(args.orgId, 10); assert(onSuccess); @@ -559,8 +542,7 @@ var MutateMembership = clearCacheAndCall(function(args, mutationFunction, onSucc unencryptedOldGroupKey = crypto.loadFromJson(args._privateKey.decrypt(acl.groupKeyEncryptedForMe)); } } - // - var doRest = function(unencryptedOldGroupKey) { + function doRest(unencryptedOldGroupKey: ?Object) { try { var reEncrypt = mutationFunction(group, unencryptedOldGroupKey); if (!reEncrypt) { @@ -579,9 +561,11 @@ var MutateMembership = clearCacheAndCall(function(args, mutationFunction, onSucc // re-encrypt all the secret data with the new key for (i in group.secrets) { group.secrets[i].encryptedClientData = newGroupKey.encrypt( + // $FlowFixMe unencryptedOldGroupKey.decrypt(group.secrets[i].encryptedClientData)); group.secrets[i].encryptedCriticalData = newGroupKey.encrypt( - unencryptedOldGroupKey.decrypt(group.secrets[i].encryptedCriticalData)); + // $FlowFixMe + unencryptedOldGroupKey.decrypt(group.secrets[i].encryptedCriticalData)); } group.publicKey = newGroupKey.exportPublicKey().toJson(); PostToMitro(group, args, '/mitro-core/api/EditGroup', clearCacheAndCall(onSuccess), onError); @@ -597,7 +581,7 @@ var MutateMembership = clearCacheAndCall(function(args, mutationFunction, onSucc if (unencryptedOldGroupKey === null && args.orgId) { // see if we can access this data through the top level org group (for admins) - + var groupAcl = null; for (i in group.acls) { groupAcl = group.acls[i]; @@ -615,8 +599,13 @@ var MutateMembership = clearCacheAndCall(function(args, mutationFunction, onSucc var orgAcl = org.acls[i]; // make sure we're not already in the ACL (no duplicates!) if (orgAcl.memberIdentity === args.uid) { + + if (groupAcl === null) { + throw (`groupacl for orgId ${args.orgId} not found`); + } + // it's me! - var unencryptedOrgGroupKey = crypto.loadFromJson(args._privateKey.decrypt(orgAcl.groupKeyEncryptedForMe)); + const unencryptedOrgGroupKey = crypto.loadFromJson(args._privateKey.decrypt(orgAcl.groupKeyEncryptedForMe)); unencryptedOldGroupKey = crypto.loadFromJson(unencryptedOrgGroupKey.decrypt(groupAcl.groupKeyEncryptedForMe)); } } @@ -634,13 +623,13 @@ var MutateMembership = clearCacheAndCall(function(args, mutationFunction, onSucc /** * AddMember - * - * args {uid: userId, gid: group_id, target_uid: target user, + * + * args {uid: userId, gid: group_id, target_uid: target user, * _privateKey: key object for me, - * _targetPublicKey: key object for the target, + * _targetPublicKey: key object for the target, * calls onSuccess with : see RPC.java */ -var AddMember = clearCacheAndCall(function(args, onSuccess, onError) { +var AddMember = clearCacheAndCall(function(args, onSuccess: OnSuccess, onError: OnError) { MutateMembership(args, function(group, unencryptedGroupKey) { assert(unencryptedGroupKey); group.acls.push( @@ -657,13 +646,13 @@ var AddMember = clearCacheAndCall(function(args, onSuccess, onError) { /** * RemoveMember - * - * args {uid: userId, gid: group_id, target_uid: target user, + * + * args {uid: userId, gid: group_id, target_uid: target user, * _privateKey: key object for me, - * _targetPublicKey: key object for the target, + * _targetPublicKey: key object for the target, * calls onSuccess with: see RPC.java */ -var RemoveMember = clearCacheAndCall(function(args, onSuccess, onError) { +var RemoveMember = clearCacheAndCall(function(args, onSuccess: OnSuccess, onError: OnError) { args.includeCriticalData = true; MutateMembership(args, function(group, unencryptedOldGroupKey) { var newacls = []; @@ -680,25 +669,71 @@ var RemoveMember = clearCacheAndCall(function(args, onSuccess, onError) { }, onSuccess, onError); }); + +// TODO(tom): move to RPC module +type Secret = { + secretId: number, + hostname: string, + encryptedClientData: string, + encryptedCriticalData: string, + groups: Array, + hiddenGroups: Array, + + users: Array, + icons: Array, + /** Maps group ids to group names to display them in the ACL. */ + groupMap: { [key: number]: GroupInfo }, + title: string, + + /** Group ID of the owning organization. This is immutable. */ + owningOrgId: number, + owningOrgName: string, + + lastModified: AuditAction, + lastAccessed: AuditAction, + king: string, + isViewable: boolean, + + canEditServerSecret: boolean, + groupIdToPublicKeyMap: { [key: number]: string }, + + // local unencrypted data: + clientData: any, + criticalData: any, +}; + +type SecretToPath = Secret & { groupIdPath: Array }; + +type Group = { + groupId: number; + secrets: Array; +}; + /** * GetGroup - * + * * args {uid: userId, gid: group_id * _privateKey: key object for me, * calls onSuccess with: see RPC.java */ -var GetGroup = function(args, onSuccess, onError) { +function GetGroup(args: Object, onSuccess: OnSuccess, onError: OnError) { try { assert(args.gid); - var request = {groupId : args.gid, userId: args.uid, includeCriticalData: args.includeCriticalData}; - var resp = _getCache(args).getItem(mitro.cache.makeKey('GetGroup', args.uid, args.gid, args.includeCriticalData)); + const request = { + groupId: args.gid, + userId: args.uid, + includeCriticalData: args.includeCriticalData, + }; + const resp = _getCache(args).getItem(lru_cache.makeKey('GetGroup', args.uid, args.gid, args.includeCriticalData)); + if (resp) { console.log('mitro_lib GetGroup: Found response in cache'); onSuccess(JSON.parse(resp)); return; // IMPORTANT DO NOT REMOVE } + PostToMitro(request, args, '/mitro-core/api/GetGroup', function(resp) { - _getCache(args).setItem(mitro.cache.makeKey('GetGroup', args.uid, args.gid, args.includeCriticalData), + _getCache(args).setItem(lru_cache.makeKey('GetGroup', args.uid, args.gid, args.includeCriticalData), JSON.stringify(resp), {expirationAbsolute: new Date(new Date().getTime() + CACHE_TIME_MS)}); onSuccess(resp); @@ -706,11 +741,10 @@ var GetGroup = function(args, onSuccess, onError) { } catch (e) { onError(makeLocalException(e)); } - }; -var AddSecrets = clearCacheAndCall(function(args, data, onSuccess, onError) { +var AddSecrets = clearCacheAndCall(function(args, data, onSuccess: OnSuccess, onError: OnError) { try { if (data.groupIds.length === 0) { onSuccess(); @@ -722,26 +756,29 @@ var AddSecrets = clearCacheAndCall(function(args, data, onSuccess, onError) { try { //Used to prevent creating a function within a loop - var messageFunction = function(response, onSuccess, onError){ + var messageFunction = function(response, onSuccess: OnSuccess, onError: OnError){ for (var j = 2; j < toRun.length; j++) { + // $FlowFixMe toRun[j][1][0].secretId = response.secretId; } onSuccess(); }; - + for (var i = 0; i < data.groupIds.length; ++i) { var gid = data.groupIds[i]; var publicKeyString = keys.groupIdToPublicKey[gid]; console.log("getting key for ", gid); assert(publicKeyString); - var groupPublicKey = crypto.loadFromJson(publicKeyString); - var secretId = data.secretId; // could be undefined, this is OK (!) + const groupPublicKey = crypto.loadFromJson(publicKeyString); + const secretId = data.secretId; // could be undefined, this is OK (!) assert (groupPublicKey); - var request = {myUserId: args.uid, ownerGroupId : gid, - encryptedClientData: groupPublicKey.encrypt(clientSecret), - encryptedCriticalData: groupPublicKey.encrypt(criticalSecret), - secretId: secretId - }; + const request = { + myUserId: args.uid, + ownerGroupId : gid, + encryptedClientData: groupPublicKey.encrypt(clientSecret), + encryptedCriticalData: groupPublicKey.encrypt(criticalSecret), + secretId: secretId + }; toRun.push([PostToMitro, [request, args, '/mitro-core/api/AddSecret']]); // if we are adding a new secret, set the secret id for the subsequent requests // TODO: Ideally this API should do this with one server request @@ -775,33 +812,33 @@ var AddSecrets = clearCacheAndCall(function(args, data, onSuccess, onError) { }); /** * AddSecret - * - * args {uid: userId, gid: group_id, + * + * args {uid: userId, gid: group_id, * _privateKey: key object for me, * secretId: If provided, adds an existing secret to a new group * '_' : ['hostname', 'client secret', 'critical secret'], - * + * * if chainedValue is set and group id is not, it is used in place of group id. * calls onSuccess with: see RPC.java */ -var AddSecret = clearCacheAndCall(function(args, gid, onSuccess, onError) { - +const AddSecret = clearCacheAndCall(function(args, gid: number, onSuccess: OnSuccess, onError: OnError) { try { - // this is a horrible hack. + // TODO(tom): what does this do? if (onError === undefined) { onError = onSuccess; onSuccess = gid; + // $FlowFixMe gid = undefined; } if (args.gid === undefined) { args.gid = gid; - console.log('set gid to ' + gid); + console.log('set gid to ' + gid ? gid : "undefined"); } assert(args.gid); - var wrappedOnSuccess = function(results) { + const wrappedOnSuccess = function(results) { try { onSuccess(results[0]); } catch (e) { @@ -815,10 +852,10 @@ var AddSecret = clearCacheAndCall(function(args, gid, onSuccess, onError) { console.log('usage: mitro.js add --uid=me@example.com --gid=21 HOSTNAME client critical'); process.exit(-1); } else { - AddSecrets(args, - {groupIds : [args.gid], + AddSecrets(args, + {groupIds : [args.gid], secretId : args.secretId, - clientData: args._[2], + clientData: args._[2], // TODO: WTF? criticalData: (args._.length === 4) ? args._[3] : null}, wrappedOnSuccess, onError); @@ -832,14 +869,14 @@ var AddSecret = clearCacheAndCall(function(args, gid, onSuccess, onError) { /** * RemoveSecret - * - * args {uid: userId, + * + * args {uid: userId, * gid: if provided, remove a secret from only this group * _privateKey: key object for me, * '_' : ['secret id'], * calls onSuccess with: see RPC.java */ -var RemoveSecret = clearCacheAndCall(function(args, onSuccess, onError) { +var RemoveSecret = clearCacheAndCall(function(args, onSuccess: OnSuccess, onError: OnError) { try { assert(args.uid); var secretId; @@ -863,18 +900,8 @@ var RemoveSecret = clearCacheAndCall(function(args, onSuccess, onError) { /** * AddIssue - report a new issue and add to the DB. - * Args: - * args: - * { url: url where the issue appeared, if any (string) - * type: type of issue (string) - * description: user description of issue (string) - * email : user id of the user reporting the issue (string) - * } - * onSuccess: function(response) - * callback to call - * - */ -var AddIssue = function(args, onSuccess, onError) { + */ +function AddIssue(args: Object, onSuccess: OnSuccess, onError: OnError) { try { console.log('>>Add Issue'); PostToMitro(args, args, '/mitro-core/api/AddIssue', onSuccess, onError); @@ -882,9 +909,9 @@ var AddIssue = function(args, onSuccess, onError) { console.log('>> exception in add issue'); onError(makeLocalException(e)); } -}; +} -var GetAuditLog = function (args, onSuccess, onError) { +function GetAuditLog(args: Object, onSuccess: OnSuccess, onError: OnError) { try { console.log('>>Get Audit Log'); var request = args; @@ -893,14 +920,14 @@ var GetAuditLog = function (args, onSuccess, onError) { console.log('>> exception in get audit log'); onError(makeLocalException(e)); } -}; +} -var runCommandWithPrivateKey = function(cmdFcn, argv, unencryptedPrivateKey, onSuccessIn, onErrorIn) { +function runCommandWithPrivateKey(cmdFcn: any, argv: any, unencryptedPrivateKey, onSuccessIn: any, onErrorIn: any) { - var onError = function(e) { - console.log('ERROR IN TRANSACTION CODE:', e.message, e.stack); - onErrorIn(mitro.lib.makeLocalException(e)); - }; + var onError = function(e) { + console.log('ERROR IN TRANSACTION CODE:', e.message, e.stack); + onErrorIn(makeLocalException(e)); + }; argv._privateKey = unencryptedPrivateKey; PostToMitro({}, argv, '/mitro-core/api/BeginTransaction', function(txResp) { @@ -939,15 +966,15 @@ var runCommandWithPrivateKey = function(cmdFcn, argv, unencryptedPrivateKey, onS } }, onError); return true; -}; +} -var runCommand = function(cmdFcn, argv, onSuccess, onError) { +var runCommand = function(cmdFcn: any, argv: Object, onSuccess: OnSuccess, onError: OnError) { var success = false; - onSuccess = onSuccess || mitro.rpc.DefaultResponseHandler; - onError = onError || mitro.rpc.DefaultErrorHandler; + onSuccess = onSuccess || rpc.DefaultResponseHandler; + onError = onError || rpc.DefaultErrorHandler; - argv._keyCache = argv._keyCache || mitro.keycache.MakeKeyCache(); + argv._keyCache = argv._keyCache || KeyCache.MakeKeyCache(); if (cmdFcn) { if (cmdFcn !== AddIdentity && cmdFcn !== GetPrivateKey) { // get the current user's private key and pass it along. @@ -967,7 +994,7 @@ var runCommand = function(cmdFcn, argv, onSuccess, onError) { }; -var parallel = function(fcnArgListTuple, onSuccess, onError) { +function parallel(fcnArgListTuple: Array, onSuccess: OnSuccess, onError: OnError) { if (fcnArgListTuple.length === 0) { setTimeout(function() {onSuccess([]);}, 0); return; @@ -1003,9 +1030,10 @@ var parallel = function(fcnArgListTuple, onSuccess, onError) { return function(response) {_success(counter, response);}; }; - for (var i in fcnArgListTuple) { - var myargs = fcnArgListTuple[i][1]; - if (fcnArgListTuple[i][2] === undefined || fcnArgListTuple[i][2] === undefined) { + let i = 0; + for (let x of fcnArgListTuple) { + var myargs = x[1]; + if (x[2] === undefined || x[2] === undefined) { myargs.push(makeCallback(i)); myargs.push(_error); } else { @@ -1013,7 +1041,8 @@ var parallel = function(fcnArgListTuple, onSuccess, onError) { myargs.push(_error); myargs.push(makeCallback(i)); } - fcnArgListTuple[i][0].apply(undefined, myargs); + i++; + x[0].apply(undefined, myargs); } }; @@ -1023,20 +1052,21 @@ var parallel = function(fcnArgListTuple, onSuccess, onError) { @param {function(Error)} onError @param {*=} chainedArg */ -var series = function(fcnArgListTuple, onSuccess, onError, chainedArg) { +function series(fcnArgListTuple: Array, onSuccess: OnSuccess, onError: OnError, chainedArg: any) { var labels = []; - for (var i in fcnArgListTuple) { - if (typeof(fcnArgListTuple[i][0]) === 'string' || typeof(fcnArgListTuple[i][0]) === 'number') { + for (let x of fcnArgListTuple) { + if (typeof(x[0]) === 'string' || typeof(x[0]) === 'number') { // assume this is a label - labels[i] = String(fcnArgListTuple[i][0]); - fcnArgListTuple[i].shift(); + labels.push(String(x[0])); + x.shift(); + } else { + labels.push(undefined); } } return _series(labels, fcnArgListTuple, onSuccess, onError, chainedArg); -}; - +} -var _series = function(labels, fcnArgListTuple, onSuccess, onError, chainedArg) { +function _series(labels: Array, fcnArgListTuple: Array, onSuccess: OnSuccess, onError: OnError, chainedArg) { if (fcnArgListTuple.length === 0) { setTimeout(function() {onSuccess([]);}, 0); return; @@ -1068,12 +1098,12 @@ var _series = function(labels, fcnArgListTuple, onSuccess, onError, chainedArg) onSuccess(rvals, rvalMap); } }; - + var _error = onError; - for (var i in fcnArgListTuple) { - var myargs = fcnArgListTuple[i][1]; - if (fcnArgListTuple[i][2] === undefined || fcnArgListTuple[i][2] === undefined) { + for (let x of fcnArgListTuple) { + var myargs = x[1]; + if (x[2] === undefined || x[2] === undefined) { myargs.push(_success); myargs.push(_error); } else { @@ -1081,15 +1111,15 @@ var _series = function(labels, fcnArgListTuple, onSuccess, onError, chainedArg) myargs.push(_error); myargs.push(_success); } - functions.push([fcnArgListTuple[i][0], myargs]); + functions.push([x[0], myargs]); } functions.reverse(); var fcnargs = functions.pop(); - + // replace any undefined value with the chained value or push to the end. - for (i in fcnargs[1]) { - if (fcnargs[1][i] === undefined) { - fcnargs[1][i] = chainedArg; + for (let x of fcnargs[1]) { + if (x === undefined) { + x = chainedArg; chainedArg = undefined; break; } @@ -1102,43 +1132,46 @@ var _series = function(labels, fcnArgListTuple, onSuccess, onError, chainedArg) // TODO: implement batch operations in Java var batch = series; -lib.parallel = parallel; -lib.series = series; -lib.batch = batch; - -lib.GetGroup = GetGroup; -lib.GetSecret = GetSecret; -lib.GetPrivateKey = GetPrivateKey; -lib.GetPublicKey = GetPublicKey; -lib.GetPublicKeys = GetPublicKeys; -lib.GetUserAndGroupPublicKeys = GetUserAndGroupPublicKeys; -lib.MutateMembership = MutateMembership; -lib.AddSecret = AddSecret; -lib.AddSecrets = AddSecrets; - -lib.RetrieveDeviceSpecificKey = RetrieveDeviceSpecificKey; -lib.AddMember = AddMember; -lib.AddGroup = AddGroup; -lib.AddIdentity = AddIdentity; -lib.RemoveSecret = RemoveSecret; -lib.RemoveMember = RemoveMember; -lib.ListGroupsAndSecrets = ListGroupsAndSecrets; -lib.GetOrganizationState = GetOrganizationState; -lib.AddIssue = AddIssue; -lib.GetAuditLog = GetAuditLog; -lib.runCommand = runCommand; -lib.runCommandWithPrivateKey = runCommandWithPrivateKey; - -lib.initForTest = initForTest; -lib.postEndTransaction = postEndTransaction; -lib.getCrypto = getCrypto; -lib.PostToMitro = PostToMitro; -lib.PostToMitroAgent = PostToMitroAgent; -lib.setPostToMitroForTest = setPostToMitroForTest; -lib.decryptSecretWithGroups = decryptSecretWithGroups; -lib.EditEncryptedPrivateKey = EditEncryptedPrivateKey; -lib.clearCaches = function() {delete txnSpecificCaches[null];}; -lib.makeLocalException = makeLocalException; - -lib.checkTwoFactor = checkTwoFactor; -})(); +function clearCaches() {delete txnSpecificCaches[GENERAL_TRANSACTION];}; + + +export { + + parallel, + series, + batch, + + GetGroup, + GetSecret, + GetPrivateKey, + GetPublicKey, + GetPublicKeys, + GetUserAndGroupPublicKeys, + MutateMembership, + AddSecret, + AddSecrets, + + RetrieveDeviceSpecificKey, + AddMember, + AddGroup, + AddIdentity, + RemoveSecret, + RemoveMember, + ListGroupsAndSecrets, + GetOrganizationState, + AddIssue, + GetAuditLog, + runCommand, + runCommandWithPrivateKey, + + postEndTransaction, + PostToMitro, + PostToMitroAgent, + setPostToMitroForTest, + decryptSecretWithGroups, + EditEncryptedPrivateKey, + clearCaches, + makeLocalException, + + checkTwoFactor, +} diff --git a/api/js/cli/mitroclient.js b/api/js/cli/mitroclient.js index 7e08936..f794694 100644 --- a/api/js/cli/mitroclient.js +++ b/api/js/cli/mitroclient.js @@ -1,73 +1,211 @@ +// @flow /* - * ***************************************************************************** - * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. - * Authors: - * Vijay Pandurangan (vijayp@mitro.co) - * Evan Jones (ej@mitro.co) - * Adam Hilss (ahilss@mitro.co) - * - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * You can contact the authors at inbound@mitro.co. - * ***************************************************************************** - */ - -/* Mitro Client: a client API that is compiled and tested using the Closure Compiler. */ -/** @suppress{duplicate} */ -var mitro = mitro || {}; -/** @suppress{duplicate} */ -var kew = kew || require('./kew'); - -/** -@param {!mitro.LegacyAPI} legacyApi makes requests with the old API. -@constructor +* ***************************************************************************** +* Copyright (c) 2012, 2013, 2014 Lectorius, Inc. +* Authors: +* Vijay Pandurangan (vijayp@mitro.co) +* Evan Jones (ej@mitro.co) +* Adam Hilss (ahilss@mitro.co) +* +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* You can contact the authors at inbound@mitro.co. +* ***************************************************************************** */ -mitro.Client = function(legacyApi) { + +import * as kew from "./kew"; +import { assert } from "./assert"; +import * as mitro_legacyapi from "./mitro_legacyapi"; + +export class Client { + legacyApi: any; + constructor(legacyApi: any) { + this.legacyApi = legacyApi; + } + getRSAKeys(numKeys: number) { + var d = kew.defer(); + this.legacyApi.getNewRSAKeysAsync(numKeys, function(keys) { + d.resolve(keys); + }, function(error) { + d.reject(error); + }); + return d; + }; + + /** Returns a promise for the decrypted key from group with id=groupId. + @param {number} groupId + @param {mitro.Transaction} transaction transaction this request belongs to (or null). + @returns {!kew.Promise} + */ + getGroupKey(groupId: number, transaction: any) { + var groupPromise = new kew.Promise(); + var legacyApi = this.legacyApi; + legacyApi.getGroup(groupId, transaction, function(groupData) { + groupPromise.resolve(groupData); + }, function(error) { + groupPromise.reject(error); + }); + + // after the group promise resolves, decrypt and extract the key + var keyPromise = groupPromise.then(function(groupData) { + return getAndDecryptGroupKey(legacyApi, /**@type{!mitro.GroupRpc}*/ (groupData)); + }); + return keyPromise; + }; + + /** Returns a promise for a map from user email to public key object. + @param {!Array.} identities + @param {mitro.Transaction} transaction transaction this request belongs to (or null). + @returns {!kew.Promise} + */ + getPublicKeys(identities: Array, transaction: any) { + var d = kew.defer(); + var legacyApi = this.legacyApi; + legacyApi.getPublicKeys(identities, transaction, function(publicKeys) { + // load all the public keys to objects + var result = {}; + var userNames = Object.getOwnPropertyNames(publicKeys); + for (var i = 0; i < userNames.length; i++ in publicKeys) { + var keyString = publicKeys[userNames[i]]; + result[userNames[i]] = legacyApi.cryptoLoadFromJson(keyString); + } + d.resolve(result); + }, function(error) { + d.reject(error); + }); + return d; + }; + /** - @type {!mitro.LegacyAPI} - @private + @param {string} path + @param {!Object} request + @param {mitro.Transaction} transaction transaction this request belongs to (or null). + @returns {!kew.Promise} */ - this.legacyApi = legacyApi; -}; + postSigned(path: string, request: Object, transaction: any) { + var d = kew.defer(); + this.legacyApi.postSigned(path, request, transaction, function(result) { + d.resolve(result); + }, function(error) { + d.reject(error); + }); + return d; + }; + + /** + @param {string} name Name of the organization. + @param {!Array.} admins addresses of admins. Must contain the requesting identity. + @param {!Array.} members addresses of regular member of the organization. Must not + contain any admins. + @param {function(!Object)} onSuccess + @param {function(!Error)} onError + */ + createOrganization(name: string, admins: Array, members: Array, onSuccess: any, onError: any) { + // TODO: local parameter validation? + var client = this; // avoids this scoping "fun" in nested functions + + // get public keys and generate keys for the org, admins and members (in parallel) + var combinedUsers = combineUniqueUsers(admins, members); + assert(combinedUsers.length == admins.length + members.length); + var publicKeysPromise = client.getPublicKeys(combinedUsers, null); + + var numKeys = 1 + combinedUsers.length; + var generatedKeysPromise = client.getRSAKeys(numKeys); + var waitForAllKeys = kew.all([publicKeysPromise, generatedKeysPromise]); + + // after both succeed, send the create organization request + var createdOrganization = waitForAllKeys.then(function(resultArray) { + // build and send the create organization request + var publicKeys = resultArray[0]; + var keys = resultArray[1]; + var request = makeOrganizationRequest( + client.legacyApi, name, admins, members, keys, publicKeys + ); + return client.postSigned('/mitro-core/api/CreateOrganization', request, null); + }); -/** @param {boolean} expressionResult */ -mitro.assert = function(expressionResult) { - if (!expressionResult) { - throw new Error('Assertion failed'); + // handle all errors or wait for all success + createdOrganization.then(function(result) { + onSuccess(/** @type {!Object} */ (result)); + }).fail(function(error) { + onError(/** @type {!Error} */ (error)); + }).done(); } -}; -/** -@param {number} numKeys -@returns {!kew.Promise} -*/ -mitro.Client.prototype.getRSAKeys = function(numKeys) { - var d = kew.defer(); - this.legacyApi.getNewRSAKeysAsync(numKeys, function(keys) { - d.resolve(keys); - }, function(error) { - d.reject(error); - }); - return d; -}; + /** Adds new admins and members and removes existing admins and members to an organization. + membersToPromote and adminsToDemote only apply to a user's admin rights. To add a brand + new administrator, they must appear in both membersToPromote and newMembers. To remove an administrator + from the organization completely, they must appear in both adminsToDemote and membersToRemove. + The server will verify these properties. + + @param {!mitro.MutateOrganizationClientRequest} request + @param {mitro.Transaction} transaction transaction this request belongs to (or null). + @param {function(!Object)} onSuccess + @param {function(!Error)} onError + */ + mutateOrganization( + request: mitro_legacyapi.MutateOrganizationClientRequest, + transaction: mitro_legacyapi.Transaction, onSuccess: any, onError: any + ) { + var client = this; // avoids this scoping issues in nested functions + + // get public keys for all users + var uniqueAddedUsers = combineUniqueUsers(request.membersToPromote, request.newMembers); + var publicKeysPromise = client.getPublicKeys(uniqueAddedUsers, transaction); + + var numKeys = uniqueAddedUsers.length; + var generatedKeysPromise = client.getRSAKeys(numKeys); + + // after we get public keys, get the group keys + var orgGroupKeyAfterPublicKeyPromise = publicKeysPromise.then(function(publicKeys) { + var groupKeysPromise = client.getGroupKey(request.orgId, transaction); + // return both the public keys and group keys + return groupKeysPromise.then(function(groupKeys) { + return [publicKeys, groupKeys]; + }); + }); + + var waitForAllKeys = kew.all([orgGroupKeyAfterPublicKeyPromise, generatedKeysPromise]); + + // after all succeed, send the mutate organization request + var mutatedOrganization = waitForAllKeys.then(function(resultArray) { + // build and send the create organization request + var publicKeys = resultArray[0][0]; + var orgGroupKey = resultArray[0][1]; + var generatedKeys = resultArray[1]; + var rpc = makeMutateOrganizationRequestRpc( + client.legacyApi, request, orgGroupKey, publicKeys, generatedKeys + ); + return client.postSigned('/mitro-core/api/MutateOrganization', rpc, transaction); + }); + + // handle all errors or wait for all success + mutatedOrganization.then(function(result) { + onSuccess(/** @type {!Object} */ (result)); + }).fail(function(error) { + onError(/** @type {!Error} */ (error)); + }).done(); + } +} // end of class + /** Finds the group key encrypted for identity from acls. @param {!Array.} acls @param {string} identity */ -var findEncryptedKeyForIdentity = function(acls, identity) { +export const findEncryptedKeyForIdentity = function(acls: Array, identity: string) { for (var i = 0; i < acls.length; i++) { var acl = acls[i]; if (acl.memberIdentity === identity) { @@ -89,111 +227,8 @@ var getAndDecryptGroupKey = function(legacyApi, groupData) { return legacyApi.cryptoLoadFromJson(keyString); }; -/** Returns a promise for the decrypted key from group with id=groupId. -@param {number} groupId -@param {mitro.Transaction} transaction transaction this request belongs to (or null). -@returns {!kew.Promise} -*/ -mitro.Client.prototype.getGroupKey = function(groupId, transaction) { - var groupPromise = new kew.Promise(); - var legacyApi = this.legacyApi; - legacyApi.getGroup(groupId, transaction, function(groupData) { - groupPromise.resolve(groupData); - }, function(error) { - groupPromise.reject(error); - }); - - // after the group promise resolves, decrypt and extract the key - var keyPromise = groupPromise.then(function(groupData) { - return getAndDecryptGroupKey(legacyApi, /**@type{!mitro.GroupRpc}*/ (groupData)); - }); - return keyPromise; -}; - -/** Returns a promise for a map from user email to public key object. -@param {!Array.} identities -@param {mitro.Transaction} transaction transaction this request belongs to (or null). -@returns {!kew.Promise} -*/ -mitro.Client.prototype.getPublicKeys = function(identities, transaction) { - var d = kew.defer(); - var legacyApi = this.legacyApi; - legacyApi.getPublicKeys(identities, transaction, function(publicKeys) { - // load all the public keys to objects - var result = {}; - var userNames = Object.getOwnPropertyNames(publicKeys); - for (var i = 0; i < userNames.length; i++ in publicKeys) { - var keyString = publicKeys[userNames[i]]; - result[userNames[i]] = legacyApi.cryptoLoadFromJson(keyString); - } - d.resolve(result); - }, function(error) { - d.reject(error); - }); - return d; -}; - -/** -@param {string} path -@param {!Object} request -@param {mitro.Transaction} transaction transaction this request belongs to (or null). -@returns {!kew.Promise} -*/ -mitro.Client.prototype.postSigned = function(path, request, transaction) { - var d = kew.defer(); - this.legacyApi.postSigned(path, request, transaction, function(result) { - d.resolve(result); - }, function(error) { - d.reject(error); - }); - return d; -}; - -/** -@param {string} name Name of the organization. -@param {!Array.} admins addresses of admins. Must contain the requesting identity. -@param {!Array.} members addresses of regular member of the organization. Must not - contain any admins. -@param {function(!Object)} onSuccess -@param {function(!Error)} onError -*/ -mitro.Client.prototype.createOrganization = function(name, admins, members, onSuccess, onError) { - // TODO: local parameter validation? - var client = this; // avoids this scoping "fun" in nested functions - - // get public keys and generate keys for the org, admins and members (in parallel) - var combinedUsers = mitro.combineUniqueUsers(admins, members); - mitro.assert(combinedUsers.length == admins.length + members.length); - var publicKeysPromise = client.getPublicKeys(combinedUsers, null); - - var numKeys = 1 + combinedUsers.length; - var generatedKeysPromise = client.getRSAKeys(numKeys); - var waitForAllKeys = kew.all([publicKeysPromise, generatedKeysPromise]); - - // after both succeed, send the create organization request - var createdOrganization = waitForAllKeys.then(function(resultArray) { - // build and send the create organization request - var publicKeys = resultArray[0]; - var keys = resultArray[1]; - var request = mitro.makeOrganizationRequest( - client.legacyApi, name, admins, members, keys, publicKeys); - return client.postSigned('/mitro-core/api/CreateOrganization', request, null); - }); - - // handle all errors or wait for all success - createdOrganization.then(function(result) { - onSuccess(/** @type {!Object} */ (result)); - }).fail(function(error) { - onError(/** @type {!Error} */ (error)); - }).done(); -}; - -/** Returns an Array of unique users from admins and members. -@param {!Array.} admins -@param {!Array.} members -@return {!Array.} -*/ -mitro.combineUniqueUsers = function(admins, members) { +function combineUniqueUsers(admins: Array, members: Array): Array { + // TODO(tom): this can probably be just a set (?) var combined = admins.concat(members); var uniqueUsers = {}; for (var i = 0; i < combined.length; i++) { @@ -203,7 +238,7 @@ mitro.combineUniqueUsers = function(admins, members) { } } return Object.getOwnPropertyNames(uniqueUsers); -}; +} /** Returns a correctly initialized CreateOrganizationRequest object. @param {!mitro.LegacyAPI} legacyApi @@ -214,22 +249,23 @@ mitro.combineUniqueUsers = function(admins, members) { @param {!Object.} publicKeys @returns {!mitro.CreateOrganizationRequest} */ -mitro.makeOrganizationRequest = function( - legacyApi, name, admins, members, generatedKeys, publicKeys) { +export function makeOrganizationRequest(legacyApi: any, name: string, admins: Array, members: Array, generatedKeys: any, publicKeys: any) { var organizationKey = generatedKeys.pop(); var i; - var request = new mitro.CreateOrganizationRequest(); + var request = new CreateOrganizationRequest(); request.name = name; request.publicKey = organizationKey.exportPublicKey().toJson(); // Give each admin access to the organization key - request.adminEncryptedKeys = mitro.encryptOrganizationKeyForAdmins( - legacyApi, organizationKey, admins, publicKeys); + request.adminEncryptedKeys = encryptOrganizationKeyForAdmins( + legacyApi, organizationKey, admins, publicKeys + ); // Give each member a private group - var combinedUsers = mitro.combineUniqueUsers(admins, members); - request.memberGroupKeys = mitro.createOrganizationMemberGroups( - legacyApi, combinedUsers, organizationKey, publicKeys, generatedKeys); + var combinedUsers = combineUniqueUsers(admins, members); + request.memberGroupKeys = createOrganizationMemberGroups( + legacyApi, combinedUsers, organizationKey, publicKeys, generatedKeys + ); return request; }; @@ -241,7 +277,12 @@ mitro.makeOrganizationRequest = function( @param {!Object.} publicKeys map of emails to public keys. @returns {!Object.} */ -mitro.encryptOrganizationKeyForAdmins = function(legacyApi, organizationKey, admins, publicKeys) { +export function encryptOrganizationKeyForAdmins( + legacyApi: mitro_legacyapi.LegacyAPI, + organizationKey: mitro_legacyapi.CryptoKey, + admins: Array, + publicKeys: { [key: string]: mitro_legacyapi.CryptoKey } +): { [key: string]: string } { var organizationKeyJson = organizationKey.toJson(); var result = {}; @@ -259,7 +300,12 @@ mitro.encryptOrganizationKeyForAdmins = function(legacyApi, organizationKey, adm @param {Object.} publicKeys @returns {string} */ -var encryptForUser = function(legacyApi, plaintext, user, publicKeys) { +export const encryptForUser = function( + legacyApi: mitro_legacyapi.LegacyAPI, + plaintext: string, + user: string, + publicKeys: { [key: string]: mitro_legacyapi.CryptoKey } +) { var publicKey = publicKeys[user]; if (!publicKey) { throw new Error('Missing public key for ' + user); @@ -276,75 +322,30 @@ the organization and the member. @param {!Array.} generatedKeys list of available generated private keys. @return {!Object.} */ -mitro.createOrganizationMemberGroups = function( - legacyApi, members, organizationKey, publicKeys, generatedKeys) { - mitro.assert(generatedKeys.length >= members.length); +export function createOrganizationMemberGroups( + legacyApi: mitro_legacyapi.LegacyAPI, + members: Array, + organizationKey: mitro_legacyapi.CryptoKey, + publicKeys: { [key: string]: mitro_legacyapi.CryptoKey }, + generatedKeys: Array +): { [key: string]: PrivateGroupKeys } { + assert(generatedKeys.length >= members.length); var result = {}; for (var i = 0; i < members.length; i++) { var privateGroupKey = generatedKeys.pop(); var privateGroupKeyJson = privateGroupKey.toJson(); - var memberPrivateGroup = new mitro.PrivateGroupKeys(); + var memberPrivateGroup = new PrivateGroupKeys(); memberPrivateGroup.publicKey = privateGroupKey.exportPublicKey().toJson(); memberPrivateGroup.keyEncryptedForUser = - encryptForUser(legacyApi, privateGroupKeyJson, members[i], publicKeys); + encryptForUser(legacyApi, privateGroupKeyJson, members[i], publicKeys); memberPrivateGroup.keyEncryptedForOrganization = organizationKey.encrypt(privateGroupKeyJson); result[members[i]] = memberPrivateGroup; } return result; }; -/** Adds new admins and members and removes existing admins and members to an organization. -membersToPromote and adminsToDemote only apply to a user's admin rights. To add a brand -new administrator, they must appear in both membersToPromote and newMembers. To remove an administrator -from the organization completely, they must appear in both adminsToDemote and membersToRemove. -The server will verify these properties. - -@param {!mitro.MutateOrganizationClientRequest} request -@param {mitro.Transaction} transaction transaction this request belongs to (or null). -@param {function(!Object)} onSuccess -@param {function(!Error)} onError -*/ -mitro.Client.prototype.mutateOrganization = function(request, transaction, onSuccess, onError) { - var client = this; // avoids this scoping issues in nested functions - - // get public keys for all users - var uniqueAddedUsers = mitro.combineUniqueUsers(request.membersToPromote, request.newMembers); - var publicKeysPromise = client.getPublicKeys(uniqueAddedUsers, transaction); - - var numKeys = uniqueAddedUsers.length; - var generatedKeysPromise = client.getRSAKeys(numKeys); - - // after we get public keys, get the group keys - var orgGroupKeyAfterPublicKeyPromise = publicKeysPromise.then(function(publicKeys) { - var groupKeysPromise = client.getGroupKey(request.orgId, transaction); - // return both the public keys and group keys - return groupKeysPromise.then(function(groupKeys) { - return [publicKeys, groupKeys]; - }); - }); - - var waitForAllKeys = kew.all([orgGroupKeyAfterPublicKeyPromise, generatedKeysPromise]); - - // after all succeed, send the mutate organization request - var mutatedOrganization = waitForAllKeys.then(function(resultArray) { - // build and send the create organization request - var publicKeys = resultArray[0][0]; - var orgGroupKey = resultArray[0][1]; - var generatedKeys = resultArray[1]; - var rpc = mitro.makeMutateOrganizationRequestRpc( - client.legacyApi, request, orgGroupKey, publicKeys, generatedKeys); - return client.postSigned('/mitro-core/api/MutateOrganization', rpc, transaction); - }); - - // handle all errors or wait for all success - mutatedOrganization.then(function(result) { - onSuccess(/** @type {!Object} */ (result)); - }).fail(function(error) { - onError(/** @type {!Error} */ (error)); - }).done(); -}; /** Returns a correctly initialized MutateOrganizationRequestRpc object. @param {!mitro.LegacyAPI} legacyApi @@ -354,78 +355,48 @@ mitro.Client.prototype.mutateOrganization = function(request, transaction, onSuc @param {!Array.} generatedKeys @returns {!mitro.MutateOrganizationRequestRpc} */ -mitro.makeMutateOrganizationRequestRpc = function( - legacyApi, request, organizationKey, publicKeys, generatedKeys) { - var rpc = new mitro.MutateOrganizationRequestRpc(); +export function makeMutateOrganizationRequestRpc( + legacyApi: mitro_legacyapi.LegacyAPI, + request: mitro_legacyapi.MutateOrganizationClientRequest, + organizationKey: mitro_legacyapi.CryptoKey, + publicKeys: { [key: string]: mitro_legacyapi.CryptoKey }, + generatedKeys: Array +) { + var rpc = new MutateOrganizationRequestRpc(); rpc.orgId = request.orgId; - rpc.promotedMemberEncryptedKeys = mitro.encryptOrganizationKeyForAdmins( - legacyApi, organizationKey, request.membersToPromote, publicKeys); + rpc.promotedMemberEncryptedKeys = encryptOrganizationKeyForAdmins( + legacyApi, organizationKey, request.membersToPromote, publicKeys + ); // Give each member a private group - rpc.newMemberGroupKeys = mitro.createOrganizationMemberGroups( - legacyApi, request.newMembers, organizationKey, publicKeys, generatedKeys); + rpc.newMemberGroupKeys = createOrganizationMemberGroups( + legacyApi, request.newMembers, organizationKey, publicKeys, generatedKeys + ); rpc.adminsToDemote = request.adminsToDemote; rpc.membersToRemove = request.membersToRemove; return rpc; -}; - -/** -@constructor -@struct -*/ -mitro.PrivateGroupKeys = function() { - /** @type {string|null} */ - this.publicKey = null; - /** @type {string|null} */ - this.keyEncryptedForUser = null; - /** @type {string|null} */ - this.keyEncryptedForOrganization = null; -}; - -/** -@constructor -@struct -*/ -mitro.CreateOrganizationRequest = function() { - /** - @type {string|null} - */ - this.name = null; - /** - @type {string|null} - */ - this.publicKey = null; - /** - @type {!Object.} - */ - this.adminEncryptedKeys = {}; - /** - @type {!Object.} - */ - this.memberGroupKeys = {}; -}; +} -/** -@constructor -@struct -*/ -mitro.MutateOrganizationRequestRpc = function() { - /** @type {number} */ - this.orgId = 0; - /** @type {!Object.} */ - this.promotedMemberEncryptedKeys = {}; - /** @type {!Object.} */ - this.newMemberGroupKeys = {}; - /** @type {!Array.} */ - this.adminsToDemote = []; - /** @type {!Array.} */ - this.membersToRemove = []; -}; +export class PrivateGroupKeys { + publicKey: ?string; + keyEncryptedForUser: ?string; + keyEncryptedForOrganization: ?string; +} -// exports for node -if (typeof(module) !== 'undefined' && module.exports) { - module.exports = mitro; +export class CreateOrganizationRequest { + name: ?string; + publicKey: ?string; + adminEncryptedKeys: { [key: string]: string }; + memberGroupKeys: { [key: string]: PrivateGroupKeys }; } + +export class MutateOrganizationRequestRpc { + orgId: number; + promotedMemberEncryptedKeys: { [key: string]: string }; + newMemberGroupKeys: { [key: string]: PrivateGroupKeys }; + adminsToDemote: Array; + membersToRemove: Array; +} \ No newline at end of file diff --git a/api/js/cli/parallel_test.js b/api/js/cli/parallel_test.js index 87825b7..0042136 100644 --- a/api/js/cli/parallel_test.js +++ b/api/js/cli/parallel_test.js @@ -24,16 +24,17 @@ * ***************************************************************************** */ -lib = require('./mitro_lib'); +import * as mitro_lib from "./mitro_lib"; + function arraysEqual(a1,a2) { return JSON.stringify(a1)==JSON.stringify(a2); } var failIfThree = function(val, s,e) { setTimeout(function() { - if (val ===3) - e(3); - else + if (val ===3) + e(3); + else s(1); }, 100); }; @@ -46,7 +47,7 @@ lib.parallel([ [failIfThree, [3]] ], failIfRun, - function(r) { + function(r) { (r===3) || (failIfRun()); }); @@ -55,9 +56,9 @@ lib.parallel([ [failIfThree, [1]], [failIfThree, [2]], [failIfThree, [2]] - ], - function(r) { - if (!arraysEqual(r, [1,1,1])) + ], + function(r) { + if (!arraysEqual(r, [1,1,1])) failIfRun(); }, - failIfRun); \ No newline at end of file + failIfRun); diff --git a/api/js/cli/rpc.js b/api/js/cli/rpc.js index fbadeb8..aa122e2 100644 --- a/api/js/cli/rpc.js +++ b/api/js/cli/rpc.js @@ -1,3 +1,4 @@ +// @flow /* * ***************************************************************************** * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. @@ -23,143 +24,71 @@ * You can contact the authors at inbound@mitro.co. * ***************************************************************************** */ - -/** @suppress{duplicate} */ -var mitro = mitro || {}; -(function() { - mitro.rpc = {}; - var PLATFORM = 'unknown'; - if(typeof(window) !== 'undefined') { - try { - if (CHROME) { - PLATFORM = 'CHROME'; - } else if (SAFARI) { - PLATFORM = 'SAFARI'; - } else if (FIREFOX) { - PLATFORM = 'FIREFOX'; - } else if (WEBPAGE) { - PLATFORM = 'WEBPAGE'; - } - } catch (e) { +declare var CHROME: boolean; +declare var FIREFOX: boolean; +declare var WEBPAGE: boolean; +declare var SAFARI: boolean; + +import { helper } from "../../../login/frontend/static/js/background-init"; + +let PLATFORM = 'unknown'; +if(typeof(window) !== 'undefined') { + try { + if (CHROME) { + PLATFORM = 'CHROME'; + } else if (SAFARI) { + PLATFORM = 'SAFARI'; + } else if (FIREFOX) { + PLATFORM = 'FIREFOX'; + } else if (WEBPAGE) { + PLATFORM = 'WEBPAGE'; } - - ////////////////////////////////////////////////////////////////////////// - // Code below here is only for the browser - ////////////////////////////////////////////////////////////////////////// - mitro.rpc._PostToMitro = function(outdict, args, path, onSuccess, onError) { - var url = 'https://' + args.server_host + ':' + args.server_port + path; - outdict.clientIdentifier = helper.getClientIdentifier(); - outdict.platform = PLATFORM; - - var requestString = JSON.stringify(outdict); - helper.ajax({ - type: 'POST', - url: url, - data: requestString, - dataType: 'json', - complete: function (response) { - try { - var rval = JSON.parse(response.text); - rval.status = response.status; - if(response.status === 200){ - onSuccess(rval); - } else { - onError(rval); - } - } catch(e) { - onError({ - status : response.status, - userVisibleError: 'Unknown error', - exceptionType: 'UnknownException' - }); - } - } - }); - }; + } catch (e) { } - //////////////////////////////////////////////////////////////////////////// - // Code below here is only for the node.js implementation - //////////////////////////////////////////////////////////////////////////// - else if(typeof(module) !== 'undefined' && module.exports) { - var https = require('https'); - module.exports = mitro.rpc; - - var _certificateValidation = true; - - // Setting this to false allows connecting to a self-signed SSL certificate - // Should only be used for testing! - mitro.rpc.setCertificateValidationForTest = function(value) { - _certificateValidation = Boolean(value); - }; - - mitro.rpc._PostToMitro = function(outdict, args, path, onSuccess, onError) { - onSuccess = onSuccess || mitro.rpc.DefaultResponseHandler; - onError = onError || mitro.rpc.DefaultErrorHandler; - - var requestString = JSON.stringify(outdict); - var headers = { - 'Content-Type': 'application/json', - 'Content-Length': requestString.length - }; - - var options = { - host: args.server_host, - port: args.server_port, - path: path, - method: 'POST', - headers: headers, - - // validate TLS certificates: (default; specify it just in case) - rejectUnauthorized: true - }; - - if (!_certificateValidation) { - // disable certificate validation for testing with self-signed certificate - options.agent = false; - options.rejectUnauthorized = false; - } - - var req = https.request(options, function(res) { - res.setEncoding('utf-8'); - - var responseString = ''; - - res.on('data', function(data) { - responseString += data; - }); - - res.on('end', function() { - console.log("status: " + res.statusCode); - if (200 === res.statusCode) { - onSuccess(JSON.parse(responseString)); - } else { - var dict = {'status' : res.statusCode}; - try { - dict = JSON.parse(responseString); - } finally { - console.log('rpc error: ', responseString); - onError(dict); - } - } +} + +// TODO(tom): find good place for this +type Args = { + server_host: string; + server_port: number; +} + +export const _PostToMitro = function(outdict: Object, args: Args, path: string, onSuccess: any, onError: any) { + var url = 'https://' + args.server_host + ':' + args.server_port + path; + outdict.clientIdentifier = helper.getClientIdentifier(); + outdict.platform = PLATFORM; + + var requestString = JSON.stringify(outdict); + helper.ajax({ + type: 'POST', + url: url, + data: requestString, + dataType: 'json', + complete: function (response) { + try { + var rval = JSON.parse(response.text); + rval.status = response.status; + if(response.status === 200){ + onSuccess(rval); + } else { + onError(rval); + } + } catch(e) { + onError({ + status : response.status, + userVisibleError: 'Unknown error', + exceptionType: 'UnknownException' }); - }); - - req.on('error', function(e) { - console.log('rpc.js: RPC error'); - onError({'status' : -1}); - }); - req.write(requestString); - req.end(); - }; - - mitro.rpc.DefaultResponseHandler = function(data) { - console.log(JSON.stringify(data, null, 4)); - }; + } + } + }); +}; - mitro.rpc.DefaultErrorHandler = function(data) { - console.log(JSON.stringify(data, null, 4)); - throw new Error('RPC error: ' + data); - }; - } +export const DefaultResponseHandler = function(data: Object) { + console.log(JSON.stringify(data, null, 4)); +}; -})(); +export const DefaultErrorHandler = function(data: Object) { + console.log(JSON.stringify(data, null, 4)); + throw new Error('RPC error: ' + data.toString()); +}; \ No newline at end of file diff --git a/login/chrome/helpers.js b/login/chrome/helpers.js index 42afa1c..95cff7c 100644 --- a/login/chrome/helpers.js +++ b/login/chrome/helpers.js @@ -28,36 +28,36 @@ var _CHROME_VERSION = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+) var LAST_AUTOCOMPLETE_COMPLIANT_CHROME_VERSION = 33; -function getExtensionId() { +export function getExtensionId() { return chrome.runtime.id; } -var getURL = function(path){ +export function getURL(path){ path = path ? path : ''; return chrome.extension.getURL(path); }; /** @constructor */ -function ExtensionHelper() { +export function ExtensionHelper() { this.tabs = chrome.tabs; this.getURL = getURL; this.runPopupActions = function() {return;}; - + this.isPopup = function(callback) { chrome.tabs.getCurrent(function(tab) { callback(!tab); }); }; - + this.setLocation = function(path){ window.location = '/html/' + path; }; - + this.copyFromInput = function($element, callback) { $element.select(); document.execCommand('copy'); - + if (typeof(callback) !== 'undefined') { callback(); } @@ -74,7 +74,7 @@ function ExtensionHelper() { } /** @constructor */ -function ContentHelper() { +export function ContentHelper() { this.getURL = getURL; var that = this; this.redirectTo = function(url) { @@ -84,12 +84,12 @@ function ContentHelper() { this.createTab = function(url) { that.background.createTab({url: url}); }; - + this.bindClient = function(client){ chrome.extension.onMessage.addListener(function(message, sender, sendResponse){ client.processIncoming(message); }); - + client.addSender('background', function(message){ chrome.extension.sendMessage(message); }); diff --git a/login/chrome/helpers_background.js b/login/chrome/helpers_background.js index a61f452..1f6f6b3 100644 --- a/login/chrome/helpers_background.js +++ b/login/chrome/helpers_background.js @@ -24,143 +24,135 @@ * ***************************************************************************** */ -var helpers_background = {}; -(function(){ - -var helpers_common; -if (typeof(module) !== 'undefined') { - helpers_common = require('../common/helpers_common'); -} else { - helpers_common = window.helpers_common; -} +helpers_common = window.helpers_common; var _createMitroTab = function(chromeTab) { - var tab = new helpers_common.MitroTab(chromeTab.id); - tab.windowId = chromeTab.windowId; - tab.index = chromeTab.index; - tab.url = chromeTab.url; - tab.title = chromeTab.title; - return tab; + var tab = new helpers_common.MitroTab(chromeTab.id); + tab.windowId = chromeTab.windowId; + tab.index = chromeTab.index; + tab.url = chromeTab.url; + tab.title = chromeTab.title; + return tab; }; /** @constructor */ -helpers_background.BackgroundHelper = function() { - this.getURL = getURL; - this.storage = chrome.storage; - this.storage_sync = chrome.storage.sync; - this.ajax = helpers_common.ajax; - this.cookies = chrome.cookies; - this.setIcon = function(details) { - if (details.path) { - // Chrome throws an exception if we pass unsupported sizes - var newPath = {}; - if ('19' in details.path) { - newPath['19'] = details.path['19']; - } - if ('38' in details.path) { - newPath['38'] = details.path['38']; - } - details.path = newPath; - } - chrome.browserAction.setIcon(details); - }; +BackgroundHelper = function() { + this.getURL = getURL; + this.storage = chrome.storage; + this.storage_sync = chrome.storage.sync; + this.ajax = helpers_common.ajax; + this.cookies = chrome.cookies; + this.setIcon = function(details) { + if (details.path) { + // Chrome throws an exception if we pass unsupported sizes + var newPath = {}; + if ('19' in details.path) { + newPath['19'] = details.path['19']; + } + if ('38' in details.path) { + newPath['38'] = details.path['38']; + } + details.path = newPath; + } + chrome.browserAction.setIcon(details); + }; - this.tabs = { - onUpdated: function(listener){ - chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){ - if(changeInfo.status === "complete"){ - listener(tabId); - } - }); - }, - onRemoved: function(listener){ - chrome.tabs.onRemoved.addListener(function(tabId, changeInfo, tab){ - listener(tabId); - }); - }, - sendMessage: function(tabId, message){ - chrome.tabs.sendMessage(tabId, message); - }, - create: chrome.tabs.create, - remove: chrome.tabs.remove, - getSelected: function(callback) { - // Pass null to get selected tab in current window. - chrome.tabs.getSelected(null, function(tab) { - callback(_createMitroTab(tab)); - }); - }, - getAll: function(callback) { - // Empty query returns all tabs. - chrome.tabs.query({}, function(tabs) { - var allTabs = []; - for (var i = 0; i < tabs.length; i++) { - allTabs.push(_createMitroTab(tabs[i])); - } - callback(allTabs); - }); - }, - setUrl: function(tabId, url) { - chrome.tabs.update(tabId, {url: url}); + this.tabs = { + onUpdated: function(listener){ + chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){ + if(changeInfo.status === "complete"){ + listener(tabId); } - }; - - this.getClientIdentifier = function(){ - var clientIdentifier; - try { - clientIdentifier = 'extension:[' + chrome.runtime.id + ',' + chrome.runtime.getManifest().version+']'; - } catch (e) { - console.log('could not read chrome extension info'); - clientIdentifier = 'unknown'; + }); + }, + onRemoved: function(listener){ + chrome.tabs.onRemoved.addListener(function(tabId, changeInfo, tab){ + listener(tabId); + }); + }, + sendMessage: function(tabId, message){ + chrome.tabs.sendMessage(tabId, message); + }, + create: chrome.tabs.create, + remove: chrome.tabs.remove, + getSelected: function(callback) { + // Pass null to get selected tab in current window. + chrome.tabs.getSelected(null, function(tab) { + callback(_createMitroTab(tab)); + }); + }, + getAll: function(callback) { + // Empty query returns all tabs. + chrome.tabs.query({}, function(tabs) { + var allTabs = []; + for (var i = 0; i < tabs.length; i++) { + allTabs.push(_createMitroTab(tabs[i])); } + callback(allTabs); + }); + }, + setUrl: function(tabId, url) { + chrome.tabs.update(tabId, {url: url}); + } + }; - return clientIdentifier; - }; + this.getClientIdentifier = function(){ + var clientIdentifier; + try { + clientIdentifier = 'extension:[' + chrome.runtime.id + ',' + chrome.runtime.getManifest().version+']'; + } catch (e) { + console.log('could not read chrome extension info'); + clientIdentifier = 'unknown'; + } - /** - * Activate context menu features - */ - this.addContextMenu = function() { - // Create menu group - chrome.contextMenus.create({ - id: 'mitro_context_group', - title: 'Passopolis', - contexts: ['selection'] - }); + return clientIdentifier; + }; + + /** + * Activate context menu features + */ + this.addContextMenu = function() { + // Create menu group + chrome.contextMenus.create({ + id: 'mitro_context_group', + title: 'Passopolis', + contexts: ['selection'] + }); - // Add menu item to save selected text as a secret - chrome.contextMenus.create({ - id: 'save_secret', - title: 'Save selection as secure note', - contexts: ['selection'], - onclick: function() { - // Send command to the active tab to copy selected text - helper.tabs.getSelected(function(tab) { - helper.tabs.sendMessage(tab.id, - client.composeMessage('content', 'copySelection', {tabUrl: tab.url})); - }); - }, - parentId: 'mitro_context_group' + // Add menu item to save selected text as a secret + chrome.contextMenus.create({ + id: 'save_secret', + title: 'Save selection as secure note', + contexts: ['selection'], + onclick: function() { + // Send command to the active tab to copy selected text + helper.tabs.getSelected(function(tab) { + helper.tabs.sendMessage(tab.id, + client.composeMessage('content', 'copySelection', {tabUrl: tab.url})); }); - }; + }, + parentId: 'mitro_context_group' + }); + }; - /** - * Deactivate context menu features - */ - this.removeContextMenu = function() { - chrome.contextMenus.remove('mitro_context_group'); - }; + /** + * Deactivate context menu features + */ + this.removeContextMenu = function() { + chrome.contextMenus.remove('mitro_context_group'); + }; - this.bindClient = function(client){ - chrome.extension.onMessage.addListener(function(request, sender, sendResponse){ - var message = request; - message.sender = sender.tab; - message.sendResponse = function(data){ - var newMessage = client.composeResponse(this, data); - chrome.tabs.sendMessage(this.sender.id, newMessage); - }; - client.processIncoming(message); - }); - }; + this.bindClient = function(client){ + chrome.extension.onMessage.addListener(function(request, sender, sendResponse){ + var message = request; + message.sender = sender.tab; + message.sendResponse = function(data){ + var newMessage = client.composeResponse(this, data); + chrome.tabs.sendMessage(this.sender.id, newMessage); + }; + client.processIncoming(message); + }); + }; }; -})(); +export { BackgroundHelper }; diff --git a/login/common/URI.js b/login/common/URI.js index e0424c6..ba264d8 100644 --- a/login/common/URI.js +++ b/login/common/URI.js @@ -1,11 +1,12 @@ +// @flow /* * Copyright © 2007 Dominic Mitchell - * + * * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, @@ -14,7 +15,7 @@ * Neither the name of the Dominic Mitchell nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR @@ -26,7 +27,7 @@ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ + */ /* * An URI datatype. Based upon examples in RFC3986. @@ -38,258 +39,258 @@ * TODO keyword escaping */ -// Globals we introduce. -var URI; -var URIQuery; -// Introduce a new scope to define some private helper functions. -(function () { +//// HELPER FUNCTIONS ///// - //// HELPER FUNCTIONS ///// - - // RFC3986 §5.2.3 (Merge Paths) - function merge(base, rel_path) { - var dirname = /^(.*)\//; - if (base.authority && !base.path) { - return "/" + rel_path; - } - else { - return base.getPath().match(dirname)[0] + rel_path; - } - } +// RFC3986 §5.2.3 (Merge Paths) +function merge(base: URI, rel_path: string) { + const dirname = /^(.*)\//; + if (base.authority && !base.path) { + return "/" + rel_path; + } + const path = base.getPath() + if (!path) { + throw "path must not be null when merging" + } + const matched = path.match(dirname); + if (!matched) { + throw ("path was not a path but: " + path) + } + return matched[0] + rel_path; +} - // Match two path segments, where the second is ".." and the first must - // not be "..". - var DoubleDot = /\/((?!\.\.\/)[^\/]*)\/\.\.\//; +// Match two path segments, where the second is ".." and the first must +// not be "..". +var DoubleDot = /\/((?!\.\.\/)[^\/]*)\/\.\.\//; - function remove_dot_segments(path) { - if (!path) { - return ""; - } - // Remove any single dots - var newpath = path.replace(/\/\.\//g, '/'); - // Remove any trailing single dots. - newpath = newpath.replace(/\/\.$/, '/'); - // Remove any double dots and the path previous. NB: We can't use - // the "g", modifier because we are changing the string that we're - // matching over. - while (newpath.match(DoubleDot)) { - newpath = newpath.replace(DoubleDot, '/'); - } - // Remove any trailing double dots. - newpath = newpath.replace(/\/([^\/]*)\/\.\.$/, '/'); - // If there are any remaining double dot bits, then they're wrong - // and must be nuked. Again, we can't use the g modifier. - while (newpath.match(/\/\.\.\//)) { - newpath = newpath.replace(/\/\.\.\//, '/'); - } - return newpath; - } +function remove_dot_segments(path: ?string) { + if (!path) { + return ""; + } + // Remove any single dots + var newpath = path.replace(/\/\.\//g, '/'); + // Remove any trailing single dots. + newpath = newpath.replace(/\/\.$/, '/'); + // Remove any double dots and the path previous. NB: We can't use + // the "g", modifier because we are changing the string that we're + // matching over. + while (newpath.match(DoubleDot)) { + newpath = newpath.replace(DoubleDot, '/'); + } + // Remove any trailing double dots. + newpath = newpath.replace(/\/([^\/]*)\/\.\.$/, '/'); + // If there are any remaining double dot bits, then they're wrong + // and must be nuked. Again, we can't use the g modifier. + while (newpath.match(/\/\.\.\//)) { + newpath = newpath.replace(/\/\.\.\//, '/'); + } + return newpath; +} - // give me an ordered list of keys of this object - function hashkeys(obj) { - var list = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - list.push(key); - } - } - return list.sort(); +// give me an ordered list of keys of this object +function hashkeys(obj) { + var list = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + list.push(key); } + } + return list.sort(); +} - // TODO: Make these do something - function uriEscape(source) { - return source; - } +// TODO: Make these do something +function uriEscape(source) { + return source; +} + +function uriUnescape(source) { + return source; +} + +class URI { + scheme: ?string; + authority: ?string; + path: ?string; + query: ?string; + fragment: ?string; - function uriUnescape(source) { - return source; + constructor(str: ?string) { + if (!str) { + str = ""; } + // Based on the regex in RFC2396 Appendix B. + var parser = /^(?:([^:\/?\#]+):)?(?:\/\/([^\/?\#]*))?([^?\#]*)(?:\?([^\#]*))?(?:\#(.*))?/; + var result = str.match(parser); + if (result === null || result == undefined) { + throw "Invalid URI"; + } - //// URI CLASS ///// + // Keep the results in private variables. + this.scheme = result[1] || null; + this.authority = result[2] || null; + this.path = result[3] || null; + this.query = result[4] || null; + this.fragment = result[5] || null; + } - // Constructor for the URI object. Parse a string into its components. - // note that this 'exports' 'URI' to the 'global namespace' - /** - @constructor - @param {string=} str - */ - URI = function (str) { - if (!str) { - str = ""; - } - // Based on the regex in RFC2396 Appendix B. - var parser = /^(?:([^:\/?\#]+):)?(?:\/\/([^\/?\#]*))?([^?\#]*)(?:\?([^\#]*))?(?:\#(.*))?/; - var result = str.match(parser); - - // Keep the results in private variables. - var scheme = result[1] || null; - var authority = result[2] || null; - var path = result[3] || null; - var query = result[4] || null; - var fragment = result[5] || null; - - // Set up accessors. - this.getScheme = function () { - return scheme; - }; - this.setScheme = function (newScheme) { - scheme = newScheme; - }; - this.getAuthority = function () { - return authority; - }; - this.setAuthority = function (newAuthority) { - authority = newAuthority; - }; - this.getPath = function () { - return path; - }; - this.setPath = function (newPath) { - path = newPath; - }; - this.getQuery = function () { - return query; - }; - this.setQuery = function (newQuery) { - query = newQuery; - }; - this.getFragment = function () { - return fragment; - }; - this.setFragment = function (newFragment) { - fragment = newFragment; - }; - }; + getScheme() { + return this.scheme; + }; + setScheme(newScheme: ?string) { + this.scheme = newScheme; + }; + getAuthority() { + return this.authority; + }; + setAuthority(newAuthority: ?string) { + this.authority = newAuthority; + }; + getPath(): ?string { + return this.path; + }; + setPath(newPath: ?string) { + this.path = newPath; + }; + getQuery(): ?string { + return this.query; + }; + setQuery(newQuery: ?string) { + this.query = newQuery; + }; + getFragment(): ?string { + return this.fragment; + }; + setFragment(newFragment: ?string) { + this.fragment = newFragment; + }; - // Restore the URI to it's stringy glory. - URI.prototype.toString = function () { - var str = ""; - if (this.getScheme()) { - str += this.getScheme() + ":"; - } - if (this.getAuthority()) { - str += "//" + this.getAuthority(); - } - if (this.getPath()) { - str += this.getPath(); - } - if (this.getQuery()) { - str += "?" + this.getQuery(); - } - if (this.getFragment()) { - str += "#" + this.getFragment(); - } - return str; - }; + toString() { + var str = ""; + if (this.scheme) { + str += this.scheme + ":"; + } + if (this.authority) { + str += "//" + this.authority; + } + if (this.path) { + str += this.path; + } + if (this.query) { + str += "?" + this.query; + } + if (this.fragment) { + str += "#" + this.fragment; + } + return str; + } - // RFC3986 §5.2.2. Transform References; - URI.prototype.resolve = function (base) { - var target = new URI(); - if (this.getScheme()) { - target.setScheme(this.getScheme()); - target.setAuthority(this.getAuthority()); - target.setPath(remove_dot_segments(this.getPath())); + // RFC3986 §5.2.2. Transform References; + resolve(base: URI) { + var target = new URI(); + if (this.scheme) { + target.setScheme(this.scheme); + target.setAuthority(this.authority); + target.setPath(remove_dot_segments(this.path)); + target.setQuery(this.getQuery()); + } else { + if (this.authority) { + target.setAuthority(this.authority); + target.setPath(remove_dot_segments(this.path)); + target.setQuery(this.getQuery()); + } else { + // XXX Original spec says "if defined and empty"…; + const path = this.path; + if (path) { + if (path.charAt(0) === '/') { + target.setPath(remove_dot_segments(path)); + } else { + target.setPath(merge(base, path)); + target.setPath(remove_dot_segments(target.getPath())); + } + target.setQuery(this.getQuery()); + } else { + target.setPath(base.getPath()); + if (this.getQuery()) { target.setQuery(this.getQuery()); + } else { + target.setQuery(base.getQuery()); + } } - else { - if (this.getAuthority()) { - target.setAuthority(this.getAuthority()); - target.setPath(remove_dot_segments(this.getPath())); - target.setQuery(this.getQuery()); - } - else { - // XXX Original spec says "if defined and empty"…; - if (!this.getPath()) { - target.setPath(base.getPath()); - if (this.getQuery()) { - target.setQuery(this.getQuery()); - } - else { - target.setQuery(base.getQuery()); - } - } - else { - if (this.getPath().charAt(0) === '/') { - target.setPath(remove_dot_segments(this.getPath())); - } else { - target.setPath(merge(base, this.getPath())); - target.setPath(remove_dot_segments(target.getPath())); - } - target.setQuery(this.getQuery()); - } - target.setAuthority(base.getAuthority()); - } - target.setScheme(base.getScheme()); - } + target.setAuthority(base.getAuthority()); + } + target.setScheme(base.getScheme()); + } + target.setFragment(this.getFragment()); + return target; + } + + parseQuery(): ?URIQuery { + if (this.query) { + return URIQuery.fromString(this.query, null); + } + return null + } +} - target.setFragment(this.getFragment()); - return target; - }; +class URIQuery { + params: any; + separator: string; - URI.prototype.parseQuery = function () { - return URIQuery.fromString(this.getQuery()); - }; + constuctor() { + this.params = {}; + this.separator = "&"; + } - //// URIQuery CLASS ///// - /** @constructor */ - URIQuery = function () { - this.params = {}; - this.separator = "&"; - }; + static fromString(sourceString: string, separator: ?string) { + var result = new URIQuery(); + if (separator) { + result.separator = separator; + } + result.addStringParams(sourceString); + return result; + } - /** - @param {string} sourceString - @param {string=} separator - */ - URIQuery.fromString = function (sourceString, separator) { - var result = new URIQuery(); - if (separator) { - result.separator = separator; - } - result.addStringParams(sourceString); - return result; - }; + // From http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 + // (application/x-www-form-urlencoded). + // + // NB: The user can get this.params and modify it directly. + addStringParams(sourceString: string) { + var kvp = sourceString.split(this.separator); + var list, key, value; + for (var i = 0; i < kvp.length; i++) { + // var [key,value] = kvp.split("=", 2) only works on >= JS 1.7 + list = kvp[i].split("=", 2); + key = uriUnescape(list[0].replace(/\+/g, " ")); + value = uriUnescape(list[1].replace(/\+/g, " ")); + if (!this.params.hasOwnProperty(key)) { + this.params[key] = []; + } + this.params[key].push(value); + } + } - - // From http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 - // (application/x-www-form-urlencoded). - // - // NB: The user can get this.params and modify it directly. - URIQuery.prototype.addStringParams = function (sourceString) { - var kvp = sourceString.split(this.separator); - var list, key, value; - for (var i = 0; i < kvp.length; i++) { - // var [key,value] = kvp.split("=", 2) only works on >= JS 1.7 - list = kvp[i].split("=", 2); - key = uriUnescape(list[0].replace(/\+/g, " ")); - value = uriUnescape(list[1].replace(/\+/g, " ")); - if (!this.params.hasOwnProperty(key)) { - this.params[key] = []; - } - this.params[key].push(value); - } - }; + getParam(key: string) { + if (this.params.hasOwnProperty(key)) { + return this.params[key][0]; + } + return null; + } + + toString() { + var kvp = []; + var keys = hashkeys(this.params); + var ik, ip; + for (ik = 0; ik < keys.length; ik++) { + for (ip = 0; ip < this.params[keys[ik]].length; ip++) { + kvp.push(keys[ik].replace(/ /g, "+") + "=" + this.params[keys[ik]][ip].replace(/ /g, "+")); + } + } + return kvp.join(this.separator); + } +} - URIQuery.prototype.getParam = function (key) { - if (this.params.hasOwnProperty(key)) { - return this.params[key][0]; - } - return null; - }; - URIQuery.prototype.toString = function () { - var kvp = []; - var keys = hashkeys(this.params); - var ik, ip; - for (ik = 0; ik < keys.length; ik++) { - for (ip = 0; ip < this.params[keys[ik]].length; ip++) { - kvp.push(keys[ik].replace(/ /g, "+") + "=" + this.params[keys[ik]][ip].replace(/ /g, "+")); - } - } - return kvp.join(this.separator); - }; -})(); +export { URI, URIQuery }; diff --git a/login/common/background_api.js b/login/common/background_api.js index 9cadc45..14c5a27 100644 --- a/login/common/background_api.js +++ b/login/common/background_api.js @@ -1,3 +1,4 @@ +// @flow /* * ***************************************************************************** * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. @@ -24,8 +25,14 @@ * ***************************************************************************** */ -var keycache = mitro.keycache.MakeKeyCache(); -mitro.keycache.startFiller(keycache); +import * as KeyCache from "../../api/js/cli/keycache"; +import { Client } from "../../api/js/cli/mitroclient"; +import * as forge from "../../node_modules/node-forge/js/forge"; +import {$, jQuery} from "../../node_modules/jquery/dist/jquery"; +import * as WorkerMod from "../../login/common/worker"; + +var keycache = KeyCache.MakeKeyCache(); +KeyCache.startFiller(keycache); var getNewRSAKeysAsync = function(numKeys, onSuccess, onError) { keycache.getNewRSAKeysAsync(numKeys, function(keys) { @@ -51,12 +58,9 @@ var getRandomness = function(onSuccess, onError) { }; var client = new Client('background'); +var worker = new Worker('worker.js'); -// we have to use unsafeWindow in firefox -var _Worker = typeof(unsafeWindow) !== 'undefined' ? unsafeWindow.Worker : Worker; - -var worker = new _Worker('worker.js'); -worker.addEventListener('message', function(event) { +worker.onmessage = function(event: MessageEvent) { // make a deep copy of message var message = jQuery.extend(true, {}, event.data); @@ -65,24 +69,46 @@ worker.addEventListener('message', function(event) { worker.postMessage(new_message); }; client.processIncoming(message); -}); +} + client.addSender('worker', function(message){ worker.postMessage(message); }); var ajax = mitro.rpc._PostToMitro; - client.initRemoteCalls('worker', [ 'signMessageAsync', - 'setExtensionId', 'setFailover', 'setDeviceId', 'getDeviceId', 'getDeviceIdAsync', - 'workerInvokeOnIdentity', 'createIdentity', 'workerCreateIdentity', 'workerLogin', 'workerLoginWithToken', - 'workerLoginWithTokenAndLocalKey', 'login', 'loginWithToken', 'loginWithTokenAndLocalKey', 'addIssue', 'initCacheFromFile', - 'initCacheFromJson', 'clearCaches', 'bidirectionalSetDiff', 'workerLogout' - ]); - -client.initRemoteExecution('worker', ['getNewRSAKeysAsync', 'console_log', 'ajax', 'getRandomness'], this); -client.setExtensionId(getExtensionId()); + 'setExtensionId', + 'setFailover', + 'setDeviceId', + 'getDeviceId', + 'getDeviceIdAsync', + 'workerInvokeOnIdentity', + 'createIdentity', + 'workerCreateIdentity', + 'workerLogin', + 'workerLoginWithToken', + 'workerLoginWithTokenAndLocalKey', + 'login', + 'loginWithToken', + 'loginWithTokenAndLocalKey', + 'addIssue', + 'initCacheFromFile', + 'initCacheFromJson', + 'clearCaches', + 'bidirectionalSetDiff', + 'workerLogout', +]); + +client.initRemoteExecution('worker', [ + 'getNewRSAKeysAsync', + 'console_log', + 'ajax', + 'getRandomness', +], this); + +client.setExtensionId(Worker.getExtensionId()); // try to catch loading things in the incorrect order assert(mitro.fe); @@ -153,15 +179,8 @@ var getLoginToken = function(email, callback) { var key = 'loginToken:' + email; helper.storage.local.get(key, function(r) { var token = r; - try { - if (CHROME && chrome.runtime.lastError) { - // TODO(ivan): safari and ff implementation - console.log('local storage error', chrome.runtime.lastError.message); - } else { - token = JSON.parse(r[key]); - console.log("got login token for user " + email); - } + console.log('local storage error', chrome.runtime.lastError.message); } catch (e) { console.log('problem getting key', (e.stack ? e.stack : '')); } finally { diff --git a/login/common/domain.js b/login/common/domain.js index f8547ef..16b6939 100644 --- a/login/common/domain.js +++ b/login/common/domain.js @@ -1,3 +1,4 @@ +// @flow /* * ***************************************************************************** * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. @@ -23,18 +24,20 @@ * You can contact the authors at inbound@mitro.co. * ***************************************************************************** */ +import { URI } from './URI'; + +let getCanonicalHost: (string) => string; -var getCanonicalHost; (function () { 'use strict'; // From the URL - getCanonicalHost = function(full_url) { + getCanonicalHost = function(full_url: string) { var host = new URI(full_url).getAuthority(); if (!host) { return ""; } - // Cookies are not isolated by port, but the chrome cookies API will not + // Cookies are not isolated by port, but the chrome cookies API will not // match the domain if the port is included. var index = host.indexOf(':'); if (index !== -1) { diff --git a/login/common/loginpage.js b/login/common/loginpage.js index 056d73a..853d8bf 100644 --- a/login/common/loginpage.js +++ b/login/common/loginpage.js @@ -1,3 +1,4 @@ +// @flow /* * ***************************************************************************** * Copyright (c) 2012, 2013, 2014 Lectorius, Inc. @@ -24,362 +25,357 @@ * ***************************************************************************** */ -// for testing -var __testing__ = {}; +import { assert } from "../../api/js/cli/assert"; +import * as domain from "./domain"; +import {$, jQuery} from "../../node_modules/jquery/dist/jquery"; +import * as helper from "../chrome/helpers"; -(function () { - 'use strict'; +const LARGE_SCORE_VALUE = 1000; - var assert; - var getCanonicalHost; +let SERVER_HINTS = {}; - if (typeof(window) === 'undefined') { - assert = require('assert'); - var domain = require('./domain'); - getCanonicalHost = domain.getCanonicalHost; - } else { - assert = window.assert; - getCanonicalHost = window.getCanonicalHost; - } - -var LARGE_SCORE_VALUE = 1000; - -var SERVER_HINTS = {}; -var setServerHints = function(hints) { - SERVER_HINTS = hints; -}; +function setServerHints(hints: Object) { + SERVER_HINTS = hints; +} -var evaluateServerHintsForEntity = function(type, fieldDict) { - if (!SERVER_HINTS) { - return 0; - } - var reject = SERVER_HINTS.reject; - if (!reject) { - return 0; - } - var hintForType = reject[type]; - if (!hintForType) { - return 0; - } - for (var i = 0; i < hintForType.length; ++i) { - var matched = false; - for (var j = 0; j < hintForType[i].length; ++j) { - var attributeName = hintForType[i][j].attributeName; - // protect against bad data matching all kinds of things - if (hintForType[i][j].exactMatch) { - if (fieldDict[attributeName] !== hintForType[i][j].exactMatch) { - // if we don't have an exact match, this set is not matching - matched = false; - break; - } else { - matched = true; - } - } else if (hintForType[i][j].regexMatch) { - if (fieldDict[attributeName] && fieldDict[attributeName].match(hintForType[i][j].regexMatch)) { - matched = true; - } else { - matched = false; - break; - } - } +function evaluateServerHintsForEntity(type, fieldDict) { + if (!SERVER_HINTS) { + return 0; + } + var reject = SERVER_HINTS.reject; + if (!reject) { + return 0; + } + var hintForType = reject[type]; + if (!hintForType) { + return 0; + } + for (var i = 0; i < hintForType.length; ++i) { + var matched = false; + for (var j = 0; j < hintForType[i].length; ++j) { + var attributeName = hintForType[i][j].attributeName; + // protect against bad data matching all kinds of things + if (hintForType[i][j].exactMatch) { + if (fieldDict[attributeName] !== hintForType[i][j].exactMatch) { + // if we don't have an exact match, this set is not matching + matched = false; + break; + } else { + matched = true; } - if (matched) { - return -LARGE_SCORE_VALUE; + } else if (hintForType[i][j].regexMatch) { + if (fieldDict[attributeName] && fieldDict[attributeName].match(hintForType[i][j].regexMatch)) { + matched = true; + } else { + matched = false; + break; } + } } - return 0; + if (matched) { + return -LARGE_SCORE_VALUE; + } + } + return 0; }; -var getMaximumElement = function (elements, scoreFunc) { - if (elements) { - var maxScore = -1; - var maxElement = null; - - for (var i = 0; i < elements.length; i++) { - var score = scoreFunc(elements[i]); - if (score > maxScore) { - maxScore = score; - maxElement = elements[i]; - } - } - return maxElement; - } - return null; +function getMaximumElement(elements, scoreFunc): null | Object { + if (elements) { + var maxScore = -1; + var maxElement = null; + + for (var i = 0; i < elements.length; i++) { + var score = scoreFunc(elements[i]); + if (score > maxScore) { + maxScore = score; + maxElement = elements[i]; + } + } + return maxElement; + } + return null; }; // Score a username input field. Field type is the most important criteria. // Having a non-empty value is useful to determine the username when saving a // form. var usernameScoreFunc = function (a, passwordFieldHint) { - var score = 0; - if (evaluateServerHintsForEntity('username', a) < 0) { - console.log('user: rejected ', a, ' due to server hint'); - return -LARGE_SCORE_VALUE; - } - - if (a.type === 'text' || a.type === 'email') { - score += 2; - } else { - score -= LARGE_SCORE_VALUE; - } - if (passwordFieldHint) { - // as of jquery 1.3.2, items are returned in document order, this - // distance will tell us the number of matching elements between - // the two items. - var distance = passwordFieldHint.itemNo - a.itemNo; - if (distance > 0) { - score += 1.0/distance; - } - } - - console.log('username field=', a, ' score:',score); - return score; + var score = 0; + if (evaluateServerHintsForEntity('username', a) < 0) { + console.log('user: rejected ', a, ' due to server hint'); + return -LARGE_SCORE_VALUE; + } + + if (a.type === 'text' || a.type === 'email') { + score += 2; + } else { + score -= LARGE_SCORE_VALUE; + } + if (passwordFieldHint) { + // as of jquery 1.3.2, items are returned in document order, this + // distance will tell us the number of matching elements between + // the two items. + var distance = passwordFieldHint.itemNo - a.itemNo; + if (distance > 0) { + score += 1.0/distance; + } + } + + console.log('username field=', a, ' score:',score); + return score; }; -var guessUsernameField = function (elements, passwordFieldHint) { - var usernameField = getMaximumElement(elements, function(a) {return usernameScoreFunc(a, passwordFieldHint);}); - // Enforce a minimum threshold to prevent really bad guesses. - if (usernameField !== null && usernameScoreFunc(usernameField) > 0) { - assert(usernameField.type !== 'password'); - return usernameField; - } else { - return null; - } +function guessUsernameField(elements: Array, passwordFieldHint: Object) { + var usernameField = getMaximumElement(elements, function(a) {return usernameScoreFunc(a, passwordFieldHint);}); + // Enforce a minimum threshold to prevent really bad guesses. + if (usernameField !== null && usernameScoreFunc(usernameField) > 0) { + assert(usernameField.type !== 'password'); + return usernameField; + } else { + return null; + } }; // Score a password input field. Field type is the most important criteria. // Having a non-empty value is useful to determine the username when saving a // form. Non-password fields are heavily penalized. var passwordScoreFunc = function (a) { - if (evaluateServerHintsForEntity('password', a) < 0) { - console.log('password: rejected ', a, ' due to server hint'); - return -LARGE_SCORE_VALUE; - } - - var score = a.value ? 1 : 0; - var name = a.name ? a.name.toLowerCase(): ""; - if (a.type === 'password') { - // catch credit card CCV fields - if (a.maxlength <=4) { - score -= LARGE_SCORE_VALUE; - } else if (name.indexOf('creditcard') != -1) { - score -= LARGE_SCORE_VALUE; - } else { - score += 2; - } + if (evaluateServerHintsForEntity('password', a) < 0) { + console.log('password: rejected ', a, ' due to server hint'); + return -LARGE_SCORE_VALUE; + } + + var score = a.value ? 1 : 0; + var name = a.name ? a.name.toLowerCase(): ""; + if (a.type === 'password') { + // catch credit card CCV fields + if (a.maxlength <=4) { + score -= LARGE_SCORE_VALUE; + } else if (name.indexOf('creditcard') != -1) { + score -= LARGE_SCORE_VALUE; } else { - score -= LARGE_SCORE_VALUE; + score += 2; } - console.log('password field=', a, ' score:',score); - return score; + } else { + score -= LARGE_SCORE_VALUE; + } + console.log('password field=', a, ' score:',score); + return score; }; -var guessPasswordField = function (elements) { - var passwordField = getMaximumElement(elements, passwordScoreFunc); +type Element = any // TODO(tom) - if (passwordField !== null && passwordScoreFunc(passwordField) > 0) { - return passwordField; - } else { - return null; - } +function guessPasswordField(elements: Array) { + var passwordField = getMaximumElement(elements, passwordScoreFunc); + + if (passwordField !== null && passwordScoreFunc(passwordField) > 0) { + return passwordField; + } else { + return null; + } }; // we allow image submit buttons for SUBMITTING once we have chosen // a form, but ignore them for RANKING purposes. var guessSubmitField = function (elements, allowImageButtons, $preferAfterThisField) { - var submitScoreFunc = function (a) { - console.log('begin scoring of ', a); - if (evaluateServerHintsForEntity('submit', a) < 0) { - console.log('rejected ', a, ' due to server hint'); - return -LARGE_SCORE_VALUE; - } - if (a.type === 'text' || a.type === 'password') { - return -LARGE_SCORE_VALUE; - } - var score = (a.type === 'submit' || a.type === 'button') ? 1 : 0; - // in some cases (when submitting the form) we want to match image - // buttons - score += (a.type === 'image' && allowImageButtons) ? 0.75 : 0; - score += (a.type === 'a') ? 0.25 : 0; - var value = a.value ? a.value.toLowerCase() : ''; - if (value.indexOf('forgot') !== -1) { - score -= LARGE_SCORE_VALUE; - } - // prefer exact matches. - // this helps avoid things like "forgot login" - if ( - // TODO: internationalize - (value ==='sign in') || - (value ==='log in') || - (value ==='log on') || - (value ==='submit') || - (value ==='login') || - (value ==='go')) { - score += 2; - } else if ( - (value.indexOf('sign in') != -1) || - (value.indexOf('log in') != -1) || - (value.indexOf('log on') != -1) || - (value.indexOf('submit') != -1) || - (value.indexOf('login') != -1) || - (value.indexOf('go') != -1)) { - score += 1; - } + var submitScoreFunc = function (a) { + console.log('begin scoring of ', a); + if (evaluateServerHintsForEntity('submit', a) < 0) { + console.log('rejected ', a, ' due to server hint'); + return -LARGE_SCORE_VALUE; + } + if (a.type === 'text' || a.type === 'password') { + return -LARGE_SCORE_VALUE; + } + var score = (a.type === 'submit' || a.type === 'button') ? 1 : 0; + // in some cases (when submitting the form) we want to match image + // buttons + score += (a.type === 'image' && allowImageButtons) ? 0.75 : 0; + score += (a.type === 'a') ? 0.25 : 0; + var value = a.value ? a.value.toLowerCase() : ''; + if (value.indexOf('forgot') !== -1) { + score -= LARGE_SCORE_VALUE; + } + // prefer exact matches. + // this helps avoid things like "forgot login" + if ( + // TODO: internationalize + (value ==='sign in') || + (value ==='log in') || + (value ==='log on') || + (value ==='submit') || + (value ==='login') || + (value ==='go')) { + score += 2; + } else if ( + (value.indexOf('sign in') != -1) || + (value.indexOf('log in') != -1) || + (value.indexOf('log on') != -1) || + (value.indexOf('submit') != -1) || + (value.indexOf('login') != -1) || + (value.indexOf('go') != -1)) { + score += 1; + } + + if ($preferAfterThisField) { + var BITMASK = 4; //Node.DOCUMENT_POSITION_FOLLOWING is defined as 4 but not in node. + var isAfter = $preferAfterThisField[0].compareDocumentPosition(a.pointer[0]) & BITMASK; + score += (isAfter) ? 1 : -1; + } + console.log('submit field field=', a, ' score:',score); - if ($preferAfterThisField) { - var BITMASK = 4; //Node.DOCUMENT_POSITION_FOLLOWING is defined as 4 but not in node. - var isAfter = $preferAfterThisField[0].compareDocumentPosition(a.pointer[0]) & BITMASK; - score += (isAfter) ? 1 : -1; - } - console.log('submit field field=', a, ' score:',score); - - return score; - }; - //console.log('>START searching for submit button'); - var submitField = getMaximumElement(elements, submitScoreFunc); - if (submitField !== null && submitScoreFunc(submitField) > 0) { - return submitField; - } else { - return null; - } - //console.log('>END searching for submit button'); + return score; + }; + //console.log('>START searching for submit button'); + var submitField = getMaximumElement(elements, submitScoreFunc); + if (submitField !== null && submitScoreFunc(submitField) > 0) { + return submitField; + } else { + return null; + } + //console.log('>END searching for submit button'); }; -var createFieldDict = function(fields) { - var rval = []; - - for (var i = 0; i < fields.length; ++i) { - var $field = $(fields[i]); - - if ($field.is('button')) { - rval.push({ - name: $field.attr('name'), - id: $field.attr('id'), - 'class':$field.attr('class'), - value: $field.text(), - type: 'button', - pointer: $field, - itemNo : i - }); - } else if ($field.is('a')) { - rval.push({ - name: $field.attr('id') || $field.attr('class'), - id: $field.attr('id'), - 'class':$field.attr('class'), - type: 'a', - value: $field.text(), - pointer: $field, - itemNo : i - }); - } else { - rval.push({ - name: $field.attr('name'), - id: $field.attr('id'), - 'class':$field.attr('class'), - value: ($field.attr('type') === 'image') ? $field.attr('alt') : $field.prop('value'), - type: $field.attr('type') || 'text', - maxlength: parseInt($field.attr('maxlength'), 10), - pointer: $field, - itemNo : i - }); - } - } - return rval; +function createFieldDict(fields) { + var rval: Array = []; // TODO Object could be more specific + + for (var i = 0; i < fields.length; ++i) { + var $field = $(fields[i]); + + if ($field.is('button')) { + rval.push({ + name: $field.attr('name'), + id: $field.attr('id'), + 'class':$field.attr('class'), + value: $field.text(), + type: 'button', + pointer: $field, + itemNo : i + }); + } else if ($field.is('a')) { + rval.push({ + name: $field.attr('id') || $field.attr('class'), + id: $field.attr('id'), + 'class':$field.attr('class'), + type: 'a', + value: $field.text(), + pointer: $field, + itemNo : i + }); + } else { + rval.push({ + name: $field.attr('name'), + id: $field.attr('id'), + 'class':$field.attr('class'), + value: ($field.attr('type') === 'image') ? $field.attr('alt') : $field.prop('value'), + type: $field.attr('type') || 'text', + maxlength: parseInt($field.attr('maxlength'), 10), + pointer: $field, + itemNo : i + }); + } + } + return rval; }; // Returns a login form dict if input form is a login form, or null otherwise. // Setting requireFieldVisibility only considers visible form fields when // looking for username/password/submit fields (default: true). -var getLoginForm = function (form, requireFieldVisibility) { - console.log('trying to get login form from ', $(form)); - if (typeof requireFieldVisibility === 'undefined') { - requireFieldVisibility = true; - } - - var $fields = $(form).find('input[type!=hidden],button[type!=hidden],a'); - if (requireFieldVisibility) { - $fields = $fields.filter(':visible'); - var $fields2 = $(form).find('input[type!=hidden]:visible,button[type!=hidden]:visible,a:visible'); - // remove fields that are positioned off screen. - assert($fields.length === $fields2.length); - - $fields = []; - for (var i = 0; i < $fields2.length; ++i) { - var elem = $fields2[i]; - var rect = elem.getBoundingClientRect(); - if (// NB: we use bottom instead of top and right instead of left to ensure that - // we only eliminate those items that are COMPLETELY off the page. - rect.bottom >= 0 && rect.right >= 0 - - // TODO: enabling these will ignore login forms that are not in the viewport, - // but that's not quite right, because sometimes login forms may - // be outside the user's vision. What should we do about this? - - // && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) - // && rect.right <= (window.innerWidth || document.documentElement.clientWidth) - ) { - $fields.push(elem); - } else { - console.log('rejected element ', elem, ' because it is outside the viewport'); - } - } - } - - if (SERVER_HINTS && SERVER_HINTS.additional_submit_button_ids) { - for (var i = 0; i < SERVER_HINTS.additional_submit_button_ids.length; ++i) { - assert (SERVER_HINTS.additional_submit_button_ids[i].indexOf(' ') === -1); - $fields = $fields.add($('#' + SERVER_HINTS.additional_submit_button_ids[i])); - } - } - var fieldsRecord = createFieldDict($fields); - - var passwordField = guessPasswordField(fieldsRecord); - var usernameField = null; - // TODO: for debugging. - //SERVER_HINTS.empty_password_username_selector = 'input[type=text]#un'; - var goAhead = !!passwordField; - if (passwordField) { - usernameField = guessUsernameField(fieldsRecord, passwordField); - } else if (SERVER_HINTS.empty_password_username_selector) { - - // if there is no password field, and we're provided with a username field selector, offer to fill it up. - var $un = $(SERVER_HINTS.empty_password_username_selector); - if ($un) { - usernameField = createFieldDict($un)[0]; - usernameField.emptyPasswordPermitted = true; - goAhead = true; - } - } - - goAhead = goAhead && (SERVER_HINTS.allow_empty_username || (usernameField !== null)); - if (goAhead) { - var submitField = guessSubmitField(fieldsRecord, - true, usernameField && usernameField.pointer); - var fieldDict = createFieldDict((passwordField?passwordField:usernameField).pointer.closest('form')); - // we need to find a form. - if (!fieldDict) { - return null; - } - //createfielddict returns a one-element-list. - fieldDict = fieldDict[0]; - - - if (evaluateServerHintsForEntity('form', fieldDict) < 0) { - console.log('rejected ', fieldDict, ' due to server hint'); - return null; - } - - return {usernameField: usernameField, - passwordField: passwordField, - submitField: submitField, - allFields: fieldsRecord, - formDict: fieldDict, - id: form.id}; - } else { - return null; - } +function getLoginForm(form: Form, requireFieldVisibility: boolean) { + console.log('trying to get login form from ', $(form)); + if (typeof requireFieldVisibility === 'undefined') { + requireFieldVisibility = true; + } + + var $fields = $(form).find('input[type!=hidden],button[type!=hidden],a'); + if (requireFieldVisibility) { + $fields = $fields.filter(':visible'); + var $fields2 = $(form).find('input[type!=hidden]:visible,button[type!=hidden]:visible,a:visible'); + // remove fields that are positioned off screen. + assert($fields.length === $fields2.length); + + $fields = []; + for (var i = 0; i < $fields2.length; ++i) { + var elem = $fields2[i]; + var rect = elem.getBoundingClientRect(); + if (// NB: we use bottom instead of top and right instead of left to ensure that + // we only eliminate those items that are COMPLETELY off the page. + rect.bottom >= 0 && rect.right >= 0 + + // TODO: enabling these will ignore login forms that are not in the viewport, + // but that's not quite right, because sometimes login forms may + // be outside the user's vision. What should we do about this? + + // && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + // && rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ) { + $fields.push(elem); + } else { + console.log('rejected element ', elem, ' because it is outside the viewport'); + } + } + } + + if (SERVER_HINTS && SERVER_HINTS.additional_submit_button_ids) { + for (var i = 0; i < SERVER_HINTS.additional_submit_button_ids.length; ++i) { + assert (SERVER_HINTS.additional_submit_button_ids[i].indexOf(' ') === -1); + // $FlowFixMe + $fields = $fields.add($('#' + SERVER_HINTS.additional_submit_button_ids[i])); + } + } + var fieldsRecord = createFieldDict($fields); + + var passwordField = guessPasswordField(fieldsRecord); + var usernameField = null; + + // TODO: for debugging. + //SERVER_HINTS.empty_password_username_selector = 'input[type=text]#un'; + var goAhead = !!passwordField; + if (passwordField) { + usernameField = guessUsernameField(fieldsRecord, passwordField); + } else if (SERVER_HINTS.empty_password_username_selector) { + + // if there is no password field, and we're provided with a username field selector, offer to fill it up. + var $un = $(SERVER_HINTS.empty_password_username_selector); + if ($un) { + usernameField = createFieldDict($un)[0]; + usernameField.emptyPasswordPermitted = true; + goAhead = true; + } + } + + goAhead = goAhead && (SERVER_HINTS.allow_empty_username || (usernameField !== null)); + if (goAhead) { + var submitField = guessSubmitField( + fieldsRecord, true, usernameField && usernameField.pointer); + + // $FlowFixMe + var fieldDict = createFieldDict((passwordField ? passwordField : usernameField).pointer.closest('form')); + + // we need to find a form. + if (!fieldDict) { + return null; + } + //createfielddict returns a one-element-list. + fieldDict = fieldDict[0]; + + + if (evaluateServerHintsForEntity('form', fieldDict) < 0) { + console.log('rejected ', fieldDict, ' due to server hint'); + return null; + } + + return {usernameField: usernameField, + passwordField: passwordField, + submitField: submitField, + allFields: fieldsRecord, + formDict: fieldDict, + id: form.id}; + } else { + return null; + } }; var LOGIN_BUTTON_LABELS = ['login', 'log in', 'log on', 'signin', 'sign in']; @@ -387,158 +383,159 @@ var SIGNUP_BUTTON_LABELS = ['signup', 'sign up', 'join', 'create', 'register', ' // Score the submit button as var loginSubmitButtonScoreFunc = function (field) { - var submitScore = 0; - if (evaluateServerHintsForEntity('login_submit', field) < 0) { - console.log('rejected ', field, ' due to server hint'); - return -LARGE_SCORE_VALUE; - } - - if (field && field.value) { - var label = field.value.toLowerCase(); - var i; - var s; - - for (i = 0; i < LOGIN_BUTTON_LABELS.length; ++i) { - s = LOGIN_BUTTON_LABELS[i]; - if (label.indexOf(s) !== -1) { - submitScore += 2; - } - } - for (i = 0; i < SIGNUP_BUTTON_LABELS.length; ++i) { - s = SIGNUP_BUTTON_LABELS[i]; - if (label.indexOf(s) !== -1) { - submitScore -= LARGE_SCORE_VALUE; - } - } - } - console.log('submit button score ', field, ' score ', submitScore); - - return submitScore; + var submitScore = 0; + if (evaluateServerHintsForEntity('login_submit', field) < 0) { + console.log('rejected ', field, ' due to server hint'); + return -LARGE_SCORE_VALUE; + } + + if (field && field.value) { + var label = field.value.toLowerCase(); + var i; + var s; + + for (i = 0; i < LOGIN_BUTTON_LABELS.length; ++i) { + s = LOGIN_BUTTON_LABELS[i]; + if (label.indexOf(s) !== -1) { + submitScore += 2; + } + } + for (i = 0; i < SIGNUP_BUTTON_LABELS.length; ++i) { + s = SIGNUP_BUTTON_LABELS[i]; + if (label.indexOf(s) !== -1) { + submitScore -= LARGE_SCORE_VALUE; + } + } + } + console.log('submit button score ', field, ' score ', submitScore); + + return submitScore; }; var LOGIN_FORM_ID_STRINGS = ['login', 'signin']; var SIGNUP_FORM_ID_STRINGS = ['signup', 'regist']; var loginIdScoreFunc = function (formId) { - var idScore = 0; - - if (formId) { - var id = formId.toLowerCase(); - var i; - - for (i = 0; i < LOGIN_FORM_ID_STRINGS.length; ++i) { - var s = LOGIN_FORM_ID_STRINGS[i]; - if (id.indexOf(s) !== -1) { - idScore += 1; - } - } - // sometimes forms have both signup and signin. we should not punish those forms. - if (idScore === 0) { - for (i = 0; i < SIGNUP_FORM_ID_STRINGS.length; ++i) { - var s = SIGNUP_FORM_ID_STRINGS[i]; - if (id.indexOf(s) !== -1) { - idScore -= LARGE_SCORE_VALUE; - } - } + var idScore = 0; + + if (formId) { + var id = formId.toLowerCase(); + var i; + + for (i = 0; i < LOGIN_FORM_ID_STRINGS.length; ++i) { + var s = LOGIN_FORM_ID_STRINGS[i]; + if (id.indexOf(s) !== -1) { + idScore += 1; + } + } + // sometimes forms have both signup and signin. we should not punish those forms. + if (idScore === 0) { + for (i = 0; i < SIGNUP_FORM_ID_STRINGS.length; ++i) { + var s = SIGNUP_FORM_ID_STRINGS[i]; + if (id.indexOf(s) !== -1) { + idScore -= LARGE_SCORE_VALUE; } + } } + } - return idScore; + return idScore; }; var countFieldsOfType = function (fields, type) { - var count = 0; - for (var i = 0; i < fields.length; ++i) { - if (fields[i].type === type) { - ++count; - } + var count = 0; + for (var i = 0; i < fields.length; ++i) { + if (fields[i].type === type) { + ++count; } - return count; + } + return count; }; // Finds best match for a login form in the current document, or null if no // login form is found. -var guessLoginForm = function (hints) { - var loginFormScoreFunc = function (formDict) { - // TODO: +function guessLoginForm(hints: Object) { + function loginFormScoreFunc(formDict: Object) { - if (evaluateServerHintsForEntity('form', formDict.formDict) < 0) { - console.log('rejected ', formDict.formDict, ' due to server hint'); - return -LARGE_SCORE_VALUE; - } + if (evaluateServerHintsForEntity('form', formDict.formDict) < 0) { + console.log('rejected ', formDict.formDict, ' due to server hint'); + return -LARGE_SCORE_VALUE; + } - if (!formDict) { - return 0; - } - var score = 0; - // Try to find an exact match to the username/password fields. - // If a match is not found, take the highest scoring form. - if (hints && - formDict.usernameField && - formDict.usernameField.name === hints.clientData.usernameField && - formDict.passwordField && - formDict.passwordField.name === hints.clientData.passwordField) { - score += 100; - } - if (formDict.usernameField) { - score += usernameScoreFunc(formDict.usernameField); - } - if (!formDict.passwordField && formDict.usernameField.emptyPasswordPermitted) { + if (!formDict) { + return 0; + } + var score = 0; + // Try to find an exact match to the username/password fields. + // If a match is not found, take the highest scoring form. + if (hints && + formDict.usernameField && + formDict.usernameField.name === hints.clientData.usernameField && + formDict.passwordField && + formDict.passwordField.name === hints.clientData.passwordField) { + score += 100; + } + if (formDict.usernameField) { + score += usernameScoreFunc(formDict.usernameField); + } + if (!formDict.passwordField && formDict.usernameField.emptyPasswordPermitted) { - } else { - score += passwordScoreFunc(formDict.passwordField); - } - score += loginSubmitButtonScoreFunc(formDict.submitField); - score += loginIdScoreFunc(formDict.id); - // Prevents matches for signup and change password forms. - if (countFieldsOfType(formDict.allFields, 'password') > 1) { - score -= LARGE_SCORE_VALUE; - } - console.log('FORM FOR ', formDict, ' SCORE:', score); - return score; - }; - console.log('trying to guess login form'); - var scoringFunction = loginFormScoreFunc; - var forms = $('form').map(function () { - console.log('mapping for ', this); - return getLoginForm(this); - }); - var loginForm = getMaximumElement(forms, scoringFunction); - if (loginForm && scoringFunction(loginForm) < 0) { - return null; + } else { + score += passwordScoreFunc(formDict.passwordField); + } + score += loginSubmitButtonScoreFunc(formDict.submitField); + score += loginIdScoreFunc(formDict.id); + // Prevents matches for signup and change password forms. + if (countFieldsOfType(formDict.allFields, 'password') > 1) { + score -= LARGE_SCORE_VALUE; + } + console.log('FORM FOR ', formDict, ' SCORE:', score); + return score; + }; + console.log('trying to guess login form'); + const scoringFunction = loginFormScoreFunc; + const forms = $('form').map(function () { + console.log('mapping for ', this); + return getLoginForm(this, false); + }); + const loginForm = getMaximumElement(forms, scoringFunction); + if (loginForm && scoringFunction(loginForm) < 0) { + return null; + } + if (loginForm && SERVER_HINTS && SERVER_HINTS.highlightSelectedForms) { + if (loginForm.usernameField) { + loginForm.usernameField.pointer.css({"border-color":"#0000ff", 'border-width' : '10px', 'border-style' : 'dotted'}); } - if (loginForm && SERVER_HINTS && SERVER_HINTS.highlightSelectedForms) { - if (loginForm.usernameField) { - loginForm.usernameField.pointer.css({"border-color":"#0000ff", 'border-width' : '10px', 'border-style' : 'dotted'}); - } - loginForm.passwordField.pointer.css({"border-color":"#00ff00", 'border-width' : '10px', 'border-style' : 'dotted'}); - if (loginForm.submitField) { - loginForm.submitField.pointer.css({"border-color":"#ff0000", 'border-width' : '10px', 'border-style' : 'dotted'}); - } + loginForm.passwordField.pointer.css({"border-color":"#00ff00", 'border-width' : '10px', 'border-style' : 'dotted'}); + if (loginForm.submitField) { + loginForm.submitField.pointer.css({"border-color":"#ff0000", 'border-width' : '10px', 'border-style' : 'dotted'}); } - return loginForm; + } + return loginForm; }; -function isInputPresentAndVisible(name) { - var $userInput = $('input[name="' + name + '"]:visible'); - return $userInput.length > 0; +function isInputPresentAndVisible(name: string) { + var $userInput = $('input[name="' + name + '"]:visible'); + return $userInput.length > 0; } -var isLoginForm = function (form) { - return getLoginForm(form) !== null; +type Form = any // TODO(tom) + +function isLoginForm(form: Form) { + return getLoginForm(form, false) !== null; }; -var isLoginPage = function () { - return guessLoginForm() !== null; +function isLoginPage () { + return guessLoginForm({}) !== null; }; // Are we on a login page for this service? -function isLoginPageForService(service) { - var serviceHost = getCanonicalHost(service.clientData.loginUrl); - var curHost = getCanonicalHost(window.location.href); - return serviceHost === curHost && isLoginPage(); +function isLoginPageForService(service: Object) { + var serviceHost = domain.getCanonicalHost(service.clientData.loginUrl); + var curHost = domain.getCanonicalHost(window.location.href); + return serviceHost === curHost && isLoginPage(); } // TODO: We should verify that the message has come from mitro using signatures @@ -550,200 +547,156 @@ function isLoginPageForService(service) { //// TODO: this should be in a common location. currently it is copied into // test_form.js in the phantom automation code!! -var fillLoginForm = function(formData) { - // TODO: the canonical host may be overly restrictive. Ideally we - // would like to check against the domain of the login cookie or - // possibly the document.domain of the login form. - var formDomain = getCanonicalHost(formData.loginUrl); - var pageDomain = getCanonicalHost(document.URL); - - if (formDomain !== pageDomain) { - throw 'Domain mismatch: ' + pageDomain + ', expected: ' + formDomain; - } - - var un = (formData.usernameField) ? formData.usernameField.value : null; - var pw = (formData.passwordField) ? formData.passwordField.value : null; - +function fillLoginForm(formData: FormData) { + // TODO: the canonical host may be overly restrictive. Ideally we + // would like to check against the domain of the login cookie or + // possibly the document.domain of the login form. + var formDomain = domain.getCanonicalHost(formData.loginUrl); + var pageDomain = domain.getCanonicalHost(document.URL); + + if (formDomain !== pageDomain) { + throw 'Domain mismatch: ' + pageDomain + ', expected: ' + formDomain; + } + + var un = (formData.usernameField) ? formData.usernameField.value : null; + var pw = (formData.passwordField) ? formData.passwordField.value : null; + + + // forms sometimes load strangely. + // TODO: Which ones? What does that mean exactly? + // Lastpass can cause conflicts: previously we filled the form then delayed submit() + // lastpass would fill the form in the middle, causing a login for the wrong account. + setTimeout(function() { + // Find the form with the username / password + var sendKeyEvents = function(inputId) { + console.log('dispatching event'); + var event = new KeyboardEvent('keydown', {keyCode: 13}); + var el = document.getElementById(inputId); + + if (!el) { + console.error("could not find element I expected to be in DOM: ", inputId); + return; + } + + el.dispatchEvent(event); + event = new KeyboardEvent('keypress', {keyCode: 13}); + el.dispatchEvent(event); + event = new KeyboardEvent('keyup', {keyCode: 13}); + el.dispatchEvent(event); + }; - // forms sometimes load strangely. - // TODO: Which ones? What does that mean exactly? - // Lastpass can cause conflicts: previously we filled the form then delayed submit() - // lastpass would fill the form in the middle, causing a login for the wrong account. + var $user_input = formData.usernameField ? formData.usernameField.pointer : formData.usernameField; + console.log("UI", $user_input); + if ($user_input) { + var $user_form = $user_input.closest("form"); + $user_input.val(un); + var userInputId = $user_input.attr('id'); + if (!userInputId) { + userInputId = 'MITRO____184379378465893'; + $user_input.attr('id', userInputId); + } + setTimeout(function() { + $user_input.change(); + sendKeyEvents(userInputId); + }, 10); + } + if (formData.passwordField) { + var $pass_input = formData.passwordField.pointer; + assert($pass_input.attr('type') === 'password'); + var $pass_form = $pass_input.closest("form"); + $pass_input.val(pw); + helper.preventAutoFill($pass_input, $pass_form); + + // simulate keypresses + var passInputId = $pass_input.attr('id'); + if (!passInputId) { + passInputId = 'MITRO____184379378465894'; + $pass_input.attr('id', passInputId); + } + + setTimeout(function() { + $pass_input.trigger('change'); + sendKeyEvents(passInputId); + }, 20); + // This call will disable autocomplete and the browser won't offer to save + // the password on the browser's secure area + // Tested in Chrome and Firefox + $($pass_form[0]).attr('autocomplete', 'off'); + if ($user_input) { + $user_input.attr('autocomplete', 'off'); + } + $pass_input.attr('autocomplete', 'off'); + + } + + + // Simulate implicit form submission (e.g. pressing enter) + // http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#implicit-submission + // Click the first submit button. Submit buttons are: + // - + // - + // -