From 1e1f5deb2a4ee98d754f9601f5bd27d10d3918b8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Pasma Date: Mon, 4 Jun 2018 21:37:36 +0200 Subject: [PATCH 01/16] iOS 9.3 fix --- src/js/controllers/modals/search.js | 12 ++++++------ src/js/services/walletService.js | 20 ++++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/js/controllers/modals/search.js b/src/js/controllers/modals/search.js index 55d51bfcc..439df891d 100644 --- a/src/js/controllers/modals/search.js +++ b/src/js/controllers/modals/search.js @@ -104,12 +104,12 @@ angular.module('copayApp.controllers').controller('searchController', function($ }; $scope.searchOnBlockchain = function(searchTerm) { - const url = 'https://explorer.bitcoin.com/'+$scope.wallet.coin+'/search/' + searchTerm; - const optIn = true; - const title = null; - const message = gettextCatalog.getString('Search on Explorer.Bitcoin.com'); - const okText = gettextCatalog.getString('Open Explorer'); - const cancelText = gettextCatalog.getString('Go Back'); + var url = 'https://explorer.bitcoin.com/'+$scope.wallet.coin+'/search/' + searchTerm; + var optIn = true; + var title = null; + var message = gettextCatalog.getString('Search on Explorer.Bitcoin.com'); + var okText = gettextCatalog.getString('Open Explorer'); + var cancelText = gettextCatalog.getString('Go Back'); externalLinkService.open(url, optIn, title, message, okText, cancelText); }; diff --git a/src/js/services/walletService.js b/src/js/services/walletService.js index 641e7e612..e8c636d79 100644 --- a/src/js/services/walletService.js +++ b/src/js/services/walletService.js @@ -1179,17 +1179,21 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim if (signedTxp.status == 'accepted') { ongoingProcess.set('broadcastingTx', true, customStatusHandler); - function handleBroadcastTx(err, broadcastedTxp) { - ongoingProcess.set('broadcastingTx', false, customStatusHandler); - if (err) return cb(bwcError.msg(err)); - $rootScope.$emit('Local/TxAction', wallet.id); - return cb(null, broadcastedTxp); - } if (signedTxp.payProUrl && signedTxp.coin == 'bch') { - payproService.broadcastBchTx(signedTxp, handleBroadcastTx); + payproService.broadcastBchTx(signedTxp, function(err, broadcastedTxp) { + ongoingProcess.set('broadcastingTx', false, customStatusHandler); + if (err) return cb(bwcError.msg(err)); + $rootScope.$emit('Local/TxAction', wallet.id); + return cb(null, broadcastedTxp); + }); } else { - root.broadcastTx(wallet, signedTxp, handleBroadcastTx); + root.broadcastTx(wallet, signedTxp, function(err, broadcastedTxp) { + ongoingProcess.set('broadcastingTx', false, customStatusHandler); + if (err) return cb(bwcError.msg(err)); + $rootScope.$emit('Local/TxAction', wallet.id); + return cb(null, broadcastedTxp); + }); } } else { $rootScope.$emit('Local/TxAction', wallet.id); From 6c6d90ebfef5c24079a355601ccbee004ada2e50 Mon Sep 17 00:00:00 2001 From: Sebastiaan Pasma Date: Tue, 5 Jun 2018 11:15:33 +0200 Subject: [PATCH 02/16] Cleaner way without redundant code --- src/js/services/walletService.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/js/services/walletService.js b/src/js/services/walletService.js index e8c636d79..774fa0906 100644 --- a/src/js/services/walletService.js +++ b/src/js/services/walletService.js @@ -1180,20 +1180,17 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim if (signedTxp.status == 'accepted') { ongoingProcess.set('broadcastingTx', true, customStatusHandler); + var handleBroadcastTx = function(err, broadcastedTxp) { + ongoingProcess.set('broadcastingTx', false, customStatusHandler); + if (err) return cb(bwcError.msg(err)); + $rootScope.$emit('Local/TxAction', wallet.id); + return cb(null, broadcastedTxp); + }; + if (signedTxp.payProUrl && signedTxp.coin == 'bch') { - payproService.broadcastBchTx(signedTxp, function(err, broadcastedTxp) { - ongoingProcess.set('broadcastingTx', false, customStatusHandler); - if (err) return cb(bwcError.msg(err)); - $rootScope.$emit('Local/TxAction', wallet.id); - return cb(null, broadcastedTxp); - }); + payproService.broadcastBchTx(signedTxp, handleBroadcastTx); } else { - root.broadcastTx(wallet, signedTxp, function(err, broadcastedTxp) { - ongoingProcess.set('broadcastingTx', false, customStatusHandler); - if (err) return cb(bwcError.msg(err)); - $rootScope.$emit('Local/TxAction', wallet.id); - return cb(null, broadcastedTxp); - }); + root.broadcastTx(wallet, signedTxp, handleBroadcastTx); } } else { $rootScope.$emit('Local/TxAction', wallet.id); From 6d90a0277a168fe1759d96ebce2e3f69381b6175 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 15 Jun 2018 21:12:25 +1200 Subject: [PATCH 03/16] Fix equality test for credentials in profile merging, to match on something unique. --- src/js/models/profile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/models/profile.js b/src/js/models/profile.js index 7690d1c2d..56cc48e2e 100644 --- a/src/js/models/profile.js +++ b/src/js/models/profile.js @@ -76,7 +76,7 @@ Profile.prototype.merge = function(other) { other.credentials.forEach(function(otherCredential) { var credentialExists = false; thisProfile.credentials.forEach(function(thisCredential) { - if (otherCredential.walletId === thisCredential.walletId) { + if (otherCredential.mnemonic === thisCredential.mnemonic) { credentialExists = true; } }); From 35ba9bcb3c6d0e3861a496c9924f6a8702c2927c Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Sun, 17 Jun 2018 16:02:51 +1200 Subject: [PATCH 04/16] Test for merging profiles where each one contains a different copayer of the same shared wallet. --- src/js/models/profile.spec.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/js/models/profile.spec.js diff --git a/src/js/models/profile.spec.js b/src/js/models/profile.spec.js new file mode 100644 index 000000000..1287ab1e4 --- /dev/null +++ b/src/js/models/profile.spec.js @@ -0,0 +1,32 @@ +describe('Profiles', function(){ + var profileNew, + profileNewWithCopayer1String = '{"version":"1.0.0","appVersion":"4.11.0","createdOn":1529205102152,"credentials":[{"coin":"bch","network":"livenet","xPrivKey":"xprv9s21ZrQH143K2NCcpNJx1AeP4KQpdX32YdQZYbCTDaggotfSPR3vMKHyATyHdnPz5Qi4kDKUEuQPFGa6imiihby8GYkWThAcG3KjffT1HeU","xPubKey":"xpub6C9CWjLy5XasVGNtadbMwq51ZyW3crgHMA7VMbcQcb6thzisFFzeAKrKQhW1RD6u1DBhQPJb9Az8NjJ9PYFPyw37C6J6HTB7NxBjMBttUC7","requestPrivKey":"c1dc4bd23639e6058835e6039526d7cc7876ef5d7a8ace787606921973b8ae2f","requestPubKey":"0208aea4fcbed1511924e4af167915c9e5f8638e88f5904c3ed97dbae87000644c","copayerId":"00adbea27e4eaba8ba4da39ebc1ad966bf1f10f786eab4caa918f645bea72629","publicKeyRing":[{"xPubKey":"xpub6C9CWjLy5XasVGNtadbMwq51ZyW3crgHMA7VMbcQcb6thzisFFzeAKrKQhW1RD6u1DBhQPJb9Az8NjJ9PYFPyw37C6J6HTB7NxBjMBttUC7","requestPubKey":"0208aea4fcbed1511924e4af167915c9e5f8638e88f5904c3ed97dbae87000644c"}],"walletId":"9c49c70d-f18e-4b98-9d2f-1cd7663b7814","walletName":"Personal Wallet","m":1,"n":1,"walletPrivKey":"a5fd054f4445c5f3a46ba83977348cbeff59cfcaffa912c3550c6855ad86bd6c","personalEncryptingKey":"VEk6iRC6VUxDOV+PcwfgGQ==","sharedEncryptingKey":"gyp7GQjDeRUoa0VYDpAZ/g==","copayerName":"me","mnemonic":"art ability taxi tennis scheme cage room bunker gentle degree peasant juice","entropySource":"a0d41b5f876335bd61def4ad5277beefadc5981d15e23c488836c91c0add864a","mnemonicHasPassphrase":false,"derivationStrategy":"BIP44","account":0,"compliantDerivation":true,"addressType":"P2PKH"},{"coin":"btc","network":"livenet","xPrivKey":"xprv9s21ZrQH143K2NCcpNJx1AeP4KQpdX32YdQZYbCTDaggotfSPR3vMKHyATyHdnPz5Qi4kDKUEuQPFGa6imiihby8GYkWThAcG3KjffT1HeU","xPubKey":"xpub6C9CWjLy5XasVGNtadbMwq51ZyW3crgHMA7VMbcQcb6thzisFFzeAKrKQhW1RD6u1DBhQPJb9Az8NjJ9PYFPyw37C6J6HTB7NxBjMBttUC7","requestPrivKey":"c1dc4bd23639e6058835e6039526d7cc7876ef5d7a8ace787606921973b8ae2f","requestPubKey":"0208aea4fcbed1511924e4af167915c9e5f8638e88f5904c3ed97dbae87000644c","copayerId":"b7e068e7f01a84be25383037bdded240565a97897791b034e390201c411b1ff8","publicKeyRing":[{"xPubKey":"xpub6C9CWjLy5XasVGNtadbMwq51ZyW3crgHMA7VMbcQcb6thzisFFzeAKrKQhW1RD6u1DBhQPJb9Az8NjJ9PYFPyw37C6J6HTB7NxBjMBttUC7","requestPubKey":"0208aea4fcbed1511924e4af167915c9e5f8638e88f5904c3ed97dbae87000644c"}],"walletId":"8180ad5e-8e0a-40e2-88b6-507badee3de4","walletName":"Personal Wallet","m":1,"n":1,"walletPrivKey":"4dcc3e1ce1fb8e22e14dfd1501d5e96aca05d43f8aef01bcc761a8a3d6916068","personalEncryptingKey":"VEk6iRC6VUxDOV+PcwfgGQ==","sharedEncryptingKey":"1WeeJVxGFB7wzS4w4S+4Tw==","copayerName":"me","mnemonic":"art ability taxi tennis scheme cage room bunker gentle degree peasant juice","entropySource":"a0d41b5f876335bd61def4ad5277beefadc5981d15e23c488836c91c0add864a","mnemonicHasPassphrase":false,"derivationStrategy":"BIP44","account":0,"compliantDerivation":true,"addressType":"P2PKH"},{"coin":"bch","network":"livenet","xPrivKey":"xprv9s21ZrQH143K2ZcybcyUf3bAfq3SAvBZbMPz7sMp8E8baXLc9GAgoVDho6iqPSAwvf124mabfWqPntRB93KPccmywNCQLmn6ukoGKqYWB1r","xPubKey":"xpub6DQPHX7xWx8EcAAtWAwmE5a4U4LoHCZHSkdcmgZpRn1A1LvupaWNiDf5pWEK2t4PdnxXWrGYS7uzjWWsmhXJgpkMfKRB3seQXk2KZngUyPc","requestPrivKey":"cdf993dded8c8d5d45006852f56624efc6637120225509c351c32b973bf21f5d","requestPubKey":"0260197aa6c1e7a1889d3686bd4e903931134c949bd8d85347eb7464d2ed66818b","copayerId":"449732a920b1315d730edb83fd4d4751f3f99a396844a91a2dd2bd981effeafb","publicKeyRing":[{"xPubKey":"xpub6DQPHX7xWx8EcAAtWAwmE5a4U4LoHCZHSkdcmgZpRn1A1LvupaWNiDf5pWEK2t4PdnxXWrGYS7uzjWWsmhXJgpkMfKRB3seQXk2KZngUyPc","requestPubKey":"0260197aa6c1e7a1889d3686bd4e903931134c949bd8d85347eb7464d2ed66818b","copayerName":"Alice"},{"xPubKey":"xpub6D94BNBSMLvAfsUs6p7Nt64zdeno4vMYWEAU8d7WJ7X44FaeWjZcuR9LyDtUHeJGKx475vfNwhgc8rAsFYN37JFb9ojavAhUas4deTeJ3bB","requestPubKey":"022d2938551609c30b5640c202ddf07cecd828e204647846fc3dc1c2fa7299aea1","copayerName":"Bob"}],"walletId":"4e5f4de5-20fd-4b69-9337-ef05ea204b8b","walletName":"Alice and Bob\'s Wallet","m":2,"n":2,"walletPrivKey":"a624db620778bb9c3b6645dfe734c0c55c1b8946d5c474904d10ed4c227f2574","personalEncryptingKey":"58lHfKK/gfwg6uGk6fqi7g==","sharedEncryptingKey":"IrVSZuuVr0q0CjJ+mdOM8A==","copayerName":"Alice","mnemonic":"enrich change conduct popular angle hover easy left month demand parade game","entropySource":"50c019f295dd3bc8ae22327c96cb2535809dbbdd2496b2946b64c81ff2b8c3c2","mnemonicHasPassphrase":false,"derivationStrategy":"BIP44","account":0,"compliantDerivation":true,"addressType":"P2SH"}],"disclaimerAccepted":true,"checked":{"9c49c70d-f18e-4b98-9d2f-1cd7663b7814":true,"8180ad5e-8e0a-40e2-88b6-507badee3de4":true,"4e5f4de5-20fd-4b69-9337-ef05ea204b8b":true},"checkedUA":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36"}', + profileOld, + profileOldWithCopayer2String = '{"version":"1.0.0","createdOn":1529205124796,"credentials":[{"coin":"bch","network":"livenet","xPrivKey":"xprv9s21ZrQH143K2GSFBcThSsdmTXUVKwHaifwLh5CY4swQGE1PSM5ESg8mjiXZg3U8SUfvQsyzTeWSTxCwPwvqwJHzsPDPPGZ781yswvPeCJL","xPubKey":"xpub6C27o1AABpLovccSoVJUG8KCH9EiR6949LTtsLY8zLd6GXaAaFmQPkJrfm5QCHQRT7nkJKRAMAZrXtPbEixHTPMrf48Q8gVbAQ25odupVCW","requestPrivKey":"6be2636e3a5523e34c0804e775f72fdcb2253f30e02275c1da9fae57bcdfc6d8","requestPubKey":"036c2c1c3ecf63de8e8aaa8e63720348df437ba9267b209c83daa1e5b16a37620c","copayerId":"01543b105191e2d29b9cbba7b1a9464360bc6dd1adcbcd6de326ca6d67caf33f","publicKeyRing":[{"xPubKey":"xpub6C27o1AABpLovccSoVJUG8KCH9EiR6949LTtsLY8zLd6GXaAaFmQPkJrfm5QCHQRT7nkJKRAMAZrXtPbEixHTPMrf48Q8gVbAQ25odupVCW","requestPubKey":"036c2c1c3ecf63de8e8aaa8e63720348df437ba9267b209c83daa1e5b16a37620c"}],"walletId":"ccf23d6d-bb4e-484f-b237-8da0299166a1","walletName":"Personal Wallet","m":1,"n":1,"walletPrivKey":"6f4a51d9b4753eaf90fbe25a27b59c12b548a7f294b2212f68684c9e2343e1a4","personalEncryptingKey":"wzVaz18pLOhMrWHXSifjyw==","sharedEncryptingKey":"u1818GdNZi8FE4MAWLKqpA==","copayerName":"me","mnemonic":"protect bike roof thunder dilemma gas online mask sleep blush kit follow","entropySource":"e1530f3b3d2eaea95a689f75e203ffc8ef3e7dda51c7600336a5aac297806293","mnemonicHasPassphrase":false,"derivationStrategy":"BIP44","account":0,"compliantDerivation":true,"addressType":"P2PKH"},{"coin":"btc","network":"livenet","xPrivKey":"xprv9s21ZrQH143K2GSFBcThSsdmTXUVKwHaifwLh5CY4swQGE1PSM5ESg8mjiXZg3U8SUfvQsyzTeWSTxCwPwvqwJHzsPDPPGZ781yswvPeCJL","xPubKey":"xpub6C27o1AABpLovccSoVJUG8KCH9EiR6949LTtsLY8zLd6GXaAaFmQPkJrfm5QCHQRT7nkJKRAMAZrXtPbEixHTPMrf48Q8gVbAQ25odupVCW","requestPrivKey":"6be2636e3a5523e34c0804e775f72fdcb2253f30e02275c1da9fae57bcdfc6d8","requestPubKey":"036c2c1c3ecf63de8e8aaa8e63720348df437ba9267b209c83daa1e5b16a37620c","copayerId":"aaa253bffbc6265bec18672a0410b4dea693ef28d3b0241cea360a17f6448e77","publicKeyRing":[{"xPubKey":"xpub6C27o1AABpLovccSoVJUG8KCH9EiR6949LTtsLY8zLd6GXaAaFmQPkJrfm5QCHQRT7nkJKRAMAZrXtPbEixHTPMrf48Q8gVbAQ25odupVCW","requestPubKey":"036c2c1c3ecf63de8e8aaa8e63720348df437ba9267b209c83daa1e5b16a37620c"}],"walletId":"3843bee7-955f-4621-9b8d-c000afa1da1d","walletName":"Personal Wallet","m":1,"n":1,"walletPrivKey":"a9b6a00180b9d7be05aa5562575d46b2ebe1130b5899796d9b2d340917cb105d","personalEncryptingKey":"wzVaz18pLOhMrWHXSifjyw==","sharedEncryptingKey":"lmjMQWWJdIuW4qikpuZlFQ==","copayerName":"me","mnemonic":"protect bike roof thunder dilemma gas online mask sleep blush kit follow","entropySource":"e1530f3b3d2eaea95a689f75e203ffc8ef3e7dda51c7600336a5aac297806293","mnemonicHasPassphrase":false,"derivationStrategy":"BIP44","account":0,"compliantDerivation":true,"addressType":"P2PKH"},{"coin":"bch","network":"livenet","xPrivKey":"xprv9s21ZrQH143K2GysbnXofDJh5UmhqvqvnGEw3raagcHr7qP8bSkFB33hEnAXWkxzg2VNF7ttkMVuU8zAnQ7rJHDvYaCB1T15fNek2fw62XC","xPubKey":"xpub6D94BNBSMLvAfsUs6p7Nt64zdeno4vMYWEAU8d7WJ7X44FaeWjZcuR9LyDtUHeJGKx475vfNwhgc8rAsFYN37JFb9ojavAhUas4deTeJ3bB","requestPrivKey":"f81b5dad3a83bcdb44a589791448e7c4acc83adce692432e26c57d4ad689ee25","requestPubKey":"022d2938551609c30b5640c202ddf07cecd828e204647846fc3dc1c2fa7299aea1","copayerId":"774e71854e43c6806d900b76f7b801aebe2f061b2da2aa42cdba2ffbdff64737","publicKeyRing":[{"xPubKey":"xpub6DQPHX7xWx8EcAAtWAwmE5a4U4LoHCZHSkdcmgZpRn1A1LvupaWNiDf5pWEK2t4PdnxXWrGYS7uzjWWsmhXJgpkMfKRB3seQXk2KZngUyPc","requestPubKey":"0260197aa6c1e7a1889d3686bd4e903931134c949bd8d85347eb7464d2ed66818b","copayerName":"Alice"},{"xPubKey":"xpub6D94BNBSMLvAfsUs6p7Nt64zdeno4vMYWEAU8d7WJ7X44FaeWjZcuR9LyDtUHeJGKx475vfNwhgc8rAsFYN37JFb9ojavAhUas4deTeJ3bB","requestPubKey":"022d2938551609c30b5640c202ddf07cecd828e204647846fc3dc1c2fa7299aea1","copayerName":"Bob"}],"walletId":"4e5f4de5-20fd-4b69-9337-ef05ea204b8b","walletName":"Alice and Bob\'s Wallet","m":2,"n":2,"walletPrivKey":"a624db620778bb9c3b6645dfe734c0c55c1b8946d5c474904d10ed4c227f2574","personalEncryptingKey":"VU5HJrwLAAZyz+npJ9V9Jw==","sharedEncryptingKey":"IrVSZuuVr0q0CjJ+mdOM8A==","copayerName":"Bob","mnemonic":"raccoon degree refuse night result tag elbow game mule board quantum october","entropySource":"c575a2f4ce7a5962ca2819907625a8844ce7ab867376279fa4dbe1c0fdf360b5","mnemonicHasPassphrase":false,"derivationStrategy":"BIP44","account":0,"compliantDerivation":true,"addressType":"P2SH"}],"disclaimerAccepted":true,"checked":{"ccf23d6d-bb4e-484f-b237-8da0299166a1":true,"3843bee7-955f-4621-9b8d-c000afa1da1d":true,"4e5f4de5-20fd-4b69-9337-ef05ea204b8b":true},"checkedUA":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:60.0) Gecko/20100101 Firefox/60.0"}'; + + + beforeEach(function(){ + module('copayApp.services'); + profileNew = Profile.fromString(profileNewWithCopayer1String); + profileOld = Profile.fromString(profileOldWithCopayer2String); + + }); + + it('merge() one copayer for same shared wallet in each profile.', function() { + expect(profileNew.credentials[2].mnemonic).toBe('enrich change conduct popular angle hover easy left month demand parade game'); + expect(profileNew.credentials[2].walletId).toBe('4e5f4de5-20fd-4b69-9337-ef05ea204b8b'); + expect(profileOld.credentials[2].mnemonic).toBe('raccoon degree refuse night result tag elbow game mule board quantum october'); + expect(profileOld.credentials[2].walletId).toBe('4e5f4de5-20fd-4b69-9337-ef05ea204b8b'); + + expect(profileNew.credentials.length).toBe(3); + expect(profileOld.credentials.length).toBe(3); + + profileNew.merge(profileOld); + + expect(profileNew.credentials.length).toBe(6); + expect(profileNew.credentials[2].mnemonic).toBe('enrich change conduct popular angle hover easy left month demand parade game'); + expect(profileNew.credentials[2].walletId).toBe('4e5f4de5-20fd-4b69-9337-ef05ea204b8b'); + expect(profileNew.credentials[5].mnemonic).toBe('raccoon degree refuse night result tag elbow game mule board quantum october'); + expect(profileNew.credentials[5].walletId).toBe('4e5f4de5-20fd-4b69-9337-ef05ea204b8b'); + }); +}); \ No newline at end of file From e215ecfb526038b10e47d39d98d22630ddd57008 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 28 Jun 2018 08:32:45 +1200 Subject: [PATCH 05/16] Initial encryption test is working on Chrome and iOS. --- Gruntfile.js | 10 ++++ bower.json | 1 + src/js/services/encryptionService.js | 86 ++++++++++++++++++++++++++++ src/js/services/storageService.js | 55 +++++++++++++++--- 4 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 src/js/services/encryptionService.js diff --git a/Gruntfile.js b/Gruntfile.js index f9ed59621..9c8ae169e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -175,6 +175,16 @@ module.exports = function(grunt) { 'src/js/trezor-url.js', 'bower_components/trezor-connect/connect.js', 'node_modules/bezier-easing/dist/bezier-easing.min.js', + + 'bower_components/crypto-js/core.js', + 'bower_components/crypto-js/enc-base64.js', + 'bower_components/crypto-js/hmac.js', + 'bower_components/crypto-js/md5.js', + 'bower_components/crypto-js/sha1.js', + 'bower_components/crypto-js/evpkdf.js', + 'bower_components/crypto-js/cipher-core.js', + 'bower_components/crypto-js/aes.js', + 'node_modules/cordova-plugin-qrscanner/dist/cordova-plugin-qrscanner-lib.min.js' ], dest: 'www/js/app.js' diff --git a/bower.json b/bower.json index 08a82d9b8..a66685254 100644 --- a/bower.json +++ b/bower.json @@ -11,6 +11,7 @@ "angular-gettext": "2.2.1", "angular-moment": "0.10.1", "angular-qrcode": "bitpay/angular-qrcode#~6.3.0", + "crypto-js": "^3.1.9", "ionic": "https://github.com/ionic-team/ionic-v1.git", "moment": "2.10.3", "ng-lodash": "0.2.3", diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js new file mode 100644 index 000000000..1d3b6ff4e --- /dev/null +++ b/src/js/services/encryptionService.js @@ -0,0 +1,86 @@ +'use strict'; + +angular.module('copayApp.services').factory('encryptionService', function($log) { + var root = {}; + + //lazy creation of cipher and decipher? + + // need a function to get the key + var password = 'password'; + + function _getGetOrCreateKey() { + + //crytpo.scrypt() + //crypto.createCipheriv() + }; + + function encryptUsingcrypto(str) { + var cipher = crypto.createCipher('aes256', password); + + cipher.on('readable', () => { + var data = cipher.read(); + if (data) { + encrypted += data.toString('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.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'); + } + }); + + cipher.on('end', () => { + console.log('Encrypted: ' + encrypted); + //cb(); + }); + + //cipher.write(str); + cipher.write('I am secret'); + cipher.end(); + */ + + }; + + root.encryptedObjectFromString = function(str) { + try { + var parsed = JSON.parse(str); + } catch(e) { + return null; + } + + if (parsed.encryptionVersion) { + return parsed; + } else { + return null; + } + }; + + return root; +}); \ No newline at end of file diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index a2d85950b..acfe36cd4 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, logHeader, fileStorageService, localStorageService, sjcl, $log, lodash, platformInfo, secureStorageService, $timeout) { + .factory('storageService', function(appConfigService, encryptionService, logHeader, fileStorageService, localStorageService, sjcl, $log, lodash, platformInfo, secureStorageService, $timeout) { var root = {}; var storage; @@ -32,7 +32,7 @@ angular.module('copayApp.services') // This is only used in Copay, we used to encrypt profile // using device's UUID. - var decryptOnMobile = function(text, cb) { + var copayDecryptOnMobile = function(text, cb) { var json; try { json = JSON.parse(text); @@ -121,11 +121,11 @@ angular.module('copayApp.services') root.storeProfile = function(profile, cb) { var profileString = profile.toObj(); - if (platformInfo.isNW) { + //if (platformInfo.isNW) { storage.set('profile', profileString, cb); - } else { - secureStorageService.set('profile', profileString, cb); - } + //} else { + // secureStorageService.set('profile', profileString, cb); + //} }; /** @@ -150,7 +150,7 @@ angular.module('copayApp.services') return cb(null, null); } - decryptOnMobile(profileStr, function(decryptErr, decryptedStr) { + copayDecryptOnMobile(profileStr, function(decryptErr, decryptedStr) { if (decryptErr) return cb(decryptErr, null); var profile; try { @@ -205,6 +205,46 @@ angular.module('copayApp.services') * @param {getProfileCallback} cb */ root.getProfile = function(cb) { + $log.debug('getProfile()'); + storage.get('profile', function onProfileRetrieved(getErr, profileStr){ + if (getErr) { + $log.error(getErr); + return cb(getErr, null); + } + + if (!profileStr) { + $log.debug('No string loaded, returning nothing.'); + return cb(null, null); + } + + var encryptedProfile = encryptionService.encryptedObjectFromString(profileStr); + if (!encryptedProfile) { + + 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); + } + + encryptedProfile = encryptionService.encrypt(profile); + $log.debug('encryptedProfile'); + + //cb(null, profile) + }); + } else { + $log.debug('profile was encrypted.'); + } + + + + }); + + /* + if (platformInfo.isNW) { storage.get('profile', function(getErr, getStr) { _onOldProfileRetrieved(getErr, getStr, cb); @@ -248,6 +288,7 @@ angular.module('copayApp.services') }); }); }); + */ }; root.setFeedbackInfo = function(feedbackValues, cb) { From ecaa13f6d4f075889e8336b6e8e05f5e2885b7b1 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 28 Jun 2018 17:33:38 +1200 Subject: [PATCH 06/16] Migrating profile and storing key, when getting profile. --- Gruntfile.js | 1 + src/js/services/encryptionService.js | 173 +++++++++++++++++------ src/js/services/jsonEncryptionService.js | 75 ++++++++++ src/js/services/storageService.js | 95 ++++++++++--- 4 files changed, 281 insertions(+), 63 deletions(-) create mode 100644 src/js/services/jsonEncryptionService.js 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.'); } From 52b9a206c3f7a467034f90fca6ce6b76ade6fcf9 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 28 Jun 2018 20:31:22 +1200 Subject: [PATCH 07/16] Creation of new profile on startup works. --- src/js/services/encryptionService.js | 44 +++++++++++++- src/js/services/jsonEncryptionService.js | 10 ++-- src/js/services/storageService.js | 75 +++++++----------------- 3 files changed, 67 insertions(+), 62 deletions(-) diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js index d31e6bd56..00830af65 100644 --- a/src/js/services/encryptionService.js +++ b/src/js/services/encryptionService.js @@ -53,17 +53,54 @@ angular.module('copayApp.services').factory('encryptionService', function($log, }); }; + /** + * + * @param {*} str + * @param {CryptoJS.WordArray} key + * @param {string, hex} iv + */ function _decryptUsingCryptoJS(str, key, iv) { - var plaintext = CryptoJS.AES.decrypt(str, key, { iv: iv}); - return plaintext; + $log.debug('decrypt() str: ' + str); + $log.debug('decrypt() using iv:' + iv + ', key: ' + JSON.stringify(key)); + + var ivWords = CryptoJS.enc.Hex.parse(iv); + var plaintext = CryptoJS.AES.decrypt(str, key, { iv: ivWords }); + $log.debug('plaintext', JSON.stringify(plaintext)); + + var plaintextWords = CryptoJS.lib.WordArray.create(); + plaintextWords.init(plaintext.words, plaintext.sigBytes); + $log.debug('plaintextWords', JSON.stringify(plaintextWords)); + var plaintextString = plaintextWords.toString(CryptoJS.enc.Utf8); + $log.debug('plaintextString: ', JSON.stringify(plaintextString)); + + return plaintextString; } function _encryptUsingCryptoJS(str, key) { + $log.debug('encrypt() str: ' + str); var iv = CryptoJS.lib.WordArray.random(16); + $log.debug('Encrypting profile: ', JSON.stringify(str)); + var cipherParams = CryptoJS.AES.encrypt(str, key, { iv: iv }); - $log.debug('cipherText: ' + cipherParams.ciphertext); + var ciphertext = cipherParams.ciphertext.toString(CryptoJS.enc.Base64); + var iv = iv.toString(CryptoJS.enc.Hex); + $log.debug('ciphertext: ' + ciphertext); + $log.debug('iv: ' + iv); + + + root.decrypt(ciphertext, {iv: iv}, function onDecryptionTest(err, decrypted){ + if (err) { + $log.error('Failed to decrypt encrypted.', err); + + } else { + $log.debug('Freshly decrypted:', JSON.stringify(decrypted)); + } + + + }); + return { ciphertext: cipherParams.ciphertext.toString(CryptoJS.enc.Base64), @@ -87,6 +124,7 @@ angular.module('copayApp.services').factory('encryptionService', function($log, }; root.encrypt = function(str, cb) { + $log.debug('encrypt()', JSON.stringify('str')); $log.debug('*** crypto exists: ' + !!crypto); $log.debug('*** CryptoJS exists: ' + !!CryptoJS); diff --git a/src/js/services/jsonEncryptionService.js b/src/js/services/jsonEncryptionService.js index 23133ce9b..54d871d11 100644 --- a/src/js/services/jsonEncryptionService.js +++ b/src/js/services/jsonEncryptionService.js @@ -35,11 +35,11 @@ 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? - + //var ciphertext = CryptoJS.enc.Base64.parse(encryptedData.ciphertext) + //var iv = CryptoJS.enc.Hex.parse(encryptedData.iv); + var ciphertext = encryptedData.ciphertext; + var iv = encryptedData.iv; + return { ciphertext: ciphertext, opts: { diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index c8c880b67..280b75135 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -121,11 +121,22 @@ angular.module('copayApp.services') root.storeProfile = function(profile, cb) { var profileString = profile.toObj(); - //if (platformInfo.isNW) { - storage.set('profile', profileString, cb); - //} else { - // secureStorageService.set('profile', profileString, cb); - //} + encryptionService.encrypt(profileString, function onProfileEncrypted(encryptionErr, encryptedProfile){ + if (encryptionErr) { + $log.error('Failed to encrypt profile.', enctryptionErr); + cb(encryptionErr, null); + return; + } + + $log.debug('storing profile ciphertext:', JSON.stringify(encryptedProfile.ciphertext)); + var persistentProfileStr = jsonEncryptionService.stringify( + encryptedProfile.ciphertext, + encryptedProfile.opts + ); + + + storage.set('profile', persistentProfileStr, cb); + }); }; /** @@ -264,6 +275,7 @@ angular.module('copayApp.services') var isEncrypted = jsonEncryptionService.isEncrypted(profileStr); if (isEncrypted) { $log.debug('profile was encrypted.'); + $log.debug('profileStr: ', profileStr); var encryptedProfileObject; try { @@ -274,6 +286,8 @@ angular.module('copayApp.services') return; } + $log.debug('profileStr after JSON: ', JSON.stringify(encryptedProfileObject)); + encryptionService.decrypt( encryptedProfileObject.ciphertext, encryptedProfileObject.opts, @@ -284,6 +298,8 @@ angular.module('copayApp.services') return } + $log.debug('Decrypted profile:', JSON.stringify(decryptedProfile)); + var profileObj = Profile.fromString(decryptedProfile); cb(null, profileObj); }); @@ -300,56 +316,7 @@ angular.module('copayApp.services') }); } - - }); - - /* - - if (platformInfo.isNW) { - storage.get('profile', function(getErr, getStr) { - _onOldProfileRetrieved(getErr, getStr, cb); - }); - return - } - - secureStorageService.get('profile', function(secureErr, secureStr) { - var secureProfile; - var oldProfile; - - if (secureErr) { - return cb(secureErr, null); - } - - if (secureStr) { - try { - secureProfile = Profile.fromString(secureStr); - $log.debug('profile: ' + JSON.stringify(secureProfile)); - } catch (e) { - $log.error(e); - return cb(e, null); - } - } - - storage.get('profile', function(getErr, getStr) { - _onOldProfileRetrieved(getErr, getStr, function(oldErr, oldProfile){ - if (oldErr) { - return cb(oldErr, null); - } - - if (!oldProfile) { - if (secureProfile) { - return cb(null, secureProfile); - } else { - // No profiles found. No errors either. - return cb(null, null); - } - } - _migrateProfiles(oldProfile, secureProfile, cb); - }); - }); - }); - */ }; root.setFeedbackInfo = function(feedbackValues, cb) { From f04417bc3988b455876ca929c760d2bed097ab22 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 28 Jun 2018 20:39:54 +1200 Subject: [PATCH 08/16] Clarification of some items. --- src/js/services/encryptionService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js index 00830af65..9e5bda702 100644 --- a/src/js/services/encryptionService.js +++ b/src/js/services/encryptionService.js @@ -55,7 +55,7 @@ angular.module('copayApp.services').factory('encryptionService', function($log, /** * - * @param {*} str + * @param {string, Base64 encoded} str * @param {CryptoJS.WordArray} key * @param {string, hex} iv */ @@ -89,7 +89,7 @@ angular.module('copayApp.services').factory('encryptionService', function($log, $log.debug('ciphertext: ' + ciphertext); $log.debug('iv: ' + iv); - + // Just for testing - do we get back what we put in? root.decrypt(ciphertext, {iv: iv}, function onDecryptionTest(err, decrypted){ if (err) { $log.error('Failed to decrypt encrypted.', err); From 63ddf545e441aee2ecef76df3a2513808cce0727 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 28 Jun 2018 20:49:54 +1200 Subject: [PATCH 09/16] Removed oboslete code from encryptionService and gave it a better layout. --- src/js/services/encryptionService.js | 309 +++++++++++---------------- 1 file changed, 125 insertions(+), 184 deletions(-) diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js index 9e5bda702..3a34e6352 100644 --- a/src/js/services/encryptionService.js +++ b/src/js/services/encryptionService.js @@ -1,205 +1,146 @@ -'use strict'; +(function() { + 'use strict'; -angular.module('copayApp.services').factory('encryptionService', function($log, secureStorageService) { - var root = {}; - var keySize = 512; - var iterations = 1500; - var storageKey = 'encryptionKey'; + angular.module('copayApp.services').factory('encryptionService', function($log, secureStorageService) { + var keySize = 512; + var iterations = 1500; + var storageKey = 'encryptionKey'; - // need a function to get the key - var password = 'password'; + var service = { + decrypt: decrypt, + encrypt: encrypt + }; + return service; - 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 }); + 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 }); - $log.debug('Generated key: ' + key); - return key; - } + $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; - } + /** + * + * @param {*} cb + */ + function _getOrCreateKey(cb) { - 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); + secureStorageService.get(storageKey, function onKeyRetrieved(keyErr, keyHex) { + if (keyErr) { + cb(keyErr, null); return; } - cb(null, key); + 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); + }); + + }); + }; + + /** + * + * @param {string, Base64 encoded} str + * @param {CryptoJS.WordArray} key + * @param {string, hex} iv + */ + function _decryptUsingCryptoJS(str, key, iv) { + $log.debug('decrypt() str: ' + str); + $log.debug('decrypt() using iv:' + iv + ', key: ' + JSON.stringify(key)); + + var ivWords = CryptoJS.enc.Hex.parse(iv); + var plaintext = CryptoJS.AES.decrypt(str, key, { iv: ivWords }); + $log.debug('plaintext', JSON.stringify(plaintext)); + + var plaintextWords = CryptoJS.lib.WordArray.create(); + plaintextWords.init(plaintext.words, plaintext.sigBytes); + $log.debug('plaintextWords', JSON.stringify(plaintextWords)); + var plaintextString = plaintextWords.toString(CryptoJS.enc.Utf8); + $log.debug('plaintextString: ', JSON.stringify(plaintextString)); + + return plaintextString; + } + + + function _encryptUsingCryptoJS(str, key) { + $log.debug('encrypt() str: ' + str); + var iv = CryptoJS.lib.WordArray.random(16); + + $log.debug('Encrypting profile: ', JSON.stringify(str)); + + var cipherParams = CryptoJS.AES.encrypt(str, key, { iv: iv }); + var ciphertext = cipherParams.ciphertext.toString(CryptoJS.enc.Base64); + var iv = iv.toString(CryptoJS.enc.Hex); + $log.debug('ciphertext: ' + ciphertext); + $log.debug('iv: ' + iv); + + // Just for testing - do we get back what we put in? + decrypt(ciphertext, {iv: iv}, function onDecryptionTest(err, decrypted){ + if (err) { + $log.error('Failed to decrypt encrypted.', err); + + } else { + $log.debug('Freshly decrypted:', JSON.stringify(decrypted)); + } + + }); - }); - }; - /** - * - * @param {string, Base64 encoded} str - * @param {CryptoJS.WordArray} key - * @param {string, hex} iv - */ - function _decryptUsingCryptoJS(str, key, iv) { - $log.debug('decrypt() str: ' + str); - $log.debug('decrypt() using iv:' + iv + ', key: ' + JSON.stringify(key)); - - var ivWords = CryptoJS.enc.Hex.parse(iv); - var plaintext = CryptoJS.AES.decrypt(str, key, { iv: ivWords }); - $log.debug('plaintext', JSON.stringify(plaintext)); + return { + ciphertext: cipherParams.ciphertext.toString(CryptoJS.enc.Base64), + opts: { + iv: iv.toString(CryptoJS.enc.Hex) + } + }; + } - var plaintextWords = CryptoJS.lib.WordArray.create(); - plaintextWords.init(plaintext.words, plaintext.sigBytes); - $log.debug('plaintextWords', JSON.stringify(plaintextWords)); - var plaintextString = plaintextWords.toString(CryptoJS.enc.Utf8); - $log.debug('plaintextString: ', JSON.stringify(plaintextString)); + function decrypt(str, opts, cb) { + _getOrCreateKey(function onKey(err, key) { + if (err) { + $log.error('Failed to get or create key.', err); + cb(err, null); + return; + } - return plaintextString; - } - - - function _encryptUsingCryptoJS(str, key) { - $log.debug('encrypt() str: ' + str); - var iv = CryptoJS.lib.WordArray.random(16); - - $log.debug('Encrypting profile: ', JSON.stringify(str)); - - var cipherParams = CryptoJS.AES.encrypt(str, key, { iv: iv }); - var ciphertext = cipherParams.ciphertext.toString(CryptoJS.enc.Base64); - var iv = iv.toString(CryptoJS.enc.Hex); - $log.debug('ciphertext: ' + ciphertext); - $log.debug('iv: ' + iv); - - // Just for testing - do we get back what we put in? - root.decrypt(ciphertext, {iv: iv}, function onDecryptionTest(err, decrypted){ - if (err) { - $log.error('Failed to decrypt encrypted.', err); - - } else { - $log.debug('Freshly decrypted:', JSON.stringify(decrypted)); - } - - - }); - - - return { - ciphertext: cipherParams.ciphertext.toString(CryptoJS.enc.Base64), - opts: { - iv: iv.toString(CryptoJS.enc.Hex) - } + var decrypted = _decryptUsingCryptoJS(str, key, opts.iv); + cb(null, decrypted); + }); }; - } - 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; - } + function encrypt(str, cb) { + $log.debug('encrypt()', JSON.stringify('str')); + $log.debug('*** crypto exists: ' + !!crypto); + $log.debug('*** CryptoJS exists: ' + !!CryptoJS); - var decrypted = _decryptUsingCryptoJS(str, key, opts.iv); - cb(null, decrypted); - }); - }; + _getOrCreateKey(function onKey(err, key){ + if (err) { + cb(err, null); + return; + } - root.encrypt = function(str, cb) { - $log.debug('encrypt()', JSON.stringify('str')); - $log.debug('*** crypto exists: ' + !!crypto); - $log.debug('*** CryptoJS exists: ' + !!CryptoJS); + var encrypted = _encryptUsingCryptoJS(str, key); + cb(null, encrypted); + }); + }; - _getOrCreateKey(function onKey(err, key){ - if (err) { - cb(err, null); - return; - } - - var encrypted = _encryptUsingCryptoJS(str, key); - cb(null, encrypted); - }); - }; - - - root.encryptedObjectFromString = function(str) { - try { - var parsed = JSON.parse(str); - } catch(e) { - return null; - } - - if (parsed.encryptionVersion) { - return parsed; - } else { - return null; - } - }; - - 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 + }); +})(); \ No newline at end of file From a388e6deacb0633901e9e0f91c027b1a31913bff Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 28 Jun 2018 21:13:37 +1200 Subject: [PATCH 10/16] Some more clean up and documentation in encryptionService. --- src/js/services/encryptionService.js | 33 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js index 3a34e6352..a73a28ed9 100644 --- a/src/js/services/encryptionService.js +++ b/src/js/services/encryptionService.js @@ -14,6 +14,9 @@ }; return service; + /** + * Returns a CryptoJS.WordArray + */ function _generateKey() { var salt = CryptoJS.lib.WordArray.random(128/8); var passphrase = CryptoJS.lib.WordArray.random(128/8); @@ -79,36 +82,35 @@ return plaintextString; } - + /** + * Generates its own Initialization Vector, which is returned. + * @param {string, Base64 encoded} str + * @param {CryptoJS.WordArray} key + * @returns {*} The ciphertext created, and the IV used. + */ function _encryptUsingCryptoJS(str, key) { - $log.debug('encrypt() str: ' + str); var iv = CryptoJS.lib.WordArray.random(16); - $log.debug('Encrypting profile: ', JSON.stringify(str)); - var cipherParams = CryptoJS.AES.encrypt(str, key, { iv: iv }); - var ciphertext = cipherParams.ciphertext.toString(CryptoJS.enc.Base64); - var iv = iv.toString(CryptoJS.enc.Hex); - $log.debug('ciphertext: ' + ciphertext); - $log.debug('iv: ' + iv); + var ciphertextWords = cipherParams.ciphertext.toString(CryptoJS.enc.Base64); + var ivHex = iv.toString(CryptoJS.enc.Hex); // Just for testing - do we get back what we put in? - decrypt(ciphertext, {iv: iv}, function onDecryptionTest(err, decrypted){ + /* + decrypt(ciphertext, {iv: ivHex}, function onDecryptionTest(err, decrypted){ if (err) { $log.error('Failed to decrypt encrypted.', err); } else { $log.debug('Freshly decrypted:', JSON.stringify(decrypted)); } - - }); - + */ return { - ciphertext: cipherParams.ciphertext.toString(CryptoJS.enc.Base64), + ciphertext: ciphertextWords.toString(CryptoJS.enc.Base64), opts: { - iv: iv.toString(CryptoJS.enc.Hex) + iv: ivHex } }; } @@ -127,9 +129,6 @@ }; function encrypt(str, cb) { - $log.debug('encrypt()', JSON.stringify('str')); - $log.debug('*** crypto exists: ' + !!crypto); - $log.debug('*** CryptoJS exists: ' + !!CryptoJS); _getOrCreateKey(function onKey(err, key){ if (err) { From 074e691cf99a8cec0e0bc00b44e6b6e8602bc63d Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 28 Jun 2018 21:38:14 +1200 Subject: [PATCH 11/16] Removing encryption key if it already exists from a previous installation. --- src/js/services/encryptionService.js | 13 +++++++++- src/js/services/mobileSecureStorageService.js | 26 +++++++++++++++++++ src/js/services/secureStorageService.js | 14 ++++++++-- src/js/services/storageService.js | 2 ++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js index a73a28ed9..f87a1f4f3 100644 --- a/src/js/services/encryptionService.js +++ b/src/js/services/encryptionService.js @@ -10,7 +10,8 @@ var service = { decrypt: decrypt, - encrypt: encrypt + encrypt: encrypt, + removeKeyIfExists: removeKeyIfExists }; return service; @@ -141,5 +142,15 @@ }); }; + function removeKeyIfExists() { + secureStorageService.remove(storageKey, function onKeyRemoved(err){ + if (err) { + $log.Error('Error removing key.', err); + return; + } + $log.debug('Key removed.'); + }); + } + }); })(); \ No newline at end of file diff --git a/src/js/services/mobileSecureStorageService.js b/src/js/services/mobileSecureStorageService.js index f9994fdf8..890cd3f67 100644 --- a/src/js/services/mobileSecureStorageService.js +++ b/src/js/services/mobileSecureStorageService.js @@ -57,6 +57,32 @@ angular.module('copayApp.services').factory('mobileSecureStorageService', functi key); }; + root.remove = function(key, cb) { + + if (!platformInfo.isMobile) { + cb(new Error('mobileSecureStorageService is only available on mobile.')); + } + + if (!isReady) { + if (initialisationFailed) { + cb(new Error('mobileSecureStorageService initialisation failed.')); + } else { + pending.push(function(){ root.remove(key, cb); }); + } + return; + } + + storage.remove( + function (value) { + cb(); + }, + function (error) { + cb(new Error(error)); + }, + key); + + }; + root.set = function(key, value, cb) { if (!platformInfo.isMobile) { diff --git a/src/js/services/secureStorageService.js b/src/js/services/secureStorageService.js index c066109c2..b4d9a6fc3 100644 --- a/src/js/services/secureStorageService.js +++ b/src/js/services/secureStorageService.js @@ -16,7 +16,17 @@ angular.module('copayApp.services').factory('secureStorageService', function(des } else { // Browser localStorageService.get(alteredKeyIndicatingDesireForSecureStorage(k), cb); } - } + }; + + root.remove = function(k, cb) { + if (platformInfo.isMobile) { + mobileSecureStorageService.remove(k, cb); + } else if (platformInfo.isNW) { + desktopSecureStorageService.remove(k, cb); + } else { // Browser + localStorageService.remove(alteredKeyIndicatingDesireForSecureStorage(k), cb); + } + }; root.set = function(k, v, cb) { if (platformInfo.isMobile) { @@ -26,7 +36,7 @@ angular.module('copayApp.services').factory('secureStorageService', function(des } else { // Browser localStorageService.set(alteredKeyIndicatingDesireForSecureStorage(k), v, cb); } - } + }; return root; }); \ No newline at end of file diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index 280b75135..8b1dfa1eb 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -269,6 +269,8 @@ angular.module('copayApp.services') if (!profileStr) { $log.debug('No string loaded, returning nothing.'); + // Don't want to use the same key as a previous installation + encryptionService.removeKeyIfExists(); return cb(null, null); } From fb88b05463f7bb6fe5919bd0425f2bd66568c6ce Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Thu, 28 Jun 2018 21:55:06 +1200 Subject: [PATCH 12/16] Catching exception caused by using the wrong key to decrypt the profile. --- src/js/services/encryptionService.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js index f87a1f4f3..37b1b2c3b 100644 --- a/src/js/services/encryptionService.js +++ b/src/js/services/encryptionService.js @@ -124,7 +124,15 @@ return; } - var decrypted = _decryptUsingCryptoJS(str, key, opts.iv); + var decrypted; + try { + decrypted = _decryptUsingCryptoJS(str, key, opts.iv); + } catch (e) { + // Can get this when using the wrong key: Malformed UTF-8 data + $log.error('Error when decrypting.', e); + cb(e, null); + return; + } cb(null, decrypted); }); }; From 6ef1bca911d193011baf66fee6dca44c9a61e247 Mon Sep 17 00:00:00 2001 From: Sebastiaan Pasma Date: Thu, 28 Jun 2018 14:47:27 +0200 Subject: [PATCH 13/16] small fix for key + exception message --- src/js/services/mobileSecureStorageService.js | 2 +- src/js/services/storageService.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/services/mobileSecureStorageService.js b/src/js/services/mobileSecureStorageService.js index 890cd3f67..473b62885 100644 --- a/src/js/services/mobileSecureStorageService.js +++ b/src/js/services/mobileSecureStorageService.js @@ -47,7 +47,7 @@ angular.module('copayApp.services').factory('mobileSecureStorageService', functi function (error) { $log.debug('mss get failed. ' + error); if (error.message === 'Failure in SecureStorage.get() - The specified item could not be found in the keychain' || // iOS - error.message === 'Key [_SS_profile] not found.') { // Android + error.message === 'Key [_SS_' + key + '] not found.') { // Android // The callback expects no error, but also no value, if it cannot be found. cb(null, null); } else { diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index 8b1dfa1eb..ba19d6439 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -123,7 +123,7 @@ angular.module('copayApp.services') var profileString = profile.toObj(); encryptionService.encrypt(profileString, function onProfileEncrypted(encryptionErr, encryptedProfile){ if (encryptionErr) { - $log.error('Failed to encrypt profile.', enctryptionErr); + $log.error('Failed to encrypt profile.', encryptionErr); cb(encryptionErr, null); return; } From f4f2a84fbb0ac4ad7da4a31d2aa0b10778467e06 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Fri, 29 Jun 2018 00:48:03 +0900 Subject: [PATCH 14/16] 351 - Improvement - Fix return was missing --- src/js/services/mobileSecureStorageService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/services/mobileSecureStorageService.js b/src/js/services/mobileSecureStorageService.js index 473b62885..634f1b75c 100644 --- a/src/js/services/mobileSecureStorageService.js +++ b/src/js/services/mobileSecureStorageService.js @@ -87,6 +87,7 @@ angular.module('copayApp.services').factory('mobileSecureStorageService', functi if (!platformInfo.isMobile) { cb(new Error('mobileSecureStorageService is only available on mobile.')); + return; } if (!isReady) { From 9b20cb36fda2a4a4060a7815839ca8bf4bc0fa2c Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 29 Jun 2018 08:13:13 +1200 Subject: [PATCH 15/16] Fixed incorrect case. --- src/js/services/encryptionService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/services/encryptionService.js b/src/js/services/encryptionService.js index 37b1b2c3b..b90ce7639 100644 --- a/src/js/services/encryptionService.js +++ b/src/js/services/encryptionService.js @@ -153,7 +153,7 @@ function removeKeyIfExists() { secureStorageService.remove(storageKey, function onKeyRemoved(err){ if (err) { - $log.Error('Error removing key.', err); + $log.error('Error removing key.', err); return; } $log.debug('Key removed.'); From 60c7a03d8b2eba5c7eb37d783071fea29116d370 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Fri, 29 Jun 2018 08:43:43 +1200 Subject: [PATCH 16/16] Scripts for running on Android emulator. --- app-template/package-template.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app-template/package-template.json b/app-template/package-template.json index 75f93a3d0..bcfb41425 100644 --- a/app-template/package-template.json +++ b/app-template/package-template.json @@ -87,6 +87,7 @@ "start": "npm run build:www && ionic serve --nolivereload --nogulp -s --address 0.0.0.0", "start:ios": "npm run build:www && npm run build:ios && npm run open:ios", "start:android": "npm run build:www && npm run build:android && npm run run:android", + "start:android-emulator": "npm run build:www && npm run build:android && npm run run:android-emulator", "start:windows": "npm run build:www && npm run build:windows", "start:desktop": "npm start", "watch": "grunt watch", @@ -99,7 +100,7 @@ "build:android-release": "cordova prepare android && cordova build android --release", "build:windows-release": "cordova prepare windows && cordova build windows --release --arch=\"ARM\"", "build:desktop": "grunt desktop", - "build:osx": "grunt osx", + "build:osx": "grunt osx", "open:ios": "open platforms/ios/*.xcodeproj", "open:android": "open -a open -a /Applications/Android\\ Studio.app platforms/android", "final:www": "npm run build:www-release", @@ -108,7 +109,8 @@ "final:windows": "npm run final:www && npm run build:windows-release", "final:desktop": "npm run build:desktop && npm run build:osx", "run:android": "cordova run android --device", - "run:android-release": "cordova run android --device --release", + "run:android-emulator": "cordova run android --emulator", + "run:android-release": "cordova run android --device --release", "log:android": "adb logcat | grep chromium", "sign:android": "rm -f platforms/android/build/outputs/apk/android-release-signed-aligned.apk; jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ../bitcoin-com-release-key.jks -signedjar platforms/android/build/outputs/apk/android-release-signed.apk platforms/android/build/outputs/apk/android-release-unsigned.apk bitcoin-com && $ANDROID_HOME/build-tools/27.0.1/zipalign -v 4 platforms/android/build/outputs/apk/android-release-signed.apk platforms/android/build/outputs/apk/android-release-signed-aligned.apk", "apply:copay": "npm i fs-extra && cd app-template && node apply.js copay && npm i && cordova prepare",