修改: app/api/bedrock.ts

修改:     app/client/api.ts
	修改:     app/store/access.ts
	新文件:   app/utils/encryption.ts
	修改:     package.json
This commit is contained in:
glay 2024-11-06 00:21:30 +08:00
parent 1f66d3779c
commit cae20af24d
6 changed files with 2277 additions and 1470 deletions

View File

@ -1,6 +1,7 @@
import { getServerSideConfig } from "../config/server";
import { prettyObject } from "../utils/format";
import { NextRequest, NextResponse } from "next/server";
import { decrypt } from "../utils/encryption";
import {
BedrockRuntimeClient,
ConverseStreamCommand,
@ -12,12 +13,20 @@ import {
const ALLOWED_PATH = new Set(["converse"]);
function decrypt(str: string): string {
try {
return Buffer.from(str, "base64").toString().split("").reverse().join("");
} catch {
return "";
}
// AWS Credential Validation Function
function validateAwsCredentials(
region: string,
accessKeyId: string,
secretAccessKey: string,
): boolean {
const regionRegex = /^[a-z]{2}-[a-z]+-\d+$/;
const accessKeyRegex = /^(AKIA|A3T|ASIA)[A-Z0-9]{16}$/;
return (
regionRegex.test(region) &&
accessKeyRegex.test(accessKeyId) &&
secretAccessKey.length === 40
);
}
export interface ConverseRequest {
@ -140,6 +149,7 @@ export async function handle(
let secretAccessKey = serverConfig.awsSecretKey;
let sessionToken = undefined;
// Attempt to get credentials from headers if not in server config
if (!region || !accessKeyId || !secretAccessKey) {
region = decrypt(req.headers.get("X-Region") ?? "");
accessKeyId = decrypt(req.headers.get("X-Access-Key") ?? "");
@ -149,9 +159,13 @@ export async function handle(
: undefined;
}
if (!region || !accessKeyId || !secretAccessKey) {
// Validate AWS credentials
if (!validateAwsCredentials(region, accessKeyId, secretAccessKey)) {
return NextResponse.json(
{ error: true, msg: "Missing AWS credentials" },
{
error: true,
msg: "Invalid AWS credentials. Please check your region, access key, and secret key.",
},
{ status: 401 },
);
}
@ -159,7 +173,11 @@ export async function handle(
try {
const client = new BedrockRuntimeClient({
region,
credentials: { accessKeyId, secretAccessKey, sessionToken },
credentials: {
accessKeyId,
secretAccessKey,
sessionToken,
},
});
const body = (await req.json()) as ConverseRequest;

View File

@ -23,6 +23,7 @@ import { MoonshotApi } from "./platforms/moonshot";
import { SparkApi } from "./platforms/iflytek";
import { XAIApi } from "./platforms/xai";
import { ChatGLMApi } from "./platforms/glm";
import { encrypt } from "../utils/encryption";
export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number];
@ -330,13 +331,11 @@ export function getHeaders(ignoreHeaders: boolean = false) {
const authHeader = getAuthHeader();
if (isBedrock) {
// 简单加密 AWS credentials
const encrypt = (str: string) =>
Buffer.from(str.split("").reverse().join("")).toString("base64");
// 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 (accessStore.awsSessionToken) {
headers["X-Session-Token"] = encrypt(accessStore.awsSessionToken);
}

View File

@ -22,6 +22,7 @@ import { getClientConfig } from "../config/client";
import { createPersistStore } from "../utils/store";
import { ensure } from "../utils/clone";
import { DEFAULT_CONFIG } from "./config";
import { encrypt, decrypt } from "../utils/encryption";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
@ -137,6 +138,9 @@ const DEFAULT_ACCESS_STATE = {
edgeTTSVoiceName: "zh-CN-YunxiNeural",
};
type AccessState = typeof DEFAULT_ACCESS_STATE;
type BedrockCredentialKey = "awsAccessKey" | "awsSecretKey" | "awsSessionToken";
export const useAccessStore = createPersistStore(
{ ...DEFAULT_ACCESS_STATE },
@ -158,7 +162,43 @@ export const useAccessStore = createPersistStore(
},
isValidBedrock() {
return ensure(get(), ["awsAccessKey", "awsSecretKey", "awsRegion"]);
const state = get();
return (
ensure(state, ["awsAccessKey", "awsSecretKey", "awsRegion"]) &&
this.validateAwsCredentials(
this.getDecryptedAwsCredential("awsAccessKey"),
this.getDecryptedAwsCredential("awsSecretKey"),
state.awsRegion,
)
);
},
validateAwsCredentials(
accessKey: string,
secretKey: string,
region: string,
) {
// Comprehensive AWS credential validation
const accessKeyRegex = /^(AKIA|A3T|ASIA)[A-Z0-9]{16}$/;
const regionRegex = /^[a-z]{2}-[a-z]+-\d+$/;
return (
accessKeyRegex.test(accessKey) && // Validate access key format
secretKey.length === 40 && // Validate secret key length
regionRegex.test(region) && // Validate region format
accessKey !== "" &&
secretKey !== "" &&
region !== ""
);
},
setEncryptedAwsCredential(key: BedrockCredentialKey, value: string) {
set({ [key]: encrypt(value) });
},
getDecryptedAwsCredential(key: BedrockCredentialKey): string {
const encryptedValue = get()[key];
return encryptedValue ? decrypt(encryptedValue) : "";
},
isValidAzure() {
@ -226,6 +266,7 @@ export const useAccessStore = createPersistStore(
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
);
},
fetch() {
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
fetchState = 1;
@ -247,9 +288,25 @@ export const useAccessStore = createPersistStore(
return res;
})
.then((res: DangerConfig) => {
.then((res: Partial<AccessState>) => {
console.log("[Config] got config from server", res);
set(() => ({ ...res }));
// Encrypt Bedrock-related sensitive data before storing
const encryptedRes = { ...res };
const keysToEncrypt: BedrockCredentialKey[] = [
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
];
keysToEncrypt.forEach((key) => {
const value = encryptedRes[key];
if (value) {
(encryptedRes[key] as string) = encrypt(value as string);
}
});
set(() => ({ ...encryptedRes }));
})
.catch(() => {
console.error("[Config] failed to fetch config");

22
app/utils/encryption.ts Normal file
View File

@ -0,0 +1,22 @@
import { AES, enc } from "crypto-js";
const SECRET_KEY = "your-secret-key"; // Replace this with a secure, randomly generated key
export function encrypt(data: string): string {
try {
return AES.encrypt(data, SECRET_KEY).toString();
} catch (error) {
console.error("Encryption failed:", error);
return data; // Fallback to unencrypted data if encryption fails
}
}
export function decrypt(encryptedData: string): string {
try {
const bytes = AES.decrypt(encryptedData, SECRET_KEY);
return bytes.toString(enc.Utf8);
} catch (error) {
console.error("Decryption failed:", error);
return encryptedData; // Fallback to the original data if decryption fails
}
}

View File

@ -20,13 +20,16 @@
"test:ci": "jest --ci"
},
"dependencies": {
"@aws-sdk/client-bedrock-runtime": "^3.679.0",
"@fortaine/fetch-event-source": "^3.0.6",
"@hello-pangea/dnd": "^16.5.0",
"@next/third-parties": "^14.1.0",
"@svgr/webpack": "^6.5.1",
"@types/crypto-js": "^4.2.2",
"@vercel/analytics": "^0.1.11",
"@vercel/speed-insights": "^1.0.2",
"axios": "^1.7.5",
"crypto-js": "^4.2.0",
"emoji-picker-react": "^4.9.2",
"fuse.js": "^7.0.0",
"heic2any": "^0.0.4",
@ -51,8 +54,7 @@
"sass": "^1.59.2",
"spark-md5": "^3.0.2",
"use-debounce": "^9.0.4",
"zustand": "^4.3.8",
"@aws-sdk/client-bedrock-runtime": "^3.679.0"
"zustand": "^4.3.8"
},
"devDependencies": {
"@tauri-apps/api": "^1.6.0",

3613
yarn.lock

File diff suppressed because it is too large Load Diff