From acba6a390d8b95361c2ae2a99c8aeb7668c5fb63 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 10 Sep 2017 23:46:04 -0500 Subject: [PATCH] Passcode lock --- src/lib/crypto.js | 6 + src/lib/storage.js | 11 ++ src/screens/Account.js | 71 ++++++++++++ src/screens/Authenticate.js | 215 ++++++++++++++++++++++++++++++++++++ src/screens/Notes.js | 37 +++++-- src/screens/index.js | 2 + 6 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 src/screens/Authenticate.js diff --git a/src/lib/crypto.js b/src/lib/crypto.js index 8c662566..591c24e5 100644 --- a/src/lib/crypto.js +++ b/src/lib/crypto.js @@ -18,6 +18,12 @@ export default class Crypto { }); } + static async pbkdf2(password, salt, cost, length) { + return Aes.pbkdf2(password, salt, cost, length).then(key => { + return key; + }); + } + static async generateRandomKey(length) { return Aes.randomKey(length); } diff --git a/src/lib/storage.js b/src/lib/storage.js index b349f724..71447c7b 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -11,6 +11,17 @@ export default class Storage { } } + static async getMultiItems(keys) { + return AsyncStorage.multiGet(keys, (err, stores) => { + stores.map((result, i, store) => { + let key = store[i][0]; + let value = store[i][1]; + items.push(value); + }); + return items; + }); + } + static async setItem(key, value) { if(value == null || value == undefined || key == null || key == undefined) { return; diff --git a/src/screens/Account.js b/src/screens/Account.js index 46883548..419e7ccd 100644 --- a/src/screens/Account.js +++ b/src/screens/Account.js @@ -1,12 +1,14 @@ import React, { Component } from 'react'; import Sync from '../lib/sync' import Auth from '../lib/auth' +import AlertManager from '../lib/alertManager' import ModelManager from '../lib/modelManager' import SectionHeader from "../components/SectionHeader"; import ButtonCell from "../components/ButtonCell"; import TableSection from "../components/TableSection"; import SectionedTableCell from "../components/SectionedTableCell"; import Abstract from "./Abstract" +import {Authenticate, AuthenticationState} from "./Authenticate" var _ = require('lodash') import GlobalStyles from "../Styles" @@ -27,7 +29,15 @@ export default class Account extends Abstract { this.state = {params: {email: "a@bitar.io", password: "password"}}; } + loadPasscodeStatus() { + var hasPasscode = AuthenticationState.get().hasPasscode() + this.setState(function(prevState){ + return _.merge(prevState, {hasPasscode: hasPasscode}); + }) + } + componentDidMount() { + this.loadPasscodeStatus(); Auth.getInstance().serverUrl().then(function(server){ this.setState(function(prevState) { var params = prevState.params; @@ -42,6 +52,7 @@ export default class Account extends Abstract { switch(event.id) { case 'willAppear': + this.loadPasscodeStatus(); this.forceUpdate(); break; } @@ -102,6 +113,31 @@ export default class Account extends Abstract { } + onPasscodeEnable = () => { + this.props.navigator.showModal({ + screen: 'sn.Authenticate', + title: 'Setup Passcode', + animationType: 'slide-up', + passProps: { + mode: "setup", + onSetupSuccess: () => {} + } + }); + } + + onPasscodeDisable = () => { + AlertManager.showConfirmationAlert( + "Disable Passcode", "Are you sure you want to disable your local passcode?", "Disable Passcode", + function(){ + AuthenticationState.get().clearPasscode(); + this.setState(function(prevState){ + return _.merge(prevState, {hasPasscode: false}) + }) + this.forceUpdate(); + }.bind(this) + ) + } + render() { let signedIn = !Auth.getInstance().offline(); return ( @@ -114,6 +150,12 @@ export default class Account extends Abstract { + + ); @@ -226,3 +268,32 @@ class OptionsSection extends Component { ); } } + +class PasscodeSection extends Component { + + constructor(props) { + super(props); + } + + render() { + return ( + + + + + {this.props.hasPasscode && + + + + } + + {!this.props.hasPasscode && + + + + } + + + ); + } +} diff --git a/src/screens/Authenticate.js b/src/screens/Authenticate.js new file mode 100644 index 00000000..5e971a8a --- /dev/null +++ b/src/screens/Authenticate.js @@ -0,0 +1,215 @@ +import React, { Component } from 'react'; +import Auth from '../lib/auth' +import Crypto from '../lib/crypto' +import SectionHeader from "../components/SectionHeader"; +import ButtonCell from "../components/ButtonCell"; +import TableSection from "../components/TableSection"; +import SectionedTableCell from "../components/SectionedTableCell"; +import Abstract from "./Abstract" +import Storage from '../lib/storage' +import GlobalStyles from "../Styles" +var _ = require('lodash') + +import { + TextInput, + SectionList, + ScrollView, + View, + Alert, + Keyboard +} from 'react-native'; + +let ParamsKey = "pc_params"; + +export class AuthenticationState { + static instance = null; + + static get() { + if (this.instance == null) { + this.instance = new AuthenticationState(); + } + + return this.instance; + } + + async load() { + this._hasPasscode = await Storage.getItem(ParamsKey); + } + + hasPasscode() { + return this._hasPasscode; + } + + isUnlocked() { + return this._isUnlocked; + } + + setIsUnlocked(isUnlocked) { + this._isUnlocked = isUnlocked; + } + + setHasPasscode(hasPasscode) { + this._hasPasscode = hasPasscode; + } + + clearPasscode() { + if(this.isUnlocked() === false) { + console.error("Can't remove passcode while locked."); + return; + } + Storage.removeItem(ParamsKey); + this.setHasPasscode(false); + } +} + +export default class Authenticate extends Abstract { + + constructor(props) { + super(props); + this.state = {passcode: null}; + } + + defaultPasswordParams() { + return { + cost: 100000, + length: 512 + }; + } + + onNavigatorEvent(event) { + super.onNavigatorEvent(event); + if (event.type == 'NavBarButtonPress') { // this is the event type for button presses + if (event.id == 'cancel') { // this is the same id field from the static navigatorButtons definition + this.dismiss(); + } + } + } + + dismiss() { + this.props.navigator.dismissModal({animationType: "slide-down"}) + } + + configureNavBar() { + if(this.props.mode === "setup") { + this.props.navigator.setButtons({ + leftButtons: [ + { + title: 'Cancel', + id: 'cancel', + showAsAction: 'ifRoom', + buttonColor: GlobalStyles.constants.mainTintColor, + buttonFontSize: 17 + } + ], + animated: false + }); + } + } + + async onSavePress() { + var passcode = this.state.passcode; + if(passcode.length == 0) { + Alert.alert('Invalid Passcode', "Please enter a valid passcode and try again.", [{text: 'OK'}]) + return; + } + var salt = await Crypto.generateRandomKey(256); + var params = _.merge(this.defaultPasswordParams(), {salt: salt}); + params.hash = await Crypto.pbkdf2(passcode, params.salt, params.cost, params.length); + + await Storage.setItem(ParamsKey, JSON.stringify(params)); + + console.log("Saving params", params); + + AuthenticationState.get().setHasPasscode(true); + AuthenticationState.get().setIsUnlocked(true); + + this.props.onSetupSuccess(); + + this.dismiss(); + } + + async onUnlockPress() { + var invalid = function() { + Alert.alert('Invalid Passcode', "Please enter a valid passcode and try again.", [{text: 'OK'}]) + } + + var passcode = this.state.passcode; + if(!passcode) { + invalid(); + return; + } + + Storage.getItem(ParamsKey).then(async function(object){ + if(!object) { + Alert.alert('Invalid Passcode State', "Unable to read passcode parameters from system. Please delete the app and re-install to sign in to your account.", [{text: 'OK'}]) + return; + } + var params = JSON.parse(object); + var savedHash = params.hash; + var computedHash = await Crypto.pbkdf2(passcode, params.salt, params.cost, params.length); + console.log("Saved hash:", savedHash, "computed", computedHash); + if(savedHash === computedHash) { + AuthenticationState.get().setIsUnlocked(true); + this.dismiss(); + this.props.onAuthenticateSuccess(); + } else { + invalid(); + } + }.bind(this)) + } + + render() { + return ( + + + {this.props.mode == "authenticate" && + + + + + this.setState({passcode: text})} + value={this.state.passcode} + autoCorrect={false} + autoCapitalize={'none'} + autoFocus={true} + secureTextEntry={true} + /> + + + + this.onUnlockPress()} /> + + + } + + {this.props.mode == "setup" && + + + + + this.setState({passcode: text})} + value={this.state.passcode} + autoCorrect={false} + autoCapitalize={'none'} + autoFocus={true} + /> + + + + this.onSavePress()} /> + + + } + + + + ); + } + +} diff --git a/src/screens/Notes.js b/src/screens/Notes.js index 8722e125..6437a5d7 100644 --- a/src/screens/Notes.js +++ b/src/screens/Notes.js @@ -9,6 +9,7 @@ import Keychain from "../lib/keychain" import {iconsMap, iconsLoaded} from '../Icons'; import NoteList from "../containers/NoteList" import Abstract from "./Abstract" +import {Authenticate, AuthenticationState} from "./Authenticate" export default class Notes extends Abstract { @@ -31,7 +32,7 @@ export default class Notes extends Abstract { // Refresh every 30s setInterval(function () { Sync.getInstance().sync(null); - }, 1000); + }, 30000); } registerObservers() { @@ -68,18 +69,40 @@ export default class Notes extends Abstract { Storage.getItem("options").then(function(result){ this.options = JSON.parse(result) || this.defaultOptions(); }.bind(this)), + AuthenticationState.get().load(), Auth.getInstance().loadKeys() ]).then(function(){ - console.log("===Keys and options loaded==="); // options and keys loaded - Sync.getInstance().loadLocalItems(function(items) { - this.loadNotes(); - }.bind(this)); - // perform initial sync - Sync.getInstance().sync(null); + console.log("===Keys and options loaded==="); + + var run = function() { + Sync.getInstance().loadLocalItems(function(items) { + this.loadNotes(); + }.bind(this)); + // perform initial sync + Sync.getInstance().sync(null); + }.bind(this) + + if(AuthenticationState.get().hasPasscode()) { + this.presentPasscodeAuther(run); + } else { + run(); + } }.bind(this)) } + presentPasscodeAuther(onAuthenticate) { + this.props.navigator.showModal({ + screen: 'sn.Authenticate', + title: 'Passcode Required', + animationType: 'slide-up', + passProps: { + mode: "authenticate", + onAuthenticateSuccess: onAuthenticate + } + }); + } + loadNotes = (reloadNavBar = true) => { if(!this.visible && !this.willBeVisible) { console.log("===Scheduling Load Notes==="); diff --git a/src/screens/index.js b/src/screens/index.js index fa4a7013..965deb30 100644 --- a/src/screens/index.js +++ b/src/screens/index.js @@ -3,6 +3,7 @@ import {Navigation, ScreenVisibilityListener} from 'react-native-navigation'; import Notes from './Notes' import Compose from './Compose' import Account from './Account' +import Authenticate from './Authenticate' import Filter from './Filter' import InputModal from './InputModal' import Sync from '../lib/sync' @@ -13,6 +14,7 @@ export function registerScreens() { Navigation.registerComponent('sn.Account', () => Account); Navigation.registerComponent('sn.Filter', () => Filter); Navigation.registerComponent('sn.InputModal', () => InputModal); + Navigation.registerComponent('sn.Authenticate', () => Authenticate); } export function registerScreenVisibilityListener() {