From e768a07738e7fc9bed9b69c7e5f0855c7d1ed50f Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Thu, 20 Mar 2025 21:46:11 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20fix:=20Invalid=20Key=20Length=20?= =?UTF-8?q?in=202FA=20Encryption=20(#6432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 feat: Implement v3 encryption and decryption methods for TOTP secrets * 🚀 feat: Refactor Two-Factor Authentication methods and enhance 2FA verification process * 🚀 feat: Update encryption methods to use hex decoding for legacy keys and improve error handling for AES-256-CTR * 🚀 feat: Update import paths in TwoFactorController for consistency and clarity --- api/server/controllers/TwoFactorController.js | 105 ++++++++------ .../auth/TwoFactorAuthController.js | 24 ++-- api/server/routes/auth.js | 23 +-- api/server/services/twoFactorService.js | 132 ++++++++---------- api/server/utils/crypto.js | 87 +++++++----- 5 files changed, 192 insertions(+), 179 deletions(-) diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index 36bc603ae5..f5783f45ad 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -1,22 +1,30 @@ const { - verifyTOTP, - verifyBackupCode, generateTOTPSecret, generateBackupCodes, + verifyTOTP, + verifyBackupCode, getTOTPSecret, } = require('~/server/services/twoFactorService'); const { updateUser, getUserById } = require('~/models'); const { logger } = require('~/config'); -const { encryptV2 } = require('~/server/utils/crypto'); +const { encryptV3 } = require('~/server/utils/crypto'); -const enable2FAController = async (req, res) => { - const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); +const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); + +/** + * Enable 2FA for the user by generating a new TOTP secret and backup codes. + * The secret is encrypted and stored, and 2FA is marked as disabled until confirmed. + */ +const enable2FA = async (req, res) => { try { const userId = req.user.id; const secret = generateTOTPSecret(); const { plainCodes, codeObjects } = await generateBackupCodes(); - const encryptedSecret = await encryptV2(secret); - // Set twoFactorEnabled to false until the user confirms 2FA. + + // Encrypt the secret with v3 encryption before saving. + const encryptedSecret = encryptV3(secret); + + // Update the user record: store the secret & backup codes and set twoFactorEnabled to false. const user = await updateUser(userId, { totpSecret: encryptedSecret, backupCodes: codeObjects, @@ -24,45 +32,50 @@ const enable2FAController = async (req, res) => { }); const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`; - res.status(200).json({ - otpauthUrl, - backupCodes: plainCodes, - }); + + return res.status(200).json({ otpauthUrl, backupCodes: plainCodes }); } catch (err) { - logger.error('[enable2FAController]', err); - res.status(500).json({ message: err.message }); + logger.error('[enable2FA]', err); + return res.status(500).json({ message: err.message }); } }; -const verify2FAController = async (req, res) => { +/** + * Verify a 2FA code (either TOTP or backup code) during setup. + */ +const verify2FA = async (req, res) => { try { const userId = req.user.id; const { token, backupCode } = req.body; const user = await getUserById(userId); - // Ensure that 2FA is enabled for this user. + if (!user || !user.totpSecret) { return res.status(400).json({ message: '2FA not initiated' }); } - // Retrieve the plain TOTP secret using getTOTPSecret. const secret = await getTOTPSecret(user.totpSecret); + let isVerified = false; - if (token && (await verifyTOTP(secret, token))) { - return res.status(200).json(); + if (token) { + isVerified = await verifyTOTP(secret, token); } else if (backupCode) { - const verified = await verifyBackupCode({ user, backupCode }); - if (verified) { - return res.status(200).json(); - } + isVerified = await verifyBackupCode({ user, backupCode }); } - return res.status(400).json({ message: 'Invalid token.' }); + + if (isVerified) { + return res.status(200).json(); + } + return res.status(400).json({ message: 'Invalid token or backup code.' }); } catch (err) { - logger.error('[verify2FAController]', err); - res.status(500).json({ message: err.message }); + logger.error('[verify2FA]', err); + return res.status(500).json({ message: err.message }); } }; -const confirm2FAController = async (req, res) => { +/** + * Confirm and enable 2FA after a successful verification. + */ +const confirm2FA = async (req, res) => { try { const userId = req.user.id; const { token } = req.body; @@ -72,52 +85,54 @@ const confirm2FAController = async (req, res) => { return res.status(400).json({ message: '2FA not initiated' }); } - // Retrieve the plain TOTP secret using getTOTPSecret. const secret = await getTOTPSecret(user.totpSecret); - if (await verifyTOTP(secret, token)) { - // Upon successful verification, enable 2FA. await updateUser(userId, { twoFactorEnabled: true }); return res.status(200).json(); } - return res.status(400).json({ message: 'Invalid token.' }); } catch (err) { - logger.error('[confirm2FAController]', err); - res.status(500).json({ message: err.message }); + logger.error('[confirm2FA]', err); + return res.status(500).json({ message: err.message }); } }; -const disable2FAController = async (req, res) => { +/** + * Disable 2FA by clearing the stored secret and backup codes. + */ +const disable2FA = async (req, res) => { try { const userId = req.user.id; await updateUser(userId, { totpSecret: null, backupCodes: [], twoFactorEnabled: false }); - res.status(200).json(); + return res.status(200).json(); } catch (err) { - logger.error('[disable2FAController]', err); - res.status(500).json({ message: err.message }); + logger.error('[disable2FA]', err); + return res.status(500).json({ message: err.message }); } }; -const regenerateBackupCodesController = async (req, res) => { +/** + * Regenerate backup codes for the user. + */ +const regenerateBackupCodes = async (req, res) => { try { const userId = req.user.id; const { plainCodes, codeObjects } = await generateBackupCodes(); await updateUser(userId, { backupCodes: codeObjects }); - res.status(200).json({ + return res.status(200).json({ backupCodes: plainCodes, backupCodesHash: codeObjects, }); } catch (err) { - logger.error('[regenerateBackupCodesController]', err); - res.status(500).json({ message: err.message }); + logger.error('[regenerateBackupCodes]', err); + return res.status(500).json({ message: err.message }); } }; module.exports = { - enable2FAController, - verify2FAController, - confirm2FAController, - disable2FAController, - regenerateBackupCodesController, + enable2FA, + verify2FA, + confirm2FA, + disable2FA, + regenerateBackupCodes, }; diff --git a/api/server/controllers/auth/TwoFactorAuthController.js b/api/server/controllers/auth/TwoFactorAuthController.js index 1690783368..15cde8122a 100644 --- a/api/server/controllers/auth/TwoFactorAuthController.js +++ b/api/server/controllers/auth/TwoFactorAuthController.js @@ -8,7 +8,10 @@ const { setAuthTokens } = require('~/server/services/AuthService'); const { getUserById } = require('~/models/userMethods'); const { logger } = require('~/config'); -const verify2FA = async (req, res) => { +/** + * Verifies the 2FA code during login using a temporary token. + */ +const verify2FAWithTempToken = async (req, res) => { try { const { tempToken, token, backupCode } = req.body; if (!tempToken) { @@ -23,26 +26,23 @@ const verify2FA = async (req, res) => { } const user = await getUserById(payload.userId); - // Ensure that the user exists and has 2FA enabled if (!user || !user.twoFactorEnabled) { return res.status(400).json({ message: '2FA is not enabled for this user' }); } - // Retrieve (and decrypt if necessary) the TOTP secret. const secret = await getTOTPSecret(user.totpSecret); - - let verified = false; - if (token && (await verifyTOTP(secret, token))) { - verified = true; + let isVerified = false; + if (token) { + isVerified = await verifyTOTP(secret, token); } else if (backupCode) { - verified = await verifyBackupCode({ user, backupCode }); + isVerified = await verifyBackupCode({ user, backupCode }); } - if (!verified) { + if (!isVerified) { return res.status(401).json({ message: 'Invalid 2FA code or backup code' }); } - // Prepare user data for response. + // Prepare user data to return (omit sensitive fields). const userData = user.toObject ? user.toObject() : { ...user }; delete userData.password; delete userData.__v; @@ -52,9 +52,9 @@ const verify2FA = async (req, res) => { const authToken = await setAuthTokens(user._id, res); return res.status(200).json({ token: authToken, user: userData }); } catch (err) { - logger.error('[verify2FA]', err); + logger.error('[verify2FAWithTempToken]', err); return res.status(500).json({ message: 'Something went wrong' }); } }; -module.exports = { verify2FA }; +module.exports = { verify2FAWithTempToken }; diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index 03046d903f..6536b98e92 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -7,12 +7,13 @@ const { } = require('~/server/controllers/AuthController'); const { loginController } = require('~/server/controllers/auth/LoginController'); const { logoutController } = require('~/server/controllers/auth/LogoutController'); -const { verify2FA } = require('~/server/controllers/auth/TwoFactorAuthController'); +const { verify2FAWithTempToken } = require('~/server/controllers/auth/TwoFactorAuthController'); const { - enable2FAController, - verify2FAController, - disable2FAController, - regenerateBackupCodesController, confirm2FAController, + enable2FA, + verify2FA, + disable2FA, + regenerateBackupCodes, + confirm2FA, } = require('~/server/controllers/TwoFactorController'); const { checkBan, @@ -57,11 +58,11 @@ router.post( ); router.post('/resetPassword', checkBan, validatePasswordReset, resetPasswordController); -router.get('/2fa/enable', requireJwtAuth, enable2FAController); -router.post('/2fa/verify', requireJwtAuth, verify2FAController); -router.post('/2fa/verify-temp', checkBan, verify2FA); -router.post('/2fa/confirm', requireJwtAuth, confirm2FAController); -router.post('/2fa/disable', requireJwtAuth, disable2FAController); -router.post('/2fa/backup/regenerate', requireJwtAuth, regenerateBackupCodesController); +router.get('/2fa/enable', requireJwtAuth, enable2FA); +router.post('/2fa/verify', requireJwtAuth, verify2FA); +router.post('/2fa/verify-temp', checkBan, verify2FAWithTempToken); +router.post('/2fa/confirm', requireJwtAuth, confirm2FA); +router.post('/2fa/disable', requireJwtAuth, disable2FA); +router.post('/2fa/backup/regenerate', requireJwtAuth, regenerateBackupCodes); module.exports = router; diff --git a/api/server/services/twoFactorService.js b/api/server/services/twoFactorService.js index e48b2ac938..d000c8fcfc 100644 --- a/api/server/services/twoFactorService.js +++ b/api/server/services/twoFactorService.js @@ -1,15 +1,14 @@ -const { sign } = require('jsonwebtoken'); const { webcrypto } = require('node:crypto'); -const { hashBackupCode, decryptV2 } = require('~/server/utils/crypto'); -const { updateUser } = require('~/models/userMethods'); +const { decryptV3, decryptV2 } = require('../utils/crypto'); +const { hashBackupCode } = require('~/server/utils/crypto'); +// Base32 alphabet for TOTP secret encoding. const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; /** - * Encodes a Buffer into a Base32 string using the RFC 4648 alphabet. - * - * @param {Buffer} buffer - The buffer to encode. - * @returns {string} The Base32 encoded string. + * Encodes a Buffer into a Base32 string. + * @param {Buffer} buffer + * @returns {string} */ const encodeBase32 = (buffer) => { let bits = 0; @@ -30,10 +29,9 @@ const encodeBase32 = (buffer) => { }; /** - * Decodes a Base32-encoded string back into a Buffer. - * - * @param {string} base32Str - The Base32-encoded string. - * @returns {Buffer} The decoded buffer. + * Decodes a Base32 string into a Buffer. + * @param {string} base32Str + * @returns {Buffer} */ const decodeBase32 = (base32Str) => { const cleaned = base32Str.replace(/=+$/, '').toUpperCase(); @@ -56,20 +54,8 @@ const decodeBase32 = (base32Str) => { }; /** - * Generates a temporary token for 2FA verification. - * The token is signed with the JWT_SECRET and expires in 5 minutes. - * - * @param {string} userId - The unique identifier of the user. - * @returns {string} The signed JWT token. - */ -const generate2FATempToken = (userId) => - sign({ userId, twoFAPending: true }, process.env.JWT_SECRET, { expiresIn: '5m' }); - -/** - * Generates a TOTP secret. - * Creates 10 random bytes using WebCrypto and encodes them into a Base32 string. - * - * @returns {string} A Base32-encoded secret for TOTP. + * Generates a new TOTP secret (Base32 encoded). + * @returns {string} */ const generateTOTPSecret = () => { const randomArray = new Uint8Array(10); @@ -78,29 +64,25 @@ const generateTOTPSecret = () => { }; /** - * Generates a Time-based One-Time Password (TOTP) based on the provided secret and time. - * This implementation uses a 30-second time step and produces a 6-digit code. - * - * @param {string} secret - The Base32-encoded TOTP secret. - * @param {number} [forTime=Date.now()] - The time (in milliseconds) for which to generate the TOTP. - * @returns {Promise} A promise that resolves to the 6-digit TOTP code. + * Generates a TOTP code based on the secret and time. + * Uses a 30-second time step and produces a 6-digit code. + * @param {string} secret + * @param {number} [forTime=Date.now()] + * @returns {Promise} */ const generateTOTP = async (secret, forTime = Date.now()) => { const timeStep = 30; // seconds const counter = Math.floor(forTime / 1000 / timeStep); const counterBuffer = new ArrayBuffer(8); const counterView = new DataView(counterBuffer); - // Write counter into the last 4 bytes (big-endian) counterView.setUint32(4, counter, false); - // Decode the secret into an ArrayBuffer const keyBuffer = decodeBase32(secret); const keyArrayBuffer = keyBuffer.buffer.slice( keyBuffer.byteOffset, keyBuffer.byteOffset + keyBuffer.byteLength, ); - // Import the key for HMAC-SHA1 signing const cryptoKey = await webcrypto.subtle.importKey( 'raw', keyArrayBuffer, @@ -108,12 +90,10 @@ const generateTOTP = async (secret, forTime = Date.now()) => { false, ['sign'], ); - - // Generate HMAC signature const signatureBuffer = await webcrypto.subtle.sign('HMAC', cryptoKey, counterBuffer); const hmac = new Uint8Array(signatureBuffer); - // Dynamic truncation as per RFC 4226 + // Dynamic truncation per RFC 4226. const offset = hmac[hmac.length - 1] & 0xf; const slice = hmac.slice(offset, offset + 4); const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength); @@ -123,12 +103,10 @@ const generateTOTP = async (secret, forTime = Date.now()) => { }; /** - * Verifies a provided TOTP token against the secret. - * It allows for a ±1 time-step window to account for slight clock discrepancies. - * - * @param {string} secret - The Base32-encoded TOTP secret. - * @param {string} token - The TOTP token provided by the user. - * @returns {Promise} A promise that resolves to true if the token is valid; otherwise, false. + * Verifies a TOTP token by checking a ±1 time step window. + * @param {string} secret + * @param {string} token + * @returns {Promise} */ const verifyTOTP = async (secret, token) => { const timeStepMS = 30 * 1000; @@ -143,27 +121,24 @@ const verifyTOTP = async (secret, token) => { }; /** - * Generates backup codes for two-factor authentication. - * Each backup code is an 8-character hexadecimal string along with its SHA-256 hash. - * The plain codes are returned for one-time download, while the hashed objects are meant for secure storage. - * - * @param {number} [count=10] - The number of backup codes to generate. + * Generates backup codes (default count: 10). + * Each code is an 8-character hexadecimal string and stored with its SHA-256 hash. + * @param {number} [count=10] * @returns {Promise<{ plainCodes: string[], codeObjects: Array<{ codeHash: string, used: boolean, usedAt: Date | null }> }>} - * A promise that resolves to an object containing both plain backup codes and their corresponding code objects. */ const generateBackupCodes = async (count = 10) => { const plainCodes = []; const codeObjects = []; const encoder = new TextEncoder(); + for (let i = 0; i < count; i++) { const randomArray = new Uint8Array(4); webcrypto.getRandomValues(randomArray); const code = Array.from(randomArray) .map((b) => b.toString(16).padStart(2, '0')) - .join(''); // 8-character hex code + .join(''); plainCodes.push(code); - // Compute SHA-256 hash of the code using WebCrypto const codeBuffer = encoder.encode(code); const hashBuffer = await webcrypto.subtle.digest('SHA-256', codeBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); @@ -174,12 +149,11 @@ const generateBackupCodes = async (count = 10) => { }; /** - * Verifies a backup code for a user and updates its status as used if valid. - * - * @param {Object} params - The parameters object. - * @param {TUser | undefined} [params.user] - The user object containing backup codes. - * @param {string | undefined} [params.backupCode] - The backup code to verify. - * @returns {Promise} A promise that resolves to true if the backup code is valid and updated; otherwise, false. + * Verifies a backup code and, if valid, marks it as used. + * @param {Object} params + * @param {Object} params.user + * @param {string} params.backupCode + * @returns {Promise} */ const verifyBackupCode = async ({ user, backupCode }) => { if (!backupCode || !user || !Array.isArray(user.backupCodes)) { @@ -197,42 +171,54 @@ const verifyBackupCode = async ({ user, backupCode }) => { ? { ...codeObj, used: true, usedAt: new Date() } : codeObj, ); - + // Update the user record with the marked backup code. + const { updateUser } = require('~/models'); await updateUser(user._id, { backupCodes: updatedBackupCodes }); return true; } - return false; }; /** - * Retrieves and, if necessary, decrypts a stored TOTP secret. - * If the secret contains a colon, it is assumed to be in the format "iv:encryptedData" and will be decrypted. - * If the secret is exactly 16 characters long, it is assumed to be a legacy plain secret. - * - * @param {string|null} storedSecret - The stored TOTP secret (which may be encrypted). - * @returns {Promise} A promise that resolves to the plain TOTP secret, or null if none is provided. + * Retrieves and decrypts a stored TOTP secret. + * - Uses decryptV3 if the secret has a "v3:" prefix. + * - Falls back to decryptV2 for colon-delimited values. + * - Assumes a 16-character secret is already plain. + * @param {string|null} storedSecret + * @returns {Promise} */ const getTOTPSecret = async (storedSecret) => { - if (!storedSecret) { return null; } - // Check for a colon marker (encrypted secrets are stored as "iv:encryptedData") + if (!storedSecret) { + return null; + } + if (storedSecret.startsWith('v3:')) { + return decryptV3(storedSecret); + } if (storedSecret.includes(':')) { return await decryptV2(storedSecret); } - // If it's exactly 16 characters, assume it's already plain (legacy secret) if (storedSecret.length === 16) { return storedSecret; } - // Fallback in case it doesn't meet our criteria. return storedSecret; }; +/** + * Generates a temporary JWT token for 2FA verification that expires in 5 minutes. + * @param {string} userId + * @returns {string} + */ +const generate2FATempToken = (userId) => { + const { sign } = require('jsonwebtoken'); + return sign({ userId, twoFAPending: true }, process.env.JWT_SECRET, { expiresIn: '5m' }); +}; + module.exports = { - verifyTOTP, - generateTOTP, - getTOTPSecret, - verifyBackupCode, generateTOTPSecret, + generateTOTP, + verifyTOTP, generateBackupCodes, + verifyBackupCode, + getTOTPSecret, generate2FATempToken, }; diff --git a/api/server/utils/crypto.js b/api/server/utils/crypto.js index 407fad62ac..333cd7573a 100644 --- a/api/server/utils/crypto.js +++ b/api/server/utils/crypto.js @@ -1,27 +1,25 @@ require('dotenv').config(); +const crypto = require('node:crypto'); +const { webcrypto } = crypto; -const { webcrypto } = require('node:crypto'); +// Use hex decoding for both key and IV for legacy methods. const key = Buffer.from(process.env.CREDS_KEY, 'hex'); const iv = Buffer.from(process.env.CREDS_IV, 'hex'); const algorithm = 'AES-CBC'; +// --- Legacy v1/v2 Setup: AES-CBC with fixed key and IV --- + async function encrypt(value) { const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [ 'encrypt', ]); - const encoder = new TextEncoder(); const data = encoder.encode(value); - const encryptedBuffer = await webcrypto.subtle.encrypt( - { - name: algorithm, - iv: iv, - }, + { name: algorithm, iv: iv }, cryptoKey, data, ); - return Buffer.from(encryptedBuffer).toString('hex'); } @@ -29,73 +27,85 @@ async function decrypt(encryptedValue) { const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [ 'decrypt', ]); - const encryptedBuffer = Buffer.from(encryptedValue, 'hex'); - const decryptedBuffer = await webcrypto.subtle.decrypt( - { - name: algorithm, - iv: iv, - }, + { name: algorithm, iv: iv }, cryptoKey, encryptedBuffer, ); - const decoder = new TextDecoder(); return decoder.decode(decryptedBuffer); } -// Programmatically generate iv +// --- v2: AES-CBC with a random IV per encryption --- + async function encryptV2(value) { const gen_iv = webcrypto.getRandomValues(new Uint8Array(16)); - const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [ 'encrypt', ]); - const encoder = new TextEncoder(); const data = encoder.encode(value); - const encryptedBuffer = await webcrypto.subtle.encrypt( - { - name: algorithm, - iv: gen_iv, - }, + { name: algorithm, iv: gen_iv }, cryptoKey, data, ); - return Buffer.from(gen_iv).toString('hex') + ':' + Buffer.from(encryptedBuffer).toString('hex'); } async function decryptV2(encryptedValue) { const parts = encryptedValue.split(':'); - // Already decrypted from an earlier invocation if (parts.length === 1) { return parts[0]; } const gen_iv = Buffer.from(parts.shift(), 'hex'); const encrypted = parts.join(':'); - const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [ 'decrypt', ]); - const encryptedBuffer = Buffer.from(encrypted, 'hex'); - const decryptedBuffer = await webcrypto.subtle.decrypt( - { - name: algorithm, - iv: gen_iv, - }, + { name: algorithm, iv: gen_iv }, cryptoKey, encryptedBuffer, ); - const decoder = new TextDecoder(); return decoder.decode(decryptedBuffer); } +// --- v3: AES-256-CTR using Node's crypto functions --- +const algorithm_v3 = 'aes-256-ctr'; + +/** + * Encrypts a value using AES-256-CTR. + * Note: AES-256 requires a 32-byte key. Ensure that process.env.CREDS_KEY is a 64-character hex string. + * + * @param {string} value - The plaintext to encrypt. + * @returns {string} The encrypted string with a "v3:" prefix. + */ +function encryptV3(value) { + if (key.length !== 32) { + throw new Error(`Invalid key length: expected 32 bytes, got ${key.length} bytes`); + } + const iv_v3 = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm_v3, key, iv_v3); + const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]); + return `v3:${iv_v3.toString('hex')}:${encrypted.toString('hex')}`; +} + +function decryptV3(encryptedValue) { + const parts = encryptedValue.split(':'); + if (parts[0] !== 'v3') { + throw new Error('Not a v3 encrypted value'); + } + const iv_v3 = Buffer.from(parts[1], 'hex'); + const encryptedText = Buffer.from(parts.slice(2).join(':'), 'hex'); + const decipher = crypto.createDecipheriv(algorithm_v3, key, iv_v3); + const decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]); + return decrypted.toString('utf8'); +} + async function hashToken(str) { const data = new TextEncoder().encode(str); const hashBuffer = await webcrypto.subtle.digest('SHA-256', data); @@ -106,30 +116,31 @@ async function getRandomValues(length) { if (!Number.isInteger(length) || length <= 0) { throw new Error('Length must be a positive integer'); } - const randomValues = new Uint8Array(length); webcrypto.getRandomValues(randomValues); return Buffer.from(randomValues).toString('hex'); } /** - * Computes SHA-256 hash for the given input using WebCrypto + * Computes SHA-256 hash for the given input. * @param {string} input - * @returns {Promise} - Hex hash string + * @returns {Promise} */ -const hashBackupCode = async (input) => { +async function hashBackupCode(input) { const encoder = new TextEncoder(); const data = encoder.encode(input); const hashBuffer = await webcrypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); -}; +} module.exports = { encrypt, decrypt, encryptV2, decryptV2, + encryptV3, + decryptV3, hashToken, hashBackupCode, getRandomValues,