1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-17 23:27:48 +02:00
This commit is contained in:
Laurent Cozic
2021-06-26 18:18:10 +01:00
parent 400ede122d
commit 05e4b32d9b
41 changed files with 284 additions and 109 deletions

View File

@@ -1478,6 +1478,9 @@ packages/lib/services/synchronizer/migrations/1.js.map
packages/lib/services/synchronizer/migrations/2.d.ts
packages/lib/services/synchronizer/migrations/2.js
packages/lib/services/synchronizer/migrations/2.js.map
packages/lib/services/synchronizer/migrations/3.d.ts
packages/lib/services/synchronizer/migrations/3.js
packages/lib/services/synchronizer/migrations/3.js.map
packages/lib/services/synchronizer/syncTargetInfoUtils.d.ts
packages/lib/services/synchronizer/syncTargetInfoUtils.js
packages/lib/services/synchronizer/syncTargetInfoUtils.js.map

3
.gitignore vendored
View File

@@ -1464,6 +1464,9 @@ packages/lib/services/synchronizer/migrations/1.js.map
packages/lib/services/synchronizer/migrations/2.d.ts
packages/lib/services/synchronizer/migrations/2.js
packages/lib/services/synchronizer/migrations/2.js.map
packages/lib/services/synchronizer/migrations/3.d.ts
packages/lib/services/synchronizer/migrations/3.js
packages/lib/services/synchronizer/migrations/3.js.map
packages/lib/services/synchronizer/syncTargetInfoUtils.d.ts
packages/lib/services/synchronizer/syncTargetInfoUtils.js
packages/lib/services/synchronizer/syncTargetInfoUtils.js.map

View File

@@ -5,6 +5,7 @@ import BaseItem from '@joplin/lib/models/BaseItem';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import { _ } from '@joplin/lib/locale';
import { encryptionEnabled } from '@joplin/lib/services/synchronizer/syncTargetInfoUtils';
const { BaseCommand } = require('./base-command.js');
const pathUtils = require('@joplin/lib/path-utils');
const imageType = require('image-type');
@@ -116,7 +117,7 @@ class Command extends BaseCommand {
}
if (args.command === 'status') {
this.stdout(_('Encryption is: %s', Setting.value('encryption.enabled') ? _('Enabled') : _('Disabled')));
this.stdout(_('Encryption is: %s', encryptionEnabled() ? _('Enabled') : _('Disabled')));
return;
}

View File

@@ -187,6 +187,7 @@ class Command extends BaseCommand {
try {
const migrationHandler = new MigrationHandler(
sync.api(),
reg.db(),
sync.lockHandler(),
Setting.value('appType'),
Setting.value('clientId')

View File

@@ -17,7 +17,6 @@ const BaseItem = require('@joplin/lib/models/BaseItem').default;
const Note = require('@joplin/lib/models/Note').default;
const Tag = require('@joplin/lib/models/Tag').default;
const NoteTag = require('@joplin/lib/models/NoteTag').default;
const MasterKey = require('@joplin/lib/models/MasterKey').default;
const Setting = require('@joplin/lib/models/Setting').default;
const Revision = require('@joplin/lib/models/Revision').default;
const Logger = require('@joplin/lib/Logger').default;
@@ -45,7 +44,7 @@ BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
// BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', `net.cozic.joplin${env === 'dev' ? 'dev' : ''}-cli`);

View File

@@ -10,6 +10,7 @@ import dialogs from './dialogs';
import { _ } from '@joplin/lib/locale';
import bridge from '../services/bridge';
import { MasterKeyEntity } from '@joplin/lib/services/database/types';
import { activeMasterKeyId, encryptionEnabled } from '@joplin/lib/services/synchronizer/syncTargetInfoUtils';
const shared = require('@joplin/lib/components/shared/encryption-config-shared.js');
interface Props {
@@ -167,7 +168,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props, any> {
}
const onToggleButtonClick = async () => {
const isEnabled = Setting.value('encryption.enabled');
const isEnabled = encryptionEnabled();
let answer = null;
if (isEnabled) {
@@ -299,8 +300,8 @@ const mapStateToProps = (state: any) => {
themeId: state.settings.theme,
masterKeys: state.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
encryptionEnabled: encryptionEnabled(),
activeMasterKeyId: activeMasterKeyId(),
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
};

View File

@@ -3,7 +3,6 @@ import { useState, useEffect } from 'react';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
import DialogButtonRow from './DialogButtonRow';
import { themeStyle, buildStyle } from '@joplin/lib/theme';
import { reg } from '@joplin/lib/registry';
@@ -15,6 +14,7 @@ import { NoteEntity } from '@joplin/lib/services/database/types';
import Button from './Button/Button';
import { connect } from 'react-redux';
import { AppState } from '../app';
import { encryptionEnabled } from '@joplin/lib/services/synchronizer/syncTargetInfoUtils';
const { clipboard } = require('electron');
interface Props {
@@ -207,7 +207,7 @@ export function ShareNoteDialog(props: Props) {
};
function renderEncryptionWarningMessage() {
if (!Setting.value('encryption.enabled')) return null;
if (!encryptionEnabled()) return null;
return <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}<hr/></div>;
}

View File

@@ -17,7 +17,6 @@ const BaseItem = require('@joplin/lib/models/BaseItem').default;
const Note = require('@joplin/lib/models/Note').default;
const Tag = require('@joplin/lib/models/Tag').default;
const NoteTag = require('@joplin/lib/models/NoteTag').default;
const MasterKey = require('@joplin/lib/models/MasterKey').default;
const Setting = require('@joplin/lib/models/Setting').default;
const Revision = require('@joplin/lib/models/Revision').default;
const Logger = require('@joplin/lib/Logger').default;
@@ -70,7 +69,7 @@ BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
// BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', `net.cozic.joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`);

View File

@@ -7,6 +7,7 @@ import time from '@joplin/lib/time';
import { _ } from '@joplin/lib/locale';
import { MasterKeyEntity } from '@joplin/lib/services/database/types';
import { setupAndDisableEncryption, generateMasterKeyAndEnableEncryption } from '@joplin/lib/services/e2ee/utils';
import { activeMasterKeyId, encryptionEnabled } from '@joplin/lib/services/synchronizer/syncTargetInfoUtils';
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
const { BaseScreenComponent } = require('../base-screen.js');
@@ -302,8 +303,8 @@ const EncryptionConfigScreen = connect((state: any) => {
themeId: state.settings.theme,
masterKeys: state.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
encryptionEnabled: encryptionEnabled(),
activeMasterKeyId: activeMasterKeyId(),
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
})(EncryptionConfigScreenComponent);

View File

@@ -457,7 +457,7 @@ async function initialize(dispatch: Function) {
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
// BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
const fsDriver = new FsDriverRN();

View File

@@ -48,6 +48,7 @@ import ShareService from './services/share/ShareService';
import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation';
import SyncTargetJoplinCloud from './SyncTargetJoplinCloud';
import { loadMasterKeysFromSettings } from './services/e2ee/utils';
import { encryptionEnabled } from './services/synchronizer/syncTargetInfoUtils';
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');
@@ -424,8 +425,17 @@ export default class BaseApplication {
syswidecas.addCAs(f);
}
},
'encryption.enabled': async () => {
// Note: this used to run when "encryption.enabled" was changed, but
// now we run it anytime any property of the sync target info is
// changed. This is not optimal but:
// - The sync target info rarely changes.
// - All the calls below are cheap or do nothing if there's nothing
// to do.
'sync.info': async () => {
if (this.hasGui()) {
appLogger.info('"sync.info" was changed - setting up encryption related code');
await loadMasterKeysFromSettings(EncryptionService.instance());
void DecryptionWorker.instance().scheduleStart();
const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds();
@@ -440,6 +450,7 @@ export default class BaseApplication {
void reg.scheduleSync();
}
},
'sync.interval': async () => {
if (this.hasGui()) reg.setupRecurrentSync();
},
@@ -447,8 +458,7 @@ export default class BaseApplication {
sideEffects['timeFormat'] = sideEffects['dateFormat'];
sideEffects['locale'] = sideEffects['dateFormat'];
sideEffects['encryption.activeMasterKeyId'] = sideEffects['encryption.enabled'];
sideEffects['encryption.passwordCache'] = sideEffects['encryption.enabled'];
sideEffects['encryption.passwordCache'] = sideEffects['sync.info'];
if (action) {
const effect = sideEffects[action.key];
@@ -783,7 +793,7 @@ export default class BaseApplication {
// and if encryption is enabled. This code runs only when shouldReencrypt = -1
// which can be set by a maintenance script for example.
const folderCount = await Folder.count();
const itShould = Setting.value('encryption.enabled') && !!folderCount ? Setting.SHOULD_REENCRYPT_YES : Setting.SHOULD_REENCRYPT_NO;
const itShould = encryptionEnabled() && !!folderCount ? Setting.SHOULD_REENCRYPT_YES : Setting.SHOULD_REENCRYPT_NO;
Setting.setValue('encryption.shouldReencrypt', itShould);
}

View File

@@ -22,6 +22,7 @@ import ItemUploader from './services/synchronizer/ItemUploader';
import { FileApi } from './file-api';
import { localSyncTargetInfo, remoteSyncTargetInfo, setLocalSyncTargetInfo, syncTargetInfoEquals, setRemoteSyncTargetInfo, mergeSyncTargetInfos, activeMasterKey } from './services/synchronizer/syncTargetInfoUtils';
import { setupAndEnableEncryption, setupAndDisableEncryption } from './services/e2ee/utils';
import JoplinDatabase from './JoplinDatabase';
const { sprintf } = require('sprintf-js');
const { Dirnames } = require('./services/synchronizer/utils/types');
@@ -63,7 +64,7 @@ export default class Synchronizer {
public static verboseMode: boolean = true;
private db_: any;
private db_: JoplinDatabase;
private api_: FileApi;
private appType_: string;
private logger_: Logger = new Logger();
@@ -88,7 +89,7 @@ export default class Synchronizer {
public dispatch: Function;
public constructor(db: any, api: FileApi, appType: string) {
public constructor(db: JoplinDatabase, api: FileApi, appType: string) {
this.db_ = db;
this.api_ = api;
this.appType_ = appType;
@@ -134,7 +135,7 @@ export default class Synchronizer {
private migrationHandler() {
if (this.migrationHandler_) return this.migrationHandler_;
this.migrationHandler_ = new MigrationHandler(this.api(), this.lockHandler(), this.appType_, this.clientId_);
this.migrationHandler_ = new MigrationHandler(this.api(), this.db(), this.lockHandler(), this.appType_, this.clientId_);
return this.migrationHandler_;
}
@@ -426,7 +427,6 @@ export default class Synchronizer {
const localInfo = localSyncTargetInfo();
if (!syncTargetInfoEquals(localInfo, remoteInfo)) {
// TODO: if e2ee changed - need to enable/disable encryption
const newInfo = mergeSyncTargetInfos(localInfo, remoteInfo);
await setRemoteSyncTargetInfo(this.api(), newInfo);
setLocalSyncTargetInfo(newInfo);

View File

@@ -7,6 +7,8 @@ const { fileExtension } = require('../path-utils');
const script = {};
script.exec = async function() {
if (!Setting.value('resourceDir')) return; // Probably running tests
const stats = await shim.fsDriver().readDirStats(Setting.value('resourceDir'));
let queries = [];

View File

@@ -3,7 +3,11 @@ const SearchEngine = require('../services/searchengine/SearchEngine').default;
const script = {};
script.exec = async function() {
try {
await SearchEngine.instance().rebuildIndex();
} catch {
// Probably running tests.
}
};
module.exports = script;

View File

@@ -9,6 +9,7 @@ import Database from '../database';
import ItemChange from './ItemChange';
import ShareService from '../services/share/ShareService';
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
import { encryptionEnabled } from '../services/synchronizer/syncTargetInfoUtils';
const JoplinError = require('../JoplinError.js');
const { sprintf } = require('sprintf-js');
const moment = require('moment');
@@ -48,7 +49,7 @@ export default class BaseItem extends BaseModel {
{ type: BaseModel.TYPE_RESOURCE, className: 'Resource' },
{ type: BaseModel.TYPE_TAG, className: 'Tag' },
{ type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' },
{ type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' },
// { type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' },
{ type: BaseModel.TYPE_REVISION, className: 'Revision' },
];
@@ -410,7 +411,7 @@ export default class BaseItem extends BaseModel {
const serialized = await ItemClass.serialize(item, shownKeys);
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported() || !itemCanBeEncrypted(item)) {
if (!encryptionEnabled() || !ItemClass.encryptionSupported() || !itemCanBeEncrypted(item)) {
// Normally not possible since itemsThatNeedSync should only return decrypted items
if (item.encryption_applied) throw new JoplinError('Item is encrypted but encryption is currently disabled', 'cannotSyncEncrypted');
return serialized;
@@ -697,7 +698,7 @@ export default class BaseItem extends BaseModel {
const temp = this.syncItemClassNames();
const output = [];
for (let i = 0; i < temp.length; i++) {
if (temp[i] === 'MasterKey') continue;
// if (temp[i] === 'MasterKey') continue;
output.push(temp[i]);
}
return output;

View File

@@ -1,5 +1,6 @@
import BaseModel from '../BaseModel';
import { MasterKeyEntity } from '../services/database/types';
import { masterKeyAll, masterKeyById, saveMasterKey } from '../services/synchronizer/syncTargetInfoUtils';
import BaseItem from './BaseItem';
export default class MasterKey extends BaseItem {
@@ -16,20 +17,30 @@ export default class MasterKey extends BaseItem {
}
static latest() {
return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)');
throw new Error('Not implemented');
// return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)');
}
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) {
return masterKeys.filter(m => m.encryption_method !== method);
}
static async save(o: MasterKeyEntity, options: any = null) {
return super.save(o, options).then(item => {
public static async load(id: string): Promise<MasterKeyEntity> {
return masterKeyById(id);
}
public static async all(): Promise<MasterKeyEntity[]> {
return masterKeyAll();
}
public static async save(o: MasterKeyEntity): Promise<MasterKeyEntity> {
const newMasterKey = saveMasterKey(o);
this.dispatch({
type: 'MASTERKEY_UPDATE_ONE',
item: item,
});
return item;
item: newMasterKey,
});
return newMasterKey;
}
}

View File

@@ -1,6 +1,7 @@
import BaseModel from '../BaseModel';
import JoplinDatabase from '../JoplinDatabase';
import migration40 from '../migrations/40';
import { MigrationEntity } from '../services/database/types';
export interface MigrationScript {
exec(db: JoplinDatabase): Promise<void>;
@@ -23,7 +24,7 @@ export default class Migration extends BaseModel {
return BaseModel.TYPE_MIGRATION;
}
static migrationsToDo() {
public static migrationsToDo(): Promise<MigrationEntity[]> {
return this.modelSelectAll('SELECT * FROM migrations ORDER BY number ASC');
}

View File

@@ -13,6 +13,7 @@ const { filename, safeFilename } = require('../path-utils');
const { FsDriverDummy } = require('../fs-driver-dummy.js');
import JoplinError from '../JoplinError';
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
import { encryptionEnabled } from '../services/synchronizer/syncTargetInfoUtils';
export default class Resource extends BaseItem {
@@ -196,7 +197,7 @@ export default class Resource extends BaseItem {
public static async fullPathForSyncUpload(resource: ResourceEntity) {
const plainTextPath = this.fullPath(resource);
if (!Setting.value('encryption.enabled') || !itemCanBeEncrypted(resource as any)) {
if (!encryptionEnabled() || !itemCanBeEncrypted(resource as any)) {
// Normally not possible since itemsThatNeedSync should only return decrypted items
if (resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled');
return { path: plainTextPath, resource: resource };

View File

@@ -196,7 +196,7 @@ class Setting extends BaseModel {
cacheDir: '',
pluginDir: '',
flagOpenDevTools: false,
syncVersion: 2,
syncVersion: 3,
startupDevPlugins: [],
};
@@ -212,6 +212,7 @@ class Setting extends BaseModel {
private static customSections_: SettingSections = {};
private static changedKeys_: string[] = [];
private static fileHandler_: FileHandler = null;
private static settingFilename_: string = 'settings.json';
static tableName() {
return 'settings';
@@ -235,7 +236,15 @@ class Setting extends BaseModel {
}
public static get settingFilePath(): string {
return `${this.value('profileDir')}/settings.json`;
return `${this.value('profileDir')}/${this.settingFilename_}`;
}
public static get settingFilename(): string {
return this.settingFilename_;
}
public static set settingFilename(v: string) {
this.settingFilename_ = v;
}
public static get fileHandler(): FileHandler {

View File

@@ -51,4 +51,8 @@ export default class FileHandler {
this.valueJsonCache_ = json;
}
public async clearCache() {
this.valueJsonCache_ = null;
}
}

View File

@@ -12,7 +12,7 @@
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json",
"generatePluginTypes": "rm -rf ./plugin_types && node node_modules/typescript/bin/tsc --declaration --declarationDir ./plugin_types --project tsconfig.json",
"test": "jest",
"test": "jest --verbose=false",
"test-ci": "npm run test"
},
"devDependencies": {

View File

@@ -5,6 +5,7 @@ import Setting from '../models/Setting';
import BaseItem from '../models/BaseItem';
import MasterKey from '../models/MasterKey';
import EncryptionService from '../services/EncryptionService';
import { setEncryptionEnabled } from './synchronizer/syncTargetInfoUtils';
let service: EncryptionService = null;
@@ -15,7 +16,7 @@ describe('services_EncryptionService', function() {
await switchClient(1);
service = new EncryptionService();
BaseItem.encryptionService_ = service;
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
done();
});

View File

@@ -7,6 +7,7 @@ import BaseItem from '../models/BaseItem';
const { padLeft } = require('../string-utils.js');
import JoplinError from '../JoplinError';
import { activeMasterKeyId, setActiveMasterKeyId } from './synchronizer/syncTargetInfoUtils';
function hexPad(s: string, length: number) {
return padLeft(s, length, '0');
@@ -45,7 +46,7 @@ export default class EncryptionService {
// changed easily since the chunk size is incorporated into the encrypted data.
private chunkSize_ = 5000;
private loadedMasterKeys_: Record<string, string> = {};
private activeMasterKeyId_: string = null;
// private activeMasterKeyId_: string = null;
public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
private logger_ = new Logger();
@@ -74,7 +75,7 @@ export default class EncryptionService {
// changed easily since the chunk size is incorporated into the encrypted data.
this.chunkSize_ = 5000;
this.loadedMasterKeys_ = {};
this.activeMasterKeyId_ = null;
// this.activeMasterKeyId_ = null;
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
this.logger_ = new Logger();
@@ -120,16 +121,20 @@ export default class EncryptionService {
}
setActiveMasterKeyId(id: string) {
this.activeMasterKeyId_ = id;
setActiveMasterKeyId(id);
// this.activeMasterKeyId_ = id;
}
activeMasterKeyId() {
if (!this.activeMasterKeyId_) {
const id = activeMasterKeyId();
if (!id) {
const error: any = new Error('No master key is defined as active. Check this: Either one or more master keys exist but no password was provided for any of them. Or no master key exist. Or master keys and password exist, but none was set as active.');
error.code = 'noActiveMasterKey';
throw error;
}
return this.activeMasterKeyId_;
return id;
}
isMasterKeyLoaded(id: string) {

View File

@@ -2,7 +2,7 @@ import MasterKey from '../models/MasterKey';
import Setting from '../models/Setting';
import { encryptionService, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
import MigrationService from './MigrationService';
import { SyncTargetInfo } from './synchronizer/syncTargetInfoUtils';
import { setActiveMasterKeyId, setEncryptionEnabled, SyncTargetInfo } from './synchronizer/syncTargetInfoUtils';
function migrationService() {
return new MigrationService();
@@ -20,8 +20,8 @@ describe('MigrationService', function() {
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey('1'));
const mk2 = await MasterKey.save(await encryptionService().generateMasterKey('2'));
Setting.setValue('encryption.enabled', true);
Setting.setValue('encryption.activeMasterKeyId', mk2.id);
setEncryptionEnabled(true);
setActiveMasterKeyId(mk2.id);
await migrationService().runScript(40);

View File

@@ -16,15 +16,17 @@ export default class MigrationService extends BaseService {
await script.exec(Migration.db());
}
public async run() {
public async run(migrationsToSkip: number[] = []) {
const migrations = await Migration.migrationsToDo();
for (const migration of migrations) {
this.logger().info(`Running migration: ${migration.number}`);
try {
if (migrationsToSkip.includes(migration.number)) continue;
await this.runScript(migration.number);
await Migration.delete(migration.id);
await Migration.delete(migration.id as any);
} catch (error) {
this.logger().error(`Cannot run migration: ${migration.number}`, error);
break;

View File

@@ -4,16 +4,12 @@ import MasterKey from '../../models/MasterKey';
import Setting from '../../models/Setting';
import { MasterKeyEntity } from '../database/types';
import EncryptionService from '../EncryptionService';
import { localSyncTargetInfo, setLocalSyncTargetInfo } from '../synchronizer/syncTargetInfoUtils';
import { localSyncTargetInfo, setEncryptionEnabled } from '../synchronizer/syncTargetInfoUtils';
const logger = Logger.create('e2ee/utils');
export async function setupAndEnableEncryption(masterKey: MasterKeyEntity, password: string = null) {
setLocalSyncTargetInfo({
...localSyncTargetInfo(),
e2ee: true,
activeMasterKeyId: masterKey.id,
});
setEncryptionEnabled(true, masterKey.id);
if (password) {
const passwordCache = Setting.value('encryption.passwordCache');
@@ -32,13 +28,7 @@ export async function setupAndDisableEncryption() {
// long as there are encrypted items). Also even if decryption is disabled, it's possible that encrypted items
// will still be received via synchronisation.
// const hasEncryptedItems = await BaseItem.hasEncryptedItems();
// if (hasEncryptedItems) throw new Error(_('Encryption cannot currently be disabled because some items are still encrypted. Please wait for all the items to be decrypted and try again.'));
setLocalSyncTargetInfo({
...localSyncTargetInfo(),
e2ee: false,
});
setEncryptionEnabled(false);
// The only way to make sure everything gets decrypted on the sync target is
// to re-sync everything.

View File

@@ -8,6 +8,7 @@ import shim from '../../shim';
import filterParser from './filterParser';
import queryBuilder from './queryBuilder';
import { ItemChangeEntity, NoteEntity } from '../database/types';
import JoplinDatabase from '../../JoplinDatabase';
const { sprintf } = require('sprintf-js');
const { pregQuote, scriptType, removeDiacritics } = require('../../string-utils.js');
@@ -22,7 +23,7 @@ export default class SearchEngine {
public dispatch: Function = (_o: any) => {};
private logger_ = new Logger();
private db_: any = null;
private db_: JoplinDatabase = null;
private isIndexing_ = false;
private syncCalls_: any[] = [];
private scheduleSyncTablesIID_: any;
@@ -62,7 +63,8 @@ export default class SearchEngine {
}
async rebuildIndex_() {
let noteIds: string[] = await this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND encryption_applied = 0');
let noteIds: string[] = await this.db().selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND encryption_applied = 0') as any;
noteIds = noteIds.map((n: any) => n.id);
const lastChangeId = await ItemChange.lastChangeId();

View File

@@ -1,23 +1,30 @@
import LockHandler, { LockType } from './LockHandler';
import { Dirnames } from './utils/types';
import BaseService from '../BaseService';
import migration1 from './migrations/1';
import migration2 from './migrations/2';
import migration3 from './migrations/3';
export type MigrationFunction = (api: FileApi, db: JoplinDatabase)=> Promise<void>;
// To add a new migration:
// - Add the migration logic in ./migrations/VERSION_NUM.js
// - Add the file to the array below.
// - Set Setting.syncVersion to VERSION_NUM in models/Setting.js
// - Add tests in synchronizer_migrationHandler
const migrations = [
const migrations: MigrationFunction[] = [
null,
require('./migrations/1.js').default,
require('./migrations/2.js').default,
migration1,
migration2,
migration3,
];
import Setting from '../../models/Setting';
const { sprintf } = require('sprintf-js');
import JoplinError from '../../JoplinError';
import { FileApi } from '../../file-api';
import { remoteSyncTargetInfo, setRemoteSyncTargetInfo, setLocalSyncTargetInfo, SyncTargetInfo } from './syncTargetInfoUtils';
import { remoteSyncTargetInfo, setRemoteSyncTargetInfo, setLocalSyncTargetInfo, SyncTargetInfo, mergeSyncTargetInfos, localSyncTargetInfo } from './syncTargetInfoUtils';
import JoplinDatabase from '../../JoplinDatabase';
export default class MigrationHandler extends BaseService {
@@ -25,10 +32,12 @@ export default class MigrationHandler extends BaseService {
private lockHandler_: LockHandler = null;
private clientType_: string;
private clientId_: string;
private db_: JoplinDatabase;
constructor(api: FileApi, lockHandler: LockHandler, clientType: string, clientId: string) {
constructor(api: FileApi, db: JoplinDatabase, lockHandler: LockHandler, clientType: string, clientId: string) {
super();
this.api_ = api;
this.db_ = db;
this.lockHandler_ = lockHandler;
this.clientType_ = clientType;
this.clientId_ = clientId;
@@ -51,7 +60,7 @@ export default class MigrationHandler extends BaseService {
public async upgrade(targetVersion: number = 0) {
const supportedSyncTargetVersion = Setting.value('syncVersion');
const info = await remoteSyncTargetInfo(this.api_);
const info = mergeSyncTargetInfos(localSyncTargetInfo(false), await remoteSyncTargetInfo(this.api_));
if (info.version > supportedSyncTargetVersion) {
throw new JoplinError(sprintf('Sync version of the target (%d) is greater than the version supported by the client (%d). Please upgrade your client.', info.version, supportedSyncTargetVersion), 'outdatedClient');
@@ -96,7 +105,7 @@ export default class MigrationHandler extends BaseService {
try {
if (autoLockError) throw autoLockError;
await migration(this.api_);
await migration(this.api_, this.db_);
if (autoLockError) throw autoLockError;
const newInfo: SyncTargetInfo = {

View File

@@ -1,11 +1,10 @@
import time from '../../time';
import Setting from '../../models/Setting';
import { allNotesFolders, localNotesFoldersSameAsRemote } from '../../testing/test-utils-synchronizer';
const { synchronizerStart, setupDatabaseAndSynchronizer, sleep, switchClient, syncTargetId, loadEncryptionMasterKey, decryptionWorker } = require('../../testing/test-utils.js');
import { synchronizerStart, setupDatabaseAndSynchronizer, sleep, switchClient, syncTargetId, loadEncryptionMasterKey, decryptionWorker } from '../../testing/test-utils';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import BaseItem from '../../models/BaseItem';
import { setEncryptionEnabled } from './syncTargetInfoUtils';
describe('Synchronizer.conflicts', function() {
@@ -227,7 +226,7 @@ describe('Synchronizer.conflicts', function() {
async function ignorableNoteConflictTest(withEncryption: boolean) {
if (withEncryption) {
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
}

View File

@@ -11,6 +11,7 @@ import BaseItem from '../../models/BaseItem';
import { ResourceEntity } from '../database/types';
import Synchronizer from '../../Synchronizer';
import { setupAndDisableEncryption, setupAndEnableEncryption, loadMasterKeysFromSettings } from '../e2ee/utils';
import { encryptionEnabled, localSyncTargetInfo, setEncryptionEnabled } from './syncTargetInfoUtils';
let insideBeforeEach = false;
@@ -32,7 +33,7 @@ describe('Synchronizer.e2ee', function() {
});
it('notes and folders should get encrypted when encryption is enabled', (async () => {
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
const masterKey = await loadEncryptionMasterKey();
const folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'un', body: 'to be encrypted', parent_id: folder1.id });
@@ -75,7 +76,7 @@ describe('Synchronizer.e2ee', function() {
it('should enable encryption automatically when downloading new master key (and none was previously available)',(async () => {
// Enable encryption on client 1 and sync an item
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: 'folder1' });
await synchronizerStart();
@@ -83,9 +84,9 @@ describe('Synchronizer.e2ee', function() {
await switchClient(2);
// Synchronising should enable encryption since we're going to get a master key
expect(Setting.value('encryption.enabled')).toBe(false);
expect(encryptionEnabled()).toBe(false);
await synchronizerStart();
expect(Setting.value('encryption.enabled')).toBe(true);
expect(encryptionEnabled()).toBe(true);
// Check that we got the master key from client 1
const masterKey = (await MasterKey.all())[0];
@@ -135,6 +136,7 @@ describe('Synchronizer.e2ee', function() {
// First create a folder, without encryption enabled, and sync it
await Folder.save({ title: 'folder1' });
await synchronizerStart();
let files = await fileApi().list('', { includeDirs: false, syncItemsOnly: true });
let content = await fileApi().get(files.items[0].path);
expect(content.indexOf('folder1') >= 0).toBe(true);
@@ -142,24 +144,27 @@ describe('Synchronizer.e2ee', function() {
// Then enable encryption and sync again
let masterKey = await encryptionService().generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await setupAndEnableEncryption(masterKey, '123456');
await loadMasterKeysFromSettings(encryptionService());
await synchronizerStart();
// Even though the folder has not been changed it should have been synced again so that
// an encrypted version of it replaces the decrypted version.
files = await fileApi().list('', { includeDirs: false, syncItemsOnly: true });
expect(files.items.length).toBe(2);
expect(files.items.length).toBe(1);
// By checking that the folder title is not present, we can confirm that the item has indeed been encrypted
// One of the two items is the master key
content = await fileApi().get(files.items[0].path);
expect(content.indexOf('folder1') < 0).toBe(true);
content = await fileApi().get(files.items[1].path);
expect(content.indexOf('folder1') < 0).toBe(true);
// Also verify that we got the master key back
const syncTargetInfo = localSyncTargetInfo();
expect(syncTargetInfo.masterKeys[masterKey.id]).toBeTruthy();
}));
it('should upload decrypted items to sync target after encryption disabled', (async () => {
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
await Folder.save({ title: 'folder1' });
@@ -180,7 +185,7 @@ describe('Synchronizer.e2ee', function() {
// which means it's going to fail in unexpected way. So the loop below wait for beforeEach to be done.
while (insideBeforeEach) await time.msleep(100);
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
const masterKey = await loadEncryptionMasterKey();
await Folder.save({ title: 'folder1' });
@@ -189,7 +194,7 @@ describe('Synchronizer.e2ee', function() {
await switchClient(2);
await synchronizerStart();
expect(Setting.value('encryption.enabled')).toBe(true);
expect(encryptionEnabled()).toBe(true);
// If we try to disable encryption now, it should throw an error because some items are
// currently encrypted. They must be decrypted first so that they can be sent as
@@ -213,7 +218,7 @@ describe('Synchronizer.e2ee', function() {
}));
it('should set the resource file size after decryption', (async () => {
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
const masterKey = await loadEncryptionMasterKey();
const folder1 = await Folder.save({ title: 'folder1' });
@@ -368,7 +373,7 @@ describe('Synchronizer.e2ee', function() {
}));
it('should not encrypt notes that are shared by link', (async () => {
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
await createFolderTree('', [
@@ -460,7 +465,7 @@ describe('Synchronizer.e2ee', function() {
return;
}
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
const folder1 = await createFolderTree('', [

View File

@@ -12,6 +12,7 @@ import ResourceFetcher from '../../services/ResourceFetcher';
import BaseItem from '../../models/BaseItem';
import { ModelType } from '../../BaseModel';
import { loadMasterKeysFromSettings } from '../e2ee/utils';
import { setEncryptionEnabled } from './syncTargetInfoUtils';
let insideBeforeEach = false;
@@ -145,7 +146,7 @@ describe('Synchronizer.resources', function() {
}));
it('should encrypt resources', (async () => {
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
const masterKey = await loadEncryptionMasterKey();
const folder1 = await Folder.save({ title: 'folder1' });

View File

@@ -1,9 +1,9 @@
import Setting from '../../models/Setting';
import { synchronizerStart, setupDatabaseAndSynchronizer, switchClient, encryptionService, loadEncryptionMasterKey } from '../../testing/test-utils';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import Tag from '../../models/Tag';
import MasterKey from '../../models/MasterKey';
import { setEncryptionEnabled } from './syncTargetInfoUtils';
describe('Synchronizer.tags', function() {
@@ -17,7 +17,7 @@ describe('Synchronizer.tags', function() {
async function shoudSyncTagTest(withEncryption: boolean) {
let masterKey = null;
if (withEncryption) {
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
masterKey = await loadEncryptionMasterKey();
}

View File

@@ -27,6 +27,7 @@ export default function useSyncTargetUpgrade(): SyncTargetUpgradeResult {
reg.logger().info('useSyncTargetUpgrade: Create migration handler...');
const migrationHandler = new MigrationHandler(
synchronizer.api(),
synchronizer.db(),
synchronizer.lockHandler(),
Setting.value('appType'),
Setting.value('clientId')

View File

@@ -1,4 +1,6 @@
export default async function(api: any) {
import { FileApi } from '../../../file-api';
export default async function(api: FileApi) {
await Promise.all([
api.mkdir('.resource'),
api.mkdir('.sync'),

View File

@@ -1,6 +1,7 @@
import { FileApi } from '../../../file-api';
import { Dirnames } from '../utils/types';
export default async function(api: any) {
export default async function(api: FileApi) {
await Promise.all([
api.put('.sync/version.txt', '2'),
api.put('.sync/readme.txt', '2020-07-16: In the new sync format, the version number is stored in /info.json. However, for backward compatibility, we need to keep the old version.txt file here, otherwise old clients will automatically recreate it, and assume a sync target version 1. So we keep it here but set its value to "2", so that old clients know that they need to be upgraded. This directory can be removed after a year or so, once we are confident that all clients have been upgraded to recent versions.'),

View File

@@ -0,0 +1,15 @@
import { FileApi } from '../../../file-api';
import JoplinDatabase from '../../../JoplinDatabase';
import Setting from '../../../models/Setting';
import { localSyncTargetInfo, setRemoteSyncTargetInfo, SyncTargetInfo } from '../syncTargetInfoUtils';
export default async function(api: FileApi, _db: JoplinDatabase) {
const syncInfo: SyncTargetInfo = {
...localSyncTargetInfo(),
version: 3,
updatedTime: Date.now(),
};
await setRemoteSyncTargetInfo(api, syncInfo);
Setting.setValue('sync.info', JSON.stringify(syncInfo));
}

View File

@@ -1,6 +1,7 @@
import { FileApi } from '../../file-api';
// import Logger from '../../Logger';
import Setting from '../../models/Setting';
import uuid from '../../uuid';
import { MasterKeyEntity } from '../database/types';
const ArrayUtils = require('../../ArrayUtils');
@@ -40,14 +41,24 @@ function unserializeSyncTargetInfo(info: string): SyncTargetInfo {
// return (info as any)[key];
// }
function defaultSyncTargetInfo(): SyncTargetInfo {
return {
e2ee: false,
activeMasterKeyId: '',
masterKeys: {},
version: 0,
updatedTime: 0,
};
}
export function setLocalSyncTargetInfo(info: SyncTargetInfo) {
Setting.setValue('sync.info', serializeSyncTargetInfo(info));
}
export function localSyncTargetInfo(mustExist: boolean = true): SyncTargetInfo | null {
export function localSyncTargetInfo(mustExist: boolean = false): SyncTargetInfo | null {
const info = Setting.value('sync.info');
if (mustExist && !info) throw new Error('Sync info is not set');
return unserializeSyncTargetInfo(info);
return info ? unserializeSyncTargetInfo(info) : defaultSyncTargetInfo();
}
function validateInfo(info: SyncTargetInfo) {
@@ -102,7 +113,7 @@ export async function remoteSyncTargetInfo(api: FileApi): Promise<SyncTargetInfo
const defaultFields: SyncTargetInfo = {
version: 0,
e2ee: false,
updatedTime: Date.now(),
updatedTime: 0,
masterKeys: {},
activeMasterKeyId: '',
};
@@ -139,13 +150,73 @@ export function activeMasterKey(info: SyncTargetInfo): MasterKeyEntity {
return info.masterKeys[info.activeMasterKeyId];
}
export function enableEncryption(enable: boolean = true) {
export function activeMasterKeyId() {
return localSyncTargetInfo().activeMasterKeyId;
}
export function setActiveMasterKeyId(id: string) {
const info = localSyncTargetInfo();
if (info.activeMasterKeyId === id) return;
setLocalSyncTargetInfo({
...localSyncTargetInfo(),
e2ee: enable,
activeMasterKeyId: id,
updatedTime: Date.now(),
});
}
export function disableEncryption() {
enableEncryption(false);
export function setEncryptionEnabled(enable: boolean = true, activeMasterKeyId: string = null) {
const info = localSyncTargetInfo(false);
if (info.e2ee === enable) return;
const newInfo = {
...info,
e2ee: enable,
updatedTime: Date.now(),
};
if (activeMasterKeyId !== null) newInfo.activeMasterKeyId = activeMasterKeyId;
setLocalSyncTargetInfo(newInfo);
}
export function encryptionEnabled() {
const info = localSyncTargetInfo(false);
return info.e2ee;
}
export function encryptionDisabled() {
return !encryptionEnabled();
}
export function masterKeyById(id: string): MasterKeyEntity {
return localSyncTargetInfo().masterKeys[id];
}
export function saveMasterKey(mk: MasterKeyEntity): MasterKeyEntity {
const info = localSyncTargetInfo();
const id = mk.id ? mk.id : uuid.create();
const newMasterKey = {
id,
...info.masterKeys[id],
...mk,
};
setLocalSyncTargetInfo({
...info,
masterKeys: {
...info.masterKeys,
[newMasterKey.id]: newMasterKey,
},
updatedTime: Date.now(),
});
return newMasterKey;
}
export function masterKeyAll(): MasterKeyEntity[] {
const masterKeys = localSyncTargetInfo().masterKeys;
return Object.keys(masterKeys).map(id => masterKeys[id]);
}

View File

@@ -8,7 +8,7 @@ import { Dirnames } from '../../services/synchronizer/utils/types';
// gulp buildTests -L && node tests-build/support/createSyncTargetSnapshot.js normal && node tests-build/support/createSyncTargetSnapshot.js e2ee
import { setSyncTargetName, fileApi, synchronizer, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } from '../../testing/test-utils';
import { setSyncTargetName, fileApi, synchronizer, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow, db } from '../../testing/test-utils';
import { deploySyncTargetSnapshot, testData, checkTestData } from '../../testing/syncTargetUtils';
import Setting from '../../models/Setting';
import MasterKey from '../../models/MasterKey';
@@ -28,7 +28,7 @@ function lockHandler(): LockHandler {
function migrationHandler(clientId: string = 'abcd'): MigrationHandler {
if (migrationHandler_) return migrationHandler_;
migrationHandler_ = new MigrationHandler(fileApi(), lockHandler(), 'desktop', clientId);
migrationHandler_ = new MigrationHandler(fileApi(), db(), lockHandler(), 'desktop', clientId);
return migrationHandler_;
}

View File

@@ -7,6 +7,7 @@ import Resource from '../models/Resource';
import markdownUtils from '../markdownUtils';
import shim from '../shim';
import * as fs from 'fs-extra';
import { setEncryptionEnabled } from '../services/synchronizer/syncTargetInfoUtils';
const snapshotBaseDir = `${supportDir}/syncTargetSnapshots`;
@@ -113,7 +114,7 @@ export async function main(syncTargetType: string) {
await createTestData(testData);
if (syncTargetType === 'e2ee') {
Setting.setValue('encryption.enabled', true);
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
}

View File

@@ -52,6 +52,8 @@ import { FolderEntity } from '../services/database/types';
import { credentialFile, readCredentialFile } from '../utils/credentialFiles';
import SyncTargetJoplinCloud from '../SyncTargetJoplinCloud';
import { FileApi } from '../file-api';
import { setEncryptionEnabled } from '../services/synchronizer/syncTargetInfoUtils';
import MigrationService from '../services/MigrationService';
const { loadKeychainServiceAndSettings } = require('../services/SettingUtils');
const md5 = require('md5');
const S3 = require('aws-sdk/clients/s3');
@@ -70,7 +72,7 @@ const fileApis_: any = {};
const encryptionServices_: EncryptionService[] = [];
const revisionServices_: any[] = [];
const decryptionWorkers_: any[] = [];
const resourceServices_: any[] = [];
const resourceServices_: ResourceService[] = [];
const resourceFetchers_: any[] = [];
const kvStores_: KvStore[] = [];
let currentClient_ = 1;
@@ -168,7 +170,7 @@ BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
// BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', 'net.cozic.joplintest-cli');
@@ -261,6 +263,8 @@ async function switchClient(id: number, options: any = null) {
BaseItem.revisionService_ = revisionServices_[id];
await Setting.reset();
Setting.settingFilename = `settings-${id}.json`;
Setting.setConstant('resourceDirName', resourceDirName(id));
Setting.setConstant('resourceDir', resourceDir(id));
Setting.setConstant('pluginDir', pluginDir(id));
@@ -299,6 +303,10 @@ async function clearDatabase(id: number = null) {
queries.push(`DELETE FROM sqlite_sequence WHERE name="${n}"`); // Reset autoincremented IDs
}
await databases_[id].transactionExecBatch(queries);
// More generally, this function should clear all data, and so that should
// include settings.json
await clearSettingFile(id);
}
async function setupDatabase(id: number = null, options: any = null) {
@@ -335,7 +343,18 @@ async function setupDatabase(id: number = null, options: any = null) {
await databases_[id].open({ name: filePath });
BaseModel.setDb(databases_[id]);
await clearSettingFile(id);
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
await MigrationService.instance().run([20, 27,33,35]);
}
async function clearSettingFile(id: number) {
Setting.settingFilename = `settings-${id}.json`;
await fs.remove(Setting.settingFilePath);
}
export async function createFolderTree(parentId: string, tree: any[], num: number = 0): Promise<FolderEntity> {
@@ -844,7 +863,7 @@ class TestApp extends BaseApplication {
// For now, disable sync and encryption to avoid spurious intermittent failures
// caused by them interupting processing and causing delays.
Setting.setValue('sync.interval', 0);
Setting.setValue('encryption.enabled', false);
setEncryptionEnabled(false);
this.initRedux();
Setting.dispatchUpdateAll();

View File

@@ -7,7 +7,7 @@ import Folder from '@joplin/lib/models/Folder';
import Resource from '@joplin/lib/models/Resource';
import NoteTag from '@joplin/lib/models/NoteTag';
import Tag from '@joplin/lib/models/Tag';
import MasterKey from '@joplin/lib/models/MasterKey';
// import MasterKey from '@joplin/lib/models/MasterKey';
import Revision from '@joplin/lib/models/Revision';
import { Config } from './types';
import * as fs from 'fs-extra';
@@ -77,7 +77,7 @@ export async function initializeJoplinUtils(config: Config, models: Models, must
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
// BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
// mustache_ = new MustacheService(config.viewDir, config.baseUrl);