From fdf9f2ba5402e5b99915165cb024cc42756982ec Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 6 Nov 2018 15:07:36 +0000 Subject: [PATCH 01/14] Refactor PIN and auth actions --- src/action/auth-mobile.js | 184 ++++++++++++++++++++++++++++++++++++++ src/action/wallet.js | 38 -------- 2 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 src/action/auth-mobile.js diff --git a/src/action/auth-mobile.js b/src/action/auth-mobile.js new file mode 100644 index 000000000..154d2adb0 --- /dev/null +++ b/src/action/auth-mobile.js @@ -0,0 +1,184 @@ +import { PIN_LENGTH } from '../config'; + +const PIN = 'DevicePin'; +const PASS = 'WalletPassword'; +const PASS_SIZE = 32; // 32 bytes (256 bits) + +class AuthAction { + constructor(store, nav, Alert, SecureStore, LocalAuthentication) { + this._store = store; + this._nav = nav; + this._Alert = Alert; + this._SecureStore = SecureStore; + this._LocalAuthentication = LocalAuthentication; + this.STORE = { + keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + }; + } + + // + // Auth PIN actions + // + + /** + * Initialize the set pin view by resetting input values + * and then navigating to the view. + * @return {undefined} + */ + initSetPin() { + this._store.auth.newPin = ''; + this._store.auth.pinVerify = ''; + this._nav.goSetPin(); + } + + /** + * Initialize the pin view by resetting input values + * and then navigating to the view. + * @return {undefined} + */ + initPin() { + this._store.auth.pin = ''; + this._nav.goPin(); + } + + /** + * Append a digit input to the pin parameter. + * @param {string} options.digit The digit to append to the pin + * @param {string} options.param The pin parameter name + * @return {undefined} + */ + pushPinDigit({ digit, param }) { + const { auth } = this._store; + if (auth[param].length < PIN_LENGTH) { + auth[param] += digit; + } + if (auth[param].length < PIN_LENGTH) { + return; + } + if (param === 'newPin') { + this._nav.goSetPinConfirm(); + } else if (param === 'pinVerify') { + this.checkNewPin(); + } else if (param === 'pin') { + this.checkPin(); + } + } + + /** + * Remove the last digit from the pin parameter. + * @param {string} options.param The pin parameter name + * @return {undefined} + */ + popPinDigit({ param }) { + const { auth } = this._store; + if (auth[param]) { + auth[param] = auth[param].slice(0, -1); + } else if (param === 'pinVerify') { + this.initSetPin(); + } + } + + /** + * Check the PIN that was chosen by the user has the correct + * length and that it was also entered correctly twice to make sure that + * there was no typo. + * @return {Promise} + */ + async checkNewPin() { + const { newPin, pinVerify } = this._store.auth; + if (newPin !== pinVerify) { + this._Alert.alert( + 'Incorrect PIN', + 'PINs do not match!', + [ + { + text: 'TRY AGAIN', + onPress: () => this.initSetPin(), + }, + ], + { cancelable: false } + ); + return; + } + await this._SecureStore.setItemAsync(PIN, newPin, this.STORE); + await this.unlockWallet(); + } + + async checkPin() { + const { pin } = this._store.auth; + const storedPin = await this._SecureStore.getItemAsync(PIN, this.STORE); + if (pin !== storedPin) { + this._Alert.alert( + 'Incorrect PIN', + [ + { + text: 'TRY AGAIN', + onPress: () => this.initPin(), + }, + ], + { cancelable: false } + ); + return; + } + await this.unlockWallet(); + } + + // + // Fingerprint Authentication + // + + /** + * Authenticate the user using either TouchID/FaceID on iOS or + * a fingerprint reader on Android + * @return {Promise} + */ + async authenticateUser() { + const hasHardware = await this._LocalAuthentication.hasHardwareAsync(); + if (!hasHardware) { + return; + } + const msg = 'Unlock your Wallet'; + const { success } = await this._LocalAuthentication.authenticateAsync(msg); + if (!success) { + return; + } + await this.unlockWallet(); + } + + async unlockWallet() { + const storedPass = await this._SecureStore.getItemAsync(PASS, this.STORE); + if (storedPass) { + this._store.password = storedPass; + // await this._wallet.checkPassword(); + this._nav.goHome(); + return; + } + // If no password exists yet, generate a random one + const newPass = this._totallyNotSecureRandomPassword(); + await this._SecureStore.setItemAsync(PASS, newPass, this.STORE); + this._store.newPassword = newPass; + this._store.passwordVerify = newPass; + // await this._wallet.checkNewPassword(); + this._nav.goHome(); + } + + /** + * NOT SECURE ... DO NOT USE IN PRODUCTION !!! + * + * Just a stop gap during development until we have a secure native + * PRNG: https://github.com/lightninglabs/lightning-app/issues/777 + * + * Generate some hex bytes for a random wallet password on mobile + * (which will be stretched using a KDF in lnd). + * @return {string} A hex string containing some random bytes + */ + _totallyNotSecureRandomPassword() { + const bytes = new Uint8Array(PASS_SIZE); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = Math.floor(256 * Math.random()); + } + return Buffer.from(bytes.buffer).toString('hex'); + } +} + +export default AuthAction; diff --git a/src/action/wallet.js b/src/action/wallet.js index 6903eaf08..876b1ecd7 100644 --- a/src/action/wallet.js +++ b/src/action/wallet.js @@ -9,7 +9,6 @@ import { NOTIFICATION_DELAY, RATE_DELAY, RECOVERY_WINDOW, - PIN_LENGTH, } from '../config'; import { when } from 'mobx'; import * as log from './log'; @@ -118,43 +117,6 @@ class WalletAction { this._store.wallet.passwordVerify = password; } - /** - * Append a digit input to the password parameter. - * @param {string} options.digit The digit to append to the password - * @param {string} options.param The password parameter name - * @return {undefined} - */ - pushPinDigit({ digit, param }) { - const { wallet } = this._store; - if (wallet[param].length < PIN_LENGTH) { - wallet[param] += digit; - } - if (wallet[param].length < PIN_LENGTH) { - return; - } - if (param === 'newPassword') { - this._nav.goSetPasswordConfirm(); - } else if (param === 'passwordVerify') { - this.checkNewPassword(PIN_LENGTH); - } else if (param === 'password') { - this.checkPassword(); - } - } - - /** - * Remove the last digit from the password parameter. - * @param {string} options.param The password parameter name - * @return {undefined} - */ - popPinDigit({ param }) { - const { wallet } = this._store; - if (wallet[param]) { - wallet[param] = wallet[param].slice(0, -1); - } else if (param === 'passwordVerify') { - this.initSetPassword(); - } - } - /** * Set whether or not we're restoring the wallet. * @param {boolean} options.restoring Whether or not we're restoring. From e605d57e90f47dbbac827fab4edda54968ccc1a7 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 6 Nov 2018 15:08:17 +0000 Subject: [PATCH 02/14] Refactor pin mobile view --- src/view/password-mobile.js | 59 -------------------------------- src/view/pin-mobile.js | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 59 deletions(-) delete mode 100644 src/view/password-mobile.js create mode 100644 src/view/pin-mobile.js diff --git a/src/view/password-mobile.js b/src/view/password-mobile.js deleted file mode 100644 index 7db3d5bc7..000000000 --- a/src/view/password-mobile.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; -import { observer } from 'mobx-react'; -import PropTypes from 'prop-types'; -import Background from '../component/background'; -import MainContent from '../component/main-content'; -import BoltIcon from '../asset/icon/lightning-bolt'; -import LightningWord from '../asset/icon/lightning-word'; -import { Text } from '../component/text'; -import { FormStretcher } from '../component/form'; -import { PinBubbles, PinKeyboard } from '../component/pin-entry'; - -// -// Password View (Mobile) -// - -const styles = StyleSheet.create({ - content: { - paddingLeft: 20, - paddingRight: 20, - }, - boltWrapper: { - marginTop: 50, - }, - wordWrapper: { - marginTop: 35, - }, - bubbles: { - marginTop: 10, - }, -}); - -const PasswordView = ({ store, wallet }) => ( - - - - - - - - - - Unlock with your pin - - - wallet.pushPinDigit({ digit, param: 'password' })} - onBackspace={() => wallet.popPinDigit({ param: 'password' })} - /> - - -); - -PasswordView.propTypes = { - store: PropTypes.object.isRequired, - wallet: PropTypes.object.isRequired, -}; - -export default observer(PasswordView); diff --git a/src/view/pin-mobile.js b/src/view/pin-mobile.js new file mode 100644 index 000000000..3843afcf1 --- /dev/null +++ b/src/view/pin-mobile.js @@ -0,0 +1,68 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import Background from '../component/background'; +import MainContent from '../component/main-content'; +import BoltIcon from '../asset/icon/lightning-bolt'; +import LightningWord from '../asset/icon/lightning-word'; +import { Text } from '../component/text'; +import { FormStretcher } from '../component/form'; +import { PinBubbles, PinKeyboard } from '../component/pin-entry'; + +// +// Pin View (Mobile) +// + +const styles = StyleSheet.create({ + content: { + paddingLeft: 20, + paddingRight: 20, + }, + boltWrapper: { + marginTop: 50, + }, + wordWrapper: { + marginTop: 35, + }, + bubbles: { + marginTop: 10, + }, +}); + +class PinView extends React.Component { + componentDidMount() { + this.props.auth.authenticateUser(); + } + + render() { + const { store, auth } = this.props; + return ( + + + + + + + + + + Unlock with your pin + + + auth.pushPinDigit({ digit, param: 'pin' })} + onBackspace={() => auth.popPinDigit({ param: 'pin' })} + /> + + + ); + } +} + +PinView.propTypes = { + store: PropTypes.object.isRequired, + auth: PropTypes.object.isRequired, +}; + +export default observer(PinView); From 4ccf00dca7f4ccae79e14178809e22f99860d4e2 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 6 Nov 2018 15:08:31 +0000 Subject: [PATCH 03/14] Refactor set pin mobile view --- ...{set-password-mobile.js => set-pin-mobile.js} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename src/view/{set-password-mobile.js => set-pin-mobile.js} (72%) diff --git a/src/view/set-password-mobile.js b/src/view/set-pin-mobile.js similarity index 72% rename from src/view/set-password-mobile.js rename to src/view/set-pin-mobile.js index 071493ff9..8874d18c6 100644 --- a/src/view/set-password-mobile.js +++ b/src/view/set-pin-mobile.js @@ -9,7 +9,7 @@ import { FormStretcher } from '../component/form'; import { PinBubbles, PinKeyboard } from '../component/pin-entry'; // -// Set Password View (Mobile) +// Set Pin View (Mobile) // const styles = StyleSheet.create({ @@ -27,7 +27,7 @@ const styles = StyleSheet.create({ }, }); -const SetPasswordView = ({ store, wallet }) => ( +const SetPinView = ({ store, auth }) => ( Set PIN @@ -35,19 +35,19 @@ const SetPasswordView = ({ store, wallet }) => ( Type the PIN you want to use to unlock your wallet. - + wallet.pushPinDigit({ digit, param: 'newPassword' })} - onBackspace={() => wallet.popPinDigit({ param: 'newPassword' })} + onInput={digit => auth.pushPinDigit({ digit, param: 'newPin' })} + onBackspace={() => auth.popPinDigit({ param: 'newPin' })} /> ); -SetPasswordView.propTypes = { +SetPinView.propTypes = { store: PropTypes.object.isRequired, - wallet: PropTypes.object.isRequired, + auth: PropTypes.object.isRequired, }; -export default observer(SetPasswordView); +export default observer(SetPinView); From c52b4693983a0341abb90021959a684511d646b4 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 6 Nov 2018 15:08:45 +0000 Subject: [PATCH 04/14] Refactor set pin confirm mobile view --- ...irm-mobile.js => set-pin-confirm-mobile.js} | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) rename src/view/{set-password-confirm-mobile.js => set-pin-confirm-mobile.js} (71%) diff --git a/src/view/set-password-confirm-mobile.js b/src/view/set-pin-confirm-mobile.js similarity index 71% rename from src/view/set-password-confirm-mobile.js rename to src/view/set-pin-confirm-mobile.js index 9ffc043e1..e72e01780 100644 --- a/src/view/set-password-confirm-mobile.js +++ b/src/view/set-pin-confirm-mobile.js @@ -9,7 +9,7 @@ import { FormStretcher } from '../component/form'; import { PinBubbles, PinKeyboard } from '../component/pin-entry'; // -// Set Password Confirm View (Mobile) +// Set Pin Confirm View (Mobile) // const styles = StyleSheet.create({ @@ -27,7 +27,7 @@ const styles = StyleSheet.create({ }, }); -const SetPasswordConfirmView = ({ store, wallet }) => ( +const SetPinConfirmView = ({ store, auth }) => ( Re-type PIN @@ -35,21 +35,19 @@ const SetPasswordConfirmView = ({ store, wallet }) => ( {"Type your PIN again to make sure it's the correct one."} - + - wallet.pushPinDigit({ digit, param: 'passwordVerify' }) - } - onBackspace={() => wallet.popPinDigit({ param: 'passwordVerify' })} + onInput={digit => auth.pushPinDigit({ digit, param: 'pinVerify' })} + onBackspace={() => auth.popPinDigit({ param: 'pinVerify' })} /> ); -SetPasswordConfirmView.propTypes = { +SetPinConfirmView.propTypes = { store: PropTypes.object.isRequired, - wallet: PropTypes.object.isRequired, + auth: PropTypes.object.isRequired, }; -export default observer(SetPasswordConfirmView); +export default observer(SetPinConfirmView); From 19d2d83bf188c6e70ef225022e6830a855540109 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 6 Nov 2018 15:09:46 +0000 Subject: [PATCH 05/14] Wire up pin views --- mobile/main.js | 27 +++++++++++++++------------ mobile/rn-cli.config.js | 1 + src/action/nav-mobile.js | 16 ++++++++-------- src/store.js | 5 +++++ stories/screen-story.js | 20 ++++++++++---------- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/mobile/main.js b/mobile/main.js index 9ecb7d5be..78e7ddcff 100644 --- a/mobile/main.js +++ b/mobile/main.js @@ -1,14 +1,15 @@ import React from 'react'; import { Clipboard } from 'react-native'; +import { Alert, SecureStore, LocalAuthentication } from 'expo'; import { createStackNavigator } from 'react-navigation'; import FontLoader from './component/font-loader'; -import SetPasswordView from '../src/view/set-password-mobile'; -import SetPasswordConfirmView from '../src/view/set-password-confirm-mobile'; +import SetPinView from '../src/view/set-pin-mobile'; +import SetPinConfirmView from '../src/view/set-pin-confirm-mobile'; import SeedSuccessView from '../src/view/seed-success'; import NewAddressView from '../src/view/new-address'; -import PasswordView from '../src/view/password-mobile'; +import PinView from '../src/view/pin-mobile'; import WaitView from '../src/view/wait-mobile'; import HomeView from '../src/view/home'; import SettingView from '../src/view/setting'; @@ -35,6 +36,8 @@ import PaymentAction from '../src/action/payment'; import ChannelAction from '../src/action/channel'; import TransactionAction from '../src/action/transaction'; +import AuthAction from '../src/action/auth-mobile'; + const store = new Store(); store.init(); const nav = new NavAction(store); @@ -61,12 +64,12 @@ sinon.stub(channel, 'update'); sinon.stub(channel, 'connectAndOpen'); sinon.stub(channel, 'closeSelectedChannel'); -const SetPassword = () => ( - -); +const auth = new AuthAction(store, nav, Alert, SecureStore, LocalAuthentication); + +const SetPin = () => ; -const SetPasswordConfirm = () => ( - +const SetPinConfirm = () => ( + ); const SeedSuccess = () => ; @@ -79,7 +82,7 @@ const NewAddress = () => ( /> ); -const Password = () => ; +const Pin = () => ; const Wait = () => ; @@ -119,11 +122,11 @@ const Pay = () => ; const MainStack = createStackNavigator( { - SetPassword, - SetPasswordConfirm, + SetPin, + SetPinConfirm, SeedSuccess, NewAddress, - Password, + Pin, Wait, Home, Settings, diff --git a/mobile/rn-cli.config.js b/mobile/rn-cli.config.js index f7e9066a1..40f21c1b7 100644 --- a/mobile/rn-cli.config.js +++ b/mobile/rn-cli.config.js @@ -4,6 +4,7 @@ module.exports = { extraNodeModules: { react: path.resolve(__dirname, 'node_modules/react'), 'react-native': path.resolve(__dirname, 'node_modules/react-native'), + expo: path.resolve(__dirname, 'node_modules/expo'), 'prop-types': path.resolve(__dirname, 'node_modules/prop-types'), 'react-native-svg': path.resolve( __dirname, diff --git a/src/action/nav-mobile.js b/src/action/nav-mobile.js index 90d2f399c..5f55e0fb6 100644 --- a/src/action/nav-mobile.js +++ b/src/action/nav-mobile.js @@ -36,24 +36,24 @@ class NavAction { this._navigate('RestoreSeed'); } - goRestorePassword() { - this._navigate('RestorePassword'); + goRestorePin() { + this._navigate('RestorePin'); } goSeedSuccess() { this._navigate('SeedSuccess'); } - goSetPassword() { - this._navigate('SetPassword'); + goSetPin() { + this._navigate('SetPin'); } - goSetPasswordConfirm() { - this._navigate('SetPasswordConfirm'); + goSetPinConfirm() { + this._navigate('SetPinConfirm'); } - goPassword() { - this._navigate('Password'); + goPin() { + this._navigate('Pin'); } goNewAddress() { diff --git a/src/store.js b/src/store.js index 2b3857402..9b6ad3d9b 100644 --- a/src/store.js +++ b/src/store.js @@ -36,6 +36,11 @@ export class Store { pubKey: null, walletAddress: null, displayCopied: false, + auth: { + pin: '', + newPin: '', + pinVerify: '', + }, wallet: { password: '', newPassword: '', diff --git a/stories/screen-story.js b/stories/screen-story.js index 99193e3b3..4c94d270f 100644 --- a/stories/screen-story.js +++ b/stories/screen-story.js @@ -16,6 +16,7 @@ import InvoiceAction from '../src/action/invoice'; import PaymentAction from '../src/action/payment'; import ChannelAction from '../src/action/channel'; import TransactionAction from '../src/action/transaction'; +import AuthAction from '../src/action/auth-mobile'; import Welcome from '../src/view/welcome'; import Transaction from '../src/view/transaction'; import Setting from '../src/view/setting'; @@ -46,11 +47,11 @@ import SeedSuccess from '../src/view/seed-success'; import Seed from '../src/view/seed'; import SeedVerify from '../src/view/seed-verify'; import SetPassword from '../src/view/set-password'; -import SetPasswordMobile from '../src/view/set-password-mobile'; +import SetPinMobile from '../src/view/set-pin-mobile'; import SetPasswordConfirm from '../src/view/set-password-confirm'; -import SetPasswordConfirmMobile from '../src/view/set-password-confirm-mobile'; +import SetPinConfirmMobile from '../src/view/set-pin-confirm-mobile'; import Password from '../src/view/password'; -import PasswordMobile from '../src/view/password-mobile'; +import PinMobile from '../src/view/pin-mobile'; import RestorePassword from '../src/view/restore-password'; import ResetPasswordCurrent from '../src/view/reset-password-current'; import ResetPasswordNew from '../src/view/reset-password-new'; @@ -88,6 +89,7 @@ const channel = new ChannelAction(store, grpc, nav, notify); sinon.stub(channel, 'update'); sinon.stub(channel, 'connectAndOpen'); sinon.stub(channel, 'closeSelectedChannel'); +const auth = sinon.createStubInstance(AuthAction); storiesOf('Screens', module) .add('Welcome', () => ) @@ -109,16 +111,14 @@ storiesOf('Screens', module) .add('Set Password Confirm', () => ( )) - .add('Set Password (Mobile)', () => ( - + .add('Set PIN (Mobile)', () => ( + )) - .add('Set Password Confirm (Mobile)', () => ( - + .add('Set PIN Confirm (Mobile)', () => ( + )) .add('Password', () => ) - .add('Password (Mobile)', () => ( - - )) + .add('PIN (Mobile)', () => ) .add('Restore Wallet: Password', () => ( )) From 8172c78123de1483da5111edccf485edc7ad6153 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 6 Nov 2018 16:42:17 +0000 Subject: [PATCH 06/14] Fix pin input in storybook --- mobile/main.js | 4 ++-- src/action/auth-mobile.js | 4 ++-- stories/screen-story.js | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/mobile/main.js b/mobile/main.js index 78e7ddcff..f55604652 100644 --- a/mobile/main.js +++ b/mobile/main.js @@ -1,6 +1,6 @@ import React from 'react'; import { Clipboard } from 'react-native'; -import { Alert, SecureStore, LocalAuthentication } from 'expo'; +import { SecureStore, LocalAuthentication, Alert } from 'expo'; import { createStackNavigator } from 'react-navigation'; import FontLoader from './component/font-loader'; @@ -64,7 +64,7 @@ sinon.stub(channel, 'update'); sinon.stub(channel, 'connectAndOpen'); sinon.stub(channel, 'closeSelectedChannel'); -const auth = new AuthAction(store, nav, Alert, SecureStore, LocalAuthentication); +const auth = new AuthAction(store, nav, SecureStore, LocalAuthentication, Alert); const SetPin = () => ; diff --git a/src/action/auth-mobile.js b/src/action/auth-mobile.js index 154d2adb0..750de28db 100644 --- a/src/action/auth-mobile.js +++ b/src/action/auth-mobile.js @@ -5,12 +5,12 @@ const PASS = 'WalletPassword'; const PASS_SIZE = 32; // 32 bytes (256 bits) class AuthAction { - constructor(store, nav, Alert, SecureStore, LocalAuthentication) { + constructor(store, nav, SecureStore, LocalAuthentication, Alert) { this._store = store; this._nav = nav; - this._Alert = Alert; this._SecureStore = SecureStore; this._LocalAuthentication = LocalAuthentication; + this._Alert = Alert; this.STORE = { keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, }; diff --git a/stories/screen-story.js b/stories/screen-story.js index 4c94d270f..83453604f 100644 --- a/stories/screen-story.js +++ b/stories/screen-story.js @@ -89,7 +89,11 @@ const channel = new ChannelAction(store, grpc, nav, notify); sinon.stub(channel, 'update'); sinon.stub(channel, 'connectAndOpen'); sinon.stub(channel, 'closeSelectedChannel'); -const auth = sinon.createStubInstance(AuthAction); +const auth = new AuthAction(store, nav, {}); +sinon.stub(auth, 'checkNewPin'); +sinon.stub(auth, 'checkPin'); +sinon.stub(auth, 'authenticateUser'); +sinon.stub(auth, 'unlockWallet'); storiesOf('Screens', module) .add('Welcome', () => ) From ba2a64343592abec2b9913177d9dd1955ce2baed Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 9 Nov 2018 11:40:41 +0000 Subject: [PATCH 07/14] Wire up auth-mobile module to be more testable --- mobile/main.js | 12 ++++++--- src/action/auth-mobile.js | 52 ++++++++++++++++++--------------------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/mobile/main.js b/mobile/main.js index f55604652..ae36146cf 100644 --- a/mobile/main.js +++ b/mobile/main.js @@ -1,6 +1,6 @@ import React from 'react'; -import { Clipboard } from 'react-native'; -import { SecureStore, LocalAuthentication, Alert } from 'expo'; +import { Clipboard, Alert } from 'react-native'; +import { SecureStore, LocalAuthentication } from 'expo'; import { createStackNavigator } from 'react-navigation'; import FontLoader from './component/font-loader'; @@ -64,7 +64,13 @@ sinon.stub(channel, 'update'); sinon.stub(channel, 'connectAndOpen'); sinon.stub(channel, 'closeSelectedChannel'); -const auth = new AuthAction(store, nav, SecureStore, LocalAuthentication, Alert); +const auth = new AuthAction( + store, + nav, + SecureStore, + LocalAuthentication, + Alert +); const SetPin = () => ; diff --git a/src/action/auth-mobile.js b/src/action/auth-mobile.js index 750de28db..dda1de17f 100644 --- a/src/action/auth-mobile.js +++ b/src/action/auth-mobile.js @@ -11,9 +11,6 @@ class AuthAction { this._SecureStore = SecureStore; this._LocalAuthentication = LocalAuthentication; this._Alert = Alert; - this.STORE = { - keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, - }; } // @@ -87,37 +84,18 @@ class AuthAction { async checkNewPin() { const { newPin, pinVerify } = this._store.auth; if (newPin !== pinVerify) { - this._Alert.alert( - 'Incorrect PIN', - 'PINs do not match!', - [ - { - text: 'TRY AGAIN', - onPress: () => this.initSetPin(), - }, - ], - { cancelable: false } - ); + this._alert('Incorrect PIN', () => this.initSetPin()); return; } - await this._SecureStore.setItemAsync(PIN, newPin, this.STORE); + await this._setToKeyStore(PIN, newPin); await this.unlockWallet(); } async checkPin() { const { pin } = this._store.auth; - const storedPin = await this._SecureStore.getItemAsync(PIN, this.STORE); + const storedPin = await this._getFromKeyStore(PIN); if (pin !== storedPin) { - this._Alert.alert( - 'Incorrect PIN', - [ - { - text: 'TRY AGAIN', - onPress: () => this.initPin(), - }, - ], - { cancelable: false } - ); + this._alert('Incorrect PIN', () => this.initPin()); return; } await this.unlockWallet(); @@ -146,7 +124,7 @@ class AuthAction { } async unlockWallet() { - const storedPass = await this._SecureStore.getItemAsync(PASS, this.STORE); + const storedPass = await this._getFromKeyStore(PASS); if (storedPass) { this._store.password = storedPass; // await this._wallet.checkPassword(); @@ -155,13 +133,31 @@ class AuthAction { } // If no password exists yet, generate a random one const newPass = this._totallyNotSecureRandomPassword(); - await this._SecureStore.setItemAsync(PASS, newPass, this.STORE); + await this._setToKeyStore(PASS, newPass); this._store.newPassword = newPass; this._store.passwordVerify = newPass; // await this._wallet.checkNewPassword(); this._nav.goHome(); } + _getFromKeyStore(key) { + const options = { + keychainAccessible: this._SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + }; + return this._SecureStore.getItemAsync(key, options); + } + + _setToKeyStore(key, value) { + const options = { + keychainAccessible: this._SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + }; + return this._SecureStore.setItemAsync(key, value, options); + } + + _alert(title, callback) { + this._Alert.alert(title, '', [{ text: 'TRY AGAIN', onPress: callback }]); + } + /** * NOT SECURE ... DO NOT USE IN PRODUCTION !!! * From 687b105844c730fff60e6f6a0db5ecfe572b7d20 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 9 Nov 2018 11:42:00 +0000 Subject: [PATCH 08/14] Fix wallet action tests --- src/action/wallet.js | 6 ++-- test/unit/action/wallet.spec.js | 57 --------------------------------- 2 files changed, 3 insertions(+), 60 deletions(-) diff --git a/src/action/wallet.js b/src/action/wallet.js index 876b1ecd7..9b2729f81 100644 --- a/src/action/wallet.js +++ b/src/action/wallet.js @@ -202,11 +202,11 @@ class WalletAction { * there was no typo. * @return {Promise} */ - async checkNewPassword(minLength = MIN_PASSWORD_LENGTH) { + async checkNewPassword() { const { newPassword, passwordVerify } = this._store.wallet; let errorMsg; - if (!newPassword || newPassword.length < minLength) { - errorMsg = `Set a password with at least ${minLength} characters.`; + if (!newPassword || newPassword.length < MIN_PASSWORD_LENGTH) { + errorMsg = `Set a password with at least ${MIN_PASSWORD_LENGTH} characters.`; } else if (newPassword !== passwordVerify) { errorMsg = 'Passwords do not match!'; } diff --git a/test/unit/action/wallet.spec.js b/test/unit/action/wallet.spec.js index 2d0ef0721..2b0defeec 100644 --- a/test/unit/action/wallet.spec.js +++ b/test/unit/action/wallet.spec.js @@ -113,53 +113,6 @@ describe('Action Wallet Unit Tests', () => { }); }); - describe('pushPinDigit()', () => { - it('should add a digit for empty password', () => { - wallet.pushPinDigit({ digit: '1', param: 'password' }); - expect(store.wallet.password, 'to equal', '1'); - }); - - it('should add no digit for max length password', () => { - store.wallet.password = '000000'; - wallet.pushPinDigit({ digit: '1', param: 'password' }); - expect(store.wallet.password, 'to equal', '000000'); - }); - - it('should go to next screen on last digit', () => { - store.wallet.newPassword = '00000'; - wallet.pushPinDigit({ digit: '1', param: 'newPassword' }); - expect(store.wallet.newPassword, 'to equal', '000001'); - expect(nav.goSetPasswordConfirm, 'was called once'); - }); - - it('should not go to next screen on fifth digit', () => { - store.wallet.newPassword = '0000'; - wallet.pushPinDigit({ digit: '1', param: 'newPassword' }); - expect(store.wallet.newPassword, 'to equal', '00001'); - expect(nav.goSetPasswordConfirm, 'was not called'); - }); - }); - - describe('popPinDigit()', () => { - it('should remove digit from a password', () => { - store.wallet.password = '000000'; - wallet.popPinDigit({ param: 'password' }); - expect(store.wallet.password, 'to equal', '00000'); - }); - - it('should not remove a digit from an empty password', () => { - store.wallet.password = ''; - wallet.popPinDigit({ param: 'password' }); - expect(store.wallet.password, 'to equal', ''); - }); - - it('should go back to SetPassword screen on empty string', () => { - store.wallet.passwordVerify = ''; - wallet.popPinDigit({ param: 'passwordVerify' }); - expect(nav.goSetPassword, 'was called once'); - }); - }); - describe('setRestoringWallet()', () => { it('should clear attributes', () => { wallet.setRestoringWallet({ restoring: true }); @@ -287,16 +240,6 @@ describe('Action Wallet Unit Tests', () => { expect(wallet.initSetPassword, 'was called once'); expect(notification.display, 'was called once'); }); - - it('should support custom length', async () => { - wallet.setNewPassword({ password: 'secret' }); - wallet.setPasswordVerify({ password: 'secret' }); - await wallet.checkNewPassword(6); - expect(wallet.initWallet, 'was called with', { - walletPassword: 'secret', - seedMnemonic: ['foo', 'bar', 'baz'], - }); - }); }); describe('checkResetPassword()', () => { From 1c10839b844a63b5b776ed3640ea9da2af8f9ca2 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 9 Nov 2018 13:45:57 +0000 Subject: [PATCH 09/14] Write auth-mobile unit tests --- mobile/main.js | 9 +- src/action/auth-mobile.js | 88 +++++++---- src/action/nav-mobile.js | 7 +- src/view/pin-mobile.js | 2 +- stories/screen-story.js | 7 +- test/unit/action/auth-mobile.spec.js | 218 +++++++++++++++++++++++++++ 6 files changed, 288 insertions(+), 43 deletions(-) create mode 100644 test/unit/action/auth-mobile.spec.js diff --git a/mobile/main.js b/mobile/main.js index ae36146cf..f80753fa0 100644 --- a/mobile/main.js +++ b/mobile/main.js @@ -1,7 +1,11 @@ +/** + * @fileOverview The main module that wires up all depdencies for mobile. + */ + import React from 'react'; import { Clipboard, Alert } from 'react-native'; import { SecureStore, LocalAuthentication } from 'expo'; -import { createStackNavigator } from 'react-navigation'; +import { createStackNavigator, NavigationActions } from 'react-navigation'; import FontLoader from './component/font-loader'; import SetPinView from '../src/view/set-pin-mobile'; @@ -40,7 +44,7 @@ import AuthAction from '../src/action/auth-mobile'; const store = new Store(); store.init(); -const nav = new NavAction(store); +const nav = new NavAction(store, NavigationActions); const db = sinon.createStubInstance(AppStorage); const ipc = sinon.createStubInstance(IpcAction); const grpc = sinon.createStubInstance(GrpcAction); @@ -66,6 +70,7 @@ sinon.stub(channel, 'closeSelectedChannel'); const auth = new AuthAction( store, + wallet, nav, SecureStore, LocalAuthentication, diff --git a/src/action/auth-mobile.js b/src/action/auth-mobile.js index dda1de17f..05ab02274 100644 --- a/src/action/auth-mobile.js +++ b/src/action/auth-mobile.js @@ -1,20 +1,25 @@ +/** + * @fileOverview action to handle mobile specific authentication + * using PINs, TouchID, and KeyStore storage. + */ + import { PIN_LENGTH } from '../config'; const PIN = 'DevicePin'; const PASS = 'WalletPassword'; -const PASS_SIZE = 32; // 32 bytes (256 bits) class AuthAction { - constructor(store, nav, SecureStore, LocalAuthentication, Alert) { + constructor(store, wallet, nav, SecureStore, Fingerprint, Alert) { this._store = store; + this._wallet = wallet; this._nav = nav; this._SecureStore = SecureStore; - this._LocalAuthentication = LocalAuthentication; + this._Fingerprint = Fingerprint; this._Alert = Alert; } // - // Auth PIN actions + // PIN actions // /** @@ -76,21 +81,28 @@ class AuthAction { } /** - * Check the PIN that was chosen by the user has the correct - * length and that it was also entered correctly twice to make sure that - * there was no typo. + * Check the PIN that was chosen by the user was entered + * correctly twice to make sure that there was no typo. + * If everything is ok, store the pin in the keystore and + * unlock the wallet. * @return {Promise} */ async checkNewPin() { const { newPin, pinVerify } = this._store.auth; - if (newPin !== pinVerify) { + if (newPin.length !== PIN_LENGTH || newPin !== pinVerify) { this._alert('Incorrect PIN', () => this.initSetPin()); return; } await this._setToKeyStore(PIN, newPin); - await this.unlockWallet(); + await this._generateWalletPassword(); } + /** + * Check the PIN that was entered by the user in the unlock + * screen matches the pin stored in the keystore and unlock + * the wallet. + * @return {Promise} + */ async checkPin() { const { pin } = this._store.auth; const storedPin = await this._getFromKeyStore(PIN); @@ -98,46 +110,56 @@ class AuthAction { this._alert('Incorrect PIN', () => this.initPin()); return; } - await this.unlockWallet(); + await this._unlockWallet(); } // - // Fingerprint Authentication + // TouchID & KeyStore Authentication // /** - * Authenticate the user using either TouchID/FaceID on iOS or - * a fingerprint reader on Android + * Try authenticating the user using either via TouchID/FaceID on iOS + * or a fingerprint reader on Android. * @return {Promise} */ - async authenticateUser() { - const hasHardware = await this._LocalAuthentication.hasHardwareAsync(); + async tryFingerprint() { + const hasHardware = await this._Fingerprint.hasHardwareAsync(); if (!hasHardware) { return; } const msg = 'Unlock your Wallet'; - const { success } = await this._LocalAuthentication.authenticateAsync(msg); + const { success } = await this._Fingerprint.authenticateAsync(msg); if (!success) { return; } - await this.unlockWallet(); + await this._unlockWallet(); } - async unlockWallet() { - const storedPass = await this._getFromKeyStore(PASS); - if (storedPass) { - this._store.password = storedPass; - // await this._wallet.checkPassword(); - this._nav.goHome(); - return; - } - // If no password exists yet, generate a random one + /** + * A new wallet password is generated and stored in the keystore + * during device setup. This password is not intended to be displayed + * to the user but is unlocked at the application layer via TouchID + * or PIN (which is stored in the keystore). + * @return {Promise} + */ + async _generateWalletPassword() { const newPass = this._totallyNotSecureRandomPassword(); await this._setToKeyStore(PASS, newPass); - this._store.newPassword = newPass; - this._store.passwordVerify = newPass; - // await this._wallet.checkNewPassword(); - this._nav.goHome(); + this._store.wallet.newPassword = newPass; + this._store.wallet.passwordVerify = newPass; + await this._wallet.checkNewPassword(); + } + + /** + * Unlock the wallet using a randomly generated password that is + * stored in the keystore. This password is not intended to be displayed + * to the user but rather unlocked at the application layer. + * @return {Promise} + */ + async _unlockWallet() { + const storedPass = await this._getFromKeyStore(PASS); + this._store.wallet.password = storedPass; + await this._wallet.checkPassword(); } _getFromKeyStore(key) { @@ -164,12 +186,12 @@ class AuthAction { * Just a stop gap during development until we have a secure native * PRNG: https://github.com/lightninglabs/lightning-app/issues/777 * - * Generate some hex bytes for a random wallet password on mobile - * (which will be stretched using a KDF in lnd). + * Generate a hex encoded 256 bit entropy wallet password (which will + * be stretched using a KDF in lnd). * @return {string} A hex string containing some random bytes */ _totallyNotSecureRandomPassword() { - const bytes = new Uint8Array(PASS_SIZE); + const bytes = new Uint8Array(32); for (let i = 0; i < bytes.length; i++) { bytes[i] = Math.floor(256 * Math.random()); } diff --git a/src/action/nav-mobile.js b/src/action/nav-mobile.js index 5f55e0fb6..eacbe1832 100644 --- a/src/action/nav-mobile.js +++ b/src/action/nav-mobile.js @@ -4,16 +4,15 @@ * and only change the route to be rendered in the user interface. */ -import { NavigationActions } from 'react-navigation'; - class NavAction { - constructor(store) { + constructor(store, Navigation) { this._store = store; + this._Navigation = Navigation; } setTopLevelNavigator(navigatorRef) { this._navigate = (routeName, params) => - navigatorRef.dispatch(NavigationActions.navigate({ routeName, params })); + navigatorRef.dispatch(this._Navigation.navigate({ routeName, params })); } goLoader() { diff --git a/src/view/pin-mobile.js b/src/view/pin-mobile.js index 3843afcf1..f56c572e7 100644 --- a/src/view/pin-mobile.js +++ b/src/view/pin-mobile.js @@ -32,7 +32,7 @@ const styles = StyleSheet.create({ class PinView extends React.Component { componentDidMount() { - this.props.auth.authenticateUser(); + this.props.auth.tryFingerprint(); } render() { diff --git a/stories/screen-story.js b/stories/screen-story.js index 83453604f..d6962de54 100644 --- a/stories/screen-story.js +++ b/stories/screen-story.js @@ -89,11 +89,12 @@ const channel = new ChannelAction(store, grpc, nav, notify); sinon.stub(channel, 'update'); sinon.stub(channel, 'connectAndOpen'); sinon.stub(channel, 'closeSelectedChannel'); -const auth = new AuthAction(store, nav, {}); +const auth = new AuthAction(store, wallet, nav); sinon.stub(auth, 'checkNewPin'); sinon.stub(auth, 'checkPin'); -sinon.stub(auth, 'authenticateUser'); -sinon.stub(auth, 'unlockWallet'); +sinon.stub(auth, 'tryFingerprint'); +sinon.stub(auth, '_unlockWallet'); +sinon.stub(auth, '_generateWalletPassword'); storiesOf('Screens', module) .add('Welcome', () => ) diff --git a/test/unit/action/auth-mobile.spec.js b/test/unit/action/auth-mobile.spec.js new file mode 100644 index 000000000..865937a08 --- /dev/null +++ b/test/unit/action/auth-mobile.spec.js @@ -0,0 +1,218 @@ +import { Store } from '../../../src/store'; +import NavAction from '../../../src/action/nav-mobile'; +import WalletAction from '../../../src/action/wallet'; +import AuthAction from '../../../src/action/auth-mobile'; + +describe('Action AuthMobile Unit Tests', () => { + let sandbox; + let store; + let wallet; + let nav; + let auth; + let SecureStore; + let Fingerprint; + let Alert; + + beforeEach(() => { + sandbox = sinon.createSandbox({}); + store = new Store(); + wallet = sinon.createStubInstance(WalletAction); + nav = sinon.createStubInstance(NavAction); + SecureStore = { + getItemAsync: sinon.stub(), + setItemAsync: sinon.stub(), + }; + Fingerprint = { + hasHardwareAsync: sinon.stub(), + authenticateAsync: sinon.stub(), + }; + Alert = { + alert: sinon.stub(), + }; + auth = new AuthAction(store, wallet, nav, SecureStore, Fingerprint, Alert); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('initSetPin()', () => { + it('should init values and navigate', () => { + auth.initSetPin(); + expect(store.auth.newPin, 'to equal', ''); + expect(store.auth.pinVerify, 'to equal', ''); + expect(nav.goSetPin, 'was called once'); + }); + }); + + describe('initPin()', () => { + it('should init values and navigate', () => { + auth.initPin(); + expect(store.auth.pin, 'to equal', ''); + expect(nav.goPin, 'was called once'); + }); + }); + + describe('pushPinDigit()', () => { + it('should add a digit for empty pin', () => { + auth.pushPinDigit({ digit: '1', param: 'pin' }); + expect(store.auth.pin, 'to equal', '1'); + }); + + it('should add no digit for max length pin', () => { + store.auth.pin = '000000'; + auth.pushPinDigit({ digit: '1', param: 'pin' }); + expect(store.auth.pin, 'to equal', '000000'); + }); + + it('should go to next screen on last digit', () => { + store.auth.newPin = '00000'; + auth.pushPinDigit({ digit: '1', param: 'newPin' }); + expect(store.auth.newPin, 'to equal', '000001'); + expect(nav.goSetPinConfirm, 'was called once'); + }); + + it('should not go to next screen on fifth digit', () => { + store.auth.newPin = '0000'; + auth.pushPinDigit({ digit: '1', param: 'newPin' }); + expect(store.auth.newPin, 'to equal', '00001'); + expect(nav.goSetPinConfirm, 'was not called'); + }); + }); + + describe('popPinDigit()', () => { + it('should remove digit from a pin', () => { + store.auth.pin = '000000'; + auth.popPinDigit({ param: 'pin' }); + expect(store.auth.pin, 'to equal', '00000'); + }); + + it('should not remove a digit from an empty pin', () => { + store.auth.pin = ''; + auth.popPinDigit({ param: 'pin' }); + expect(store.auth.pin, 'to equal', ''); + }); + + it('should go back to SetPassword screen on empty string', () => { + store.auth.pinVerify = ''; + auth.popPinDigit({ param: 'pinVerify' }); + expect(nav.goSetPin, 'was called once'); + }); + }); + + describe('checkNewPin()', () => { + beforeEach(() => { + sandbox.stub(auth, '_generateWalletPassword'); + }); + + it('should work for two same pins', async () => { + store.auth.newPin = '000000'; + store.auth.pinVerify = '000000'; + await auth.checkNewPin(); + expect( + SecureStore.setItemAsync, + 'was called with', + 'DevicePin', + '000000' + ); + expect(auth._generateWalletPassword, 'was called once'); + }); + + it('should display error for too short pins', async () => { + store.auth.newPin = '00000'; + store.auth.pinVerify = '00000'; + await auth.checkNewPin(); + expect(Alert.alert, 'was called once'); + expect(SecureStore.setItemAsync, 'was not called'); + expect(auth._generateWalletPassword, 'was not called'); + }); + + it('should display error for non matching pins', async () => { + store.auth.newPin = '000000'; + store.auth.pinVerify = '000001'; + await auth.checkNewPin(); + expect(Alert.alert, 'was called once'); + expect(SecureStore.setItemAsync, 'was not called'); + expect(auth._generateWalletPassword, 'was not called'); + }); + }); + + describe('checkPin()', () => { + beforeEach(() => { + sandbox.stub(auth, '_unlockWallet'); + }); + + it('should work for two same pins', async () => { + store.auth.pin = '000000'; + SecureStore.getItemAsync.resolves('000000'); + await auth.checkPin(); + expect(auth._unlockWallet, 'was called once'); + }); + + it('should display error for non matching pins', async () => { + store.auth.pin = '000001'; + SecureStore.getItemAsync.resolves('000000'); + await auth.checkPin(); + expect(Alert.alert, 'was called once'); + expect(auth._unlockWallet, 'was not called'); + }); + }); + + describe('tryFingerprint()', () => { + beforeEach(() => { + sandbox.stub(auth, '_unlockWallet'); + }); + + it('should not unlock wallet without hardware support', async () => { + Fingerprint.hasHardwareAsync.resolves(false); + await auth.tryFingerprint(); + expect(auth._unlockWallet, 'was not called'); + }); + + it('should not unlock wallet if authentication failed', async () => { + Fingerprint.hasHardwareAsync.resolves(true); + Fingerprint.authenticateAsync.resolves({ success: false }); + await auth.tryFingerprint(); + expect(auth._unlockWallet, 'was not called'); + }); + + it('should unlock wallet if authentication worked', async () => { + Fingerprint.hasHardwareAsync.resolves(true); + Fingerprint.authenticateAsync.resolves({ success: true }); + await auth.tryFingerprint(); + expect(auth._unlockWallet, 'was called once'); + }); + }); + + describe('_generateWalletPassword()', () => { + it('should generate a password and store it', async () => { + await auth._generateWalletPassword(); + expect( + SecureStore.setItemAsync, + 'was called with', + 'WalletPassword', + /^[0-9a-f]{64}$/ + ); + expect(store.wallet.newPassword, 'to match', /^[0-9a-f]{64}$/); + expect(store.wallet.passwordVerify, 'to match', /^[0-9a-f]{64}$/); + expect(wallet.checkNewPassword, 'was called once'); + }); + }); + + describe('_unlockWallet()', () => { + it('should not unlock wallet without hardware support', async () => { + SecureStore.getItemAsync.resolves('some-password'); + await auth._unlockWallet(); + expect(SecureStore.getItemAsync, 'was called with', 'WalletPassword'); + expect(store.wallet.password, 'to equal', 'some-password'); + expect(wallet.checkPassword, 'was called once'); + }); + }); + + describe('_totallyNotSecureRandomPassword()', () => { + it('should generate hex encoded 256bit entropy password', async () => { + const pass = auth._totallyNotSecureRandomPassword(); + expect(pass.length, 'to equal', 64); + }); + }); +}); From 791430d0a33c6fa5827d60796d933b46dc621b72 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 9 Nov 2018 16:39:45 +0000 Subject: [PATCH 10/14] Fix typos in mobile/main.js --- mobile/main.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mobile/main.js b/mobile/main.js index f80753fa0..4926c0a53 100644 --- a/mobile/main.js +++ b/mobile/main.js @@ -1,5 +1,5 @@ /** - * @fileOverview The main module that wires up all depdencies for mobile. + * @fileOverview the main module that wires up all depedencies for mobile. */ import React from 'react'; @@ -39,7 +39,6 @@ import InvoiceAction from '../src/action/invoice'; import PaymentAction from '../src/action/payment'; import ChannelAction from '../src/action/channel'; import TransactionAction from '../src/action/transaction'; - import AuthAction from '../src/action/auth-mobile'; const store = new Store(); From 90f52ce103da1016261b26375d659a211cee4824 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 9 Nov 2018 17:38:35 +0000 Subject: [PATCH 11/14] Remove npm audit from travis build --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dee00d488..c7e786584 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,6 @@ before_install: - ./assets/script/install_lnd.sh script: - - npm audit - npm test - npm run build-storybook From e6ecda768ee13eee2c8b76384f6384d826788de2 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 9 Nov 2018 19:09:29 +0000 Subject: [PATCH 12/14] Add copy to mobile wait screen --- src/view/wait-mobile.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/view/wait-mobile.js b/src/view/wait-mobile.js index 0b46e9ff0..f65f079a7 100644 --- a/src/view/wait-mobile.js +++ b/src/view/wait-mobile.js @@ -2,7 +2,8 @@ import React from 'react'; import { StyleSheet, ActivityIndicator } from 'react-native'; import Background from '../component/background'; import MainContent from '../component/main-content'; -import { color } from '../component/style'; +import Text from '../component/text'; +import { color, font } from '../component/style'; const styles = StyleSheet.create({ content: { @@ -11,6 +12,11 @@ const styles = StyleSheet.create({ spinner: { transform: [{ scale: 1.5 }], }, + copy: { + marginTop: 15, + fontSize: font.sizeXS, + color: color.white, + }, }); const WaitView = () => ( @@ -21,6 +27,7 @@ const WaitView = () => ( color={color.lightPurple} style={styles.spinner} /> + Loading network... ); From 1f861d6aa8ac1b8b5b15446fd82c1344fbb88489 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Mon, 12 Nov 2018 10:40:49 +0000 Subject: [PATCH 13/14] Fix typo in main.js --- mobile/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/main.js b/mobile/main.js index 4926c0a53..24c30dda7 100644 --- a/mobile/main.js +++ b/mobile/main.js @@ -1,5 +1,5 @@ /** - * @fileOverview the main module that wires up all depedencies for mobile. + * @fileOverview the main module that wires up all dependencies for mobile. */ import React from 'react'; From 65ed1878ec52b5d63ab7351515df38fc910fbfba Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Mon, 12 Nov 2018 10:43:39 +0000 Subject: [PATCH 14/14] Fix new pin error message --- src/action/auth-mobile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/auth-mobile.js b/src/action/auth-mobile.js index 05ab02274..140a098b2 100644 --- a/src/action/auth-mobile.js +++ b/src/action/auth-mobile.js @@ -90,7 +90,7 @@ class AuthAction { async checkNewPin() { const { newPin, pinVerify } = this._store.auth; if (newPin.length !== PIN_LENGTH || newPin !== pinVerify) { - this._alert('Incorrect PIN', () => this.initSetPin()); + this._alert("PINs don't match", () => this.initSetPin()); return; } await this._setToKeyStore(PIN, newPin);