End-to-End Encryption là gì? Triển Khai E2EE với TypeScript và Web Crypto API
03 Jul, 2025
Hướng nội
AuthorEnd-to-End Encryption là cơ chế bảo mật trong đó chỉ người liên lạc đầu cuối mới có khả năng truy cập và giải mã nội dung dữ liệu được gửi đi

Mục Lục
Bạn đang xây dựng một ứng dụng chat? Một hệ thống chia sẻ tài liệu nội bộ? Hay bất kỳ sản phẩm nào cần xử lý dữ liệu nhạy cảm của người dùng? Nếu câu trả lời là có, thì End-to-End Encryption (E2EE) không phải là một tính năng "nice-to-have", mà là một yêu cầu bắt buộc để bảo vệ người dùng và xây dựng lòng tin.
1. nd-to-End Encryption (E2EE) là gì?
Để triển khai E2EE, chúng ta cần phải hiểu rõ hai cơ chế mã hóa nền tảng:
- Symmetric Encryption (Mã hóa Đối xứng): Đây là phương pháp sử dụng cùng một
secret key
cho cả quá trình mã hóa và giải mã. Cơ chế này có tốc độ xử lý nhanh nhưng tồn tại một điểm yếu cốt lõi: làm sao để chia sẻsecret key
đó cho người nhận một cách an toàn mà không bị lộ trong quá trình truyền?

- Asymmetric Encryption (Mã hóa Bất đối xứng): Cơ chế này giải quyết vấn đề phân phối khóa của mã hóa đối xứng. Mỗi người dùng sở hữu một cặp
key
:- Public Key: Được chia sẻ công khai, dùng để mã hóa dữ liệu hoặc xác thực chữ ký số.
- Private Key: Được giữ bí mật tuyệt đối, dùng để giải mã dữ liệu đã được mã hóa bằng
Public Key
tương ứng, hoặc để tạo chữ ký số.

Một luồng E2EE tiêu chuẩn thường kết hợp cả hai: sử dụng Asymmetric Encryption
để thiết lập một kênh an toàn và trao đổi một shared secret key
. Sau đó, shared secret key
này được dùng để mã hóa toàn bộ dữ liệu về sau bằng Symmetric Encryption
để tận dụng hiệu suất tốc độ.
2. Các rủi ro tiềm ẩn
Mô hình kết hợp trên vẫn tồn tại một lỗ hổng nghiêm trọng. Hãy cùng mình xem xét kịch bản tấn công sau:
- Alice yêu cầu
Public Key
của Bob từ server. - Kẻ tấn công (Eve) chặn yêu cầu này.
- Eve gửi
Public Key
của chính mình cho Alice, nhưng giả mạo làPublic Key
của Bob. - Alice nhận
Public Key
giả mạo và dùng nó để mã hóa mộtsession key
(khóa phiên) rồi gửi đi. - Eve chặn lại, dùng
Private Key
của mình để giải mã và lấy đượcsession key
. - Tiếp theo, Eve dùng
Public Key
thật của Bob để mã hóa lạisession key
này và gửi cho Bob. - Kể từ thời điểm này, Eve có thể giải mã, đọc và sửa đổi toàn bộ tin nhắn giữa Alice và Bob mà cả hai không hề hay biết.
Đây là kịch bản tấn công Man-in-the-Middle (MITM).
3. Trust On First Use (TOFU) & Out-of-Band Authentication
Các ứng dụng E2EE hiện đại không phụ thuộc vào một Certificate Authority (CA) trung gian để giải quyết MITM. Thay vào đó, họ triển khai một cơ chế xác thực do người dùng chủ động thực hiện:
- Key Exchange & Fingerprint Generation: Khi hai người dùng bắt đầu một phiên liên lạc, thiết bị của họ trao đổi
Public Key
và sử dụng một thuật toán (như Diffie-Hellman) để tạo ra mộtshared secret key
. Từ khóa chung này, một "dấu vân tay" định danh cho phiên (thường gọi là Safety Number hoặc Fingerprint) sẽ được tạo ra. - Out-of-Band Authentication: Đây là bước bảo mật quyết định. Người dùng được cung cấp công cụ để tự xác thực rằng
Safety Number
trên thiết bị của họ và của người nhận là trùng khớp. Việc xác thực này phải được thực hiện qua một kênh liên lạc đáng tin cậy khác (gọi là out-of-band), ví dụ như gặp mặt trực tiếp, gọi điện, hoặc video call để so sánh mã. - Trust On First Use (TOFU): Sau khi xác thực thành công lần đầu, thiết bị sẽ lưu lại
Public Key
của đối phương và "tin tưởng" nó. Nếu trong tương laiPublic Key
này bị thay đổi, ứng dụng sẽ ngay lập tức đưa ra cảnh báo về một rủi ro an ninh tiềm tàng, có thể là một cuộc tấn công MITM.
Cơ chế này trao quyền xác thực cho người dùng, phù hợp với triết lý phi tập trung của E2EE.
4. Hướng Dẫn Triển Khai với TypeScript & Web Crypto API
Ở phần này, chúng ta sẽ mô phỏng quy trình E2EE giữa hai người dùng – Alice và Bob – bằng TypeScript. Công cụ sử dụng là Web Crypto API, một API chuẩn được tích hợp sẵn trong trình duyệt hiện đại và Node.js (từ phiên bản 15.7 trở lên).
Mục tiêu: Mô phỏng việc Alice gửi một tin nhắn đã được mã hóa đầu-cuối (E2EE) tới Bob.
// crypto-e2ee-demo.ts
// Trong môi trường Node.js, cần import webcrypto
// import { webcrypto } from 'node:crypto';
// const crypto = webcrypto;
// --- STEP 1: Định nghĩa lớp User ---
class User {
name: string;
private privateKey!: CryptoKey;
publicKey!: CryptoKey;
constructor(name: string) {
this.name = name;
}
// Tạo cặp khóa ECDH (Elliptic-curve Diffie-Hellman)
async generateKeys(): Promise<void> {
const keyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true, // Key có thể được export
["deriveKey"] // Mục đích của key là để tạo ra key khác
);
this.privateKey = keyPair.privateKey;
this.publicKey = keyPair.publicKey;
console.log(`[${this.name}] Đã tạo cặp key.`);
}
// Mã hóa tin nhắn cho người nhận
async encryptMessage(recipientPublicKey: CryptoKey, message: string): Promise<{ iv: Uint8Array, encryptedData: ArrayBuffer }> {
// Tạo shared secret từ private key của mình và public key của người nhận
const sharedSecret = await this.deriveSharedSecret(recipientPublicKey);
// Sử dụng shared secret làm khóa cho mã hóa đối xứng AES-GCM
const iv = crypto.getRandomValues(new Uint8Array(12)); // Initialization Vector
const encoder = new TextEncoder();
const encodedMessage = encoder.encode(message);
const encryptedData = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
sharedSecret,
encodedMessage
);
console.log(`[${this.name}] Đã mã hóa tin nhắn.`);
return { iv, encryptedData };
}
// Giải mã tin nhắn nhận được
async decryptMessage(senderPublicKey: CryptoKey, payload: { iv: Uint8Array, encryptedData: ArrayBuffer }): Promise<string> {
const sharedSecret = await this.deriveSharedSecret(senderPublicKey);
const decryptedData = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: payload.iv },
sharedSecret,
payload.encryptedData
);
console.log(`[${this.name}] Đã giải mã tin nhắn.`);
const decoder = new TextDecoder();
return decoder.decode(decryptedData);
}
// Hàm tiện ích tạo shared secret key
private async deriveSharedSecret(recipientPublicKey: CryptoKey): Promise<CryptoKey> {
// Thuật toán ECDH được dùng để tạo ra một shared secret key.
// Key này sau đó được dùng cho thuật toán AES-GCM.
return await crypto.subtle.deriveKey(
{ name: "ECDH", public: recipientPublicKey },
this.privateKey,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
}
}
// --- STEP 2: Mô phỏng kịch bản ---
async function main() {
console.log("--- Bắt đầu kịch bản E2EE ---");
// 1. Khởi tạo hai đối tượng User
const alice = new User("Alice");
const bob = new User("Bob");
// 2. Mỗi user tự tạo key pair
await alice.generateKeys();
await bob.generateKeys();
// 3. Trao đổi Public Key (giả định quá trình này diễn ra qua server)
// Đây là bước có thể bị tấn công MITM nếu thiếu cơ chế xác thực.
const aliceKnowsBobsPublicKey = bob.publicKey;
const bobKnowsAlicesPublicKey = alice.publicKey;
console.log("
[Hệ thống] Alice và Bob đã trao đổi Public Key.");
// 4. Alice chuẩn bị gửi tin nhắn
const secretMessage = "E2EE technical implementation.";
console.log(`
[Alice] Chuẩn bị gửi: "${secretMessage}"`);
// 5. Alice mã hóa tin nhắn với Public Key của Bob
const encryptedPayload = await alice.encryptMessage(aliceKnowsBobsPublicKey, secretMessage);
console.log("[Hệ thống] Alice gửi payload đã được mã hóa cho Bob.");
// Trong ứng dụng thực tế, payload này sẽ được gửi qua server.
// 6. Bob nhận payload và giải mã với Public Key của Alice
const decryptedMessage = await bob.decryptMessage(bobKnowsAlicesPublicKey, encryptedPayload);
// 7. Xác thực kết quả
console.log(`
[Bob] Đã nhận và giải mã tin nhắn: "${decryptedMessage}"`);
console.log("
--- Kết thúc kịch bản ---");
if (secretMessage === decryptedMessage) {
console.log("✅ Thành công: Tin nhắn gốc và tin nhắn giải mã trùng khớp.");
} else {
console.log("❌ Thất bại: Có lỗi xảy ra trong quá trình E2EE.");
}
}
main();
Đoạn code trên chỉ là một minh họa tối giản về mặt khái niệm. Một hệ thống E2EE thực tế sẽ yêu cầu xử lý các vấn đề phức tạp hơn:
- Quản lý và lưu trữ Key:
PrivateKey
phải được lưu trữ an toàn trên thiết bị của người dùng, không bao giờ được rời khỏi thiết bị. - Perfect Forward Secrecy (PFS): Sử dụng các
ephemeral key
(khóa dùng một lần) cho mỗi phiên giao tiếp. Điều này đảm bảo nếu mộtprivate key
dài hạn bị lộ, các tin nhắn trong quá khứ vẫn được bảo vệ. - Mã hóa cho Group Chat: Việc triển khai E2EE cho nhóm nhiều người dùng phức tạp hơn đáng kể so với giao tiếp 1-1.
- Xây dựng giao diện xác thực: Cần có UI/UX để người dùng có thể dễ dàng so sánh
Safety Number
.
5. Kết luận
Việc triển khai E2EE không chỉ là một bài toán về mã hóa. Nó đòi hỏi một protocol
quản lý key
chặt chẽ, một kiến trúc hệ thống an toàn, và một thiết kế sản phẩm thông minh để người dùng có thể tham gia vào quá trình xác thực.
Để xây dựng ứng dụng thực tế, các bạn nên nghiên cứu và sử dụng các thư viện đã được kiểm chứng nhưlibsodium-wrappers
hoặc các thư viện triển khai sẵn Signal Protocol
. Chúc các bạn thành công