1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-03-18 10:17:44 +02:00

Compare commits

..

34 Commits

Author SHA1 Message Date
Laurent Cozic
d71ac2e218 tests 2021-09-18 11:27:26 +01:00
Laurent Cozic
c35c5a5821 ui 2021-09-17 20:15:43 +01:00
Laurent Cozic
bcb08ac8a2 Merge branch 'dev' into server_tasks 2021-09-17 18:28:43 +01:00
Laurent Cozic
f91b4edb30 Tools: Tweak to stress test script 2021-09-17 18:27:25 +01:00
Laurent Cozic
b56177a4e3 Tools: Added tools to stress test Joplin Server 2021-09-17 10:59:10 +01:00
Laurent Cozic
6ff8d775c2 Add support for server tasks 2021-09-16 17:37:51 +01:00
Laurent Cozic
4e70ca6fd0 Server: Exclude certain queries from slow log 2021-09-16 17:36:06 +01:00
Laurent Cozic
5e8b7420ff Server: Added support for app level slow SQL query log 2021-09-15 23:14:14 +01:00
Laurent Cozic
8ae4e30fd2 Server v2.4.7 2021-09-15 16:58:59 +01:00
Laurent Cozic
3ce947e82c Server: Fixed handling of brute force limiter by getting correct user IP 2021-09-15 16:57:18 +01:00
Laurent Cozic
c2298213d7 Server: Improve flag logic 2021-09-15 12:06:01 +01:00
Abdunnasir Saeed
9679f03cfa All: Translation: Update ar.po (#5464)
A complete Arabic translation
2021-09-15 06:39:50 -04:00
Laurent Cozic
3cddac3931 Server v2.4.6 2021-09-14 16:02:51 +01:00
Laurent Cozic
41c1e3bec9 Server: Fix transaction deadlock logging 2021-09-14 15:59:01 +01:00
Laurent Cozic
25c5892e74 Server v2.4.5 2021-09-14 13:02:56 +01:00
Laurent Cozic
a661a73511 Revert "Server: Enable multi platform builds (amd64, armv7 and arm64) (#5338)"
This reverts commit ab134807ea.

Does not build:

https://github.com/laurent22/joplin/runs/3597996286?check_suite_focus=true#step:8:388
2021-09-14 13:01:33 +01:00
Laurent Cozic
b00959e143 Server v2.4.4 2021-09-14 12:16:47 +01:00
Laurent Cozic
f6f5d6808d Merge branch 'dev' into release-2.4 2021-09-14 12:07:04 +01:00
Laurent Cozic
01b653fc34 Server: Add transaction info to debug deadlock issues 2021-09-14 12:05:29 +01:00
Laurent Cozic
4e7fe66883 Server: Add link to Stripe subscription page to manage payment details 2021-09-13 12:30:36 +01:00
mrkaato
cd99e675d9 All: Translation: Update fi_FI.po (#5452) 2021-09-12 13:00:30 -04:00
Laurent Cozic
a7130ce17a Tools: Added script to compile SASS files 2021-09-12 16:35:08 +01:00
Laurent Cozic
20f8743079 Tools: Upgrade back package-lock files to v2 2021-09-12 16:34:03 +01:00
Laurent Cozic
660b53575e Doc: Update sponsors 2021-09-12 13:08:02 +01:00
Kenichi Kobayashi
6c43b78496 All: Fixes #5447: Plugin onNoteSelectionChange() is triggered twice after a search (#5449) 2021-09-12 11:40:14 +01:00
Helmut K. C. Tessarek
9d5d891fe3 Desktop, Mobile: Resolves #5295: Update Mermaid 8.10.2 -> 8.12.1 and fix gitGraph crash (#5448) 2021-09-11 19:47:01 +01:00
Laurent Cozic
96ac12b460 Chore: Converted encryption config screens to React Hooks to share logic between desktop and mobile 2021-09-10 19:05:47 +01:00
Laurent Cozic
4b93664240 Desktop release v2.4.6 2021-09-09 19:25:54 +01:00
Laurent Cozic
a2c6461af8 Desktop: Fix handling of disabled master keys when enabling E2EE 2021-09-09 19:24:52 +01:00
Laurent Cozic
d33b99cffb Desktop: Fix handling of disabled master keys when enabling E2EE 2021-09-09 18:46:58 +01:00
Laurent Cozic
267c32143b Desktop: Fix handling of disabled master keys when enabling E2EE 2021-09-09 18:42:00 +01:00
Laurent Cozic
9260b2a9ab Plugins: Add support for enabledConditions when creating menu item from command 2021-09-09 14:44:16 +01:00
Xavi Ivars
0a54854f54 All: Translation: Update ca.po (#5432)
* Update ca.po

* Update ca.po

A ton of improvements
2021-09-08 16:33:30 -04:00
xnumad
496039f15c Doc: Update doc links (#5425) 2021-09-08 14:18:01 +01:00
151 changed files with 3295 additions and 4033 deletions

View File

@@ -76,6 +76,9 @@ packages/app-cli/app/command-e2ee.js.map
packages/app-cli/app/command-settingschema.d.ts
packages/app-cli/app/command-settingschema.js
packages/app-cli/app/command-settingschema.js.map
packages/app-cli/app/command-testing.d.ts
packages/app-cli/app/command-testing.js
packages/app-cli/app/command-testing.js.map
packages/app-cli/app/services/plugins/PluginRunner.d.ts
packages/app-cli/app/services/plugins/PluginRunner.js
packages/app-cli/app/services/plugins/PluginRunner.js.map
@@ -109,6 +112,9 @@ packages/app-cli/tests/services/plugins/sandboxProxy.js.map
packages/app-cli/tests/testUtils.d.ts
packages/app-cli/tests/testUtils.js
packages/app-cli/tests/testUtils.js.map
packages/app-cli/tools/populateDatabase.d.ts
packages/app-cli/tools/populateDatabase.js
packages/app-cli/tools/populateDatabase.js.map
packages/app-desktop/ElectronAppWrapper.d.ts
packages/app-desktop/ElectronAppWrapper.js
packages/app-desktop/ElectronAppWrapper.js.map
@@ -208,9 +214,9 @@ packages/app-desktop/gui/DialogTitle.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
@@ -328,9 +334,6 @@ 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
@@ -931,15 +934,12 @@ 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
packages/lib/components/shared/encryption-config-shared.d.ts
packages/lib/components/shared/encryption-config-shared.js
packages/lib/components/shared/encryption-config-shared.js.map
packages/lib/components/EncryptionConfigScreen/utils.d.ts
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/EncryptionConfigScreen/utils.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map
@@ -1237,12 +1237,6 @@ 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/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/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
@@ -1579,9 +1573,6 @@ 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

@@ -190,7 +190,7 @@ 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|SJCL.*)$',
'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD)$',
'match': true,
},
},

View File

@@ -35,14 +35,6 @@ jobs:
sudo apt-get update || true
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
# the next line enables multi-architecture support for docker, it basically makes it use qemu for non native platforms
# See https://hub.docker.com/r/tonistiigi/binfmt for more info
docker run --privileged --rm tonistiigi/binfmt --install all
# this just prints the info about what platforms are supported in the builder (can help debugging if something isn't working right)
# and also proves the above worked properly
sudo docker buildx ls
- uses: actions/checkout@v2
- uses: olegtarasov/get-tag@v2.1
- uses: actions/setup-node@v2

33
.gitignore vendored
View File

@@ -61,6 +61,9 @@ packages/app-cli/app/command-e2ee.js.map
packages/app-cli/app/command-settingschema.d.ts
packages/app-cli/app/command-settingschema.js
packages/app-cli/app/command-settingschema.js.map
packages/app-cli/app/command-testing.d.ts
packages/app-cli/app/command-testing.js
packages/app-cli/app/command-testing.js.map
packages/app-cli/app/services/plugins/PluginRunner.d.ts
packages/app-cli/app/services/plugins/PluginRunner.js
packages/app-cli/app/services/plugins/PluginRunner.js.map
@@ -94,6 +97,9 @@ packages/app-cli/tests/services/plugins/sandboxProxy.js.map
packages/app-cli/tests/testUtils.d.ts
packages/app-cli/tests/testUtils.js
packages/app-cli/tests/testUtils.js.map
packages/app-cli/tools/populateDatabase.d.ts
packages/app-cli/tools/populateDatabase.js
packages/app-cli/tools/populateDatabase.js.map
packages/app-desktop/ElectronAppWrapper.d.ts
packages/app-desktop/ElectronAppWrapper.js
packages/app-desktop/ElectronAppWrapper.js.map
@@ -193,9 +199,9 @@ packages/app-desktop/gui/DialogTitle.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
@@ -313,9 +319,6 @@ 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
@@ -916,15 +919,12 @@ 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
packages/lib/components/shared/encryption-config-shared.d.ts
packages/lib/components/shared/encryption-config-shared.js
packages/lib/components/shared/encryption-config-shared.js.map
packages/lib/components/EncryptionConfigScreen/utils.d.ts
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/EncryptionConfigScreen/utils.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map
@@ -1222,12 +1222,6 @@ 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/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/types.d.ts
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/types.js.map
@@ -1564,9 +1558,6 @@ 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

View File

@@ -23,4 +23,6 @@ tests/support/dropbox-auth.txt
tests/support/nextcloud-auth.json
tests/support/onedrive-auth.txt
build/
patches/
patches/
createUsers-*.txt
tools/temp/

View File

@@ -89,7 +89,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
flags = cliUtils.parseFlags(flags);
if (!flags.arg) {
booleanFlags.push(flags.short);
if (flags.short) booleanFlags.push(flags.short);
if (flags.long) booleanFlags.push(flags.long);
}

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`.'); // `generate-ppk`
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file` and `target-status`.');
}
options() {
@@ -151,19 +151,6 @@ 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');

View File

@@ -0,0 +1,95 @@
const { BaseCommand } = require('./base-command.js');
import { reg } from '@joplin/lib/registry';
import Note from '@joplin/lib/models/Note';
import uuid from '@joplin/lib/uuid';
import populateDatabase from '@joplin/lib/services/debug/populateDatabase';
function randomElement(array: any[]): any {
if (!array.length) return null;
return array[Math.floor(Math.random() * array.length)];
}
function itemCount(args: any) {
const count = Number(args.arg0);
if (!count || isNaN(count)) throw new Error('Note count must be specified');
return count;
}
class Command extends BaseCommand {
usage() {
return 'testing <command> [arg0]';
}
description() {
return 'testing';
}
enabled() {
return false;
}
options(): any[] {
return [
['--folder-count <count>', 'Folders to create'],
['--note-count <count>', 'Notes to create'],
['--tag-count <count>', 'Tags to create'],
['--tags-per-note <count>', 'Tags per note'],
['--silent', 'Silent'],
];
}
async action(args: any) {
const { command, options } = args;
if (command === 'populate') {
await populateDatabase(reg.db(), {
folderCount: options['folder-count'],
noteCount: options['note-count'],
tagCount: options['tag-count'],
tagsPerNote: options['tags-per-note'],
silent: options['silent'],
});
}
const promises: any[] = [];
if (command === 'createRandomNotes') {
const noteCount = itemCount(args);
for (let i = 0; i < noteCount; i++) {
promises.push(Note.save({
title: `Note ${uuid.createNano()}`,
}));
}
}
if (command === 'updateRandomNotes') {
const noteCount = itemCount(args);
const noteIds = await Note.allIds();
for (let i = 0; i < noteCount; i++) {
const noteId = randomElement(noteIds);
promises.push(Note.save({
id: noteId,
title: `Note ${uuid.createNano()}`,
}));
}
}
if (command === 'deleteRandomNotes') {
const noteCount = itemCount(args);
const noteIds = await Note.allIds();
for (let i = 0; i < noteCount; i++) {
const noteId = randomElement(noteIds);
promises.push(Note.delete(noteId));
}
}
await Promise.all(promises);
}
}
module.exports = Command;

52
packages/app-cli/createUsers.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
# Start the server with:
#
# JOPLIN_IS_TESTING=1 npm run start-dev
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# curl --data '{"action": "clearDatabase"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
# SMALL
# curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
NUM=398
while [ "$NUM" -lt 400 ]; do
NUM=$(( NUM + 1 ))
echo "User $NUM"
CMD_FILE="$SCRIPT_DIR/createUsers-$NUM.txt"
PROFILE_DIR=~/.config/joplindev-testing-$NUM
USER_EMAIL="user$NUM@example.com"
rm -rf "$CMD_FILE" "$PROFILE_DIR"
touch "$CMD_FILE"
FLAG_FOLDER_COUNT=100
FLAG_NOTE_COUNT=1000
FLAG_TAG_COUNT=20
if [ "$NUM" -gt 300 ]; then
FLAG_FOLDER_COUNT=2000
FLAG_NOTE_COUNT=10000
FLAG_TAG_COUNT=200
fi
if [ "$NUM" -gt 399 ]; then
FLAG_FOLDER_COUNT=10000
FLAG_NOTE_COUNT=150000
FLAG_TAG_COUNT=2000
fi
echo "testing populate --silent --folder-count $FLAG_FOLDER_COUNT --note-count $FLAG_NOTE_COUNT --tag-count $FLAG_TAG_COUNT" >> "$CMD_FILE"
echo "config keychain.supported 0" >> "$CMD_FILE"
echo "config sync.target 10" >> "$CMD_FILE"
echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE"
echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE"
echo "sync" >> "$CMD_FILE"
npm start -- --profile "$PROFILE_DIR" batch "$CMD_FILE"
done

View File

@@ -10,6 +10,7 @@
"test-ci": "jest --config=jest.config.js --forceExit",
"build": "gulp build",
"start": "gulp build -L && node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
"start-no-build": "node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json"
},

View File

@@ -0,0 +1,108 @@
// This script can be used to simulate a running production environment, by
// having multiple users in parallel changing notes and synchronising.
//
// To get it working:
//
// - Run the Postgres database -- `sudo docker-compose --file docker-compose.db-dev.yml up`
// - Update the DB parameters in ~/joplin-credentials/server.env to use the dev
// database
// - Run the server - `JOPLIN_IS_TESTING=1 npm run start-dev`
// - Then run this script - `node populateDatabase.js`
//
// Currently it doesn't actually create the users, so that should be done using:
//
// curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
//
// That will create n users with email `user<n>@example.com`
import * as fs from 'fs-extra';
import { homedir } from 'os';
import { execCommand2 } from '@joplin/tools/tool-utils';
import { chdir } from 'process';
const minUserNum = 1;
const maxUserNum = 400;
const cliDir = `${__dirname}/..`;
const tempDir = `${__dirname}/temp`;
function randomInt(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const processing_: Record<number, boolean> = {};
const processUser = async (userNum: number) => {
if (processing_[userNum]) {
console.info(`User already being processed: ${userNum} - skipping`);
return;
}
processing_[userNum] = true;
try {
const userEmail = `user${userNum}@example.com`;
const userPassword = 'hunter1hunter2hunter3';
const commandFile = `${tempDir}/populateDatabase-${userNum}.txt`;
const profileDir = `${homedir()}/.config/joplindev-populate/joplindev-testing-${userNum}`;
const commands: string[] = [];
const jackpot = Math.random() >= 0.95 ? 100 : 1;
commands.push(`testing createRandomNotes ${randomInt(1, 500 * jackpot)}`);
commands.push(`testing updateRandomNotes ${randomInt(1, 1500 * jackpot)}`);
commands.push(`testing deleteRandomNotes ${randomInt(1, 200 * jackpot)}`);
commands.push('config keychain.supported 0');
commands.push('config sync.target 10');
commands.push(`config sync.10.username ${userEmail}`);
commands.push(`config sync.10.password ${userPassword}`);
commands.push('sync');
await fs.writeFile(commandFile, commands.join('\n'), 'utf8');
await chdir(cliDir);
await execCommand2(['npm', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]);
} catch (error) {
console.error(`Could not process user ${userNum}:`, error);
} finally {
delete processing_[userNum];
}
};
const waitForProcessing = (count: number) => {
return new Promise((resolve) => {
const iid = setInterval(() => {
if (Object.keys(processing_).length <= count) {
clearInterval(iid);
resolve(null);
}
}, 100);
});
};
const main = async () => {
await fs.mkdirp(tempDir);
// Build the app once before starting, because we'll use start-no-build to
// run the scripts (faster)
await execCommand2(['npm', 'run', 'build']);
const focusUserNum = 400;
while (true) {
let userNum = randomInt(minUserNum, maxUserNum);
if (Math.random() >= .7) userNum = focusUserNum;
void processUser(userNum);
await waitForProcessing(10);
}
};
main().catch((error) => {
console.error('Fatal error', error);
process.exit(1);
});

View File

@@ -535,12 +535,12 @@ class Application extends BaseApplication {
// }, 2000);
setTimeout(() => {
this.dispatch({
type: 'DIALOG_OPEN',
name: 'masterPassword',
});
}, 2000);
// setTimeout(() => {
// this.dispatch({
// type: 'DIALOG_OPEN',
// name: 'syncWizard',
// });
// }, 2000);
// setTimeout(() => {
// this.dispatch({

View File

@@ -6,7 +6,7 @@ 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';
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');

View File

@@ -27,12 +27,11 @@ const DialogRoot = styled.div`
interface Props {
renderContent: Function;
className?: string;
}
export default function Dialog(props: Props) {
return (
<DialogModalLayer className={props.className}>
<DialogModalLayer>
<DialogRoot>
{props.renderContent()}
</DialogRoot>

View File

@@ -17,13 +17,10 @@ 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[];
}
@@ -71,15 +68,15 @@ export default function DialogButtonRow(props: Props) {
if (props.okButtonShow !== false) {
buttonComps.push(
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
<button key="ok" style={buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
{_('OK')}
</button>
);
}
if (props.cancelButtonShow !== false) {
buttonComps.push(
<button disabled={props.cancelButtonDisabled} key="cancel" style={Object.assign({}, buttonStyle)} onClick={cancelButton_click}>
<button 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 : 'center'};
justify-content: ${props => props.justifyContent ? props.justifyContent : 'flex-start'};
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: 1em;
margin-bottom: 1.2em;
`;

View File

@@ -1,417 +0,0 @@
const React = require('react');
const { connect } = require('react-redux');
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import { State } from '@joplin/lib/reducer';
import shim from '@joplin/lib/shim';
import dialogs from './dialogs';
import bridge from '../services/bridge';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, toggleAndSetupEncryption, getMasterPassword } from '@joplin/lib/services/e2ee/utils';
import MasterKey from '@joplin/lib/models/MasterKey';
import StyledInput from './style/StyledInput';
import Button, { ButtonLevel } from './Button/Button';
import styled from 'styled-components';
const MasterPasswordInput = styled(StyledInput)`
min-width: 300px;
align-items: center;
`;
interface Props {}
class EncryptionConfigScreenComponent extends React.Component<Props> {
constructor(props: Props) {
super(props);
shared.initialize(this, props);
}
componentWillUnmount() {
this.isMounted_ = false;
shared.componentWillUnmount();
}
componentDidMount() {
this.isMounted_ = true;
shared.componentDidMount(this);
}
componentDidUpdate(prevProps: Props) {
shared.componentDidUpdate(this, prevProps);
}
async checkPasswords() {
return shared.checkPasswords(this);
}
private renderMasterKey(mk: MasterKeyEntity, _isDefault: boolean) {
const theme = themeStyle(this.props.themeId);
const onToggleEnabledClick = () => {
return shared.onToggleEnabledClick(this, mk);
};
const passwordStyle = {
color: theme.color,
backgroundColor: theme.backgroundColor,
border: '1px solid',
borderColor: theme.dividerColor,
};
const onSaveClick = () => {
return shared.onSavePasswordClick(this, mk);
};
const onPasswordChange = (event: any) => {
return shared.onPasswordChange(this, mk, event.target.value);
};
const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
return (
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
({_('Master password')})
</td>
);
} else {
return (
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
{_('Save')}
</button>
</td>
);
}
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const isActive = this.props.activeMasterKeyId === mk.id;
const activeIcon = isActive ? '✔' : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
return (
<tr key={mk.id}>
<td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
{renderPasswordInput(mk.id)}
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
</td>
</tr>
);
}
renderNeedUpgradeSection() {
if (!shim.isElectron()) return null;
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys);
if (!needUpgradeMasterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const rows = [];
const comp = this;
for (const mk of needUpgradeMasterKeys) {
rows.push(
<tr key={mk.id}>
<td style={theme.textStyle}>{mk.id}</td>
<td><button onClick={() => shared.upgradeMasterKey(comp, mk)} style={theme.buttonStyle}>Upgrade</button></td>
</tr>
);
}
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>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Upgrade')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
renderReencryptData() {
if (!shim.isElectron()) return null;
if (!this.props.shouldReencrypt) return null;
const theme = themeStyle(this.props.themeId);
const buttonLabel = _('Re-encrypt data');
const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
t = t.replace(/\n\n/g, '</p><p>');
t = t.replace(/\n/g, '<br>');
t = `<p>${t}</p>`;
return (
<div>
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
<span style={{ marginRight: 10 }}>
<button onClick={() => shared.reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
</span>
{ !this.props.shouldReencrypt ? null : <button onClick={() => shared.dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
</div>
);
}
private renderMasterKeySection(masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) {
const theme = themeStyle(this.props.themeId);
const mkComps = [];
const showTable = isEnabledMasterKeys || this.state.showDisabledMasterKeys;
const latestMasterKey = MasterKey.latest();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk, isEnabledMasterKeys && latestMasterKey && mk.id === latestMasterKey.id));
}
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => shared.toggleShowDisabledMasterKeys(this) } 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 tableComp = !showTable ? null : (
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Date')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Valid')}</th>
<th style={theme.textStyle}>{_('Actions')}</th>
</tr>
{mkComps}
</tbody>
</table>
);
if (mkComps.length) {
return (
<div>
{headerComp}
{tableComp}
{infoComp}
</div>
);
}
return null;
}
private renderMasterPassword() {
// if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const onMasterPasswordSave = async () => {
shared.onMasterPasswordSave(this);
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
alert('Password is invalid. Please try again.');
}
};
// const status = this.getMasterPasswordStatus();
// const statusMessages = {
// [MasterPasswordStatus.NotSet]: 'Not set',
// [MasterPasswordStatus.Valid]: '✓ ' + 'Valid',
// [MasterPasswordStatus.Invalid]: '❌ ' + 'Invalid',
// };
return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span style={theme.textStyle}>{_('Master password:')}</span>&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}>{statusMessages[status]}</span>
</div>
);
// if (this.state.passwordChecks['master']) {
// return (
// <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
// <span style={theme.textStyle}>{_('Status:')}</span>&nbsp;&nbsp;
// <span style={{ ...theme.textStyle, fontWeight: 'bold' }}>{statusMessages[status]}</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={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
// <Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
// </div>
// </div>
// );
// }
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: theme.configScreenPadding,
overflow: 'auto',
backgroundColor: theme.backgroundColor3,
});
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
const isEnabled = getEncryptionEnabled();
let masterKey = getDefaultMasterKey();
// If the user has explicitly disabled the master key, we generate a
// new one. Needed for one the password has been forgotten.
if (!masterKey.enabled) masterKey = null;
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?'));
} else {
const msg = shared.enableEncryptionConfirmationMessages(masterKey);
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
}
if (!answer) return;
try {
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
} catch (error) {
await dialogs.alert(error.message);
}
};
const decryptedItemsInfo = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
const toggleButton = (
<button
style={theme.buttonStyle}
onClick={() => {
void onToggleButtonClick();
}}
>
{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
</button>
);
const needUpgradeSection = this.renderNeedUpgradeSection();
const reencryptDataSection = this.renderReencryptData();
const enabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => masterKeyEnabled(mk)), true);
const disabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
let nonExistingMasterKeySection = null;
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>
<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 (
<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}>{_('Master password')}</h1>
{this.renderMasterPassword()}
<h1 style={theme.h1Style}>{_('End-to-end encryption')}</h1>
<p style={theme.textStyle}>
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
{this.props.shouldReencrypt ? reencryptDataSection : null}
{enabledMasterKeySection}
{disabledMasterKeySection}
{nonExistingMasterKeySection}
{!this.props.shouldReencrypt ? reencryptDataSection : null}
</div>
</div>
);
}
}
const mapStateToProps = (state: State) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
themeId: state.settings.theme,
masterKeys: syncInfo.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId,
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
};
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
export default EncryptionConfigScreen;

View File

@@ -0,0 +1,366 @@
const React = require('react');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
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 { 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 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;
`;
interface Props {
themeId: any;
masterKeys: MasterKeyEntity[];
passwords: Record<string, string>;
notLoadedMasterKeys: string[];
encryptionEnabled: boolean;
shouldReencrypt: boolean;
activeMasterKeyId: string;
masterPassword: string;
}
const EncryptionConfigScreen = (props: Props) => {
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
const theme: any = useMemo(() => {
return themeStyle(props.themeId);
}, [props.themeId]);
const stats = useStats();
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
const onUpgradeMasterKey = useCallback((mk: MasterKeyEntity) => {
void upgradeMasterKey(mk, passwordChecks, props.passwords);
}, [passwordChecks, props.passwords]);
const renderNeedUpgradeSection = () => {
if (!shim.isElectron()) return null;
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(props.masterKeys);
if (!needUpgradeMasterKeys.length) return null;
const theme = themeStyle(props.themeId);
const rows = [];
for (const mk of needUpgradeMasterKeys) {
rows.push(
<tr key={mk.id}>
<td style={theme.textStyle}>{mk.id}</td>
<td><button onClick={() => onUpgradeMasterKey(mk)} style={theme.buttonStyle}>Upgrade</button></td>
</tr>
);
}
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>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Upgrade')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
};
const renderReencryptData = () => {
if (!shim.isElectron()) return null;
if (!props.shouldReencrypt) return null;
const theme = themeStyle(props.themeId);
const buttonLabel = _('Re-encrypt data');
const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
t = t.replace(/\n\n/g, '</p><p>');
t = t.replace(/\n/g, '<br>');
t = `<p>${t}</p>`;
return (
<div>
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
<span style={{ marginRight: 10 }}>
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
</span>
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
</div>
);
};
const renderMasterKey = (mk: MasterKeyEntity) => {
const theme = themeStyle(props.themeId);
const passwordStyle = {
color: theme.color,
backgroundColor: theme.backgroundColor,
border: '1px solid',
borderColor: theme.dividerColor,
};
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
const isActive = props.activeMasterKeyId === mk.id;
const activeIcon = isActive ? '✔' : '';
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
const renderPasswordInput = (masterKeyId: string) => {
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
return (
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
({_('Master password')})
</td>
);
} else {
return (
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onInputPasswordChange(mk, event.target.value)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSavePasswordClick(mk, props.passwords)}>
{_('Save')}
</button>
</td>
);
}
};
return (
<tr key={mk.id}>
<td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
{renderPasswordInput(mk.id)}
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick(mk)}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
</td>
</tr>
);
};
const renderMasterKeySection = (masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) => {
const theme = themeStyle(props.themeId);
const mkComps = [];
const showTable = isEnabledMasterKeys || showDisabledMasterKeys;
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
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 tableComp = !showTable ? null : (
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Date')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Valid')}</th>
<th style={theme.textStyle}>{_('Actions')}</th>
</tr>
{mkComps}
</tbody>
</table>
);
if (mkComps.length) {
return (
<div>
{headerComp}
{tableComp}
{infoComp}
</div>
);
}
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 isEnabled = getEncryptionEnabled();
const masterKey = getDefaultMasterKey();
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?'));
} else {
const msg = enableEncryptionConfirmationMessages(masterKey);
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
}
if (!answer) return;
try {
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
} catch (error) {
await dialogs.alert(error.message);
}
};
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 needUpgradeSection = renderNeedUpgradeSection();
const reencryptDataSection = renderReencryptData();
const enabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true);
const disabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
let nonExistingMasterKeySection = null;
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>
<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 (
<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>
);
};
const mapStateToProps = (state: AppState) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
themeId: state.settings.theme,
masterKeys: syncInfo.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId,
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
};
export default connect(mapStateToProps)(EncryptionConfigScreen);

View File

@@ -0,0 +1,5 @@
.encryption-config-test {
& > .item {
font-weight: bold;
}
}

View File

@@ -37,7 +37,6 @@ import { reg } from '@joplin/lib/registry';
import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems';
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
import { MasterKeyEntity } from '../../../lib/services/e2ee/types';
import commands from './commands/index';
const { connect } = require('react-redux');
@@ -546,8 +545,8 @@ class MainScreenComponent extends React.Component<Props, State> {
bridge().restart();
};
const onInvitationRespond = async (shareUserId: string, masterKey: MasterKeyEntity, accept: boolean) => {
await ShareService.instance().respondInvitation(shareUserId, masterKey, accept);
const onInvitationRespond = async (shareUserId: string, accept: boolean) => {
await ShareService.instance().respondInvitation(shareUserId, accept);
await ShareService.instance().refreshShareInvitations();
void reg.scheduleSync(1000);
};
@@ -594,9 +593,9 @@ class MainScreenComponent extends React.Component<Props, State> {
msg = this.renderNotificationMessage(
_('%s (%s) would like to share a notebook with you.', sharer.full_name, sharer.email),
_('Accept'),
() => onInvitationRespond(invitation.id, invitation.master_key, true),
() => onInvitationRespond(invitation.id, true),
_('Reject'),
() => onInvitationRespond(invitation.id, invitation.master_key, false)
() => onInvitationRespond(invitation.id, false)
);
} else if (this.props.hasDisabledSyncItems) {
msg = this.renderNotificationMessage(

View File

@@ -1,165 +0,0 @@
import * as React from 'react';
import { useCallback, useState, useEffect } 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, masterPasswordIsValid, MasterPasswordStatus, updateMasterPassword } from '@joplin/lib/services/e2ee/utils';
import { reg } from '@joplin/lib/registry';
interface Props {
themeId: number;
dispatch: Function;
}
export default function(props: Props) {
const [status, setStatus] = useState(MasterPasswordStatus.NotSet);
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);
function closeDialog(dispatch: Function) {
dispatch({
type: 'DIALOG_CLOSE',
name: 'masterPassword',
});
}
useAsyncEffect(async (event: AsyncEffectEvent) => {
const newStatus = await getMasterPasswordStatus();
if (event.cancelled) return;
setStatus(newStatus);
}, []);
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
if (event.buttonName === 'cancel') {
closeDialog(props.dispatch);
return;
}
if (event.buttonName === 'ok') {
setUpdatingPassword(true);
try {
await updateMasterPassword(currentPassword, password1, () => reg.waitForSyncFinishedThenSync());
closeDialog(props.dispatch);
} catch (error) {
alert(error.message);
} finally {
setUpdatingPassword(false);
}
return;
}
}, [props.dispatch, currentPassword, password1]);
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);
}, []);
useEffect(() => {
setSaveButtonDisabled(updatingPassword || (!password1 || password1 !== password2));
}, [password1, password2, updatingPassword]);
useEffect(() => {
setShowPasswordForm(status === MasterPasswordStatus.NotSet);
}, [status]);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const isValid = await masterPasswordIsValid(currentPassword);
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() {
if (showPasswordForm) {
return (
<div>
<div className="form">
<div className="form-input-group">
<label>{'Current password'}</label>
<div className="current-password-wrapper">
<StyledInput
disabled={status === MasterPasswordStatus.NotSet}
placeholder={status === MasterPasswordStatus.NotSet ? `(${_('Not set')})` : ''}
type="password"
value={currentPassword}
onChange={onCurrentPasswordChange}
/>
{renderCurrentPasswordIcon()}
</div>
</div>
<div className="form-input-group">
<label>{'Enter new password'}</label>
<StyledInput type="password" value={password1} onChange={onPasswordChange1}/>
</div>
<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>
</div>
);
} else {
return (
<p>
<a onClick={onShowPasswordForm} href="#">Change master password</a>
</p>
);
}
}
function renderContent() {
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>
);
}
function renderDialogWrapper() {
return (
<div className="dialog-root">
<DialogTitle title={_('Master password')}/>
{renderContent()}
<DialogButtonRow
themeId={props.themeId}
onClick={onButtonRowClick}
okButtonLabel={_('Save')}
okButtonDisabled={saveButtonDisabled}
cancelButtonDisabled={updatingPassword}
/>
</div>
);
}
return (
<Dialog className="master-password-dialog" renderContent={renderDialogWrapper}/>
);
}

View File

@@ -752,7 +752,6 @@ 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

@@ -20,7 +20,6 @@ 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');
@@ -62,12 +61,6 @@ 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

@@ -171,7 +171,7 @@ function ShareFolderDialog(props: Props) {
try {
setLatestError(null);
const share = await ShareService.instance().shareFolder(props.folderId);
await ShareService.instance().addShareRecipient(share.id, share.master_key_id, recipientEmail);
await ShareService.instance().addShareRecipient(share.id, recipientEmail);
await Promise.all([
ShareService.instance().refreshShares(),
ShareService.instance().refreshShareUsers(share.id),

View File

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

View File

@@ -1,5 +1,6 @@
const gulp = require('gulp');
const utils = require('@joplin/tools/gulp/utils');
const compileSass = require('@joplin/tools/compileSass');
const tasks = {
compileScripts: {
@@ -20,6 +21,14 @@ const tasks = {
tsc: require('@joplin/tools/gulp/tasks/tsc'),
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'),
compileSass: {
fn: async () => {
const guiDir = `${__dirname}/gui`;
await compileSass([
`${guiDir}/EncryptionConfigScreen/style.scss`,
], `${__dirname}/style.min.css`);
},
},
};
utils.registerGulpTasks(gulp, tasks);
@@ -31,6 +40,7 @@ const buildParallel = [
'copyTinyMceLangs',
'updateIgnoredTypeScriptBuild',
'buildCommandIndex',
'compileSass',
];
gulp.task('build', gulp.parallel(...buildParallel));

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.5",
"version": "2.4.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/app-desktop",
"version": "2.4.5",
"version": "2.4.6",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.5",
"version": "2.4.6",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

@@ -3,11 +3,6 @@
# 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 )"
@@ -55,21 +50,12 @@ 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

@@ -143,87 +143,4 @@ a {
*:focus {
outline: none;
}
/* =========================================================================================
General classes
========================================================================================= */
* {
color: var(--joplin-color);
}
.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
========================================================================================= */
.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);
}

5
packages/app-desktop/style.min.css vendored Normal file
View File

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

View File

@@ -2,68 +2,56 @@ const React = require('react');
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
const { BaseScreenComponent } = require('../base-screen.js');
const { themeStyle } = require('../global-style.js');
const DialogBox = require('react-native-dialogbox').default;
const { dialogs } = require('../../utils/dialogs.js');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { State } from '@joplin/lib/reducer';
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import { useMemo, useRef, useState } from 'react';
interface Props {}
interface Props {
themeId: any;
masterKeys: MasterKeyEntity[];
passwords: Record<string, string>;
notLoadedMasterKeys: string[];
encryptionEnabled: boolean;
shouldReencrypt: boolean;
activeMasterKeyId: string;
masterPassword: string;
}
class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
static navigationOptions(): any {
return { header: null };
}
const EncryptionConfigScreen = (props: Props) => {
const [passwordPromptShow, setPasswordPromptShow] = useState(false);
const [passwordPromptAnswer, setPasswordPromptAnswer] = useState('');
const [passwordPromptConfirmAnswer, setPasswordPromptConfirmAnswer] = useState('');
const stats = useStats();
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
const dialogBoxRef = useRef(null);
constructor(props: Props) {
super(props);
const mkComps = [];
this.state = {
passwordPromptShow: false,
passwordPromptAnswer: '',
passwordPromptConfirmAnswer: '',
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
const theme: any = useMemo(() => {
return themeStyle(props.themeId);
}, [props.themeId]);
const rootStyle = useMemo(() => {
return {
flex: 1,
backgroundColor: theme.backgroundColor,
};
}, [theme]);
shared.initialize(this, props);
this.styles_ = {};
}
componentWillUnmount() {
this.isMounted_ = false;
}
async refreshStats() {
return shared.refreshStats(this);
}
componentDidMount() {
this.isMounted_ = true;
shared.componentDidMount(this);
}
componentDidUpdate(prevProps: Props) {
shared.componentDidUpdate(this, prevProps);
}
async checkPasswords() {
return shared.checkPasswords(this);
}
styles() {
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
if (this.styles_[themeId]) return this.styles_[themeId];
this.styles_ = {};
const styles = useMemo(() => {
const styles = {
titleText: {
flex: 1,
@@ -93,39 +81,32 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
},
};
this.styles_[themeId] = StyleSheet.create(styles);
return this.styles_[themeId];
}
return StyleSheet.create(styles);
}, [theme]);
renderMasterKey(_num: number, mk: MasterKeyEntity) {
const theme = themeStyle(this.props.themeId);
const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null;
const onSaveClick = () => {
return shared.onSavePasswordClick(this, mk);
};
const renderMasterKey = (_num: number, mk: MasterKeyEntity) => {
const theme = themeStyle(props.themeId);
const onPasswordChange = (text: string) => {
return shared.onPasswordChange(this, mk, text);
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
return (
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
<Text style={{ ...styles.normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
);
} else {
return (
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onInputPasswordChange(mk, text)} style={inputStyle}></TextInput>
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
<Button title={_('Save')} onPress={() => onSavePasswordClick(mk, props.passwords)}></Button>
</View>
);
}
@@ -133,69 +114,65 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
return (
<View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<Text style={styles.titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={styles.normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
{renderPasswordInput(mk.id)}
</View>
</View>
);
}
};
passwordPromptComponent() {
const theme = themeStyle(this.props.themeId);
let masterKey = getDefaultMasterKey();
// If the user has explicitly disabled the master key, we generate a
// new one. Needed for one the password has been forgotten.
if (!masterKey.enabled) masterKey = null;
const renderPasswordPrompt = () => {
const theme = themeStyle(props.themeId);
const masterKey = getDefaultMasterKey();
const onEnableClick = async () => {
try {
const password = this.state.passwordPromptAnswer;
const password = passwordPromptAnswer;
if (!password) throw new Error(_('Password cannot be empty'));
const password2 = this.state.passwordPromptConfirmAnswer;
const password2 = passwordPromptConfirmAnswer;
if (!password2) throw new Error(_('Confirm password cannot be empty'));
if (password !== password2) throw new Error(_('Passwords do not match!'));
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password);
// await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password);
this.setState({ passwordPromptShow: false });
setPasswordPromptShow(false);
} catch (error) {
await dialogs.error(this, error.message);
alert(error.message);
}
};
const messages = shared.enableEncryptionConfirmationMessages(masterKey);
const messages = enableEncryptionConfirmationMessages(masterKey);
const messageComps = messages.map(msg => {
const messageComps = messages.map((msg: string) => {
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;
});
return (
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
<View>{messageComps}</View>
<Text style={this.styles().normalText}>{_('Password:')}</Text>
<Text style={styles.normalText}>{_('Password:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
style={this.styles().normalTextInput}
style={styles.normalTextInput}
secureTextEntry={true}
value={this.state.passwordPromptAnswer}
value={passwordPromptAnswer}
onChangeText={(text: string) => {
this.setState({ passwordPromptAnswer: text });
setPasswordPromptAnswer(text);
}}
></TextInput>
<Text style={this.styles().normalText}>{_('Confirm password:')}</Text>
<Text style={styles.normalText}>{_('Confirm password:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
style={this.styles().normalTextInput}
style={styles.normalTextInput}
secureTextEntry={true}
value={this.state.passwordPromptConfirmAnswer}
value={passwordPromptConfirmAnswer}
onChangeText={(text: string) => {
this.setState({ passwordPromptConfirmAnswer: text });
setPasswordPromptConfirmAnswer(text);
}}
></TextInput>
<View style={{ flexDirection: 'row' }}>
@@ -211,156 +188,132 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
<Button
title={_('Cancel')}
onPress={() => {
this.setState({ passwordPromptShow: false });
setPasswordPromptShow(false);
}}
></Button>
</View>
</View>
</View>
);
}
};
private renderMasterPassword() {
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const onMasterPasswordSave = async () => {
shared.onMasterPasswordSave(this);
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
alert('Password is invalid. Please try again.');
}
};
const renderMasterPassword = () => {
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
if (this.state.passwordChecks['master']) {
if (passwordChecks['master']) {
return (
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
<Text style={{ ...styles.normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
<Text style={{ ...styles.normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
</View>
);
} else {
return (
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
<Text style={this.styles().normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
<Text style={styles.normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={this.state.masterPasswordInput} onChangeText={(text: string) => shared.onMasterPasswordChange(this, text)} style={inputStyle}></TextInput>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={inputMasterPassword} onChangeText={(text: string) => onMasterPasswordChange(text)} style={inputStyle}></TextInput>
<Button onPress={onMasterPasswordSave} title={_('Save')} />
</View>
</View>
);
}
};
for (let i = 0; i < props.masterKeys.length; i++) {
const mk = props.masterKeys[i];
mkComps.push(renderMasterKey(i + 1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys = this.props.masterKeys;
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
const onToggleButtonClick = async () => {
if (props.encryptionEnabled) {
const ok = await dialogs.confirmRef(dialogBoxRef.current, _('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 (!ok) return;
const mkComps = [];
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i + 1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
try {
await setupAndDisableEncryption(EncryptionService.instance());
} catch (error) {
alert(error.message);
}
} else {
setPasswordPromptShow(true);
setPasswordPromptAnswer('');
setPasswordPromptConfirmAnswer('');
return;
}
};
const onToggleButtonClick = async () => {
if (this.props.encryptionEnabled) {
const ok = await dialogs.confirm(this, _('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 (!ok) return;
let nonExistingMasterKeySection = null;
try {
await setupAndDisableEncryption(EncryptionService.instance());
} catch (error) {
await dialogs.error(this, error.message);
}
} else {
this.setState({
passwordPromptShow: true,
passwordPromptAnswer: '',
passwordPromptConfirmAnswer: '',
});
return;
}
};
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<Text style={this.styles().normalText} key={id}>
{id}
</Text>
);
}
nonExistingMasterKeySection = (
<View>
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
<Text style={this.styles().normalText}>{_('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.')}</Text>
<View style={{ marginTop: 10 }}>{rows}</View>
</View>
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<Text style={styles.normalText} key={id}>
{id}
</Text>
);
}
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
const toggleButton = !this.state.passwordPromptShow ? (
<View style={{ marginTop: 10 }}>
<Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
</View>
) : null;
return (
<View style={this.rootStyle(this.props.themeId).root}>
<ScreenHeader title={_('Encryption Config')} />
<ScrollView style={this.styles().container}>
{
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity
onPress={() => {
Linking.openURL('https://joplinapp.org/e2ee/');
}}
>
<Text>https://joplinapp.org/e2ee/</Text>
</TouchableOpacity>
</View>
}
<Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{this.renderMasterPassword()}
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{ flex: 1, height: 20 }}></View>
</ScrollView>
<DialogBox
ref={(dialogbox: any) => {
this.dialogbox = dialogbox;
}}
/>
nonExistingMasterKeySection = (
<View>
<Text style={styles.titleText}>{_('Missing Master Keys')}</Text>
<Text style={styles.normalText}>{_('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.')}</Text>
<View style={{ marginTop: 10 }}>{rows}</View>
</View>
);
}
}
const EncryptionConfigScreen = connect((state: State) => {
const passwordPromptComp = passwordPromptShow ? renderPasswordPrompt() : null;
const toggleButton = !passwordPromptShow ? (
<View style={{ marginTop: 10 }}>
<Button title={props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
</View>
) : null;
return (
<View style={rootStyle}>
<ScreenHeader title={_('Encryption Config')} />
<ScrollView style={styles.container}>
{
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity
onPress={() => {
Linking.openURL('https://joplinapp.org/e2ee/');
}}
>
<Text>https://joplinapp.org/e2ee/</Text>
</TouchableOpacity>
</View>
}
<Text style={styles.titleText}>{_('Status')}</Text>
<Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{renderMasterPassword()}
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{ flex: 1, height: 20 }}></View>
</ScrollView>
<DialogBox ref={dialogBoxRef}/>
</View>
);
};
export default connect((state: State) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
@@ -372,6 +325,4 @@ const EncryptionConfigScreen = connect((state: State) => {
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
})(EncryptionConfigScreenComponent);
export default EncryptionConfigScreen;
})(EncryptionConfigScreen);

View File

@@ -15,8 +15,8 @@
"postinstall": "jetify && npm run build"
},
"dependencies": {
"@joplin/lib": "^2.2.0",
"@joplin/renderer": "^2.2.0",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@react-native-community/clipboard": "^1.5.0",
"@react-native-community/datetimepicker": "^3.0.3",
"@react-native-community/geolocation": "^2.0.2",
@@ -69,7 +69,7 @@
"@codemirror/lang-markdown": "^0.18.4",
"@codemirror/state": "^0.18.7",
"@codemirror/view": "^0.18.19",
"@joplin/tools": "^1.0.9",
"@joplin/tools": "~2.4",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/node": "^14.14.6",

View File

@@ -1 +1 @@
module.exports = `LyoKCkF0b20gT25lIERhcmsgV2l0aCBzdXBwb3J0IGZvciBSZWFzb25NTCBieSBHaWRpIE1vcnJpcywgYmFzZWQgb2ZmIHdvcmsgYnkgRGFuaWVsIEdhbWFnZQoKT3JpZ2luYWwgT25lIERhcmsgU3ludGF4IHRoZW1lIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2F0b20vb25lLWRhcmstc3ludGF4CgoqLwouaGxqcyB7CiAgZGlzcGxheTogYmxvY2s7CiAgb3ZlcmZsb3cteDogYXV0bzsKICBwYWRkaW5nOiAwLjVlbTsKICBjb2xvcjogI2FiYjJiZjsKICBiYWNrZ3JvdW5kOiAjMjgyYzM0Owp9Ci5obGpzLWtleXdvcmQsIC5obGpzLW9wZXJhdG9yIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIC5obGpzLWNvbnN0cnVjdG9yIHsKICBjb2xvcjogIzYxYWVlZTsKfQouaGxqcy1mdW5jdGlvbiB7CiAgY29sb3I6ICM2MWFlZWU7Cn0KLmhsanMtZnVuY3Rpb24gLmhsanMtcGFyYW1zIHsKICBjb2xvcjogI0E2RTIyRTsKfQouaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5nIHsKICBjb2xvcjogI0ZEOTcxRjsKfQouaGxqcy1tb2R1bGUtYWNjZXNzIC5obGpzLW1vZHVsZSB7CiAgY29sb3I6ICM3ZTU3YzI7Cn0KLmhsanMtY29uc3RydWN0b3IgewogIGNvbG9yOiAjZTJiOTNkOwp9Ci5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZyB7CiAgY29sb3I6ICM5Q0NDNjU7Cn0KLmhsanMtY29tbWVudCwgLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYjE4ZWIxOwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQouaGxqcy1kb2N0YWcsIC5obGpzLWZvcm11bGEgewogIGNvbG9yOiAjYzY3OGRkOwp9Ci5obGpzLXNlY3Rpb24sIC5obGpzLW5hbWUsIC5obGpzLXNlbGVjdG9yLXRhZywgLmhsanMtZGVsZXRpb24sIC5obGpzLXN1YnN0IHsKICBjb2xvcjogI2UwNmM3NTsKfQouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzU2YjZjMjsKfQouaGxqcy1zdHJpbmcsIC5obGpzLXJlZ2V4cCwgLmhsanMtYWRkaXRpb24sIC5obGpzLWF0dHJpYnV0ZSwgLmhsanMtbWV0YS1zdHJpbmcgewogIGNvbG9yOiAjOThjMzc5Owp9Ci5obGpzLWJ1aWx0X2luLCAuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSB7CiAgY29sb3I6ICNlNmMwN2I7Cn0KLmhsanMtYXR0ciwgLmhsanMtdmFyaWFibGUsIC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLCAuaGxqcy10eXBlLCAuaGxqcy1zZWxlY3Rvci1jbGFzcywgLmhsanMtc2VsZWN0b3ItYXR0ciwgLmhsanMtc2VsZWN0b3ItcHNldWRvLCAuaGxqcy1udW1iZXIgewogIGNvbG9yOiAjZDE5YTY2Owp9Ci5obGpzLXN5bWJvbCwgLmhsanMtYnVsbGV0LCAuaGxqcy1saW5rLCAuaGxqcy1tZXRhLCAuaGxqcy1zZWxlY3Rvci1pZCwgLmhsanMtdGl0bGUgewogIGNvbG9yOiAjNjFhZWVlOwp9Ci5obGpzLWVtcGhhc2lzIHsKICBmb250LXN0eWxlOiBpdGFsaWM7Cn0KLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQouaGxqcy1saW5rIHsKICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsKfQo=`;
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiNhYmIyYmY7YmFja2dyb3VuZDojMjgyYzM0fS5obGpzLWtleXdvcmQsLmhsanMtb3BlcmF0b3IsLmhsanMtcGF0dGVybi1tYXRjaHtjb2xvcjojZjkyNjcyfS5obGpzLWZ1bmN0aW9uLC5obGpzLXBhdHRlcm4tbWF0Y2ggLmhsanMtY29uc3RydWN0b3J7Y29sb3I6IzYxYWVlZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXN7Y29sb3I6I2E2ZTIyZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5ne2NvbG9yOiNmZDk3MWZ9LmhsanMtbW9kdWxlLWFjY2VzcyAuaGxqcy1tb2R1bGV7Y29sb3I6IzdlNTdjMn0uaGxqcy1jb25zdHJ1Y3Rvcntjb2xvcjojZTJiOTNkfS5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZ3tjb2xvcjojOWNjYzY1fS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2IxOGViMTtmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYXtjb2xvcjojYzY3OGRkfS5obGpzLWRlbGV0aW9uLC5obGpzLW5hbWUsLmhsanMtc2VjdGlvbiwuaGxqcy1zZWxlY3Rvci10YWcsLmhsanMtc3Vic3R7Y29sb3I6I2UwNmM3NX0uaGxqcy1saXRlcmFse2NvbG9yOiM1NmI2YzJ9LmhsanMtYWRkaXRpb24sLmhsanMtYXR0cmlidXRlLC5obGpzLW1ldGEgLmhsanMtc3RyaW5nLC5obGpzLXJlZ2V4cCwuaGxqcy1zdHJpbmd7Y29sb3I6Izk4YzM3OX0uaGxqcy1idWlsdF9pbiwuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSwuaGxqcy10aXRsZS5jbGFzc197Y29sb3I6I2U2YzA3Yn0uaGxqcy1hdHRyLC5obGpzLW51bWJlciwuaGxqcy1zZWxlY3Rvci1hdHRyLC5obGpzLXNlbGVjdG9yLWNsYXNzLC5obGpzLXNlbGVjdG9yLXBzZXVkbywuaGxqcy10ZW1wbGF0ZS12YXJpYWJsZSwuaGxqcy10eXBlLC5obGpzLXZhcmlhYmxle2NvbG9yOiNkMTlhNjZ9LmhsanMtYnVsbGV0LC5obGpzLWxpbmssLmhsanMtbWV0YSwuaGxqcy1zZWxlY3Rvci1pZCwuaGxqcy1zeW1ib2wsLmhsanMtdGl0bGV7Y29sb3I6IzYxYWVlZX0uaGxqcy1lbXBoYXNpc3tmb250LXN0eWxlOml0YWxpY30uaGxqcy1zdHJvbmd7Zm9udC13ZWlnaHQ6NzAwfS5obGpzLWxpbmt7dGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZX0=`;

View File

@@ -1 +1 @@
module.exports = `LyoKCkF0b20gT25lIExpZ2h0IGJ5IERhbmllbCBHYW1hZ2UKT3JpZ2luYWwgT25lIExpZ2h0IFN5bnRheCB0aGVtZSBmcm9tIGh0dHBzOi8vZ2l0aHViLmNvbS9hdG9tL29uZS1saWdodC1zeW50YXgKCmJhc2U6ICAgICNmYWZhZmEKbW9uby0xOiAgIzM4M2E0Mgptb25vLTI6ICAjNjg2Yjc3Cm1vbm8tMzogICNhMGExYTcKaHVlLTE6ICAgIzAxODRiYgpodWUtMjogICAjNDA3OGYyCmh1ZS0zOiAgICNhNjI2YTQKaHVlLTQ6ICAgIzUwYTE0ZgpodWUtNTogICAjZTQ1NjQ5Cmh1ZS01LTI6ICNjOTEyNDMKaHVlLTY6ICAgIzk4NjgwMQpodWUtNi0yOiAjYzE4NDAxCgoqLwoKLmhsanMgewogIGRpc3BsYXk6IGJsb2NrOwogIG92ZXJmbG93LXg6IGF1dG87CiAgcGFkZGluZzogMC41ZW07CiAgY29sb3I6ICMzODNhNDI7CiAgYmFja2dyb3VuZDogI2ZhZmFmYTsKfQoKLmhsanMtY29tbWVudCwKLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYTBhMWE3OwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtZG9jdGFnLAouaGxqcy1rZXl3b3JkLAouaGxqcy1mb3JtdWxhIHsKICBjb2xvcjogI2E2MjZhNDsKfQoKLmhsanMtc2VjdGlvbiwKLmhsanMtbmFtZSwKLmhsanMtc2VsZWN0b3ItdGFnLAouaGxqcy1kZWxldGlvbiwKLmhsanMtc3Vic3QgewogIGNvbG9yOiAjZTQ1NjQ5Owp9CgouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzAxODRiYjsKfQoKLmhsanMtc3RyaW5nLAouaGxqcy1yZWdleHAsCi5obGpzLWFkZGl0aW9uLAouaGxqcy1hdHRyaWJ1dGUsCi5obGpzLW1ldGEtc3RyaW5nIHsKICBjb2xvcjogIzUwYTE0ZjsKfQoKLmhsanMtYnVpbHRfaW4sCi5obGpzLWNsYXNzIC5obGpzLXRpdGxlIHsKICBjb2xvcjogI2MxODQwMTsKfQoKLmhsanMtYXR0ciwKLmhsanMtdmFyaWFibGUsCi5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLAouaGxqcy10eXBlLAouaGxqcy1zZWxlY3Rvci1jbGFzcywKLmhsanMtc2VsZWN0b3ItYXR0ciwKLmhsanMtc2VsZWN0b3ItcHNldWRvLAouaGxqcy1udW1iZXIgewogIGNvbG9yOiAjOTg2ODAxOwp9CgouaGxqcy1zeW1ib2wsCi5obGpzLWJ1bGxldCwKLmhsanMtbGluaywKLmhsanMtbWV0YSwKLmhsanMtc2VsZWN0b3ItaWQsCi5obGpzLXRpdGxlIHsKICBjb2xvcjogIzQwNzhmMjsKfQoKLmhsanMtZW1waGFzaXMgewogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQoKLmhsanMtbGluayB7CiAgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7Cn0K`;
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiMzODNhNDI7YmFja2dyb3VuZDojZmFmYWZhfS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2EwYTFhNztmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYSwuaGxqcy1rZXl3b3Jke2NvbG9yOiNhNjI2YTR9LmhsanMtZGVsZXRpb24sLmhsanMtbmFtZSwuaGxqcy1zZWN0aW9uLC5obGpzLXNlbGVjdG9yLXRhZywuaGxqcy1zdWJzdHtjb2xvcjojZTQ1NjQ5fS5obGpzLWxpdGVyYWx7Y29sb3I6IzAxODRiYn0uaGxqcy1hZGRpdGlvbiwuaGxqcy1hdHRyaWJ1dGUsLmhsanMtbWV0YSAuaGxqcy1zdHJpbmcsLmhsanMtcmVnZXhwLC5obGpzLXN0cmluZ3tjb2xvcjojNTBhMTRmfS5obGpzLWF0dHIsLmhsanMtbnVtYmVyLC5obGpzLXNlbGVjdG9yLWF0dHIsLmhsanMtc2VsZWN0b3ItY2xhc3MsLmhsanMtc2VsZWN0b3ItcHNldWRvLC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLC5obGpzLXR5cGUsLmhsanMtdmFyaWFibGV7Y29sb3I6Izk4NjgwMX0uaGxqcy1idWxsZXQsLmhsanMtbGluaywuaGxqcy1tZXRhLC5obGpzLXNlbGVjdG9yLWlkLC5obGpzLXN5bWJvbCwuaGxqcy10aXRsZXtjb2xvcjojNDA3OGYyfS5obGpzLWJ1aWx0X2luLC5obGpzLWNsYXNzIC5obGpzLXRpdGxlLC5obGpzLXRpdGxlLmNsYXNzX3tjb2xvcjojYzE4NDAxfS5obGpzLWVtcGhhc2lze2ZvbnQtc3R5bGU6aXRhbGljfS5obGpzLXN0cm9uZ3tmb250LXdlaWdodDo3MDB9LmhsanMtbGlua3t0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lfQ==`;

View File

@@ -1,5 +1,5 @@
module.exports = {
hash:"6608023b8053b48e0eec248644475e33", files: {
hash:"de3871f000c87478973d7cd0913bd3ff", files: {
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },

File diff suppressed because one or more lines are too long

View File

@@ -552,7 +552,7 @@ async function initialize(dispatch: Function) {
// / E2EE SETUP
// ----------------------------------------------------------------
await ShareService.instance().initialize(store, EncryptionService.instance());
await ShareService.instance().initialize(store);
reg.logger().info('Loading folders...');

View File

@@ -7,14 +7,13 @@ const { Keyboard } = require('react-native');
const dialogs = {};
dialogs.confirm = (parentComponent, message) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
dialogs.confirmRef = (ref, message) => {
if (!ref) throw new Error('ref is required');
return new Promise((resolve) => {
Keyboard.dismiss();
parentComponent.dialogbox.confirm({
ref.confirm({
content: message,
ok: {
@@ -32,6 +31,13 @@ dialogs.confirm = (parentComponent, message) => {
});
};
dialogs.confirm = (parentComponent, message) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
return dialogs.confirmRef(parentComponent.dialogBox, message);
};
dialogs.pop = (parentComponent, message, buttons, options = null) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');

View File

@@ -630,7 +630,7 @@ export default class BaseApplication {
BaseSyncTarget.dispatch = this.store().dispatch;
DecryptionWorker.instance().dispatch = this.store().dispatch;
ResourceFetcher.instance().dispatch = this.store().dispatch;
ShareService.instance().initialize(this.store(), EncryptionService.instance());
ShareService.instance().initialize(this.store());
}
deinitRedux() {

View File

@@ -900,11 +900,6 @@ 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

@@ -142,7 +142,7 @@ export default class JoplinServerApi {
}
if (sessionId) headers['X-API-AUTH'] = sessionId;
headers['X-API-MIN-VERSION'] = '2.5.0';
headers['X-API-MIN-VERSION'] = '2.1.4';
const fetchOptions: any = {};
fetchOptions.headers = headers;

View File

@@ -24,7 +24,6 @@ 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 { setPpkIfNotExist } from './services/e2ee/ppk';
const { sprintf } = require('sprintf-js');
const { Dirnames } = require('./services/synchronizer/utils/types');
@@ -421,52 +420,49 @@ export default class Synchronizer {
this.api().setTempDirName(Dirnames.Temp);
try {
let remoteInfo = await fetchSyncInfo(this.api());
const 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'));
remoteInfo = await fetchSyncInfo(this.api());
}
logger.info('Sync target is already setup - checking it...');
await this.migrationHandler().checkCanSync(remoteInfo);
const localInfo = await localSyncInfo();
logger.info('Sync target local info:', localInfo);
await setPpkIfNotExist(this.encryptionService(), localInfo, remoteInfo);
// console.info('LOCAL', localInfo);
// console.info('REMOTE', remoteInfo);
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());
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);
logger.info('Sync target is already setup - checking it...');
await this.migrationHandler().checkCanSync(remoteInfo);
const localInfo = await localSyncInfo();
logger.info('Sync target local info:', localInfo);
// console.info('LOCAL', localInfo);
// console.info('REMOTE', remoteInfo);
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());
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);
}
}
} catch (error) {
if (error.code === 'outdatedSyncTarget') {

View File

@@ -1,13 +1,11 @@
// 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

@@ -1,20 +0,0 @@
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

@@ -0,0 +1,183 @@
import shim from '../../shim';
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 EncryptionService from '../../services/e2ee/EncryptionService';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import MasterKey from '../../models/MasterKey';
import { reg } from '../../registry';
import Setting from '../../models/Setting';
const { useCallback, useEffect, useState } = shim.react();
type PasswordChecks = Record<string, boolean>;
export const useStats = () => {
const [stats, setStats] = useState<EncryptedItemsStats>({ encrypted: null, total: null });
const [statsUpdateTime, setStatsUpdateTime] = useState<number>(0);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const r = await BaseItem.encryptedItemsStats();
if (event.cancelled) return;
setStats(r);
}, [statsUpdateTime]);
useEffect(() => {
const iid = shim.setInterval(() => {
setStatsUpdateTime(Date.now());
}, 3000);
return () => {
clearInterval(iid);
};
}, []);
return stats;
};
export const decryptedStatText = (stats: EncryptedItemsStats) => {
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
const totalCount = stats.total !== null ? stats.total : '-';
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
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;
};
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);
}
return false;
};
export const reencryptData = async () => {
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
if (!ok) return;
await BaseItem.forceSyncAll();
void reg.waitForSyncFinishedThenSync();
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
alert(_('Your data is going to be re-encrypted and synced again.'));
};
export const dontReencryptData = () => {
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
};
export const useToggleShowDisabledMasterKeys = () => {
const [showDisabledMasterKeys, setShowDisabledMasterKeys] = useState<boolean>(false);
const toggleShowDisabledMasterKeys = () => {
setShowDisabledMasterKeys((current) => !current);
};
return { showDisabledMasterKeys, toggleShowDisabledMasterKeys };
};
export const onToggleEnabledClick = (mk: MasterKeyEntity) => {
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
};
export const onSavePasswordClick = (mk: MasterKeyEntity, passwords: Record<string, string>) => {
const password = passwords[mk.id];
if (!password) {
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
} else {
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
}
};
export const onMasterPasswordSave = (masterPasswordInput: string) => {
Setting.setValue('encryption.masterPassword', masterPasswordInput);
};
export const useInputMasterPassword = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string) => {
const [inputMasterPassword, setInputMasterPassword] = useState<string>('');
const onMasterPasswordSave = useCallback(async () => {
Setting.setValue('encryption.masterPassword', inputMasterPassword);
if (!(await masterPasswordIsValid(masterKeys, activeMasterKeyId, inputMasterPassword))) {
alert('Password is invalid. Please try again.');
}
}, [inputMasterPassword]);
const onMasterPasswordChange = useCallback((password: string) => {
setInputMasterPassword(password);
}, []);
return { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange };
};
export const useInputPasswords = (propsPasswords: Record<string, string>) => {
const [inputPasswords, setInputPasswords] = useState<Record<string, string>>(propsPasswords);
useEffect(() => {
setInputPasswords(propsPasswords);
}, [propsPasswords]);
const onInputPasswordChange = useCallback((mk: MasterKeyEntity, password: string) => {
setInputPasswords(current => {
return {
...current,
[mk.id]: password,
};
});
}, []);
return { inputPasswords, onInputPasswordChange };
};
export const usePasswordChecker = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string, passwords: Record<string, string>) => {
const [passwordChecks, setPasswordChecks] = useState<PasswordChecks>({});
const [masterPasswordKeys, setMasterPasswordKeys] = useState<PasswordChecks>({});
useAsyncEffect(async (event: AsyncEffectEvent) => {
const newPasswordChecks: PasswordChecks = {};
const newMasterPasswordKeys: PasswordChecks = {};
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
const password = await findMasterKeyPassword(EncryptionService.instance(), mk, passwords);
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
newPasswordChecks[mk.id] = ok;
newMasterPasswordKeys[mk.id] = password === masterPassword;
}
newPasswordChecks['master'] = await masterPasswordIsValid(masterKeys, activeMasterKeyId, masterPassword);
if (event.cancelled) return;
setPasswordChecks(newPasswordChecks);
setMasterPasswordKeys(newMasterPasswordKeys);
}, [masterKeys, masterPassword]);
return { passwordChecks, masterPasswordKeys };
};
export const upgradeMasterKey = async (masterKey: MasterKeyEntity, passwordChecks: PasswordChecks, passwords: Record<string, string>): Promise<string> => {
const passwordCheck = passwordChecks[masterKey.id];
if (!passwordCheck) {
return _('Please enter your password in the master key list below before upgrading the key.');
}
try {
const password = passwords[masterKey.id];
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
await MasterKey.save(newMasterKey);
void reg.waitForSyncFinishedThenSync();
return _('The master key has been upgraded successfully!');
} catch (error) {
return _('Could not upgrade master key: %s', error.message);
}
};

View File

@@ -1,196 +0,0 @@
import EncryptionService from '../../services/e2ee/EncryptionService';
import { _ } from '../../locale';
import BaseItem from '../../models/BaseItem';
import Setting from '../../models/Setting';
import MasterKey from '../../models/MasterKey';
import { reg } from '../../registry.js';
import shim from '../../shim';
import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import { findMasterKeyPassword } from '../../services/e2ee/utils';
class Shared {
private refreshStatsIID_: any;
public initialize(comp: any, props: any) {
comp.state = {
passwordChecks: {},
// Master keys that can be decrypted with the master password
// (normally all of them, but for legacy support we need this).
masterPasswordKeys: {},
stats: {
encrypted: null,
total: null,
},
passwords: Object.assign({}, props.passwords),
showDisabledMasterKeys: false,
masterPasswordInput: '',
};
comp.isMounted_ = false;
this.refreshStatsIID_ = null;
}
public async refreshStats(comp: any) {
const stats = await BaseItem.encryptedItemsStats();
comp.setState({
stats: stats,
});
}
public async toggleShowDisabledMasterKeys(comp: any) {
comp.setState({ showDisabledMasterKeys: !comp.state.showDisabledMasterKeys });
}
public async reencryptData() {
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
if (!ok) return;
await BaseItem.forceSyncAll();
void reg.waitForSyncFinishedThenSync();
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
alert(_('Your data is going to be re-encrypted and synced again.'));
}
public dontReencryptData() {
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
}
public async upgradeMasterKey(comp: any, masterKey: MasterKeyEntity) {
const passwordCheck = comp.state.passwordChecks[masterKey.id];
if (!passwordCheck) {
alert(_('Please enter your password in the master key list below before upgrading the key.'));
return;
}
try {
const password = comp.state.passwords[masterKey.id];
const newMasterKey = await EncryptionService.instance().reencryptMasterKey(masterKey, password, password);
await MasterKey.save(newMasterKey);
void reg.waitForSyncFinishedThenSync();
alert(_('The master key has been upgraded successfully!'));
} catch (error) {
alert(_('Could not upgrade master key: %s', error.message));
}
}
public componentDidMount(comp: any) {
this.componentDidUpdate(comp);
void this.refreshStats(comp);
if (this.refreshStatsIID_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
}
this.refreshStatsIID_ = shim.setInterval(() => {
if (!comp.isMounted_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
return;
}
void this.refreshStats(comp);
}, 3000);
}
public componentDidUpdate(comp: any, prevProps: any = null) {
if (prevProps && comp.props.passwords !== prevProps.passwords) {
comp.setState({ passwords: Object.assign({}, comp.props.passwords) });
}
if (!prevProps || comp.props.masterKeys !== prevProps.masterKeys || comp.props.passwords !== prevProps.passwords) {
comp.checkPasswords();
}
}
public componentWillUnmount() {
if (this.refreshStatsIID_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
}
}
public async masterPasswordIsValid(comp: any, masterPassword: string = null) {
const activeMasterKey = comp.props.masterKeys.find((mk: MasterKeyEntity) => mk.id === comp.props.activeMasterKeyId);
masterPassword = masterPassword === null ? comp.props.masterPassword : masterPassword;
if (activeMasterKey && masterPassword) {
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
}
return false;
}
public async checkPasswords(comp: any) {
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
for (let i = 0; i < comp.props.masterKeys.length; i++) {
const mk = comp.props.masterKeys[i];
const password = await findMasterKeyPassword(EncryptionService.instance(), mk);
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
passwordChecks[mk.id] = ok;
masterPasswordKeys[mk.id] = password === comp.props.masterPassword;
}
passwordChecks['master'] = await this.masterPasswordIsValid(comp);
comp.setState({ passwordChecks, masterPasswordKeys });
}
public masterPasswordStatus(comp: any) {
// Don't translate for now because that's temporary - later it should
// always be set and the label should be replaced by a "Change master
// password" button.
return comp.props.masterPassword ? 'Master password is set' : 'Master password is not set';
}
public decryptedStatText(comp: any) {
const stats = comp.state.stats;
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
const totalCount = stats.total !== null ? stats.total : '-';
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
return result;
}
public onSavePasswordClick(comp: any, mk: MasterKeyEntity) {
const password = comp.state.passwords[mk.id];
if (!password) {
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
} else {
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
}
comp.checkPasswords();
}
public onMasterPasswordChange(comp: any, value: string) {
comp.setState({ masterPasswordInput: value });
}
public onMasterPasswordSave(comp: any) {
Setting.setValue('encryption.masterPassword', comp.state.masterPasswordInput);
}
public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) {
const passwords = Object.assign({}, comp.state.passwords);
passwords[mk.id] = password;
comp.setState({ passwords: passwords });
}
public onToggleEnabledClick(_comp: any, mk: MasterKeyEntity) {
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
}
public 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;
}
}
const shared = new Shared();
export default shared;

View File

@@ -40,10 +40,6 @@ export default class FileApiDriverJoplinServer {
return true;
}
public get requiresPublicPrivateKeyPair() {
return true;
}
public requestRepeatCount() {
return 3;
}

View File

@@ -106,10 +106,6 @@ class FileApi {
return !!this.driver().supportsAccurateTimestamp;
}
public get requiresPublicPrivateKeyPair(): boolean {
return !!this.driver().requiresPublicPrivateKeyPair;
}
async fetchRemoteDateOffset_() {
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
const startTime = Date.now();

View File

@@ -25,6 +25,7 @@ function useEventListener(
const eventListener = (event: Event) => {
// eslint-disable-next-line no-extra-boolean-cast
if (!!savedHandler?.current) {
// @ts-ignore
savedHandler.current(event);
}
};

View File

@@ -10,7 +10,7 @@ import ItemChange from './ItemChange';
import ShareService from '../services/share/ShareService';
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
import { getEncryptionEnabled } from '../services/synchronizer/syncInfoUtils';
import JoplinError from '../JoplinError';
const JoplinError = require('../JoplinError.js');
const { sprintf } = require('sprintf-js');
const moment = require('moment');
@@ -25,7 +25,6 @@ export interface ItemThatNeedSync {
type_: ModelType;
updated_time: number;
encryption_applied: number;
share_id: string;
}
export interface ItemsThatNeedSyncResult {
@@ -34,6 +33,11 @@ export interface ItemsThatNeedSyncResult {
neverSyncedItemIds: string[];
}
export interface EncryptedItemsStats {
encrypted: number;
total: number;
}
export default class BaseItem extends BaseModel {
public static encryptionService_: any = null;
@@ -410,7 +414,6 @@ export default class BaseItem extends BaseModel {
const shownKeys = ItemClass.fieldNames();
shownKeys.push('type_');
const share = item.share_id ? await this.shareService().shareById(item.share_id) : null;
const serialized = await ItemClass.serialize(item, shownKeys);
if (!getEncryptionEnabled() || !ItemClass.encryptionSupported() || !itemCanBeEncrypted(item)) {
@@ -428,9 +431,7 @@ export default class BaseItem extends BaseModel {
let cipherText = null;
try {
cipherText = await this.encryptionService().encryptString(serialized, {
masterKeyId: share && share.master_key_id ? share.master_key_id : '',
});
cipherText = await this.encryptionService().encryptString(serialized);
} catch (error) {
const msg = [`Could not encrypt item ${item.id}`];
if (error && error.message) msg.push(error.message);
@@ -517,7 +518,7 @@ export default class BaseItem extends BaseModel {
return output;
}
static async encryptedItemsStats() {
public static async encryptedItemsStats(): Promise<EncryptedItemsStats> {
const classNames = this.encryptableItemClassNames();
let encryptedCount = 0;
let totalCount = 0;

View File

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

View File

@@ -1,6 +1,5 @@
import { BaseItemEntity } from '../../services/database/types';
export default function(_resource: BaseItemEntity): boolean {
return true;
// return !resource.is_shared && !resource.share_id;
export default function(resource: BaseItemEntity): boolean {
return !resource.is_shared && !resource.share_id;
}

View File

@@ -41,7 +41,6 @@
"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",
@@ -68,7 +67,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",
"sharp": "^0.26.2",
@@ -1063,15 +1062,6 @@
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==",
"dev": true
},
"node_modules/@types/node-rsa": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/node-rsa/-/node-rsa-1.1.1.tgz",
"integrity": "sha512-itzxtaBgk4OMbrCawVCvas934waMZWjW17v7EYgFVlfYS/cl0/P7KZdojWCq9SDJMI5cnLQLUP8ayhVCTY8TEg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@@ -1084,6 +1074,29 @@
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"node_modules/@types/react": {
"version": "17.0.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"node_modules/@types/stack-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@@ -2264,6 +2277,12 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true
},
"node_modules/csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
"dev": true
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -5650,6 +5669,7 @@
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.",
"optional": true,
"dependencies": {
"block-stream": "*",
@@ -5761,14 +5781,6 @@
"nopt": "bin/nopt.js"
}
},
"node_modules/node-rsa": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz",
"integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==",
"dependencies": {
"asn1": "^0.2.4"
}
},
"node_modules/noop-logger": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
@@ -9765,15 +9777,6 @@
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==",
"dev": true
},
"@types/node-rsa": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/node-rsa/-/node-rsa-1.1.1.tgz",
"integrity": "sha512-itzxtaBgk4OMbrCawVCvas934waMZWjW17v7EYgFVlfYS/cl0/P7KZdojWCq9SDJMI5cnLQLUP8ayhVCTY8TEg==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@@ -9786,6 +9789,29 @@
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
"dev": true
},
"@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"@types/react": {
"version": "17.0.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"@types/stack-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@@ -10753,6 +10779,12 @@
}
}
},
"csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
"dev": true
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -13539,14 +13571,6 @@
}
}
},
"node-rsa": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz",
"integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==",
"requires": {
"asn1": "^0.2.4"
}
},
"noop-logger": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
@@ -15679,7 +15703,6 @@
},
"uslug": {
"version": "git+ssh://git@github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
"integrity": "sha512-6zzxOsQp+hbOW4zeplEUhKXnBzYIrqYAVlPepBFz/u5q2OulN7tCmBKyWEzDxaiZOLYnUCTViDLazNoq1J6ciA==",
"from": "uslug@git+https://github.com/laurent22/uslug.git#emoji-support",
"requires": {
"node-emoji": "^1.10.0",

View File

@@ -19,7 +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",
"sharp": "^0.26.2",
@@ -63,7 +63,6 @@
"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

@@ -589,4 +589,25 @@ describe('reducer', function() {
expect(state.selectedFolderId).toEqual(null);
expect(state.selectedNoteIds[0]).toEqual(notes[1].id);
});
// tests for NOTE_UPDATE_ALL about issue #5447
it('should not change selectedNoteIds object when selections are not changed', async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
{
// Case 1. Selected notes are changed when one of selected notes is deleted.
let state = initTestState(folders, 0, notes, [0, 2, 4]);
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes.slice(0, 4), notesSource: 'test' });
const expected = [notes[0].id, notes[2].id].sort();
expect([...state.selectedNoteIds].sort()).toEqual(expected);
}
{
// Case 2. Selected notes and object identity are unchanged when notes are not changed.
let state = initTestState(folders, 0, notes, [0, 2, 4]);
const expected = state.selectedNoteIds;
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, notesSource: 'test' });
// Object identity is checked. Don't use toEqual() or toStrictEqual() here.
expect(state.selectedNoteIds).toBe(expected);
}
});
});

View File

@@ -433,7 +433,7 @@ function updateSelectedNotesFromExistingNotes(draft: Draft<State>) {
}
}
}
if (JSON.stringify(draft.selectedNoteIds) === JSON.stringify(newSelectedNoteIds)) return;
draft.selectedNoteIds = newSelectedNoteIds;
}

View File

@@ -12,6 +12,7 @@ interface MenuItem {
click: Function;
role?: any;
accelerator?: string;
enabled: boolean;
}
interface MenuItems {
@@ -78,6 +79,7 @@ export default class MenuUtils {
id: command.declaration.name,
label: this.service.label(commandName),
click: () => onClick(command.declaration.name),
enabled: true,
};
if (command.declaration.role) item.role = command.declaration.role;
@@ -132,10 +134,13 @@ export default class MenuUtils {
public pluginContextMenuItems(plugins: PluginStates, location: MenuItemLocation): MenuItem[] {
const output: MenuItem[] = [];
const pluginViewInfos = pluginUtils.viewInfosByType(plugins, 'menuItem');
const whenClauseContext = this.service.currentWhenClauseContext();
for (const info of pluginViewInfos) {
if (info.view.location !== location) continue;
output.push(this.commandToStatefulMenuItem(info.view.commandName));
const menuItem = this.commandToStatefulMenuItem(info.view.commandName);
menuItem.enabled = this.service.isEnabled(info.view.commandName, whenClauseContext);
output.push(menuItem);
}
if (output.length) output.splice(0, 0, { type: 'separator' } as any);

View File

@@ -18,8 +18,6 @@ export interface BaseItemEntity {
// AUTO-GENERATED BY packages/tools/generate-database-types.js
/*
@@ -52,7 +50,6 @@ export interface FolderEntity {
"parent_id"?: string
"is_shared"?: number
"share_id"?: string
"master_key_id"?: string
"type_"?: number
}
export interface ItemChangeEntity {
@@ -129,7 +126,6 @@ export interface NoteEntity {
"is_shared"?: number
"share_id"?: string
"conflict_original_id"?: string
"master_key_id"?: string
"type_"?: number
}
export interface NotesNormalizedEntity {

File diff suppressed because one or more lines are too long

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, { EncryptionMethod } from './EncryptionService';
import EncryptionService 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: EncryptionMethod.SJCL,
encryptionMethod: EncryptionService.METHOD_SJCL,
masterKeyId: '01234568abcdefgh01234568abcdefgh',
};
@@ -39,33 +39,33 @@ describe('services_EncryptionService', function() {
let hasThrown = false;
try {
await service.decryptMasterKeyContent(masterKey, 'wrongpassword');
await service.decryptMasterKey_(masterKey, 'wrongpassword');
} catch (error) {
hasThrown = true;
}
expect(hasThrown).toBe(true);
const decryptedMasterKey = await service.decryptMasterKeyContent(masterKey, '123456');
const decryptedMasterKey = await service.decryptMasterKey_(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: EncryptionMethod.SJCL2,
encryptionMethod: EncryptionService.METHOD_SJCL_2,
});
masterKey = await MasterKey.save(masterKey);
let upgradedMasterKey = await service.reencryptMasterKey(masterKey, '123456', '123456');
let upgradedMasterKey = await service.upgradeMasterKey(masterKey, '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.decryptMasterKeyContent(masterKey, '123456');
const plainTextNew = await service.decryptMasterKeyContent(upgradedMasterKey, '123456');
const plainTextOld = await service.decryptMasterKey_(masterKey, '123456');
const plainTextNew = await service.decryptMasterKey_(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: EncryptionMethod.SJCL2,
encryptionMethod: EncryptionService.METHOD_SJCL_2,
});
await checkThrowAsync(async () => await service.reencryptMasterKey(masterKey, '777', '777'));
await checkThrowAsync(async () => await service.upgradeMasterKey(masterKey, '777'));
}));
it('should require a checksum only for old master keys', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionMethod.SJCL2,
encryptionMethod: EncryptionService.METHOD_SJCL_2,
});
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: EncryptionMethod.SJCL4,
encryptionMethod: EncryptionService.METHOD_SJCL_4,
});
expect(!masterKey.checksum).toBe(true);
expect(!!masterKey.content).toBe(true);
const decryptedMasterKey = await service.decryptMasterKeyContent(masterKey, '123456');
const decryptedMasterKey = await service.decryptMasterKey_(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: EncryptionMethod.SJCL4,
encryptionMethod: EncryptionService.METHOD_SJCL_4,
});
const hasThrown = await checkThrowAsync(async () => await service.decryptMasterKeyContent(masterKey, 'wrong'));
const hasThrown = await checkThrowAsync(async () => await service.decryptMasterKey_(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: EncryptionMethod.SJCL2,
encryptionMethod: EncryptionService.METHOD_SJCL_2,
}));
const masterKey2 = await MasterKey.save(await service.generateMasterKey('123456', {
encryptionMethod: EncryptionMethod.SJCL,
encryptionMethod: EncryptionService.METHOD_SJCL,
}));
await MasterKey.save(await service.generateMasterKey('123456'));
@@ -164,22 +164,22 @@ describe('services_EncryptionService', function() {
{
const cipherText = await service.encryptString('some secret', {
encryptionMethod: EncryptionMethod.SJCL2,
encryptionMethod: EncryptionService.METHOD_SJCL_2,
});
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('some secret');
const header = await service.decodeHeaderString(cipherText);
expect(header.encryptionMethod).toBe(EncryptionMethod.SJCL2);
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_2);
}
{
const cipherText = await service.encryptString('some secret', {
encryptionMethod: EncryptionMethod.SJCL3,
encryptionMethod: EncryptionService.METHOD_SJCL_3,
});
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('some secret');
const header = await service.decodeHeaderString(cipherText);
expect(header.encryptionMethod).toBe(EncryptionMethod.SJCL3);
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_3);
}
}));
@@ -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_ = EncryptionMethod.SJCL;
service.defaultEncryptionMethod_ = EncryptionService.METHOD_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_ = EncryptionMethod.SJCL1a;
service.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
const cipherText = await service.encryptString('🐶🐶🐶'.substr(0,5));
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('🐶🐶🐶'.substr(0,5));
@@ -293,5 +293,4 @@ describe('services_EncryptionService', function() {
masterKey = await MasterKey.save(masterKey);
expect(service.isMasterKeyLoaded(masterKey)).toBe(false);
}));
});

View File

@@ -25,32 +25,16 @@ 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.
@@ -68,8 +52,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_ = EncryptionMethod.SJCL1a; // public because used in tests
private defaultMasterKeyEncryptionMethod_ = EncryptionMethod.SJCL4;
public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; // public because used in tests
private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
private headerTemplates_ = {
// Template version 1
@@ -95,8 +79,8 @@ export default class EncryptionService {
// changed easily since the chunk size is incorporated into the encrypted data.
this.chunkSize_ = 5000;
this.decryptedMasterKeys_ = {};
this.defaultEncryptionMethod_ = EncryptionMethod.SJCL1a;
this.defaultMasterKeyEncryptionMethod_ = EncryptionMethod.SJCL4;
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
this.headerTemplates_ = {
// Template version 1
@@ -113,10 +97,6 @@ export default class EncryptionService {
return this.instance_;
}
public get defaultMasterKeyEncryptionMethod() {
return this.defaultMasterKeyEncryptionMethod_;
}
loadedMasterKeysCount() {
return Object.keys(this.decryptedMasterKeys_).length;
}
@@ -155,7 +135,7 @@ export default class EncryptionService {
logger.info(`Loading master key: ${model.id}. Make active:`, makeActive);
this.decryptedMasterKeys_[model.id] = {
plainText: await this.decryptMasterKeyContent(model, password),
plainText: await this.decryptMasterKey_(model, password),
updatedTime: model.updated_time,
};
@@ -195,7 +175,7 @@ export default class EncryptionService {
return await this.randomHexString(64);
}
private async randomHexString(byteCount: number) {
async randomHexString(byteCount: number) {
const bytes: any[] = await shim.randomBytes(byteCount);
return bytes
.map(a => {
@@ -204,39 +184,32 @@ export default class EncryptionService {
.join('');
}
public masterKeysThatNeedUpgrading(masterKeys: MasterKeyEntity[]) {
return MasterKey.allWithoutEncryptionMethod(masterKeys, [this.defaultMasterKeyEncryptionMethod_, EncryptionMethod.Custom]);
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 async reencryptMasterKey(model: MasterKeyEntity, decryptionPassword: string, encryptionPassword: string, decryptOptions: EncryptOptions = null, encryptOptions: EncryptOptions = null): Promise<MasterKeyEntity> {
async upgradeMasterKey(model: MasterKeyEntity, decryptionPassword: string) {
const newEncryptionMethod = this.defaultMasterKeyEncryptionMethod_;
const plainText = await this.decryptMasterKeyContent(model, decryptionPassword, decryptOptions);
const newContent = await this.encryptMasterKeyContent(newEncryptionMethod, plainText, encryptionPassword, encryptOptions);
const plainText = await this.decryptMasterKey_(model, decryptionPassword);
const newContent = await this.encryptMasterKeyContent_(newEncryptionMethod, plainText, decryptionPassword);
return { ...model, ...newContent };
}
public async encryptMasterKeyContent(encryptionMethod: EncryptionMethod, hexaBytes: string, password: string, options: EncryptOptions = null): Promise<MasterKeyEntity> {
options = { ...options };
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);
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),
};
}
return {
checksum: checksum,
encryption_method: encryptionMethod,
content: cipherText,
};
}
private async generateMasterKeyContent_(password: string, options: EncryptOptions = null) {
async generateMasterKeyContent_(password: string, options: any = null) {
options = Object.assign({}, {
encryptionMethod: this.defaultMasterKeyEncryptionMethod_,
}, options);
@@ -244,10 +217,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, options);
return this.encryptMasterKeyContent_(options.encryptionMethod, hexaBytes, password);
}
public async generateMasterKey(password: string, options: EncryptOptions = null) {
async generateMasterKey(password: string, options: any = null) {
const model = await this.generateMasterKeyContent_(password, options);
const now = Date.now();
@@ -258,16 +231,9 @@ export default class EncryptionService {
return model;
}
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);
}
public async decryptMasterKey_(model: MasterKeyEntity, password: string): Promise<string> {
const plainText = await this.decrypt(model.encryption_method, password, model.content);
if (model.encryption_method === EncryptionMethod.SJCL2) {
if (model.encryption_method === EncryptionService.METHOD_SJCL_2) {
const checksum = this.sha256(plainText);
if (checksum !== model.checksum) throw new Error('Could not decrypt master key (checksum failed)');
}
@@ -277,7 +243,7 @@ export default class EncryptionService {
public async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
try {
await this.decryptMasterKeyContent(model, password);
await this.decryptMasterKey_(model, password);
} catch (error) {
return false;
}
@@ -291,14 +257,14 @@ export default class EncryptionService {
return error;
}
public async encrypt(method: EncryptionMethod, key: string, plainText: string): Promise<string> {
async encrypt(method: number, key: string, plainText: 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 === EncryptionMethod.SJCL) {
if (method === EncryptionService.METHOD_SJCL) {
try {
// Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/
return sjcl.json.encrypt(key, plainText, {
@@ -317,7 +283,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 === EncryptionMethod.SJCL1a) {
if (method === EncryptionService.METHOD_SJCL_1A) {
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
@@ -338,7 +304,7 @@ export default class EncryptionService {
// 2020-01-23: Deprectated - see above.
// Was used to encrypt master keys
if (method === EncryptionMethod.SJCL2) {
if (method === EncryptionService.METHOD_SJCL_2) {
try {
return sjcl.json.encrypt(key, plainText, {
v: 1,
@@ -353,7 +319,7 @@ export default class EncryptionService {
}
}
if (method === EncryptionMethod.SJCL3) {
if (method === EncryptionService.METHOD_SJCL_3) {
try {
// Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/
return sjcl.json.encrypt(key, plainText, {
@@ -371,7 +337,7 @@ export default class EncryptionService {
}
// Same as above but more secure (but slower) to encrypt master keys
if (method === EncryptionMethod.SJCL4) {
if (method === EncryptionService.METHOD_SJCL_4) {
try {
return sjcl.json.encrypt(key, plainText, {
v: 1,
@@ -389,7 +355,7 @@ export default class EncryptionService {
throw new Error(`Unknown encryption method: ${method}`);
}
async decrypt(method: EncryptionMethod, key: string, cipherText: string) {
async decrypt(method: number, key: string, cipherText: string) {
if (!method) throw new Error('Encryption method is required');
if (!key) throw new Error('Encryption key is required');
@@ -399,7 +365,7 @@ export default class EncryptionService {
try {
const output = sjcl.json.decrypt(key, cipherText);
if (method === EncryptionMethod.SJCL1a) {
if (method === EncryptionService.METHOD_SJCL_1A) {
return unescape(output);
} else {
return output;
@@ -410,13 +376,13 @@ export default class EncryptionService {
}
}
async encryptAbstract_(source: any, destination: any, options: EncryptOptions = null) {
async encryptAbstract_(source: any, destination: any, options: any = null) {
options = Object.assign({}, {
encryptionMethod: this.defaultEncryptionMethod(),
}, options);
const method = options.encryptionMethod;
const masterKeyId = options.masterKeyId ? options.masterKeyId : this.activeMasterKeyId();
const masterKeyId = this.activeMasterKeyId();
const masterKeyPlainText = this.loadedMasterKey(masterKeyId).plainText;
const header = {
@@ -446,7 +412,7 @@ export default class EncryptionService {
}
}
async decryptAbstract_(source: any, destination: any, options: EncryptOptions = null) {
async decryptAbstract_(source: any, destination: any, options: any = null) {
if (!options) options = {};
const header: any = await this.decodeHeaderSource_(source);
@@ -523,21 +489,21 @@ export default class EncryptionService {
};
}
public async encryptString(plainText: any, options: EncryptOptions = null): Promise<string> {
async encryptString(plainText: any, options: any = null) {
const source = this.stringReader_(plainText);
const destination = this.stringWriter_();
await this.encryptAbstract_(source, destination, options);
return destination.result();
}
public async decryptString(cipherText: any, options: EncryptOptions = null): Promise<string> {
async decryptString(cipherText: any, options: any = null) {
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: EncryptOptions = null) {
async encryptFile(srcPath: string, destPath: string, options: any = null) {
let source = await this.fileReader_(srcPath, 'base64');
let destination = await this.fileWriter_(destPath, 'ascii');
@@ -562,7 +528,7 @@ export default class EncryptionService {
await cleanUp();
}
async decryptFile(srcPath: string, destPath: string, options: EncryptOptions = null) {
async decryptFile(srcPath: string, destPath: string, options: any = null) {
let source = await this.fileReader_(srcPath, 'ascii');
let destination = await this.fileWriter_(destPath, 'base64');
@@ -651,8 +617,8 @@ export default class EncryptionService {
return output;
}
isValidEncryptionMethod(method: EncryptionMethod) {
return [EncryptionMethod.SJCL, EncryptionMethod.SJCL1a, EncryptionMethod.SJCL2, EncryptionMethod.SJCL3, EncryptionMethod.SJCL4].indexOf(method) >= 0;
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;
}
async itemIsEncrypted(item: any) {

View File

@@ -1,85 +0,0 @@
import { afterAllCleanUp, encryptionService, expectThrow, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import { decryptPrivateKey, generateKeyPair, ppkDecryptMasterKeyContent, ppkGenerateMasterKey, ppkPasswordIsValid, mkReencryptFromPasswordToPublicKey, mkReencryptFromPublicKeyToPassword } from './ppk';
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);
}));
});

View File

@@ -1,194 +0,0 @@
import * as NodeRSA from 'node-rsa';
import uuid from '../../uuid';
import { getActiveMasterKey, saveLocalSyncInfo, SyncInfo } from '../synchronizer/syncInfoUtils';
import EncryptionService, { EncryptionCustomHandler, EncryptionMethod } from './EncryptionService';
import { MasterKeyEntity } from './types';
import { getMasterPassword } from './utils';
interface PrivateKey {
encryptionMethod: EncryptionMethod;
ciphertext: string;
}
export type PublicKey = string;
export interface PublicPrivateKeyPair {
id: string;
publicKey: PublicKey;
privateKey: PrivateKey;
createdTime: number;
}
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);
}
const nodeRSAEncryptionScheme = 'pkcs1_oaep';
function nodeRSAOptions(): NodeRSA.Options {
return {
encryptionScheme: nodeRSAEncryptionScheme,
};
}
export async function generateKeyPair(encryptionService: EncryptionService, password: string): Promise<PublicPrivateKeyPair> {
const keys = new NodeRSA();
keys.setOptions(nodeRSAOptions());
keys.generateKeyPair(2048, 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 {
id: uuid.createNano(),
privateKey: await encryptPrivateKey(encryptionService, password, keys.exportKey('pkcs1-private-pem')),
publicKey: keys.exportKey('pkcs1-public-pem'),
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 generateKeyPairAndSave(encryptionService: EncryptionService, localInfo: SyncInfo, password: string): Promise<PublicPrivateKeyPair> {
localInfo.ppk = await generateKeyPair(encryptionService, password);
saveLocalSyncInfo(localInfo);
return localInfo.ppk;
}
export async function setPpkIfNotExist(service: EncryptionService, localInfo: SyncInfo, remoteInfo: SyncInfo) {
if (localInfo.ppk || remoteInfo.ppk) return;
const masterKey = getActiveMasterKey(localInfo);
if (!masterKey) return;
const password = getMasterPassword(false);
if (!password) return;
await generateKeyPairAndSave(service, localInfo, getMasterPassword());
}
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<NodeRSA> {
const keys = new NodeRSA();
keys.setOptions(nodeRSAOptions());
keys.importKey(ppk.publicKey, 'pkcs1-public-pem');
keys.importKey(await decryptPrivateKey(service, ppk.privateKey, password), 'pkcs1-private-pem');
return keys;
}
async function loadPublicKey(publicKey: PublicKey): Promise<NodeRSA> {
const keys = new NodeRSA();
keys.setOptions(nodeRSAOptions());
keys.importKey(publicKey, 'pkcs1-public-pem');
return keys;
}
export function ppkEncryptionHandler(ppkId: string, nodeRSA: NodeRSA): EncryptionCustomHandler {
interface Context {
nodeRSA: NodeRSA;
ppkId: string;
}
return {
context: {
nodeRSA,
ppkId,
},
encrypt: async (context: Context, hexaBytes: string, _password: string): Promise<string> => {
return JSON.stringify({
ppkId: context.ppkId,
scheme: nodeRSAEncryptionScheme,
ciphertext: context.nodeRSA.encrypt(hexaBytes, 'hex'),
});
},
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 context.nodeRSA.decrypt(Buffer.from(parsed.ciphertext, 'hex'), 'utf8');
},
};
}
// 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 };
}
// export async function reencryptFromPasswordToPassword(service: EncryptionService, masterKey: MasterKeyEntity, decryptionPassword: string, encryptionPassword: string): Promise<MasterKeyEntity> {
// const plainText = await service.decryptMasterKeyContent(masterKey, decryptionPassword);
// const newContent = await service.encryptMasterKeyContent(null, plainText, encryptionPassword);
// return { ...masterKey, ...newContent };
// }
// export async function ppkReencryptMasterKey(service: EncryptionService, masterKey: MasterKeyEntity, decryptionPpk: PublicPrivateKeyPair, decryptionPassword: string, encryptionPublicKey: PublicPrivateKeyPair): Promise<MasterKeyEntity> {
// const encryptionHandler = ppkEncryptionHandler(encryptionPublicKey.id, await loadPublicKey(encryptionPublicKey.publicKey));
// const decryptionHandler = ppkEncryptionHandler(decryptionPpk.id, await loadPpk(service, decryptionPpk, decryptionPassword));
// return service.reencryptMasterKey(masterKey, '', '', {
// encryptionHandler: decryptionHandler,
// }, {
// encryptionHandler: encryptionHandler,
// });
// }

View File

@@ -1,9 +1,8 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow, expectThrow } from '../../testing/test-utils';
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow } from '../../testing/test-utils';
import MasterKey from '../../models/MasterKey';
import { migrateMasterPassword, showMissingMasterKeyMessage, updateMasterPassword } from './utils';
import { migrateMasterPassword, showMissingMasterKeyMessage } from './utils';
import { localSyncInfo, setActiveMasterKeyId, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
import Setting from '../../models/Setting';
import { generateKeyPairAndSave, ppkPasswordIsValid } from './ppk';
describe('e2ee/utils', function() {
@@ -72,32 +71,4 @@ 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));
await generateKeyPairAndSave(encryptionService(), localSyncInfo(), 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 the master password and generate a PPK if not already set', async () => {
expect(localSyncInfo().ppk).toBeFalsy();
await updateMasterPassword('', '111111');
expect(Setting.value('encryption.masterPassword')).toBe('111111');
expect(await ppkPasswordIsValid(encryptionService(), localSyncInfo().ppk, '111111')).toBe(true);
});
});

View File

@@ -4,9 +4,7 @@ import MasterKey from '../../models/MasterKey';
import Setting from '../../models/Setting';
import { MasterKeyEntity } from './types';
import EncryptionService from './EncryptionService';
import { getActiveMasterKey, getActiveMasterKeyId, localSyncInfo, masterKeyEnabled, saveLocalSyncInfo, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
import JoplinError from '../../JoplinError';
import { generateKeyPairAndSave, pkReencryptPrivateKey, ppkPasswordIsValid } from './ppk';
import { getActiveMasterKey, getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
const logger = Logger.create('e2ee/utils');
@@ -105,7 +103,7 @@ export async function migrateMasterPassword() {
// previously any master key could be encrypted with any password, so to support
// this legacy case, we first check if the MK decrypts with the master password.
// If not, try with the master key specific password, if any is defined.
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity): Promise<string> {
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity, passwordCache: Record<string, string> = null): Promise<string> {
const masterPassword = Setting.value('encryption.masterPassword');
if (masterPassword && await service.checkMasterKeyPassword(masterKey, masterPassword)) {
logger.info('findMasterKeyPassword: Using master password');
@@ -114,7 +112,7 @@ export async function findMasterKeyPassword(service: EncryptionService, masterKe
logger.info('findMasterKeyPassword: No master password is defined - trying to get master key specific password');
const passwords = Setting.value('encryption.passwordCache');
const passwords = passwordCache ? passwordCache : Setting.value('encryption.passwordCache');
return passwords[masterKey.id];
}
@@ -163,112 +161,9 @@ export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterK
}
export function getDefaultMasterKey(): MasterKeyEntity {
const mk = getActiveMasterKey();
if (mk) return mk;
return MasterKey.latest();
}
// 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;
}
export async function updateMasterPassword(currentPassword: string, newPassword: string, waitForSyncFinishedThenSync: Function = null) {
const syncInfo = localSyncInfo();
if (currentPassword) {
const reencryptedMasterKeys: MasterKeyEntity[] = [];
let reencryptedPpk = null;
for (const mk of localSyncInfo().masterKeys) {
try {
reencryptedMasterKeys.push(await EncryptionService.instance().reencryptMasterKey(mk, currentPassword, newPassword));
} catch (error) {
error.message = `Master 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;
}
}
Setting.setValue('encryption.masterPassword', newPassword);
for (const mk of reencryptedMasterKeys) {
await MasterKey.save(mk);
}
if (reencryptedPpk) {
const syncInfo = localSyncInfo();
syncInfo.ppk = reencryptedPpk;
saveLocalSyncInfo(syncInfo);
}
} else {
if (syncInfo.ppk || syncInfo.masterKeys?.length) throw new Error('Previous password must be provided in order to reencrypt the encryption keys');
await generateKeyPairAndSave(EncryptionService.instance(), syncInfo, newPassword);
Setting.setValue('encryption.masterPassword', newPassword);
let mk = getActiveMasterKey();
if (!mk || masterKeyEnabled(mk)) {
mk = MasterKey.latest();
}
if (waitForSyncFinishedThenSync) void waitForSyncFinishedThenSync();
}
export enum MasterPasswordStatus {
Unknown = 0,
Loaded = 1,
NotSet = 2,
Invalid = 3,
Valid = 4,
}
export async function getMasterPasswordStatus(): Promise<MasterPasswordStatus> {
const password = getMasterPassword(false);
if (!password) return MasterPasswordStatus.NotSet;
try {
const isValid = await masterPasswordIsValid(password);
return isValid ? MasterPasswordStatus.Valid : MasterPasswordStatus.Invalid;
} catch (error) {
if (error.code === 'noKeyToDecrypt') return MasterPasswordStatus.Loaded;
throw error;
}
}
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): 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.
const ppk = localSyncInfo().ppk;
if (ppk) {
return ppkPasswordIsValid(EncryptionService.instance(), ppk, masterPassword);
}
const masterKey = getDefaultMasterKey();
if (masterKey) {
return EncryptionService.instance().checkMasterKeyPassword(masterKey, masterPassword);
}
throw new JoplinError('Cannot check master password validity as no key is present', 'noKeyToDecrypt');
return mk && masterKeyEnabled(mk) ? mk : null;
}

View File

@@ -1,19 +1,26 @@
import Note from '../../models/Note';
import { encryptionService, msleep, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import { msleep, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import ShareService from './ShareService';
import reducer from '../../reducer';
import { createStore } from 'redux';
import { NoteEntity } from '../database/types';
import Folder from '../../models/Folder';
import { localSyncInfo, setEncryptionEnabled } from '../synchronizer/syncInfoUtils';
import { generateKeyPair, generateKeyPairAndSave } from '../e2ee/ppk';
import MasterKey from '../../models/MasterKey';
import { MasterKeyEntity } from '../e2ee/types';
function mockService(api: any) {
function mockApi() {
return {
exec: (method: string, path: string = '', _query: Record<string, any> = null, _body: any = null, _headers: any = null, _options: any = null): Promise<any> => {
if (method === 'GET' && path === 'api/shares') return { items: [] } as any;
return null;
},
personalizedUserContentBaseUrl(_userId: string) {
},
};
}
function mockService() {
const service = new ShareService();
const store = createStore(reducer as any);
service.initialize(store, encryptionService(), api);
service.initialize(store, mockApi() as any);
return service;
}
@@ -25,17 +32,9 @@ describe('ShareService', function() {
done();
});
it('should not change the note user timestamps when sharing or unsharing', async () => {
it('should not change the note user timestamps when sharing or unsharing', (async () => {
let note = await Note.save({});
const service = mockService({
exec: (method: string, path: string = '', _query: Record<string, any> = null, _body: any = null, _headers: any = null, _options: any = null): Promise<any> => {
if (method === 'GET' && path === 'api/shares') return { items: [] } as any;
return null;
},
personalizedUserContentBaseUrl(_userId: string) {
},
});
const service = mockService();
await msleep(1);
await service.shareNote(note.id);
@@ -62,86 +61,6 @@ describe('ShareService', function() {
const noteReloaded = await Note.load(note.id);
checkTimestamps(note, noteReloaded);
}
});
function testShareFolderService(extraExecHandlers: Record<string, Function> = {}) {
return mockService({
exec: async (method: string, path: string, query: Record<string, any>, body: any) => {
if (extraExecHandlers[`${method} ${path}`]) return extraExecHandlers[`${method} ${path}`](query, body);
if (method === 'POST' && path === 'api/shares') {
return {
id: 'share_1',
};
}
throw new Error(`Unhandled: ${method} ${path}`);
},
});
}
async function testShareFolder(service: ShareService) {
const folder = await Folder.save({});
const note = await Note.save({ parent_id: folder.id });
const share = await service.shareFolder(folder.id);
expect(share.id).toBe('share_1');
expect((await Folder.load(folder.id)).share_id).toBe('share_1');
expect((await Note.load(note.id)).share_id).toBe('share_1');
return share;
}
it('should share a folder', async () => {
await testShareFolder(testShareFolderService());
});
it('should share a folder - E2EE', async () => {
setEncryptionEnabled(true);
const ppk = await generateKeyPairAndSave(encryptionService(), localSyncInfo(), '111111');
await testShareFolder(testShareFolderService());
expect((await MasterKey.all()).length).toBe(1);
const mk = (await MasterKey.all())[0];
const content = JSON.parse(mk.content);
expect(content.ppkId).toBe(ppk.id);
});
it('should add a recipient', async () => {
setEncryptionEnabled(true);
const ppk = await generateKeyPairAndSave(encryptionService(), localSyncInfo(), '111111');
const recipientPpk = await generateKeyPair(encryptionService(), '222222');
expect(ppk.id).not.toBe(recipientPpk.id);
let uploadedEmail: string = '';
let uploadedMasterKey: MasterKeyEntity = null;
const service = testShareFolderService({
'POST api/shares': (_query: Record<string, any>, body: any) => {
return {
id: 'share_1',
master_key_id: body.master_key_id,
};
},
'GET api/users/toto%40example.com/public_key': async (_query: Record<string, any>, _body: any) => {
return recipientPpk;
},
'POST api/shares/share_1/users': async (_query: Record<string, any>, body: any) => {
uploadedEmail = body.email;
uploadedMasterKey = JSON.parse(body.master_key);
},
});
const share = await testShareFolder(service);
await service.addShareRecipient(share.id, share.master_key_id, 'toto@example.com');
expect(uploadedEmail).toBe('toto@example.com');
const content = JSON.parse(uploadedMasterKey.content);
expect(content.ppkId).toBe(recipientPpk.id);
});
}));
});

View File

@@ -1,41 +1,18 @@
import { Store } from 'redux';
import JoplinServerApi from '../../JoplinServerApi';
import { _ } from '../../locale';
import Logger from '../../Logger';
import Folder from '../../models/Folder';
import MasterKey from '../../models/MasterKey';
import Note from '../../models/Note';
import Setting from '../../models/Setting';
import { FolderEntity } from '../database/types';
import EncryptionService from '../e2ee/EncryptionService';
import { PublicPrivateKeyPair, mkReencryptFromPasswordToPublicKey, mkReencryptFromPublicKeyToPassword } from '../e2ee/ppk';
import { MasterKeyEntity } from '../e2ee/types';
import { getMasterPassword } from '../e2ee/utils';
import { addMasterKey, getEncryptionEnabled, localSyncInfo } from '../synchronizer/syncInfoUtils';
import { ShareInvitation, State, stateRootKey, StateShare } from './reducer';
import { State, stateRootKey, StateShare } from './reducer';
const logger = Logger.create('ShareService');
export interface ApiShare {
id: string;
master_key_id: string;
}
function formatShareInvitations(invitations: any[]): ShareInvitation[] {
return invitations.map(inv => {
return {
...inv,
master_key: inv.master_key ? JSON.parse(inv.master_key) : null,
};
});
}
export default class ShareService {
private static instance_: ShareService;
private api_: JoplinServerApi = null;
private store_: Store<any> = null;
private encryptionService_: EncryptionService = null;
public static instance(): ShareService {
if (this.instance_) return this.instance_;
@@ -43,9 +20,8 @@ export default class ShareService {
return this.instance_;
}
public initialize(store: Store<any>, encryptionService: EncryptionService, api: JoplinServerApi = null) {
public initialize(store: Store<any>, api: JoplinServerApi = null) {
this.store_ = store;
this.encryptionService_ = encryptionService;
this.api_ = api;
}
@@ -80,40 +56,15 @@ export default class ShareService {
return this.api_;
}
public async shareFolder(folderId: string): Promise<ApiShare> {
public async shareFolder(folderId: string) {
const folder = await Folder.load(folderId);
if (!folder) throw new Error(`No such folder: ${folderId}`);
let folderMasterKey: MasterKeyEntity = null;
if (getEncryptionEnabled()) {
const syncInfo = localSyncInfo();
// Shouldn't happen
if (!syncInfo.ppk) throw new Error('Cannot share notebook because E2EE is enabled and no Public Private Key pair exists.');
folderMasterKey = await this.encryptionService_.generateMasterKey(getMasterPassword());
folderMasterKey = await MasterKey.save(folderMasterKey);
addMasterKey(syncInfo, folderMasterKey);
if (folder.parent_id) {
await Folder.save({ id: folder.id, parent_id: '' });
}
const newFolderProps: FolderEntity = {};
if (folder.parent_id) newFolderProps.parent_id = '';
if (folderMasterKey) newFolderProps.master_key_id = folderMasterKey.id;
if (Object.keys(newFolderProps).length) {
await Folder.save({
id: folder.id,
...newFolderProps,
});
}
const share = await this.api().exec('POST', 'api/shares', {}, {
folder_id: folderId,
master_key_id: folderMasterKey ? folderMasterKey.id : '',
});
const share = await this.api().exec('POST', 'api/shares', {}, { folder_id: folderId });
// Note: race condition if the share is created but the app crashes
// before setting share_id on the folder. See unshareFolder() for info.
@@ -223,34 +174,9 @@ export default class ShareService {
return this.state.shareInvitations;
}
private async userPublicKey(userEmail: string): Promise<PublicPrivateKeyPair> {
return this.api().exec('GET', `api/users/${encodeURIComponent(userEmail)}/public_key`);
}
public async addShareRecipient(shareId: string, masterKeyId: string, recipientEmail: string) {
let recipientMasterKey: MasterKeyEntity = null;
if (getEncryptionEnabled()) {
const syncInfo = localSyncInfo();
const masterKey = syncInfo.masterKeys.find(m => m.id === masterKeyId);
if (!masterKey) throw new Error(`Cannot find master key with ID "${masterKeyId}"`);
const recipientPublicKey: PublicPrivateKeyPair = await this.userPublicKey(recipientEmail);
if (!recipientPublicKey) throw new Error(_('Cannot share notebook with recipient %s because they do not have a public key. Ask them to create one from the menu "%s"', recipientEmail, 'Tools > Generate Public-Private Key pair'));
logger.info('Reencrypting master key with recipient public key', recipientPublicKey);
recipientMasterKey = await mkReencryptFromPasswordToPublicKey(
this.encryptionService_,
masterKey,
getMasterPassword(),
recipientPublicKey
);
}
public async addShareRecipient(shareId: string, recipientEmail: string) {
return this.api().exec('POST', `api/shares/${shareId}/users`, {}, {
email: recipientEmail,
master_key: JSON.stringify(recipientMasterKey),
});
}
@@ -274,24 +200,8 @@ export default class ShareService {
return this.api().exec('GET', 'api/share_users');
}
public async respondInvitation(shareUserId: string, masterKey: MasterKeyEntity, accept: boolean) {
logger.info('respondInvitation: ', shareUserId, accept);
public async respondInvitation(shareUserId: string, accept: boolean) {
if (accept) {
if (masterKey) {
const reencryptedMasterKey = await mkReencryptFromPublicKeyToPassword(
this.encryptionService_,
masterKey,
localSyncInfo().ppk,
getMasterPassword(),
getMasterPassword()
);
logger.info('respondInvitation: Key has been reencrypted using master password', reencryptedMasterKey);
await MasterKey.save(reencryptedMasterKey);
}
await this.api().exec('PATCH', `api/share_users/${shareUserId}`, null, { status: 1 });
} else {
await this.api().exec('PATCH', `api/share_users/${shareUserId}`, null, { status: 2 });
@@ -301,57 +211,15 @@ export default class ShareService {
public async refreshShareInvitations() {
const result = await this.loadShareInvitations();
const invitations = formatShareInvitations(result.items);
logger.info('Refresh share invitations:', invitations);
this.store.dispatch({
type: 'SHARE_INVITATION_SET',
shareInvitations: invitations,
shareInvitations: result.items,
});
}
public async shareById(id: string) {
const stateShare = this.state.shares.find(s => s.id === id);
if (stateShare) return stateShare;
const refreshedShares = await this.refreshShares();
const refreshedShare = refreshedShares.find(s => s.id === id);
if (!refreshedShare) throw new Error(`Could not find share with ID: ${id}`);
return refreshedShare;
}
// In most cases the share objects will already be part of the state, so
// this function checks there first. If the required share objects are not
// present, it refreshes them from the API.
public async sharesByIds(ids: string[]) {
const buildOutput = async (shares: StateShare[]) => {
const output: Record<string, StateShare> = {};
for (const share of shares) {
if (ids.includes(share.id)) output[share.id] = share;
}
return output;
};
let output = await buildOutput(this.state.shares);
if (Object.keys(output).length === ids.length) return output;
const refreshedShares = await this.refreshShares();
output = await buildOutput(refreshedShares);
if (Object.keys(output).length !== ids.length) {
logger.error('sharesByIds: Need:', ids);
logger.error('sharesByIds: Got:', Object.keys(refreshedShares));
throw new Error('Could not retrieve required share objects');
}
return output;
}
public async refreshShares(): Promise<StateShare[]> {
const result = await this.loadShares();
logger.info('Refreshed shares:', result);
this.store.dispatch({
type: 'SHARE_SET',
shares: result.items,
@@ -363,8 +231,6 @@ export default class ShareService {
public async refreshShareUsers(shareId: string) {
const result = await this.loadShareUsers(shareId);
logger.info('Refreshed share users:', result);
this.store.dispatch({
type: 'SHARE_USER_SET',
shareId: shareId,

View File

@@ -1,7 +1,6 @@
import { State as RootState } from '../../reducer';
import { Draft } from 'immer';
import { FolderEntity } from '../database/types';
import { MasterKeyEntity } from '../e2ee/types';
interface StateShareUserUser {
id: string;
@@ -26,13 +25,11 @@ export interface StateShare {
type: number;
folder_id: string;
note_id: string;
master_key_id: string;
user?: StateShareUserUser;
}
export interface ShareInvitation {
id: string;
master_key: MasterKeyEntity;
share: StateShare;
status: ShareUserStatus;
}

View File

@@ -1,56 +0,0 @@
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';
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 () => {
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,7 +2,6 @@ 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 {
@@ -15,11 +14,6 @@ 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
@@ -94,7 +88,6 @@ 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();
@@ -122,12 +115,10 @@ 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);
}
@@ -138,7 +129,6 @@ export class SyncInfo {
e2ee: this.e2ee_,
activeMasterKeyId: this.activeMasterKeyId_,
masterKeys: this.masterKeys,
ppk: this.ppk_,
};
}
@@ -152,7 +142,6 @@ 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) {
@@ -172,16 +161,6 @@ 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;
}
@@ -278,11 +257,3 @@ 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);
}

View File

@@ -1,22 +1,26 @@
import * as React from 'react';
import { NoteEntity, ResourceEntity } from './services/database/types';
let isTestingEnv_ = false;
// We need to ensure that there's only one instance of React being used by
// all the packages. In particular, the lib might need React to define
// generic hooks, but it shouldn't have React in its dependencies as that
// would cause the following error:
// We need to ensure that there's only one instance of React being used by all
// the packages. In particular, the lib might need React to define generic
// hooks, but it shouldn't have React in its dependencies as that would cause
// the following error:
//
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
//
// So instead, the **applications** include React as a dependency, then
// pass it to any other packages using the shim. Essentially, only one
// package should require React, and in our case that should be one of the
// applications (app-desktop, app-mobile, etc.) since we are sure they
// won't be dependency to other packages (unlike the lib which can be
// included anywhere).
let react_: any = null;
// So instead, the **applications** include React as a dependency, then pass it
// to any other packages using the shim. Essentially, only one package should
// require React, and in our case that should be one of the applications
// (app-desktop, app-mobile, etc.) since we are sure they won't be dependency to
// other packages (unlike the lib which can be included anywhere).
//
// Regarding the type - althought we import React, we only use it as a type
// using `typeof React`. This is just to get types in hooks.
//
// https://stackoverflow.com/a/42816077/561309
let react_: typeof React = null;
const shim = {
Geolocation: null as any,

View File

@@ -505,12 +505,11 @@ 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(password);
masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
} else { // Use the one already available
const masterKeys = await MasterKey.all();
@@ -518,12 +517,7 @@ async function loadEncryptionMasterKey(id: number = null, useExisting = false) {
masterKey = masterKeys[0];
}
const passwordCache = Setting.value('encryption.passwordCache');
passwordCache[masterKey.id] = password;
Setting.setValue('encryption.passwordCache', passwordCache);
await Setting.saveAll();
await service.loadMasterKey(masterKey, password, true);
await service.loadMasterKey(masterKey, '123456', 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,7 +14,6 @@ const theme: Theme = {
oddBackgroundColor: '#141517',
color: '#dddddd',
colorError: 'red',
colorCorrect: '#72b972',
colorWarn: '#9A5B00',
colorWarnUrl: '#ffff82',
colorFaded: '#999999', // For less important text

View File

@@ -11,7 +11,6 @@ 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,7 +13,6 @@ 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

@@ -1,75 +1 @@
/*
Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #abb2bf;
background: #282c34;
}
.hljs-keyword, .hljs-operator {
color: #F92672;
}
.hljs-pattern-match {
color: #F92672;
}
.hljs-pattern-match .hljs-constructor {
color: #61aeee;
}
.hljs-function {
color: #61aeee;
}
.hljs-function .hljs-params {
color: #A6E22E;
}
.hljs-function .hljs-params .hljs-typing {
color: #FD971F;
}
.hljs-module-access .hljs-module {
color: #7e57c2;
}
.hljs-constructor {
color: #e2b93d;
}
.hljs-constructor .hljs-string {
color: #9CCC65;
}
.hljs-comment, .hljs-quote {
color: #b18eb1;
font-style: italic;
}
.hljs-doctag, .hljs-formula {
color: #c678dd;
}
.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string {
color: #98c379;
}
.hljs-built_in, .hljs-class .hljs-title {
color: #e6c07b;
}
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number {
color: #d19a66;
}
.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-keyword,.hljs-operator,.hljs-pattern-match{color:#f92672}.hljs-function,.hljs-pattern-match .hljs-constructor{color:#61aeee}.hljs-function .hljs-params{color:#a6e22e}.hljs-function .hljs-params .hljs-typing{color:#fd971f}.hljs-module-access .hljs-module{color:#7e57c2}.hljs-constructor{color:#e2b93d}.hljs-constructor .hljs-string{color:#9ccc65}.hljs-comment,.hljs-quote{color:#b18eb1;font-style:italic}.hljs-doctag,.hljs-formula{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

View File

@@ -1,96 +1 @@
/*
Atom One Light by Daniel Gamage
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
base: #fafafa
mono-1: #383a42
mono-2: #686b77
mono-3: #a0a1a7
hue-1: #0184bb
hue-2: #4078f2
hue-3: #a626a4
hue-4: #50a14f
hue-5: #e45649
hue-5-2: #c91243
hue-6: #986801
hue-6-2: #c18401
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #383a42;
background: #fafafa;
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #a626a4;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e45649;
}
.hljs-literal {
color: #0184bb;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #50a14f;
}
.hljs-built_in,
.hljs-class .hljs-title {
color: #c18401;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #986801;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #4078f2;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

File diff suppressed because one or more lines are too long

View File

@@ -29,7 +29,7 @@
"markdown-it-sup": "^1.0.0",
"markdown-it-toc-done-right": "^4.1.0",
"md5": "^2.2.1",
"mermaid": "^8.10.2",
"mermaid": "^8.12.1",
"uslug": "git+https://github.com/laurent22/uslug.git#emoji-support"
},
"devDependencies": {
@@ -1998,7 +1998,8 @@
"node_modules/buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"node_modules/cache-base": {
"version": "1.0.1",
@@ -2029,15 +2030,6 @@
"node": ">=6"
}
},
"node_modules/camel-case": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
"dependencies": {
"no-case": "^2.2.0",
"upper-case": "^1.1.1"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -2135,25 +2127,6 @@
"node": ">=0.10.0"
}
},
"node_modules/clean-css": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
"integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==",
"dependencies": {
"source-map": "~0.6.0"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/clean-css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -2286,17 +2259,6 @@
"node": "*"
}
},
"node_modules/css-b64-images": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
"integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI=",
"bin": {
"css-b64-images": "bin/css-b64-images"
},
"engines": {
"node": "*"
}
},
"node_modules/cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@@ -2647,6 +2609,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -2797,6 +2760,11 @@
"node": ">=8"
}
},
"node_modules/dompurify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz",
"integrity": "sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw=="
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -2839,14 +2807,6 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
},
"node_modules/entity-decode": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entity-decode/-/entity-decode-2.0.2.tgz",
"integrity": "sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==",
"dependencies": {
"he": "^1.1.1"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -3542,14 +3502,6 @@
"node": ">=0.10.0"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/highlight.js": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.2.0.tgz",
@@ -3590,26 +3542,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"node_modules/html-minifier": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
"integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
"dependencies": {
"camel-case": "^3.0.0",
"clean-css": "^4.2.1",
"commander": "^2.19.0",
"he": "^1.2.0",
"param-case": "^2.1.1",
"relateurl": "^0.2.7",
"uglify-js": "^3.5.1"
},
"bin": {
"html-minifier": "cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -6045,11 +5977,6 @@
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
},
"node_modules/lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -6234,21 +6161,19 @@
"dev": true
},
"node_modules/mermaid": {
"version": "8.10.2",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.10.2.tgz",
"integrity": "sha512-Za5MrbAOMbEsyY4ONgGjfYz06sbwF1iNGRzp1sQqpOtvXxjxGu/J1jRJ8QyE9kD/D9zj1/KlRrYegWEvA7eZ5Q==",
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.12.1.tgz",
"integrity": "sha512-0UCcSF0FLoNcPBsRF4f9OIV32t41fV18//z8o3S+FDz2PbDA1CRGKdQF9IX84VP4Tv9kcgJI/oqJdcBEtB/GPA==",
"dependencies": {
"@braintree/sanitize-url": "^3.1.0",
"d3": "^5.7.0",
"dagre": "^0.8.4",
"d3": "^5.16.0",
"dagre": "^0.8.5",
"dagre-d3": "^0.6.4",
"entity-decode": "^2.0.2",
"graphlib": "^2.1.7",
"he": "^1.2.0",
"khroma": "^1.1.0",
"minify": "^4.1.1",
"moment-mini": "^2.22.1",
"stylis": "^3.5.2"
"dompurify": "2.3.1",
"graphlib": "^2.1.8",
"khroma": "^1.4.1",
"moment-mini": "^2.24.0",
"stylis": "^4.0.10"
}
},
"node_modules/micromatch": {
@@ -6294,26 +6219,6 @@
"node": ">=6"
}
},
"node_modules/minify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz",
"integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==",
"dependencies": {
"clean-css": "^4.1.6",
"css-b64-images": "~0.2.5",
"debug": "^4.1.0",
"html-minifier": "^4.0.0",
"terser": "^4.0.0",
"try-catch": "^2.0.0",
"try-to-catch": "^1.0.2"
},
"bin": {
"minify": "bin/minify.js"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -6365,7 +6270,8 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/nanomatch": {
"version": "1.2.13",
@@ -6401,14 +6307,6 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node_modules/no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
"dependencies": {
"lower-case": "^1.1.1"
}
},
"node_modules/node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@@ -6684,14 +6582,6 @@
"node": ">=6"
}
},
"node_modules/param-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
"dependencies": {
"no-case": "^2.2.0"
}
},
"node_modules/parse-json": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
@@ -6973,14 +6863,6 @@
"node": ">=0.10.0"
}
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -7661,6 +7543,7 @@
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -7670,6 +7553,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7876,9 +7760,9 @@
}
},
"node_modules/stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz",
"integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg=="
},
"node_modules/supports-color": {
"version": "5.5.0",
@@ -7948,30 +7832,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/terser": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"dependencies": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/terser/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -8084,19 +7944,6 @@
"node": ">=8"
}
},
"node_modules/try-catch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz",
"integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/try-to-catch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz",
"integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -8172,17 +8019,6 @@
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/uglify-js": {
"version": "3.13.9",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
"integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g==",
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -8262,11 +8098,6 @@
"node": ">=0.10.0"
}
},
"node_modules/upper-case": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
},
"node_modules/uri-js": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
@@ -10235,7 +10066,8 @@
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"cache-base": {
"version": "1.0.1",
@@ -10260,15 +10092,6 @@
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
},
"camel-case": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
"requires": {
"no-case": "^2.2.0",
"upper-case": "^1.1.1"
}
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -10347,21 +10170,6 @@
}
}
},
"clean-css": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
"integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==",
"requires": {
"source-map": "~0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -10475,11 +10283,6 @@
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
},
"css-b64-images": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
"integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI="
},
"cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@@ -10811,6 +10614,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
@@ -10921,6 +10725,11 @@
}
}
},
"dompurify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz",
"integrity": "sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw=="
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -10957,14 +10766,6 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
},
"entity-decode": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entity-decode/-/entity-decode-2.0.2.tgz",
"integrity": "sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==",
"requires": {
"he": "^1.1.1"
}
},
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -11512,11 +11313,6 @@
}
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"highlight.js": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.2.0.tgz",
@@ -11548,20 +11344,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"html-minifier": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
"integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
"requires": {
"camel-case": "^3.0.0",
"clean-css": "^4.2.1",
"commander": "^2.19.0",
"he": "^1.2.0",
"param-case": "^2.1.1",
"relateurl": "^0.2.7",
"uglify-js": "^3.5.1"
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -13427,11 +13209,6 @@
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
},
"lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -13594,21 +13371,19 @@
"dev": true
},
"mermaid": {
"version": "8.10.2",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.10.2.tgz",
"integrity": "sha512-Za5MrbAOMbEsyY4ONgGjfYz06sbwF1iNGRzp1sQqpOtvXxjxGu/J1jRJ8QyE9kD/D9zj1/KlRrYegWEvA7eZ5Q==",
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.12.1.tgz",
"integrity": "sha512-0UCcSF0FLoNcPBsRF4f9OIV32t41fV18//z8o3S+FDz2PbDA1CRGKdQF9IX84VP4Tv9kcgJI/oqJdcBEtB/GPA==",
"requires": {
"@braintree/sanitize-url": "^3.1.0",
"d3": "^5.7.0",
"dagre": "^0.8.4",
"d3": "^5.16.0",
"dagre": "^0.8.5",
"dagre-d3": "^0.6.4",
"entity-decode": "^2.0.2",
"graphlib": "^2.1.7",
"he": "^1.2.0",
"khroma": "^1.1.0",
"minify": "^4.1.1",
"moment-mini": "^2.22.1",
"stylis": "^3.5.2"
"dompurify": "2.3.1",
"graphlib": "^2.1.8",
"khroma": "^1.4.1",
"moment-mini": "^2.24.0",
"stylis": "^4.0.10"
}
},
"micromatch": {
@@ -13642,20 +13417,6 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"minify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz",
"integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==",
"requires": {
"clean-css": "^4.1.6",
"css-b64-images": "~0.2.5",
"debug": "^4.1.0",
"html-minifier": "^4.0.0",
"terser": "^4.0.0",
"try-catch": "^2.0.0",
"try-to-catch": "^1.0.2"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -13700,7 +13461,8 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"nanomatch": {
"version": "1.2.13",
@@ -13733,14 +13495,6 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
"requires": {
"lower-case": "^1.1.1"
}
},
"node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@@ -13951,14 +13705,6 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"param-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
"requires": {
"no-case": "^2.2.0"
}
},
"parse-json": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
@@ -14172,11 +13918,6 @@
"safe-regex": "^1.1.0"
}
},
"relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -14731,6 +14472,7 @@
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -14739,7 +14481,8 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
@@ -14905,9 +14648,9 @@
"dev": true
},
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz",
"integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg=="
},
"supports-color": {
"version": "5.5.0",
@@ -14961,23 +14704,6 @@
"supports-hyperlinks": "^2.0.0"
}
},
"terser": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.12"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -15068,16 +14794,6 @@
"punycode": "^2.1.1"
}
},
"try-catch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz",
"integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg=="
},
"try-to-catch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz",
"integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA=="
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -15134,11 +14850,6 @@
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"uglify-js": {
"version": "3.13.9",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
"integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g=="
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -15201,11 +14912,6 @@
}
}
},
"upper-case": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
},
"uri-js": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
@@ -15229,7 +14935,6 @@
},
"uslug": {
"version": "git+ssh://git@github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
"integrity": "sha512-6zzxOsQp+hbOW4zeplEUhKXnBzYIrqYAVlPepBFz/u5q2OulN7tCmBKyWEzDxaiZOLYnUCTViDLazNoq1J6ciA==",
"from": "uslug@git+https://github.com/laurent22/uslug.git#emoji-support",
"requires": {
"node-emoji": "^1.10.0",

View File

@@ -45,7 +45,7 @@
"markdown-it-sup": "^1.0.0",
"markdown-it-toc-done-right": "^4.1.0",
"md5": "^2.2.1",
"mermaid": "^8.10.2",
"mermaid": "^8.12.1",
"uslug": "git+https://github.com/laurent22/uslug.git#emoji-support"
},
"gitHead": "80c0089d2c52aff608b2bea74389de5a7f12f2e2"

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/server",
"version": "2.4.3",
"version": "2.4.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/server",
"version": "2.4.3",
"version": "2.4.7",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@koa/cors": "^3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.5.0",
"version": "2.4.7",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",

Binary file not shown.

View File

@@ -48,6 +48,9 @@ export interface EnvVariables {
BUSINESS_EMAIL?: string;
COOKIES_SECURE?: string;
SLOW_QUERY_LOG_ENABLED?: string;
SLOW_QUERY_LOG_MIN_DURATION?: string; // ms
}
let runningInDocker_: boolean = false;
@@ -56,6 +59,17 @@ export function runningInDocker(): boolean {
return runningInDocker_;
}
function envParseBool(s: string): boolean {
return s === '1';
}
function envParseInt(s: string, defaultValue: number = null): number {
if (!s) return defaultValue === null ? 0 : defaultValue;
const output = Number(s);
if (isNaN(output)) throw new Error(`Invalid number: ${s}`);
return output;
}
function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables): string {
if (env.POSTGRES_HOST) {
// When running within Docker, the app localhost is different from the
@@ -72,8 +86,16 @@ function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables): strin
}
function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): DatabaseConfig {
const baseConfig: DatabaseConfig = {
client: DatabaseConfigClient.Null,
name: '',
slowQueryLogEnabled: envParseBool(env.SLOW_QUERY_LOG_ENABLED),
slowQueryLogMinDuration: envParseInt(env.SLOW_QUERY_LOG_MIN_DURATION, 10000),
};
if (env.DB_CLIENT === 'pg') {
return {
...baseConfig,
client: DatabaseConfigClient.PostgreSQL,
name: env.POSTGRES_DATABASE || 'joplin',
user: env.POSTGRES_USER || 'joplin',
@@ -84,6 +106,7 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): Dat
}
return {
...baseConfig,
client: DatabaseConfigClient.SQLite,
name: env.SQLITE_DATABASE,
asyncStackTraces: true,

View File

@@ -104,25 +104,63 @@ export async function waitForConnection(dbConfig: DatabaseConfig): Promise<Conne
}
}
function makeSlowQueryHandler(duration: number, connection: any, sql: string, bindings: any[]) {
return setTimeout(() => {
try {
logger.warn(`Slow query (${duration}ms+):`, connection.raw(sql, bindings).toString());
} catch (error) {
logger.error('Could not log slow query', { sql, bindings }, error);
}
}, duration);
}
export function setupSlowQueryLog(connection: DbConnection, slowQueryLogMinDuration: number) {
interface QueryInfo {
timeoutId: any;
startTime: number;
}
const queryInfos: Record<any, QueryInfo> = {};
// These queries do not return a response, so "query-response" is not
// called.
const ignoredQueries = /^BEGIN|SAVEPOINT|RELEASE SAVEPOINT|COMMIT|ROLLBACK/gi;
connection.on('query', (data) => {
const sql: string = data.sql;
if (!sql || sql.match(ignoredQueries)) return;
const timeoutId = makeSlowQueryHandler(slowQueryLogMinDuration, connection, sql, data.bindings);
queryInfos[data.__knexQueryUid] = {
timeoutId,
startTime: Date.now(),
};
});
connection.on('query-response', (_response, data) => {
const q = queryInfos[data.__knexQueryUid];
if (q) {
clearTimeout(q.timeoutId);
delete queryInfos[data.__knexQueryUid];
}
});
connection.on('query-error', (_response, data) => {
const q = queryInfos[data.__knexQueryUid];
if (q) {
clearTimeout(q.timeoutId);
delete queryInfos[data.__knexQueryUid];
}
});
}
export async function connectDb(dbConfig: DatabaseConfig): Promise<DbConnection> {
const connection = knex(makeKnexConfig(dbConfig));
const debugSlowQueries = false;
if (debugSlowQueries) {
const startTimes: Record<string, number> = {};
const slowQueryDuration = 10;
connection.on('query', (data) => {
startTimes[data.__knexQueryUid] = Date.now();
});
connection.on('query-response', (_response, data) => {
const duration = Date.now() - startTimes[data.__knexQueryUid];
if (duration < slowQueryDuration) return;
console.info(`SQL: ${data.sql} (${duration}ms)`);
});
if (dbConfig.slowQueryLogEnabled) {
setupSlowQueryLog(connection, dbConfig.slowQueryLogMinDuration);
}
return connection;

View File

@@ -42,6 +42,13 @@ async function handleSqliteInProdNotification(ctx: AppContext) {
}
}
function levelClassName(level: NotificationLevel): string {
if (level === NotificationLevel.Important) return 'is-warning';
if (level === NotificationLevel.Normal) return 'is-info';
if (level === NotificationLevel.Error) return 'is-danger';
throw new Error(`Unknown level: ${level}`);
}
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
const markdownIt = new MarkdownIt();
@@ -52,7 +59,7 @@ async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[
views.push({
id: n.id,
messageHtml: markdownIt.render(n.message),
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
levelClassName: levelClassName(n.level),
closeUrl: notificationModel.closeUrl(n.id),
});
}

View File

@@ -2,6 +2,7 @@ import { routeResponseFormat, Response, RouteResponseFormat, execRequest } from
import { AppContext, Env } from '../utils/types';
import { isView, View } from '../services/MustacheService';
import config from '../config';
import { userIp } from '../utils/requestUtils';
export default async function(ctx: AppContext) {
const requestStartTime = Date.now();
@@ -26,9 +27,9 @@ export default async function(ctx: AppContext) {
}
} catch (error) {
if (error.httpCode >= 400 && error.httpCode < 500) {
ctx.joplin.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
ctx.joplin.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + `: ${userIp(ctx)}: ${error.message}`);
} else {
ctx.joplin.appLogger().error(error);
ctx.joplin.appLogger().error(userIp(ctx), error);
}
// Uncomment this when getting HTML blobs as errors while running tests.

View File

@@ -1,22 +0,0 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';
export async function up(db: DbConnection): Promise<any> {
await db.schema.alterTable('share_users', (table: Knex.CreateTableBuilder) => {
table.text('master_key', 'mediumtext').defaultTo('').notNullable();
});
await db.schema.alterTable('shares', (table: Knex.CreateTableBuilder) => {
table.string('master_key_id', 32).defaultTo('').notNullable();
});
}
export async function down(db: DbConnection): Promise<any> {
await db.schema.alterTable('share_users', (table: Knex.CreateTableBuilder) => {
table.dropColumn('master_key');
});
await db.schema.alterTable('shares', (table: Knex.CreateTableBuilder) => {
table.dropColumn('master_key_id');
});
}

View File

@@ -7,6 +7,9 @@ import { Models } from './factory';
import * as EventEmitter from 'events';
import { Config } from '../utils/types';
import personalizedUserContentBaseUrl from '@joplin/lib/services/joplinServer/personalizedUserContentBaseUrl';
import Logger from '@joplin/lib/Logger';
const logger = Logger.create('BaseModel');
export interface SaveOptions {
isNew?: boolean;
@@ -163,36 +166,38 @@ export default abstract class BaseModel<T> {
//
// The `name` argument is only for debugging, so that any stuck transaction
// can be more easily identified.
protected async withTransaction<T>(fn: Function, name: string = null): Promise<T> {
const debugTransaction = false;
protected async withTransaction<T>(fn: Function, name: string): Promise<T> {
const debugSteps = false;
const debugTimeout = true;
const timeoutMs = 10000;
const debugTimerId = debugTransaction ? setTimeout(() => {
console.info('Transaction did not complete:', name, txIndex);
}, 5000) : null;
let txIndex = 0;
const txIndex = await this.transactionHandler_.start();
const debugTimerId = debugTimeout ? setTimeout(() => {
logger.error(`Transaction #${txIndex} did not complete:`, name);
logger.error('Transaction stack:');
logger.error(this.transactionHandler_.stackInfo);
}, timeoutMs) : null;
if (debugTransaction) console.info('START', name, txIndex);
txIndex = await this.transactionHandler_.start(name);
if (debugSteps) console.info('START', name, txIndex);
let output: T = null;
try {
output = await fn();
} catch (error) {
if (debugSteps) console.info('ROLLBACK', name, txIndex);
await this.transactionHandler_.rollback(txIndex);
if (debugTransaction) {
console.info('ROLLBACK', name, txIndex);
clearTimeout(debugTimerId);
}
throw error;
} finally {
if (debugTimerId) clearTimeout(debugTimerId);
}
if (debugTransaction) {
console.info('COMMIT', name, txIndex);
clearTimeout(debugTimerId);
}
if (debugSteps) console.info('COMMIT', name, txIndex);
await this.transactionHandler_.commit(txIndex);
return output;

View File

@@ -34,7 +34,7 @@ export default class ItemResourceModel extends BaseModel<ItemResource> {
resource_id: resourceId,
});
}
});
}, 'ItemResourceModel::addResourceIds');
}
public async byItemId(itemId: Uuid): Promise<string[]> {

View File

@@ -48,7 +48,7 @@ export default class NotificationModel extends BaseModel<KeyValue> {
value: this.serializeValue(value),
type,
});
});
}, 'KeyValueModel::setValue');
}
public async value<T>(key: string, defaultValue: Value = null): Promise<T> {

View File

@@ -1,8 +1,10 @@
import { Notification, NotificationLevel, Uuid } from '../services/database/types';
import { ErrorUnprocessableEntity } from '../utils/errors';
import uuidgen from '../utils/uuidgen';
import BaseModel, { ValidateOptions } from './BaseModel';
export enum NotificationKey {
Any = 'any',
ConfirmEmail = 'confirmEmail',
PasswordSet = 'passwordSet',
EmailConfirmed = 'emailConfirmed',
@@ -52,6 +54,10 @@ export default class NotificationModel extends BaseModel<Notification> {
level: NotificationLevel.Normal,
message: 'Thank you! Your account has been successfully upgraded to Pro.',
},
[NotificationKey.Any]: {
level: NotificationLevel.Normal,
message: '',
},
};
const type = notificationTypes[key];
@@ -72,7 +78,9 @@ export default class NotificationModel extends BaseModel<Notification> {
}
}
return this.save({ key, message, level, owner_id: userId });
const actualKey = key === NotificationKey.Any ? `any_${uuidgen()}` : key;
return this.save({ key: actualKey, message, level, owner_id: userId });
}
public async markAsRead(userId: Uuid, key: NotificationKey): Promise<void> {

View File

@@ -67,20 +67,6 @@ describe('ShareModel', function() {
expect(shares3.length).toBe(1);
expect(shares3.find(s => s.folder_id === '000000000000000000000000000000F1')).toBeTruthy();
const participatedShares1 = await models().share().participatedSharesByUser(user1.id, ShareType.Folder);
const participatedShares2 = await models().share().participatedSharesByUser(user2.id, ShareType.Folder);
const participatedShares3 = await models().share().participatedSharesByUser(user3.id, ShareType.Folder);
expect(participatedShares1.length).toBe(1);
expect(participatedShares1[0].owner_id).toBe(user2.id);
expect(participatedShares1[0].folder_id).toBe('000000000000000000000000000000F2');
expect(participatedShares2.length).toBe(0);
expect(participatedShares3.length).toBe(1);
expect(participatedShares3[0].owner_id).toBe(user1.id);
expect(participatedShares3[0].folder_id).toBe('000000000000000000000000000000F1');
});
test('should generate only one link per shared note', async function() {
@@ -92,8 +78,8 @@ describe('ShareModel', function() {
},
});
const share1 = await models().share().shareNote(user1, '00000000000000000000000000000001', '');
const share2 = await models().share().shareNote(user1, '00000000000000000000000000000001', '');
const share1 = await models().share().shareNote(user1, '00000000000000000000000000000001');
const share2 = await models().share().shareNote(user1, '00000000000000000000000000000001');
expect(share1.id).toBe(share2.id);
});
@@ -107,7 +93,7 @@ describe('ShareModel', function() {
},
});
await models().share().shareNote(user1, '00000000000000000000000000000001', '');
await models().share().shareNote(user1, '00000000000000000000000000000001');
const noteItem = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');
await models().item().delete(noteItem.id);
expect(await models().item().load(noteItem.id)).toBeFalsy();

View File

@@ -60,7 +60,6 @@ export default class ShareModel extends BaseModel<Share> {
if (object.folder_id) output.folder_id = object.folder_id;
if (object.owner_id) output.owner_id = object.owner_id;
if (object.note_id) output.note_id = object.note_id;
if (object.master_key_id) output.master_key_id = object.master_key_id;
return output;
}
@@ -149,20 +148,6 @@ export default class ShareModel extends BaseModel<Share> {
return query;
}
public async participatedSharesByUser(userId: Uuid, type: ShareType = null): Promise<Share[]> {
const query = this.db(this.tableName)
.select(this.defaultFields)
.whereIn('id', this.db('share_users')
.select('share_id')
.where('user_id', '=', userId)
.andWhere('status', '=', ShareUserStatus.Accepted
));
if (type) void query.andWhere('type', '=', type);
return query;
}
// Returns all user IDs concerned by the share. That includes all the users
// the folder has been shared with, as well as the folder owner.
public async allShareUserIds(share: Share): Promise<Uuid[]> {
@@ -330,41 +315,39 @@ export default class ShareModel extends BaseModel<Share> {
for (const item of items) {
await this.models().userItem().add(userId, item.id);
}
});
}, 'ShareModel::createSharedFolderUserItems');
}
public async shareFolder(owner: User, folderId: string, masterKeyId: string): Promise<Share> {
public async shareFolder(owner: User, folderId: string): Promise<Share> {
const folderItem = await this.models().item().loadByJopId(owner.id, folderId);
if (!folderItem) throw new ErrorNotFound(`No such folder: ${folderId}`);
const share = await this.models().share().byUserAndItemId(owner.id, folderItem.id);
if (share) return share;
const shareToSave: Share = {
const shareToSave = {
type: ShareType.Folder,
item_id: folderItem.id,
owner_id: owner.id,
folder_id: folderId,
master_key_id: masterKeyId,
};
await this.checkIfAllowed(owner, AclAction.Create, shareToSave);
return super.save(shareToSave);
}
public async shareNote(owner: User, noteId: string, masterKeyId: string): Promise<Share> {
public async shareNote(owner: User, noteId: string): Promise<Share> {
const noteItem = await this.models().item().loadByJopId(owner.id, noteId);
if (!noteItem) throw new ErrorNotFound(`No such note: ${noteId}`);
const existingShare = await this.byItemId(noteItem.id);
if (existingShare) return existingShare;
const shareToSave: Share = {
const shareToSave = {
type: ShareType.Note,
item_id: noteItem.id,
owner_id: owner.id,
note_id: noteId,
master_key_id: masterKeyId,
};
await this.checkIfAllowed(owner, AclAction.Create, shareToSave);

View File

@@ -80,14 +80,14 @@ export default class ShareUserModel extends BaseModel<ShareUser> {
return this.db(this.tableName).where(link).first();
}
public async shareWithUserAndAccept(share: Share, shareeId: Uuid, masterKey: string = '') {
await this.models().shareUser().addById(share.id, shareeId, masterKey);
public async shareWithUserAndAccept(share: Share, shareeId: Uuid) {
await this.models().shareUser().addById(share.id, shareeId);
await this.models().shareUser().setStatus(share.id, shareeId, ShareUserStatus.Accepted);
}
public async addById(shareId: Uuid, userId: Uuid, masterKey: string): Promise<ShareUser> {
public async addById(shareId: Uuid, userId: Uuid): Promise<ShareUser> {
const user = await this.models().user().load(userId);
return this.addByEmail(shareId, user.email, masterKey);
return this.addByEmail(shareId, user.email);
}
public async byShareAndEmail(shareId: Uuid, userEmail: string): Promise<ShareUser> {
@@ -100,7 +100,7 @@ export default class ShareUserModel extends BaseModel<ShareUser> {
.first();
}
public async addByEmail(shareId: Uuid, userEmail: string, masterKey: string): Promise<ShareUser> {
public async addByEmail(shareId: Uuid, userEmail: string): Promise<ShareUser> {
const share = await this.models().share().load(shareId);
if (!share) throw new ErrorNotFound(`No such share: ${shareId}`);
@@ -110,7 +110,6 @@ export default class ShareUserModel extends BaseModel<ShareUser> {
return this.save({
share_id: shareId,
user_id: user.id,
master_key: masterKey,
});
}
@@ -127,7 +126,7 @@ export default class ShareUserModel extends BaseModel<ShareUser> {
}
return this.save({ ...shareUser, status });
});
}, 'ShareUserModel::setStatus');
}
public async deleteByShare(share: Share): Promise<void> {

View File

@@ -91,14 +91,14 @@ export default class SubscriptionModel extends BaseModel<Subscription> {
last_payment_time: now,
last_payment_failed_time: 0,
});
});
}, 'SubscriptionModel::handlePayment');
} else {
// We only update the payment failed time if it's not already set
// since the only thing that matter is the first time the payment
// failed.
//
// We don't update the user can_upload and enabled properties here
// because it's done after a few days from CronService.
// because it's done after a few days from TaskService.
if (!sub.last_payment_failed_time) {
const user = await this.models().user().load(sub.user_id, { fields: ['email', 'id', 'full_name'] });
@@ -145,7 +145,7 @@ export default class SubscriptionModel extends BaseModel<Subscription> {
});
return { user, subscription };
});
}, 'SubscriptionModel::saveUserAndSubscription');
}
public async toggleSoftDelete(id: number, isDeleted: boolean) {

View File

@@ -70,7 +70,7 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
await this.add(userId, flagType, { updateUser: false });
}
await this.updateUserFromFlags(userId);
});
}, 'UserFlagModels::addMulti');
}
public async removeMulti(userId: Uuid, flagTypes: UserFlagType[]) {
@@ -79,7 +79,7 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
await this.remove(userId, flagType, { updateUser: false });
}
await this.updateUserFromFlags(userId);
});
}, 'UserFlagModels::removeMulti');
}
// As a general rule the `enabled` and `can_upload` properties should not
@@ -95,17 +95,34 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
enabled: 1,
};
if (flags.find(f => f.type === UserFlagType.AccountWithoutSubscription)) {
const accountWithoutSubscriptionFlag = flags.find(f => f.type === UserFlagType.AccountWithoutSubscription);
const accountOverLimitFlag = flags.find(f => f.type === UserFlagType.AccountOverLimit);
const failedPaymentWarningFlag = flags.find(f => f.type === UserFlagType.FailedPaymentWarning);
const failedPaymentFinalFlag = flags.find(f => f.type === UserFlagType.FailedPaymentFinal);
const subscriptionCancelledFlag = flags.find(f => f.type === UserFlagType.SubscriptionCancelled);
const manuallyDisabledFlag = flags.find(f => f.type === UserFlagType.ManuallyDisabled);
if (accountWithoutSubscriptionFlag) {
newProps.can_upload = 0;
} else if (flags.find(f => f.type === UserFlagType.AccountOverLimit)) {
}
if (accountOverLimitFlag) {
newProps.can_upload = 0;
} else if (flags.find(f => f.type === UserFlagType.FailedPaymentWarning)) {
}
if (failedPaymentWarningFlag) {
newProps.can_upload = 0;
} else if (flags.find(f => f.type === UserFlagType.FailedPaymentFinal)) {
}
if (failedPaymentFinalFlag) {
newProps.enabled = 0;
} else if (flags.find(f => f.type === UserFlagType.SubscriptionCancelled)) {
}
if (subscriptionCancelledFlag) {
newProps.enabled = 0;
} else if (flags.find(f => f.type === UserFlagType.ManuallyDisabled)) {
}
if (manuallyDisabledFlag) {
newProps.enabled = 0;
}

Some files were not shown because too many files have changed in this diff Show More