Skip to content
This repository was archived by the owner on Feb 23, 2021. It is now read-only.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ before_install:
- ./assets/script/install_lnd.sh

script:
- npm audit
- npm test
- npm run build-storybook

Expand Down
41 changes: 27 additions & 14 deletions mobile/main.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
/**
* @fileOverview the main module that wires up all dependencies for mobile.
*/

import React from 'react';
import { Clipboard } from 'react-native';
import { createStackNavigator } from 'react-navigation';
import { Clipboard, Alert } from 'react-native';
import { SecureStore, LocalAuthentication } from 'expo';
import { createStackNavigator, NavigationActions } 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';
Expand All @@ -34,10 +39,11 @@ 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();
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);
Expand All @@ -61,12 +67,19 @@ sinon.stub(channel, 'update');
sinon.stub(channel, 'connectAndOpen');
sinon.stub(channel, 'closeSelectedChannel');

const SetPassword = () => (
<SetPasswordView store={store} wallet={wallet} nav={nav} />
const auth = new AuthAction(
store,
wallet,
nav,
SecureStore,
LocalAuthentication,
Alert
);

const SetPasswordConfirm = () => (
<SetPasswordConfirmView store={store} wallet={wallet} nav={nav} />
const SetPin = () => <SetPinView store={store} auth={auth} nav={nav} />;

const SetPinConfirm = () => (
<SetPinConfirmView store={store} auth={auth} nav={nav} />
);

const SeedSuccess = () => <SeedSuccessView wallet={wallet} />;
Expand All @@ -79,7 +92,7 @@ const NewAddress = () => (
/>
);

const Password = () => <PasswordView store={store} wallet={wallet} />;
const Pin = () => <PinView store={store} auth={auth} />;

const Wait = () => <WaitView />;

Expand Down Expand Up @@ -119,11 +132,11 @@ const Pay = () => <PaymentView store={store} payment={payment} nav={nav} />;

const MainStack = createStackNavigator(
{
SetPassword,
SetPasswordConfirm,
SetPin,
SetPinConfirm,
SeedSuccess,
NewAddress,
Password,
Pin,
Wait,
Home,
Settings,
Expand Down
1 change: 1 addition & 0 deletions mobile/rn-cli.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
202 changes: 202 additions & 0 deletions src/action/auth-mobile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/**
* @fileOverview action to handle mobile specific authentication
* using PINs, TouchID, and KeyStore storage.
*/

import { PIN_LENGTH } from '../config';

const PIN = 'DevicePin';
const PASS = 'WalletPassword';

class AuthAction {
constructor(store, wallet, nav, SecureStore, Fingerprint, Alert) {
this._store = store;
this._wallet = wallet;
this._nav = nav;
this._SecureStore = SecureStore;
this._Fingerprint = Fingerprint;
this._Alert = Alert;
}

//
// 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 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<undefined>}
*/
async checkNewPin() {
const { newPin, pinVerify } = this._store.auth;
if (newPin.length !== PIN_LENGTH || newPin !== pinVerify) {
this._alert("PINs don't match", () => this.initSetPin());
return;
}
await this._setToKeyStore(PIN, newPin);
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<undefined>}
*/
async checkPin() {
const { pin } = this._store.auth;
const storedPin = await this._getFromKeyStore(PIN);
if (pin !== storedPin) {
this._alert('Incorrect PIN', () => this.initPin());
return;
}
await this._unlockWallet();
}

//
// TouchID & KeyStore Authentication
//

/**
* Try authenticating the user using either via TouchID/FaceID on iOS
* or a fingerprint reader on Android.
* @return {Promise<undefined>}
*/
async tryFingerprint() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a plan to add copy so the user knows that they can use their fingerprint/faceID?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the device supports it the native OS UI will already display copy...

img_5921

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, not on Android. I just tried it and it worked

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok cool. I try to write the code so that it at least runs on Android, but I think we can ignore Android specific issues and focus on iOS for now.

const hasHardware = await this._Fingerprint.hasHardwareAsync();
if (!hasHardware) {
return;
}
const msg = 'Unlock your Wallet';
const { success } = await this._Fingerprint.authenticateAsync(msg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can confirm that fingerprint auth works on Android!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Did you get that iOS device yet?

if (!success) {
return;
}
await this._unlockWallet();
}

/**
* 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<undefined>}
*/
async _generateWalletPassword() {
const newPass = this._totallyNotSecureRandomPassword();
await this._setToKeyStore(PASS, newPass);
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<undefined>}
*/
async _unlockWallet() {
const storedPass = await this._getFromKeyStore(PASS);
this._store.wallet.password = storedPass;
await this._wallet.checkPassword();
}

_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 !!!
*
* Just a stop gap during development until we have a secure native
* PRNG: https://github.com/lightninglabs/lightning-app/issues/777
*
* 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(32);
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;
23 changes: 11 additions & 12 deletions src/action/nav-mobile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -36,24 +35,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() {
Expand Down
Loading