Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions apps/client/src/setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import "jquery";
import utils from "./services/utils.js";

import ko from "knockout";

import utils from "./services/utils.js";

// TriliumNextTODO: properly make use of below types
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
// type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop";
Expand All @@ -16,6 +18,8 @@ class SetupModel {
syncServerHost: ko.Observable<string | undefined>;
syncProxy: ko.Observable<string | undefined>;
password: ko.Observable<string | undefined>;
totpToken: ko.Observable<string | undefined>;
totpEnabled: ko.Observable<boolean>;

constructor(syncInProgress: boolean) {
this.syncInProgress = syncInProgress;
Expand All @@ -27,6 +31,8 @@ class SetupModel {
this.syncServerHost = ko.observable();
this.syncProxy = ko.observable();
this.password = ko.observable();
this.totpToken = ko.observable();
this.totpEnabled = ko.observable(false);

if (this.syncInProgress) {
setInterval(checkOutstandingSyncs, 1000);
Expand All @@ -40,7 +46,7 @@ class SetupModel {
return !!this.setupType();
}

selectSetupType() {
async selectSetupType() {
if (this.setupType() === "new-document") {
this.step("new-document-in-progress");

Expand All @@ -52,6 +58,24 @@ class SetupModel {
}
}

async checkTotpStatus() {
const syncServerHost = this.syncServerHost();
if (!syncServerHost) {
this.totpEnabled(false);
return;
}

try {
const resp = await $.post("api/setup/check-server-totp", {
syncServerHost
});
this.totpEnabled(!!resp.totpEnabled);
} catch {
// If we can't reach the server, don't show TOTP field yet
this.totpEnabled(false);
}
}

back() {
this.step("setup-type");
this.setupType("");
Expand All @@ -72,11 +96,22 @@ class SetupModel {
return;
}

// Check TOTP status before submitting (in case it wasn't checked yet)
await this.checkTotpStatus();

const totpToken = this.totpToken();

if (this.totpEnabled() && !totpToken) {
showAlert("TOTP token can't be empty when two-factor authentication is enabled");
return;
}

// not using server.js because it loads too many dependencies
const resp = await $.post("api/setup/sync-from-server", {
syncServerHost: syncServerHost,
syncProxy: syncProxy,
password: password
syncServerHost,
syncProxy,
password,
totpToken
});

if (resp.result === "success") {
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/assets/translations/cn/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@
"proxy-instruction": "如果您将代理设置留空,将使用系统代理(仅适用于桌面应用)",
"password": "密码",
"password-placeholder": "密码",
"totp-token": "TOTP 验证码",
"totp-token-placeholder": "请输入 TOTP 验证码",
"back": "返回",
"finish-setup": "完成设置"
},
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/assets/translations/en/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@
"proxy-instruction": "If you leave proxy setting blank, system proxy will be used (applies to the desktop application only)",
"password": "Password",
"password-placeholder": "Password",
"totp-token": "TOTP Token",
"totp-token-placeholder": "Enter your TOTP code",
"back": "Back",
"finish-setup": "Finish setup"
},
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/assets/translations/tw/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@
"proxy-instruction": "如果您將代理設定留空,將使用系統代理(僅適用於桌面版)",
"password": "密碼",
"password-placeholder": "密碼",
"totp-token": "TOTP 驗證碼",
"totp-token-placeholder": "請輸入 TOTP 驗證碼",
"back": "返回",
"finish-setup": "完成設定"
},
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/assets/views/setup.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@

<div class="form-group">
<label for="sync-server-host"><%= t("setup_sync-from-server.server-host") %></label>
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="<%= t("setup_sync-from-server.server-host-placeholder") %>">
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost, event: { blur: checkTotpStatus }" placeholder="<%= t("setup_sync-from-server.server-host-placeholder") %>">
</div>
<div class="form-group">
<label for="sync-proxy"><%= t("setup_sync-from-server.proxy-server") %></label>
Expand All @@ -141,6 +141,10 @@
<label for="password"><%= t("setup_sync-from-server.password") %></label>
<input type="password" id="password" class="form-control" data-bind="value: password" placeholder="<%= t("setup_sync-from-server.password-placeholder") %>">
</div>
<div class="form-group" style="margin-bottom: 8px;" data-bind="visible: totpEnabled">
<label for="totpToken"><%= t("setup_sync-from-server.totp-token") %></label>
<input type="text" id="totpToken" class="form-control" data-bind="value: totpToken" placeholder="<%= t("setup_sync-from-server.totp-token-placeholder") %>" autocomplete="one-time-code">
</div>

<button type="button" data-bind="click: back" class="btn btn-secondary"><%= t("setup_sync-from-server.back") %></button>

Expand Down
1 change: 1 addition & 0 deletions apps/server/src/express.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export declare module "express-serve-static-core" {

authorization?: string;
"trilium-cred"?: string;
"trilium-totp"?: string;
"x-csrf-token"?: string;

"trilium-component-id"?: string;
Expand Down
37 changes: 28 additions & 9 deletions apps/server/src/routes/api/setup.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"use strict";

import sqlInit from "../../services/sql_init.js";
import setupService from "../../services/setup.js";
import log from "../../services/log.js";
import appInfo from "../../services/app_info.js";

import type { Request } from "express";

import appInfo from "../../services/app_info.js";
import log from "../../services/log.js";
import setupService from "../../services/setup.js";
import sqlInit from "../../services/sql_init.js";
import totp from "../../services/totp.js";

function getStatus() {
return {
isInitialized: sqlInit.isDbInitialized(),
schemaExists: sqlInit.schemaExists(),
syncVersion: appInfo.syncVersion
syncVersion: appInfo.syncVersion,
totpEnabled: totp.isTotpEnabled()
};
}

Expand All @@ -19,9 +22,9 @@ async function setupNewDocument() {
}

function setupSyncFromServer(req: Request) {
const { syncServerHost, syncProxy, password } = req.body;
const { syncServerHost, syncProxy, password, totpToken } = req.body;

return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password);
return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password, totpToken);
}

function saveSyncSeed(req: Request) {
Expand Down Expand Up @@ -82,10 +85,26 @@ function getSyncSeed() {
};
}

async function checkServerTotpStatus(req: Request) {
const { syncServerHost } = req.body;

if (!syncServerHost) {
return { totpEnabled: false };
}

try {
const resp = await setupService.checkRemoteTotpStatus(syncServerHost);
return { totpEnabled: !!resp.totpEnabled };
} catch {
return { totpEnabled: false };
}
}

export default {
getStatus,
setupNewDocument,
setupSyncFromServer,
getSyncSeed,
saveSyncSeed
saveSyncSeed,
checkServerTotpStatus
};
1 change: 1 addition & 0 deletions apps/server/src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ function register(app: express.Application) {
asyncRoute(PST, "/api/setup/sync-from-server", [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
route(GET, "/api/setup/sync-seed", [loginRateLimiter, auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
asyncRoute(PST, "/api/setup/sync-seed", [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
asyncRoute(PST, "/api/setup/check-server-totp", [auth.checkAppNotInitialized], setupApiRoute.checkServerTotpStatus, apiResultHandler);

apiRoute(GET, "/api/autocomplete", autocompleteApiRoute.getAutocomplete);
apiRoute(GET, "/api/autocomplete/notesCount", autocompleteApiRoute.getNotesCount);
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/services/api-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { OptionRow } from "@triliumnext/commons";
export interface SetupStatusResponse {
syncVersion: number;
schemaExists: boolean;
totpEnabled: boolean;
}

/**
Expand Down
40 changes: 30 additions & 10 deletions apps/server/src/services/auth.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import etapiTokenService from "./etapi_tokens.js";
import log from "./log.js";
import sqlInit from "./sql_init.js";
import { isElectron } from "./utils.js";
import passwordEncryptionService from "./encryption/password_encryption.js";
import type { NextFunction, Request, Response } from "express";

import attributes from "./attributes.js";
import config from "./config.js";
import passwordService from "./encryption/password.js";
import totp from "./totp.js";
import passwordEncryptionService from "./encryption/password_encryption.js";
import recoveryCodeService from "./encryption/recovery_codes.js";
import etapiTokenService from "./etapi_tokens.js";
import log from "./log.js";
import openID from "./open_id.js";
import options from "./options.js";
import attributes from "./attributes.js";
import type { NextFunction, Request, Response } from "express";
import sqlInit from "./sql_init.js";
import totp from "./totp.js";
import { isElectron } from "./utils.js";

let noAuthentication = false;
refreshAuth();
Expand Down Expand Up @@ -161,9 +163,27 @@ function checkCredentials(req: Request, res: Response, next: NextFunction) {
if (!passwordEncryptionService.verifyPassword(password)) {
res.setHeader("Content-Type", "text/plain").status(401).send("Incorrect password");
log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
} else {
next();
return;
}

// Verify TOTP if enabled
if (totp.isTotpEnabled()) {
const totpToken = req.headers["trilium-totp"] || "";
if (typeof totpToken !== "string" || !totpToken) {
res.setHeader("Content-Type", "text/plain").status(401).send("TOTP token is required");
log.info(`WARNING: Missing TOTP token from ${req.ip}, rejecting.`);
return;
}
Comment thread
JYC333 marked this conversation as resolved.
Outdated

// Accept TOTP code or recovery code
if (!totp.validateTOTP(totpToken) && !recoveryCodeService.verifyRecoveryCode(totpToken)) {
res.setHeader("Content-Type", "text/plain").status(401).send("Incorrect TOTP token");
log.info(`WARNING: Wrong TOTP token from ${req.ip}, rejecting.`);
return;
}
}

next();
}

export default {
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/services/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ async function exec<T>(opts: ExecOpts): Promise<T> {

if (opts.auth) {
headers["trilium-cred"] = Buffer.from(`dummy:${opts.auth.password}`).toString("base64");
if (opts.auth.totpToken) {
headers["trilium-totp"] = opts.auth.totpToken;
}
}

const request = (await client).request({
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/services/request_interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ExecOpts {
cookieJar?: CookieJar;
auth?: {
password?: string;
totpToken?: string;
};
timeout: number;
body?: string | {};
Expand Down
35 changes: 25 additions & 10 deletions apps/server/src/services/setup.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import syncService from "./sync.js";
import becca from "../becca/becca.js";
import type { SetupStatusResponse, SetupSyncSeedResponse } from "./api-interface.js";
import appInfo from "./app_info.js";
import log from "./log.js";
import sqlInit from "./sql_init.js";
import optionService from "./options.js";
import syncOptions from "./sync_options.js";
import request from "./request.js";
import appInfo from "./app_info.js";
import sqlInit from "./sql_init.js";
import syncService from "./sync.js";
import syncOptions from "./sync_options.js";
import { timeLimit } from "./utils.js";
import becca from "../becca/becca.js";
import type { SetupStatusResponse, SetupSyncSeedResponse } from "./api-interface.js";

async function hasSyncServerSchemaAndSeed() {
const response = await requestToSyncServer<SetupStatusResponse>("GET", "/api/setup/status");
Expand Down Expand Up @@ -55,13 +55,13 @@ async function requestToSyncServer<T>(method: string, path: string, body?: strin
url: syncOptions.getSyncServerHost() + path,
body,
proxy: syncOptions.getSyncProxy(),
timeout: timeout
timeout
}),
timeout
)) as T;
}

async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string) {
async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string, totpToken?: string) {
if (sqlInit.isDbInitialized()) {
return {
result: "failure",
Expand All @@ -76,7 +76,7 @@ async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string
const resp = await request.exec<SetupSyncSeedResponse>({
method: "get",
url: `${syncServerHost}/api/setup/sync-seed`,
auth: { password },
auth: { password, totpToken },
proxy: syncProxy,
timeout: 30000 // seed request should not take long
});
Expand Down Expand Up @@ -111,10 +111,25 @@ function getSyncSeedOptions() {
return [becca.getOption("documentId"), becca.getOption("documentSecret")];
}

async function checkRemoteTotpStatus(syncServerHost: string): Promise<{ totpEnabled: boolean }> {
try {
const resp = await request.exec<{ totpEnabled?: boolean }>({
method: "get",
url: `${syncServerHost}/api/setup/status`,
proxy: null,
timeout: 10000
});
return { totpEnabled: !!resp?.totpEnabled };
} catch {
return { totpEnabled: false };
}
}
Comment thread
JYC333 marked this conversation as resolved.

export default {
hasSyncServerSchemaAndSeed,
triggerSync,
sendSeedToSyncServer,
setupSyncFromSyncServer,
getSyncSeedOptions
getSyncSeedOptions,
checkRemoteTotpStatus
};