reality.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. // Pure REALITY helpers: X25519 keygen (WebCrypto) and server/client config
  2. // templating. No React/DOM — runs in the browser and in vitest (Node 22+,
  3. // which exposes globalThis.crypto.subtle with X25519).
  4. import { bytesToBase64Url } from './base64';
  5. import { buildVless } from './links';
  6. export interface X25519KeyPair {
  7. /** base64url-encoded 32-byte private scalar (xray's format). */
  8. privateKey: string;
  9. /** base64url-encoded 32-byte public key. */
  10. publicKey: string;
  11. }
  12. export function isX25519Available(): boolean {
  13. return typeof globalThis.crypto !== 'undefined' && !!globalThis.crypto.subtle;
  14. }
  15. /**
  16. * Generate an X25519 keypair and return raw 32-byte keys as base64url, matching
  17. * the output of `xray x25519`. The private key cannot be exported as 'raw' in
  18. * WebCrypto, so we export PKCS#8 and take the final 32 bytes (the scalar).
  19. *
  20. * `subtle` is injectable so tests can run deterministically against a stub.
  21. */
  22. export async function generateX25519KeyPair(
  23. subtle: SubtleCrypto = globalThis.crypto.subtle,
  24. ): Promise<X25519KeyPair> {
  25. const pair = (await subtle.generateKey({ name: 'X25519' }, true, [
  26. 'deriveBits',
  27. ])) as CryptoKeyPair;
  28. const rawPublic = new Uint8Array(await subtle.exportKey('raw', pair.publicKey));
  29. const pkcs8 = new Uint8Array(await subtle.exportKey('pkcs8', pair.privateKey));
  30. const rawPrivate = pkcs8.slice(pkcs8.length - 32);
  31. return {
  32. privateKey: bytesToBase64Url(rawPrivate),
  33. publicKey: bytesToBase64Url(rawPublic),
  34. };
  35. }
  36. export function randomShortId(byteLength = 4): string {
  37. const bytes = new Uint8Array(byteLength);
  38. globalThis.crypto.getRandomValues(bytes);
  39. return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
  40. }
  41. export function randomUuid(): string {
  42. return globalThis.crypto.randomUUID();
  43. }
  44. export interface RealityConfig {
  45. address: string;
  46. port: number;
  47. uuid: string;
  48. /** Camouflage target, e.g. `www.microsoft.com:443`. */
  49. dest: string;
  50. /** SNI / serverNames; the first is used in the client link. */
  51. serverNames: string[];
  52. shortIds: string[];
  53. privateKey: string;
  54. publicKey: string;
  55. fingerprint: string;
  56. spiderX: string;
  57. flow: string;
  58. }
  59. /** Server-side VLESS + REALITY inbound (Xray config shape). */
  60. export function realityServerInbound(c: RealityConfig): unknown {
  61. return {
  62. listen: null,
  63. port: c.port,
  64. protocol: 'vless',
  65. settings: {
  66. clients: [{ id: c.uuid, flow: c.flow }],
  67. decryption: 'none',
  68. },
  69. streamSettings: {
  70. network: 'tcp',
  71. security: 'reality',
  72. realitySettings: {
  73. show: false,
  74. dest: c.dest,
  75. xver: 0,
  76. serverNames: c.serverNames,
  77. privateKey: c.privateKey,
  78. shortIds: c.shortIds,
  79. fingerprint: c.fingerprint,
  80. },
  81. },
  82. sniffing: { enabled: true, destOverride: ['http', 'tls', 'quic'] },
  83. };
  84. }
  85. /** Client `vless://` share link carrying the public REALITY parameters. */
  86. export function realityClientLink(c: RealityConfig): string {
  87. return buildVless({
  88. credential: c.uuid,
  89. address: c.address,
  90. port: c.port,
  91. params: {
  92. type: 'tcp',
  93. security: 'reality',
  94. pbk: c.publicKey,
  95. fp: c.fingerprint,
  96. sni: c.serverNames[0] ?? '',
  97. sid: c.shortIds[0] ?? '',
  98. spx: c.spiderX,
  99. flow: c.flow,
  100. },
  101. name: `${c.address}-reality`,
  102. });
  103. }