Passcode lock

This commit is contained in:
Mo Bitar
2017-09-10 23:46:04 -05:00
parent cdb8ee696e
commit acba6a390d
6 changed files with 335 additions and 7 deletions

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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 {
<OptionsSection signedIn={signedIn} title={"Options"} onSignOutPress={this.onSignOutPress} onExportPress={this.onExportPress} />
<PasscodeSection
hasPasscode={this.state.hasPasscode}
onEnable={this.onPasscodeEnable}
onDisable={this.onPasscodeDisable}
title={"Local Passcode"} />
</ScrollView>
</View>
);
@@ -226,3 +268,32 @@ class OptionsSection extends Component {
);
}
}
class PasscodeSection extends Component {
constructor(props) {
super(props);
}
render() {
return (
<TableSection>
<SectionHeader title={this.props.title} />
{this.props.hasPasscode &&
<SectionedTableCell buttonCell={true} first={true}>
<ButtonCell leftAligned={true} title="Disable Passcode Lock" onPress={this.props.onDisable} />
</SectionedTableCell>
}
{!this.props.hasPasscode &&
<SectionedTableCell buttonCell={true} first={true}>
<ButtonCell leftAligned={true} title="Enable Passcode Lock" onPress={this.props.onEnable} />
</SectionedTableCell>
}
</TableSection>
);
}
}

215
src/screens/Authenticate.js Normal file
View File

@@ -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 (
<View style={GlobalStyles.rules.container}>
<TableSection>
{this.props.mode == "authenticate" &&
<View>
<SectionHeader title={"Enter local passcode"} />
<SectionedTableCell textInputCell={true} first={true}>
<TextInput
style={GlobalStyles.rules.sectionedTableCellTextInput}
placeholder={"Local passcode"}
onChangeText={(text) => this.setState({passcode: text})}
value={this.state.passcode}
autoCorrect={false}
autoCapitalize={'none'}
autoFocus={true}
secureTextEntry={true}
/>
</SectionedTableCell>
<SectionedTableCell buttonCell={true}>
<ButtonCell title={"Unlock"} bold={true} onPress={() => this.onUnlockPress()} />
</SectionedTableCell>
</View>
}
{this.props.mode == "setup" &&
<View>
<SectionHeader title={"Choose passcode"} />
<SectionedTableCell textInputCell={true} first={true}>
<TextInput
style={GlobalStyles.rules.sectionedTableCellTextInput}
placeholder={"Local passcode"}
onChangeText={(text) => this.setState({passcode: text})}
value={this.state.passcode}
autoCorrect={false}
autoCapitalize={'none'}
autoFocus={true}
/>
</SectionedTableCell>
<SectionedTableCell buttonCell={true}>
<ButtonCell title={"Save"} bold={true} onPress={() => this.onSavePress()} />
</SectionedTableCell>
</View>
}
</TableSection>
</View>
);
}
}

View File

@@ -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===");

View File

@@ -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() {