From b8f630f8ab2c7515379d986330b4a6cf83f68675 Mon Sep 17 00:00:00 2001 From: web Date: Tue, 16 Sep 2025 04:32:16 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Refactor=20key=20generati?= =?UTF-8?q?on=20logic=20and=20update=20dependencies=20for=20ML-KEM-768=20i?= =?UTF-8?q?ntegration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/dashboard/servers/form-schema.ts | 14 +++-- .../app/dashboard/servers/generate/index.ts | 10 ++-- .../dashboard/servers/generate/mlkem768.ts | 34 ++++++++++++ .../servers/generate/mlkem768x25519plus.ts | 23 -------- .../dashboard/servers/generate/reality-key.ts | 49 ------------------ .../dashboard/servers/generate/short-id.ts | 15 ++++++ .../servers/generate/{random.ts => uid.ts} | 0 .../app/dashboard/servers/generate/util.ts | 6 +++ .../app/dashboard/servers/generate/x25519.ts | 11 ++++ apps/admin/package.json | 1 + bun.lockb | Bin 653120 -> 653488 bytes 11 files changed, 80 insertions(+), 83 deletions(-) create mode 100644 apps/admin/app/dashboard/servers/generate/mlkem768.ts delete mode 100644 apps/admin/app/dashboard/servers/generate/mlkem768x25519plus.ts delete mode 100644 apps/admin/app/dashboard/servers/generate/reality-key.ts create mode 100644 apps/admin/app/dashboard/servers/generate/short-id.ts rename apps/admin/app/dashboard/servers/generate/{random.ts => uid.ts} (100%) create mode 100644 apps/admin/app/dashboard/servers/generate/util.ts create mode 100644 apps/admin/app/dashboard/servers/generate/x25519.ts diff --git a/apps/admin/app/dashboard/servers/form-schema.ts b/apps/admin/app/dashboard/servers/form-schema.ts index a211701..84b8f5f 100644 --- a/apps/admin/app/dashboard/servers/form-schema.ts +++ b/apps/admin/app/dashboard/servers/form-schema.ts @@ -1,6 +1,10 @@ import { z } from 'zod'; -import { generatePassword, generateRealityKeyPair, generateRealityShortId } from './generate'; -import { generateVlessX25519Pair } from './generate/mlkem768x25519plus'; +import { + generateMLKEM768KeyPair, + generatePassword, + generateRealityKeyPair, + generateRealityShortId, +} from './generate'; export const protocols = [ 'shadowsocks', @@ -700,10 +704,10 @@ export const PROTOCOL_FIELDS: Record = { placeholder: (t) => t('encryption_private_key_placeholder'), group: 'encryption', generate: { - function: () => generateVlessX25519Pair(), + function: generateMLKEM768KeyPair, updateFields: { - encryption_private_key: 'privateKeyB64', - encryption_password: 'passwordB64', + encryption_private_key: 'privateKey', + encryption_password: 'publicKey', }, }, condition: (p) => p.encryption === 'mlkem768x25519plus', diff --git a/apps/admin/app/dashboard/servers/generate/index.ts b/apps/admin/app/dashboard/servers/generate/index.ts index 7a5ddda..3cb29f3 100644 --- a/apps/admin/app/dashboard/servers/generate/index.ts +++ b/apps/admin/app/dashboard/servers/generate/index.ts @@ -1,6 +1,4 @@ -export { generatePassword } from './random'; -export { - generateRealityKeyPair, - generateRealityShortId, - publicKeyFromPrivate, -} from './reality-key'; +export { generateMLKEM768KeyPair } from './mlkem768'; +export { generateRealityShortId } from './short-id'; +export { generatePassword } from './uid'; +export { generateRealityKeyPair } from './x25519'; diff --git a/apps/admin/app/dashboard/servers/generate/mlkem768.ts b/apps/admin/app/dashboard/servers/generate/mlkem768.ts new file mode 100644 index 0000000..7e4261c --- /dev/null +++ b/apps/admin/app/dashboard/servers/generate/mlkem768.ts @@ -0,0 +1,34 @@ +import mlkem from 'mlkem-wasm'; +import { toB64Url } from './util'; + +export async function generateMLKEM768KeyPair() { + const mlkemKeyPair = await mlkem.generateKey({ name: 'ML-KEM-768' }, true, [ + 'encapsulateBits', + 'decapsulateBits', + ]); + const mlkemPublicKeyRaw = await mlkem.exportKey('raw-public', mlkemKeyPair.publicKey); + const mlkemPrivateKeyRaw = await mlkem.exportKey('raw-seed', mlkemKeyPair.privateKey); + + return { + publicKey: toB64Url(new Uint8Array(mlkemPublicKeyRaw)), + privateKey: toB64Url(new Uint8Array(mlkemPrivateKeyRaw)), + }; +} +const test = await generateMLKEM768KeyPair(); + +console.log('生成的密钥信息:'); +console.log('私钥长度:', test.privateKey.length); +console.log('公钥长度:', test.publicKey.length); +console.log(test); + +// 从 VLESS 配置字符串中提取的密钥 +const extractedKeys = { + decryptionKey: + 'B2qLcDHhiztvBaB4BhMCnU-fM-axE4DZowMK9TIvL5qma2M5fVAmFswPdfej2N1SmlJa5ppidC1ksHLVfoEvjw', + encryptionKey: + 'FgEI5sTIzBgZS4psemcdGDCRb5JvdDFqzOeVvLJYEmmZFSgxG1SkYxx8DUO-txGqvYdhFruja-ZzmIQoGOCQEMd0RbK4BhYRm4jE96GXTytu7Hi7pYo9-2SEuKC10righ2ceqis0LoggubajmAFvkop6igZfcmEA3MJ9aHpjGiUszDWy0pA99WY7c1ebTom8xQetW2J2OnkrA_UE8Cuy8AhMPvSBQrsFNmBqnttfVlmKzPS3RWBkIpAZIie1XMGk76nDEXgEsBulEhmYO2Mc-oRfirQfGmYfD1ybYBs9gAKgzQixTrkJtOuRU4GjkwQpxuyeVuww8Pin19IzAlVpUdeEJPVlRHwQDNJ__YsZDJB6rSIaY4tM4ysSs3N-1mmYvkVs1WSa0uy14KRheqMIPfsw_KxLBvp2_ZdZeQIrW3dDxgOuxXWip-g78desyONwwkx0bQK-JDcELHEH0TVmTOe4mSqW1fPI6jJI_ZChmQwBxZKpp2RN2xKmw6W3z4ETdUQDTZgePEkXDveneltNHrGT73WJQ7uEs6hwfXwD2vGcuFx6DKybfYAzgWh9t4IM3mBI7OiGqDItigqIDgaBF6LPTXwby3VxgfEVXXqXzMVdc0BL4da1BPGF-lAtvVJtbpBp7_O_JlB1wWajv8eLCpKjRiKGQey7oLZyRROQ3loNFyRDBjoBVSa2etCmFRCV1wegIBqmJRImJQxPsIV_kkIxeWmPfUIik1GpSTtrWkAFCXZAl4oSFiNhLwlmW_g_s_glGeqcZBNAkiw-OlssPESmqLti-bSJPbmoJMNE0poJXCYX27YyPKVyCtJKM5pzOkUBqMIVsxliU4N2IaWHb8uMYKW8U0tQ8aaeR3Bf5ll0fza78aY3lSQw7At21XkN9LhugzWv-_CPGZQpStGCWtJl5dChLPlmZbRddmekp7UEfXAPW6ONyNrFZ9WUvBCwsdmXcChTXwkg-FIMFHpU25c6IwWqyveuZrQmpvZYEQSgyWhsUBMPywI1vqiLfuhnaqBNU9wPbGA0IrG-w9UGpQErl9ssqPdZRjaIbiM-PKKooehp34QzU3ENj5h944gC4yHMkMzOPUaFl8YUWwmkGCsTNnJyq7NLucTOdQSUsLM2QWEkxWk9c6YyQkwx2mUsR1eGmrsbo8BGK6ppbgotpzMjGZfPOQRdHYh0lzGQ28HGetJ97Vei8Vxxl2u4j6CfHTPET4EHZ_uTuPtRIaaMegKOtUgyKqKj4BqsB5tatIECz8N1H_LP5qlRvUxYqrU-JikuRCoAdfl7VFYLLhe_80cny2UoWFpJ1iovprnALDbIIKZ03LG0OXI8dfEolQijO1xhwoUb7Ci3Xma-wKyEWHhSw0e600SRT0RhDuOh5hVg4KVLILUDDuMIJQyiPRBW3qwAMCk1SzCeXvFqbWFU-TQnVoLGNboaygQ6aumgP_B4DBcmEdyVqAMHIOdAPENvLauALKSiBHVjEHgeB7KpVCGsIGOjdCgb4UmKdsxnXBxC7peXspcmGgmHL-VU6KdMhwHwq1mYeNVEzeshb5mWYj5fgysp_e5U--geYKefs5Y', +}; + +console.log('提取的密钥信息:'); +console.log('私钥长度:', extractedKeys.decryptionKey.length); +console.log('公钥长度:', extractedKeys.encryptionKey.length); diff --git a/apps/admin/app/dashboard/servers/generate/mlkem768x25519plus.ts b/apps/admin/app/dashboard/servers/generate/mlkem768x25519plus.ts deleted file mode 100644 index 73914f9..0000000 --- a/apps/admin/app/dashboard/servers/generate/mlkem768x25519plus.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { x25519 } from '@noble/curves/ed25519'; - -const toB64Url = (u8: Uint8Array) => - (typeof Buffer !== 'undefined' - ? Buffer.from(u8).toString('base64') - : btoa(String.fromCharCode(...u8)) - ) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/g, ''); - -export type VlessX25519Pair = { - passwordB64: string; - privateKeyB64: string; -}; - -export function generateVlessX25519Pair(): VlessX25519Pair { - const { secretKey, publicKey } = x25519.keygen(); - return { - passwordB64: toB64Url(publicKey), - privateKeyB64: toB64Url(secretKey), - }; -} diff --git a/apps/admin/app/dashboard/servers/generate/reality-key.ts b/apps/admin/app/dashboard/servers/generate/reality-key.ts deleted file mode 100644 index 6869e99..0000000 --- a/apps/admin/app/dashboard/servers/generate/reality-key.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { x25519 } from '@noble/curves/ed25519.js'; - -function toB64Url(bytes: Uint8Array) { - return btoa(String.fromCharCode(...bytes)) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/g, ''); -} -function fromB64Url(s: string) { - const b64 = s - .replace(/-/g, '+') - .replace(/_/g, '/') - .padEnd(Math.ceil(s.length / 4) * 4, '='); - const bin = atob(b64); - return new Uint8Array([...bin].map((c) => c.charCodeAt(0))); -} -/** - * Generate a Reality key pair - * @returns An object containing the private and public keys in base64url format - */ -export function generateRealityKeyPair() { - const { secretKey, publicKey } = x25519.keygen(); - return { privateKey: toB64Url(secretKey), publicKey: toB64Url(publicKey) }; -} - -/** - * Derive public key from private key - * @param privateKeyB64Url Private key in base64url format - * @returns Public key in base64url format - */ -export function publicKeyFromPrivate(privateKeyB64Url: string) { - return toB64Url(x25519.getPublicKey(fromB64Url(privateKeyB64Url))); -} - -/** - * Generate a short ID for Reality - * @returns A random hexadecimal string of length 2, 4, 6, 8, 10, 12, 14, or 16 - */ -export function generateRealityShortId() { - const hex = '0123456789abcdef'; - const lengths = [2, 4, 6, 8, 10, 12, 14, 16]; - const idx = Math.floor(Math.random() * lengths.length); - const len = lengths[idx] ?? 16; - let out = ''; - for (let i = 0; i < len; i++) { - out += hex.charAt(Math.floor(Math.random() * hex.length)); - } - return out; -} diff --git a/apps/admin/app/dashboard/servers/generate/short-id.ts b/apps/admin/app/dashboard/servers/generate/short-id.ts new file mode 100644 index 0000000..98b5070 --- /dev/null +++ b/apps/admin/app/dashboard/servers/generate/short-id.ts @@ -0,0 +1,15 @@ +/** + * Generate a short ID for Reality + * @returns A random hexadecimal string of length 2, 4, 6, 8, 10, 12, 14, or 16 + */ +export function generateRealityShortId() { + const hex = '0123456789abcdef'; + const lengths = [2, 4, 6, 8, 10, 12, 14, 16]; + const idx = Math.floor(Math.random() * lengths.length); + const len = lengths[idx] ?? 16; + let out = ''; + for (let i = 0; i < len; i++) { + out += hex.charAt(Math.floor(Math.random() * hex.length)); + } + return out; +} diff --git a/apps/admin/app/dashboard/servers/generate/random.ts b/apps/admin/app/dashboard/servers/generate/uid.ts similarity index 100% rename from apps/admin/app/dashboard/servers/generate/random.ts rename to apps/admin/app/dashboard/servers/generate/uid.ts diff --git a/apps/admin/app/dashboard/servers/generate/util.ts b/apps/admin/app/dashboard/servers/generate/util.ts new file mode 100644 index 0000000..96a9057 --- /dev/null +++ b/apps/admin/app/dashboard/servers/generate/util.ts @@ -0,0 +1,6 @@ +export function toB64Url(bytes: Uint8Array) { + return btoa(String.fromCharCode(...bytes)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); +} diff --git a/apps/admin/app/dashboard/servers/generate/x25519.ts b/apps/admin/app/dashboard/servers/generate/x25519.ts new file mode 100644 index 0000000..1ec8d72 --- /dev/null +++ b/apps/admin/app/dashboard/servers/generate/x25519.ts @@ -0,0 +1,11 @@ +import { x25519 } from '@noble/curves/ed25519'; +import { toB64Url } from './util'; + +/** + * Generate a Reality key pair + * @returns An object containing the private and public keys in base64url format + */ +export function generateRealityKeyPair() { + const { secretKey, publicKey } = x25519.keygen(); + return { privateKey: toB64Url(secretKey), publicKey: toB64Url(publicKey) }; +} diff --git a/apps/admin/package.json b/apps/admin/package.json index b3180f9..4da437d 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -18,6 +18,7 @@ "ahooks": "^3.9.4", "axios": "^1.11.0", "js-yaml": "^4.1.0", + "mlkem-wasm": "^0.0.6", "nanoid": "^5.1.5", "next": "^15.5.2", "next-intl": "^3.26.3", diff --git a/bun.lockb b/bun.lockb index 39843e530a3a241ad65d088175b43792d9c743cd..a6c0067fe631dfc77856541c40612d160620a20b 100755 GIT binary patch delta 6016 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