🕸️ refactor: Migrate from crypto to Web Crypto API (#3357)

* 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 <danacordially@gmail.com>
This commit is contained in:
matt burnett 2024-08-04 23:59:45 -04:00 committed by GitHub
parent b6fe7e5570
commit 3e0f95458f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 108 additions and 51 deletions

View file

@ -1,6 +1,6 @@
const crypto = require('crypto');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const signPayload = require('~/server/services/signPayload'); const signPayload = require('~/server/services/signPayload');
const { hashToken } = require('~/server/utils/crypto');
const { logger } = require('~/config'); const { logger } = require('~/config');
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
@ -39,8 +39,7 @@ sessionSchema.methods.generateRefreshToken = async function () {
expirationTime: Math.floor((expiresIn - Date.now()) / 1000), expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
}); });
const hash = crypto.createHash('sha256'); this.refreshTokenHash = await hashToken(refreshToken);
this.refreshTokenHash = hash.update(refreshToken).digest('hex');
await this.save(); await this.save();

View file

@ -1,4 +1,3 @@
const crypto = require('crypto');
const cookies = require('cookie'); const cookies = require('cookie');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { const {
@ -7,6 +6,7 @@ const {
setAuthTokens, setAuthTokens,
requestPasswordReset, requestPasswordReset,
} = require('~/server/services/AuthService'); } = require('~/server/services/AuthService');
const { hashToken } = require('~/server/utils/crypto');
const { Session, getUserById } = require('~/models'); const { Session, getUserById } = require('~/models');
const { logger } = require('~/config'); const { logger } = require('~/config');
@ -74,8 +74,7 @@ const refreshController = async (req, res) => {
} }
// Hash the refresh token // Hash the refresh token
const hash = crypto.createHash('sha256'); const hashedToken = await hashToken(refreshToken);
const hashedToken = hash.update(refreshToken).digest('hex');
// Find the session with the hashed refresh token // Find the session with the hashed refresh token
const session = await Session.findOne({ user: userId, refreshTokenHash: hashedToken }); const session = await Session.findOne({ user: userId, refreshTokenHash: hashedToken });

View file

@ -42,7 +42,7 @@ router.post('/:assistant_id', async (req, res) => {
return res.status(400).json({ message: 'No functions provided' }); return res.status(400).json({ message: 'No functions provided' });
} }
let metadata = encryptMetadata(_metadata); let metadata = await encryptMetadata(_metadata);
let { domain } = metadata; let { domain } = metadata;
domain = await domainParser(req, domain, true); domain = await domainParser(req, domain, true);

View file

@ -116,8 +116,8 @@ async function loadActionSets(searchParams) {
* @param {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call. * @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. * @returns { { _call: (toolInput: Object) => unknown} } An object with `_call` method to execute the tool input.
*/ */
function createActionTool({ action, requestBuilder }) { async function createActionTool({ action, requestBuilder }) {
action.metadata = decryptMetadata(action.metadata); action.metadata = await decryptMetadata(action.metadata);
const _call = async (toolInput) => { const _call = async (toolInput) => {
try { try {
requestBuilder.setParams(toolInput); requestBuilder.setParams(toolInput);
@ -153,23 +153,23 @@ function createActionTool({ action, requestBuilder }) {
* @param {ActionMetadata} metadata - The action metadata to encrypt. * @param {ActionMetadata} metadata - The action metadata to encrypt.
* @returns {ActionMetadata} The updated action metadata with encrypted values. * @returns {ActionMetadata} The updated action metadata with encrypted values.
*/ */
function encryptMetadata(metadata) { async function encryptMetadata(metadata) {
const encryptedMetadata = { ...metadata }; const encryptedMetadata = { ...metadata };
// ServiceHttp // ServiceHttp
if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) { if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) {
if (metadata.api_key) { if (metadata.api_key) {
encryptedMetadata.api_key = encryptV2(metadata.api_key); encryptedMetadata.api_key = await encryptV2(metadata.api_key);
} }
} }
// OAuth // OAuth
else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) { else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) {
if (metadata.oauth_client_id) { 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) { 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. * @param {ActionMetadata} metadata - The action metadata to decrypt.
* @returns {ActionMetadata} The updated action metadata with decrypted values. * @returns {ActionMetadata} The updated action metadata with decrypted values.
*/ */
function decryptMetadata(metadata) { async function decryptMetadata(metadata) {
const decryptedMetadata = { ...metadata }; const decryptedMetadata = { ...metadata };
// ServiceHttp // ServiceHttp
if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) { if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) {
if (metadata.api_key) { if (metadata.api_key) {
decryptedMetadata.api_key = decryptV2(metadata.api_key); decryptedMetadata.api_key = await decryptV2(metadata.api_key);
} }
} }
// OAuth // OAuth
else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) { else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) {
if (metadata.oauth_client_id) { 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) { if (metadata.oauth_client_secret) {
decryptedMetadata.oauth_client_secret = decryptV2(metadata.oauth_client_secret); decryptedMetadata.oauth_client_secret = await decryptV2(metadata.oauth_client_secret);
} }
} }

View file

@ -1,4 +1,3 @@
const crypto = require('crypto');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const { SystemRoles, errorsToString } = require('librechat-data-provider'); const { SystemRoles, errorsToString } = require('librechat-data-provider');
const { const {
@ -12,6 +11,7 @@ const {
} = require('~/models/userMethods'); } = require('~/models/userMethods');
const { sendEmail, checkEmailConfig } = require('~/server/utils'); const { sendEmail, checkEmailConfig } = require('~/server/utils');
const { registerSchema } = require('~/strategies/validators'); const { registerSchema } = require('~/strategies/validators');
const { hashToken } = require('~/server/utils/crypto');
const isDomainAllowed = require('./isDomainAllowed'); const isDomainAllowed = require('./isDomainAllowed');
const Token = require('~/models/schema/tokenSchema'); const Token = require('~/models/schema/tokenSchema');
const Session = require('~/models/Session'); const Session = require('~/models/Session');
@ -34,7 +34,7 @@ const genericVerificationMessage = 'Please check your email to verify your email
*/ */
const logoutUser = async (userId, refreshToken) => { const logoutUser = async (userId, refreshToken) => {
try { try {
const hash = crypto.createHash('sha256').update(refreshToken).digest('hex'); const hash = await hashToken(refreshToken);
// Find the session with the matching user and refreshTokenHash // Find the session with the matching user and refreshTokenHash
const session = await Session.findOne({ user: userId, refreshTokenHash: hash }); const session = await Session.findOne({ user: userId, refreshTokenHash: hash });

View file

@ -29,7 +29,7 @@ const getUserPluginAuthValue = async (userId, authField) => {
throw new Error(`No plugin auth ${authField} found for user ${userId}`); throw new Error(`No plugin auth ${authField} found for user ${userId}`);
} }
const decryptedValue = decrypt(pluginAuth.value); const decryptedValue = await decrypt(pluginAuth.value);
return decryptedValue; return decryptedValue;
} catch (err) { } catch (err) {
logger.error('[getUserPluginAuthValue]', err); logger.error('[getUserPluginAuthValue]', err);
@ -64,7 +64,7 @@ const getUserPluginAuthValue = async (userId, authField) => {
const updateUserPluginAuth = async (userId, authField, pluginKey, value) => { const updateUserPluginAuth = async (userId, authField, pluginKey, value) => {
try { try {
const encryptedValue = encrypt(value); const encryptedValue = await encrypt(value);
const pluginAuth = await PluginAuth.findOne({ userId, authField }).lean(); const pluginAuth = await PluginAuth.findOne({ userId, authField }).lean();
if (pluginAuth) { if (pluginAuth) {
const pluginAuth = await PluginAuth.updateOne( const pluginAuth = await PluginAuth.updateOne(

View file

@ -335,7 +335,7 @@ async function processRequiredActions(client, requiredActions) {
continue; continue;
} }
tool = createActionTool({ action: actionSet, requestBuilder }); tool = await createActionTool({ action: actionSet, requestBuilder });
isActionTool = !!tool; isActionTool = !!tool;
ActionToolMap[currentAction.tool] = tool; ActionToolMap[currentAction.tool] = tool;
} }

View file

@ -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. * after encrypting the provided value. It sets the provided expiry date for the key.
*/ */
const updateUserKey = async ({ userId, name, value, expiresAt = null }) => { const updateUserKey = async ({ userId, name, value, expiresAt = null }) => {
const encryptedValue = encrypt(value); const encryptedValue = await encrypt(value);
let updateObject = { let updateObject = {
userId, userId,
name, name,

View file

@ -1,34 +1,74 @@
require('dotenv').config(); require('dotenv').config();
const crypto = require('crypto'); const { webcrypto } = require('node:crypto');
const key = Buffer.from(process.env.CREDS_KEY, 'hex'); const key = Buffer.from(process.env.CREDS_KEY, 'hex');
const iv = Buffer.from(process.env.CREDS_IV, 'hex'); const iv = Buffer.from(process.env.CREDS_IV, 'hex');
const algorithm = 'aes-256-cbc'; const algorithm = 'aes-256-cbc';
function encrypt(value) { async function encrypt(value) {
const cipher = crypto.createCipheriv(algorithm, key, iv); const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [
let encrypted = cipher.update(value, 'utf8', 'hex'); 'encrypt',
encrypted += cipher.final('hex'); ]);
return encrypted;
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) { async function decrypt(encryptedValue) {
const decipher = crypto.createDecipheriv(algorithm, key, iv); const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [
let decrypted = decipher.update(encryptedValue, 'hex', 'utf8'); 'decrypt',
decrypted += decipher.final('utf8'); ]);
return decrypted;
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 // Programmatically generate iv
function encryptV2(value) { async function encryptV2(value) {
const gen_iv = crypto.randomBytes(16); const gen_iv = webcrypto.getRandomValues(new Uint8Array(16));
const cipher = crypto.createCipheriv(algorithm, key, gen_iv);
let encrypted = cipher.update(value, 'utf8', 'hex'); const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [
encrypted += cipher.final('hex'); 'encrypt',
return gen_iv.toString('hex') + ':' + encrypted; ]);
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(':'); const parts = encryptedValue.split(':');
// Already decrypted from an earlier invocation // Already decrypted from an earlier invocation
if (parts.length === 1) { if (parts.length === 1) {
@ -36,10 +76,30 @@ function decryptV2(encryptedValue) {
} }
const gen_iv = Buffer.from(parts.shift(), 'hex'); const gen_iv = Buffer.from(parts.shift(), 'hex');
const encrypted = parts.join(':'); const encrypted = parts.join(':');
const decipher = crypto.createDecipheriv(algorithm, key, gen_iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8'); const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [
decrypted += decipher.final('utf8'); 'decrypt',
return decrypted; ]);
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 };

View file

@ -5,6 +5,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent');
const { Issuer, Strategy: OpenIDStrategy, custom } = require('openid-client'); const { Issuer, Strategy: OpenIDStrategy, custom } = require('openid-client');
const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { findUser, createUser, updateUser } = require('~/models/userMethods'); const { findUser, createUser, updateUser } = require('~/models/userMethods');
const { hashToken } = require('~/server/utils/crypto');
const { logger } = require('~/config'); const { logger } = require('~/config');
let crypto; let crypto;
@ -184,9 +185,7 @@ async function setupOpenId() {
let fileName; let fileName;
if (crypto) { if (crypto) {
const hash = crypto.createHash('sha256'); fileName = (await hashToken(userinfo.sub)) + '.png';
hash.update(userinfo.sub);
fileName = hash.digest('hex') + '.png';
} else { } else {
fileName = userinfo.sub + '.png'; fileName = userinfo.sub + '.png';
} }