diff --git a/src/app/components/login/login.js b/src/app/components/login/login.js index 3f7a87213..84b61f269 100644 --- a/src/app/components/login/login.js +++ b/src/app/components/login/login.js @@ -1,6 +1,3 @@ -import crypto from 'crypto'; -import mnemonic from 'bitcore-mnemonic'; - import './login.less'; import './save.less'; @@ -11,17 +8,20 @@ app.component('login', { onLogin: '&', }, controller: class login { - constructor($scope, $rootScope, $timeout, $document, $mdDialog, $mdMedia, $cookies, $peers) { + + /* eslint no-param-reassign: ["error", { "props": false }] */ + constructor($scope, $rootScope, $timeout, $document, $mdMedia, $cookies, $peers, Passphrase) { this.$scope = $scope; this.$rootScope = $rootScope; this.$timeout = $timeout; this.$document = $document; - this.$mdDialog = $mdDialog; this.$mdMedia = $mdMedia; this.$cookies = $cookies; this.$peers = $peers; + this.Passphrase = Passphrase; + this.generatingNewPassphrase = false; - this.$scope.$watch('$ctrl.input_passphrase', this.isValidPassphrase.bind(this)); + this.$scope.$watch('$ctrl.input_passphrase', val => this.valid = this.Passphrase.isValidPassphrase(val)); this.$timeout(this.devTestAccount.bind(this), 200); this.$scope.$watch(() => this.$mdMedia('xs') || this.$mdMedia('sm'), (wantsFullScreen) => { @@ -35,180 +35,32 @@ app.component('login', { this.$peers.setActive($peers.stack.official[0]); } }); - } - reset() { - this.input_passphrase = ''; - this.progress = 0; - this.seed = login.emptyBytes().map(() => '00'); - } - - stopNewPassphraseGeneration() { - this.generatingNewPassphrase = false; - this.$document.unbind('mousemove', this.listener); + this.$scope.$on('onAfterSignup', (ev, args) => { + if (args.target === 'primary-pass') { + this.passConfirmSubmit(args.passphrase); + } + }); } - doTheLogin() { - if (this.isValidPassphrase(this.input_passphrase) === 1) { - this.passphrase = login.fixCaseAndWhitespace(this.input_passphrase); + passConfirmSubmit(_passphrase = this.input_passphrase) { + if (this.Passphrase.normalize.constructor === Function) { + this.passphrase = this.Passphrase.normalize(_passphrase); - this.reset(); this.$timeout(this.onLogin); } } - isValidPassphrase(value) { - const fixedValue = login.fixCaseAndWhitespace(value); - - if (fixedValue === '') { - this.valid = 2; - } else if (fixedValue.split(' ').length < 12 || !mnemonic.isValid(fixedValue)) { - this.valid = 0; - } else { - this.valid = 1; - } - return this.valid; - } - - startGenratingNewPassphrase() { - this.reset(); - + generatePassphrase() { this.generatingNewPassphrase = true; - - let last = [0, 0]; - let used = login.emptyBytes(); - - const turns = 10 + parseInt(Math.random() * 10, 10); - const steps = 2; - const total = turns * used.length; - let count = 0; - - this.listener = (ev) => { - const distance = Math.sqrt(Math.pow(ev.pageX - last[0], 2) + - (Math.pow(ev.pageY - last[1]), 2)); - - if (distance > 60 || ev.isTrigger) { - for (let p = 0; p < steps; p++) { - if (count >= total) { - this.stopNewPassphraseGeneration(); - this.setNewPassphrase(this.seed); - return; - } - - count++; - - if (!ev.isTrigger) { - last = [ev.pageX, ev.pageY]; - } - - used = this.updateSeedAndProgress(used, count / total); - } - } - }; - - this.$timeout(() => this.$document.mousemove(this.listener), 300); - } - - updateSeedAndProgress(_used, progress) { - let pos; - let used = _used; - const available = used.map((u, i) => (!u ? i : null)).filter(u => u !== null); - - if (!available.length) { - used = used.map(() => 0); - pos = parseInt(Math.random() * used.length, 10); - } else { - pos = available[parseInt(Math.random() * available.length, 10)]; - } - - this.seed[pos] = login.lpad(crypto.randomBytes(1)[0].toString(16), '0', 2); - this.progress = parseInt((progress) * 100, 10); - - if (this.$scope.$root.$$phase !== '$apply' && this.$scope.$root.$$phase !== '$digest') { - this.$scope.$apply(); - } - - used[pos] = 1; - return used; - } - - simulateMousemove() { - this.$document.mousemove(); - } - - setNewPassphrase(seed) { - const passphrase = (new mnemonic(new Buffer(seed.join(''), 'hex'))).toString(); - const ok = () => { - this.input_passphrase = passphrase; - this.$timeout(this.doTheLogin.bind(this), 100); - }; - - this.$mdDialog.show({ - controllerAs: '$ctrl', - controller: /* @ngInject*/ class save { - constructor($scope, $mdDialog) { - this.$mdDialog = $mdDialog; - this.passphrase = passphrase; - - $scope.$watch('$ctrl.missing_input', () => { - this.missing_ok = this.missing_input && this.missing_input === this.missing_word; - }); - } - - next() { - this.enter = true; - - const words = this.passphrase.split(' '); - const missingNumber = parseInt(Math.random() * words.length, 10); - - this.missing_word = words[missingNumber]; - this.pre = words.slice(0, missingNumber).join(' '); - this.pos = words.slice(missingNumber + 1).join(' '); - } - - ok() { - ok(); - this.close(); - } - - close() { - this.$mdDialog.hide(); - } - }, - - template: require('./save.pug')(), - fullscreen: (this.$mdMedia('sm') || this.$mdMedia('xs')) && this.$scope.customFullscreen, - }); } devTestAccount() { const passphrase = this.$cookies.get('passphrase'); if (passphrase) { this.input_passphrase = passphrase; - this.$timeout(this.doTheLogin.bind(this), 10); + this.$timeout(this.passConfirmSubmit.bind(this), 10); } } - - static fixCaseAndWhitespace(v) { - return (v || '').replace(/ +/g, ' ').trim().toLowerCase(); - } - - static lpad(str, pad, length) { - let result = str; - while (result.length < length) result = pad + str; - return result; - } - - static emptyBytes() { - return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - } - - // istanbul ignore next - // Let's consider this to be third party code that we don't want to test - static mobileAndTabletcheck() { - let check = false - ;(function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd(m|p|t)|hei|hi(pt|ta)|hp( i|ip)|hsc|ht(c(| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i(20|go|ma)|i230|iac( ||\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|[a-w])|libw|lynx|m1w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|mcr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|([1-8]|c))|phil|pire|pl(ay|uc)|pn2|po(ck|rt|se)|prox|psio|ptg|qaa|qc(07|12|21|32|60|[2-7]|i)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h|oo|p)|sdk\/|se(c(|0|1)|47|mc|nd|ri)|sgh|shar|sie(|m)|sk0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h|v|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl|tdg|tel(i|m)|tim|tmo|to(pl|sh)|ts(70|m|m3|m5)|tx9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas|your|zeto|zte/i.test(a.substr(0, 4))) check = true; }(navigator.userAgent || navigator.vendor || window.opera)); - return check; - } }, }); diff --git a/src/app/components/login/login.less b/src/app/components/login/login.less index 0e1cf775f..471636faf 100644 --- a/src/app/components/login/login.less +++ b/src/app/components/login/login.less @@ -11,24 +11,4 @@ login { .random-input { margin: 0 0 25px 0; } - - .byte { - display: inline-block; - text-align: center; - font-size: 140%; - margin: 5px; - font-family: monospace; - - &.change-add, &.change-remove { - transition: all .15s ease; - } - - &.change, &.change-add-active, &.change-remove { - transform: scale(1.3); - } - - &.change-remove-active { - transform: none; - } - } } diff --git a/src/app/components/login/login.pug b/src/app/components/login/login.pug index ed28135bb..e0ef11a15 100644 --- a/src/app/components/login/login.pug +++ b/src/app/components/login/login.pug @@ -3,7 +3,7 @@ md-card md-card-title-text span.md-title Sign In md-card-content(flex='100', flex-gt-sm='70', flex-offset-gt-sm='15') - form(ng-submit='$ctrl.doTheLogin()') + form(ng-submit='$ctrl.passConfirmSubmit()') md-input-container.md-block(md-is-error='$ctrl.valid === 0') label.select Choose a peer md-select(ng-model='$ctrl.$peers.currentPeerConfig', aria-label='Peer') @@ -14,14 +14,8 @@ md-card input(type="{{ $ctrl.show_passphrase ? 'text' : 'password' }}", ng-model='$ctrl.input_passphrase', ng-disabled='$ctrl.generatingNewPassphrase', autofocus) md-input-container.md-block md-checkbox.md-primary(ng-model="$ctrl.show_passphrase", aria-label="Show passphrase") Show passphrase - md-content(layout='row', layout-align='center center') - // md-button(ng-disabled='$ctrl.generatingNewPassphrase', ng-click='$ctrl.devTestAccount()') Dev Test Account - md-button.md-primary(ng-disabled='$ctrl.random || $ctrl.generatingNewPassphrase', ng-click='$ctrl.startGenratingNewPassphrase()') NEW ACCOUNT - md-button.md-raised.md-primary(md-autofocus, ng-disabled='$ctrl.valid !== 1', ng-click='$ctrl.doTheLogin()') Login - md-content(layout-padding, layout='column', layout-align='center center', ng-show='$ctrl.generatingNewPassphrase') - h4.move(ng-show='$ctrl.mobileAndTabletcheck()') Enter text below to generate random bytes - h4.move(ng-hide='$ctrl.mobileAndTabletcheck()') Move your mouse to generate random bytes - input.random-input(type="text", ng-keydown='$ctrl.simulateMousemove()', ng-show='$ctrl.mobileAndTabletcheck()') - md-progress-linear(md-mode='determinate', value='{{ $ctrl.progress }}') - md-content.bytes - span.byte(ng-repeat='byte in $ctrl.seed track by $index', ng-bind='byte', animate-on-change='byte') + md-content(layout='row', layout-align='center center') + // md-button(ng-disabled='$ctrl.generatingNewPassphrase', ng-click='$ctrl.devTestAccount()') Dev Test Account + md-button.md-primary(ng-disabled='$ctrl.random || $ctrl.generatingNewPassphrase', ng-click='$ctrl.generatePassphrase()') NEW ACCOUNT + md-button.md-raised.md-primary(md-autofocus, ng-disabled='$ctrl.valid != undefined && $ctrl.valid !== 1', type='submit') Login + passphrase(ng-if='$ctrl.generatingNewPassphrase', data-on-login='$ctrl.onLogin', data-target='primary-pass') \ No newline at end of file diff --git a/src/app/components/login/passphrase.js b/src/app/components/login/passphrase.js new file mode 100644 index 000000000..747818fdf --- /dev/null +++ b/src/app/components/login/passphrase.js @@ -0,0 +1,95 @@ +import './passphrase.less'; + +app.directive('passphrase', ($rootScope, $document, Passphrase, $mdDialog, $mdMedia, $timeout) => { + /* eslint no-param-reassign: ["error", { "props": false }] */ + const PassphraseLink = function (scope, element, attrs) { + const bindEvents = (listener) => { + $document.bind('mousemove', listener); + }; + + const unbindEvents = (listener) => { + $document.unbind('mousemove', listener); + }; + + const generateAndDoubleCheck = (seed) => { + const passphrase = Passphrase.generatePassPhrase(seed); + + const ok = () => { + // this.input_passphrase = passphrase; + $timeout(() => { + $rootScope.$broadcast('onAfterSignup', { + passphrase, + target: attrs.target, + }); + }, 100); + }; + + $mdDialog.show({ + controllerAs: '$ctrl', + controller: /* @ngInject*/ class save { + constructor($scope) { + this.$mdDialog = $mdDialog; + this.passphrase = passphrase; + + $scope.$watch('$ctrl.missing_input', () => { + this.missing_ok = this.missing_input && this.missing_input === this.missing_word; + }); + } + + next() { + this.enter = true; + + const words = this.passphrase.split(' '); + const missingNumber = parseInt(Math.random() * words.length, 10); + + this.missing_word = words[missingNumber]; + this.pre = words.slice(0, missingNumber).join(' '); + this.pos = words.slice(missingNumber + 1).join(' '); + } + + ok() { + ok(); + this.close(); + } + + close() { + this.$mdDialog.hide(); + } + }, + + template: require('./save.pug')(), + fullscreen: ($mdMedia('sm') || $mdMedia('xs')) && this.scope.customFullscreen, + }); + }; + + const terminate = (seed) => { + unbindEvents(Passphrase.listene); + generateAndDoubleCheck(seed); + }; + + scope.simulateMousemove = () => { + $document.mousemove(); + }; + + scope.mobileAndTabletcheck = (agent) => { + let check = false; + if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(agent || navigator.userAgent || navigator.vendor || window.opera)) { + check = true; + } + return check; + }; + + Passphrase.init(); + bindEvents(e => Passphrase.listener(e, terminate)); + scope.progress = Passphrase.progress; + }; + + return { + link: PassphraseLink, + restrict: 'E', + scope: { + onLogin: '=', + }, + template: require('./passphrase.pug')(), + }; +}); diff --git a/src/app/components/login/passphrase.less b/src/app/components/login/passphrase.less new file mode 100644 index 000000000..d1aeb5f80 --- /dev/null +++ b/src/app/components/login/passphrase.less @@ -0,0 +1,19 @@ +md-content.bytes .byte { + display: inline-block; + text-align: center; + font-size: 140%; + margin: 5px; + font-family: monospace; + + &.change-add, &.change-remove { + transition: all .15s ease; + } + + &.change, &.change-add-active, &.change-remove { + transform: scale(1.3); + } + + &.change-remove-active { + transform: none; + } + } \ No newline at end of file diff --git a/src/app/components/login/passphrase.pug b/src/app/components/login/passphrase.pug new file mode 100644 index 000000000..5692044bc --- /dev/null +++ b/src/app/components/login/passphrase.pug @@ -0,0 +1,7 @@ +md-content(layout-padding, layout='column', layout-align='center center') + h4.move(ng-show='mobileAndTabletcheck()') Enter text below to generate random bytes + h4.move(ng-hide='mobileAndTabletcheck()') Move your mouse to generate random bytes + input.random-input(type="text", ng-keydown='simulateMousemove()', ng-show='mobileAndTabletcheck()') + md-progress-linear(md-mode='determinate', value='{{ progress.percentage }}') + md-content.bytes + span.byte(ng-repeat='byte in progress.seed track by $index', ng-bind='byte', animate-on-change='byte') diff --git a/src/app/components/main/main.pug b/src/app/components/main/main.pug index 65e4fa927..91e4b3718 100644 --- a/src/app/components/main/main.pug +++ b/src/app/components/main/main.pug @@ -3,6 +3,8 @@ md-content(layout='row', id="main") md-content.header(layout='row', layout-align='center center', layout-padding) img.logo(src=require('./images/LISK-nano.png')) div(flex) + md-button.md-raised.md-secondary.set-2nd(data-set-second-pass, ng-if='$ctrl.logged && !$ctrl.account.secondSignature', + data-account='{{$ctrl.account.publicKey}}', data-passphrase='{{$ctrl.passphrase}}') Set 2nd passphrase md-button.md-raised.md-primary.send(data-show-send-modal, ng-if='$ctrl.logged') Send md-button.md-raised.md-secondary.logout(ng-click='$ctrl.logout()', ng-if='$ctrl.logged') Logout md-menu.top-menu(ng-if='$ctrl.logged', md-position-mode='target-right target', md-offset='14 0') diff --git a/src/app/components/main/secondPass.less b/src/app/components/main/secondPass.less new file mode 100644 index 000000000..600f9ceed --- /dev/null +++ b/src/app/components/main/secondPass.less @@ -0,0 +1,4 @@ +md-dialog.dialog-second { + width: 80%; + max-width: 1000px; +} \ No newline at end of file diff --git a/src/app/components/main/secondPass.pug b/src/app/components/main/secondPass.pug new file mode 100644 index 000000000..9402e8ee9 --- /dev/null +++ b/src/app/components/main/secondPass.pug @@ -0,0 +1,6 @@ +md-dialog.dialog-second(aria-label='Generate a second passphrase for your account') + form + md-toolbar + .md-toolbar-tools + h2 Generate a second passphrase of your account + passphrase(data-on-login='md.onLogin', data-target='second-pass') \ No newline at end of file diff --git a/src/app/components/main/setSecondPassDirective.js b/src/app/components/main/setSecondPassDirective.js new file mode 100644 index 000000000..39001cc5f --- /dev/null +++ b/src/app/components/main/setSecondPassDirective.js @@ -0,0 +1,38 @@ +import './secondPass.less'; + +app.directive('setSecondPass', (setSecondPass, $peers, $rootScope, success, error) => { + /* eslint no-param-reassign: ["error", { "props": false }] */ + const SetSecondPassLink = function (scope, element, attrs) { + element.bind('click', () => { + setSecondPass.show(); + }); + + scope.passConfirmSubmit = (secondsecret) => { + $peers.active.setSignature(secondsecret, attrs.publicKey, attrs.passphrase) + .then(() => { + success.dialog('Your second passphrase was successfully registered.'); + }) + .catch((err) => { + let text = ''; + if (err.message === 'Missing sender second signature') { + text = 'You already have a second passphrase.'; + } else if (/^(Account does not have enough LSK)/.test(err.message)) { + text = 'You have insuffcient funds to register a second passphrase.'; + } else { + text = 'An error occurred while registering your second passphrase. Please try again.'; + } + error.dialog({ text }); + }); + }; + + scope.$on('onAfterSignup', (ev, args) => { + if (args.target === 'second-pass') { + scope.passConfirmSubmit(args.passphrase); + } + }); + }; + return { + restrict: 'A', + link: SetSecondPassLink, + }; +}); diff --git a/src/app/components/main/setSecondPassService.js b/src/app/components/main/setSecondPassService.js new file mode 100644 index 000000000..94ae3d715 --- /dev/null +++ b/src/app/components/main/setSecondPassService.js @@ -0,0 +1,27 @@ +const setSecondPassConstructor = function ($mdDialog) { + this.ok = () => { + $mdDialog.hide(); + }; + + this.cancel = () => { + $mdDialog.hide(); + }; + + this.show = () => { + $mdDialog.show({ + template: require('./secondPass.pug')(), + parent: angular.element('#main'), + bindToController: true, + locals: { + ok: this.ok, + cancel: this.cancel, + }, + controller: () => {}, + controllerAs: 'md', + }); + }; + + return this; +}; + +app.factory('setSecondPass', setSecondPassConstructor); diff --git a/src/app/lisk-nano.js b/src/app/lisk-nano.js index 3ec1afbd1..71c429dd2 100644 --- a/src/app/lisk-nano.js +++ b/src/app/lisk-nano.js @@ -3,7 +3,10 @@ import './index.less'; import './theme/theme'; import './util/animateOnChange/animateOnChange'; import './components/main/main'; +import './components/main/setSecondPassService'; +import './components/main/setSecondPassDirective'; import './components/login/login'; +import './components/login/passphrase'; import './components/top/top'; import './components/send/send'; import './components/send/sendModalService'; @@ -21,6 +24,7 @@ import './services/peers/peers'; import './services/lsk'; import './services/success'; import './services/error'; +import './services/passphrase'; import './services/sign-verify'; import './filters/lsk'; diff --git a/src/app/services/passphrase.js b/src/app/services/passphrase.js new file mode 100644 index 000000000..29ece1db1 --- /dev/null +++ b/src/app/services/passphrase.js @@ -0,0 +1,104 @@ +import crypto from 'crypto'; +import mnemonic from 'bitcore-mnemonic'; + +/* eslint no-param-reassign: ["error", { "props": false }] */ + +app.factory('Passphrase', function ($rootScope) { + this.progress = { + seed: null, + percentage: 0, + step: 0, + }; + const lastCaptured = { + coordination: { + x: 0, + y: 0, + }, + time: 0, + }; + let byte = null; + + const emptyBytes = () => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + this.normalize = (v = '') => v.replace(/ +/g, ' ').trim().toLowerCase(); + + this.reset = () => { + this.progress.percentage = 0; + this.progress.seed = emptyBytes().map(() => '00'); + }; + + /** + * fills the left side of str with a given padding string to meet the required length + */ + const leftPadd = (str, pad, length) => { + let paddedStr = str; + while (paddedStr.length < length) paddedStr = pad + paddedStr; + return paddedStr; + }; + + this.isValidPassphrase = (value) => { + const normalizedValue = this.normalize(value); + + if (normalizedValue === '') { + return 2; + } else if (normalizedValue.split(' ').length < 12 || !mnemonic.isValid(normalizedValue)) { + return 0; + } + return 1; + }; + + this.init = () => { + this.reset(); + byte = emptyBytes(); + this.progress.step = (160 + Math.floor(Math.random() * 160)) / 100; + }; + + const updateSeedAndProgress = () => { + let pos; + const available = byte.map((bit, index) => (!bit ? index : null)).filter(bit => (bit !== null)); + if (!available.length) { + byte = byte.map(() => 0); + pos = parseInt(Math.random() * byte.length, 10); + } else { + pos = available[parseInt(Math.random() * available.length, 10)]; + } + + this.progress.seed[pos] = leftPadd(crypto.randomBytes(1)[0].toString(16), '0', 2); + + /** + * @todo why it's not working without manual digestion + */ + if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') { + $rootScope.$apply(); + } + + byte[pos] = 1; + return byte; + }; + + this.generatePassPhrase = seed => (new mnemonic(new Buffer(seed.join(''), 'hex'))).toString(); + + this.listener = (ev, callback) => { + const distance = Math.sqrt(Math.pow(ev.pageX - lastCaptured.coordination.x, 2) + + (Math.pow(ev.pageY - lastCaptured.coordination.y), 2)); + + if (distance > 120 || ev.isTrigger) { + for (let p = 0; p < 2; p++) { + if (this.progress.percentage >= 100) { + callback(this.progress.seed); + return; + } + + if (!ev.isTrigger) { + lastCaptured.coordination.x = ev.pageX; + lastCaptured.coordination.y = ev.pageY; + } + + this.progress.percentage += this.progress.step; + byte = updateSeedAndProgress(byte, this.progress.percentage); + } + } + }; + + return this; +}); diff --git a/src/app/services/peers/peers.js b/src/app/services/peers/peers.js index 106eeac75..a2572f39b 100644 --- a/src/app/services/peers/peers.js +++ b/src/app/services/peers/peers.js @@ -115,6 +115,28 @@ app.factory('$peers', ($timeout, $cookies, $location, $q) => { }); return deferred.promise; }; + + this.active.listTransactionsPromise = (address, limit, offset) => { + const deferred = $q.defer(); + this.active.listTransactions(address, limit, offset, (data) => { + if (data.success) { + return deferred.resolve(data); + } + return deferred.reject(data); + }); + return deferred.promise; + }; + + this.active.setSignature = (secondSecret, publicKey, secret) => { + const deferred = $q.defer(); + this.active.sendRequest('signatures', { secondSecret, publicKey, secret }, (res) => { + if (res.success) { + deferred.resolve(res); + } + deferred.reject(res); + }); + return deferred.promise; + }; } check() { diff --git a/src/test/components/login/login.spec.js b/src/test/components/login/login.spec.js index 7721e3a2e..6069e3519 100644 --- a/src/test/components/login/login.spec.js +++ b/src/test/components/login/login.spec.js @@ -4,6 +4,8 @@ const sinonChai = require('sinon-chai'); const expect = chai.expect; chai.use(sinonChai); +const VALID_PASSPHRASE = 'illegal symbol search tree deposit youth mixture craft amazing tool soon unit'; +const INVALID_PASSPHRASE = 'INVALID_PASSPHRASE'; describe('Login component', () => { let $compile; @@ -59,11 +61,22 @@ describe('Login controller', () => { let $scope; let controller; let $componentController; + let Passphrase; let testPassphrase; + let $cookies; + /* eslint-disable no-unused-vars */ + let $timeout; + /* eslint-enable no-unused-vars */ - beforeEach(inject((_$componentController_, _$rootScope_) => { + beforeEach(inject((_$componentController_, _$rootScope_, + _Passphrase_, _$cookies_, _$timeout_) => { $componentController = _$componentController_; $rootScope = _$rootScope_; + Passphrase = _Passphrase_; + $cookies = _$cookies_; + /* eslint-disable no-unused-vars */ + $timeout = _$timeout_; + /* eslint-enable no-unused-vars */ })); beforeEach(() => { @@ -95,179 +108,63 @@ describe('Login controller', () => { $scope.$apply(); expect(controller.$peers.currentPeerConfig).to.equal(controller.$peers.stack.official[0]); }); - }); - describe('$scope.reset()', () => { - it('makes input_passphrase empty', () => { - const passphrase = 'TEST'; - controller.input_passphrase = passphrase; - expect(controller.input_passphrase).to.equal(passphrase); - controller.reset(); - expect(controller.input_passphrase).to.equal(''); - }); - }); - - describe('$scope.stopNewPassphraseGeneration()', () => { - it('sets this.generatingNewPassphrase = false', () => { - controller.generatingNewPassphrase = true; - controller.stopNewPassphraseGeneration(); - expect(controller.generatingNewPassphrase).to.equal(false); + it('should define a watcher for $ctrl.input_passphrase', () => { + $scope.$apply(); + const spy = sinon.spy(Passphrase, 'isValidPassphrase'); + controller.input_passphrase = INVALID_PASSPHRASE; + $scope.$apply(); + expect(controller.valid).to.not.equal(1); + controller.input_passphrase = VALID_PASSPHRASE; + $scope.$apply(); + expect(controller.valid).to.equal(1); + expect(spy).to.have.been.calledWith(); }); - it('unbinds mousemove listener', () => { - const unbindSpy = sinon.spy(controller.$document, 'unbind'); - controller.stopNewPassphraseGeneration(); - expect(unbindSpy).to.have.been.calledWith('mousemove', controller.listener); + it('listens for an onAfterSignup event', () => { + const spy = sinon.spy(controller, 'passConfirmSubmit'); + $rootScope.$broadcast('onAfterSignup', { + passphrase: 'TEST_VALUE', + target: 'primary-pass', + }); + expect(spy).to.have.been.calledWith('TEST_VALUE'); }); }); - describe('$scope.startGenratingNewPassphrase()', () => { + describe('generatePassphrase()', () => { it('sets this.generatingNewPassphrase = true', () => { - controller.startGenratingNewPassphrase(); + controller.generatePassphrase(); expect(controller.generatingNewPassphrase).to.equal(true); }); - - it('unbinds mousemove listener', () => { - const spy = sinon.spy(controller, 'reset'); - controller.startGenratingNewPassphrase(); - expect(spy).to.have.been.calledWith(); - }); - - it('creates this.listener(ev) which if called repeatedly will generate a random this.seed', () => { - controller.startGenratingNewPassphrase(); - expect(controller.seed).to.deep.equal(['00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00']); - expect(controller.progress).to.equal(0); - - for (let j = 0; j < 300; j++) { - const ev = { - pageX: Math.random() * 1000, - pageY: Math.random() * 1000, - }; - controller.listener(ev); - } - - expect(controller.seed).not.to.deep.equal(['00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00']); - expect(controller.progress).to.equal(100); - }); }); - describe('$scope.doTheLogin()', () => { - it('sets this.phassphrase as this.input_passphrase processed by fixCaseAndWhitespace', () => { - controller.input_passphrase = '\tGLOW two GliMpse camp aware tip brief confirm similar code float defense '; - controller.doTheLogin(); - expect(controller.passphrase).to.equal('glow two glimpse camp aware tip brief confirm similar code float defense'); + describe('passConfirmSubmit()', () => { + it('sets this.phassphrase as this.input_passphrase processed by normalizer', () => { + controller.input_passphrase = '\tTEST PassPHrASe '; + controller.passConfirmSubmit(); + expect(controller.passphrase).to.equal('test passphrase'); }); - it('calls this.reset()', () => { - controller.input_passphrase = testPassphrase; - const spy = sinon.spy(controller, 'reset'); - controller.doTheLogin(); + it('calls Passphrase.normalize()', () => { + const spy = sinon.spy(Passphrase, 'normalize'); + controller.passConfirmSubmit(); expect(spy).to.have.been.calledWith(); }); it('sets timeout with this.onLogin', () => { controller.input_passphrase = testPassphrase; const spy = sinon.spy(controller, '$timeout'); - controller.doTheLogin(); + controller.passConfirmSubmit(); expect(spy).to.have.been.calledWith(controller.onLogin); }); }); - describe('$scope.constructor()', () => { - it.skip('sets $watch on $ctrl.input_passphrase to keep validating it', () => { - // Skipped because it doesn't work - const spy = sinon.spy(controller.$scope, '$watch'); - controller.constructor(); - expect(spy).to.have.been.calledWith('$ctrl.input_passphrase', controller.isValidPassphrase); - }); - - it.skip('sets $watch that sets customFullscreen on small screens', () => { - }); - }); - - describe('$scope.simulateMousemove()', () => { - it('calls this.$document.mousemove()', () => { - const spy = sinon.spy(controller.$document, 'mousemove'); - controller.simulateMousemove(); - expect(spy).to.have.been.calledWith(); - }); - }); - - describe('$scope.setNewPassphrase()', () => { - it('opens a material design dialog', () => { - const seed = ['23', '34', '34', '34', '34', '34', '34', '34']; - const dialogSpy = sinon.spy(controller.$mdDialog, 'show'); - controller.setNewPassphrase(seed); - expect(dialogSpy).to.have.been.calledWith(); - }); - }); - - describe('$scope.devTestAccount()', () => { - it('sets input_passphrase from cookie called passphrase if present', () => { - const mock = sinon.mock(controller.$cookies); - mock.expects('get').returns(testPassphrase); - controller.devTestAccount(); - expect(controller.input_passphrase).to.equal(testPassphrase); - }); - - it('does nothing if cooke called passphrase not present', () => { - controller.input_passphrase = testPassphrase; - const mock = sinon.mock(controller.$cookies); - - mock.expects('get').returns(undefined); + describe('devTestAccount()', () => { + it('calls passConfirmSubmit with timeout if a passphrase is set in the cookies', () => { + $cookies.put('passphrase', testPassphrase); + const spy = sinon.spy(controller, '$timeout'); controller.devTestAccount(); - expect(controller.input_passphrase).to.equal(testPassphrase); - }); - }); - - describe('$scope.isValidPassphrase(value)', () => { - it('sets $scope.valid = 2 if value is empty', () => { - controller.isValidPassphrase(''); - expect(controller.valid).to.equal(2); - }); - - it('sets $scope.valid = 1 if value is valid', () => { - controller.isValidPassphrase('ability theme abandon abandon abandon abandon abandon abandon abandon abandon abandon absorb'); - expect(controller.valid).to.equal(1); - }); - - it('sets $scope.valid = 0 if value is invalid', () => { - controller.isValidPassphrase('INVALID VALUE'); - expect(controller.valid).to.equal(0); - }); - }); -}); - -describe('save $mdDialog controller', () => { - describe('constructor()', () => { - it.skip('sets $watch on $ctrl.missing_input', () => { - }); - }); - - describe('next()', () => { - it.skip('sets this.enter=true', () => { - }); - - it.skip('sets this.missing_word to a random word of passphrase', () => { - }); - - it.skip('sets this.pre to part of the passphrase before this.missing_word', () => { - }); - - it.skip('sets this.pos to part of the passphrase after this.missing_word', () => { - }); - }); - - describe('ok()', () => { - it.skip('calls ok()', () => { - }); - - it.skip('calls this.close()', () => { - }); - }); - - describe('close()', () => { - it.skip('calls this.$mdDialog.hide()', () => { + expect(spy).to.have.been.calledWith(); }); }); }); diff --git a/src/test/components/login/passphrase.spec.js b/src/test/components/login/passphrase.spec.js new file mode 100644 index 000000000..ca83a045b --- /dev/null +++ b/src/test/components/login/passphrase.spec.js @@ -0,0 +1,61 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); + +const expect = chai.expect; +chai.use(sinonChai); + +describe('Passphrase Directive', () => { + let $compile; + let $rootScope; + let $document; + let Passphrase; + let $isolateScope; + + beforeEach(() => { + // Load the myApp module, which contains the directive + angular.mock.module('app'); + + // Store references to $rootScope and $compile + // so they are available to all tests in this describe block + inject((_$compile_, _$rootScope_, _$document_, _Passphrase_) => { + $compile = _$compile_; + $rootScope = _$rootScope_; + $document = _$document_; + Passphrase = _Passphrase_; + }); + + // Compile a piece of HTML containing the directive + const element = angular.element(''); + const e = $compile(element)($rootScope); + e.scope().$digest(); + $isolateScope = e.isolateScope(); + }); + + describe('PassphraseLink', () => { + it('should assign progress to its own $scope', () => { + expect($isolateScope.progress).to.not.equal(undefined); + expect($isolateScope.progress).to.equal(Passphrase.progress); + }); + }); + + describe('$scope.simulateMousemove()', () => { + it('calls $document.mousemove()', () => { + const spy = sinon.spy($document, 'mousemove'); + $isolateScope.simulateMousemove(); + expect(spy).to.have.been.calledWith(); + }); + }); + + describe('$scope.mobileAndTabletcheck()', () => { + it('checks if the useAgent is a device', () => { + const agents = [ + 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25', + 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25', + ]; + let isDevice = true; + agents.forEach(agent => isDevice = isDevice && $isolateScope.mobileAndTabletcheck(agent)); + expect(isDevice).to.equal(true); + }); + }); +}); diff --git a/src/test/components/main/setSecondPassDirective.spec.js b/src/test/components/main/setSecondPassDirective.spec.js new file mode 100644 index 000000000..5f2697567 --- /dev/null +++ b/src/test/components/main/setSecondPassDirective.spec.js @@ -0,0 +1,137 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); + +const expect = chai.expect; +chai.use(sinonChai); + +describe('setSecondPass Directive', () => { + let $compile; + let $scope; + let $rootScope; + let element; + let $peers; + let setSecondPass; + let $q; + let success; + let error; + + beforeEach(() => { + // Load the myApp module, which contains the directive + angular.mock.module('app'); + + // Store references to $rootScope and $compile + // so they are available to all tests in this describe block + inject((_$compile_, _$rootScope_, _setSecondPass_, _$peers_, _$q_, _success_, _error_) => { + // The injector unwraps the underscores (_) from around the parameter names when matching + $compile = _$compile_; + $rootScope = _$rootScope_; + setSecondPass = _setSecondPass_; + $peers = _$peers_; + $q = _$q_; + success = _success_; + error = _error_; + $scope = $rootScope.$new(); + }); + + // Compile a piece of HTML containing the directive + element = $compile('')($scope); + $scope.$digest(); + }); + + describe('SetSecondPassLink', () => { + it('listens for an onAfterSignup event', () => { + $peers.active = { setSignature() {} }; + const mock = sinon.mock($peers.active); + const deffered = $q.defer(); + mock.expects('setSignature').returns(deffered.promise); + + const spy = sinon.spy(success, 'dialog'); + + $scope.$broadcast('onAfterSignup', { + passphrase: 'TEST_VALUE', + target: 'second-pass', + }); + + deffered.resolve({}); + $scope.$apply(); + + expect(spy).to.have.been.calledWith(); + }); + + it('binds click listener to call setSecondPass.show()', () => { + const spy = sinon.spy(setSecondPass, 'show'); + element.triggerHandler('click'); + $scope.$digest(); + + expect(spy).to.have.been.calledWith(); + }); + }); + + describe('scope.passConfirmSubmit', () => { + it('should call $peers.active.setSignature', () => { + $peers.active = { setSignature() {} }; + const mock = sinon.mock($peers.active); + const deffered = $q.defer(); + mock.expects('setSignature').returns(deffered.promise); + + const spy = sinon.spy(success, 'dialog'); + $scope.passConfirmSubmit(); + + deffered.resolve({}); + $scope.$apply(); + + expect(spy).to.have.been.calledWith(); + }); + + it('should show error dialog if trying to set second passphrase mulpiple times', () => { + $peers.active = { setSignature() {} }; + const mock = sinon.mock($peers.active); + const deffered = $q.defer(); + mock.expects('setSignature').returns(deffered.promise); + + const spy = sinon.spy(error, 'dialog'); + $scope.passConfirmSubmit(); + + deffered.reject({ message: 'Missing sender second signature' }); + $scope.$apply(); + expect(spy).to.have.been.calledWith(); + + deffered.reject({ message: 'Account does not have enough LSK : TEST_ADDRESS' }); + $scope.$apply(); + expect(spy).to.have.been.calledWith(); + + deffered.reject({ message: 'OTHER MESSAGE' }); + $scope.$apply(); + expect(spy).to.have.been.calledWith(); + }); + + it('should show error dialog if account does not have enough LSK', () => { + $peers.active = { setSignature() {} }; + const mock = sinon.mock($peers.active); + const deffered = $q.defer(); + mock.expects('setSignature').returns(deffered.promise); + + const spy = sinon.spy(error, 'dialog'); + $scope.passConfirmSubmit(); + + deffered.reject({ message: 'Missing sender second signature' }); + $scope.$apply(); + expect(spy).to.have.been.calledWith(); + }); + + it('should show error dialog for all the other errors', () => { + $peers.active = { setSignature() {} }; + const mock = sinon.mock($peers.active); + const deffered = $q.defer(); + mock.expects('setSignature').returns(deffered.promise); + + const spy = sinon.spy(error, 'dialog'); + $scope.passConfirmSubmit(); + + deffered.reject({ message: 'Other messages' }); + $scope.$apply(); + expect(spy).to.have.been.calledWith(); + }); + }); +}); diff --git a/src/test/components/main/setSecondPassService.spec.js b/src/test/components/main/setSecondPassService.spec.js new file mode 100644 index 000000000..4190f7788 --- /dev/null +++ b/src/test/components/main/setSecondPassService.spec.js @@ -0,0 +1,46 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); + +const expect = chai.expect; +chai.use(sinonChai); + +describe('Passphrase factory', () => { + let $mdDialog; + let setSecondPass; + + // Load the myApp module, which contains the directive + beforeEach(angular.mock.module('app')); + + // Store references to $rootScope and $compile + // so they are available to all tests in this describe block + beforeEach(inject((_setSecondPass_, _$mdDialog_) => { + // The injector unwraps the underscores (_) from around the parameter names when matching + $mdDialog = _$mdDialog_; + setSecondPass = _setSecondPass_; + })); + + describe('ok', () => { + it('should call $mdDialog.hide()', () => { + const spy = sinon.spy($mdDialog, 'hide'); + setSecondPass.ok(); + expect(spy).to.have.been.calledWith(); + }); + }); + + describe('cancel', () => { + it('should call $mdDialog.hide()', () => { + const spy = sinon.spy($mdDialog, 'hide'); + setSecondPass.cancel(); + expect(spy).to.have.been.calledWith(); + }); + }); + + describe('show', () => { + it('should call $mdDialog.show()', () => { + const spy = sinon.spy($mdDialog, 'show'); + setSecondPass.show(); + expect(spy).to.have.been.calledWith(); + }); + }); +}); diff --git a/src/test/services/passphrase.spec.js b/src/test/services/passphrase.spec.js new file mode 100644 index 000000000..72ea2df78 --- /dev/null +++ b/src/test/services/passphrase.spec.js @@ -0,0 +1,131 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); + +const expect = chai.expect; +chai.use(sinonChai); +const TEST_SEED = ['12', '12', '12', '12', '12', '12', '12', '12', + '12', '12', '12', '12', '12', '12', '12', '12']; +const INVALID_PASSPHRASE = 'INVALID_PASSPHRASE'; + +describe('Factory: Passphrase', () => { + let Passphrase; + + beforeEach(angular.mock.module('app')); + + beforeEach(inject((_Passphrase_) => { + Passphrase = _Passphrase_; + })); + + describe('Passphrase.reset()', () => { + it('resets percentage of progress and seed', () => { + Passphrase.init(); + Passphrase.progress = { + percentage: 50, + seed: TEST_SEED, + }; + Passphrase.reset(); + expect(Passphrase.progress.percentage).to.equal(0); + let allZero = true; + Passphrase.progress.seed.forEach(member => (allZero = (allZero && member === '00'))); + expect(allZero).to.equal(true); + }); + }); + + describe('Passphrase.init()', () => { + it('should define progress.steps as a number above 1.6', () => { + Passphrase.init(); + expect(Passphrase.progress.step).to.be.above(1.6); + }); + + it('should call Passphrase.reset()', () => { + const spy = sinon.spy(Passphrase, 'reset'); + Passphrase.init(); + expect(spy).to.have.been.calledWith(); + }); + }); + + describe('Passphrase.progress', () => { + it('should define progress object', () => { + Passphrase.init(); + expect(Passphrase.progress).to.not.equal(undefined); + }); + }); + + describe('Passphrase.generatePassPhrase', () => { + it('should generate a valid passphrase out of a given valid seed array', () => { + const passphrase = Passphrase.generatePassPhrase(TEST_SEED); + const isValid = Passphrase.isValidPassphrase(passphrase); + expect(isValid).to.equal(1); + }); + }); + + describe('Passphrase.isValidPassphrase', () => { + it('should return 1 for a valid passphrase', () => { + const passphrase = Passphrase.generatePassPhrase(TEST_SEED); + const isValid = Passphrase.isValidPassphrase(passphrase); + expect(isValid).to.equal(1); + }); + + it('should return 0 for an invalid passphrase', () => { + const isValid = Passphrase.isValidPassphrase(INVALID_PASSPHRASE); + expect(isValid).to.equal(0); + }); + + it('should return 2 for an empty passphrase', () => { + const isValid = Passphrase.isValidPassphrase(''); + expect(isValid).to.equal(2); + }); + }); + + describe('Passphrase.normalize', () => { + it('should trim multiple spaces globally and lowercase the string', () => { + const rawString = ' FIRST second Third '; + const fixedString = 'first second third'; + const result = Passphrase.normalize(rawString); + expect(result).to.equal(fixedString); + }); + }); + + describe('Passphrase.listener', () => { + it('should update progress percentage and seed if called with proper event', () => { + Passphrase.init(); + const event = { + pageY: 0, + pageX: 0, + }; + let percentage = -1; + let isProgressIncreasing = true; + + // √(2 * 90^2) > 120 + for (let i = 0; i < 100; i++) { + event.pageX = i * 90; + event.pageY = i * 90; + Passphrase.listener(event, () => {}); + isProgressIncreasing = isProgressIncreasing && + (percentage <= Passphrase.progress.percentage || + Math.floor(Passphrase.progress.percentage) === 100); + percentage = Passphrase.progress.percentage; + } + expect(isProgressIncreasing).to.equal(true); + }); + + it('should call callback if progress percentage is equal to 100', () => { + Passphrase.init(); + const event = { + pageY: 0, + pageX: 0, + }; + let seed = null; + const callback = param => seed = param; + + // √(2 * 90^2) > 120 + for (let i = 0; i < 100; i++) { + event.pageX = i * 90; + event.pageY = i * 90; + Passphrase.listener(event, callback); + } + expect(seed).to.not.equal(undefined); + }); + }); +}); diff --git a/src/test/test.js b/src/test/test.js index 30ad452fe..00960449c 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -2,7 +2,10 @@ require('./components/forging/forging.spec'); require('./components/delegates/delegates.spec'); require('./components/delegates/vote.spec'); require('./components/login/login.spec'); +require('./components/login/passphrase.spec'); require('./components/main/main.spec'); +require('./components/main/setSecondPassDirective.spec'); +require('./components/main/setSecondPassService.spec'); require('./components/send/send.spec'); require('./components/send/sendModalDirective.spec'); require('./components/top/top.spec'); @@ -12,6 +15,7 @@ require('./components/sign-verify/sign-message.spec'); require('./components/sign-verify/verify-message.spec'); require('./services/peers/peers.spec'); +require('./services/passphrase.spec'); require('./services/sign-verify.spec'); require('./services/lsk.spec');