mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🕸️ 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:
parent
b6fe7e5570
commit
3e0f95458f
10 changed files with 108 additions and 51 deletions
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue