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