feat: 🎸 move totp services to encryption logic

This commit is contained in:
Jin 2025-03-28 02:15:25 +01:00
parent 687d506ca5
commit 5742d3068e
5 changed files with 65 additions and 58 deletions

View File

@ -111,7 +111,7 @@ interface OAuthStatus {
} }
interface TOTPStatus { interface TOTPStatus {
enabled: boolean; set: boolean;
message: boolean; message: boolean;
} }
@ -278,30 +278,21 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget {
this.$oauthOptions.hide(); this.$oauthOptions.hide();
} }
server.get<OAuthStatus>("oauth/status").then((result) => { // server.get<OAuthStatus>("oauth/status").then((result) => {
if (result.enabled) { // if (result.enabled) {
if (result.name) this.$UserAccountName.text(result.name); // if (result.name) this.$UserAccountName.text(result.name);
if (result.email) this.$UserAccountEmail.text(result.email); // if (result.email) this.$UserAccountEmail.text(result.email);
this.$envEnabledOAuth.hide(); // this.$envEnabledOAuth.hide();
} else { // } else {
this.$envEnabledOAuth.text(t("multi_factor_authentication.oauth_enable_description")); // this.$envEnabledOAuth.text(t("multi_factor_authentication.oauth_enable_description"));
this.$envEnabledOAuth.show(); // this.$envEnabledOAuth.show();
} // }
}); // });
server.get<TOTPStatus>("totp/status").then((result) => { server.get<TOTPStatus>("totp/status").then((result) => {
if (result.enabled) { if (result.set) {
this.$generateTotpButton.prop("disabled", !result.message); this.$generateTotpButton.text(t("multi_factor_authentication.totp_secret_regenerate"));
this.$generateRecoveryCodeButton.prop("disabled", !result.message);
this.$envEnabledTOTP.hide();
} else {
this.$generateTotpButton.prop("disabled", true);
this.$generateRecoveryCodeButton.prop("disabled", true);
this.$envEnabledTOTP.text(t("multi_factor_authentication.totp_enable_description"));
this.$envEnabledTOTP.show();
} }
}); });
this.$protectedSessionTimeout.val(Number(options.protectedSessionTimeout)); this.$protectedSessionTimeout.val(Number(options.protectedSessionTimeout));

View File

@ -1,20 +1,15 @@
import { generateSecret } from 'time2fa'; import totpService from '../../services/totp.js';
import config from '../../services/config.js';
function generateTOTPSecret() { function generateTOTPSecret() {
return { success: true, message: generateSecret() }; return totpService.createSecret();
}
function getTotpEnabled() {
return config.MultiFactorAuthentication.totpEnabled;
} }
function getTOTPStatus() { function getTOTPStatus() {
return { success: true, message: getTotpEnabled(), enabled: getTotpEnabled() }; return { success: true, message: totpService.isTotpEnabled(), set: totpService.checkForTotpSecret() };
} }
function getSecret() { function getSecret() {
return config.MultiFactorAuthentication.totpSecret; return totpService.getTotpSecret();
} }
export default { export default {

View File

@ -8,7 +8,7 @@ const TOTP_OPTIONS: Record<string, OptionNames> = {
SALT: "totpEncryptionSalt", SALT: "totpEncryptionSalt",
ENCRYPTED_SECRET: "totpEncryptedSecret", ENCRYPTED_SECRET: "totpEncryptedSecret",
VERIFICATION_HASH: "totpVerificationHash" VERIFICATION_HASH: "totpVerificationHash"
} as const; };
function verifyTotpSecret(secret: string): boolean { function verifyTotpSecret(secret: string): boolean {
const givenSecretHash = toBase64(myScryptService.getVerificationHash(secret)); const givenSecretHash = toBase64(myScryptService.getVerificationHash(secret));
@ -21,20 +21,17 @@ function verifyTotpSecret(secret: string): boolean {
return givenSecretHash === dbSecretHash; return givenSecretHash === dbSecretHash;
} }
function setTotpSecret(secret: string): void { function setTotpSecret(secret: string) {
if (!secret) { if (!secret) {
throw new Error("TOTP secret cannot be empty"); throw new Error("TOTP secret cannot be empty");
} }
// 生成新的加密盐值
const encryptionSalt = randomSecureToken(32); const encryptionSalt = randomSecureToken(32);
optionService.setOption(TOTP_OPTIONS.SALT, encryptionSalt); optionService.setOption(TOTP_OPTIONS.SALT, encryptionSalt);
// 使用 scrypt 生成验证哈希
const verificationHash = toBase64(myScryptService.getVerificationHash(secret)); const verificationHash = toBase64(myScryptService.getVerificationHash(secret));
optionService.setOption(TOTP_OPTIONS.VERIFICATION_HASH, verificationHash); optionService.setOption(TOTP_OPTIONS.VERIFICATION_HASH, verificationHash);
// 使用数据加密密钥加密 TOTP secret
const encryptedSecret = dataEncryptionService.encrypt( const encryptedSecret = dataEncryptionService.encrypt(
Buffer.from(encryptionSalt), Buffer.from(encryptionSalt),
secret secret
@ -67,7 +64,7 @@ function getTotpSecret(): string | null {
} }
} }
function resetTotpSecret(): void { function resetTotpSecret() {
optionService.setOption(TOTP_OPTIONS.SALT, ""); optionService.setOption(TOTP_OPTIONS.SALT, "");
optionService.setOption(TOTP_OPTIONS.ENCRYPTED_SECRET, ""); optionService.setOption(TOTP_OPTIONS.ENCRYPTED_SECRET, "");
optionService.setOption(TOTP_OPTIONS.VERIFICATION_HASH, ""); optionService.setOption(TOTP_OPTIONS.VERIFICATION_HASH, "");

View File

@ -131,10 +131,10 @@ const defaultOptions: DefaultOption[] = [
{ name: "customSearchEngineUrl", value: "https://duckduckgo.com/?q={keyword}", isSynced: true }, { name: "customSearchEngineUrl", value: "https://duckduckgo.com/?q={keyword}", isSynced: true },
{ name: "promotedAttributesOpenInRibbon", value: "true", isSynced: true }, { name: "promotedAttributesOpenInRibbon", value: "true", isSynced: true },
{ name: "editedNotesOpenInRibbon", value: "true", isSynced: true }, { name: "editedNotesOpenInRibbon", value: "true", isSynced: true },
{ name: 'totpEnabled', value: 'false', isSynced: true }, { name: "mfaEnabled", value: "false", isSynced: false },
{ name: 'mfaMethod', value: 'totp', isSynced: false },
{ name: 'encryptedRecoveryCodes', value: 'false', isSynced: true }, { name: 'encryptedRecoveryCodes', value: 'false', isSynced: true },
{ name: 'userSubjectIdentifierSaved', value: 'false', isSynced: true }, { name: 'userSubjectIdentifierSaved', value: 'false', isSynced: true },
{ name: 'oAuthEnabled', value: 'false', isSynced: true },
// Appearance // Appearance
{ name: "splitEditorOrientation", value: "horizontal", isSynced: true }, { name: "splitEditorOrientation", value: "horizontal", isSynced: true },

View File

@ -1,40 +1,64 @@
import { Totp } from 'time2fa'; import { Totp, generateSecret } from 'time2fa';
import config from './config.js'; import options from './options.js';
import MFAError from '../errors/mfa_error.js'; import totpEncryptionService from './encryption/totp_encryption.js';
function isTotpEnabled(): boolean {
function isTotpEnabled() { return options.getOption('mfaEnabled') === "true" && options.getOption('mfaMethod') === "totp";
if (config.MultiFactorAuthentication.totpEnabled && config.MultiFactorAuthentication.totpSecret === "") {
throw new MFAError("TOTP secret is not set!");
}
return config.MultiFactorAuthentication.totpEnabled;
} }
function getTotpSecret() { function createSecret(): { success: boolean; message?: string } {
return config.MultiFactorAuthentication.totpSecret; try {
const secret = generateSecret();
totpEncryptionService.setTotpSecret(secret);
return {
success: true,
message: secret
};
} catch (e) {
console.error('Failed to create TOTP secret:', e);
return {
success: false,
message: e instanceof Error ? e.message : 'Unknown error occurred'
};
}
} }
function checkForTotSecret() { function getTotpSecret(): string | null {
return config.MultiFactorAuthentication.totpSecret === "" ? false : true; return totpEncryptionService.getTotpSecret();
} }
function validateTOTP(submittedPasscode: string) { function checkForTotpSecret(): boolean {
if (config.MultiFactorAuthentication.totpSecret === "") return false; return totpEncryptionService.isTotpSecretSet();
}
function validateTOTP(submittedPasscode: string): boolean {
const secret = getTotpSecret();
if (!secret) return false;
try { try {
const valid = Totp.validate({ return Totp.validate({
passcode: submittedPasscode, passcode: submittedPasscode,
secret: config.MultiFactorAuthentication.totpSecret.trim() secret: secret.trim()
}); });
return valid;
} catch (e) { } catch (e) {
console.error('Failed to validate TOTP:', e);
return false; return false;
} }
} }
function resetTotp(): void {
totpEncryptionService.resetTotpSecret();
options.setOption('mfaEnabled', 'false');
options.setOption('mfaMethod', '');
}
export default { export default {
isTotpEnabled, isTotpEnabled,
createSecret,
getTotpSecret, getTotpSecret,
checkForTotSecret, checkForTotpSecret,
validateTOTP validateTOTP,
resetTotp
}; };