From 3bf773779007d787d1a58a5d60f2fc08dbf741bd Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 1 May 2024 23:38:22 -0400 Subject: [PATCH] Add recovery codes --- .../backend/src/routers/auth/configure-2fa.js | 19 ++++++-- .../backend/src/services/auth/OTPService.js | 10 ++++ src/UI/Settings/UITabSecurity.js | 4 +- src/UI/UIWindowQR.js | 39 ++++++++++++---- src/css/style.css | 46 ++++++++++++++++++- src/i18n/translations/en.js | 3 +- 6 files changed, 105 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/routers/auth/configure-2fa.js b/packages/backend/src/routers/auth/configure-2fa.js index b355086e..231aeb2b 100644 --- a/packages/backend/src/routers/auth/configure-2fa.js +++ b/packages/backend/src/routers/auth/configure-2fa.js @@ -26,13 +26,24 @@ module.exports = eggspress('/auth/configure-2fa/:action', { actions.setup = async () => { const svc_otp = x.get('services').get('otp'); + + // generate secret const result = svc_otp.create_secret(); + + // generate recovery codes + result.codes = []; + for ( let i = 0; i < 10; i++ ) { + result.codes.push(svc_otp.create_recovery_code()); + } + + // update user await db.write( - `UPDATE user SET otp_secret = ? WHERE uuid = ?`, - [result.secret, user.uuid] + `UPDATE user SET otp_secret = ?, otp_recovery_codes = ? WHERE uuid = ?`, + [result.secret, result.codes.join(','), user.uuid] ); - // update cached user req.user.otp_secret = result.secret; + req.user.otp_recovery_codes = result.codes.join(','); + return result; }; @@ -48,7 +59,7 @@ module.exports = eggspress('/auth/configure-2fa/:action', { actions.disable = async () => { await db.write( - `UPDATE user SET otp_enabled = 0 WHERE uuid = ?`, + `UPDATE user SET otp_enabled = 0, otp_recovery_codes = '' WHERE uuid = ?`, [user.uuid] ); return { success: true }; diff --git a/packages/backend/src/services/auth/OTPService.js b/packages/backend/src/services/auth/OTPService.js index 7c61e5ee..37d6b7a8 100644 --- a/packages/backend/src/services/auth/OTPService.js +++ b/packages/backend/src/services/auth/OTPService.js @@ -26,6 +26,16 @@ class OTPService extends BaseService { }; } + create_recovery_code () { + const require = this.require; + const crypto = require('crypto'); + const { encode } = require('hi-base32'); + + const buffer = crypto.randomBytes(6); + const code = encode(buffer).replace(/=/g, "").substring(0, 8); + return code; + } + verify (secret, code) { const require = this.require; const otpauth = require('otpauth'); diff --git a/src/UI/Settings/UITabSecurity.js b/src/UI/Settings/UITabSecurity.js index 586280be..17ba0e11 100644 --- a/src/UI/Settings/UITabSecurity.js +++ b/src/UI/Settings/UITabSecurity.js @@ -64,10 +64,10 @@ export default { i18n('confirm_2fa_setup'), i18n('confirm_2fa_recovery'), ], + recovery_codes: data.codes, + has_confirm_and_cancel: true, }); - console.log('confirmation?', confirmation); - if ( ! confirmation ) return; await fetch(`${api_origin}/auth/configure-2fa/enable`, { diff --git a/src/UI/UIWindowQR.js b/src/UI/UIWindowQR.js index 281e12f1..04eb1f88 100644 --- a/src/UI/UIWindowQR.js +++ b/src/UI/UIWindowQR.js @@ -20,6 +20,8 @@ import TeePromise from '../util/TeePromise.js'; import UIWindow from './UIWindow.js' +let checkbox_id_ = 0; + async function UIWindowQR(options){ const confirmations = options.confirmations || []; @@ -36,24 +38,45 @@ async function UIWindowQR(options){ }`; h += ``; + if ( options.recovery_codes ) { + h += `
`; + h += `

${ + i18n('recovery_codes') + }

`; + h += `
`; + for ( let i=0 ; i < options.recovery_codes.length ; i++ ) { + h += `
${ + html_encode(options.recovery_codes[i]) + }
`; + } + h += `
`; + h += `
`; + } + for ( let i=0 ; i < confirmations.length ; i++ ) { const confirmation = confirmations[i]; // checkbox h += `
`; - h += ``; - h += ``; + h += ``; + h += ``; h += `
`; } // h += ``; - h += ``; - h += ``; + if ( options.has_confirm_and_cancel ) { + h += ``; + h += ``; + } else { + h += ``; + } const el_window = await UIWindow({ title: 'Instant Login!', diff --git a/src/css/style.css b/src/css/style.css index cd0d35a3..24a7572c 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -2606,11 +2606,55 @@ label { justify-content: center; flex-direction: column; align-items: center; - height: 520px; } .otp-qr-code img { width: 355px; + margin-bottom: 20px; +} + +.recovery-codes { + border: 1px solid #ccc; + padding: 20px; + margin: 20px auto; + width: 90%; + max-width: 600px; + background-color: #f9f9f9; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.recovery-codes h2 { + text-align: center; + font-size: 18px; + color: #333; + margin-bottom: 15px; +} + +.recovery-codes-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 10px; /* Adds space between grid items */ + padding: 0; +} + +.recovery-code { + background-color: #fff; + border: 1px solid #ddd; + padding: 10px; + text-align: center; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + letter-spacing: 1px; +} + +.qr-code-checkbox { + display: flex; + gap: 10px; + align-items: center; +} + +.qr-code-checkbox input[type=checkbox] { + margin: 0; } .perm-title { diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js index 753b905e..0c5d37ac 100644 --- a/src/i18n/translations/en.js +++ b/src/i18n/translations/en.js @@ -47,7 +47,7 @@ const en = { color: 'Color', hue: 'Hue', confirm_2fa_setup: 'I have added the code to my authenticator app', - confirm_2fa_recovery: 'I have saved my recovery codes', + confirm_2fa_recovery: 'I have saved my recovery codes in a secure location', confirm_account_for_free_referral_storage_c2a: 'Create an account and confirm your email address to receive 1 GB of free storage. Your friend will get 1 GB of free storage too.', confirm_code_generic_incorrect: "Incorrect Code.", confirm_code_generic_title: "Enter Confirmation Code", @@ -212,6 +212,7 @@ const en = { save_session: 'Save session', save_session_c2a: 'Create an account to save your current session and avoid losing your work.', scan_qr_c2a: 'Scan the code below to log into this session from other devices', + scan_qr_2fa: 'Scan the QR code with your authenticator app', scan_qr_generic: 'Scan this QR code using your phone or another device', seconds: 'seconds', security: "Security",