diff --git a/ElectronClient/app/gui/EncryptionConfigScreen.jsx b/ElectronClient/app/gui/EncryptionConfigScreen.jsx
index 08643cb6c..117599a0a 100644
--- a/ElectronClient/app/gui/EncryptionConfigScreen.jsx
+++ b/ElectronClient/app/gui/EncryptionConfigScreen.jsx
@@ -8,22 +8,13 @@ const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const { time } = require('lib/time-utils.js');
const dialogs = require('./dialogs');
+const shared = require('lib/components/shared/encryption-config-shared.js');
class EncryptionConfigScreenComponent extends React.Component {
constructor() {
super();
- this.state = {
- masterKeys: [],
- passwords: {},
- passwordChecks: {},
- stats: {
- encrypted: null,
- total: null,
- },
- };
- this.isMounted_ = false;
- this.refreshStatsIID_ = null;
+ shared.constructor(this);
}
componentDidMount() {
@@ -35,28 +26,11 @@ class EncryptionConfigScreenComponent extends React.Component {
}
initState(props) {
- this.setState({
- masterKeys: props.masterKeys,
- passwords: props.passwords ? props.passwords : {},
- }, () => {
- this.checkPasswords();
- });
+ return shared.initState(this, props);
+ }
- this.refreshStats();
-
- if (this.refreshStatsIID_) {
- clearInterval(this.refreshStatsIID_);
- this.refreshStatsIID_ = null;
- }
-
- this.refreshStatsIID_ = setInterval(() => {
- if (!this.isMounted_) {
- clearInterval(this.refreshStatsIID_);
- this.refreshStatsIID_ = null;
- return;
- }
- this.refreshStats();
- }, 3000);
+ async refreshStats() {
+ return shared.refreshStats(this);
}
componentWillMount() {
@@ -67,40 +41,19 @@ class EncryptionConfigScreenComponent extends React.Component {
this.initState(nextProps);
}
- async refreshStats() {
- const stats = await BaseItem.encryptedItemsStats();
- this.setState({ stats: stats });
- }
-
async checkPasswords() {
- const passwordChecks = Object.assign({}, this.state.passwordChecks);
- for (let i = 0; i < this.state.masterKeys.length; i++) {
- const mk = this.state.masterKeys[i];
- const password = this.state.passwords[mk.id];
- const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
- passwordChecks[mk.id] = ok;
- }
- this.setState({ passwordChecks: passwordChecks });
+ return shared.checkPasswords(this);
}
renderMasterKey(mk) {
const theme = themeStyle(this.props.theme);
const onSaveClick = () => {
- const password = this.state.passwords[mk.id];
- if (!password) {
- Setting.deleteObjectKey('encryption.passwordCache', mk.id);
- } else {
- Setting.setObjectKey('encryption.passwordCache', mk.id, password);
- }
-
- this.checkPasswords();
+ return shared.onSavePasswordClick(this, mk);
}
const onPasswordChange = (event) => {
- const passwords = this.state.passwords;
- passwords[mk.id] = event.target.value;
- this.setState({ passwords: passwords });
+ return shared.onPasswordChange(this, mk, event.target.value);
}
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
@@ -166,8 +119,7 @@ class EncryptionConfigScreenComponent extends React.Component {
}
}
- const stats = this.state.stats;
- const decryptedItemsInfo = this.props.encryptionEnabled ?
{_('Decrypted items: %s / %s', stats.encrypted !== null ? (stats.total - stats.encrypted) : '-', stats.total !== null ? stats.total : '-')}
: null;
+ const decryptedItemsInfo = this.props.encryptionEnabled ? {shared.decryptedStatText(this)}
: null;
const toggleButton =
let masterKeySection = null;
diff --git a/ReactNativeClient/lib/components/global-style.js b/ReactNativeClient/lib/components/global-style.js
index c98bf8677..874cb78bc 100644
--- a/ReactNativeClient/lib/components/global-style.js
+++ b/ReactNativeClient/lib/components/global-style.js
@@ -19,6 +19,8 @@ const globalStyle = {
raisedColor: "#003363",
raisedHighlightedColor: "#ffffff",
+ warningBackgroundColor: "#FFD08D",
+
// For WebView - must correspond to the properties above
htmlFontSize: '16px',
htmlColor: 'black', // Note: CSS in WebView component only supports named colors or rgb() notation
diff --git a/ReactNativeClient/lib/components/screen-header.js b/ReactNativeClient/lib/components/screen-header.js
index 66f4f1acd..2a4e119b5 100644
--- a/ReactNativeClient/lib/components/screen-header.js
+++ b/ReactNativeClient/lib/components/screen-header.js
@@ -43,7 +43,7 @@ class ScreenHeaderComponent extends Component {
let styleObject = {
container: {
- flexDirection: 'row',
+ flexDirection: 'column',
backgroundColor: theme.raisedBackgroundColor,
alignItems: 'center',
shadowColor: '#000000',
@@ -123,11 +123,17 @@ class ScreenHeaderComponent extends Component {
},
titleText: {
flex: 1,
+ textAlignVertical: 'center',
marginLeft: 0,
color: theme.raisedHighlightedColor,
fontWeight: 'bold',
fontSize: theme.fontSize,
- }
+ },
+ warningBox: {
+ backgroundColor: "#ff9900",
+ flexDirection: 'row',
+ padding: theme.marginLeft,
+ },
};
styleObject.topIcon = Object.assign({}, theme.icon);
@@ -198,6 +204,20 @@ class ScreenHeaderComponent extends Component {
});
}
+ encryptionConfig_press() {
+ this.props.dispatch({
+ type: 'NAV_GO',
+ routeName: 'EncryptionConfig',
+ });
+ }
+
+ warningBox_press() {
+ this.props.dispatch({
+ type: 'NAV_GO',
+ routeName: 'EncryptionConfig',
+ });
+ }
+
async debugReport_press() {
const service = new ReportService();
@@ -324,6 +344,11 @@ class ScreenHeaderComponent extends Component {
menuOptionComponents.push();
}
+ menuOptionComponents.push(
+ this.encryptionConfig_press()} key={'menuOption_encryptionConfig'} style={this.styles().contextMenuItem}>
+ {_('Encryption Configuration')}
+ );
+
menuOptionComponents.push(
this.config_press()} key={'menuOption_config'} style={this.styles().contextMenuItem}>
{_('Configuration')}
@@ -405,6 +430,12 @@ class ScreenHeaderComponent extends Component {
}
}
+ const warningComp = this.props.showMissingMasterKeyMessage ? (
+ this.warningBox_press()} activeOpacity={0.8}>
+ {_('Press to set the decryption password.')}
+
+ ) : null;
+
const titleComp = createTitleComponent();
const sideMenuComp = this.props.noteSelectionEnabled ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press());
const backButtonComp = backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack);
@@ -427,13 +458,16 @@ class ScreenHeaderComponent extends Component {
return (
- { sideMenuComp }
- { backButtonComp }
- { saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
- { titleComp }
- { searchButtonComp }
- { deleteButtonComp }
- { menuComp }
+
+ { sideMenuComp }
+ { backButtonComp }
+ { saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
+ { titleComp }
+ { searchButtonComp }
+ { deleteButtonComp }
+ { menuComp }
+
+ { warningComp }
{ this.dialogbox = dialogbox }}/>
);
@@ -455,6 +489,7 @@ const ScreenHeader = connect(
showAdvancedOptions: state.settings.showAdvancedOptions,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
+ showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
};
}
)(ScreenHeaderComponent)
diff --git a/ReactNativeClient/lib/components/screens/encryption-config.js b/ReactNativeClient/lib/components/screens/encryption-config.js
new file mode 100644
index 000000000..e947b1a41
--- /dev/null
+++ b/ReactNativeClient/lib/components/screens/encryption-config.js
@@ -0,0 +1,151 @@
+const React = require('react'); const Component = React.Component;
+const { TextInput, TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView } = require('react-native');
+const { connect } = require('react-redux');
+const { ScreenHeader } = require('lib/components/screen-header.js');
+const { _ } = require('lib/locale.js');
+const { BaseScreenComponent } = require('lib/components/base-screen.js');
+const { Dropdown } = require('lib/components/Dropdown.js');
+const { themeStyle } = require('lib/components/global-style.js');
+const { time } = require('lib/time-utils.js');
+const Setting = require('lib/models/Setting.js');
+const shared = require('lib/components/shared/encryption-config-shared.js');
+
+class EncryptionConfigScreenComponent extends BaseScreenComponent {
+
+ static navigationOptions(options) {
+ return { header: null };
+ }
+
+ constructor() {
+ super();
+
+ shared.constructor(this);
+
+ this.styles_ = {};
+ }
+
+ componentDidMount() {
+ this.isMounted_ = true;
+ }
+
+ componentWillUnmount() {
+ this.isMounted_ = false;
+ }
+
+ initState(props) {
+ return shared.initState(this, props);
+ }
+
+ async refreshStats() {
+ return shared.refreshStats(this);
+ }
+
+ componentWillMount() {
+ this.initState(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.initState(nextProps);
+ }
+
+ async checkPasswords() {
+ return shared.checkPasswords(this);
+ }
+
+ styles() {
+ const themeId = this.props.theme;
+ const theme = themeStyle(themeId);
+
+ if (this.styles_[themeId]) return this.styles_[themeId];
+ this.styles_ = {};
+
+ let styles = {
+ titleText: {
+ flex: 1,
+ fontWeight: 'bold',
+ flexDirection: 'column',
+ fontSize: theme.fontSize,
+ paddingTop: 5,
+ paddingBottom: 5,
+ },
+ normalText: {
+ flex: 1,
+ fontSize: theme.fontSize,
+ },
+ container: {
+ flex: 1,
+ padding: theme.margin,
+ },
+ }
+
+ this.styles_[themeId] = StyleSheet.create(styles);
+ return this.styles_[themeId];
+ }
+
+ renderMasterKey(num, mk) {
+ const theme = themeStyle(this.props.theme);
+
+ const onSaveClick = () => {
+ return shared.onSavePasswordClick(this, mk);
+ }
+
+ const onPasswordChange = (text) => {
+ return shared.onPasswordChange(this, mk, text);
+ }
+
+ const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
+ const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
+ const active = this.props.activeMasterKeyId === mk.id ? '✔' : '';
+
+ return (
+
+ {_('Master Key %d', num)}
+ {_('Created: %s', time.formatMsToLocal(mk.created_time))}
+
+ {_('Password:')}
+ onPasswordChange(text)} style={{flex:1, marginRight: 10}}>
+ {passwordOk}
+
+
+
+ );
+ }
+
+ render() {
+ const masterKeys = this.state.masterKeys;
+ const decryptedItemsInfo = this.props.encryptionEnabled ? {shared.decryptedStatText(this)} : null;
+
+ const mkComps = [];
+ for (let i = 0; i < masterKeys.length; i++) {
+ const mk = masterKeys[i];
+ mkComps.push(this.renderMasterKey(i+1, mk));
+ }
+
+ return (
+
+
+
+ {_('Status')}
+ {_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}
+ {decryptedItemsInfo}
+ {mkComps}
+
+
+ );
+ }
+
+}
+
+const EncryptionConfigScreen = connect(
+ (state) => {
+ return {
+ theme: state.settings.theme,
+ masterKeys: state.masterKeys,
+ passwords: state.settings['encryption.passwordCache'],
+ encryptionEnabled: state.settings['encryption.enabled'],
+ activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
+ };
+ }
+)(EncryptionConfigScreenComponent)
+
+module.exports = { EncryptionConfigScreen };
\ No newline at end of file
diff --git a/ReactNativeClient/lib/components/shared/encryption-config-shared.js b/ReactNativeClient/lib/components/shared/encryption-config-shared.js
new file mode 100644
index 000000000..3f9787798
--- /dev/null
+++ b/ReactNativeClient/lib/components/shared/encryption-config-shared.js
@@ -0,0 +1,87 @@
+const EncryptionService = require('lib/services/EncryptionService');
+const { _ } = require('lib/locale.js');
+const BaseItem = require('lib/models/BaseItem.js');
+const Setting = require('lib/models/Setting.js');
+
+const shared = {};
+
+shared.constructor = function(comp) {
+ comp.state = {
+ masterKeys: [],
+ passwords: {},
+ passwordChecks: {},
+ stats: {
+ encrypted: null,
+ total: null,
+ },
+ };
+ comp.isMounted_ = false;
+
+ comp.refreshStatsIID_ = null;
+}
+
+shared.initState = function(comp, props) {
+ comp.setState({
+ masterKeys: props.masterKeys,
+ passwords: props.passwords ? props.passwords : {},
+ }, () => {
+ comp.checkPasswords();
+ });
+
+ comp.refreshStats();
+
+ if (comp.refreshStatsIID_) {
+ clearInterval(comp.refreshStatsIID_);
+ comp.refreshStatsIID_ = null;
+ }
+
+ comp.refreshStatsIID_ = setInterval(() => {
+ if (!comp.isMounted_) {
+ clearInterval(comp.refreshStatsIID_);
+ comp.refreshStatsIID_ = null;
+ return;
+ }
+ comp.refreshStats();
+ }, 3000);
+}
+
+shared.refreshStats = async function(comp) {
+ const stats = await BaseItem.encryptedItemsStats();
+ comp.setState({ stats: stats });
+}
+
+shared.checkPasswords = async function(comp) {
+ const passwordChecks = Object.assign({}, comp.state.passwordChecks);
+ for (let i = 0; i < comp.state.masterKeys.length; i++) {
+ const mk = comp.state.masterKeys[i];
+ const password = comp.state.passwords[mk.id];
+ const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
+ passwordChecks[mk.id] = ok;
+ }
+ console.info(passwordChecks);
+ comp.setState({ passwordChecks: passwordChecks });
+}
+
+shared.decryptedStatText = function(comp) {
+ const stats = comp.state.stats;
+ return _('Decrypted items: %s / %s', stats.encrypted !== null ? (stats.total - stats.encrypted) : '-', stats.total !== null ? stats.total : '-');
+}
+
+shared.onSavePasswordClick = function(comp, mk) {
+ const password = comp.state.passwords[mk.id];
+ if (!password) {
+ Setting.deleteObjectKey('encryption.passwordCache', mk.id);
+ } else {
+ Setting.setObjectKey('encryption.passwordCache', mk.id, password);
+ }
+
+ comp.checkPasswords();
+}
+
+shared.onPasswordChange = function(comp, mk, password) {
+ const passwords = comp.state.passwords;
+ passwords[mk.id] = password;
+ comp.setState({ passwords: passwords });
+}
+
+module.exports = shared;
\ No newline at end of file
diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js
index 5bcdff894..4645690c5 100644
--- a/ReactNativeClient/root.js
+++ b/ReactNativeClient/root.js
@@ -33,6 +33,7 @@ const { StatusScreen } = require('lib/components/screens/status.js');
const { WelcomeScreen } = require('lib/components/screens/welcome.js');
const { SearchScreen } = require('lib/components/screens/search.js');
const { OneDriveLoginScreen } = require('lib/components/screens/onedrive-login.js');
+const { EncryptionConfigScreen } = require('lib/components/screens/encryption-config.js');
const Setting = require('lib/models/Setting.js');
const { MenuContext } = require('react-native-popup-menu');
const { SideMenu } = require('lib/components/side-menu.js');
@@ -51,6 +52,10 @@ const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
+const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
+const DecryptionWorker = require('lib/services/DecryptionWorker');
+const EncryptionService = require('lib/services/EncryptionService');
+
const generalMiddleware = store => next => async (action) => {
if (action.type !== 'SIDE_MENU_OPEN_PERCENT') reg.logger().info('Reducer action', action.type);
PoorManIntervals.update(); // This function needs to be called regularly so put it here
@@ -85,6 +90,10 @@ const generalMiddleware = store => next => async (action) => {
Setting.setValue('activeFolderId', newState.selectedFolderId);
}
+ if (action.type === 'SYNC_GOT_ENCRYPTED_ITEM') {
+ DecryptionWorker.instance().scheduleStart();
+ }
+
return result;
}
@@ -307,6 +316,10 @@ async function initialize(dispatch) {
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
+ const fsDriver = new FsDriverRN();
+
+ Resource.fsDriver_ = fsDriver;
+
AlarmService.setDriver(new AlarmServiceDriver());
AlarmService.setLogger(mainLogger);
@@ -343,6 +356,21 @@ async function initialize(dispatch) {
setLocale(Setting.value('locale'));
+ // ----------------------------------------------------------------
+ // E2EE SETUP
+ // ----------------------------------------------------------------
+
+ EncryptionService.fsDriver_ = fsDriver;
+ EncryptionService.instance().setLogger(mainLogger);
+ BaseItem.encryptionService_ = EncryptionService.instance();
+ DecryptionWorker.instance().setLogger(mainLogger);
+ DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
+ await EncryptionService.instance().loadMasterKeysFromSettings();
+
+ // ----------------------------------------------------------------
+ // / E2EE SETUP
+ // ----------------------------------------------------------------
+
reg.logger().info('Loading folders...');
await FoldersScreenUtils.refreshFolders();
@@ -354,6 +382,13 @@ async function initialize(dispatch) {
items: tags,
});
+ const masterKeys = await MasterKey.all();
+
+ dispatch({
+ type: 'MASTERKEY_UPDATE_ALL',
+ items: masterKeys,
+ });
+
let folderId = Setting.value('activeFolderId');
let folder = await Folder.load(folderId);
@@ -385,6 +420,8 @@ async function initialize(dispatch) {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
AlarmService.updateAllNotifications();
+
+ DecryptionWorker.instance().scheduleStart();
});
reg.logger().info('Application initialized');
@@ -472,6 +509,7 @@ class AppComponent extends React.Component {
Note: { screen: NoteScreen },
Folder: { screen: FolderScreen },
OneDriveLogin: { screen: OneDriveLoginScreen },
+ EncryptionConfig: { screen: EncryptionConfigScreen },
Log: { screen: LogScreen },
Status: { screen: StatusScreen },
Search: { screen: SearchScreen },