Skip to content

Chi tiết mã hóa

Signal sử dụng ECDH helper từ IGNIS Framework cho tất cả các thao tác mật mã. Helper này bọc Web Crypto API (crypto.subtle) và cung cấp giao diện cấp cao cho trao đổi khóa và mã hóa có xác thực.

1. Tham số mật mã

Tham sốGiá trị
Đường congECDH P-256 (secp256r1)
Dẫn xuất khóaHKDF-SHA-256
Mã hóaAES-256-GCM
Kích thước IV12 bytes (96 bits), tạo ngẫu nhiên cho mỗi tin nhắn
Độ dài Tag128 bits
HKDF Salt32 bytes — ngẫu nhiên mặc định (xem Hành vi Salt)
HKDF InfoCấu hình qua APP_ENV_WEBSOCKET_ECDH_INFO
Định dạng xuất khóaPublic key dạng raw, mã hóa base64 (65 bytes cho P-256 không nén)

2. IGNIS ECDH API

Class ECDH được khởi tạo trong NxWebSocketComponent với thuật toán và chuỗi info HKDF:

typescript
// packages/signal/src/components/websocket.component.ts
private readonly ecdh = new ECDH({
  algorithm: 'ecdh-p256',
  hkdfInfo: applicationEnvironment.get<string>(EnvironmentKeys.APP_ENV_WEBSOCKET_ECDH_INFO),
});
Phương thứcTham sốTrả vềMô tả
generateKeyPair(){ keyPair, publicKeyB64 }Tạo cặp khóa ECDH P-256 tạm thời. publicKeyB64 là public key dạng raw mã hóa base64.
importPublicKey(opts){ rawKeyB64 }CryptoKeyImport public key base64 của đối tác để sử dụng trong dẫn xuất khóa.
deriveAESKey(opts){ privateKey, peerPublicKey, salt? }{ key: CryptoKey, salt: string }Dẫn xuất khóa AES-256-GCM qua ECDH shared secret + HKDF-SHA-256. Trả về khóa và salt base64. Nếu không cung cấp salt, salt 32 bytes ngẫu nhiên sẽ được tạo.
encrypt(opts){ message, secret, opts? }{ iv, ct }Mã hóa chuỗi plaintext. Trả về IV (12 bytes) và ciphertext dạng base64. Hỗ trợ additionalData cho AEAD.
decrypt(opts){ message: { iv, ct }, secret, opts? }stringGiải mã payload { iv, ct } về plaintext. Hỗ trợ additionalData cho AEAD.

Đặc tính chính:

  • Tất cả phương thức đều async (yêu cầu Web Crypto API)
  • Cặp khóa tạm thời cung cấp forward secrecy — xâm phạm một phiên không ảnh hưởng đến các phiên khác
  • AES-GCM cung cấp mã hóa có xác thực — mọi sự giả mạo được phát hiện khi giải mã
  • Chuỗi hkdfInfo ngăn tái sử dụng khóa xuyên ngữ cảnh giữa các ứng dụng khác nhau
  • Tương thích với Bun, Browser, và Node.js qua crypto.subtle

3. Hành vi HKDF Salt

Trong @venizia/ignis, deriveAESKey() tạo salt 32 bytes ngẫu nhiên khi không cung cấp tham số salt. Salt được trả về như một phần của kết quả ({ key, salt }).

Để server và client dẫn xuất cùng một khóa AES, chúng phải sử dụng cùng HKDF salt. Cách tiếp cận hiện tại:

Trao đổi salt: Server tạo salt ngẫu nhiên trong deriveAESKey(), và IGNIS WebSocketServerHelper bao gồm nó dưới dạng salt trong sự kiện connected cùng với serverPublicKey. Client sử dụng salt do server cung cấp này trong quá trình dẫn xuất khóa HKDF.

Server: deriveAESKey({ privateKey, peerPublicKey }) → { key, salt }
         ↓ salt gửi trong sự kiện connected
Client: HKDF(sharedBits, salt=data.salt, info=ECDH_INFO) → cùng khóa AES

Mã nguồn phía server trong handshake handler:

typescript
// packages/signal/src/components/websocket.component.ts — bindHandshakeHandler()
const { keyPair, publicKeyB64: serverPublicKey } = await this.ecdh.generateKeyPair();
const clientPub = await this.ecdh.importPublicKey({ rawKeyB64: clientPubB64 });
const { key: aesKey, salt } = await this.ecdh.deriveAESKey({
  privateKey: keyPair.privateKey,
  peerPublicKey: clientPub,
});

this._clientAesKeys.set(clientId, aesKey);
return { serverPublicKey, salt };

IMPORTANT

Giá trị APP_ENV_WEBSOCKET_ECDH_INFO trên server và chuỗi info HKDF trên client phải khớp chính xác. Giá trị không khớp sẽ tạo ra khóa AES khác nhau và giải mã sẽ thất bại.

4. Định dạng tin nhắn đã mã hóa

Tất cả tin nhắn ứng dụng sau handshake được bọc bởi outbound transformer:

typescript
// packages/signal/src/components/websocket.component.ts — bindOutboundTransformer()
const plaintext = JSON.stringify({ event, data });
const encrypted = await this.ecdh.encrypt({ message: plaintext, secret: aesKey });
return { event: WebSocketEvents.ENCRYPTED, data: encrypted };

Định dạng trên đường truyền:

json
{
  "event": "encrypted",
  "data": {
    "iv": "<IV 12 bytes mã hóa base64>",
    "ct": "<AES-GCM ciphertext mã hóa base64>"
  }
}

Plaintext bên trong ct (sau khi giải mã) là chuỗi JSON:

json
{
  "event": "<tên-sự-kiện-thực>",
  "data": { ... }
}

5. Sự kiện Plaintext (Không mã hóa)

Outbound transformer bỏ qua mã hóa rõ ràng cho sự kiện connectederror:

typescript
// packages/signal/src/components/websocket.component.ts — bindOutboundTransformer()
if (event === WebSocketEvents.CONNECTED || event === WebSocketEvents.ERROR) {
  return null;  // null = gửi nguyên trạng (không mã hóa)
}

Các sự kiện control-plane luôn được gửi dạng plaintext:

Sự kiệnHướngMục đích
authenticateClient → ServerJWT token + ECDH public key
connectedServer → ClientXác nhận xác thực + serverPublicKey + salt
errorServer → ClientThông báo lỗi
heartbeatClient → ServerPing giữ kết nối
joinClient → ServerTham gia phòng
leaveClient → ServerRời phòng

6. Giải mã tin nhắn đến

Message handler xác thực cấu trúc payload đã mã hóa trước khi giải mã:

typescript
// packages/signal/src/components/websocket.component.ts — bindMessageHandler()
const encryptedPayload = message.data as IECDHEncryptedPayload | undefined;
if (!encryptedPayload?.iv || !encryptedPayload?.ct) {
  logger.warn('Malformed encrypted message | id: %s', clientId);
  return;
}

const plaintext = await this.ecdh.decrypt({ message: encryptedPayload, secret: aesKey });
const decryptedMessage = JSON.parse(plaintext) as IWebSocketMessage;

NOTE

Message handler hiện tại chỉ ghi log tin nhắn đã giải mã. Định tuyến tin nhắn ở cấp ứng dụng (ví dụ: chuyển tiếp đến dịch vụ khác) chưa được triển khai trong message handler. Luồng tin nhắn chính là server → client (qua REST API hoặc emitter liên dịch vụ), không phải client → server.

7. Vòng đời khóa

Giai đoạnHành động
Client kết nốiClient tạo cặp khóa ECDH P-256, gửi clientPublicKey trong authenticate
Server handshakeServer tạo cặp khóa ECDH tạm thời, dẫn xuất khóa AES, lưu vào _clientAesKeys map
Phiên hoạt độngTất cả tin nhắn gửi đi được mã hóa qua outbound transformer, tất cả tin nhắn đến được giải mã qua message handler
Client ngắt kết nốiclientDisconnectedFn xóa khóa AES khỏi _clientAesKeys map
Kết nối lạiClient tạo cặp khóa ECDH mới (forward secrecy), server dẫn xuất khóa AES mới với salt ngẫu nhiên mới

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.