diff --git a/Gruntfile.js b/Gruntfile.js index 9c8ae169e..9d73c081a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -184,6 +184,7 @@ module.exports = function(grunt) { 'bower_components/crypto-js/evpkdf.js', 'bower_components/crypto-js/cipher-core.js', 'bower_components/crypto-js/aes.js', + 'bower_components/crypto-js/pbkdf2.js', 'node_modules/cordova-plugin-qrscanner/dist/cordova-plugin-qrscanner-lib.min.js' ], diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js index 1d3b6ff4e..d31e6bd56 100644 --- a/src/js/services/encryptionService.js +++ b/src/js/services/encryptionService.js @@ -1,72 +1,106 @@ + 'use strict'; -angular.module('copayApp.services').factory('encryptionService', function($log) { +angular.module('copayApp.services').factory('encryptionService', function($log, secureStorageService) { var root = {}; - //lazy creation of cipher and decipher? + var keySize = 512; + var iterations = 1500; + var storageKey = 'encryptionKey'; // need a function to get the key var password = 'password'; - function _getGetOrCreateKey() { + function _generateKey() { + var salt = CryptoJS.lib.WordArray.random(128/8); + var passphrase = CryptoJS.lib.WordArray.random(128/8); + var key = CryptoJS.PBKDF2(passphrase, salt, { keySize: keySize/32, iterations: iterations }); - //crytpo.scrypt() - //crypto.createCipheriv() + $log.debug('Generated key: ' + key); + return key; + } + + /** + * + * @param {*} cb + */ + function _getOrCreateKey(cb) { + // TODO: Get from secure storage + secureStorageService.get(storageKey, function onKeyRetrieved(keyErr, keyHex) { + if (keyErr) { + cb(keyErr, null); + return; + } + + if (keyHex) { + var key = CryptoJS.enc.Hex.parse(keyHex); + cb(null, key); + return; + } + + key = _generateKey(); + var keyHex = CryptoJS.enc.Hex.stringify(key); + secureStorageService.set(storageKey, keyHex, function onKeyStored(storeErr) { + if (storeErr) { + $log.error('Error storing key.', storeErr); + cb(storeErr, null); + return; + } + + cb(null, key); + }); + + }); }; - function encryptUsingcrypto(str) { - var cipher = crypto.createCipher('aes256', password); + function _decryptUsingCryptoJS(str, key, iv) { + var plaintext = CryptoJS.AES.decrypt(str, key, { iv: iv}); + return plaintext; + } - cipher.on('readable', () => { - var data = cipher.read(); - if (data) { - encrypted += data.toString('hex'); + + function _encryptUsingCryptoJS(str, key) { + var iv = CryptoJS.lib.WordArray.random(16); + + var cipherParams = CryptoJS.AES.encrypt(str, key, { iv: iv }); + $log.debug('cipherText: ' + cipherParams.ciphertext); + + return { + ciphertext: cipherParams.ciphertext.toString(CryptoJS.enc.Base64), + opts: { + iv: iv.toString(CryptoJS.enc.Hex) } - }); - - cipher.on('end', () => { - console.log('Encrypted 1: ' + encrypted); - //cb(); - }); - - //cipher.write(str); - cipher.write(str); - cipher.end(); + }; } - function encryptUsingCryptoJS(str) { - var ciphertext = CryptoJS.AES.encrypt(str, password); - $log.debug('cipherText: ' + ciphertext); - } + root.decrypt = function(str, opts, cb) { + _getOrCreateKey(function onKey(err, key) { + if (err) { + $log.error('Failed to get or create key.', err); + cb(err, null); + return; + } + + var decrypted = _decryptUsingCryptoJS(str, key, opts.iv); + cb(null, decrypted); + }); + }; root.encrypt = function(str, cb) { $log.debug('*** crypto exists: ' + !!crypto); $log.debug('*** CryptoJS exists: ' + !!CryptoJS); - encryptUsingCryptoJS('I am a secret.'); - -/* - // var ciphertext = CryptoJS.AES.encrypt(str, password); - var cipher = crypto.createCipher('aes256', password); - - cipher.on('readable', () => { - var data = cipher.read(); - if (data) { - encrypted += data.toString('hex'); + _getOrCreateKey(function onKey(err, key){ + if (err) { + cb(err, null); + return; } - }); - - cipher.on('end', () => { - console.log('Encrypted: ' + encrypted); - //cb(); - }); - //cipher.write(str); - cipher.write('I am secret'); - cipher.end(); - */ - + var encrypted = _encryptUsingCryptoJS(str, key); + cb(null, encrypted); + }); }; + root.encryptedObjectFromString = function(str) { try { @@ -82,5 +116,52 @@ angular.module('copayApp.services').factory('encryptionService', function($log) } }; + var JsonFormatter = { + stringify: function (cipherParams) { + // create json object with ciphertext + var jsonObj = { + ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64) + }; + // optionally add iv and salt + if (cipherParams.iv) { + jsonObj.iv = cipherParams.iv.toString(); + } + + if (cipherParams.salt) { + jsonObj.s = cipherParams.salt.toString(); + } + + // stringify json object + return JSON.stringify(jsonObj); + }, + parse: function (jsonStr) { + // parse json string + var jsonObj = JSON.parse(jsonStr); + // extract ciphertext from json object, and create cipher params object + var cipherParams = CryptoJS.lib.CipherParams.create({ + ciphertext: CryptoJS.enc.Base64.parse(jsonObj.ct) + }); + + // optionally extract iv and salt + if (jsonObj.iv) { + cipherParams.iv = CryptoJS.enc.Hex.parse(jsonObj.iv) + } + + if (jsonObj.s) { + cipherParams.salt = CryptoJS.enc.Hex.parse(jsonObj.s) + } + + return cipherParams; + } + }; + + /* + var encrypted = CryptoJS.AES.encrypt("Message", "Secret Passphrase", { format: JsonFormatter }); + alert(encrypted); // {"ct":"tZ4MsEnfbcDOwqau68aOrQ==","iv":"8a8c8fd8fe33743d3638737ea4a00698","s":"ba06373c8f57179c"} + + var decrypted = CryptoJS.AES.decrypt(encrypted, "Secret Passphrase", { format: JsonFormatter }); + alert(decrypted.toString(CryptoJS.enc.Utf8)); // Message + */ + return root; }); \ No newline at end of file diff --git a/src/js/services/jsonEncryptionService.js b/src/js/services/jsonEncryptionService.js new file mode 100644 index 000000000..23133ce9b --- /dev/null +++ b/src/js/services/jsonEncryptionService.js @@ -0,0 +1,75 @@ +(function() { + 'use strict'; + + angular + .module('copayApp.services') + .factory('jsonEncryptionService', jsonEncryptionService); + + function jsonEncryptionService($log) { + var currentVersion = 1; + + var service = { + isEncrypted: isEncrypted, + parse: parse, + stringify: stringify + }; + return service; + + function isEncrypted(jsonStr) { + try { + var jsonObj = JSON.parse(jsonStr); + } catch (e) { + $log.error('Failed to parse JSON when looking for encypted data.', e); + return false; + } + return jsonObj.version && jsonObj.encryptedData; + } + + function parse(jsonStr) { + + var jsonObj = JSON.parse(jsonStr); + + if (!(jsonObj.version && jsonObj.version === currentVersion)) { + throw new Error('Incompatible version.'); + } + + var encryptedData = jsonObj.encryptedData; + // extract ciphertext from json object, and create cipher params object + var ciphertext = CryptoJS.enc.Base64.parse(encryptedData.ciphertext) + var iv = CryptoJS.enc.Hex.parse(encryptedData.iv); + + // TODO: Need to convert iv into WordArray? + + return { + ciphertext: ciphertext, + opts: { + iv: iv + } + } + } + + /** + * + * @param {string, Base64 encoded} ciphertext + * @param {*} opts So it flexible enough to handle other schemes in future. + * @throws If cipherParams does not include the iv. + */ + function stringify(ciphertext, opts) { + var iv = opts.iv; + + if(!iv) { + throw new Error('Must include iv.'); + } + + var encryptedData = { + ciphertext: ciphertext, + iv: iv + }; + + return JSON.stringify({ + version: currentVersion, + encryptedData: encryptedData + }); + } + }; +})(); \ No newline at end of file diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index acfe36cd4..c8c880b67 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -1,6 +1,6 @@ 'use strict'; angular.module('copayApp.services') - .factory('storageService', function(appConfigService, encryptionService, logHeader, fileStorageService, localStorageService, sjcl, $log, lodash, platformInfo, secureStorageService, $timeout) { + .factory('storageService', function(appConfigService, encryptionService, jsonEncryptionService, logHeader, fileStorageService, localStorageService, sjcl, $log, lodash, platformInfo, secureStorageService, $timeout) { var root = {}; var storage; @@ -200,6 +200,50 @@ angular.module('copayApp.services') }); }; + function _migrateUnencryptedProfile(profileStr, cb) { + copayDecryptOnMobile(profileStr, function(decryptErr, decryptedStr) { + if (decryptErr) return cb(decryptErr, null); + var profile; + try { + profile = Profile.fromString(decryptedStr); + } catch (e) { + $log.error('Could not read profile:', e); + return cb(new Error('Could not read profile.'), null); + } + + // This is the only change to the contents of the profile. + profile.setAppVersion(appConfigService.version); + var newProfileStr = profile.toObj(); + + encryptionService.encrypt(newProfileStr, function onProfileEncrypted(encryptErr, encryptedProfile){ + if (encryptErr) { + $log.error('Failed to encrypt profile.', encryptErr); + cb(encryptErr, null); + return; + } + + var persistentProfileStr; + try { + persistentProfileStr = jsonEncryptionService.stringify(encryptedProfile.ciphertext, encryptedProfile.opts); + } catch(e) { + $log.error('Failed to stringify to encrypted profile.', e); + cb(e, null); + return; + } + + storage.set('profile', persistentProfileStr, function onEncryptedProfileStored(setErr) { + if (setErr) { + $log.error('Failed to store encrypted profile.', setErr); + cb(setErr, null); + return; + } + + cb (null, profile); + }); + }); + }); + } + /** * * @param {getProfileCallback} cb @@ -217,26 +261,43 @@ angular.module('copayApp.services') return cb(null, null); } - var encryptedProfile = encryptionService.encryptedObjectFromString(profileStr); - if (!encryptedProfile) { + var isEncrypted = jsonEncryptionService.isEncrypted(profileStr); + if (isEncrypted) { + $log.debug('profile was encrypted.'); + + var encryptedProfileObject; + try { + encryptedProfileObject = jsonEncryptionService.parse(profileStr); + } catch (e) { + $log.error('Failed to parse encrypted profile.', e); + cb(e, null); + return; + } - copayDecryptOnMobile(profileStr, function(decryptErr, decryptedStr) { - if (decryptErr) return cb(decryptErr, null); - var profile; - try { - profile = Profile.fromString(decryptedStr); - } catch (e) { - $log.debug('Could not read profile:', e); - return cb(new Error('Could not read profile.'), null); + encryptionService.decrypt( + encryptedProfileObject.ciphertext, + encryptedProfileObject.opts, + function onDecrypted(decryptionError, decryptedProfile) { + if (decryptionError) { + $log.error('Failed to decrypt profile'); + cb(decryptionError, null); + return + } + + var profileObj = Profile.fromString(decryptedProfile); + cb(null, profileObj); + }); + + } else { + _migrateUnencryptedProfile(profileStr, function onProfileMigrated(migrationErr, migratedProfile){ + if (migrationErr) { + $log.error('Failed to migrate the profile.', migrationErr); + cb(migraionErr, null); + return; } - encryptedProfile = encryptionService.encrypt(profile); - $log.debug('encryptedProfile'); - - //cb(null, profile) + cb(null, migratedProfile); }); - } else { - $log.debug('profile was encrypted.'); }