From 3e0f95458f1e1085b1268b7dea2c602d8f902c56 Mon Sep 17 00:00:00 2001 From: matt burnett Date: Sun, 4 Aug 2024 23:59:45 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=95=B8=EF=B8=8F=20refactor:=20Migrate=20f?= =?UTF-8?q?rom=20`crypto`=20to=20Web=20Crypto=20API=20(#3357)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * move crypto to async webcrypto update encrypt/decrypt forgot await * chore: import order - openidStrategy.js * chore: import order - Session.js * chore: import order - AuthController.js * Update AuthService.js --------- Co-authored-by: Danny Avila --- api/models/Session.js | 5 +- api/server/controllers/AuthController.js | 5 +- api/server/routes/assistants/actions.js | 2 +- api/server/services/ActionService.js | 20 ++--- api/server/services/AuthService.js | 4 +- api/server/services/PluginService.js | 4 +- api/server/services/ToolService.js | 2 +- api/server/services/UserService.js | 4 +- api/server/utils/crypto.js | 108 ++++++++++++++++++----- api/strategies/openidStrategy.js | 5 +- 10 files changed, 108 insertions(+), 51 deletions(-) diff --git a/api/models/Session.js b/api/models/Session.js index de7e07400a..77cc30118b 100644 --- a/api/models/Session.js +++ b/api/models/Session.js @@ -1,6 +1,6 @@ -const crypto = require('crypto'); const mongoose = require('mongoose'); const signPayload = require('~/server/services/signPayload'); +const { hashToken } = require('~/server/utils/crypto'); const { logger } = require('~/config'); const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; @@ -39,8 +39,7 @@ sessionSchema.methods.generateRefreshToken = async function () { expirationTime: Math.floor((expiresIn - Date.now()) / 1000), }); - const hash = crypto.createHash('sha256'); - this.refreshTokenHash = hash.update(refreshToken).digest('hex'); + this.refreshTokenHash = await hashToken(refreshToken); await this.save(); diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index 1a93254f26..0225798535 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -1,4 +1,3 @@ -const crypto = require('crypto'); const cookies = require('cookie'); const jwt = require('jsonwebtoken'); const { @@ -7,6 +6,7 @@ const { setAuthTokens, requestPasswordReset, } = require('~/server/services/AuthService'); +const { hashToken } = require('~/server/utils/crypto'); const { Session, getUserById } = require('~/models'); const { logger } = require('~/config'); @@ -74,8 +74,7 @@ const refreshController = async (req, res) => { } // Hash the refresh token - const hash = crypto.createHash('sha256'); - const hashedToken = hash.update(refreshToken).digest('hex'); + const hashedToken = await hashToken(refreshToken); // Find the session with the hashed refresh token const session = await Session.findOne({ user: userId, refreshTokenHash: hashedToken }); diff --git a/api/server/routes/assistants/actions.js b/api/server/routes/assistants/actions.js index e79d7bc2a5..b780636c31 100644 --- a/api/server/routes/assistants/actions.js +++ b/api/server/routes/assistants/actions.js @@ -42,7 +42,7 @@ router.post('/:assistant_id', async (req, res) => { return res.status(400).json({ message: 'No functions provided' }); } - let metadata = encryptMetadata(_metadata); + let metadata = await encryptMetadata(_metadata); let { domain } = metadata; domain = await domainParser(req, domain, true); diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js index 6f832bce13..ff8fe5ac5b 100644 --- a/api/server/services/ActionService.js +++ b/api/server/services/ActionService.js @@ -116,8 +116,8 @@ async function loadActionSets(searchParams) { * @param {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call. * @returns { { _call: (toolInput: Object) => unknown} } An object with `_call` method to execute the tool input. */ -function createActionTool({ action, requestBuilder }) { - action.metadata = decryptMetadata(action.metadata); +async function createActionTool({ action, requestBuilder }) { + action.metadata = await decryptMetadata(action.metadata); const _call = async (toolInput) => { try { requestBuilder.setParams(toolInput); @@ -153,23 +153,23 @@ function createActionTool({ action, requestBuilder }) { * @param {ActionMetadata} metadata - The action metadata to encrypt. * @returns {ActionMetadata} The updated action metadata with encrypted values. */ -function encryptMetadata(metadata) { +async function encryptMetadata(metadata) { const encryptedMetadata = { ...metadata }; // ServiceHttp if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) { if (metadata.api_key) { - encryptedMetadata.api_key = encryptV2(metadata.api_key); + encryptedMetadata.api_key = await encryptV2(metadata.api_key); } } // OAuth else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) { if (metadata.oauth_client_id) { - encryptedMetadata.oauth_client_id = encryptV2(metadata.oauth_client_id); + encryptedMetadata.oauth_client_id = await encryptV2(metadata.oauth_client_id); } if (metadata.oauth_client_secret) { - encryptedMetadata.oauth_client_secret = encryptV2(metadata.oauth_client_secret); + encryptedMetadata.oauth_client_secret = await encryptV2(metadata.oauth_client_secret); } } @@ -182,23 +182,23 @@ function encryptMetadata(metadata) { * @param {ActionMetadata} metadata - The action metadata to decrypt. * @returns {ActionMetadata} The updated action metadata with decrypted values. */ -function decryptMetadata(metadata) { +async function decryptMetadata(metadata) { const decryptedMetadata = { ...metadata }; // ServiceHttp if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) { if (metadata.api_key) { - decryptedMetadata.api_key = decryptV2(metadata.api_key); + decryptedMetadata.api_key = await decryptV2(metadata.api_key); } } // OAuth else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) { if (metadata.oauth_client_id) { - decryptedMetadata.oauth_client_id = decryptV2(metadata.oauth_client_id); + decryptedMetadata.oauth_client_id = await decryptV2(metadata.oauth_client_id); } if (metadata.oauth_client_secret) { - decryptedMetadata.oauth_client_secret = decryptV2(metadata.oauth_client_secret); + decryptedMetadata.oauth_client_secret = await decryptV2(metadata.oauth_client_secret); } } diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 729024b7e9..9664a7e67c 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -1,4 +1,3 @@ -const crypto = require('crypto'); const bcrypt = require('bcryptjs'); const { SystemRoles, errorsToString } = require('librechat-data-provider'); const { @@ -12,6 +11,7 @@ const { } = require('~/models/userMethods'); const { sendEmail, checkEmailConfig } = require('~/server/utils'); const { registerSchema } = require('~/strategies/validators'); +const { hashToken } = require('~/server/utils/crypto'); const isDomainAllowed = require('./isDomainAllowed'); const Token = require('~/models/schema/tokenSchema'); const Session = require('~/models/Session'); @@ -34,7 +34,7 @@ const genericVerificationMessage = 'Please check your email to verify your email */ const logoutUser = async (userId, refreshToken) => { try { - const hash = crypto.createHash('sha256').update(refreshToken).digest('hex'); + const hash = await hashToken(refreshToken); // Find the session with the matching user and refreshTokenHash const session = await Session.findOne({ user: userId, refreshTokenHash: hash }); diff --git a/api/server/services/PluginService.js b/api/server/services/PluginService.js index 39d1693f87..2b09da96a7 100644 --- a/api/server/services/PluginService.js +++ b/api/server/services/PluginService.js @@ -29,7 +29,7 @@ const getUserPluginAuthValue = async (userId, authField) => { throw new Error(`No plugin auth ${authField} found for user ${userId}`); } - const decryptedValue = decrypt(pluginAuth.value); + const decryptedValue = await decrypt(pluginAuth.value); return decryptedValue; } catch (err) { logger.error('[getUserPluginAuthValue]', err); @@ -64,7 +64,7 @@ const getUserPluginAuthValue = async (userId, authField) => { const updateUserPluginAuth = async (userId, authField, pluginKey, value) => { try { - const encryptedValue = encrypt(value); + const encryptedValue = await encrypt(value); const pluginAuth = await PluginAuth.findOne({ userId, authField }).lean(); if (pluginAuth) { const pluginAuth = await PluginAuth.updateOne( diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index a91948b19a..5e9b5112a0 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -335,7 +335,7 @@ async function processRequiredActions(client, requiredActions) { continue; } - tool = createActionTool({ action: actionSet, requestBuilder }); + tool = await createActionTool({ action: actionSet, requestBuilder }); isActionTool = !!tool; ActionToolMap[currentAction.tool] = tool; } diff --git a/api/server/services/UserService.js b/api/server/services/UserService.js index c8f8e966ce..30b54c7406 100644 --- a/api/server/services/UserService.js +++ b/api/server/services/UserService.js @@ -50,7 +50,7 @@ const getUserKey = async ({ userId, name }) => { }), ); } - return decrypt(keyValue.value); + return await decrypt(keyValue.value); }; /** @@ -109,7 +109,7 @@ const getUserKeyExpiry = async ({ userId, name }) => { * after encrypting the provided value. It sets the provided expiry date for the key. */ const updateUserKey = async ({ userId, name, value, expiresAt = null }) => { - const encryptedValue = encrypt(value); + const encryptedValue = await encrypt(value); let updateObject = { userId, name, diff --git a/api/server/utils/crypto.js b/api/server/utils/crypto.js index 8989084e5a..9fe1f898fb 100644 --- a/api/server/utils/crypto.js +++ b/api/server/utils/crypto.js @@ -1,34 +1,74 @@ require('dotenv').config(); -const crypto = require('crypto'); +const { webcrypto } = require('node:crypto'); const key = Buffer.from(process.env.CREDS_KEY, 'hex'); const iv = Buffer.from(process.env.CREDS_IV, 'hex'); const algorithm = 'aes-256-cbc'; -function encrypt(value) { - const cipher = crypto.createCipheriv(algorithm, key, iv); - let encrypted = cipher.update(value, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - return encrypted; +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, + }, + cryptoKey, + data, + ); + + return Buffer.from(encryptedBuffer).toString('hex'); } -function decrypt(encryptedValue) { - const decipher = crypto.createDecipheriv(algorithm, key, iv); - let decrypted = decipher.update(encryptedValue, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; +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, + }, + cryptoKey, + encryptedBuffer, + ); + + const decoder = new TextDecoder(); + return decoder.decode(decryptedBuffer); } -// Programatically generate iv -function encryptV2(value) { - const gen_iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv(algorithm, key, gen_iv); - let encrypted = cipher.update(value, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - return gen_iv.toString('hex') + ':' + encrypted; +// Programmatically generate iv +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, + }, + cryptoKey, + data, + ); + + return Buffer.from(gen_iv).toString('hex') + ':' + Buffer.from(encryptedBuffer).toString('hex'); } -function decryptV2(encryptedValue) { +async function decryptV2(encryptedValue) { const parts = encryptedValue.split(':'); // Already decrypted from an earlier invocation if (parts.length === 1) { @@ -36,10 +76,30 @@ function decryptV2(encryptedValue) { } const gen_iv = Buffer.from(parts.shift(), 'hex'); const encrypted = parts.join(':'); - const decipher = crypto.createDecipheriv(algorithm, key, gen_iv); - let decrypted = decipher.update(encrypted, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; + + 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, + }, + cryptoKey, + encryptedBuffer, + ); + + const decoder = new TextDecoder(); + return decoder.decode(decryptedBuffer); } -module.exports = { encrypt, decrypt, encryptV2, decryptV2 }; +async function hashToken(str) { + const data = new TextEncoder().encode(str); + const hashBuffer = await webcrypto.subtle.digest('SHA-256', data); + return Buffer.from(hashBuffer).toString('hex'); +} + +module.exports = { encrypt, decrypt, encryptV2, decryptV2, hashToken }; diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 2beeaa13eb..d818725e00 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -5,6 +5,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent'); const { Issuer, Strategy: OpenIDStrategy, custom } = require('openid-client'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { findUser, createUser, updateUser } = require('~/models/userMethods'); +const { hashToken } = require('~/server/utils/crypto'); const { logger } = require('~/config'); let crypto; @@ -184,9 +185,7 @@ async function setupOpenId() { let fileName; if (crypto) { - const hash = crypto.createHash('sha256'); - hash.update(userinfo.sub); - fileName = hash.digest('hex') + '.png'; + fileName = (await hashToken(userinfo.sub)) + '.png'; } else { fileName = userinfo.sub + '.png'; }