mirror of
https://github.com/standardnotes/mobile.git
synced 2026-04-25 08:36:58 -04:00
Passcode lock
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
215
src/screens/Authenticate.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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===");
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user