From d7da64f2f0a79151cd32daa9ad35cd49eabe952b Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 5 Oct 2026 16:30:14 +0200 Subject: [PATCH] fix(qr): hide the QR only for links carrying post-quantum keys A share link's QR is suppressed only when it carries a post-quantum key payload too large to scan: an ML-DSA-65 verify key (pqv) or an ML-KEM-768 VLESS-encryption auth key. isPostQuantumLink substring-matched "mlkem768" anywhere in the URL, so it misfired on: - every VLESS/Trojan REALITY link, since ce221c33 added the support-x25519mlkem768=true hint (235 chars, QR version 10); - every VLESS-encryption link authenticated by an X25519 key, whose value always starts with mlkem768x25519plus (321 chars, version 11); - any remark or host containing mlkem768 / mldsa65 / ML-KEM-768. All four QR surfaces (inbound QR, client QR, client info, public sub page) lost their QR button for those links. The detector now reads the query: a non-empty pqv, or an encryption whose auth key vlessEncryptionAuthKind classifies as ML-KEM-768. Closes #6730 --- frontend/src/lib/xray/inbound-link.ts | 13 +++++--- frontend/src/test/inbound-link.test.ts | 44 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index c604922a1..4fd457f44 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -17,6 +17,7 @@ import { parseGeckoPacketSize } from '@/lib/xray/forms/transport/FinalMaskForm'; import { getHeaderValue } from './headers'; import { canEnableTlsFlow } from './protocol-capabilities'; import { deriveSpiderX } from './spider-x'; +import { vlessEncryptionAuthKind } from './vless-encryption'; import { normalizeTuicCongestionController, resolveTuicServerSettings } from '@/lib/tuic'; // Share-link generators. Each per-protocol fn takes a typed inbound plus @@ -1723,9 +1724,13 @@ function wgPeerCommentSuffix(peer: unknown): string { return typeof comment === 'string' && comment.trim() !== '' ? ` (${comment.trim()})` : ''; } +// Only the post-quantum key payloads outgrow a QR; the REALITY ML-KEM hint and the +// mlkem768x25519plus prefix of an X25519-authenticated encryption do not (#6730). export function isPostQuantumLink(link: string): boolean { - if (/[?&]pqv=/.test(link)) return true; - if (link.includes('mlkem768') || link.includes('mldsa65')) return true; - if (link.includes('ML-KEM-768')) return true; - return false; + const withoutRemark = link.split('#', 1)[0]; + const queryStart = withoutRemark.indexOf('?'); + if (queryStart < 0) return false; + const params = new URLSearchParams(withoutRemark.slice(queryStart + 1)); + if (params.get('pqv')) return true; + return vlessEncryptionAuthKind(params.get('encryption') ?? '')?.startsWith('mlkem768') ?? false; } diff --git a/frontend/src/test/inbound-link.test.ts b/frontend/src/test/inbound-link.test.ts index 0be93480f..084765cb6 100644 --- a/frontend/src/test/inbound-link.test.ts +++ b/frontend/src/test/inbound-link.test.ts @@ -16,6 +16,7 @@ import { genVmessLink, genWireguardConfig, genWireguardLink, + isPostQuantumLink, preferPublicHost, resolveAddr, } from '@/lib/xray/inbound-link'; @@ -1415,3 +1416,46 @@ describe('genTuicLink', () => { expect(link).not.toContain('#TUIC-Node-US-US'); }); }); + +describe('isPostQuantumLink', () => { + type RealityFixture = { + settings: { clients: Array<{ id: string }>; encryption?: string }; + streamSettings: { realitySettings: { settings: { mldsa65Verify?: string } } }; + }; + const [, raw] = fixturesForProtocol('vless').find(([name]) => name === 'vless-tcp-reality')!; + const clientId = (raw as RealityFixture).settings.clients[0].id; + const x25519Key = 'G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y'; + const mlkem768Key = 'A'.repeat(1579); + + function realityLink(edit: (inbound: RealityFixture) => void = () => {}): string { + const copy = structuredClone(raw) as RealityFixture; + edit(copy); + return genVlessLink({ inbound: InboundSchema.parse(copy), address: 'example.test', clientId }); + } + + // #6730: the REALITY ML-KEM support hint is a short flag, not a large PQ payload. + it('keeps the QR for a plain REALITY link', () => { + expect(isPostQuantumLink(realityLink())).toBe(false); + }); + + it('keeps the QR for VLESS encryption authenticated by an X25519 key', () => { + const link = realityLink((ib) => { + ib.settings.encryption = `mlkem768x25519plus.native.0rtt.${x25519Key}`; + }); + expect(isPostQuantumLink(link)).toBe(false); + }); + + it('hides the QR for VLESS encryption authenticated by an ML-KEM-768 key', () => { + const link = realityLink((ib) => { + ib.settings.encryption = `mlkem768x25519plus.native.0rtt.${mlkem768Key}`; + }); + expect(isPostQuantumLink(link)).toBe(true); + }); + + it('hides the QR for a REALITY link carrying an ML-DSA-65 verify key', () => { + const link = realityLink((ib) => { + ib.streamSettings.realitySettings.settings.mldsa65Verify = 'B'.repeat(2603); + }); + expect(isPostQuantumLink(link)).toBe(true); + }); +});