From b04325fdc0ef53253e41b834ef04e1899e918ed2 Mon Sep 17 00:00:00 2001 From: Fabrice Mouhartem Date: Fri, 12 Jun 2026 14:44:50 +0200 Subject: [PATCH] feat(recovery): add support for hashed recovery key on the server + legacy --- lib/challenge-commands/totp.js | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/challenge-commands/totp.js b/lib/challenge-commands/totp.js index a4ecfaa06..9292e3409 100644 --- a/lib/challenge-commands/totp.js +++ b/lib/challenge-commands/totp.js @@ -12,6 +12,7 @@ const Sessions = require("../storage/sessions"); const BlockStore = require("../storage/block"); const Block = require("../commands/block"); const config = require("../load-config"); +const Nacl = require('tweetnacl/nacl-fast'); const Commands = module.exports; @@ -37,6 +38,17 @@ var isValidRecoveryKey = otp => { /[A-Za-z0-9+\/]{32}/.test(otp); }; +const parseRecoveryKey = recoveryStr => { + let splitted = recoveryStr.split(':'); + let type = splitted[0]; + // Only allow authorized type. To be upgraded when supporting more recovery modes + if (!['secret', 'hash'].includes(type)) { + type = ''; + } + const content = splitted.slice(1).join(':'); + return { type, content }; +}; + // we'll only allow users to set up multi-factor auth // for keypairs they control which already have blocks // this check doesn't confirm that their id is valid base64 @@ -383,12 +395,24 @@ const revoke = Commands.TOTP_REVOKE = function (Env, body, cb) { }).nThen(function (w) { if (!recoveryKey) { return; } w.abort(); - if (!/^secret:/.test(recoveryStored)) { + const recoveryParsed = parseRecoveryKey(recoveryStored); + if (!recoveryParsed.type) { return void cb("E_NO_RECOVERY_KEY"); } - recoveryStored = recoveryStored.slice(7); - if (recoveryKey !== recoveryStored) { - return void cb("E_WRONG_RECOVERY_KEY"); + switch (recoveryParsed.type) { + case 'secret': + if (recoveryKey !== recoveryParsed.content) { + return void cb("E_WRONG_RECOVERY_KEY"); + } + break; + case 'hash': + if (Util.encodeBase64(Nacl.hash(Util.decodeBase64(recoveryKey))) + !== recoveryParsed.content) { + return void cb("E_WRONG_RECOVERY_KEY"); + } + break; + default: + return void cb("E_WRONG_RECOVERY_KEY"); } cb(); }).nThen(function () {