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
diff --git a/mobile/main.js b/mobile/main.js
index 9ecb7d5be..24c30dda7 100644
--- a/mobile/main.js
+++ b/mobile/main.js
@@ -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';
@@ -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);
@@ -61,12 +67,19 @@ sinon.stub(channel, 'update');
sinon.stub(channel, 'connectAndOpen');
sinon.stub(channel, 'closeSelectedChannel');
-const SetPassword = () => (
-
+const auth = new AuthAction(
+ store,
+ wallet,
+ nav,
+ SecureStore,
+ LocalAuthentication,
+ Alert
);
-const SetPasswordConfirm = () => (
-
+const SetPin = () => ;
+
+const SetPinConfirm = () => (
+
);
const SeedSuccess = () => ;
@@ -79,7 +92,7 @@ const NewAddress = () => (
/>
);
-const Password = () => ;
+const Pin = () => ;
const Wait = () => ;
@@ -119,11 +132,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/auth-mobile.js b/src/action/auth-mobile.js
new file mode 100644
index 000000000..140a098b2
--- /dev/null
+++ b/src/action/auth-mobile.js
@@ -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}
+ */
+ 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}
+ */
+ 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}
+ */
+ async tryFingerprint() {
+ const hasHardware = await this._Fingerprint.hasHardwareAsync();
+ if (!hasHardware) {
+ return;
+ }
+ const msg = 'Unlock your Wallet';
+ const { success } = await this._Fingerprint.authenticateAsync(msg);
+ 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}
+ */
+ 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}
+ */
+ 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;
diff --git a/src/action/nav-mobile.js b/src/action/nav-mobile.js
index 90d2f399c..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() {
@@ -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() {
diff --git a/src/action/wallet.js b/src/action/wallet.js
index 6903eaf08..9b2729f81 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.
@@ -240,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/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/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..f56c572e7
--- /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.tryFingerprint();
+ }
+
+ 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);
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);
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);
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...
);
diff --git a/stories/screen-story.js b/stories/screen-story.js
index 99193e3b3..d6962de54 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,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, wallet, nav);
+sinon.stub(auth, 'checkNewPin');
+sinon.stub(auth, 'checkPin');
+sinon.stub(auth, 'tryFingerprint');
+sinon.stub(auth, '_unlockWallet');
+sinon.stub(auth, '_generateWalletPassword');
storiesOf('Screens', module)
.add('Welcome', () => )
@@ -109,16 +116,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', () => (
))
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);
+ });
+ });
+});
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()', () => {