1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-02 22:49:09 +02:00

All: Got E2EE working in mobile app

This commit is contained in:
Laurent Cozic
2017-12-30 20:57:34 +01:00
parent d180e7b5e1
commit 6ff19063ef
6 changed files with 332 additions and 67 deletions

View File

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

View File

@@ -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(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
}
menuOptionComponents.push(
<MenuOption value={() => this.encryptionConfig_press()} key={'menuOption_encryptionConfig'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Encryption Configuration')}</Text>
</MenuOption>);
menuOptionComponents.push(
<MenuOption value={() => this.config_press()} key={'menuOption_config'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Configuration')}</Text>
@@ -405,6 +430,12 @@ class ScreenHeaderComponent extends Component {
}
}
const warningComp = this.props.showMissingMasterKeyMessage ? (
<TouchableOpacity style={this.styles().warningBox} onPress={() => this.warningBox_press()} activeOpacity={0.8}>
<Text style={{flex:1}}>{_('Press to set the decryption password.')}</Text>
</TouchableOpacity>
) : 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 (
<View style={this.styles().container} >
{ sideMenuComp }
{ backButtonComp }
{ saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
{ titleComp }
{ searchButtonComp }
{ deleteButtonComp }
{ menuComp }
<View style={{flexDirection:'row'}}>
{ sideMenuComp }
{ backButtonComp }
{ saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) }
{ titleComp }
{ searchButtonComp }
{ deleteButtonComp }
{ menuComp }
</View>
{ warningComp }
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</View>
);
@@ -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)

View File

@@ -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 (
<View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %d', num)}</Text>
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{flexDirection: 'row', alignItems: 'center'}}>
<Text style={{flex:0, fontSize: theme.fontSize, marginRight: 10}}>{_('Password:')}</Text>
<TextInput value={password} onChangeText={(text) => onPasswordChange(text)} style={{flex:1, marginRight: 10}}></TextInput>
<Text style={{fontSize: theme.fontSize, marginRight: 10}}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
</View>
</View>
);
}
render() {
const masterKeys = this.state.masterKeys;
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
const mkComps = [];
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i+1, mk));
}
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader title={_('Configuration')}/>
<ScrollView style={this.styles().container}>
<Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{mkComps}
</ScrollView>
</View>
);
}
}
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 };

View File

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