优化和重构代码,增加前端可以设置加密配置数据的密钥

This commit is contained in:
glay 2024-11-22 22:03:42 +08:00
parent bd68df1d9b
commit b0c1ccd0a0
12 changed files with 138 additions and 154 deletions

View File

@ -70,8 +70,9 @@ WHITE_WEBDAV_ENDPOINTS=
### bedrock (optional)
AWS_REGION=
AWS_ACCESS_KEY=
AWS_ACCESS_KEY=AKIA
AWS_SECRET_KEY=
### Assign this with a secure, randomly generated key
### Assign this with a secure, randomly generated key
### Generate a secure, random key that is at least 32 characters long. You can use a password generator or a command like this:
### openssl rand -base64 32
ENCRYPTION_KEY=

View File

@ -52,29 +52,6 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
msg: "you are not allowed to access with your own api key",
};
}
// Special handling for Bedrock
if (modelProvider === ModelProvider.Bedrock) {
const region = serverConfig.awsRegion;
const accessKeyId = serverConfig.awsAccessKey;
const secretAccessKey = serverConfig.awsSecretKey;
console.log("[Auth] Bedrock credentials:", {
region,
accessKeyId: accessKeyId ? "***" : undefined,
secretKey: secretAccessKey ? "***" : undefined,
});
// Check if AWS credentials are provided
if (!region || !accessKeyId || !secretAccessKey) {
return {
error: true,
msg: "Missing AWS credentials. Please configure Region, Access Key ID, and Secret Access Key in settings.",
};
}
return { error: false };
}
// if user does not provide an api key, inject system api key
if (!apiKey) {
const serverConfig = getServerSideConfig();
@ -120,6 +97,14 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
case ModelProvider.ChatGLM:
systemApiKey = serverConfig.chatglmApiKey;
break;
case ModelProvider.Bedrock:
systemApiKey =
serverConfig.awsRegion +
":" +
serverConfig.awsAccessKey +
":" +
serverConfig.awsSecretKey;
break;
case ModelProvider.GPT:
default:
if (req.nextUrl.pathname.includes("azure/deployments")) {

View File

@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { sign, decrypt } from "../utils/aws";
import { sign } from "../utils/aws";
import { getServerSideConfig } from "../config/server";
const ALLOWED_PATH = new Set(["chat", "models"]);
@ -18,7 +19,7 @@ function parseEventData(chunk: Uint8Array): any {
}
return parsed.body || parsed;
} catch (e) {
console.error("Error parsing event data:", e);
// console.error("Error parsing event data:", e);
try {
// Handle base64 encoded responses
const base64Match = text.match(/:"([A-Za-z0-9+/=]+)"/);
@ -76,7 +77,7 @@ async function* transformBedrockStream(
const parsed = parseEventData(value);
if (!parsed) continue;
console.log("Parsed response:", JSON.stringify(parsed, null, 2));
// console.log("Parsed response:", JSON.stringify(parsed, null, 2));
// Handle Titan models
if (modelId.startsWith("amazon.titan")) {
@ -182,26 +183,38 @@ function validateRequest(body: any, modelId: string): void {
async function requestBedrock(req: NextRequest) {
const controller = new AbortController();
const awsRegion = req.headers.get("X-Region") ?? "";
const awsAccessKey = req.headers.get("X-Access-Key") ?? "";
const awsSecretKey = req.headers.get("X-Secret-Key") ?? "";
const awsSessionToken = req.headers.get("X-Session-Token");
const modelId = req.headers.get("X-Model-Id") ?? "";
// Get AWS credentials from server config first
const config = getServerSideConfig();
let awsRegion = config.awsRegion;
let awsAccessKey = config.awsAccessKey;
let awsSecretKey = config.awsSecretKey;
let modelId = "";
// If server-side credentials are not available, parse from Authorization header
if (!awsRegion || !awsAccessKey || !awsSecretKey) {
const authHeader = req.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
throw new Error("Missing or invalid Authorization header");
}
const [_, credentials] = authHeader.split("Bearer ");
const [region, accessKey, secretKey, model] = credentials.split(",");
if (!region || !accessKey || !secretKey || !model) {
throw new Error("Invalid Authorization header format");
}
awsRegion = region;
awsAccessKey = accessKey;
awsSecretKey = secretKey;
modelId = model;
}
if (!awsRegion || !awsAccessKey || !awsSecretKey || !modelId) {
throw new Error("Missing required AWS credentials or model ID");
}
const decryptedAccessKey = decrypt(awsAccessKey);
const decryptedSecretKey = decrypt(awsSecretKey);
const decryptedSessionToken = awsSessionToken
? decrypt(awsSessionToken)
: undefined;
if (!decryptedAccessKey || !decryptedSecretKey) {
throw new Error("Failed to decrypt AWS credentials");
}
// Construct the base endpoint
const baseEndpoint = `https://bedrock-runtime.${awsRegion}.amazonaws.com`;
@ -236,9 +249,8 @@ async function requestBedrock(req: NextRequest) {
method: "POST",
url: endpoint,
region: awsRegion,
accessKeyId: decryptedAccessKey,
secretAccessKey: decryptedSecretKey,
sessionToken: decryptedSessionToken,
accessKeyId: awsAccessKey,
secretAccessKey: awsSecretKey,
body: requestBody,
service: "bedrock",
});

View File

@ -23,7 +23,6 @@ import { SparkApi } from "./platforms/iflytek";
import { XAIApi } from "./platforms/xai";
import { ChatGLMApi } from "./platforms/glm";
import { BedrockApi } from "./platforms/bedrock";
import { encrypt } from "../utils/aws";
export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number];
@ -258,8 +257,6 @@ export function getHeaders(ignoreHeaders: boolean = false) {
const isEnabledAccessControl = accessStore.enabledAccessControl();
const apiKey = isGoogle
? accessStore.googleApiKey
: isBedrock
? accessStore.awsAccessKey // Use AWS access key for Bedrock
: isAzure
? accessStore.azureApiKey
: isAnthropic
@ -278,6 +275,18 @@ export function getHeaders(ignoreHeaders: boolean = false) {
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
: ""
: isBedrock
? accessStore.awsRegion &&
accessStore.awsAccessKey &&
accessStore.awsSecretKey
? accessStore.awsRegion +
"," +
accessStore.awsAccessKey +
"," +
accessStore.awsSecretKey +
"," +
modelConfig.model
: ""
: accessStore.openaiApiKey;
return {
isBedrock,
@ -303,13 +312,10 @@ export function getHeaders(ignoreHeaders: boolean = false) {
? "x-api-key"
: isGoogle
? "x-goog-api-key"
: isBedrock
? "x-api-key"
: "Authorization";
}
const {
isBedrock,
isGoogle,
isAzure,
isAnthropic,
@ -322,28 +328,23 @@ export function getHeaders(ignoreHeaders: boolean = false) {
const authHeader = getAuthHeader();
if (isBedrock) {
// Secure encryption of AWS credentials using the new encryption utility
headers["X-Region"] = encrypt(accessStore.awsRegion);
headers["X-Access-Key"] = encrypt(accessStore.awsAccessKey);
headers["X-Secret-Key"] = encrypt(accessStore.awsSecretKey);
// if (isBedrock) {
// // Secure encryption of AWS credentials using the new encryption utility
// headers["X-Region"] = encrypt(accessStore.awsRegion);
// headers["X-Access-Key"] = encrypt(accessStore.awsAccessKey);
// headers["X-Secret-Key"] = encrypt(accessStore.awsSecretKey);
// } else {
const bearerToken = getBearerToken(
apiKey,
isAzure || isAnthropic || isGoogle,
);
if (accessStore.awsSessionToken) {
headers["X-Session-Token"] = encrypt(accessStore.awsSessionToken);
}
} else {
const bearerToken = getBearerToken(
apiKey,
isAzure || isAnthropic || isGoogle,
if (bearerToken) {
headers[authHeader] = bearerToken;
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
headers["Authorization"] = getBearerToken(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
if (bearerToken) {
headers[authHeader] = bearerToken;
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
headers["Authorization"] = getBearerToken(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
}
}
return headers;

View File

@ -1,5 +1,6 @@
import {
ChatOptions,
getHeaders,
LLMApi,
SpeechOptions,
RequestMessage,
@ -233,23 +234,22 @@ export class BedrockApi implements LLMApi {
const accessStore = useAccessStore.getState();
if (!accessStore.isValidBedrock()) {
throw new Error(
"Invalid AWS credentials. Please check your configuration.",
"Invalid AWS credentials. Please check your configuration and ensure ENCRYPTION_KEY is set.",
);
}
try {
const apiEndpoint = "/api/bedrock/chat";
const headers = {
"Content-Type": requestBody.contentType || "application/json",
Accept: requestBody.accept || "application/json",
"X-Region": accessStore.awsRegion,
"X-Access-Key": accessStore.awsAccessKey,
"X-Secret-Key": accessStore.awsSecretKey,
"X-Model-Id": modelConfig.model,
...(accessStore.awsSessionToken && {
"X-Session-Token": accessStore.awsSessionToken,
}),
};
// const headers = {
// "Content-Type": requestBody.contentType || "application/json",
// Accept: requestBody.accept || "application/json",
// "X-Region": accessStore.awsRegion,
// "X-Access-Key": accessStore.awsAccessKey,
// "X-Secret-Key": accessStore.awsSecretKey,
// "X-Model-Id": modelConfig.model,
// "X-Encryption-Key": accessStore.bedrockEncryptionKey,
// };
const headers = getHeaders();
if (options.config.stream) {
let index = -1;
@ -274,7 +274,6 @@ export class BedrockApi implements LLMApi {
(text: string, runTools: ChatMessageTool[]) => {
try {
const chunkJson = JSON.parse(text);
// console.log("Received chunk:", JSON.stringify(chunkJson, null, 2));
if (chunkJson?.content_block?.type === "tool_use") {
index += 1;
currentToolArgs = "";
@ -375,7 +374,6 @@ export class BedrockApi implements LLMApi {
});
const resJson = await res.json();
// console.log("Response:", JSON.stringify(resJson, null, 2));
const message = this.extractMessage(resJson, modelConfig.model);
// console.log("Extracted message:", message);
options.onFinish(message, res);

View File

@ -79,17 +79,6 @@
pointer-events: none;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
svg {
width: 16px;
height: 16px;
}
}
&:hover {
--delay: 0.5s;
width: var(--full-width);
@ -410,8 +399,8 @@
button {
padding: 7px;
}
}
}
/* Specific styles for iOS devices */
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {

View File

@ -980,12 +980,12 @@ export function Settings() {
onChange={(e) =>
accessStore.update((access) => {
const region = e.currentTarget.value;
if (!/^[a-z]{2}-[a-z]+-\d+$/.test(region)) {
showToast(Locale.Settings.Access.Bedrock.Region.Invalid);
return;
}
if (!/^[a-z]{2}-[a-z]+-\d+$/.test(region)) {
showToast(Locale.Settings.Access.Bedrock.Region.Invalid);
return;
}
access.awsRegion = region;
})
})
}
/>
</ListItem>
@ -999,7 +999,7 @@ export function Settings() {
type="text"
placeholder={Locale.Settings.Access.Bedrock.AccessKey.Placeholder}
onChange={(e) => {
accessStore.update((access) => {
accessStore.update((access) => {
const accessKey = e.currentTarget.value;
if (accessKey && accessKey.length !== 20) {
showToast(Locale.Settings.Access.Bedrock.AccessKey.Invalid);
@ -1022,11 +1022,11 @@ export function Settings() {
placeholder={Locale.Settings.Access.Bedrock.SecretKey.Placeholder}
onChange={(e) => {
accessStore.update((access) => {
const secretKey = e.currentTarget.value;
if (secretKey && secretKey.length !== 40) {
showToast(Locale.Settings.Access.Bedrock.SecretKey.Invalid);
return;
}
const secretKey = e.currentTarget.value;
if (secretKey && secretKey.length !== 40) {
showToast(Locale.Settings.Access.Bedrock.SecretKey.Invalid);
return;
}
access.awsSecretKey = secretKey;
});
}}
@ -1034,17 +1034,17 @@ export function Settings() {
/>
</ListItem>
<ListItem
title={Locale.Settings.Access.Bedrock.SessionToken.Title}
subTitle={Locale.Settings.Access.Bedrock.SessionToken.SubTitle}
title={Locale.Settings.Access.Bedrock.EncryptionKey.Title}
subTitle={Locale.Settings.Access.Bedrock.EncryptionKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Bedrock.SessionToken.Title}
value={accessStore.awsSessionToken}
aria-label={Locale.Settings.Access.Bedrock.EncryptionKey.Title}
value={accessStore.bedrockEncryptionKey}
type="text"
placeholder={Locale.Settings.Access.Bedrock.SessionToken.Placeholder}
placeholder={Locale.Settings.Access.Bedrock.EncryptionKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.awsSessionToken = e.currentTarget.value),
(access) => (access.bedrockEncryptionKey = e.currentTarget.value),
);
}}
maskWhenShow={true}

View File

@ -13,9 +13,10 @@ declare global {
OPENAI_ORG_ID?: string; // openai only
// bedrock only
BEDROCK_REGION?: string;
BEDROCK_API_KEY?: string;
BEDROCK_API_SECRET?: string;
AWS_REGION?: string;
AWS_ACCESS_KEY?: string;
AWS_SECRET_KEY?: string;
ENCRYPTION_KEY?: string;
VERCEL?: string;
BUILD_MODE?: "standalone" | "export";
@ -148,7 +149,10 @@ export const getServerSideConfig = () => {
}
const isStability = !!process.env.STABILITY_API_KEY;
const isBedrock = !!process.env.BEDROCK_API_KEY;
const isBedrock =
!!process.env.AWS_REGION &&
!!process.env.AWS_ACCESS_KEY &&
!!process.env.AWS_SECRET_KEY;
const isAzure = !!process.env.AZURE_URL;
const isGoogle = !!process.env.GOOGLE_API_KEY;
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
@ -182,6 +186,7 @@ export const getServerSideConfig = () => {
awsRegion: process.env.AWS_REGION,
awsAccessKey: process.env.AWS_ACCESS_KEY,
awsSecretKey: process.env.AWS_SECRET_KEY,
bedrockEncryptionKey: process.env.ENCRYPTION_KEY,
isStability,
stabilityUrl: process.env.STABILITY_URL,

View File

@ -361,14 +361,10 @@ const cn = {
Placeholder: "****",
Invalid: "无效的 AWS Secret Key 格式。必须为40个字符。",
},
SessionToken: {
Title: "AWS Session Token (Optional)",
SubTitle: "Your AWS session token if using temporary credentials",
Placeholder: "Optional session token",
},
Endpoint: {
Title: "AWS Bedrock Endpoint",
SubTitle: "Custom endpoint for AWS Bedrock API. Default: ",
EncryptionKey: {
Title: "加密密钥",
SubTitle: "用于配置数据的加密密钥",
Placeholder: "输入加密密钥",
},
},
Azure: {

View File

@ -365,14 +365,10 @@ const en: LocaleType = {
Placeholder: "****",
Invalid: "Invalid AWS secret key format. Must be 40 characters long.",
},
SessionToken: {
Title: "AWS Session Token (Optional)",
SubTitle: "Your AWS session token if using temporary credentials",
Placeholder: "Optional session token",
},
Endpoint: {
Title: "AWS Bedrock Endpoint",
SubTitle: "Custom endpoint for AWS Bedrock API. Default: ",
EncryptionKey: {
Title: "Encryption Key",
SubTitle: "Your encryption key for configuration data",
Placeholder: "Enter encryption key",
},
},
Azure: {

View File

@ -15,6 +15,7 @@ import {
IFLYTEK_BASE_URL,
XAI_BASE_URL,
CHATGLM_BASE_URL,
BEDROCK_BASE_URL,
} from "../constant";
import { getHeaders } from "../client/api";
import { getClientConfig } from "../config/client";
@ -51,6 +52,8 @@ const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
const DEFAULT_CHATGLM_URL = isApp ? CHATGLM_BASE_URL : ApiPath.ChatGLM;
const DEFAULT_BEDROCK_URL = isApp ? BEDROCK_BASE_URL : ApiPath.Bedrock;
const DEFAULT_ACCESS_STATE = {
accessCode: "",
useCustomConfig: false,
@ -117,10 +120,11 @@ const DEFAULT_ACCESS_STATE = {
chatglmApiKey: "",
// aws bedrock
bedrokUrl: DEFAULT_BEDROCK_URL,
awsRegion: "",
awsAccessKey: "",
awsSecretKey: "",
awsSessionToken: "",
bedrockEncryptionKey: "",
// server config
needCode: true,
@ -200,7 +204,12 @@ export const useAccessStore = createPersistStore(
},
isValidBedrock() {
return ensure(get(), ["awsRegion", "awsAccessKey", "awsSecretKey"]);
return ensure(get(), [
"awsRegion",
"awsAccessKey",
"awsSecretKey",
"bedrockEncryptionKey",
]);
},
isAuthorized() {

View File

@ -3,14 +3,14 @@ import HmacSHA256 from "crypto-js/hmac-sha256";
import Hex from "crypto-js/enc-hex";
import Utf8 from "crypto-js/enc-utf8";
import { AES, enc } from "crypto-js";
import { getServerSideConfig } from "../config/server";
const SECRET_KEY =
process.env.ENCRYPTION_KEY ||
"your-secret-key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
if (!SECRET_KEY || SECRET_KEY.length < 32) {
throw new Error(
"ENCRYPTION_KEY environment variable must be set with at least 32 characters",
);
const serverConfig = getServerSideConfig();
// console.info(serverConfig);
const SECRET_KEY = serverConfig.bedrockEncryptionKey || "";
// console.info("======SECRET_KEY:"+SECRET_KEY);
if (serverConfig.isBedrock && !SECRET_KEY) {
console.error("When use Bedrock modle,ENCRYPTION_KEY should been set!");
}
export function encrypt(data: string): string {
@ -54,7 +54,6 @@ export interface SignParams {
region: string;
accessKeyId: string;
secretAccessKey: string;
sessionToken?: string;
body: string;
service: string;
}
@ -143,7 +142,6 @@ export async function sign({
region,
accessKeyId,
secretAccessKey,
sessionToken,
body,
service,
}: SignParams): Promise<Record<string, string>> {
@ -169,11 +167,6 @@ export async function sign({
"x-amzn-bedrock-accept": "*/*",
};
// Add session token if present
if (sessionToken) {
headers["x-amz-security-token"] = sessionToken;
}
// Get sorted header keys (case-insensitive)
const sortedHeaderKeys = Object.keys(headers).sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase()),
@ -230,7 +223,6 @@ export async function sign({
"X-Amz-Content-Sha256": headers["x-amz-content-sha256"],
"X-Amz-Date": headers["x-amz-date"],
"X-Amzn-Bedrock-Accept": headers["x-amzn-bedrock-accept"],
...(sessionToken && { "X-Amz-Security-Token": sessionToken }),
Authorization: authorization,
};
}