1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

All: Add support for public-private key pairs and improved master password support (#5438)

Also improved SCSS support, which was needed for the master password dialog.
This commit is contained in:
Laurent 2021-10-03 16:00:49 +01:00 committed by GitHub
parent e5e1382255
commit c758377188
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 4495 additions and 5296 deletions

View File

@ -336,6 +336,9 @@ packages/app-desktop/gui/MainScreen/commands/toggleSideBar.js.map
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js.map
packages/app-desktop/gui/MasterPasswordDialog/Dialog.d.ts
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js.map
packages/app-desktop/gui/MenuBar.d.ts
packages/app-desktop/gui/MenuBar.js
packages/app-desktop/gui/MenuBar.js.map
@ -687,6 +690,9 @@ packages/app-desktop/services/commands/stateToWhenClauseContext.js.map
packages/app-desktop/services/commands/types.d.ts
packages/app-desktop/services/commands/types.js
packages/app-desktop/services/commands/types.js.map
packages/app-desktop/services/e2ee.d.ts
packages/app-desktop/services/e2ee.js
packages/app-desktop/services/e2ee.js.map
packages/app-desktop/services/plugins/PlatformImplementation.d.ts
packages/app-desktop/services/plugins/PlatformImplementation.js
packages/app-desktop/services/plugins/PlatformImplementation.js.map
@ -780,6 +786,9 @@ packages/app-mobile/services/AlarmServiceDriver.android.js.map
packages/app-mobile/services/AlarmServiceDriver.ios.d.ts
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.ios.js.map
packages/app-mobile/services/e2ee/RSA.react-native.d.ts
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/RSA.react-native.js.map
packages/app-mobile/setupQuickActions.d.ts
packages/app-mobile/setupQuickActions.js
packages/app-mobile/setupQuickActions.js.map
@ -939,6 +948,9 @@ packages/lib/commands/historyForward.js.map
packages/lib/commands/index.d.ts
packages/lib/commands/index.js
packages/lib/commands/index.js.map
packages/lib/commands/openMasterPasswordDialog.d.ts
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/openMasterPasswordDialog.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
@ -1245,6 +1257,18 @@ packages/lib/services/e2ee/EncryptionService.js.map
packages/lib/services/e2ee/EncryptionService.test.d.ts
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.test.js.map
packages/lib/services/e2ee/RSA.node.d.ts
packages/lib/services/e2ee/RSA.node.js
packages/lib/services/e2ee/RSA.node.js.map
packages/lib/services/e2ee/ppk.d.ts
packages/lib/services/e2ee/ppk.js
packages/lib/services/e2ee/ppk.js.map
packages/lib/services/e2ee/ppk.test.d.ts
packages/lib/services/e2ee/ppk.test.js
packages/lib/services/e2ee/ppk.test.js.map
packages/lib/services/e2ee/ppkTestUtils.d.ts
packages/lib/services/e2ee/ppkTestUtils.js
packages/lib/services/e2ee/ppkTestUtils.js.map
packages/lib/services/e2ee/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
@ -1581,6 +1605,9 @@ packages/lib/services/synchronizer/Synchronizer.conflicts.test.js.map
packages/lib/services/synchronizer/Synchronizer.e2ee.test.d.ts
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js.map
packages/lib/services/synchronizer/Synchronizer.ppk.test.d.ts
packages/lib/services/synchronizer/Synchronizer.ppk.test.js
packages/lib/services/synchronizer/Synchronizer.ppk.test.js.map
packages/lib/services/synchronizer/Synchronizer.resources.test.d.ts
packages/lib/services/synchronizer/Synchronizer.resources.test.js
packages/lib/services/synchronizer/Synchronizer.resources.test.js.map

View File

@ -182,6 +182,14 @@ module.exports = {
// leadingUnderscore: 'allow',
// trailingUnderscore: 'allow',
// },
// Each rule below is made of two blocks: first the rule we
// actually want, and below exceptions to the rule.
// -----------------------------------
// ENUM
// -----------------------------------
{
selector: 'enumMember',
format: ['StrictPascalCase'],
@ -190,14 +198,27 @@ module.exports = {
selector: 'enumMember',
format: null,
'filter': {
'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD)$',
'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD|SJCL.*)$',
'match': true,
},
},
// -----------------------------------
// INTERFACE
// -----------------------------------
{
selector: 'interface',
format: ['StrictPascalCase'],
},
{
selector: 'interface',
format: null,
'filter': {
'regex': '^(RSA|RSAKeyPair)$',
'match': true,
},
},
],
},
},

27
.gitignore vendored
View File

@ -319,6 +319,9 @@ packages/app-desktop/gui/MainScreen/commands/toggleSideBar.js.map
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.d.ts
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js.map
packages/app-desktop/gui/MasterPasswordDialog/Dialog.d.ts
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js.map
packages/app-desktop/gui/MenuBar.d.ts
packages/app-desktop/gui/MenuBar.js
packages/app-desktop/gui/MenuBar.js.map
@ -670,6 +673,9 @@ packages/app-desktop/services/commands/stateToWhenClauseContext.js.map
packages/app-desktop/services/commands/types.d.ts
packages/app-desktop/services/commands/types.js
packages/app-desktop/services/commands/types.js.map
packages/app-desktop/services/e2ee.d.ts
packages/app-desktop/services/e2ee.js
packages/app-desktop/services/e2ee.js.map
packages/app-desktop/services/plugins/PlatformImplementation.d.ts
packages/app-desktop/services/plugins/PlatformImplementation.js
packages/app-desktop/services/plugins/PlatformImplementation.js.map
@ -763,6 +769,9 @@ packages/app-mobile/services/AlarmServiceDriver.android.js.map
packages/app-mobile/services/AlarmServiceDriver.ios.d.ts
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.ios.js.map
packages/app-mobile/services/e2ee/RSA.react-native.d.ts
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/RSA.react-native.js.map
packages/app-mobile/setupQuickActions.d.ts
packages/app-mobile/setupQuickActions.js
packages/app-mobile/setupQuickActions.js.map
@ -922,6 +931,9 @@ packages/lib/commands/historyForward.js.map
packages/lib/commands/index.d.ts
packages/lib/commands/index.js
packages/lib/commands/index.js.map
packages/lib/commands/openMasterPasswordDialog.d.ts
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/openMasterPasswordDialog.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
@ -1228,6 +1240,18 @@ packages/lib/services/e2ee/EncryptionService.js.map
packages/lib/services/e2ee/EncryptionService.test.d.ts
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.test.js.map
packages/lib/services/e2ee/RSA.node.d.ts
packages/lib/services/e2ee/RSA.node.js
packages/lib/services/e2ee/RSA.node.js.map
packages/lib/services/e2ee/ppk.d.ts
packages/lib/services/e2ee/ppk.js
packages/lib/services/e2ee/ppk.js.map
packages/lib/services/e2ee/ppk.test.d.ts
packages/lib/services/e2ee/ppk.test.js
packages/lib/services/e2ee/ppk.test.js.map
packages/lib/services/e2ee/ppkTestUtils.d.ts
packages/lib/services/e2ee/ppkTestUtils.js
packages/lib/services/e2ee/ppkTestUtils.js.map
packages/lib/services/e2ee/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
@ -1564,6 +1588,9 @@ packages/lib/services/synchronizer/Synchronizer.conflicts.test.js.map
packages/lib/services/synchronizer/Synchronizer.e2ee.test.d.ts
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js.map
packages/lib/services/synchronizer/Synchronizer.ppk.test.d.ts
packages/lib/services/synchronizer/Synchronizer.ppk.test.js
packages/lib/services/synchronizer/Synchronizer.ppk.test.js.map
packages/lib/services/synchronizer/Synchronizer.resources.test.d.ts
packages/lib/services/synchronizer/Synchronizer.resources.test.js
packages/lib/services/synchronizer/Synchronizer.resources.test.js.map

1
package-lock.json generated
View File

@ -6,7 +6,6 @@
"": {
"name": "root",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"http-server": "^0.12.3",
"nodemon": "^2.0.9"

View File

@ -5,7 +5,6 @@
"type": "git",
"url": "git+https://github.com/laurent22/joplin.git"
},
"license": "MIT",
"scripts": {
"audit": "lerna-audit",
"bootstrap": "lerna bootstrap --force-local --no-ci",

View File

@ -17,7 +17,7 @@ class Command extends BaseCommand {
}
description() {
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file` and `target-status`.');
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.'); // `generate-ppk`
}
options() {
@ -151,6 +151,19 @@ class Command extends BaseCommand {
return;
}
// if (args.command === 'generate-ppk') {
// const syncInfo = localSyncInfo();
// if (syncInfo.ppk) throw new Error('This account already has a public-private key pair');
// const argPassword = options.password ? options.password.toString() : '';
// if (!argPassword) throw new Error('Password must be provided'); // TODO: should get from prompt
// const ppk = await generateKeyPair(EncryptionService.instance(), argPassword);
// syncInfo.ppk = ppk;
// saveLocalSyncInfo(syncInfo);
// await Setting.saveAll();
// }
if (args.command === 'target-status') {
const fs = require('fs-extra');

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,7 @@
"image-type": "^3.0.0",
"keytar": "^7.0.0",
"md5": "^2.2.1",
"node-rsa": "^1.1.1",
"open": "^7.0.4",
"proper-lockfile": "^2.0.1",
"read-chunk": "^2.1.0",

View File

@ -10,3 +10,4 @@ gui/note-viewer/lib.js
gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js
runForSharingCommands-*
runForTestingCommands-*
style.min.css

View File

@ -60,6 +60,7 @@ import editorCommandDeclarations from './gui/NoteEditor/editorCommandDeclaration
import ShareService from '@joplin/lib/services/share/ShareService';
import checkForUpdates from './checkForUpdates';
import { AppState } from './app.reducer';
// import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
const pluginClasses = [
require('./plugins/GotoAnything').default,
@ -74,24 +75,24 @@ class Application extends BaseApplication {
private checkAllPluginStartedIID_: any = null;
constructor() {
public constructor() {
super();
this.bridge_nativeThemeUpdated = this.bridge_nativeThemeUpdated.bind(this);
}
hasGui() {
public hasGui() {
return true;
}
reducer(state: AppState = appDefaultState, action: any) {
public reducer(state: AppState = appDefaultState, action: any) {
let newState = appReducer(state, action);
newState = resourceEditWatcherReducer(newState, action);
newState = super.reducer(newState, action);
return newState;
}
toggleDevTools(visible: boolean) {
public toggleDevTools(visible: boolean) {
if (visible) {
bridge().openDevTools();
} else {
@ -99,7 +100,7 @@ class Application extends BaseApplication {
}
}
async generalMiddleware(store: any, next: any, action: any) {
protected async generalMiddleware(store: any, next: any, action: any) {
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
setLocale(Setting.value('locale'));
// The bridge runs within the main process, with its own instance of locale.js
@ -145,7 +146,7 @@ class Application extends BaseApplication {
return result;
}
handleThemeAutoDetect() {
public handleThemeAutoDetect() {
if (!Setting.value('themeAutoDetect')) return;
if (bridge().shouldUseDarkColors()) {
@ -155,11 +156,11 @@ class Application extends BaseApplication {
}
}
bridge_nativeThemeUpdated() {
private bridge_nativeThemeUpdated() {
this.handleThemeAutoDetect();
}
updateTray() {
public updateTray() {
const app = bridge().electronApp();
if (app.trayShown() === Setting.value('showTrayIcon')) return;
@ -176,7 +177,7 @@ class Application extends BaseApplication {
}
}
updateEditorFont() {
public updateEditorFont() {
const fontFamilies = [];
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
fontFamilies.push('Avenir, Arial, sans-serif');
@ -191,7 +192,7 @@ class Application extends BaseApplication {
document.head.appendChild(styleTag);
}
setupContextMenu() {
public setupContextMenu() {
// bridge().setupContextMenu((misspelledWord: string, dictionarySuggestions: string[]) => {
// let output = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions);
// console.info(misspelledWord, dictionarySuggestions);
@ -337,7 +338,7 @@ class Application extends BaseApplication {
}, 500);
}
async start(argv: string[]): Promise<any> {
public async start(argv: string[]): Promise<any> {
// If running inside a package, the command line, instead of being "node.exe <path> <flags>" is "joplin.exe <flags>" so
// insert an extra argument so that they can be processed in a consistent way everywhere.
if (!bridge().electronIsDev()) argv.splice(1, 0, '.');
@ -549,7 +550,7 @@ class Application extends BaseApplication {
// setTimeout(() => {
// this.dispatch({
// type: 'DIALOG_OPEN',
// name: 'syncWizard',
// name: 'masterPassword',
// });
// }, 2000);
@ -563,6 +564,24 @@ class Application extends BaseApplication {
// });
// }, 2000);
// const testData = {
// "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmKpb4JiYiY16pGOabje7uMsFd7DcMnruGxJ9HSpOiOduj3ApKqRu0xWCkGyqpekyOjjooZ98wVkDPUFsyVjN+kG8yKFn2xXC5SeRyhIVbdytjYiGshr6x+T9XVI+HnJKQF3WbrcqSOejlDXJv6u7jKrLAlOT3tkqEb0ZefhcEIajq6kNkH51R0lwsFnzxDIK3MW1wNzmiOfM92f8PFxiOBmUtVIngGPlNgyld1FzKN7Ypz1uS6GOqAtRm325qyfE/+2Jgb7WaDFT7VB5pHnOiojj9+xi1DvQWCbbIYXoMi0XVi9i2ZQfM32aFwiHez5UL61IMWUcqQ0/gldh4HFlAQIDAQAB\n-----END PUBLIC KEY-----",
// "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAmKpb4JiYiY16pGOabje7uMsFd7DcMnruGxJ9HSpOiOduj3ApKqRu0xWCkGyqpekyOjjooZ98wVkDPUFsyVjN+kG8yKFn2xXC5SeRyhIVbdytjYiGshr6x+T9XVI+HnJKQF3WbrcqSOejlDXJv6u7jKrLAlOT3tkqEb0ZefhcEIajq6kNkH51R0lwsFnzxDIK3MW1wNzmiOfM92f8PFxiOBmUtVIngGPlNgyld1FzKN7Ypz1uS6GOqAtRm325qyfE/+2Jgb7WaDFT7VB5pHnOiojj9+xi1DvQWCbbIYXoMi0XVi9i2ZQfM32aFwiHez5UL61IMWUcqQ0/gldh4HFlAQIDAQABAoIBADFFMffPZ9Nk7MLnPmz54cTnCPGzC63jDLuCAQ0LnWMDxiPW4AJaJUZMt+GioISBOWue+D1JOrsv3iLD3bcxyPBOjP33UYxcfpT0a1Ha+j2FriFygX4zxOIEnlyi8VdkLWCOqGj9BlGXKKzpmx4X76Sbbn9mt9+BGNm2vOUnaZcPTVuOI7K6xZynlzMRYSyhu7J0QdYVK44vZ/TjdD/4pgX+ezrGiwx7OCf/KctjvEoYtXYV2gkBOifOlqYOp0fMEC3mVAZfwpvDTbRchb7h0rxmxfKbWsjPtDblByXBLJZ3PGcKcmJlu4Qsfd2AgrY62r+DbNt3EhK072ZilYIfKD0CgYEAybcDbucr67dWMlFh5b79bvJugw6rj1V59Tp+RX9nKgzaiBUHLun6cK5hbgg9z3ejc2SWlX7D+eOyveVjhDlxUOCFURJLo2oPMRKwBBKJkOJhdtAjPzyceYI6Yj2lvtDeijcZfg8F9YqUTMfisDsEi1MbGnqawWwUerN9P5TjRBcCgYEAwcAfw8KTnQsvXPwWwh6Wabtz0bUAKzA/D6oWTR5IbkBfb3jNU8lmh9H66H0P18Nsa3vozA6buW2LDhHCFFkQ4PUTQVKok1qhAsvJBECxdwMqb5iAXk3Yk3qQYGhR23Zkp1u82wmpSaBLKGr+SL9/q5EamqiR3PQYx/aQTeIaFqcCgYAn/N/xXGKYl/++eeOuZ+5V0DmYQZBBGfDTbIUbweXxsBqiX4jNBBVhwTAPYBLgzhbZCVfQyxCOuVT10EOqMrkED35eVAIqoxvf3pSGOiaLUlV/+EMEhj9+1xI753y0FzQGsmWbV98WjiJYFkgaJ5j/BbqZxTRoo8RrjqmFsT5cgQKBgQCWTc4WlmbfSKMIloOtOf9jrMjvoWOtHXN+WmuMjfaQmR2wI13eJvqEWRA1tXdJ4c/FHk39p0OFOQbL9ljCYknmyhiS72XZUlBgE+kwhGNnuSv9gKftAKUH2+gO8j62awUwk8lRfxA2DsTfaQk1NGH9ncauviDR8QcccRmHYeTtNwKBgQCOvHiVaNw8XJIqt2r3j8pEJcr8LO+WNtLDU+h9NhM5a5NxfeRUlxdrqR0FXS4NkE6E3h9iLIRt2V+0bghzJMhKuwdjC0K6+jCb7ImV+Xcl9LNOQ1mPLBLS1jqdQnBS1ZPtcQpMrVi6dU9vVespylKEyGnQnUUtLgYrbO9OMrP1uQ==\n-----END RSA PRIVATE KEY-----",
// "plaintext": "just testing",
// "ciphertext": "LBicxglLvMyBin8uMpUnF5ARQ+KtAM563RViMepnOcyXa/NOJonNBixm+th+jX44\r\n/rie2ESbWg/FnlR4mHCEpTQJFXt12zpeXvtM8Hy1OQMud1B1Hc9hp1hhd1t6cuDz\r\n/Cs10n1+57V6zwHottYA6tn84cBn678SvPa/WTwgvb9lnBVZbesm3dVIr5uh2hk9\r\nNcVkmqyfi+ilkNQ3FIQfL+ciHvPFUIpljgIOipZhmufubdgMGW1HEUYlsmxLE7ce\r\ndpUQJoIbfKJ1x2dJRoeYsCjvcYFWdMUcg78HkXR+UcObP6zkK8cH33fb6PKKd8Z4\r\nToj4HROza8Dp7uCV5XyBTA=="
// };
// await checkTestData(testData);
// const testData = await createTestData();
// await checkTestData(testData);
// await printTestData();
// await runIntegrationTests();
return null;
}

View File

@ -10,6 +10,11 @@ export enum ButtonLevel {
Recommended = 'recommended',
}
export enum ButtonSize {
Small = 1,
Normal = 2,
}
interface Props {
title?: string;
iconName?: string;
@ -21,29 +26,38 @@ interface Props {
tooltip?: string;
disabled?: boolean;
style?: any;
size?: ButtonSize;
}
const StyledTitle = styled.span`
`;
// const buttonHeight = 32;
const buttonHeight = (props: Props) => {
if (!props.size || props.size === ButtonSize.Normal) return 32;
if (props.size === ButtonSize.Small) return 26;
throw new Error(`Unknown size: ${props.size}`);
};
const StyledButtonBase = styled.button`
display: flex;
align-items: center;
flex-direction: row;
height: ${(props: any) => `${props.theme.toolbarHeight}px`};
min-height: ${(props: any) => `${props.theme.toolbarHeight}px`};
max-height: ${(props: any) => `${props.theme.toolbarHeight}px`};
width: ${(props: any) => props.iconOnly ? `${props.theme.toolbarHeight}px` : 'auto'};
${(props: any) => props.iconOnly ? `min-width: ${props.theme.toolbarHeight}px;` : ''}
height: ${(props: Props) => buttonHeight(props)}px;
min-height: ${(props: Props) => buttonHeight(props)}px;
max-height: ${(props: Props) => buttonHeight(props)}px;
width: ${(props: any) => props.iconOnly ? `${buttonHeight}px` : 'auto'};
${(props: any) => props.iconOnly ? `min-width: ${buttonHeight}px;` : ''}
${(props: any) => !props.iconOnly ? 'min-width: 100px;' : ''}
${(props: any) => props.iconOnly ? `max-width: ${props.theme.toolbarHeight}px;` : ''}
${(props: any) => props.iconOnly ? `max-width: ${buttonHeight}px;` : ''}
box-sizing: border-box;
border-radius: 3px;
border-style: solid;
border-width: 1px;
font-size: ${(props: any) => props.theme.fontSize}px;
padding: 0 ${(props: any) => props.iconOnly ? 4 : 8}px;
/*font-size: ${(props: any) => props.theme.fontSize}px; */
padding: 0 ${(props: any) => props.iconOnly ? 4 : 14}px;
justify-content: center;
opacity: ${(props: any) => props.disabled ? 0.5 : 1};
user-select: none;
@ -207,7 +221,7 @@ function Button(props: Props) {
}
return (
<StyledButton style={props.style} disabled={props.disabled} title={props.tooltip} className={props.className} iconOnly={iconOnly} onClick={onClick}>
<StyledButton size={props.size} style={props.style} disabled={props.disabled} title={props.tooltip} className={props.className} iconOnly={iconOnly} onClick={onClick}>
{renderIcon()}
{renderTitle()}
</StyledButton>

View File

@ -1,13 +1,12 @@
import * as React from 'react';
import Sidebar from './Sidebar';
import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button';
import Button, { ButtonLevel, ButtonSize } from '../Button/Button';
import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
import control_PluginsStates from './controls/plugins/PluginsStates';
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const pathUtils = require('@joplin/lib/path-utils');
@ -515,6 +514,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
level={ButtonLevel.Secondary}
title={_('Browse...')}
onClick={browseButtonClick}
size={ButtonSize.Small}
/>
</div>
</div>
@ -683,7 +683,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
// screenComp is a custom config screen, such as the encryption config screen or keymap config screen.
// These screens handle their own loading/saving of settings and have bespoke rendering.
// When screenComp is null, it means we are viewing the regular settings.
const screenComp = this.state.screenName ? <div style={{ overflow: 'scroll', flex: 1 }}>{this.screenFromName(this.state.screenName)}</div> : null;
const screenComp = this.state.screenName ? <div className="config-screen-content-wrapper" style={{ overflow: 'scroll', flex: 1 }}>{this.screenFromName(this.state.screenName)}</div> : null;
if (screenComp) containerStyle.display = 'none';
@ -700,7 +700,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
delete style.width;
return (
<div style={{ display: 'flex', flexDirection: 'row', height: this.props.style.height }}>
<div className="config-screen" style={{ display: 'flex', flexDirection: 'row', height: this.props.style.height }}>
<Sidebar
selection={this.state.selectedSectionName}
onSelectionChange={this.sidebar_selectionChange}

View File

@ -5,7 +5,7 @@ import { _ } from '@joplin/lib/locale';
import styled from 'styled-components';
import SearchPlugins from './SearchPlugins';
import PluginBox, { ItemEvent, UpdateState } from './PluginBox';
import Button, { ButtonLevel } from '../../../Button/Button';
import Button, { ButtonLevel, ButtonSize } from '../../../Button/Button';
import bridge from '../../../../services/bridge';
import produce from 'immer';
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
@ -309,7 +309,7 @@ export default function(props: Props) {
<div>
{renderRepoApiError()}
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
<ToolsButton tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
<ToolsButton size={ButtonSize.Small} tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
<div style={{ display: 'flex', flex: 1 }}>
{props.renderHeader(props.themeId, _('Manage your plugins'))}
</div>

View File

@ -0,0 +1,14 @@
.config-screen-content-wrapper {
padding: 24px;
overflow: auto;
background-color: var(--joplin-background-color3);
}
.config-screen-content > .section {
padding-bottom: 20px;
border-bottom: 1px solid var(--joplin-divider-color);
}
.config-screen-content > .section:last-child {
border-bottom: 0;
}

View File

@ -1,3 +1,4 @@
import { useEffect, useCallback } from 'react';
import styled from 'styled-components';
const DialogModalLayer = styled.div`
@ -27,11 +28,27 @@ const DialogRoot = styled.div`
interface Props {
renderContent: Function;
className?: string;
onClose?: Function;
}
export default function Dialog(props: Props) {
const onWindowKeydown = useCallback((event: any) => {
if (event.key === 'Escape') {
if (props.onClose) props.onClose();
}
}, [props.onClose]);
useEffect(() => {
window.addEventListener('keydown', onWindowKeydown);
return () => {
window.removeEventListener('keydown', onWindowKeydown);
};
}, [onWindowKeydown]);
return (
<DialogModalLayer>
<DialogModalLayer className={props.className}>
<DialogRoot>
{props.renderContent()}
</DialogRoot>

View File

@ -17,10 +17,13 @@ export type ClickEventHandler = (event: ClickEvent)=> void;
interface Props {
themeId: number;
onClick?: ClickEventHandler;
okButtonShow?: boolean;
cancelButtonShow?: boolean;
cancelButtonLabel?: string;
cancelButtonDisabled?: boolean;
okButtonShow?: boolean;
okButtonLabel?: string;
okButtonRef?: any;
okButtonDisabled?: boolean;
customButtons?: ButtonSpec[];
}
@ -68,15 +71,15 @@ export default function DialogButtonRow(props: Props) {
if (props.okButtonShow !== false) {
buttonComps.push(
<button key="ok" style={buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
{_('OK')}
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
</button>
);
}
if (props.cancelButtonShow !== false) {
buttonComps.push(
<button key="cancel" style={Object.assign({}, buttonStyle)} onClick={cancelButton_click}>
<button disabled={props.cancelButtonDisabled} key="cancel" style={Object.assign({}, buttonStyle)} onClick={cancelButton_click}>
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
</button>
);

View File

@ -2,13 +2,13 @@ import styled from 'styled-components';
const Root = styled.div`
display: flex;
justify-content: ${props => props.justifyContent ? props.justifyContent : 'flex-start'};
justify-content: ${props => props.justifyContent ? props.justifyContent : 'center'};
font-family: ${props => props.theme.fontFamily};
font-size: ${props => props.theme.fontSize * 1.5}px;
line-height: 1.6em;
color: ${props => props.theme.color};
font-weight: bold;
margin-bottom: 1.2em;
margin-bottom: 1em;
`;

View File

@ -5,23 +5,17 @@ import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shim from '@joplin/lib/shim';
import dialogs from '../dialogs';
import bridge from '../../services/bridge';
import { decryptedStatText, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { decryptedStatText, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputPasswords, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import StyledInput from '../style/StyledInput';
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import Button, { ButtonLevel } from '../Button/Button';
import styled from 'styled-components';
import { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
import { AppState } from '../../app.reducer';
import Setting from '@joplin/lib/models/Setting';
const MasterPasswordInput = styled(StyledInput)`
min-width: 300px;
align-items: center;
`;
import CommandService from '@joplin/lib/services/CommandService';
import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk';
interface Props {
themeId: any;
@ -32,6 +26,7 @@ interface Props {
shouldReencrypt: boolean;
activeMasterKeyId: string;
masterPassword: string;
ppk: PublicPrivateKeyPair;
}
const EncryptionConfigScreen = (props: Props) => {
@ -42,7 +37,7 @@ const EncryptionConfigScreen = (props: Props) => {
}, [props.themeId]);
const stats = useStats();
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { passwordChecks, masterPasswordKeys, masterPasswordStatus } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
const onUpgradeMasterKey = useCallback((mk: MasterKeyEntity) => {
@ -70,8 +65,8 @@ const EncryptionConfigScreen = (props: Props) => {
return (
<div>
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
<h2>{_('Keys that need upgrading')}</h2>
<p>{_('The following keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded key will still be able to decrypt and encrypt your data as usual.')}</p>
<table>
<tbody>
<tr>
@ -102,7 +97,7 @@ const EncryptionConfigScreen = (props: Props) => {
return (
<div>
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
<h2>{_('Re-encryption')}</h2>
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
<span style={{ marginRight: 10 }}>
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
@ -171,8 +166,8 @@ const EncryptionConfigScreen = (props: Props) => {
mkComps.push(renderMasterKey(mk));
}
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => toggleShowDisabledMasterKeys() } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
const headerComp = isEnabledMasterKeys ? <h2>{_('Encryption Keys')}</h2> : <a onClick={() => toggleShowDisabledMasterKeys() } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled keys') : _('Show disabled keys')}</a>;
const infoComp: any = null; // isEnabledMasterKeys ? <p>{'Note: Only one key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
const tableComp = !showTable ? null : (
<table>
<tbody>
@ -191,7 +186,7 @@ const EncryptionConfigScreen = (props: Props) => {
if (mkComps.length) {
return (
<div>
<div className="section">
{headerComp}
{tableComp}
{infoComp}
@ -202,148 +197,153 @@ const EncryptionConfigScreen = (props: Props) => {
return null;
};
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
const renderMasterPassword = () => {
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
const theme = themeStyle(props.themeId);
if (passwordChecks['master']) {
return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span style={theme.textStyle}>{_('Master password:')}</span>&nbsp;&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}> {_('Loaded')}</span>
</div>
);
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={theme.textStyle}> {'The master password is not set or is invalid. Please type it below:'}</span>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={inputMasterPassword} onChange={(event: any) => onMasterPasswordChange(event.target.value)} />{' '}
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
</div>
</div>
);
}
};
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: theme.configScreenPadding,
overflow: 'auto',
backgroundColor: theme.backgroundColor3,
});
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
for (let i = 0; i < props.masterKeys.length; i++) {
const mk = props.masterKeys[i];
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
const onToggleButtonClick = useCallback(async () => {
const isEnabled = getEncryptionEnabled();
const newEnabled = !isEnabled;
const masterKey = getDefaultMasterKey();
const hasMasterPassword = !!props.masterPassword;
let newPassword = '';
let answer = null;
if (isEnabled) {
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
const answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
if (!answer) return;
} else {
const msg = enableEncryptionConfirmationMessages(masterKey);
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
const msg = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword);
newPassword = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
}
if (!answer) return;
if (hasMasterPassword && newEnabled) {
if (!(await masterPasswordIsValid(newPassword))) {
alert('Invalid password. Please try again. If you have forgotten your password you will need to reset it.');
return;
}
}
try {
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
await toggleAndSetupEncryption(EncryptionService.instance(), newEnabled, masterKey, newPassword);
} catch (error) {
await dialogs.alert(error.message);
}
}, [props.masterPassword]);
const renderEncryptionSection = () => {
const decryptedItemsInfo = <p>{decryptedStatText(stats)}</p>;
const toggleButton = (
<Button
onClick={onToggleButtonClick}
title={props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
level={ButtonLevel.Secondary}
/>
);
const needUpgradeSection = renderNeedUpgradeSection();
const reencryptDataSection = renderReencryptData();
return (
<div className="section">
<div className="encryption-section">
<h2 className="-no-top-margin">{_('End-to-end encryption')}</h2>
<p>
{_('Encryption:')} <strong>{props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
<p>
{_('Public-Private Key Pair:')} <strong>{props.ppk ? _('Generated') : _('Not generated')}</strong>
</p>
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
{props.shouldReencrypt ? reencryptDataSection : null}
</div>
</div>
);
};
const decryptedItemsInfo = <p style={theme.textStyle}>{decryptedStatText(stats)}</p>;
const toggleButton = (
<button
style={theme.buttonStyle}
onClick={() => {
void onToggleButtonClick();
}}
>
{props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
</button>
);
const renderMasterPasswordSection = () => {
const onManageMasterPassword = async () => {
void CommandService.instance().execute('openMasterPasswordDialog');
};
const needUpgradeSection = renderNeedUpgradeSection();
const reencryptDataSection = renderReencryptData();
const buttonTitle = CommandService.instance().label('openMasterPasswordDialog');
const needPassword = Object.values(passwordChecks).includes(false);
const enabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true);
const disabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
const needPasswordMessage = !needPassword ? null : (
<p className="needpassword">{_('Your master password is needed to decrypt some of your data.')}<br/>{_('Please click on "%s" to proceed', buttonTitle)}</p>
);
let nonExistingMasterKeySection = null;
return (
<div className="section">
<div className="manage-password-section">
<h2>{_('Master password')}</h2>
<p className="status"><span>{_('Master password:')}</span>&nbsp;<span className="bold">{getMasterPasswordStatusMessage(masterPasswordStatus)}</span></p>
{needPasswordMessage}
<Button className="managebutton" level={needPassword ? ButtonLevel.Primary : ButtonLevel.Secondary} onClick={onManageMasterPassword} title={buttonTitle} />
</div>
</div>
);
};
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<tr key={id}>
<td style={theme.textStyle}>{id}</td>
</tr>
const onClearMasterPassword = useCallback(() => {
Setting.setValue('encryption.masterPassword', '');
}, []);
const renderDebugSection = () => {
if (Setting.value('env') !== 'dev') return null;
return (
<div style={{ paddingBottom: '20px' }}>
<Button level={ButtonLevel.Secondary} onClick={onClearMasterPassword} title="Clear master password" />
</div>
);
};
const renderNonExistingMasterKeysSection = () => {
let nonExistingMasterKeySection = null;
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
for (let i = 0; i < props.masterKeys.length; i++) {
const mk = props.masterKeys[i];
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<tr key={id}>
<td style={theme.textStyle}>{id}</td>
</tr>
);
}
nonExistingMasterKeySection = (
<div className="section">
<h2>{_('Missing Keys')}</h2>
<p>{_('The keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
nonExistingMasterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
return nonExistingMasterKeySection;
};
return (
<div>
<div style={containerStyle}>
{
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
<p style={theme.textStyle}>
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
<a
onClick={() => {
bridge().openExternal('https://joplinapp.org/e2ee/');
}}
href="#"
style={theme.urlStyle}
>
https://joplinapp.org/e2ee/
</a>
</p>
</div>
}
<h1 style={theme.h1Style}>{_('Status')}</h1>
<p style={theme.textStyle}>
{_('Encryption is:')} <strong>{props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
{renderMasterPassword()}
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
{props.shouldReencrypt ? reencryptDataSection : null}
{enabledMasterKeySection}
{disabledMasterKeySection}
{nonExistingMasterKeySection}
{!props.shouldReencrypt ? reencryptDataSection : null}
</div>
<div className="config-screen-content">
{renderDebugSection()}
{renderEncryptionSection()}
{renderMasterPasswordSection()}
{renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true)}
{renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false)}
{renderNonExistingMasterKeysSection()}
</div>
);
};
@ -360,6 +360,7 @@ const mapStateToProps = (state: AppState) => {
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
ppk: syncInfo.ppk,
};
};

View File

@ -1,5 +1,12 @@
.encryption-config-test {
& > .item {
font-weight: bold;
}
}
.manage-password-section > .managebutton {
display: flex;
}
.manage-password-section > .status {
display: flex;
flex-direction: row;
}
.manage-password-section > .needpassword {
color: var(--joplin-color-warn);
}

View File

@ -0,0 +1,224 @@
import * as React from 'react';
import { useCallback, useState, useEffect, useMemo } from 'react';
import { _ } from '@joplin/lib/locale';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
import Dialog from '../Dialog';
import DialogTitle from '../DialogTitle';
import StyledInput from '../style/StyledInput';
import { getMasterPasswordStatus, getMasterPasswordStatusMessage, checkHasMasterPasswordEncryptedData, masterPasswordIsValid, MasterPasswordStatus, resetMasterPassword, updateMasterPassword } from '@joplin/lib/services/e2ee/utils';
import { reg } from '@joplin/lib/registry';
import EncryptionService from '../../../lib/services/e2ee/EncryptionService';
import KvStore from '../../../lib/services/KvStore';
interface Props {
themeId: number;
dispatch: Function;
}
enum Mode {
Set = 1,
Reset = 2,
}
export default function(props: Props) {
const [status, setStatus] = useState(MasterPasswordStatus.NotSet);
const [hasMasterPasswordEncryptedData, setHasMasterPasswordEncryptedData] = useState(true);
const [currentPassword, setCurrentPassword] = useState('');
const [currentPasswordIsValid, setCurrentPasswordIsValid] = useState(false);
const [password1, setPassword1] = useState('');
const [password2, setPassword2] = useState('');
const [saveButtonDisabled, setSaveButtonDisabled] = useState(true);
const [showPasswordForm, setShowPasswordForm] = useState(false);
const [updatingPassword, setUpdatingPassword] = useState(false);
const [mode, setMode] = useState<Mode>(Mode.Set);
const onClose = useCallback(() => {
props.dispatch({
type: 'DIALOG_CLOSE',
name: 'masterPassword',
});
}, [props.dispatch]);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const newStatus = await getMasterPasswordStatus();
const hasIt = await checkHasMasterPasswordEncryptedData();
if (event.cancelled) return;
setStatus(newStatus);
setHasMasterPasswordEncryptedData(hasIt);
}, []);
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
if (event.buttonName === 'cancel') {
onClose();
return;
}
if (event.buttonName === 'ok') {
setUpdatingPassword(true);
try {
if (mode === Mode.Set) {
await updateMasterPassword(currentPassword, password1);
} else if (mode === Mode.Reset) {
await resetMasterPassword(EncryptionService.instance(), KvStore.instance(), password1);
} else {
throw new Error(`Unknown mode: ${mode}`);
}
void reg.waitForSyncFinishedThenSync();
onClose();
} catch (error) {
alert(error.message);
} finally {
setUpdatingPassword(false);
}
return;
}
}, [currentPassword, password1, onClose, mode]);
const needToRepeatPassword = useMemo(() => {
if (mode === Mode.Reset) return true;
return !hasMasterPasswordEncryptedData;
}, [hasMasterPasswordEncryptedData, mode]);
const onCurrentPasswordChange = useCallback((event: any) => {
setCurrentPassword(event.target.value);
}, []);
const onPasswordChange1 = useCallback((event: any) => {
setPassword1(event.target.value);
}, []);
const onPasswordChange2 = useCallback((event: any) => {
setPassword2(event.target.value);
}, []);
const onShowPasswordForm = useCallback(() => {
setShowPasswordForm(true);
}, []);
const onToggleMode = useCallback(() => {
setMode(m => {
return m === Mode.Set ? Mode.Reset : Mode.Set;
});
setCurrentPassword('');
setPassword1('');
setPassword2('');
}, []);
useEffect(() => {
setSaveButtonDisabled(updatingPassword || (!password1 || (needToRepeatPassword && password1 !== password2)));
}, [password1, password2, updatingPassword, needToRepeatPassword]);
useEffect(() => {
setShowPasswordForm(status === MasterPasswordStatus.NotSet);
}, [status]);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const isValid = currentPassword ? await masterPasswordIsValid(currentPassword) : false;
if (event.cancelled) return;
setCurrentPasswordIsValid(isValid);
}, [currentPassword]);
function renderCurrentPasswordIcon() {
if (!currentPassword || status === MasterPasswordStatus.NotSet) return null;
return currentPasswordIsValid ? <i className="fas fa-check"></i> : <i className="fas fa-times"></i>;
}
function renderPasswordForm() {
const renderCurrentPassword = () => {
if (status === MasterPasswordStatus.NotSet) return null;
if (mode === Mode.Reset) return null;
return (
<div className="form-input-group">
<label>{'Current password'}</label>
<div className="current-password-wrapper">
<StyledInput
type="password"
value={currentPassword}
onChange={onCurrentPasswordChange}
/>
{renderCurrentPasswordIcon()}
</div>
</div>
);
};
const renderResetMasterPasswordLink = () => {
if (mode === Mode.Reset) return null;
return <p><a href="#" onClick={onToggleMode}>Reset master password</a></p>;
};
if (showPasswordForm) {
return (
<div>
<div className="form">
{renderCurrentPassword()}
<div className="form-input-group">
<label>{'Enter password'}</label>
<StyledInput type="password" value={password1} onChange={onPasswordChange1}/>
</div>
{needToRepeatPassword && (
<div className="form-input-group">
<label>{'Re-enter password'}</label>
<StyledInput type="password" value={password2} onChange={onPasswordChange2}/>
</div>
)}
</div>
<p className="bold">Please make sure you remember your password. For security reasons, it is not possible to recover it if it is lost.</p>
{renderResetMasterPasswordLink()}
</div>
);
} else {
return (
<p>
<a onClick={onShowPasswordForm} href="#">Change master password</a>
</p>
);
}
}
function renderContent() {
if (mode === Mode.Reset) {
return (
<div className="dialog-content">
<p>Attention: After resetting your password it will no longer be possible to decrypt any data encrypted with your current password. All encrypted shared notebooks will also be unshared, so please ask the notebook owner to share it again with you.</p>
{renderPasswordForm()}
</div>
);
} else {
return (
<div className="dialog-content">
<p>Your master password is used to protect sensitive information. In particular, it is used to encrypt your notes when end-to-end encryption (E2EE) is enabled, or to share and encrypt notes with someone who has E2EE enabled.</p>
<p>
<span>{'Master password status:'}</span> <span className="bold">{getMasterPasswordStatusMessage(status)}</span>
</p>
{renderPasswordForm()}
</div>
);
}
}
const dialogTitle = mode === Mode.Set ? _('Manager master password') : `⚠️ ${_('Reset master password')} ⚠️`;
const okButtonLabel = mode === Mode.Set ? _('Save') : `⚠️ ${_('Reset master password')} ⚠️`;
function renderDialogWrapper() {
return (
<div className="dialog-root">
<DialogTitle title={dialogTitle}/>
{renderContent()}
<DialogButtonRow
themeId={props.themeId}
onClick={onButtonRowClick}
okButtonLabel={okButtonLabel}
okButtonDisabled={saveButtonDisabled}
cancelButtonDisabled={updatingPassword}
/>
</div>
);
}
return (
<Dialog onClose={onClose} className="master-password-dialog" renderContent={renderDialogWrapper}/>
);
}

View File

@ -752,6 +752,7 @@ function useMenu(props: Props) {
rootMenus.go.submenu.push(menuItemDic.gotoAnything);
rootMenus.tools.submenu.push(menuItemDic.commandPalette);
rootMenus.tools.submenu.push(menuItemDic.openMasterPasswordDialog);
for (const view of props.pluginMenuItems) {
const location: MenuItemLocation = view.location;

View File

@ -22,6 +22,10 @@ const StyledRoot = styled.div`
const StyledButton = styled(Button)`
margin-left: 8px;
width: 26px;
height: 26px;
min-width: 26px;
min-height: 26px;
`;
const ButtonContainer = styled.div`

View File

@ -20,6 +20,7 @@ import DialogTitle from './DialogTitle';
import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow';
import Dialog from './Dialog';
import SyncWizardDialog from './SyncWizard/Dialog';
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
const { ImportScreen } = require('./ImportScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
@ -61,6 +62,12 @@ const registeredDialogs: Record<string, RegisteredDialog> = {
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId}/>;
},
},
masterPassword: {
render: (props: RegisteredDialogProps) => {
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId}/>;
},
},
};
const GlobalStyle = createGlobalStyle`

View File

@ -49,5 +49,6 @@ export default function() {
'showShareFolderDialog',
'gotoAnything',
'commandPalette',
'openMasterPasswordDialog',
];
}

View File

@ -23,10 +23,10 @@ const tasks = {
buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'),
compileSass: {
fn: async () => {
const guiDir = `${__dirname}/gui`;
await compileSass([
`${guiDir}/EncryptionConfigScreen/style.scss`,
], `${__dirname}/style.min.css`);
await compileSass(
`${__dirname}/style.scss`,
`${__dirname}/style.min.css`
);
},
},
};

View File

@ -8,7 +8,7 @@
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
-->
<title>Joplin</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style.min.css">
<link rel="stylesheet" href="style/icons/style.css">
<link rel="stylesheet" href="node_modules/@fortawesome/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
@ -47,7 +47,6 @@
/* Disable dragging of links (which are often buttons) */
a:not([draggable=true]), img:not([draggable=true]) {
-webkit-user-drag: none;
user-drag: none;
}
</style>
</body>

View File

@ -143,4 +143,104 @@ a {
*:focus {
outline: none;
}
}
/* =========================================================================================
General classes
========================================================================================= */
body {
color: var(--joplin-color);
font-size: 16px;
}
h2 {
font-size: 24px;
&.-no-top-margin {
margin-top: 0;
}
}
.form {
display: flex;
flex-direction: column;
}
.form > .form-input-group {
display: flex;
flex-direction: column;
}
.form > .form-input-group > label {
margin-bottom: 10px;
}
.bold {
font-weight: bold;
}
p,
div.form,
.form > .form-input-group {
margin-top: 0;
margin-bottom: 20px;
}
.form > .form-input-group:last-child {
margin-bottom: 0;
}
button {
cursor: pointer;
}
button:disabled {
cursor: default;
}
a {
color: var(--joplin-url-color);
}
/* =========================================================================================
Component-specific classes
========================================================================================= */
.config-screen .config-section {
border-bottom: 1px solid var(--joplin-divider-color);
padding-bottom: 20px;
}
.master-password-dialog .dialog-root {
min-width: 500px;
max-width: 600px;
}
.master-password-dialog .dialog-content {
background-color: var(--joplin-background-color3);
padding: 1em;
padding-bottom: 1px;
}
.master-password-dialog .current-password-wrapper {
display: flex;
flex-direction: row;
align-items: center;
}
.master-password-dialog .current-password-wrapper input {
flex: 1;
margin-right: 10px;
}
.master-password-dialog .fa-check {
color: var(--joplin-color-correct);
}
.master-password-dialog .fa-times {
color: var(--joplin-color-error);
}

File diff suppressed because it is too large Load Diff

View File

@ -144,6 +144,7 @@
"moment": "^2.22.2",
"node-fetch": "^1.7.3",
"node-notifier": "^8.0.0",
"node-rsa": "^1.1.1",
"pretty-bytes": "^5.3.0",
"re-resizable": "^6.5.4",
"react": "16.13.1",

View File

@ -3,6 +3,11 @@
# Setup the sync parameters for user X and create a few folders and notes to
# allow sharing. Also calls the API to create the test users and clear the data.
# For example, to setup a user for sharing, and another as recipient with E2EE
# enabled:
# ./runForTesting.sh 1 createUsers,createData,reset,e2ee,sync && ./runForTesting.sh 2 reset,e2ee,sync && ./runForTesting.sh 1
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
@ -50,12 +55,21 @@ do
echo "config sync.target 10" >> "$CMD_FILE"
# echo "config sync.10.path http://api.joplincloud.local:22300" >> "$CMD_FILE"
echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE"
echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE"
echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE"
elif [[ $CMD == "e2ee" ]]; then
echo "e2ee enable --password 111111" >> "$CMD_FILE"
elif [[ $CMD == "sync" ]]; then
echo "sync" >> "$CMD_FILE"
# elif [[ $CMD == "generatePpk" ]]; then
# echo "e2ee generate-ppk --password 111111" >> "$CMD_FILE"
# echo "sync" >> "$CMD_FILE"
else
echo "Unknown command: $CMD"

View File

@ -1,5 +0,0 @@
.encryption-config-test > .item {
font-weight: bold;
}
/*# sourceMappingURL=style.min.css.map */

View File

@ -0,0 +1,3 @@
@use 'main.scss' as main;
@use 'gui/EncryptionConfigScreen/style.scss' as encryption-config-screen;
@use 'gui/ConfigScreen/style.scss' as config-screen;

View File

@ -127,6 +127,7 @@ const EncryptionConfigScreen = (props: Props) => {
const renderPasswordPrompt = () => {
const theme = themeStyle(props.themeId);
const masterKey = getDefaultMasterKey();
const hasMasterPassword = !!props.masterPassword;
const onEnableClick = async () => {
try {
@ -143,7 +144,7 @@ const EncryptionConfigScreen = (props: Props) => {
}
};
const messages = enableEncryptionConfirmationMessages(masterKey);
const messages = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword);
const messageComps = messages.map((msg: string) => {
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;

View File

@ -0,0 +1,4 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

View File

@ -11,6 +11,7 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
46E31F54C547C341F605BB66 /* libPods-Joplin.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A5E1CD825FABD6C4E704EA54 /* libPods-Joplin.a */; };
4D122473270878D700DE23E8 /* wtf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D122472270878D700DE23E8 /* wtf.swift */; };
5E556FC75AECECB13464A724 /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FAC957496DFD2368FFE3C360 /* libPods-ShareExtension.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
AE152142260F770400217DCB /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE152141260F770400217DCB /* ShareViewController.m */; };
@ -60,6 +61,8 @@
2C91CD1424C7137D07789148 /* Pods-Joplin-JoplinTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-JoplinTests.release.xcconfig"; path = "Target Support Files/Pods-Joplin-JoplinTests/Pods-Joplin-JoplinTests.release.xcconfig"; sourceTree = "<group>"; };
2DA44D9A347489A29B995F73 /* Pods-Joplin-tvOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-tvOSTests.debug.xcconfig"; path = "Target Support Files/Pods-Joplin-tvOSTests/Pods-Joplin-tvOSTests.debug.xcconfig"; sourceTree = "<group>"; };
37DBC181C4AD99CBE0D07EEB /* Pods-Joplin-tvOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-tvOSTests.release.xcconfig"; path = "Target Support Files/Pods-Joplin-tvOSTests/Pods-Joplin-tvOSTests.release.xcconfig"; sourceTree = "<group>"; };
4D122471270878D600DE23E8 /* Joplin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Joplin-Bridging-Header.h"; sourceTree = "<group>"; };
4D122472270878D700DE23E8 /* wtf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wtf.swift; sourceTree = "<group>"; };
505CB61090817F4453631957 /* Pods-Joplin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin.debug.xcconfig"; path = "Target Support Files/Pods-Joplin/Pods-Joplin.debug.xcconfig"; sourceTree = "<group>"; };
5DE39012F71F18423C665C57 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Joplin/LaunchScreen.storyboard; sourceTree = "<group>"; };
@ -127,6 +130,8 @@
13B07FB61A68108700A75B9A /* Info.plist */,
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB71A68108700A75B9A /* main.m */,
4D122472270878D700DE23E8 /* wtf.swift */,
4D122471270878D600DE23E8 /* Joplin-Bridging-Header.h */,
);
name = Joplin;
sourceTree = "<group>";
@ -261,7 +266,7 @@
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = A9BXAFS6CT;
LastSwiftMigration = 1120;
LastSwiftMigration = 1300;
};
AE82E4A72599FA3A0013551B = {
CreatedOnToolsVersion = 12.0.1;
@ -445,6 +450,7 @@
buildActionMask = 2147483647;
files = (
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
4D122473270878D700DE23E8 /* wtf.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -500,6 +506,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin;
PRODUCT_NAME = Joplin;
SWIFT_OBJC_BRIDGING_HEADER = "Joplin-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -527,6 +534,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin;
PRODUCT_NAME = Joplin;
SWIFT_OBJC_BRIDGING_HEADER = "Joplin-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";

View File

@ -8,24 +8,24 @@
#import <RNCPushNotificationIOS.h>
#import "RNQuickActionManager.h"
#ifdef FB_SONARKIT_ENABLED
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
// #ifdef FB_SONARKIT_ENABLED
// #import <FlipperKit/FlipperClient.h>
// #import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
// #import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
// #import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
// #import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
// #import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
static void InitializeFlipper(UIApplication *application) {
FlipperClient *client = [FlipperClient sharedClient];
SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
[client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
[client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
[client addPlugin:[FlipperKitReactPlugin new]];
[client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
[client start];
}
#endif
// static void InitializeFlipper(UIApplication *application) {
// FlipperClient *client = [FlipperClient sharedClient];
// SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
// [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
// [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
// [client addPlugin:[FlipperKitReactPlugin new]];
// [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
// [client start];
// }
// #endif
@implementation AppDelegate
@ -70,9 +70,9 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#ifdef FB_SONARKIT_ENABLED
InitializeFlipper(application);
#endif
// #ifdef FB_SONARKIT_ENABLED
// InitializeFlipper(application);
// #endif
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge

View File

@ -227,6 +227,8 @@ PODS:
- React-Core
- react-native-netinfo (6.0.0):
- React-Core
- react-native-rsa-native (2.0.4):
- React
- react-native-slider (3.0.3):
- React
- react-native-sqlite-storage (5.0.0):
@ -348,6 +350,7 @@ DEPENDENCIES:
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-image-resizer (from `../node_modules/react-native-image-resizer`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-rsa-native (from `../node_modules/react-native-rsa-native`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`)
- react-native-version-info (from `../node_modules/react-native-version-info`)
@ -429,6 +432,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-image-resizer"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-rsa-native:
:path: "../node_modules/react-native-rsa-native"
react-native-slider:
:path: "../node_modules/@react-native-community/slider"
react-native-sqlite-storage:
@ -510,6 +515,7 @@ SPEC CHECKSUMS:
react-native-image-picker: c6d75c4ab2cf46f9289f341242b219cb3c1180d3
react-native-image-resizer: a79bcffdef1b52160ff91db0d6fa24816a4ff332
react-native-netinfo: e849fc21ca2f4128a5726c801a82fc6f4a6db50d
react-native-rsa-native: 1f6bba06dd02f0e652a66a384c75c270f7a0062f
react-native-slider: e99fc201cefe81270fc9d81714a7a0f5e566b168
react-native-sqlite-storage: 418ef4afc5e6df6ce3574c4617e5f0b65cffde55
react-native-version-info: 36490da17d2c6b5cc21321c70e433784dee7ed0b

View File

@ -0,0 +1,15 @@
//
// wtf.swift
// Joplin
//
// Created by Laurent on 02/10/2021.
// Copyright © 2021 joplinapp.org. All rights reserved.
//
// For whatever reason, this empty file is needed to fix this bug:
// https://github.com/facebook/react-native/issues/32242
//
// Solution:
// https://github.com/facebook/react-native/issues/32242#issuecomment-924770488
import Foundation

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,10 @@
"@react-native-community/netinfo": "^6.0.0",
"@react-native-community/push-notification-ios": "^1.6.0",
"@react-native-community/slider": "^3.0.3",
"assert-browserify": "^2.0.0",
"buffer": "^5.0.8",
"constants-browserify": "^1.0.0",
"crypto-browserify": "^3.12.0",
"events": "^3.2.0",
"joplin-rn-alarm-notification": "^1.0.3",
"jsc-android": "241213.1.0",
@ -45,6 +48,7 @@
"react-native-popup-dialog": "^0.9.41",
"react-native-popup-menu": "^0.10.0",
"react-native-quick-actions": "^0.3.13",
"react-native-rsa-native": "^2.0.4",
"react-native-securerandom": "^1.0.0-rc.0",
"react-native-share": "^5.1.5",
"react-native-side-menu": "^1.1.3",
@ -75,6 +79,7 @@
"@types/node": "^14.14.6",
"@types/react": "^16.9.55",
"@types/react-native": "^0.64.4",
"babel-plugin-module-resolver": "^4.1.0",
"execa": "^4.0.0",
"fs-extra": "^8.1.0",
"gulp": "^4.0.2",

View File

@ -27,10 +27,8 @@ import { setLocale, closestSupportedLocale, defaultLocale } from '@joplin/lib/lo
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
const VersionInfo = require('react-native-version-info').default;
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar, Linking, Platform } = require('react-native');
import NetInfo from '@react-native-community/netinfo';
const DropdownAlert = require('react-native-dropdownalert').default;
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
@ -77,7 +75,6 @@ import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
const { themeStyle } = require('./components/global-style.js');
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
const SyncTargetFilesystem = require('@joplin/lib/SyncTargetFilesystem.js');
const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js');
@ -105,6 +102,9 @@ import ShareService from '@joplin/lib/services/share/ShareService';
import setupNotifications from './utils/setupNotifications';
import { loadMasterKeysFromSettings, migrateMasterPassword } from '@joplin/lib/services/e2ee/utils';
import SyncTargetNone from '../lib/SyncTargetNone';
import { setRSA } from '@joplin/lib/services/e2ee/ppk';
import RSA from './services/e2ee/RSA.react-native';
import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
let storeDispatch = function(_action: any) {};
@ -470,6 +470,8 @@ async function initialize(dispatch: Function) {
AlarmService.setDriver(new AlarmServiceDriver(mainLogger));
AlarmService.setLogger(mainLogger);
setRSA(RSA);
try {
if (Setting.value('env') == 'prod') {
await db.open({ name: 'joplin.sqlite' });
@ -637,6 +639,34 @@ async function initialize(dispatch: Function) {
// and it cannot collect anything when the app is not active.
RevisionService.instance().runInBackground(1000 * 30);
// ----------------------------------------------------------------------------
// Keep this below to test react-native-rsa-native
// ----------------------------------------------------------------------------
// const testData = await createTestData();
// await checkTestData(testData);
// const testData = {
// "publicKey": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAoMx9NBioka8DUjO3bKrWMn8uJ23LH1xySogQFR6yh6qbl6i5LKTw\nPgqvv55FUuQtYTMtUTVLggYQhdCBvwbBrD1OqO4xU6Ew7x5/TQKPV3MSgYaps3FF\nOdipC4FyA00jBe6Z1CIpL+ZaSnvjDbMUf5lW8bmfRuXfdBGAcdSBjqm9ttajOws+\n7BBSQ9nI5dnBnWRIVEUb7e9bulgANzM1LMUOE+gaef7T3uKzc+Cx3BhHgw1JdFbL\nZAndYtP52KI5N3oiFM4II26DxmDrO1tQokNM88l5xT0BXPhYiEl1CeBpo5VHZBY2\nRHr4MM/OyAXSUdulsDzbntpE+Y85zv7gpQIDAQAB\n-----END RSA PUBLIC KEY-----",
// "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAoMx9NBioka8DUjO3bKrWMn8uJ23LH1xySogQFR6yh6qbl6i5\nLKTwPgqvv55FUuQtYTMtUTVLggYQhdCBvwbBrD1OqO4xU6Ew7x5/TQKPV3MSgYap\ns3FFOdipC4FyA00jBe6Z1CIpL+ZaSnvjDbMUf5lW8bmfRuXfdBGAcdSBjqm9ttaj\nOws+7BBSQ9nI5dnBnWRIVEUb7e9bulgANzM1LMUOE+gaef7T3uKzc+Cx3BhHgw1J\ndFbLZAndYtP52KI5N3oiFM4II26DxmDrO1tQokNM88l5xT0BXPhYiEl1CeBpo5VH\nZBY2RHr4MM/OyAXSUdulsDzbntpE+Y85zv7gpQIDAQABAoIBAEA0Zmm+ztAcyX6x\nF7RUImLXVV55AHntN9V6rrFAKJjzDl1oCUhCM4sSSUqBr7yBT31YKegbF6M7OK21\nq5jS4dIcSKQ7N4bk/dz8mGfvdby9Pc5qLqhvuex3DkiBzzxyOGHN+64wVbHCkJrd\nDLQTpUOtvoGWVHrCno6Bzn+lEnYbvdr0hqI5H4D0ubk6TYed1/4ZlJf0R/o/4jnl\nou0UG2hpJN4ur506cttkZJSTxLjzdO38JuQIAkCEglrMYVY61lBNPxC11Kr3ZN7o\ncm7gWZVyP26KoU27t/g+2FoiBGsWLqGYiuTaqT6dKZ2vHyJGjJIZZStv5ye2Ez8V\nKQwpjQECgYEA3xtwYu4n/G5UjEMumkXHNd/bDamelo1aQvvjkVvxKeASNBqV8cM0\n6Jb2FCuT9Y3mWbFTM0jpqXehpHUOCCnrPKGKnJ0ZS4/SRIrtw0iM6q17fTAqmuOt\nhX0pJ77Il8lVCtx4ItsW+LUGbm6CwotlYLVUuyluhKe0pGw2yafi2N0CgYEAuIFk\ng4p7x0i1LFAlIP0YQ07bJQ0E6FEWbCfMgrV3VjtbnT99EaqPOHhMasITCuoEFlh8\ncgyZ6oH7GEy4IRWrM+Mlm47S+NTrr6KgnTGf570ZAFuqnJac97oFB7BvlQsQot6F\n0L2JKM7dQKIMlvwA9DoXZdKX/9ykiqqIpawNxmkCgYEAuyJOwAw2ads4+3UWT7wb\nfarIF8ugA3OItAqHNFNEEvWpDx8FigVMCZMl0IFE14AwKCc+PBP6OXTolgLAxEQ0\n1WRB2V9D6kc1/Nvy1guydt0QaU7PTZ+O2hrDPF0f74Cl3jhSZBoUSIO+Yz46W2eE\nnvs5mMsFsirgr9E8myRAd9kCgYAGMCDE4KIiHugkolN8dcCYkU58QaGGgSG1YuhT\nAe8Mr1T1QynYq9W92RsHAZdN6GdWsIUL9iw7VzyqpfgO9AEX7mhWfUXKHqoA6/1j\nCEUKqqbqAikIs2x0SoLcrSgw4XwfWkM2qwSsn7N/9W9iqPUHO+OJALUkWawTEoEe\nvVSA8QKBgQCEYCPnxgeQSZkrv7x5soXzgF1YN5EZRa1mTUqPBubs564ZjIIY66mI\nCTaHl7U1cPAhx7mHkSzP/i5NjjLqPZZNOyawWDEEmOzxX69OIzKImb6mEQNyS3do\nI8jnpN5q9pw5TvuEIYSrGqQVnHeaEjSvcT48W9GuzjNVscGfw76fPg==\n-----END RSA PRIVATE KEY-----",
// "plaintext": "just testing",
// "ciphertext": "BfkKLdrmd2UX4sPf0bzhfqrg3rKwH5DS7dPAqdmoQuHlrvEBrYKqheekwpnWQgGggGcm/orlrsQRwlexLv7jfRbb0bMnElkySMu4w6wTxILB66RX9H3vXCz02SwHKFRcuGJxlzTPUC23ki6f/McYJ2n/2L8qYxBO8fncTKutIWV54jY19RS1wQ4IdVDBqzji8D0QsRxUhVlpRk4qxsVnyuoyg9AyDe91LOYKfRc6NdapFij996nKzjxFcKOdBqpis34fN3Cg7avcs2Dm5vi7zlRhyGqJJhORXTU3x6hVwOBkVAisgaB7xS3lHiYp6Fs5tP3hBd0kFwVVx8gALbHsgg=="
// };
// await checkTestData(testData);
// await printTestData();
// ----------------------------------------------------------------------------
// On desktop and CLI we run various tests to check that node-rsa is working
// as expected. On mobile however we cannot run test units directly on
// device, and that's what would be needed to automatically verify
// react-native-rsa-native. So instead we run the tests every time the
// mobile app is started in dev mode. If there's any regression the below
// call will throw an error, alerting us of the issue. Otherwise it will
// just print some messages in the console.
// ----------------------------------------------------------------------------
if (Setting.value('env') == 'dev') await runIntegrationTests();
reg.logger().info('Application initialized');
}
@ -879,7 +909,7 @@ const mapStateToProps = (state: any) => {
const App = connect(mapStateToProps)(AppComponent);
export default class Root extends React.Component {
render() {
public render() {
return (
<Provider store={store}>
<App/>

View File

@ -0,0 +1,43 @@
import { RSA } from '@joplin/lib/services/e2ee/types';
const RnRSA = require('react-native-rsa-native').RSA;
interface RSAKeyPair {
public: string;
private: string;
}
const rsa: RSA = {
generateKeyPair: async (keySize: number): Promise<RSAKeyPair> => {
const keys: RSAKeyPair = await RnRSA.generateKeys(keySize);
// Sanity check
if (!keys.private) throw new Error('No private key was generated');
if (!keys.public) throw new Error('No public key was generated');
return keys;
},
loadKeys: async (publicKey: string, privateKey: string): Promise<RSAKeyPair> => {
return { public: publicKey, private: privateKey };
},
encrypt: async (plaintextUtf8: string, rsaKeyPair: RSAKeyPair): Promise<string> => {
return RnRSA.encrypt(plaintextUtf8, rsaKeyPair.public);
},
decrypt: async (ciphertextBase64: string, rsaKeyPair: RSAKeyPair): Promise<string> => {
return RnRSA.decrypt(ciphertextBase64, rsaKeyPair.private);
},
publicKey: (rsaKeyPair: RSAKeyPair): string => {
return rsaKeyPair.public;
},
privateKey: (rsaKeyPair: RSAKeyPair): string => {
return rsaKeyPair.private;
},
};
export default rsa;

View File

@ -7,6 +7,7 @@ async function main() {
const mobileDir = `${__dirname}/..`;
await fs.remove(`${mobileDir}/android/.gradle`);
await fs.remove(`${mobileDir}/android/app/build`);
await fs.remove(`${mobileDir}/ios/Pods`);
console.info('To clean the Android build, in some rare cases you might also need to clear the cache in ~/.android and ~/.gradle');
}

View File

@ -9,7 +9,6 @@ import { _, setLocale } from './locale';
import KvStore from './services/KvStore';
import SyncTargetJoplinServer from './SyncTargetJoplinServer';
import SyncTargetOneDrive from './SyncTargetOneDrive';
import { createStore, applyMiddleware, Store } from 'redux';
const { defaultState, stateUtils } = require('./reducer');
import JoplinDatabase from './JoplinDatabase';
@ -53,6 +52,8 @@ const { setAutoFreeze } = require('immer');
import { getEncryptionEnabled } from './services/synchronizer/syncInfoUtils';
import { loadMasterKeysFromSettings, migrateMasterPassword } from './services/e2ee/utils';
import SyncTargetNone from './SyncTargetNone';
import { setRSA } from './services/e2ee/ppk';
import RSA from './services/e2ee/RSA.node';
const appLogger: LoggerWrapper = Logger.create('App');
@ -79,12 +80,12 @@ export default class BaseApplication {
protected store_: Store<any> = null;
constructor() {
public constructor() {
this.eventEmitter_ = new EventEmitter();
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = this.decryptionWorker_resourceMetadataButNotBlobDecrypted.bind(this);
}
async destroy() {
public async destroy() {
if (this.scheduleAutoAddResourcesIID_) {
shim.clearTimeout(this.scheduleAutoAddResourcesIID_);
this.scheduleAutoAddResourcesIID_ = null;
@ -116,7 +117,7 @@ export default class BaseApplication {
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = null;
}
logger(): LoggerWrapper {
public logger(): LoggerWrapper {
return appLogger;
}
@ -124,11 +125,11 @@ export default class BaseApplication {
return this.store_;
}
currentFolder() {
public currentFolder() {
return this.currentFolder_;
}
async refreshCurrentFolder() {
public async refreshCurrentFolder() {
let newFolder = null;
if (this.currentFolder_) newFolder = await Folder.load(this.currentFolder_.id);
@ -137,7 +138,7 @@ export default class BaseApplication {
this.switchCurrentFolder(newFolder);
}
switchCurrentFolder(folder: any) {
public switchCurrentFolder(folder: any) {
if (!this.hasGui()) {
this.currentFolder_ = Object.assign({}, folder);
Setting.setValue('activeFolderId', folder ? folder.id : '');
@ -151,7 +152,7 @@ export default class BaseApplication {
// Handles the initial flags passed to main script and
// returns the remaining args.
async handleStartFlags_(argv: string[], setDefaults: boolean = true) {
private async handleStartFlags_(argv: string[], setDefaults: boolean = true) {
const matched: any = {};
argv = argv.slice(0);
argv.splice(0, 2); // First arguments are the node executable, and the node JS file
@ -276,16 +277,16 @@ export default class BaseApplication {
};
}
on(eventName: string, callback: Function) {
public on(eventName: string, callback: Function) {
return this.eventEmitter_.on(eventName, callback);
}
async exit(code = 0) {
public async exit(code = 0) {
await Setting.saveAll();
process.exit(code);
}
async refreshNotes(state: any, useSelectedNoteId: boolean = false, noteHash: string = '') {
public async refreshNotes(state: any, useSelectedNoteId: boolean = false, noteHash: string = '') {
let parentType = state.notesParentType;
let parentId = null;
@ -381,13 +382,13 @@ export default class BaseApplication {
}
}
resourceFetcher_downloadComplete(event: any) {
private resourceFetcher_downloadComplete(event: any) {
if (event.encrypted) {
void DecryptionWorker.instance().scheduleStart();
}
}
async decryptionWorker_resourceMetadataButNotBlobDecrypted() {
private async decryptionWorker_resourceMetadataButNotBlobDecrypted() {
ResourceFetcher.instance().scheduleAutoAddResources();
}
@ -403,15 +404,15 @@ export default class BaseApplication {
return o.join(', ');
}
hasGui() {
public hasGui() {
return false;
}
uiType() {
public uiType() {
return this.hasGui() ? 'gui' : 'cli';
}
generalMiddlewareFn() {
public generalMiddlewareFn() {
const middleware = (store: any) => (next: any) => (action: any) => {
return this.generalMiddleware(store, next, action);
};
@ -419,7 +420,7 @@ export default class BaseApplication {
return middleware;
}
async applySettingsSideEffects(action: any = null) {
protected async applySettingsSideEffects(action: any = null) {
const sideEffects: any = {
'dateFormat': async () => {
time.setLocale(Setting.value('locale'));
@ -483,7 +484,7 @@ export default class BaseApplication {
}
}
async generalMiddleware(store: any, next: any, action: any) {
protected async generalMiddleware(store: any, next: any, action: any) {
// appLogger.debug('Reducer action', this.reducerActionToString(action));
const result = next(action);
@ -619,15 +620,15 @@ export default class BaseApplication {
return result;
}
dispatch(action: any) {
public dispatch(action: any) {
if (this.store()) return this.store().dispatch(action);
}
reducer(state: any = defaultState, action: any) {
public reducer(state: any = defaultState, action: any) {
return reducer(state, action);
}
initRedux() {
public initRedux() {
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn() as any));
setStore(this.store_);
BaseModel.dispatch = this.store().dispatch;
@ -639,7 +640,7 @@ export default class BaseApplication {
ShareService.instance().initialize(this.store());
}
deinitRedux() {
public deinitRedux() {
this.store_ = null;
BaseModel.dispatch = function() {};
FoldersScreenUtils.dispatch = function() {};
@ -649,7 +650,7 @@ export default class BaseApplication {
ResourceFetcher.instance().dispatch = function() {};
}
async readFlagsFromFile(flagPath: string) {
public async readFlagsFromFile(flagPath: string) {
if (!fs.existsSync(flagPath)) return {};
let flagContent = fs.readFileSync(flagPath, 'utf8');
if (!flagContent) return {};
@ -665,7 +666,7 @@ export default class BaseApplication {
return flags.matched;
}
determineProfileDir(initArgs: any) {
public determineProfileDir(initArgs: any) {
let output = '';
if (initArgs.profileDir) {
@ -679,7 +680,7 @@ export default class BaseApplication {
return toSystemSlashes(output, 'linux');
}
async start(argv: string[], options: StartOptions = null): Promise<any> {
public async start(argv: string[], options: StartOptions = null): Promise<any> {
options = {
keychainEnabled: true,
...options,
@ -774,6 +775,8 @@ export default class BaseApplication {
reg.setDb(this.database_);
BaseModel.setDb(this.database_);
setRSA(RSA);
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
await migrateMasterPassword();
await handleSyncStartupOperation();

View File

@ -900,6 +900,11 @@ export default class JoplinDatabase extends Database {
queries.push('ALTER TABLE `notes` ADD COLUMN conflict_original_id TEXT NOT NULL DEFAULT ""');
}
// if (targetVersion == 40) {
// queries.push('ALTER TABLE `folders` ADD COLUMN master_key_id TEXT NOT NULL DEFAULT ""');
// queries.push('ALTER TABLE `notes` ADD COLUMN master_key_id TEXT NOT NULL DEFAULT ""');
// }
const updateVersionQuery = { sql: 'UPDATE version SET version = ?', params: [targetVersion] };
queries.push(updateVersionQuery);

View File

@ -22,8 +22,9 @@ import TaskQueue from './TaskQueue';
import ItemUploader from './services/synchronizer/ItemUploader';
import { FileApi } from './file-api';
import JoplinDatabase from './JoplinDatabase';
import { fetchSyncInfo, getActiveMasterKey, localSyncInfo, mergeSyncInfos, saveLocalSyncInfo, syncInfoEquals, uploadSyncInfo } from './services/synchronizer/syncInfoUtils';
import { setupAndDisableEncryption, setupAndEnableEncryption } from './services/e2ee/utils';
import { fetchSyncInfo, getActiveMasterKey, localSyncInfo, mergeSyncInfos, saveLocalSyncInfo, SyncInfo, syncInfoEquals, uploadSyncInfo } from './services/synchronizer/syncInfoUtils';
import { getMasterPassword, setupAndDisableEncryption, setupAndEnableEncryption } from './services/e2ee/utils';
import { generateKeyPair } from './services/e2ee/ppk';
const { sprintf } = require('sprintf-js');
const { Dirnames } = require('./services/synchronizer/utils/types');
@ -321,6 +322,16 @@ export default class Synchronizer {
return '';
}
private async setPpkIfNotExist(localInfo: SyncInfo, remoteInfo: SyncInfo) {
if (localInfo.ppk || remoteInfo.ppk) return localInfo;
const password = getMasterPassword(false);
if (!password) return localInfo;
localInfo.ppk = await generateKeyPair(this.encryptionService(), password);
return localInfo;
}
private async apiCall(fnName: string, ...args: any[]) {
if (this.syncTargetIsLocked_) throw new JoplinError('Sync target is locked - aborting API call', 'lockError');
@ -420,49 +431,52 @@ export default class Synchronizer {
this.api().setTempDirName(Dirnames.Temp);
try {
const remoteInfo = await fetchSyncInfo(this.api());
let remoteInfo = await fetchSyncInfo(this.api());
logger.info('Sync target remote info:', remoteInfo);
if (!remoteInfo.version) {
logger.info('Sync target is new - setting it up...');
await this.migrationHandler().upgrade(Setting.value('syncVersion'));
} else {
logger.info('Sync target is already setup - checking it...');
remoteInfo = await fetchSyncInfo(this.api());
}
await this.migrationHandler().checkCanSync(remoteInfo);
logger.info('Sync target is already setup - checking it...');
const localInfo = await localSyncInfo();
await this.migrationHandler().checkCanSync(remoteInfo);
logger.info('Sync target local info:', localInfo);
let localInfo = await localSyncInfo();
// console.info('LOCAL', localInfo);
// console.info('REMOTE', remoteInfo);
logger.info('Sync target local info:', localInfo);
if (!syncInfoEquals(localInfo, remoteInfo)) {
const newInfo = mergeSyncInfos(localInfo, remoteInfo);
const previousE2EE = localInfo.e2ee;
logger.info('Sync target info differs between local and remote - merging infos: ', newInfo.toObject());
localInfo = await this.setPpkIfNotExist(localInfo, remoteInfo);
await this.lockHandler().acquireLock(LockType.Exclusive, this.appType_, this.clientId_, { clearExistingSyncLocksFromTheSameClient: true });
await uploadSyncInfo(this.api(), newInfo);
await saveLocalSyncInfo(newInfo);
await this.lockHandler().releaseLock(LockType.Exclusive, this.appType_, this.clientId_);
// console.info('LOCAL', localInfo);
// console.info('REMOTE', remoteInfo);
// console.info('NEW', newInfo);
if (!syncInfoEquals(localInfo, remoteInfo)) {
const newInfo = mergeSyncInfos(localInfo, remoteInfo);
const previousE2EE = localInfo.e2ee;
logger.info('Sync target info differs between local and remote - merging infos: ', newInfo.toObject());
if (newInfo.e2ee !== previousE2EE) {
if (newInfo.e2ee) {
const mk = getActiveMasterKey(newInfo);
await setupAndEnableEncryption(this.encryptionService(), mk);
} else {
await setupAndDisableEncryption(this.encryptionService());
}
await this.lockHandler().acquireLock(LockType.Exclusive, this.appType_, this.clientId_, { clearExistingSyncLocksFromTheSameClient: true });
await uploadSyncInfo(this.api(), newInfo);
await saveLocalSyncInfo(newInfo);
await this.lockHandler().releaseLock(LockType.Exclusive, this.appType_, this.clientId_);
// console.info('NEW', newInfo);
if (newInfo.e2ee !== previousE2EE) {
if (newInfo.e2ee) {
const mk = getActiveMasterKey(newInfo);
await setupAndEnableEncryption(this.encryptionService(), mk);
} else {
await setupAndDisableEncryption(this.encryptionService());
}
} else {
// Set it to remote anyway so that timestamps are the same
// Note: that's probably not needed anymore?
// await uploadSyncInfo(this.api(), remoteInfo);
}
} else {
// Set it to remote anyway so that timestamps are the same
// Note: that's probably not needed anymore?
// await uploadSyncInfo(this.api(), remoteInfo);
}
} catch (error) {
if (error.code === 'outdatedSyncTarget') {

View File

@ -1,11 +1,13 @@
// AUTO-GENERATED using `gulp buildCommandIndex`
import * as historyBackward from './historyBackward';
import * as historyForward from './historyForward';
import * as openMasterPasswordDialog from './openMasterPasswordDialog';
import * as synchronize from './synchronize';
const index:any[] = [
historyBackward,
historyForward,
openMasterPasswordDialog,
synchronize,
];

View File

@ -0,0 +1,20 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
import { _ } from '../locale';
export const declaration: CommandDeclaration = {
name: 'openMasterPasswordDialog',
label: () => _('Manage master password...'),
iconName: 'fas fa-key',
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, isOpen: boolean = true) => {
context.dispatch({
type: 'DIALOG_OPEN',
name: 'masterPassword',
isOpen: isOpen,
});
},
};
};

View File

@ -3,8 +3,8 @@ import { _ } from '../../locale';
import BaseItem, { EncryptedItemsStats } from '../../models/BaseItem';
import useAsyncEffect, { AsyncEffectEvent } from '../../hooks/useAsyncEffect';
import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time';
import { findMasterKeyPassword } from '../../services/e2ee/utils';
// import time from '../../time';
import { findMasterKeyPassword, getMasterPasswordStatus, masterPasswordIsValid, MasterPasswordStatus } from '../../services/e2ee/utils';
import EncryptionService from '../../services/e2ee/EncryptionService';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import MasterKey from '../../models/MasterKey';
@ -21,7 +21,10 @@ export const useStats = () => {
useAsyncEffect(async (event: AsyncEffectEvent) => {
const r = await BaseItem.encryptedItemsStats();
if (event.cancelled) return;
setStats(r);
setStats(stats => {
if (JSON.stringify(stats) === JSON.stringify(r)) return stats;
return r;
});
}, [statsUpdateTime]);
useEffect(() => {
@ -30,7 +33,7 @@ export const useStats = () => {
}, 3000);
return () => {
clearInterval(iid);
shim.clearInterval(iid);
};
}, []);
@ -44,20 +47,18 @@ export const decryptedStatText = (stats: EncryptedItemsStats) => {
return result;
};
export const enableEncryptionConfirmationMessages = (masterKey: MasterKeyEntity) => {
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
return msg;
};
export const enableEncryptionConfirmationMessages = (_masterKey: MasterKeyEntity, hasMasterPassword: boolean) => {
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target.')];
const masterPasswordIsValid = async (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string = null) => {
const activeMasterKey = masterKeys.find((mk: MasterKeyEntity) => mk.id === activeMasterKeyId);
masterPassword = masterPassword === null ? masterPassword : masterPassword;
if (activeMasterKey && masterPassword) {
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
if (hasMasterPassword) {
msg.push(_('To continue, please enter your master password below.'));
} else {
msg.push(_('Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.'));
}
return false;
// if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
return msg;
};
export const reencryptData = async () => {
@ -107,7 +108,7 @@ export const useInputMasterPassword = (masterKeys: MasterKeyEntity[], activeMast
const onMasterPasswordSave = useCallback(async () => {
Setting.setValue('encryption.masterPassword', inputMasterPassword);
if (!(await masterPasswordIsValid(masterKeys, activeMasterKeyId, inputMasterPassword))) {
if (!(await masterPasswordIsValid(inputMasterPassword, masterKeys.find(mk => mk.id === activeMasterKeyId)))) {
alert('Password is invalid. Please try again.');
}
}, [inputMasterPassword]);
@ -141,6 +142,7 @@ export const useInputPasswords = (propsPasswords: Record<string, string>) => {
export const usePasswordChecker = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string, passwords: Record<string, string>) => {
const [passwordChecks, setPasswordChecks] = useState<PasswordChecks>({});
const [masterPasswordKeys, setMasterPasswordKeys] = useState<PasswordChecks>({});
const [masterPasswordStatus, setMasterPasswordStatus] = useState<MasterPasswordStatus>(MasterPasswordStatus.Unknown);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const newPasswordChecks: PasswordChecks = {};
@ -154,15 +156,25 @@ export const usePasswordChecker = (masterKeys: MasterKeyEntity[], activeMasterKe
newMasterPasswordKeys[mk.id] = password === masterPassword;
}
newPasswordChecks['master'] = await masterPasswordIsValid(masterKeys, activeMasterKeyId, masterPassword);
newPasswordChecks['master'] = masterPassword ? await masterPasswordIsValid(masterPassword, masterKeys.find(mk => mk.id === activeMasterKeyId)) : true;
if (event.cancelled) return;
setPasswordChecks(newPasswordChecks);
setMasterPasswordKeys(newMasterPasswordKeys);
setPasswordChecks(passwordChecks => {
if (JSON.stringify(newPasswordChecks) === JSON.stringify(passwordChecks)) return passwordChecks;
return newPasswordChecks;
});
setMasterPasswordKeys(masterPasswordKeys => {
if (JSON.stringify(newMasterPasswordKeys) === JSON.stringify(masterPasswordKeys)) return masterPasswordKeys;
console.info('====', JSON.stringify(newMasterPasswordKeys), JSON.stringify(masterPasswordKeys));
return newMasterPasswordKeys;
});
setMasterPasswordStatus(await getMasterPasswordStatus(masterPassword));
}, [masterKeys, masterPassword]);
return { passwordChecks, masterPasswordKeys };
return { passwordChecks, masterPasswordKeys, masterPasswordStatus };
};
export const upgradeMasterKey = async (masterKey: MasterKeyEntity, passwordChecks: PasswordChecks, passwords: Record<string, string>): Promise<string> => {
@ -173,7 +185,7 @@ export const upgradeMasterKey = async (masterKey: MasterKeyEntity, passwordCheck
try {
const password = passwords[masterKey.id];
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
const newMasterKey = await EncryptionService.instance().reencryptMasterKey(masterKey, password, password);
await MasterKey.save(newMasterKey);
void reg.waitForSyncFinishedThenSync();
return _('The master key has been upgraded successfully!');

View File

@ -28,8 +28,8 @@ export default class MasterKey extends BaseItem {
return output;
}
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) {
return masterKeys.filter(m => m.encryption_method !== method);
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], methods: number[]) {
return masterKeys.filter(m => !methods.includes(m.encryption_method));
}
public static async all(): Promise<MasterKeyEntity[]> {

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/node-rsa": "^1.1.1",
"@types/react": "^17.0.20",
"clean-html": "^1.5.0",
"jest": "^26.6.3",
@ -64,6 +65,7 @@
"node-fetch": "^1.7.1",
"node-notifier": "^8.0.0",
"node-persist": "^2.1.0",
"node-rsa": "^1.1.1",
"promise": "^7.1.1",
"query-string": "4.3.4",
"re-reselect": "^4.0.0",

View File

@ -4,7 +4,7 @@ import Note from '../../models/Note';
import Setting from '../../models/Setting';
import BaseItem from '../../models/BaseItem';
import MasterKey from '../../models/MasterKey';
import EncryptionService from './EncryptionService';
import EncryptionService, { EncryptionMethod } from './EncryptionService';
import { setEncryptionEnabled } from '../synchronizer/syncInfoUtils';
let service: EncryptionService = null;
@ -22,7 +22,7 @@ describe('services_EncryptionService', function() {
it('should encode and decode header', (async () => {
const header = {
encryptionMethod: EncryptionService.METHOD_SJCL,
encryptionMethod: EncryptionMethod.SJCL,
masterKeyId: '01234568abcdefgh01234568abcdefgh',
};
@ -39,33 +39,33 @@ describe('services_EncryptionService', function() {
let hasThrown = false;
try {
await service.decryptMasterKey_(masterKey, 'wrongpassword');
await service.decryptMasterKeyContent(masterKey, 'wrongpassword');
} catch (error) {
hasThrown = true;
}
expect(hasThrown).toBe(true);
const decryptedMasterKey = await service.decryptMasterKey_(masterKey, '123456');
const decryptedMasterKey = await service.decryptMasterKeyContent(masterKey, '123456');
expect(decryptedMasterKey.length).toBe(512);
}));
it('should upgrade a master key', (async () => {
// Create an old style master key
let masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
encryptionMethod: EncryptionMethod.SJCL2,
});
masterKey = await MasterKey.save(masterKey);
let upgradedMasterKey = await service.upgradeMasterKey(masterKey, '123456');
let upgradedMasterKey = await service.reencryptMasterKey(masterKey, '123456', '123456');
upgradedMasterKey = await MasterKey.save(upgradedMasterKey);
// Check that master key has been upgraded (different ciphertext)
expect(masterKey.content).not.toBe(upgradedMasterKey.content);
// Check that master key plain text is still the same
const plainTextOld = await service.decryptMasterKey_(masterKey, '123456');
const plainTextNew = await service.decryptMasterKey_(upgradedMasterKey, '123456');
const plainTextOld = await service.decryptMasterKeyContent(masterKey, '123456');
const plainTextNew = await service.decryptMasterKeyContent(upgradedMasterKey, '123456');
expect(plainTextOld).toBe(plainTextNew);
// Check that old content can be decrypted with new master key
@ -81,15 +81,15 @@ describe('services_EncryptionService', function() {
it('should not upgrade master key if invalid password', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
encryptionMethod: EncryptionMethod.SJCL2,
});
await checkThrowAsync(async () => await service.upgradeMasterKey(masterKey, '777'));
await checkThrowAsync(async () => await service.reencryptMasterKey(masterKey, '777', '777'));
}));
it('should require a checksum only for old master keys', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
encryptionMethod: EncryptionMethod.SJCL2,
});
expect(!!masterKey.checksum).toBe(true);
@ -98,33 +98,33 @@ describe('services_EncryptionService', function() {
it('should not require a checksum for new master keys', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_4,
encryptionMethod: EncryptionMethod.SJCL4,
});
expect(!masterKey.checksum).toBe(true);
expect(!!masterKey.content).toBe(true);
const decryptedMasterKey = await service.decryptMasterKey_(masterKey, '123456');
const decryptedMasterKey = await service.decryptMasterKeyContent(masterKey, '123456');
expect(decryptedMasterKey.length).toBe(512);
}));
it('should throw an error if master key decryption fails', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_4,
encryptionMethod: EncryptionMethod.SJCL4,
});
const hasThrown = await checkThrowAsync(async () => await service.decryptMasterKey_(masterKey, 'wrong'));
const hasThrown = await checkThrowAsync(async () => await service.decryptMasterKeyContent(masterKey, 'wrong'));
expect(hasThrown).toBe(true);
}));
it('should return the master keys that need an upgrade', (async () => {
const masterKey1 = await MasterKey.save(await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
encryptionMethod: EncryptionMethod.SJCL2,
}));
const masterKey2 = await MasterKey.save(await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL,
encryptionMethod: EncryptionMethod.SJCL,
}));
await MasterKey.save(await service.generateMasterKey('123456'));
@ -164,22 +164,22 @@ describe('services_EncryptionService', function() {
{
const cipherText = await service.encryptString('some secret', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
encryptionMethod: EncryptionMethod.SJCL2,
});
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('some secret');
const header = await service.decodeHeaderString(cipherText);
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_2);
expect(header.encryptionMethod).toBe(EncryptionMethod.SJCL2);
}
{
const cipherText = await service.encryptString('some secret', {
encryptionMethod: EncryptionService.METHOD_SJCL_3,
encryptionMethod: EncryptionMethod.SJCL3,
});
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('some secret');
const header = await service.decodeHeaderString(cipherText);
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_3);
expect(header.encryptionMethod).toBe(EncryptionMethod.SJCL3);
}
}));
@ -267,12 +267,12 @@ describe('services_EncryptionService', function() {
await service.loadMasterKey(masterKey, '123456', true);
// First check that we can replicate the error with the old encryption method
service.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL;
service.defaultEncryptionMethod_ = EncryptionMethod.SJCL;
const hasThrown = await checkThrowAsync(async () => await service.encryptString('🐶🐶🐶'.substr(0,5)));
expect(hasThrown).toBe(true);
// Now check that the new one fixes the problem
service.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
service.defaultEncryptionMethod_ = EncryptionMethod.SJCL1a;
const cipherText = await service.encryptString('🐶🐶🐶'.substr(0,5));
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('🐶🐶🐶'.substr(0,5));
@ -293,4 +293,5 @@ describe('services_EncryptionService', function() {
masterKey = await MasterKey.save(masterKey);
expect(service.isMasterKeyLoaded(masterKey)).toBe(false);
}));
});

View File

@ -25,16 +25,32 @@ interface DecryptedMasterKey {
plainText: string;
}
export interface EncryptionCustomHandler {
context?: any;
encrypt(context: any, hexaBytes: string, password: string): Promise<string>;
decrypt(context: any, hexaBytes: string, password: string): Promise<string>;
}
export enum EncryptionMethod {
SJCL = 1,
SJCL2 = 2,
SJCL3 = 3,
SJCL4 = 4,
SJCL1a = 5,
Custom = 6,
}
export interface EncryptOptions {
encryptionMethod?: EncryptionMethod;
onProgress?: Function;
encryptionHandler?: EncryptionCustomHandler;
masterKeyId?: string;
}
export default class EncryptionService {
public static instance_: EncryptionService = null;
public static METHOD_SJCL_2 = 2;
public static METHOD_SJCL_3 = 3;
public static METHOD_SJCL_4 = 4;
public static METHOD_SJCL_1A = 5;
public static METHOD_SJCL = 1;
public static fsDriver_: any = null;
// Note: 1 MB is very slow with Node and probably even worse on mobile.
@ -52,8 +68,8 @@ export default class EncryptionService {
// changed easily since the chunk size is incorporated into the encrypted data.
private chunkSize_ = 5000;
private decryptedMasterKeys_: Record<string, DecryptedMasterKey> = {};
public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; // public because used in tests
private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
public defaultEncryptionMethod_ = EncryptionMethod.SJCL1a; // public because used in tests
private defaultMasterKeyEncryptionMethod_ = EncryptionMethod.SJCL4;
private headerTemplates_ = {
// Template version 1
@ -79,8 +95,8 @@ export default class EncryptionService {
// changed easily since the chunk size is incorporated into the encrypted data.
this.chunkSize_ = 5000;
this.decryptedMasterKeys_ = {};
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
this.defaultEncryptionMethod_ = EncryptionMethod.SJCL1a;
this.defaultMasterKeyEncryptionMethod_ = EncryptionMethod.SJCL4;
this.headerTemplates_ = {
// Template version 1
@ -97,6 +113,10 @@ export default class EncryptionService {
return this.instance_;
}
public get defaultMasterKeyEncryptionMethod() {
return this.defaultMasterKeyEncryptionMethod_;
}
loadedMasterKeysCount() {
return Object.keys(this.decryptedMasterKeys_).length;
}
@ -135,7 +155,7 @@ export default class EncryptionService {
logger.info(`Loading master key: ${model.id}. Make active:`, makeActive);
this.decryptedMasterKeys_[model.id] = {
plainText: await this.decryptMasterKey_(model, password),
plainText: await this.decryptMasterKeyContent(model, password),
updatedTime: model.updated_time,
};
@ -175,7 +195,7 @@ export default class EncryptionService {
return await this.randomHexString(64);
}
async randomHexString(byteCount: number) {
private async randomHexString(byteCount: number) {
const bytes: any[] = await shim.randomBytes(byteCount);
return bytes
.map(a => {
@ -184,32 +204,39 @@ export default class EncryptionService {
.join('');
}
masterKeysThatNeedUpgrading(masterKeys: MasterKeyEntity[]) {
const output = MasterKey.allWithoutEncryptionMethod(masterKeys, this.defaultMasterKeyEncryptionMethod_);
// Anything below 5 is a new encryption method and doesn't need an upgrade
return output.filter(mk => mk.encryption_method <= 5);
public masterKeysThatNeedUpgrading(masterKeys: MasterKeyEntity[]) {
return MasterKey.allWithoutEncryptionMethod(masterKeys, [this.defaultMasterKeyEncryptionMethod_, EncryptionMethod.Custom]);
}
async upgradeMasterKey(model: MasterKeyEntity, decryptionPassword: string) {
public async reencryptMasterKey(model: MasterKeyEntity, decryptionPassword: string, encryptionPassword: string, decryptOptions: EncryptOptions = null, encryptOptions: EncryptOptions = null): Promise<MasterKeyEntity> {
const newEncryptionMethod = this.defaultMasterKeyEncryptionMethod_;
const plainText = await this.decryptMasterKey_(model, decryptionPassword);
const newContent = await this.encryptMasterKeyContent_(newEncryptionMethod, plainText, decryptionPassword);
const plainText = await this.decryptMasterKeyContent(model, decryptionPassword, decryptOptions);
const newContent = await this.encryptMasterKeyContent(newEncryptionMethod, plainText, encryptionPassword, encryptOptions);
return { ...model, ...newContent };
}
async encryptMasterKeyContent_(encryptionMethod: number, hexaBytes: any, password: string): Promise<MasterKeyEntity> {
// Checksum is not necessary since decryption will already fail if data is invalid
const checksum = encryptionMethod === EncryptionService.METHOD_SJCL_2 ? this.sha256(hexaBytes) : '';
const cipherText = await this.encrypt(encryptionMethod, password, hexaBytes);
public async encryptMasterKeyContent(encryptionMethod: EncryptionMethod, hexaBytes: string, password: string, options: EncryptOptions = null): Promise<MasterKeyEntity> {
options = { ...options };
return {
checksum: checksum,
encryption_method: encryptionMethod,
content: cipherText,
};
if (encryptionMethod === null) encryptionMethod = this.defaultMasterKeyEncryptionMethod_;
if (options.encryptionHandler) {
return {
checksum: '',
encryption_method: EncryptionMethod.Custom,
content: await options.encryptionHandler.encrypt(options.encryptionHandler.context, hexaBytes, password),
};
} else {
return {
// Checksum is not necessary since decryption will already fail if data is invalid
checksum: encryptionMethod === EncryptionMethod.SJCL2 ? this.sha256(hexaBytes) : '',
encryption_method: encryptionMethod,
content: await this.encrypt(encryptionMethod, password, hexaBytes),
};
}
}
async generateMasterKeyContent_(password: string, options: any = null) {
private async generateMasterKeyContent_(password: string, options: EncryptOptions = null) {
options = Object.assign({}, {
encryptionMethod: this.defaultMasterKeyEncryptionMethod_,
}, options);
@ -217,10 +244,10 @@ export default class EncryptionService {
const bytes: any[] = await shim.randomBytes(256);
const hexaBytes = bytes.map(a => hexPad(a.toString(16), 2)).join('');
return this.encryptMasterKeyContent_(options.encryptionMethod, hexaBytes, password);
return this.encryptMasterKeyContent(options.encryptionMethod, hexaBytes, password, options);
}
async generateMasterKey(password: string, options: any = null) {
public async generateMasterKey(password: string, options: EncryptOptions = null) {
const model = await this.generateMasterKeyContent_(password, options);
const now = Date.now();
@ -231,9 +258,16 @@ export default class EncryptionService {
return model;
}
public async decryptMasterKey_(model: MasterKeyEntity, password: string): Promise<string> {
public async decryptMasterKeyContent(model: MasterKeyEntity, password: string, options: EncryptOptions = null): Promise<string> {
options = options || {};
if (model.encryption_method === EncryptionMethod.Custom) {
if (!options.encryptionHandler) throw new Error('Master key was encrypted using a custom method, but no encryptionHandler is provided');
return options.encryptionHandler.decrypt(options.encryptionHandler.context, model.content, password);
}
const plainText = await this.decrypt(model.encryption_method, password, model.content);
if (model.encryption_method === EncryptionService.METHOD_SJCL_2) {
if (model.encryption_method === EncryptionMethod.SJCL2) {
const checksum = this.sha256(plainText);
if (checksum !== model.checksum) throw new Error('Could not decrypt master key (checksum failed)');
}
@ -243,7 +277,7 @@ export default class EncryptionService {
public async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
try {
await this.decryptMasterKey_(model, password);
await this.decryptMasterKeyContent(model, password);
} catch (error) {
return false;
}
@ -257,14 +291,14 @@ export default class EncryptionService {
return error;
}
async encrypt(method: number, key: string, plainText: string) {
public async encrypt(method: EncryptionMethod, key: string, plainText: string): Promise<string> {
if (!method) throw new Error('Encryption method is required');
if (!key) throw new Error('Encryption key is required');
const sjcl = shim.sjclModule;
// 2020-01-23: Deprecated and no longer secure due to the use og OCB2 mode - do not use.
if (method === EncryptionService.METHOD_SJCL) {
if (method === EncryptionMethod.SJCL) {
try {
// Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/
return sjcl.json.encrypt(key, plainText, {
@ -283,7 +317,7 @@ export default class EncryptionService {
// 2020-03-06: Added method to fix https://github.com/laurent22/joplin/issues/2591
// Also took the opportunity to change number of key derivations, per Isaac Potoczny's suggestion
if (method === EncryptionService.METHOD_SJCL_1A) {
if (method === EncryptionMethod.SJCL1a) {
try {
// We need to escape the data because SJCL uses encodeURIComponent to process the data and it only
// accepts UTF-8 data, or else it throws an error. And the notes might occasionally contain
@ -304,7 +338,7 @@ export default class EncryptionService {
// 2020-01-23: Deprectated - see above.
// Was used to encrypt master keys
if (method === EncryptionService.METHOD_SJCL_2) {
if (method === EncryptionMethod.SJCL2) {
try {
return sjcl.json.encrypt(key, plainText, {
v: 1,
@ -319,7 +353,7 @@ export default class EncryptionService {
}
}
if (method === EncryptionService.METHOD_SJCL_3) {
if (method === EncryptionMethod.SJCL3) {
try {
// Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/
return sjcl.json.encrypt(key, plainText, {
@ -337,7 +371,7 @@ export default class EncryptionService {
}
// Same as above but more secure (but slower) to encrypt master keys
if (method === EncryptionService.METHOD_SJCL_4) {
if (method === EncryptionMethod.SJCL4) {
try {
return sjcl.json.encrypt(key, plainText, {
v: 1,
@ -355,7 +389,7 @@ export default class EncryptionService {
throw new Error(`Unknown encryption method: ${method}`);
}
async decrypt(method: number, key: string, cipherText: string) {
async decrypt(method: EncryptionMethod, key: string, cipherText: string) {
if (!method) throw new Error('Encryption method is required');
if (!key) throw new Error('Encryption key is required');
@ -365,7 +399,7 @@ export default class EncryptionService {
try {
const output = sjcl.json.decrypt(key, cipherText);
if (method === EncryptionService.METHOD_SJCL_1A) {
if (method === EncryptionMethod.SJCL1a) {
return unescape(output);
} else {
return output;
@ -376,13 +410,13 @@ export default class EncryptionService {
}
}
async encryptAbstract_(source: any, destination: any, options: any = null) {
async encryptAbstract_(source: any, destination: any, options: EncryptOptions = null) {
options = Object.assign({}, {
encryptionMethod: this.defaultEncryptionMethod(),
}, options);
const method = options.encryptionMethod;
const masterKeyId = this.activeMasterKeyId();
const masterKeyId = options.masterKeyId ? options.masterKeyId : this.activeMasterKeyId();
const masterKeyPlainText = this.loadedMasterKey(masterKeyId).plainText;
const header = {
@ -412,7 +446,7 @@ export default class EncryptionService {
}
}
async decryptAbstract_(source: any, destination: any, options: any = null) {
async decryptAbstract_(source: any, destination: any, options: EncryptOptions = null) {
if (!options) options = {};
const header: any = await this.decodeHeaderSource_(source);
@ -489,21 +523,21 @@ export default class EncryptionService {
};
}
async encryptString(plainText: any, options: any = null) {
public async encryptString(plainText: any, options: EncryptOptions = null): Promise<string> {
const source = this.stringReader_(plainText);
const destination = this.stringWriter_();
await this.encryptAbstract_(source, destination, options);
return destination.result();
}
async decryptString(cipherText: any, options: any = null) {
public async decryptString(cipherText: any, options: EncryptOptions = null): Promise<string> {
const source = this.stringReader_(cipherText);
const destination = this.stringWriter_();
await this.decryptAbstract_(source, destination, options);
return destination.data.join('');
}
async encryptFile(srcPath: string, destPath: string, options: any = null) {
async encryptFile(srcPath: string, destPath: string, options: EncryptOptions = null) {
let source = await this.fileReader_(srcPath, 'base64');
let destination = await this.fileWriter_(destPath, 'ascii');
@ -528,7 +562,7 @@ export default class EncryptionService {
await cleanUp();
}
async decryptFile(srcPath: string, destPath: string, options: any = null) {
async decryptFile(srcPath: string, destPath: string, options: EncryptOptions = null) {
let source = await this.fileReader_(srcPath, 'ascii');
let destination = await this.fileWriter_(destPath, 'base64');
@ -617,8 +651,8 @@ export default class EncryptionService {
return output;
}
isValidEncryptionMethod(method: number) {
return [EncryptionService.METHOD_SJCL, EncryptionService.METHOD_SJCL_1A, EncryptionService.METHOD_SJCL_2, EncryptionService.METHOD_SJCL_3, EncryptionService.METHOD_SJCL_4].indexOf(method) >= 0;
isValidEncryptionMethod(method: EncryptionMethod) {
return [EncryptionMethod.SJCL, EncryptionMethod.SJCL1a, EncryptionMethod.SJCL2, EncryptionMethod.SJCL3, EncryptionMethod.SJCL4].indexOf(method) >= 0;
}
async itemIsEncrypted(item: any) {

View File

@ -0,0 +1,53 @@
import { RSA, RSAKeyPair } from './types';
import * as NodeRSA from 'node-rsa';
const nodeRSAOptions: NodeRSA.Options = {
// Must use pkcs1 otherwise any data encrypted with NodeRSA will crash the
// app when decrypted by RN-RSA.
// https://github.com/amitaymolko/react-native-rsa-native/issues/66#issuecomment-932768139
encryptionScheme: 'pkcs1',
};
const rsa: RSA = {
generateKeyPair: async (keySize: number): Promise<RSAKeyPair> => {
const keys = new NodeRSA();
keys.setOptions(nodeRSAOptions);
keys.generateKeyPair(keySize, 65537);
// Sanity check
if (!keys.isPrivate()) throw new Error('No private key was generated');
if (!keys.isPublic()) throw new Error('No public key was generated');
return keys;
},
loadKeys: async (publicKey: string, privateKey: string): Promise<RSAKeyPair> => {
const keys = new NodeRSA();
keys.setOptions(nodeRSAOptions);
// Don't specify the import format, and let it auto-detect because
// react-native-rsa might not create a key in the expected format.
keys.importKey(publicKey);
if (privateKey) keys.importKey(privateKey);
return keys;
},
encrypt: async (plaintextUtf8: string, rsaKeyPair: RSAKeyPair): Promise<string> => {
return rsaKeyPair.encrypt(plaintextUtf8, 'base64', 'utf8');
},
decrypt: async (ciphertextBase64: string, rsaKeyPair: RSAKeyPair): Promise<string> => {
return rsaKeyPair.decrypt(ciphertextBase64, 'utf8');
},
publicKey: (rsaKeyPair: RSAKeyPair): string => {
return rsaKeyPair.exportKey('pkcs1-public-pem');
},
privateKey: (rsaKeyPair: RSAKeyPair): string => {
return rsaKeyPair.exportKey('pkcs1-private-pem');
},
};
export default rsa;

View File

@ -0,0 +1,90 @@
import { afterAllCleanUp, encryptionService, expectNotThrow, expectThrow, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import { decryptPrivateKey, generateKeyPair, ppkDecryptMasterKeyContent, ppkGenerateMasterKey, ppkPasswordIsValid, mkReencryptFromPasswordToPublicKey, mkReencryptFromPublicKeyToPassword } from './ppk';
import { runIntegrationTests } from './ppkTestUtils';
describe('e2ee/ppk', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
afterAll(async () => {
await afterAllCleanUp();
});
it('should create a public private key pair', async () => {
const ppk = await generateKeyPair(encryptionService(), '111111');
const privateKey = await decryptPrivateKey(encryptionService(), ppk.privateKey, '111111');
const publicKey = ppk.publicKey;
expect(privateKey).toContain('BEGIN RSA PRIVATE KEY');
expect(privateKey).toContain('END RSA PRIVATE KEY');
expect(privateKey.length).toBeGreaterThan(350);
expect(publicKey).toContain('BEGIN RSA PUBLIC KEY');
expect(publicKey).toContain('END RSA PUBLIC KEY');
expect(publicKey.length).toBeGreaterThan(350);
});
it('should create different key pairs every time', async () => {
const ppk1 = await generateKeyPair(encryptionService(), '111111');
const ppk2 = await generateKeyPair(encryptionService(), '111111');
const privateKey1 = await decryptPrivateKey(encryptionService(), ppk1.privateKey, '111111');
const privateKey2 = await decryptPrivateKey(encryptionService(), ppk2.privateKey, '111111');
const publicKey1 = ppk1.publicKey;
const publicKey2 = ppk2.publicKey;
expect(privateKey1).not.toBe(privateKey2);
expect(publicKey1).not.toBe(publicKey2);
});
it('should encrypt a master key using PPK', (async () => {
const ppk = await generateKeyPair(encryptionService(), '111111');
const masterKey = await ppkGenerateMasterKey(encryptionService(), ppk, '111111');
const plainText = await ppkDecryptMasterKeyContent(encryptionService(), masterKey, ppk, '111111');
expect(plainText.length).toBeGreaterThan(50); // Just checking it's not empty
expect(plainText).not.toBe(masterKey.content);
}));
it('should check if a PPK password is valid', (async () => {
const ppk = await generateKeyPair(encryptionService(), '111111');
expect(await ppkPasswordIsValid(encryptionService(), ppk, '222')).toBe(false);
expect(await ppkPasswordIsValid(encryptionService(), ppk, '111111')).toBe(true);
await expectThrow(async () => ppkPasswordIsValid(encryptionService(), null, '111111'));
}));
it('should transmit key using a public-private key', (async () => {
// This simulate sending a key from one user to another using
// public-private key encryption. For example used when sharing a
// notebook while E2EE is enabled.
// User 1 generates a master key
const key1 = await encryptionService().generateMasterKey('mk_1111');
// Using user 2 private key, he reencrypts the master key
const ppk2 = await generateKeyPair(encryptionService(), 'ppk_1111');
const ppkEncrypted = await mkReencryptFromPasswordToPublicKey(encryptionService(), key1, 'mk_1111', ppk2);
// Once user 2 gets the master key, he can decrypt it using his private key
const key2 = await mkReencryptFromPublicKeyToPassword(encryptionService(), ppkEncrypted, ppk2, 'ppk_1111', 'mk_2222');
// Once it's done, both users should have the same master key
const plaintext1 = await encryptionService().decryptMasterKeyContent(key1, 'mk_1111');
const plaintext2 = await encryptionService().decryptMasterKeyContent(key2, 'mk_2222');
expect(plaintext1).toBe(plaintext2);
// We should make sure that the keys are also different when encrypted
// since they should be using different passwords.
expect(key1.content).not.toBe(key2.content);
}));
it('should decrypt and encrypt data from different devices', (async () => {
await expectNotThrow(async () => runIntegrationTests(true));
}));
});

View File

@ -0,0 +1,147 @@
import uuid from '../../uuid';
import EncryptionService, { EncryptionCustomHandler, EncryptionMethod } from './EncryptionService';
import { MasterKeyEntity, RSA, RSAKeyPair } from './types';
interface PrivateKey {
encryptionMethod: EncryptionMethod;
ciphertext: string;
}
export type PublicKey = string;
export interface PublicPrivateKeyPair {
id: string;
keySize: number;
publicKey: PublicKey;
privateKey: PrivateKey;
createdTime: number;
}
let rsa_: RSA = null;
export const setRSA = (rsa: RSA) => {
rsa_ = rsa;
};
export const rsa = (): RSA => {
if (!rsa_) throw new Error('RSA handler has not been set!!');
return rsa_;
};
async function encryptPrivateKey(encryptionService: EncryptionService, password: string, plainText: string): Promise<PrivateKey> {
return {
encryptionMethod: EncryptionMethod.SJCL4,
ciphertext: await encryptionService.encrypt(EncryptionMethod.SJCL4, password, plainText),
};
}
export async function decryptPrivateKey(encryptionService: EncryptionService, encryptedKey: PrivateKey, password: string): Promise<string> {
return encryptionService.decrypt(encryptedKey.encryptionMethod, password, encryptedKey.ciphertext);
}
export async function generateKeyPair(encryptionService: EncryptionService, password: string): Promise<PublicPrivateKeyPair> {
const keySize = 2048;
const keyPair = await rsa().generateKeyPair(keySize);
return {
id: uuid.createNano(),
keySize,
privateKey: await encryptPrivateKey(encryptionService, password, rsa().privateKey(keyPair)),
publicKey: rsa().publicKey(keyPair),
createdTime: Date.now(),
};
}
export async function pkReencryptPrivateKey(encryptionService: EncryptionService, ppk: PublicPrivateKeyPair, decryptionPassword: string, encryptionPassword: string): Promise<PublicPrivateKeyPair> {
const decryptedPrivate = await decryptPrivateKey(encryptionService, ppk.privateKey, decryptionPassword);
return {
...ppk,
privateKey: await encryptPrivateKey(encryptionService, encryptionPassword, decryptedPrivate),
};
}
export async function ppkPasswordIsValid(service: EncryptionService, ppk: PublicPrivateKeyPair, password: string): Promise<boolean> {
if (!ppk) throw new Error('PPK is undefined');
try {
await loadPpk(service, ppk, password);
} catch (error) {
return false;
}
return true;
}
async function loadPpk(service: EncryptionService, ppk: PublicPrivateKeyPair, password: string): Promise<RSAKeyPair> {
const privateKeyPlainText = await decryptPrivateKey(service, ppk.privateKey, password);
return rsa().loadKeys(ppk.publicKey, privateKeyPlainText);
}
async function loadPublicKey(publicKey: PublicKey): Promise<RSAKeyPair> {
return rsa().loadKeys(publicKey, '');
}
function ppkEncryptionHandler(ppkId: string, rsaKeyPair: RSAKeyPair): EncryptionCustomHandler {
interface Context {
rsaKeyPair: RSAKeyPair;
ppkId: string;
}
return {
context: {
rsaKeyPair,
ppkId,
},
encrypt: async (context: Context, hexaBytes: string, _password: string): Promise<string> => {
return JSON.stringify({
ppkId: context.ppkId,
ciphertext: await rsa().encrypt(hexaBytes, context.rsaKeyPair),
});
},
decrypt: async (context: Context, ciphertext: string, _password: string): Promise<string> => {
const parsed = JSON.parse(ciphertext);
if (parsed.ppkId !== context.ppkId) throw new Error(`Needs private key ${parsed.ppkId} to decrypt, but using ${context.ppkId}`);
return rsa().decrypt(parsed.ciphertext, context.rsaKeyPair);
},
};
}
// Generates a master key and encrypts it using the provided PPK
export async function ppkGenerateMasterKey(service: EncryptionService, ppk: PublicPrivateKeyPair, password: string): Promise<MasterKeyEntity> {
const nodeRSA = await loadPpk(service, ppk, password);
const handler = ppkEncryptionHandler(ppk.id, nodeRSA);
return service.generateMasterKey('', {
encryptionMethod: EncryptionMethod.Custom,
encryptionHandler: handler,
});
}
// Decrypt the content of a master key that was encrypted using ppkGenerateMasterKey()
export async function ppkDecryptMasterKeyContent(service: EncryptionService, masterKey: MasterKeyEntity, ppk: PublicPrivateKeyPair, password: string): Promise<string> {
const nodeRSA = await loadPpk(service, ppk, password);
const handler = ppkEncryptionHandler(ppk.id, nodeRSA);
return service.decryptMasterKeyContent(masterKey, '', {
encryptionHandler: handler,
});
}
export async function mkReencryptFromPasswordToPublicKey(service: EncryptionService, masterKey: MasterKeyEntity, decryptionPassword: string, encryptionPublicKey: PublicPrivateKeyPair): Promise<MasterKeyEntity> {
const encryptionHandler = ppkEncryptionHandler(encryptionPublicKey.id, await loadPublicKey(encryptionPublicKey.publicKey));
const plainText = await service.decryptMasterKeyContent(masterKey, decryptionPassword);
const newContent = await service.encryptMasterKeyContent(EncryptionMethod.Custom, plainText, '', { encryptionHandler });
return { ...masterKey, ...newContent };
}
export async function mkReencryptFromPublicKeyToPassword(service: EncryptionService, masterKey: MasterKeyEntity, decryptionPpk: PublicPrivateKeyPair, decryptionPassword: string, encryptionPassword: string): Promise<MasterKeyEntity> {
const decryptionHandler = ppkEncryptionHandler(decryptionPpk.id, await loadPpk(service, decryptionPpk, decryptionPassword));
const plainText = await service.decryptMasterKeyContent(masterKey, '', { encryptionHandler: decryptionHandler });
const newContent = await service.encryptMasterKeyContent(null, plainText, encryptionPassword);
return { ...masterKey, ...newContent };
}

View File

@ -0,0 +1,120 @@
import { rsa } from './ppk';
interface TestData {
publicKey: string;
privateKey: string;
plaintext: string;
ciphertext: string;
}
// This is conveninent to quickly generate some data to verify for example that
// react-native-rsa can decrypt data from node-rsa and vice-versa.
export async function createTestData() {
const plaintext = 'just testing';
const keyPair = await rsa().generateKeyPair(2048);
const ciphertext = await rsa().encrypt(plaintext, keyPair);
return {
publicKey: rsa().publicKey(keyPair),
privateKey: rsa().privateKey(keyPair),
plaintext,
ciphertext,
};
}
export async function printTestData() {
console.info(JSON.stringify(await createTestData(), null, '\t'));
}
interface CheckTestDataOptions {
throwOnError?: boolean;
silent?: boolean;
}
export async function checkTestData(data: TestData, options: CheckTestDataOptions = null) {
options = {
throwOnError: false,
silent: false,
...options,
};
// First verify that the data coming from the other app can be decrypted.
const messages: string[] = [];
let hasError = false;
const keyPair = await rsa().loadKeys(data.publicKey, data.privateKey);
const decrypted = await rsa().decrypt(data.ciphertext, keyPair);
if (decrypted !== data.plaintext) {
messages.push('RSA Tests: Data could not be decrypted');
messages.push('RSA Tests: Expected:', data.plaintext);
messages.push('RSA Tests: Got:', decrypted);
hasError = true;
} else {
messages.push('RSA Tests: Data could be decrypted');
}
// Then check that the public key can be used to encrypt new data, and then
// decrypt it with the private key.
{
const encrypted = await rsa().encrypt('something else', keyPair);
const decrypted = await rsa().decrypt(encrypted, keyPair);
if (decrypted !== 'something else') {
messages.push('RSA Tests: Data could not be encrypted, then decrypted');
messages.push('RSA Tests: Expected:', 'something else');
messages.push('RSA Tests: Got:', decrypted);
hasError = true;
} else {
messages.push('RSA Tests: Data could be encrypted then decrypted');
}
}
if (hasError && options.throwOnError) {
throw new Error(`Testing RSA failed: \n${messages.join('\n')}`);
} else {
for (const msg of messages) {
if (hasError) {
console.warn(msg);
} else {
if (!options.silent) console.info(msg);
}
}
}
}
// Data generated on mobile using react-native-rsa-native
const mobileData = {
'publicKey': '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlEVSnwMpmGC+YaRw3B37BP1IBth02OFCrlZjlkn14OijnmQaOKGxhJtthvlVVEOEc50D+MMKZ1mJleER4FnD3CoGHaVZmZRa3wnuTblctF/in0mgywFJ6HlEXngUrWt2TkCnkwg4nP0IKlQ4URBxWGllVbWUgqUs5uAtV4mkrx+Ke68j+suoN8w5BF9WnYJCclDCplUOFx77llw1Z/7O8UjkgbfYKOnwMEpxlO1SVutNQNgD4BOtGn73ai0qjHKq5as8SKJb/ch+uAX95bJHlOOvBrHw718gcbnxkn6PEN3vl4/HbmHFj/V4zxG8ZF82+oTOh6m/HGdPPLpF8e98dQIDAQAB\n-----END PUBLIC KEY-----',
'privateKey': '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAlEVSnwMpmGC+YaRw3B37BP1IBth02OFCrlZjlkn14OijnmQaOKGxhJtthvlVVEOEc50D+MMKZ1mJleER4FnD3CoGHaVZmZRa3wnuTblctF/in0mgywFJ6HlEXngUrWt2TkCnkwg4nP0IKlQ4URBxWGllVbWUgqUs5uAtV4mkrx+Ke68j+suoN8w5BF9WnYJCclDCplUOFx77llw1Z/7O8UjkgbfYKOnwMEpxlO1SVutNQNgD4BOtGn73ai0qjHKq5as8SKJb/ch+uAX95bJHlOOvBrHw718gcbnxkn6PEN3vl4/HbmHFj/V4zxG8ZF82+oTOh6m/HGdPPLpF8e98dQIDAQABAoIBAAl/FScdFz1suNTdKONYQjsUE9hoZbd8Wf57hv5Zt1dT3yLma22EIbAKGm5CKu5uMp4LCPWWXGS5LeA9HZ1+clZ4FJMyg3YcM+PEKZCt1huxZnzoRNWru/WZSsE4NK7UyquBZZo7tRCM/khjw4WhpXjRq01dh2kEtkcFRbItHTCgHgQxf3q+XoflVD9pZVj+EylP8vSodxtP1WkWb7fYOybestlvi8vwNQLoRO5PgFtjC0nOvwGnk6120XpWhP95EMy53iOygG9wfw7pxYTfSPEIQR53EGgiv8jc4WPYKc9SZea/bE0Rkt46/jMo6SpTrVNj5WwoCPwB012+edhlmaECgYEA0Q90zuD7cvjB16iDjdsvyZ0gBxozfgDsVNgPRNf/Rv3ol/Ycn/NcBi8XQKdw8NJXoPJbVbzzRvIbGqZLLgzOngjFJFiDW+7M/W2cwD1HFvDjEGYqtZqbLDWZYG0pX75kAB0YyI4ncelhr6nZMs/RMBIw9DGpoBMmP5CvXfgX6XECgYEAtY/Ava6DUKT93m6Sw9NnWesb60uEttvOCXVWQJfOzSLdbzWOw4IgG2YHE1+w+TQbumdt5tzczacvkW9C2KsPllBHeFtsDSTpe6ecuCzl6Ryv5FLg5JfQIErYje4ifmzm+DirMu4kEdsY1jfYnOYyoEo7OZEKRGttUPTH/wHGIUUCgYB3Y/9OQjf3cc6pzWfLtHg3CI+I3tK3S+mrjnQx2bTEoy6Y0gmI4x8TvQLnfnhGX6mBlcbJUQ4R3yPRdVSL6O56XAHR/uaNsvPIazfQpW4a0Nirvdz4N2IUvktoQQ8WyZEsa3GC34PxTtnlyvbqSLprXIgufMolS6pVNNihrpRhUQKBgC2d6p09xXxzl91VBsbwzJzI94DMvpF69G9n7b3Y5nqf8ebJHA9/GDYKEmkJt9tE/lp9Nh21DD0XbloqDC+H+yiXDv3sal97ELaizDtx/GnvbTn+oMaOZhpW88XlOQFutzFSe6EWODXMSJc5/NCe/cVMIUk7acr6+sJGXiFx/qfJAoGBAKVL/6KDBJqMEyqMs2Y0NpMS2Ia163RPJTiBJoIJYw3KOonaDkjk+7dAeYjGlKjLTWF2yckPbYVXmu9MrREGtIpb5oii5J2lFM5oDr40iZIZ5nBiXQfm/B7/IkpJ7IXOsYeiK+UDKSWW71GFeYtICfKlowolm0jBBS+M8XJjplz2\n-----END RSA PRIVATE KEY-----',
'plaintext': 'just testing',
'ciphertext': 'K+saH0/1Ltnc8GFqy1gKDpIY2o4OVkNFCQGFIp1574kkjLEKIgQpgc/kOdc7EO5m\r\nAN7TKh2zGrvcB9BqMOjsQNeausQzAm+b5fYWVyRHfQ5kf6+ojUO+LMRPxKqNO58m\r\nPw5/6R7zACJn1W9cHolY8+YKAeL+guQmCoD50nEgyZc5+HMRKGZpu+Vh7y562hYu\r\n39KPCcLFzWj7yi+JtbD4bFVcgPLg8T2PXCOqj+fVkAXXdkt8PnHfgf4lbfYojZ1d\r\nge7C1hx4aVrfT7vj2saXU/RrV9MlBDtAFZDynWa+LfMmt56TWCO6yWm5KpckGU/E\r\nfEs7l00aSskIai0ghZSIvQ==',
};
// Data generated on desktop using node-rsa
const desktopData = {
'publicKey': '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAvLb8Lk0UBFEQ2UJsVMgKdbPYExhYqa87diBQiFBJglgNuZVi8/HX\nvpCVcH7BhdQKkA9Mh23SpNcYHR9JrzUTrn9Q21t1uj2J60+bfq1s0BA1wkS/xBPN\nrrLw2yCPpkZzNH8HcLx/MtMaOnOVfl5KqftXROzn+Vo3rrxNprd2ETLAxr+CC6SE\nTJiiP8ovUfH+TKZ3P2nkSyBy4oY24h4HA+wVnj12DspE8CMOXCyBUxlG2ki2c/sK\nDSDla3oEjB8QdpBKhIXD/Bb4MpLHfaby7O/eYjrteB8g6JU01JDsnQoomLe4FdCU\nnYK5sFNUQ89e05lMa3yxZWV3mXAVUi/mFwIDAQAB\n-----END RSA PUBLIC KEY-----',
'privateKey': '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvLb8Lk0UBFEQ2UJsVMgKdbPYExhYqa87diBQiFBJglgNuZVi\n8/HXvpCVcH7BhdQKkA9Mh23SpNcYHR9JrzUTrn9Q21t1uj2J60+bfq1s0BA1wkS/\nxBPNrrLw2yCPpkZzNH8HcLx/MtMaOnOVfl5KqftXROzn+Vo3rrxNprd2ETLAxr+C\nC6SETJiiP8ovUfH+TKZ3P2nkSyBy4oY24h4HA+wVnj12DspE8CMOXCyBUxlG2ki2\nc/sKDSDla3oEjB8QdpBKhIXD/Bb4MpLHfaby7O/eYjrteB8g6JU01JDsnQoomLe4\nFdCUnYK5sFNUQ89e05lMa3yxZWV3mXAVUi/mFwIDAQABAoIBAQCMIm2djEsi8XfL\nfZGoW2u4/7WiaF/ekWtcSp7Cuqv7iJuYhiAW+i21KvRttxLJ6C130ISJxLm5Aqi7\nZ3J2ErnsyEoouf/wLqZuAI19QhcdYgwpmJe2aOZBpktIzSMe3A3Mm8/QnYjvGufN\nI+uNDUPwed3SJwITnjTfIqGe/XlFRtvCIurp7vDbh4kTASpg3M8kjXiznVMncC4D\nWNg0vRnj53zfiwRkxZwMubYa25qR2Kt/S703QJVh/ctccbuZ6GyRbtgBlGuxuwX8\n1aAMBScMBMFtU+Xpb55EgsFu6Snzs6yrFKXMybR6ea31CtzBZvjZdGKO1Yxh2Dlf\n7f1PWg9hAoGBAPpLMPXFIsUtq6iwh1slDXZ+IgIIYgs/JkYvDROFUbp9qrnGcQBi\nIC9Dnf7fYwVnYQ18+gz2Qcjn9e+5Y/4aBPW2PjAYurdBMNGlEEKMbl3Ocad9h8mL\nI2MRBFOpwZaVhm8PJBZkhhfkNouh13KRyr9vS4egTdEBOZGR43GSrZ9fAoGBAMEE\nZVqaTg3jAh5GJBcxKGjz77BN0X5wRkYO9OU4DYuBq+sz+JVytLTMMDTtdxbJy11b\nH3wOuz1SugtouuJZ4hmwfXuj+2AFh28tiBcz6nik2pQYgdgowP7eqXor4T+Nd0mP\nzEqa7+W62pNAVlA0DiR8obPmzKNwBm2OZXxR6wxJAoGAC6T95SFDydqjFtkHoxTp\nOG8L0/5h2VYZyMAdop/cOonoLHZwAW2PQ8OokRgBelnh6Qe8dmfqjZdFGN8OKN87\nBddxszkjTq1IwSglxoLUC6c0IG+1ponDnrNG+UF3kTLpqzcQHb6Vgn0KkJp59ImV\n3iwmXmv10th0vjIEW99QFo8CgYEAkqF/SdwtbdlF06/fXQsIMusV7K7Bdrdee3yD\nSNtTVub0ruK1dvtEEpGIEb1QmixE5TADdCBQ2B5Pnbk7OBemb3OncFU7809f+vLx\nDwdumaZLMvSHN6qGK1kGEPziyn/y3hxyyz52/uP7hp/6skVJdSiFQ4ETdxn0mCf0\nKwSkdpkCgYBDMXv2N3eF2IElZkN+8rQYHqWck1HqTcSnlrHS0i0uPQduQa+K/O+G\nVE2S8htp4/D0Gd3EwuDBIT3vUvnx7YBuMX1fXwU0m7oKKOyQqhxfrt9t7BEh2j5r\n5pvRU7dTKVbua78w1sQQMtYnWUyukBlaF/IpHPi5hwCjmDQR1EJY4w==\n-----END RSA PRIVATE KEY-----',
'plaintext': 'just testing',
'ciphertext': 'PRqiQjxnQMukoYPA9XtlGcgAjwuDJd24GtJ3iO2qhh0HnbPnx3c8ZaGWJyV1ejZCwIWv509js7sCTHtXqeGkZr//Db6oOIyi77VzRwvzPxReHPefF0rX62uMh+zTmQW7KSrFeAvtnpWiDcyynUtwycgrZcQCHZoEmSSyc3cyj09HgqEoSQb0BOc8daR0aXwOpgXsB8ypf3+m23U1gZmIyl0glymTN9h1jopV9dRtw5ufcc4ve/hHKp0gbaT2OaRKOLr6AXmbDGwkF5bsvjV+v4tTkj96OUjoG9qUMQh/JYRMl7mxJriUB3Jc6WHEKRVPQYAIZODfEOy3rkHwWAcYjA==',
};
// This can be used to run integration tests directly on device. It will throw
// an error if something cannot be decrypted, or else print info messages.
export const runIntegrationTests = async (silent: boolean = false) => {
const log = (s: string) => {
if (silent) return;
console.info(s);
};
log('RSA Tests: Running integration tests...');
log('RSA Tests: Decrypting and encrypting using desktop data...');
await checkTestData(desktopData, { silent, throwOnError: true });
log('RSA Tests: Decrypting and encrypting using mobile data...');
await checkTestData(mobileData, { silent, throwOnError: true });
log('RSA Tests: Decrypting and encrypting using local data...');
const newData = await createTestData();
await checkTestData(newData, { silent, throwOnError: true });
};

View File

@ -1,11 +1,25 @@
export interface MasterKeyEntity {
id?: string | null;
created_time?: number;
updated_time?: number;
source_application?: string;
encryption_method?: number;
checksum?: string;
content?: string;
type_?: number;
enabled?: number;
id?: string | null;
created_time?: number;
updated_time?: number;
source_application?: string;
encryption_method?: number;
checksum?: string;
content?: string;
type_?: number;
enabled?: number;
}
export type RSAKeyPair = any; // Depends on implementation
// This is the interface that each platform must implement. Data is passed as
// Base64 encoded because that's what both NodeRSA and react-native-rsa support.
export interface RSA {
generateKeyPair(keySize: number): Promise<RSAKeyPair>;
loadKeys(publicKey: string, privateKey: string): Promise<RSAKeyPair>;
encrypt(plaintextUtf8: string, rsaKeyPair: RSAKeyPair): Promise<string>; // Returns Base64 encoded data
decrypt(ciphertextBase64: string, rsaKeyPair: RSAKeyPair): Promise<string>; // Returns UTF-8 encoded string
publicKey(rsaKeyPair: RSAKeyPair): string;
privateKey(rsaKeyPair: RSAKeyPair): string;
}

View File

@ -1,8 +1,9 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow } from '../../testing/test-utils';
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow, expectThrow, kvStore } from '../../testing/test-utils';
import MasterKey from '../../models/MasterKey';
import { migrateMasterPassword, showMissingMasterKeyMessage } from './utils';
import { localSyncInfo, setActiveMasterKeyId, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
import { migrateMasterPassword, resetMasterPassword, showMissingMasterKeyMessage, updateMasterPassword } from './utils';
import { localSyncInfo, masterKeyById, masterKeyEnabled, setActiveMasterKeyId, setMasterKeyEnabled, setPpk } from '../synchronizer/syncInfoUtils';
import Setting from '../../models/Setting';
import { generateKeyPair, ppkPasswordIsValid } from './ppk';
describe('e2ee/utils', function() {
@ -71,4 +72,80 @@ describe('e2ee/utils', function() {
}
});
it('should update the master password', async () => {
const masterPassword1 = '111111';
const masterPassword2 = '222222';
Setting.setValue('encryption.masterPassword', masterPassword1);
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
const mk2 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
setPpk(await generateKeyPair(encryptionService(), masterPassword1));
await updateMasterPassword(masterPassword1, masterPassword2);
expect(Setting.value('encryption.masterPassword')).toBe(masterPassword2);
expect(await ppkPasswordIsValid(encryptionService(), localSyncInfo().ppk, masterPassword1)).toBe(false);
expect(await ppkPasswordIsValid(encryptionService(), localSyncInfo().ppk, masterPassword2)).toBe(true);
expect(await encryptionService().checkMasterKeyPassword(await MasterKey.load(mk1.id), masterPassword1)).toBe(false);
expect(await encryptionService().checkMasterKeyPassword(await MasterKey.load(mk2.id), masterPassword1)).toBe(false);
expect(await encryptionService().checkMasterKeyPassword(await MasterKey.load(mk1.id), masterPassword2)).toBe(true);
expect(await encryptionService().checkMasterKeyPassword(await MasterKey.load(mk2.id), masterPassword2)).toBe(true);
await expectThrow(async () => updateMasterPassword('wrong', masterPassword1));
});
it('should set and verify master password when a data key exists', async () => {
const password = '111111';
await MasterKey.save(await encryptionService().generateMasterKey(password));
await expectThrow(async () => updateMasterPassword('', 'wrong'));
await expectNotThrow(async () => updateMasterPassword('', password));
expect(Setting.value('encryption.masterPassword')).toBe(password);
});
it('should set and verify master password when a private key exists', async () => {
const password = '111111';
setPpk(await generateKeyPair(encryptionService(), password));
await expectThrow(async () => updateMasterPassword('', 'wrong'));
await expectNotThrow(async () => updateMasterPassword('', password));
expect(Setting.value('encryption.masterPassword')).toBe(password);
});
it('should only set the master password if not already set', async () => {
expect(localSyncInfo().ppk).toBeFalsy();
await updateMasterPassword('', '111111');
expect(Setting.value('encryption.masterPassword')).toBe('111111');
expect(localSyncInfo().ppk).toBeFalsy();
expect(localSyncInfo().masterKeys.length).toBe(0);
});
it('should change the master password even if no key is present', async () => {
await updateMasterPassword('', '111111');
expect(Setting.value('encryption.masterPassword')).toBe('111111');
await updateMasterPassword('111111', '222222');
expect(Setting.value('encryption.masterPassword')).toBe('222222');
});
it('should reset a master password', async () => {
const masterPassword1 = '111111';
const masterPassword2 = '222222';
Setting.setValue('encryption.masterPassword', masterPassword1);
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
const mk2 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
setPpk(await generateKeyPair(encryptionService(), masterPassword1));
const previousPpk = localSyncInfo().ppk;
await resetMasterPassword(encryptionService(), kvStore(), masterPassword2);
expect(masterKeyEnabled(masterKeyById(mk1.id))).toBe(false);
expect(masterKeyEnabled(masterKeyById(mk2.id))).toBe(false);
expect(localSyncInfo().ppk.id).not.toBe(previousPpk.id);
expect(localSyncInfo().ppk.privateKey.ciphertext).not.toBe(previousPpk.privateKey.ciphertext);
expect(localSyncInfo().ppk.publicKey).not.toBe(previousPpk.publicKey);
});
});

View File

@ -4,7 +4,10 @@ import MasterKey from '../../models/MasterKey';
import Setting from '../../models/Setting';
import { MasterKeyEntity } from './types';
import EncryptionService from './EncryptionService';
import { getActiveMasterKey, getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
import { getActiveMasterKey, getActiveMasterKeyId, localSyncInfo, masterKeyEnabled, saveLocalSyncInfo, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
import JoplinError from '../../JoplinError';
import { generateKeyPair, pkReencryptPrivateKey, ppkPasswordIsValid } from './ppk';
import KvStore from '../KvStore';
const logger = Logger.create('e2ee/utils');
@ -73,7 +76,16 @@ export async function generateMasterKeyAndEnableEncryption(service: EncryptionSe
// set. If it is not, we set it from the active master key. It needs to be
// called after the settings have been initialized.
export async function migrateMasterPassword() {
if (Setting.value('encryption.masterPassword')) return; // Already migrated
// Already migrated
if (Setting.value('encryption.masterPassword')) return;
// If a PPK is defined it means the master password has been set at some
// point so no need to run the migration
if (localSyncInfo().ppk) return;
// If a PPK is defined it means the master password has been set at some
// point so no need to run the migration
if (localSyncInfo().ppk) return;
logger.info('Master password is not set - trying to get it from the active master key...');
@ -167,3 +179,141 @@ export function getDefaultMasterKey(): MasterKeyEntity {
}
return mk && masterKeyEnabled(mk) ? mk : null;
}
// Get the master password if set, or throw an exception. This ensures that
// things aren't accidentally encrypted with an empty string. Calling code
// should look for "undefinedMasterPassword" code and prompt for password.
export function getMasterPassword(throwIfNotSet: boolean = true): string {
const password = Setting.value('encryption.masterPassword');
if (!password && throwIfNotSet) throw new JoplinError('Master password is not set', 'undefinedMasterPassword');
return password;
}
// - If both a current and new password is provided, and they are different, it
// means the password is being changed, so all the keys are reencrypted with
// the new password.
// - If the current password is not provided, the master password is simply set
// according to newPassword.
export async function updateMasterPassword(currentPassword: string, newPassword: string) {
if (!newPassword) throw new Error('New password must be set');
if (currentPassword && !(await masterPasswordIsValid(currentPassword))) throw new Error('Master password is not valid. Please try again.');
const needToReencrypt = !!currentPassword && !!newPassword && currentPassword !== newPassword;
if (needToReencrypt) {
const reencryptedMasterKeys: MasterKeyEntity[] = [];
let reencryptedPpk = null;
for (const mk of localSyncInfo().masterKeys) {
try {
reencryptedMasterKeys.push(await EncryptionService.instance().reencryptMasterKey(mk, currentPassword, newPassword));
} catch (error) {
if (!masterKeyEnabled(mk)) continue; // Ignore if the master key is disabled, because the password is probably forgotten
error.message = `Key ${mk.id} could not be reencrypted - this is most likely due to an incorrect password. Please try again. Error was: ${error.message}`;
throw error;
}
}
if (localSyncInfo().ppk) {
try {
reencryptedPpk = await pkReencryptPrivateKey(EncryptionService.instance(), localSyncInfo().ppk, currentPassword, newPassword);
} catch (error) {
error.message = `Private key could not be reencrypted - this is most likely due to an incorrect password. Please try again. Error was: ${error.message}`;
throw error;
}
}
for (const mk of reencryptedMasterKeys) {
await MasterKey.save(mk);
}
if (reencryptedPpk) {
const syncInfo = localSyncInfo();
syncInfo.ppk = reencryptedPpk;
saveLocalSyncInfo(syncInfo);
}
} else {
if (!currentPassword && !(await masterPasswordIsValid(newPassword))) throw new Error('Master password is not valid. Please try again.');
}
Setting.setValue('encryption.masterPassword', newPassword);
}
export async function resetMasterPassword(encryptionService: EncryptionService, kvStore: KvStore, newPassword: string) {
for (const mk of localSyncInfo().masterKeys) {
if (!masterKeyEnabled(mk)) continue;
mk.enabled = 0;
await MasterKey.save(mk);
}
const syncInfo = localSyncInfo();
if (syncInfo.ppk) {
await kvStore.setValue(`oldppk::${Date.now()}`, JSON.stringify(syncInfo.ppk));
syncInfo.ppk = await generateKeyPair(encryptionService, newPassword);
saveLocalSyncInfo(syncInfo);
}
// TODO: Unshare any folder associated with a disabled master key?
Setting.setValue('encryption.masterPassword', newPassword);
}
export enum MasterPasswordStatus {
Unknown = 0,
Loaded = 1,
NotSet = 2,
Invalid = 3,
Valid = 4,
}
export async function getMasterPasswordStatus(password: string = null): Promise<MasterPasswordStatus> {
password = password === null ? getMasterPassword(false) : password;
if (!password) return MasterPasswordStatus.NotSet;
const isValid = await masterPasswordIsValid(password);
return isValid ? MasterPasswordStatus.Valid : MasterPasswordStatus.Invalid;
}
export async function checkHasMasterPasswordEncryptedData(syncInfo: SyncInfo = null): Promise<boolean> {
syncInfo = syncInfo ? syncInfo : localSyncInfo();
return !!syncInfo.ppk || !!syncInfo.masterKeys.length;
}
const masterPasswordStatusMessages = {
[MasterPasswordStatus.Unknown]: 'Checking...',
[MasterPasswordStatus.Loaded]: 'Loaded',
[MasterPasswordStatus.NotSet]: 'Not set',
[MasterPasswordStatus.Valid]: '✓ ' + 'Valid',
[MasterPasswordStatus.Invalid]: '❌ ' + 'Invalid',
};
export function getMasterPasswordStatusMessage(status: MasterPasswordStatus): string {
return masterPasswordStatusMessages[status];
}
export async function masterPasswordIsValid(masterPassword: string, activeMasterKey: MasterKeyEntity = null): Promise<boolean> {
// A valid password is basically one that decrypts the private key, but due
// to backward compatibility not all users have a PPK yet, so we also check
// based on the active master key.
if (!masterPassword) throw new Error('Password is empty');
const ppk = localSyncInfo().ppk;
if (ppk) {
return ppkPasswordIsValid(EncryptionService.instance(), ppk, masterPassword);
}
const masterKey = activeMasterKey ? activeMasterKey : getDefaultMasterKey();
if (masterKey) {
return EncryptionService.instance().checkMasterKeyPassword(masterKey, masterPassword);
}
// If the password has never been set, then whatever password is provided is considered valid.
if (!Setting.value('encryption.masterPassword')) return true;
// There may not be any key to decrypt if the master password has been set,
// but the user has never synchronized. In which case, it's sufficient to
// compare to whatever they've entered earlier.
return Setting.value('encryption.masterPassword') === masterPassword;
}

View File

@ -11,6 +11,7 @@ const input: Theme = {
oddBackgroundColor: '#eeeeee',
color: '#32373F', // For regular text
colorError: 'red',
colorCorrect: 'green',
colorWarn: 'rgb(228,86,0)',
colorWarnUrl: '#155BDA',
colorFaded: '#7C8B9E', // For less important text
@ -64,6 +65,7 @@ const expected = `
--joplin-odd-background-color: #eeeeee;
--joplin-color: #32373F;
--joplin-color-error: red;
--joplin-color-correct: green;
--joplin-color-warn: rgb(228,86,0);
--joplin-color-warn-url: #155BDA;
--joplin-color-faded: #7C8B9E;

View File

@ -0,0 +1,59 @@
import { synchronizerStart, setupDatabaseAndSynchronizer, fileApi, switchClient, loadEncryptionMasterKey } from '../../testing/test-utils';
import Folder from '../../models/Folder';
import { fetchSyncInfo, localSyncInfo, setEncryptionEnabled } from '../synchronizer/syncInfoUtils';
import { EncryptionMethod } from '../e2ee/EncryptionService';
import { updateMasterPassword } from '../e2ee/utils';
describe('Synchronizer.ppk', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
});
it('should not create a public private key pair if not using E2EE', async () => {
await Folder.save({});
expect(localSyncInfo().ppk).toBeFalsy();
await synchronizerStart();
const remoteInfo = await fetchSyncInfo(fileApi());
expect(localSyncInfo().ppk).toBeFalsy();
expect(remoteInfo.ppk).toBeFalsy();
});
it('should create a public private key pair if it does not exist', async () => {
await updateMasterPassword('', '111111');
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
const beforeTime = Date.now();
await Folder.save({});
expect(localSyncInfo().ppk).toBeFalsy();
await synchronizerStart();
const remoteInfo = await fetchSyncInfo(fileApi());
expect(localSyncInfo().ppk).toBeTruthy();
expect(remoteInfo.ppk).toBeTruthy();
const clientLocalPPK1 = localSyncInfo().ppk;
expect(clientLocalPPK1.createdTime).toBeGreaterThanOrEqual(beforeTime);
expect(clientLocalPPK1.privateKey.encryptionMethod).toBe(EncryptionMethod.SJCL4);
// Rather arbitrary length check - it's just to make sure there's
// something there. Other tests should ensure the content is valid or
// not.
expect(clientLocalPPK1.privateKey.ciphertext.length).toBeGreaterThan(320);
expect(clientLocalPPK1.publicKey.length).toBeGreaterThan(320);
await switchClient(2);
expect(localSyncInfo().ppk).toBeFalsy();
await synchronizerStart();
expect(localSyncInfo().ppk).toBeTruthy();
const clientLocalPPK2 = localSyncInfo().ppk;
expect(clientLocalPPK1.privateKey.ciphertext).toBe(clientLocalPPK2.privateKey.ciphertext);
expect(clientLocalPPK1.publicKey).toBe(clientLocalPPK2.publicKey);
});
});

View File

@ -2,6 +2,7 @@ import { FileApi } from '../../file-api';
import JoplinDatabase from '../../JoplinDatabase';
import Setting from '../../models/Setting';
import { State } from '../../reducer';
import { PublicPrivateKeyPair } from '../e2ee/ppk';
import { MasterKeyEntity } from '../e2ee/types';
export interface SyncInfoValueBoolean {
@ -14,6 +15,11 @@ export interface SyncInfoValueString {
updatedTime: number;
}
export interface SyncInfoValuePublicPrivateKeyPair {
value: PublicPrivateKeyPair;
updatedTime: number;
}
export async function migrateLocalSyncInfo(db: JoplinDatabase) {
if (Setting.value('syncInfoCache')) return; // Already initialized
@ -88,6 +94,7 @@ export function mergeSyncInfos(s1: SyncInfo, s2: SyncInfo): SyncInfo {
output.setWithTimestamp(s1.keyTimestamp('e2ee') > s2.keyTimestamp('e2ee') ? s1 : s2, 'e2ee');
output.setWithTimestamp(s1.keyTimestamp('activeMasterKeyId') > s2.keyTimestamp('activeMasterKeyId') ? s1 : s2, 'activeMasterKeyId');
output.setWithTimestamp(s1.keyTimestamp('ppk') > s2.keyTimestamp('ppk') ? s1 : s2, 'ppk');
output.version = s1.version > s2.version ? s1.version : s2.version;
output.masterKeys = s1.masterKeys.slice();
@ -115,10 +122,12 @@ export class SyncInfo {
private e2ee_: SyncInfoValueBoolean;
private activeMasterKeyId_: SyncInfoValueString;
private masterKeys_: MasterKeyEntity[] = [];
private ppk_: SyncInfoValuePublicPrivateKeyPair;
public constructor(serialized: string = null) {
this.e2ee_ = { value: false, updatedTime: 0 };
this.activeMasterKeyId_ = { value: '', updatedTime: 0 };
this.ppk_ = { value: null, updatedTime: 0 };
if (serialized) this.load(serialized);
}
@ -129,6 +138,7 @@ export class SyncInfo {
e2ee: this.e2ee_,
activeMasterKeyId: this.activeMasterKeyId_,
masterKeys: this.masterKeys,
ppk: this.ppk_,
};
}
@ -142,6 +152,7 @@ export class SyncInfo {
this.e2ee_ = 'e2ee' in s ? s.e2ee : { value: false, updatedTime: 0 };
this.activeMasterKeyId_ = 'activeMasterKeyId' in s ? s.activeMasterKeyId : { value: '', updatedTime: 0 };
this.masterKeys_ = 'masterKeys' in s ? s.masterKeys : [];
this.ppk_ = 'ppk' in s ? s.ppk : { value: null, updatedTime: 0 };
}
public setWithTimestamp(fromSyncInfo: SyncInfo, propName: string) {
@ -161,6 +172,16 @@ export class SyncInfo {
this.version_ = v;
}
public get ppk() {
return this.ppk_.value;
}
public set ppk(v: PublicPrivateKeyPair) {
if (v === this.ppk_.value) return;
this.ppk_ = { value: v, updatedTime: Date.now() };
}
public get e2ee(): boolean {
return this.e2ee_.value;
}
@ -257,3 +278,21 @@ export function masterKeyEnabled(mk: MasterKeyEntity): boolean {
if ('enabled' in mk) return !!mk.enabled;
return true;
}
export function addMasterKey(syncInfo: SyncInfo, masterKey: MasterKeyEntity) {
// Sanity check - because shouldn't happen
if (syncInfo.masterKeys.find(mk => mk.id === masterKey.id)) throw new Error('Master key is already present');
syncInfo.masterKeys.push(masterKey);
saveLocalSyncInfo(syncInfo);
}
export function setPpk(ppk: PublicPrivateKeyPair) {
const syncInfo = localSyncInfo();
syncInfo.ppk = ppk;
saveLocalSyncInfo(syncInfo);
}
export function masterKeyById(id: string) {
return localSyncInfo().masterKeys.find(mk => mk.id === id);
}

View File

@ -62,7 +62,6 @@ const gunzipFile = function(source, destination) {
});
};
// sharp = null, keytar = null, React = null, appVersion = null, electronBridge = null, nodeSqlite = null
function shimInit(options = null) {
options = {
sharp: null,

View File

@ -57,9 +57,11 @@ import { loadKeychainServiceAndSettings } from '../services/SettingUtils';
import { setActiveMasterKeyId, setEncryptionEnabled } from '../services/synchronizer/syncInfoUtils';
import Synchronizer from '../Synchronizer';
import SyncTargetNone from '../SyncTargetNone';
import { setRSA } from '../services/e2ee/ppk';
const md5 = require('md5');
const S3 = require('aws-sdk/clients/s3');
const { Dirnames } = require('../services/synchronizer/utils/types');
import RSA from '../services/e2ee/RSA.node';
// Each suite has its own separate data and temp directory so that multiple
// suites can be run at the same time. suiteName is what is used to
@ -436,6 +438,8 @@ async function setupDatabaseAndSynchronizer(id: number, options: any = null) {
resourceFetchers_[id] = new ResourceFetcher(() => { return synchronizers_[id].api(); });
kvStores_[id] = new KvStore();
setRSA(RSA);
await fileApi().initialize();
await fileApi().clearRoot();
}
@ -505,11 +509,12 @@ function resourceFetcher(id: number = null) {
async function loadEncryptionMasterKey(id: number = null, useExisting = false) {
const service = encryptionService(id);
const password = '123456';
let masterKey = null;
if (!useExisting) { // Create it
masterKey = await service.generateMasterKey('123456');
masterKey = await service.generateMasterKey(password);
masterKey = await MasterKey.save(masterKey);
} else { // Use the one already available
const masterKeys = await MasterKey.all();
@ -517,7 +522,12 @@ async function loadEncryptionMasterKey(id: number = null, useExisting = false) {
masterKey = masterKeys[0];
}
await service.loadMasterKey(masterKey, '123456', true);
const passwordCache = Setting.value('encryption.passwordCache');
passwordCache[masterKey.id] = password;
Setting.setValue('encryption.passwordCache', passwordCache);
await Setting.saveAll();
await service.loadMasterKey(masterKey, password, true);
setActiveMasterKeyId(masterKey.id);

View File

@ -209,7 +209,7 @@ function addExtraStyles(style: any) {
backgroundColor: style.backgroundColor4,
borderColor: style.borderColor4,
userSelect: 'none',
cursor: 'pointer',
// cursor: 'pointer',
}
);

View File

@ -14,6 +14,7 @@ const theme: Theme = {
oddBackgroundColor: '#141517',
color: '#dddddd',
colorError: 'red',
colorCorrect: '#72b972',
colorWarn: '#9A5B00',
colorWarnUrl: '#ffff82',
colorFaded: '#999999', // For less important text

View File

@ -11,6 +11,7 @@ const theme: Theme = {
oddBackgroundColor: '#eeeeee',
color: '#32373F', // For regular text
colorError: 'red',
colorCorrect: 'green', // Opposite of colorError
colorWarn: 'rgb(228,86,0)',
colorWarnUrl: '#155BDA',
colorFaded: '#7C8B9E', // For less important text

View File

@ -13,6 +13,7 @@ export interface Theme {
oddBackgroundColor: string;
color: string; // For regular text
colorError: string;
colorCorrect: string;
colorWarn: string;
colorWarnUrl: string; // For URL displayed over a warningBackgroundColor
colorFaded: string; // For less important text

View File

@ -19,22 +19,42 @@ async function sassRender(options) {
});
}
module.exports = async function compileSass(inputPaths, outputPath) {
const promises = [];
for (const inputPath of inputPaths) {
console.info(`Compiling ${inputPath}...`);
// module.exports = async function compileSass(inputPaths, outputPath) {
// const promises = [];
// for (const inputPath of inputPaths) {
// console.info(`Compiling ${inputPath}...`);
promises.push(sassRender({
file: inputPath,
sourceMap: true,
outFile: outputPath,
}));
}
// promises.push(sassRender({
// file: inputPath,
// sourceMap: true,
// outFile: outputPath,
// }));
// }
const results = await Promise.all(promises);
// const results = await Promise.all(promises);
const cssString = results.map(r => r.css.toString()).join('\n');
const mapString = results.map(r => r.map.toString()).join('\n');
// const cssString = results.map(r => r.css.toString()).join('\n');
// const mapString = results.map(r => r.map.toString()).join('\n');
// await Promise.all([
// fs.writeFile(outputPath, cssString, 'utf8'),
// fs.writeFile(`${outputPath}.map`, mapString, 'utf8'),
// ]);
// console.info(`Generated ${outputPath}`);
// };
module.exports = async function compileSass(inputPath, outputPath) {
const result = await sassRender({
file: inputPath,
sourceMap: true,
outFile: outputPath,
outputStyle: 'compressed',
indentType: 'tab',
});
const cssString = result.css.toString();
const mapString = result.map.toString();
await Promise.all([
fs.writeFile(outputPath, cssString, 'utf8'),

View File

@ -65,3 +65,36 @@ Enabling/disabling E2EE while two clients are in sync might have an unintuitive
- Although messy, Joplin supports having some clients send encrypted items and others unencrypted ones. The situation gets resolved once all the clients have the same E2EE settings.
- Currently, there is no way to delete encryption keys if you do not need them anymore or if you disabled the encryption completely. You will get a persistant notification to provide a Master Key password on a new device, even if encryption is disabled. Entering the Master Key(s) password and still having the encryption disabled will get rid of the notification. See [Delete E2EE Master Keys](https://discourse.joplinapp.org/t/delete-e2ee-master-keys/906) for more info.
## Types of keys
There are two types of key:
- **Data keys**, which are used to encrypt Joplin items, such as notes, notebooks, tags, etc. when E2EE is ernabled. A data key is generated when the user enables E2EE. Data keys are also dynamically generated when a user shares a notebook with another user. In this case, we create a separate key, so that the recipient can only decrypt this specific notebook.
- **Public-private key pairs**, which are used to transfer secrets between users.
## Master password
The master password is used to encrypt E2EE data keys as well as the user's private key.
**It is possible to change the master password** - in this case, all keys are reencrypted with the new passowrd. The data, notes, notebooks, etc. does not need to be reencrypted.
If a master password is forgotten it's not possible to recover it. **It is however possible to reset it**. In that case, all associated keys are disabled, and the public-private key pair is regenerated. In practice it means that any content that was encrypted with the forgotten password can no longer be decrypted.
## Public-private key pairs
Public-private key pairs (PPK) are used to transfer secrets between users. Specifically, they are used when sharing a notebook while E2EE is enabled. The workflow is as follow:
- Alice shares a notebook with Bob
- Since the notebook is encrypted, Alice also sends the key to Bob, but it needs to be encrypted too.
- To do so, she downloads Bob's public key and encrypt the key with it
- When accepting the share, Bob receives this key
- Bob decrypts it with his private key
- Once decrypted, he reencrypts it with his master password
At this point, both users have a copy of the key and can share notes over E2EE.
A user can only have one PPK.
PPKs are generated automatically when E2EE is enabled and when the user synchronises. They are then stored in info.json on the sync target. The key is genrated during sync because otherwise multiple clients could generate a PPK, and then there would be a conflict to decide which PPK should be kept. By doing it during sync, it ensures that only one PPK is generated because the synchronizer fetches first info.json - and only generates a PPK if none is already present.