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
This commit is contained in:
MHSanaei
2026-10-05 16:30:14 +02:00
parent 2c5fc8e72c
commit d7da64f2f0
2 changed files with 53 additions and 4 deletions
+9 -4
View File
@@ -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;
}
+44
View File
@@ -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);
});
});