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 },