diff --git a/Gruntfile.js b/Gruntfile.js index d3bfbaa..98dd472 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -243,7 +243,6 @@ module.exports = function(grunt) { // Load the plugin(s) grunt.loadNpmTasks('grunt-contrib-connect'); grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-mocha'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-csso'); diff --git a/src/js/app-config.js b/src/js/app-config.js index e6df9f4..a19688f 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -25,8 +25,8 @@ define(function(require) { */ app.config = { cloudUrl: cloudUrl || 'https://keys.whiteout.io', - symKeySize: 128, - symIvSize: 128, + symKeySize: 256, + symIvSize: 96, asymKeySize: 2048, workerPath: 'js', gmail: { diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js index b317859..c0d7127 100644 --- a/src/js/dao/keychain-dao.js +++ b/src/js/dao/keychain-dao.js @@ -6,18 +6,20 @@ define(function(require) { 'use strict'; var _ = require('underscore'), - util = require('js/crypto/util'); + util = require('js/crypto/util'), + config = require('js/app-config').config; var DB_PUBLICKEY = 'publickey', DB_PRIVATEKEY = 'privatekey', DB_DEVICENAME = 'devicename', - DB_DEVICEKEY = 'devicekey'; + DB_DEVICE_SECRET = 'devicesecret'; - var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto) { + var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) { this._localDbDao = localDbDao; this._publicKeyDao = publicKeyDao; this._privateKeyDao = privateKeyDao; this._crypto = crypto; + this._pgp = pgp; }; // @@ -260,14 +262,13 @@ define(function(require) { }; /** - * Geneate a device specific key and secret to authenticate to the private key service. - * @param {Function} callback(error, deviceSecret:[base64 encoded string]) + * Get the device' memorable name from local storage. Throws an error if not set + * @param {Function} callback(error, deviceName) + * @return {String} The device name */ - KeychainDAO.prototype.getDeviceSecret = function(callback) { - var self = this; - - // check if deviceName is already persisted in storage and if not return an error - self._localDbDao.read(DB_DEVICENAME, function(err, deviceName) { + KeychainDAO.prototype.getDeviceName = function(callback) { + // check if deviceName is already persisted in storage + this._localDbDao.read(DB_DEVICENAME, function(err, deviceName) { if (err) { callback(err); return; @@ -278,54 +279,130 @@ define(function(require) { return; } - readOrGenerateDeviceKey(function(deviceKey) { - generateDeciceSecret(deviceName, deviceKey); - }); + callback(null, deviceName); }); + }; - // generate random deviceKey or get from storage - function readOrGenerateDeviceKey(localCallback) { - self._localDbDao.read(DB_DEVICEKEY, function(err, storedDevKey) { + /** + * Geneate a device specific key and secret to authenticate to the private key service. + * @param {Function} callback(error, deviceSecret:[base64 encoded string]) + */ + KeychainDAO.prototype.getDeviceSecret = function(callback) { + var self = this; + + // generate random deviceSecret or get from storage + self._localDbDao.read(DB_DEVICE_SECRET, function(err, storedDevSecret) { + if (err) { + callback(err); + return; + } + + if (storedDevSecret) { + // a device key is already available locally + callback(null, storedDevSecret); + return; + } + + // generate random deviceSecret + var deviceSecret = util.random(config.symKeySize); + // persist deviceSecret to local storage (in plaintext) + self._localDbDao.persist(DB_DEVICE_SECRET, deviceSecret, function(err) { if (err) { callback(err); return; } - if (storedDevKey) { - // a device key is already available locally - localCallback(storedDevKey); + callback(null, deviceSecret); + }); + }); + }; + + /** + * Register the device on the private key server. This will give the device access to upload an encrypted private key. + * @param {String} options.userId The user's email address + * @param {Function} callback(error) + */ + KeychainDAO.prototype.registerDevice = function(options, callback) { + var self = this; + + // check if deviceName is already persisted in storage + self.getDeviceName(function(err, deviceName) { + if (err) { + callback(err); + return; + } + + requestDeviceRegistration(deviceName); + }); + + function requestDeviceRegistration(deviceName) { + // request device registration session key + self._privateKeyDao.requestDeviceRegistration({ + userId: options.userId, + deviceName: deviceName + }, function(err, regSessionKey) { + if (err) { + callback(err); return; } - // generate random deviceKey - var deviceKey = util.random(256); - // persist deviceKey to local storage (in plaintext) - self._localDbDao.persist(DB_DEVICEKEY, deviceKey, function(err) { + if (!regSessionKey.encryptedRegSessionKey) { + callback(new Error('Invalid format for session key!')); + return; + } + + decryptSessionKey(regSessionKey); + }); + } + + function decryptSessionKey(regSessionKey) { + // TODO: fetch public key for service to verify response + self.lookupPublicKey('WELL_KNOWN_SERVER_KEY_ID', function(err, serverPubkey) { + if (err) { + callback(err); + return; + } + + // decrypt the session key + var ct = regSessionKey.encryptedRegSessionKey; + self._pgp.decrypt(ct, serverPubkey, function(err, decrypedSessionKey) { if (err) { callback(err); return; } - localCallback(deviceKey); + uploadDeviceSecret(decrypedSessionKey); }); }); } - // encrypt: deviceSecret = Es(deviceKey, deviceName) -> callback - function generateDeciceSecret(deviceName, deviceKey) { - self._crypto.encrypt(deviceName, deviceKey, callback); - } - }; + function uploadDeviceSecret(regSessionKey) { + // read device secret from local storage + self.getDeviceSecret(function(err, deviceSecret) { + if (err) { + callback(err); + return; + } - /** - * Register the device on the private key server. This will give the device access to upload an encrypted private key. - * @param {String} userId The user's email address - * @param {String} deviceName The device's memorable name e.g 'iPhone Work' - * @param {[type]} deviceSecret The device specific secret derived from the device key and the device name. - * @param {Function} callback(error) - */ - KeychainDAO.prototype.registerDevice = function(userId, deviceName, deviceSecret, callback) { - callback(new Error('Not yet implemented!')); + // generate deviceSecretIv + var deviceSecretIv = util.random(config.symIvSize); + // encrypt deviceSecret + self._crypto.encrypt(deviceSecret, regSessionKey, deviceSecretIv, function(err, encryptedDeviceSecret) { + if (err) { + callback(err); + return; + } + + // upload encryptedDeviceSecret + self._privateKeyDao.uploadDeviceSecret({ + userId: options.userId, + deviceName: options.deviceName, + encryptedDeviceSecret: encryptedDeviceSecret, + deviceSecretIv: deviceSecretIv + }, callback); + }); + }); + } }; // @@ -335,60 +412,281 @@ define(function(require) { /** * Authenticate to the private key server (required before private PGP key upload). * @param {String} userId The user's email address - * @param {Function} callback(error) + * @param {Function} callback(error, authSessionKey) + * @return {Object} {sessionId:String, sessionKey:[base64 encoded]} */ - KeychainDAO.prototype.authenticateToPrivateKeyServer = function(userId, callback) { - callback(new Error('Not yet implemented!')); + KeychainDAO.prototype._authenticateToPrivateKeyServer = function(userId, callback) { + var self = this, + sessionId; + + // request auth session key required for upload + self._privateKeyDao.requestAuthSessionKeys({ + userId: userId + }, function(err, authSessionKey) { + if (err) { + callback(err); + return; + } + + if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) { + callback(new Error('Invalid format for session key!')); + return; + } + + // remember session id for verification + sessionId = authSessionKey.sessionId; + + decryptSessionKey(authSessionKey); + }); + + function decryptSessionKey(authSessionKey) { + // TODO: fetch public key for service to verify response + self.lookupPublicKey('WELL_KNOWN_SERVER_KEY_ID', function(err, serverPubkey) { + if (err) { + callback(err); + return; + } + + // decrypt the session key + var ct1 = authSessionKey.encryptedAuthSessionKey; + self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey) { + if (err) { + callback(err); + return; + } + + // decrypt the challenge + var ct2 = authSessionKey.encryptedAuthSessionKey; + self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge) { + if (err) { + callback(err); + return; + } + + encryptChallenge(decryptedSessionKey, decryptedChallenge); + }); + }); + }); + } + + function encryptChallenge(sessionKey, challenge) { + // get device secret + self.getDeviceSecret(function(err, deviceSecret) { + if (err) { + callback(err); + return; + } + + var iv = util.random(config.symIvSize); + // encrypt the challenge + self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) { + if (err) { + callback(err); + return; + } + + // encrypt the device secret + self._crypto.encrypt(deviceSecret, sessionKey, iv, function(err, encryptedDeviceSecret) { + if (err) { + callback(err); + return; + } + + replyChallenge({ + encryptedChallenge: encryptedChallenge, + encryptedDeviceSecret: encryptedDeviceSecret, + iv: iv + }, sessionKey); + }); + }); + }); + } + + function replyChallenge(encryptedChallenge, sessionKey) { + // respond to challenge by uploading the with the session key encrypted challenge + self._privateKeyDao.verifyAuthentication({ + userId: userId, + sessionId: sessionId, + encryptedChallenge: encryptedChallenge + }, function(err) { + if (err) { + callback(err); + return; + } + + callback(null, { + sessionId: sessionId, + sessionKey: sessionKey + }); + }); + } }; /** * Encrypt and upload the private PGP key to the server. - * @param {Object} privkey - * @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key - * @param {Function} callback + * @param {String} options.userId The user's email address + * @param {String} options.code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key + * @param {Function} callback(error) */ - KeychainDAO.prototype.uploadPrivateKeyToServer = function(privkey, code, callback) { - // generate random salt + KeychainDAO.prototype.uploadPrivateKey = function(options, callback) { + var self = this, + keySize = config.symKeySize, + salt; - // derive key from the code using PBKDF2 + if (!options.userId || !options.code) { + callback(new Error('Incomplete arguments!')); + return; + } - // encrypt the private key with the derived key (AES-GCM authenticated encryption) + deriveKey(options.code); - // upload the 'privatekey' {salt:[base64 encoded string], encryptedPrivateKey:[base64 encoded string]} + function deriveKey(code) { + // generate random salt + salt = util.random(keySize); + // derive key from the code using PBKDF2 + self._crypto.deriveKey(code, salt, keySize, function(err, key) { + if (err) { + callback(err); + return; + } - callback(new Error('Not yet implemented!')); + encryptPrivateKey(key); + }); + } + + function encryptPrivateKey(encryptionKey) { + // get private key from local storage + self.getUserKeyPair(options.userId, function(err, keypair) { + if (err) { + callback(err); + return; + } + + var privkeyId = keypair.privateKey._id, + pgpBlock = keypair.privateKey.encryptedKey; + + // encrypt the private key with the derived key (AES-GCM authenticated encryption) + var iv = util.random(config.symIvSize); + self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) { + if (err) { + callback(err); + return; + } + + var payload = { + _id: privkeyId, + encryptedPrivateKey: ct, + salt: salt, + iv: iv + }; + + uploadPrivateKey(payload); + }); + }); + } + + function uploadPrivateKey(payload) { + // authenticate to server for upload + self._authenticateToPrivateKeyServer(options.userId, function(err, authSessionKey) { + if (err) { + callback(err); + return; + } + + // encrypt encryptedPrivateKey again using authSessionKey + var pt = payload.encryptedPrivateKey, + iv = payload.iv, + key = authSessionKey.sessionKey; + self._crypto.encrypt(pt, key, iv, function(err, ct) { + if (err) { + callback(err); + return; + } + + // replace the encryptedPrivateKey with the double wrapped ciphertext + payload.encryptedPrivateKey = ct; + // set sessionId + payload.sessionId = authSessionKey.sessionId; + + // upload the encrypted priavet key + self._privateKeyDao.upload(payload, callback); + }); + }); + } }; /** * Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user. - * @param {[type]} userId The user's email address + * @param {String} userId The user's email address * @param {Function} callback(error) */ KeychainDAO.prototype.requestPrivateKeyDownload = function(userId, callback) { - callback(new Error('Not yet implemented!')); + this._privateKeyDao.requestDownload({ + userId: userId + }, callback); }; /** * Download the encrypted private PGP key from the server using the recovery token. - * @param {[type]} recoveryToken The recovery token acquired via email/sms from the key server + * @param {String} options.userId The user's email address + * @param {String} options.keyId The user's email address + * @param {String} options.recoveryToken The recovery token acquired via email/sms from the key server * @param {Function} callback(error, encryptedPrivateKey) */ - KeychainDAO.prototype.downloadPrivateKeyFromServer = function(recoveryToken, callback) { - callback(new Error('Not yet implemented!')); + KeychainDAO.prototype.downloadPrivateKey = function(options, callback) { + this._privateKeyDao.download(options, callback); }; /** * This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage. - * @param {[type]} code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key - * @param {Object} encryptedPrivkey The encrypted private PGP key including the random salt {salt:[base64 encoded string], encryptedPrivateKey:[base64 encoded string]} - * @param {Function} callback(error, privateKey) + * @param {String} options.userId The user's email address + * @param {String} options.keyId The user's email address + * @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key + * @param {String} options.encryptedPrivkey The encrypted private PGP key + * @param {String} options.salt The salt required to derive the code derived key + * @param {String} options.iv The iv used to encrypt the private PGP key + * @param {Function} callback(error) */ - KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(code, encryptedPrivkey, callback) { + KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) { + var self = this, + code = options.code, + salt = options.salt, + keySize = config.keySize; + + if (!options.keyId || !options.userId) { + callback(new Error('Incomplete arguments!')); + return; + } + // derive key from the code and the salt using PBKDF2 + self._crypto.deriveKey(code, salt, keySize, function(err, key) { + if (err) { + callback(err); + return; + } - // decrypt the private key with the derived key (AES-GCM authenticated decryption) + decryptAndStore(key); + }); - callback(new Error('Not yet implemented!')); + function decryptAndStore(derivedKey) { + // decrypt the private key with the derived key + var pt = options.encryptedPrivkey, + iv = options.iv; + + self._crypto.decrypt(pt, derivedKey, iv, function(err, pgpBlock) { + if (err) { + callback(err); + return; + } + + // store private key locally + self.saveLocalPrivateKey({ + _id: options.keyId, + userId: options.userId, + encryptedKey: pgpBlock + }, callback); + }); + } }; // diff --git a/src/js/dao/privatekey-dao.js b/src/js/dao/privatekey-dao.js index a115219..f3cf24d 100644 --- a/src/js/dao/privatekey-dao.js +++ b/src/js/dao/privatekey-dao.js @@ -5,12 +5,145 @@ define(function() { this._restDao = restDao; }; + // + // Device registration functions + // + /** - * Persist the user's private key on the server + * Request registration of a new device by fetching registration session key. + * @param {String} options.userId The user's email address + * @param {String} options.deviceName The device's memorable name + * @param {Function} callback(error, regSessionKey) + * @return {Object} {encryptedRegSessionKey:[base64]} */ - PrivateKeyDAO.prototype.post = function(privkey, callback) { - var uri = '/privatekey/user/' + privkey.userId + '/key/' + privkey._id; - this._restDao.post(privkey, uri, callback); + PrivateKeyDAO.prototype.requestDeviceRegistration = function(options, callback) { + var uri; + + if (!options.userId || !options.deviceName) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; + this._restDao.post(undefined, uri, callback); + }; + + /** + * Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys. + * @param {String} options.userId The user's email address + * @param {String} options.deviceName The device's memorable name + * @param {Object} options.encryptedDeviceSecret {encryptedDeviceSecret:[base64 encoded]} + * @param {Function} callback(error) + */ + PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) { + var uri; + + if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; + this._restDao.put(options.encryptedDeviceSecret, uri, callback); + }; + + // + // Private key functions + // + + /** + * Request authSessionKeys required for upload the encrypted private PGP key. + * @param {String} options.userId The user's email address + * @param {Function} callback(error, authSessionKey) + * @return {Object} {sessionId, encryptedAuthSessionKeys:[base64 encoded], encryptedChallenge:[base64 encoded]} + */ + PrivateKeyDAO.prototype.requestAuthSessionKeys = function(options, callback) { + var uri; + + if (!options.userId) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/auth/user/' + options.userId; + this._restDao.post(undefined, uri, callback); + }; + + /** + * Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response. + * @param {String} options.userId The user's email address + * @param {Object} options.encryptedChallenge The server's challenge encrypted using the authSessionKey {encryptedChallenge:[base64 encoded], encryptedDeviceSecret:[base64 encoded], iv} + * @param {Function} callback(error) + */ + PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) { + var uri; + + if (!options.userId || !options.sessionId || !options.encryptedChallenge) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; + this._restDao.put(options.encryptedChallenge, uri, callback); + }; + + /** + * Upload the encrypted private PGP key. + * @param {String} options.encryptedPrivateKey {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], sessionId: [base64 encoded]} + * @param {Function} callback(error) + */ + PrivateKeyDAO.prototype.upload = function(options, callback) { + var uri, + key = options.encryptedPrivateKey; + + if (!options.userId || !key || !key._id) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/privatekey/user/' + options.userId + '/key/' + key._id; + this._restDao.post(key, uri, callback); + }; + + /** + * Request download for the encrypted private PGP key. + * @param {[type]} options.userId The user's email address + * @param {Function} callback(error) + */ + PrivateKeyDAO.prototype.requestDownload = function(options, callback) { + var uri; + + if (!options.userId || !options.keyId) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId; + this._restDao.get({ + uri: uri + }, callback); + }; + + /** + * Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private key. + * @param {String} options.userId The user's email address + * @param {String} options.keyId The private key id + * @param {String} options.recoveryToken The token proving the user own the email account + * @param {Function} callback(error, encryptedPrivateKey) + * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]} + */ + PrivateKeyDAO.prototype.download = function(options, callback) { + var uri; + + if (!options.userId || !options.keyId || !options.recoveryToken) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken; + this._restDao.get({ + uri: uri + }, callback); }; return PrivateKeyDAO; diff --git a/src/js/dao/rest-dao.js b/src/js/dao/rest-dao.js index 78f4236..686ebc1 100644 --- a/src/js/dao/rest-dao.js +++ b/src/js/dao/rest-dao.js @@ -17,7 +17,48 @@ define(function(require) { * @param {String} options.type (optional) The type of data that you're expecting back from the server: json, xml, text. Default: json. */ RestDAO.prototype.get = function(options, callback) { - var xhr, acceptHeader; + options.method = 'GET'; + this._processRequest(options, callback); + }; + + /** + * POST (create) request + */ + RestDAO.prototype.post = function(item, uri, callback) { + this._processRequest({ + method: 'POST', + payload: item, + uri: uri + }, callback); + }; + + /** + * PUT (update) request + */ + RestDAO.prototype.put = function(item, uri, callback) { + this._processRequest({ + method: 'PUT', + payload: item, + uri: uri + }, callback); + }; + + /** + * DELETE (remove) request + */ + RestDAO.prototype.remove = function(uri, callback) { + this._processRequest({ + method: 'DELETE', + uri: uri + }, callback); + }; + + // + // helper functions + // + + RestDAO.prototype._processRequest = function(options, callback) { + var xhr, format; if (typeof options.uri === 'undefined') { callback({ @@ -30,11 +71,11 @@ define(function(require) { options.type = options.type || 'json'; if (options.type === 'json') { - acceptHeader = 'application/json'; + format = 'application/json'; } else if (options.type === 'xml') { - acceptHeader = 'application/xml'; + format = 'application/xml'; } else if (options.type === 'text') { - acceptHeader = 'text/plain'; + format = 'text/plain'; } else { callback({ code: 400, @@ -44,14 +85,16 @@ define(function(require) { } xhr = new XMLHttpRequest(); - xhr.open('GET', this._baseUri + options.uri); - xhr.setRequestHeader('Accept', acceptHeader); + xhr.open(options.method, this._baseUri + options.uri); + xhr.setRequestHeader('Accept', format); + xhr.setRequestHeader('Content-Type', format); xhr.onload = function() { - if (xhr.readyState === 4 && xhr.status === 200) { - var res; + var res; + + if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) { if (options.type === 'json') { - res = JSON.parse(xhr.responseText); + res = xhr.responseText ? JSON.parse(xhr.responseText) : xhr.responseText; } else { res = xhr.responseText; } @@ -69,72 +112,11 @@ define(function(require) { xhr.onerror = function() { callback({ code: 42, - errMsg: 'Error calling GET on ' + options.uri + errMsg: 'Error calling ' + options.method + ' on ' + options.uri }); }; - xhr.send(); - }; - - /** - * PUT (create/update) request - */ - RestDAO.prototype.put = function(item, uri, callback) { - var xhr; - - xhr = new XMLHttpRequest(); - xhr.open('PUT', this._baseUri + uri); - xhr.setRequestHeader('Content-Type', 'application/json'); - - xhr.onload = function() { - if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) { - callback(null, xhr.responseText, xhr.status); - return; - } - - callback({ - code: xhr.status, - errMsg: xhr.statusText - }); - }; - - xhr.onerror = function() { - callback({ - errMsg: 'Error calling PUT on ' + uri - }); - }; - - xhr.send(JSON.stringify(item)); - }; - - /** - * DELETE (remove) request - */ - RestDAO.prototype.remove = function(uri, callback) { - var xhr; - - xhr = new XMLHttpRequest(); - xhr.open('DELETE', this._baseUri + uri); - - xhr.onload = function() { - if (xhr.readyState === 4 && xhr.status === 200) { - callback(null, xhr.responseText, xhr.status); - return; - } - - callback({ - code: xhr.status, - errMsg: xhr.statusText - }); - }; - - xhr.onerror = function() { - callback({ - errMsg: 'Error calling DELETE on ' + uri - }); - }; - - xhr.send(); + xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); }; return RestDAO; diff --git a/test/unit/crypto-test.js b/test/unit/crypto-test.js index 5c2e730..5cbe272 100644 --- a/test/unit/crypto-test.js +++ b/test/unit/crypto-test.js @@ -3,6 +3,7 @@ define(function(require) { var Crypto = require('js/crypto/crypto'), util = require('js/crypto/util'), + config = require('js/app-config').config, expect = chai.expect; describe('Crypto unit tests', function() { @@ -10,8 +11,8 @@ define(function(require) { var crypto, password = 'password', - keySize = 128, - ivSize = 128; + keySize = config.symKeySize, + ivSize = config.symIvSize; beforeEach(function() { crypto = new Crypto(); diff --git a/test/unit/keychain-dao-test.js b/test/unit/keychain-dao-test.js index 1b01a73..3472de8 100644 --- a/test/unit/keychain-dao-test.js +++ b/test/unit/keychain-dao-test.js @@ -6,20 +6,22 @@ define(function(require) { KeychainDAO = require('js/dao/keychain-dao'), PrivateKeyDAO = require('js/dao/privatekey-dao'), Crypto = require('js/crypto/crypto'), + PGP = require('js/crypto/pgp'), expect = chai.expect; var testUser = 'test@example.com'; describe('Keychain DAO unit tests', function() { - var keychainDao, lawnchairDaoStub, pubkeyDaoStub, privkeyDaoStub, cryptoStub; + var keychainDao, lawnchairDaoStub, pubkeyDaoStub, privkeyDaoStub, cryptoStub, pgpStub; beforeEach(function() { lawnchairDaoStub = sinon.createStubInstance(LawnchairDAO); pubkeyDaoStub = sinon.createStubInstance(PublicKeyDAO); - privkeyDaoStub = new sinon.createStubInstance(PrivateKeyDAO); - cryptoStub = new sinon.createStubInstance(Crypto); - keychainDao = new KeychainDAO(lawnchairDaoStub, pubkeyDaoStub, privkeyDaoStub, cryptoStub); + privkeyDaoStub = sinon.createStubInstance(PrivateKeyDAO); + cryptoStub = sinon.createStubInstance(Crypto); + pgpStub = sinon.createStubInstance(PGP); + keychainDao = new KeychainDAO(lawnchairDaoStub, pubkeyDaoStub, privkeyDaoStub, cryptoStub, pgpStub); }); afterEach(function() {}); @@ -586,30 +588,42 @@ define(function(require) { }); }); - describe('getDeviceSecret', function() { + describe('getDeviceName', function() { it('should fail when device name is not set', function(done) { lawnchairDaoStub.read.withArgs('devicename').yields(); - keychainDao.getDeviceSecret(function(err, deviceSecret) { + keychainDao.getDeviceName(function(err, deviceName) { expect(err.message).to.equal('Device name not set!'); - expect(deviceSecret).to.not.exist; + expect(deviceName).to.not.exist; done(); }); }); - it('should fail due to erorr when reading device name', function(done) { + it('should fail due to error when reading device name', function(done) { lawnchairDaoStub.read.withArgs('devicename').yields(42); - keychainDao.getDeviceSecret(function(err, deviceSecret) { + keychainDao.getDeviceName(function(err, deviceName) { expect(err).to.equal(42); - expect(deviceSecret).to.not.exist; + expect(deviceName).to.not.exist; done(); }); }); - it('should fail due to erorr when reading device key', function(done) { + it('should work', function(done) { lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - lawnchairDaoStub.read.withArgs('devicekey').yields(42); + + keychainDao.getDeviceName(function(err, deviceName) { + expect(err).to.not.exist; + expect(deviceName).to.equal('iPhone'); + done(); + }); + }); + }); + + describe('getDeviceSecret', function() { + it('should fail due to error when reading device secret', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); + lawnchairDaoStub.read.withArgs('devicesecret').yields(42); keychainDao.getDeviceSecret(function(err, deviceSecret) { expect(err).to.equal(42); @@ -618,10 +632,10 @@ define(function(require) { }); }); - it('should fail due to erorr when storing device key', function(done) { + it('should fail due to error when storing device secret', function(done) { lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - lawnchairDaoStub.read.withArgs('devicekey').yields(); - lawnchairDaoStub.persist.withArgs('devicekey').yields(42); + lawnchairDaoStub.read.withArgs('devicesecret').yields(); + lawnchairDaoStub.persist.withArgs('devicesecret').yields(42); keychainDao.getDeviceSecret(function(err, deviceSecret) { expect(err).to.equal(42); @@ -630,11 +644,21 @@ define(function(require) { }); }); - it('should work when device key is not set', function(done) { + it('should work when device secret is not set', function(done) { lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - lawnchairDaoStub.read.withArgs('devicekey').yields(); - lawnchairDaoStub.persist.withArgs('devicekey').yields(); - cryptoStub.encrypt.yields(null, 'secret'); + lawnchairDaoStub.read.withArgs('devicesecret').yields(); + lawnchairDaoStub.persist.withArgs('devicesecret').yields(); + + keychainDao.getDeviceSecret(function(err, deviceSecret) { + expect(err).to.not.exist; + expect(deviceSecret).to.exist; + done(); + }); + }); + + it('should work when device secret is set', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); + lawnchairDaoStub.read.withArgs('devicesecret').yields(null, 'secret'); keychainDao.getDeviceSecret(function(err, deviceSecret) { expect(err).to.not.exist; @@ -642,15 +666,569 @@ define(function(require) { done(); }); }); + }); - it('should work when device key is set', function(done) { - lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - lawnchairDaoStub.read.withArgs('devicekey').yields(null, 'key'); - cryptoStub.encrypt.withArgs('iPhone', 'key').yields(null, 'secret'); + describe('registerDevice', function() { + var getDeviceNameStub, lookupPublicKeyStub, getDeviceSecretStub; - keychainDao.getDeviceSecret(function(err, deviceSecret) { + beforeEach(function() { + getDeviceNameStub = sinon.stub(keychainDao, 'getDeviceName'); + lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey'); + getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret'); + }); + afterEach(function() { + getDeviceNameStub.restore(); + lookupPublicKeyStub.restore(); + getDeviceSecretStub.restore(); + }); + + it('should fail when reading devicename', function(done) { + getDeviceNameStub.yields(42); + + keychainDao.registerDevice({}, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail in requestDeviceRegistration', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail due to invalid requestDeviceRegistration return value', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, {}); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err.message).to.equal('Invalid format for session key!'); + done(); + }); + }); + + it('should fail in lookupPublicKey', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail in decrypt', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(null, 'pubkey'); + pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail in getDeviceSecret', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(null, 'pubkey'); + pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); + getDeviceSecretStub.yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail in encrypt', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(null, 'pubkey'); + pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); + getDeviceSecretStub.yields(null, 'secret'); + cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should work', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(null, 'pubkey'); + pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); + getDeviceSecretStub.yields(null, 'secret'); + cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(null, 'encryptedDeviceSecret'); + privkeyDaoStub.uploadDeviceSecret.yields(); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).not.exist; + expect(privkeyDaoStub.uploadDeviceSecret.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('_authenticateToPrivateKeyServer', function() { + var lookupPublicKeyStub, getDeviceSecretStub; + + beforeEach(function() { + lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey'); + getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret'); + }); + afterEach(function() { + lookupPublicKeyStub.restore(); + getDeviceSecretStub.restore(); + }); + + it('should fail due to privkeyDao.requestAuthSessionKeys', function(done) { + privkeyDaoStub.requestAuthSessionKeys.withArgs({ + userId: testUser + }).yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.equal(42); + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to privkeyDao.requestAuthSessionKeys response', function(done) { + privkeyDaoStub.requestAuthSessionKeys.yields(null, {}); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to lookupPublicKey', function(done) { + privkeyDaoStub.requestAuthSessionKeys.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to pgp.decrypt', function(done) { + privkeyDaoStub.requestAuthSessionKeys.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to getDeviceSecret', function(done) { + privkeyDaoStub.requestAuthSessionKeys.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(null, 'decryptedStuff'); + getDeviceSecretStub.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to crypto.encrypt', function(done) { + privkeyDaoStub.requestAuthSessionKeys.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(null, 'decryptedStuff'); + getDeviceSecretStub.yields(null, 'deviceSecret'); + cryptoStub.encrypt.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to privkeyDao.verifyAuthentication', function(done) { + privkeyDaoStub.requestAuthSessionKeys.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(null, 'decryptedStuff'); + getDeviceSecretStub.yields(null, 'deviceSecret'); + cryptoStub.encrypt.yields(null, 'encryptedStuff'); + privkeyDaoStub.verifyAuthentication.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should work', function(done) { + privkeyDaoStub.requestAuthSessionKeys.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(null, 'decryptedStuff'); + getDeviceSecretStub.yields(null, 'deviceSecret'); + cryptoStub.encrypt.yields(null, 'encryptedStuff'); + privkeyDaoStub.verifyAuthentication.yields(); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { expect(err).to.not.exist; - expect(deviceSecret).to.equal('secret'); + expect(authSessionKey).to.deep.equal({ + sessionKey: 'decryptedStuff', + sessionId: 'sessionId' + }); + done(); + }); + }); + }); + + describe('uploadPrivateKey', function() { + var getUserKeyPairStub, _authenticateToPrivateKeyServerStub; + + beforeEach(function() { + getUserKeyPairStub = sinon.stub(keychainDao, 'getUserKeyPair'); + _authenticateToPrivateKeyServerStub = sinon.stub(keychainDao, '_authenticateToPrivateKeyServer'); + }); + afterEach(function() { + getUserKeyPairStub.restore(); + _authenticateToPrivateKeyServerStub.restore(); + }); + + it('should fail due to missing args', function(done) { + keychainDao.uploadPrivateKey({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should fail due to error in derive key', function(done) { + cryptoStub.deriveKey.yields(42); + + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to error in getUserKeyPair', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(42); + + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to error in crypto.encrypt', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(null, { + privateKey: { + _id: 'pgpKeyId', + encryptedKey: 'pgpKey' + } + }); + cryptoStub.encrypt.yields(42); + + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + expect(cryptoStub.encrypt.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to error in _authenticateToPrivateKeyServer', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(null, { + privateKey: { + _id: 'pgpKeyId', + encryptedKey: 'pgpKey' + } + }); + cryptoStub.encrypt.yields(null, 'encryptedPgpKey'); + _authenticateToPrivateKeyServerStub.yields(42); + + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + expect(cryptoStub.encrypt.calledOnce).to.be.true; + expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to error in cryptoStub.encrypt', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(null, { + privateKey: { + _id: 'pgpKeyId', + encryptedKey: 'pgpKey' + } + }); + cryptoStub.encrypt.withArgs('pgpKey').yields(null, 'encryptedPgpKey'); + _authenticateToPrivateKeyServerStub.yields(null, { + sessionId: 'sessionId', + sessionKey: 'sessionKey' + }); + cryptoStub.encrypt.withArgs('encryptedPgpKey').yields(42); + + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + expect(cryptoStub.encrypt.calledTwice).to.be.true; + expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should work', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(null, { + privateKey: { + _id: 'pgpKeyId', + encryptedKey: 'pgpKey' + } + }); + cryptoStub.encrypt.withArgs('pgpKey').yields(null, 'encryptedPgpKey'); + _authenticateToPrivateKeyServerStub.yields(null, { + sessionId: 'sessionId', + sessionKey: 'sessionKey' + }); + cryptoStub.encrypt.withArgs('encryptedPgpKey').yields(null, 'doubleEncryptedPgpKey'); + privkeyDaoStub.upload.yields(); + + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.not.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + expect(cryptoStub.encrypt.calledTwice).to.be.true; + expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; + expect(privkeyDaoStub.upload.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('requestPrivateKeyDownload', function() { + it('should work', function(done) { + privkeyDaoStub.requestDownload.withArgs({ + userId: testUser + }).yields(); + keychainDao.requestPrivateKeyDownload(testUser, done); + }); + }); + + describe('downloadPrivateKey', function() { + it('should work', function(done) { + var options = { + recoveryToken: 'token' + }; + + privkeyDaoStub.download.withArgs(options).yields(); + keychainDao.downloadPrivateKey(options, done); + }); + }); + + describe('decryptAndStorePrivateKeyLocally', function() { + var saveLocalPrivateKeyStub; + + beforeEach(function() { + saveLocalPrivateKeyStub = sinon.stub(keychainDao, 'saveLocalPrivateKey'); + }); + afterEach(function() { + saveLocalPrivateKeyStub.restore(); + }); + + it('should fail due to invlaid args', function(done) { + keychainDao.decryptAndStorePrivateKeyLocally({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should fail due to crypto.deriveKey', function(done) { + cryptoStub.deriveKey.yields(42); + + keychainDao.decryptAndStorePrivateKeyLocally({ + userId: testUser, + keyId: 'keyId' + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to crypto.decrypt', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + cryptoStub.decrypt.yields(42); + + keychainDao.decryptAndStorePrivateKeyLocally({ + userId: testUser, + keyId: 'keyId' + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(cryptoStub.decrypt.calledOnce).to.be.true; + done(); + }); + }); + + it('should work', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + cryptoStub.decrypt.yields(null, 'pgpBlock'); + saveLocalPrivateKeyStub.yields(); + + keychainDao.decryptAndStorePrivateKeyLocally({ + userId: testUser, + keyId: 'keyId' + }, function(err) { + expect(err).to.not.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(cryptoStub.decrypt.calledOnce).to.be.true; + expect(saveLocalPrivateKeyStub.calledOnce).to.be.true; + done(); }); }); diff --git a/test/unit/main.js b/test/unit/main.js index f187b3e..411d10a 100644 --- a/test/unit/main.js +++ b/test/unit/main.js @@ -36,6 +36,7 @@ function startTests() { 'test/unit/crypto-test', 'test/unit/rest-dao-test', 'test/unit/publickey-dao-test', + 'test/unit/privatekey-dao-test', 'test/unit/lawnchair-dao-test', 'test/unit/keychain-dao-test', 'test/unit/devicestorage-dao-test', diff --git a/test/unit/privatekey-dao-test.js b/test/unit/privatekey-dao-test.js new file mode 100644 index 0000000..01d5eb4 --- /dev/null +++ b/test/unit/privatekey-dao-test.js @@ -0,0 +1,194 @@ +define(function(require) { + 'use strict'; + + var RestDAO = require('js/dao/rest-dao'), + PrivateKeyDAO = require('js/dao/privatekey-dao'), + expect = chai.expect; + + describe('Private Key DAO unit tests', function() { + + var privkeyDao, restDaoStub, + emailAddress = 'test@example.com', + deviceName = 'iPhone Work'; + + beforeEach(function() { + restDaoStub = sinon.createStubInstance(RestDAO); + privkeyDao = new PrivateKeyDAO(restDaoStub); + }); + + afterEach(function() {}); + + describe('requestDeviceRegistration', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.requestDeviceRegistration({}, function(err, sessionKey) { + expect(err).to.exist; + expect(sessionKey).to.not.exist; + done(); + }); + }); + + it('should work', function(done) { + restDaoStub.post.yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + privkeyDao.requestDeviceRegistration({ + userId: emailAddress, + deviceName: deviceName + }, function(err, sessionKey) { + expect(err).to.not.exist; + expect(sessionKey).to.exist; + done(); + }); + }); + }); + + describe('uploadDeviceSecret', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.uploadDeviceSecret({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should work', function(done) { + restDaoStub.put.yields(); + + privkeyDao.uploadDeviceSecret({ + userId: emailAddress, + deviceName: deviceName, + encryptedDeviceSecret: 'asdf' + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + + describe('requestAuthSessionKeys', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.requestAuthSessionKeys({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should work', function(done) { + restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).yields(); + + privkeyDao.requestAuthSessionKeys({ + userId: emailAddress + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + + describe('verifyAuthentication', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.verifyAuthentication({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should work', function(done) { + var sessionId = '1'; + + restDaoStub.put.withArgs('asdf', '/auth/user/' + emailAddress + '/session/' + sessionId).yields(); + + privkeyDao.verifyAuthentication({ + userId: emailAddress, + sessionId: sessionId, + encryptedChallenge: 'asdf' + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + + describe('upload', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.upload({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should work', function(done) { + var key = { + _id: '12345' + }; + + restDaoStub.post.withArgs(key, '/privatekey/user/' + emailAddress + '/key/' + key._id).yields(); + + privkeyDao.upload({ + userId: emailAddress, + encryptedPrivateKey: key + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + + describe('requestDownload', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.requestDownload({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should work', function(done) { + var key = { + _id: '12345' + }; + + restDaoStub.get.withArgs({ + uri: '/privatekey/user/' + emailAddress + '/key/' + key._id + }).yields(); + + privkeyDao.requestDownload({ + userId: emailAddress, + keyId: key._id + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + + describe('download', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.download({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should work', function(done) { + var key = { + _id: '12345' + }; + + restDaoStub.get.withArgs({ + uri: '/privatekey/user/' + emailAddress + '/key/' + key._id + '/recovery/token' + }).yields(); + + privkeyDao.download({ + userId: emailAddress, + keyId: key._id, + recoveryToken: 'token' + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + + }); + +}); \ No newline at end of file diff --git a/test/unit/rest-dao-test.js b/test/unit/rest-dao-test.js index 57859dc..0bb2e06 100644 --- a/test/unit/rest-dao-test.js +++ b/test/unit/rest-dao-test.js @@ -145,6 +145,31 @@ define(function(require) { }); }); + describe('post', function() { + it('should fail', function() { + restDao.post('/asdf', {}, function(err) { + expect(err).to.exist; + expect(err.code).to.equal(500); + }); + + expect(requests.length).to.equal(1); + requests[0].respond(500, { + "Content-Type": "text/plain" + }, 'Internal error'); + }); + + it('should work', function() { + restDao.post('/asdf', {}, function(err, res, status) { + expect(err).to.not.exist; + expect(res).to.equal(''); + expect(status).to.equal(201); + }); + + expect(requests.length).to.equal(1); + requests[0].respond(201); + }); + }); + describe('put', function() { it('should fail', function() { restDao.put('/asdf', {}, function(err) {