19 KiB
🔐 Luồng Hoạt Động - Hệ Thống Lưu Mật Khẩu AES-256
Version: 2.0 (AES-256 Encryption)
File: src/utils/rememberMe.ts
Loại Bảo Mật: AES-256 (Advanced Encryption Standard)
📋 Mục Lục
- Tổng Quan
- Kiến Thức Nền Tảng
- Luồng Lưu Thông Tin
- Luồng Tải Thông Tin
- Chi Tiết Mã Hóa AES-256
- Ví Dụ Cụ Thể
- Xử Lý Lỗi
- So Sánh Bảo Mật
🎯 Tổng Quan
Hệ thống lưu mật khẩu "Remember Me" sử dụng AES-256 encryption để bảo vệ thông tin đăng nhập người dùng khi lưu vào localStorage.
Các Bước Chính:
┌─────────────────────────────────────────────────────────┐
│ 1. Nhập Email + Password từ form đăng nhập │
├─────────────────────────────────────────────────────────┤
│ 2. Tạo Object: { email, password } │
├─────────────────────────────────────────────────────────┤
│ 3. Chuyển thành JSON string │
├─────────────────────────────────────────────────────────┤
│ 4. Mã hóa AES-256 (với SECRET_KEY) │
├─────────────────────────────────────────────────────────┤
│ 5. Lưu ciphertext vào localStorage │
├─────────────────────────────────────────────────────────┤
│ 6. Khi cần: Giải mã và lấy lại thông tin │
└─────────────────────────────────────────────────────────┘
🏫 Kiến Thức Nền Tảng
AES-256 là gì?
AES = Advanced Encryption Standard (chuẩn mã hóa quốc tế)
256 = Khóa bí mật dài 256 bit (32 bytes)
Tại sao dùng AES-256?
| Tiêu Chí | Chi Tiết |
|---|---|
| Độ An Toàn | ✅ Hầu như không thể bẻ khóa (2^256 khả năng) |
| Tốc Độ | ✅ Nhanh (xử lý được tỷ tấn dữ liệu/giây) |
| Tiêu Chuẩn | ✅ Được chính phủ Mỹ & thế giới sử dụng |
| IV (Initialization Vector) | ✅ Tự động tạo ngẫu nhiên mỗi lần |
| Pattern Hiding | ✅ Cùng plaintext → khác ciphertext |
IV (Initialization Vector) là gì?
-
Định nghĩa: Một chuỗi bit ngẫu nhiên 128-bit (16 bytes)
-
Chức năng: Đảm bảo cùng plaintext lại được mã hóa khác
-
Ví dụ:
Plaintext: "password" Lần 1: IV = random_1 → Ciphertext = "aB3dE7..." Lần 2: IV = random_2 → Ciphertext = "xY9pQ2..." (khác!) Lần 3: IV = random_3 → Ciphertext = "kL5mR8..." (khác!) Cả 3 lần đếu giải mã lại thành "password"
💾 Luồng Lưu Thông Tin
Hàm: saveCredentials(email, password)
Đầu Vào:
email = "user@gmail.com"
password = "MyPassword123"
↓
┌────────────────────────────────┐
│ Bước 1: Tạo Object Credentials │
└────────────────────────────────┘
credentials = {
email: "user@gmail.com",
password: "MyPassword123"
}
↓
┌─────────────────────────────────┐
│ Bước 2: Chuyển Object → JSON │
└─────────────────────────────────┘
json = '{"email":"user@gmail.com","password":"MyPassword123"}'
↓
↓
┌───────────────────────────────────────────┐
│ Bước 3: Mã Hóa (AES-256) │
│ │
│ Hàm: encrypt(json) │
│ CryptoJS.AES.encrypt(json, SECRET_KEY) │
└───────────────────────────────────────────┘
↓
↓
encrypted = "U2FsdGVkX1+ZzO2jNxNKbvH..."
(output = Salt + IV + Ciphertext, tất cả dạng Base64)
↓
┌─────────────────────────────────────────┐
│ Bước 4: Lưu vào localStorage │
│ localStorage.setItem(REMEMBER_ME_KEY, │
│ encrypted) │
└─────────────────────────────────────────┘
↓
localStorage['smatec_remember_login'] = "U2FsdGVkX1+ZzO2jNxNKbvH..."
Đầu Ra: ✅ Mật khẩu được mã hóa & lưu an toàn
Code Chi Tiết:
export function saveCredentials(email: string, password: string): void {
try {
// Bước 1: Tạo object
const credentials: RememberedCredentials = { email, password };
// Bước 2: JSON stringify
const json = JSON.stringify(credentials);
// Kết quả: '{"email":"user@gmail.com","password":"MyPassword123"}'
// Bước 3: Mã hóa AES-256
const encrypted = encrypt(json);
// Kết quả: "U2FsdGVkX1+ZzO2jNxNKbvH..."
// Bước 4: Lưu vào localStorage
localStorage.setItem(REMEMBER_ME_KEY, encrypted);
// Lưu thành công ✅
} catch (error) {
console.error('Error saving credentials:', error);
}
}
📂 Luồng Tải Thông Tin
Hàm: loadCredentials()
Đầu Vào: (Không có tham số - tự động từ localStorage)
↓
┌────────────────────────────────────────┐
│ Bước 1: Lấy Dữ Liệu Từ localStorage │
└────────────────────────────────────────┘
encrypted = localStorage.getItem(REMEMBER_ME_KEY)
// Kết quả: "U2FsdGVkX1+ZzO2jNxNKbvH..." hoặc null
↓
┌────────────────────────────────┐
│ Bước 2: Kiểm Tra Null │
└────────────────────────────────┘
if (!encrypted) {
return null ❌ (Không có dữ liệu lưu)
}
↓
┌───────────────────────────────────────────┐
│ Bước 3: Giải Mã (AES-256) │
│ │
│ Hàm: decrypt(encrypted) │
│ CryptoJS.AES.decrypt(encrypted, │
│ SECRET_KEY) │
└───────────────────────────────────────────┘
↓
↓
decrypted_text = '{"email":"user@gmail.com","password":"MyPassword123"}'
↓
┌──────────────────────────────────┐
│ Bước 4: Kiểm Tra Decode Hợp Lệ │
└──────────────────────────────────┘
if (!decrypted_text) {
clearCredentials() // Xóa dữ liệu lỗi
return null ❌
}
↓
┌──────────────────────────────┐
│ Bước 5: Parse JSON │
└──────────────────────────────┘
credentials = JSON.parse(decrypted_text)
// Kết quả:
// {
// email: "user@gmail.com",
// password: "MyPassword123"
// }
↓
return credentials ✅
Đầu Ra: RememberedCredentials object hoặc null
Code Chi Tiết:
export function loadCredentials(): RememberedCredentials | null {
try {
// Bước 1: Lấy từ localStorage
const encrypted = localStorage.getItem(REMEMBER_ME_KEY);
// Bước 2: Kiểm tra null
if (!encrypted) {
return null;
}
// Bước 3: Giải mã AES-256
const decrypted = decrypt(encrypted);
// Kết quả: '{"email":"user@gmail.com","password":"MyPassword123"}'
// Bước 4: Kiểm tra giải mã hợp lệ
if (!decrypted) {
clearCredentials();
return null;
}
// Bước 5: Parse JSON
const credentials: RememberedCredentials = JSON.parse(decrypted);
// Trả về object credentials ✅
return credentials;
} catch (error) {
console.error('Error loading credentials:', error);
clearCredentials();
return null;
}
}
🔐 Chi Tiết Mã Hóa AES-256
Hàm Encrypt
function encrypt(data: string): string {
try {
// CryptoJS.AES.encrypt() tự động:
// 1. Tạo salt ngẫu nhiên (8 bytes)
// 2. Từ salt + SECRET_KEY → tạo key & IV (PBKDF2)
// 3. Mã hóa data bằng AES-256-CBC
// 4. Trả về: Salt + IV + Ciphertext (tất cả Base64)
const encrypted = CryptoJS.AES.encrypt(data, SECRET_KEY).toString();
return encrypted;
} catch (error) {
console.error('Encryption error:', error);
return '';
}
}
Cấu Trúc Output:
Ciphertext cuối cùng = "U2FsdGVkX1+ZzO2jNxNKbvH..."
↓
Được chia thành 3 phần:
│ Part 1 │ Part 2 │ Part 3 │
│ Magic String │ Salt (8) │ IV + Ciphertext │
│ "Salted__" │ random │ │
└──────────────┴───────────┴─────────────────┘
↓
Tất cả encode Base64
Hàm Decrypt
function decrypt(encrypted: string): string {
try {
// CryptoJS.AES.decrypt() tự động:
// 1. Decode Base64 ciphertext
// 2. Trích xuất Salt từ đầu
// 3. Từ salt + SECRET_KEY → tạo key & IV (PBKDF2)
// 4. Giải mã dữ liệu bằng AES-256-CBC
// 5. Trả về plaintext
const decrypted = CryptoJS.AES.decrypt(encrypted, SECRET_KEY);
// Chuyển từ WordArray sang UTF8 string
const result = decrypted.toString(CryptoJS.enc.Utf8);
return result;
} catch (error) {
console.error('Decryption error:', error);
return '';
}
}
Biểu Đồ Quy Trình AES-256
ENCRYPTION SIDE (Lưu Mật Khẩu)
═══════════════════════════════════════════════════════
Plaintext: "password"
│
├─→ [PBKDF2 + Salt] ──→ Key (256-bit) + IV (128-bit)
│ │
│ ↓
└──────────────────→ [AES-256-CBC Engine]
│
↓
Ciphertext
│
↓
[Base64 Encode]
│
↓
"U2FsdGVkX1+ZzO2jNxNKbvH..."
DECRYPTION SIDE (Tải Mật Khẩu)
═══════════════════════════════════════════════════════
Ciphertext: "U2FsdGVkX1+ZzO2jNxNKbvH..."
│
├─→ [Base64 Decode]
│
├─→ Extract Salt (8 bytes)
│ │
│ └──→ [PBKDF2] ──→ Key (256-bit) + IV (128-bit)
│
├─→ Extract Ciphertext
│ │
│ └──→ [AES-256-CBC Engine]
│ │
│ ↓
└──────────→ Plaintext: "password"
💡 Ví Dụ Cụ Thể
Ví Dụ 1: Lưu Email & Mật Khẩu
Input:
saveCredentials('john@example.com', 'SecurePass2024!');
Bước Bước:
| Bước | Dữ Liệu | Mô Tả |
|---|---|---|
| 1 | { email: "john@example.com", password: "SecurePass2024!" } |
Object credentials |
| 2 | '{"email":"john@example.com","password":"SecurePass2024!"}' |
JSON stringify |
| 3 | U2FsdGVkX1...abc123... |
AES-256 encrypt + Base64 |
| 4 | localStorage | Lưu thành công ✅ |
Trong localStorage:
localStorage = {
smatec_remember_login: 'U2FsdGVkX1+ZzO2jNxNKbvHJmVaFptBWc0p...',
};
Ví Dụ 2: Tải Email & Mật Khẩu
Input:
loadCredentials();
Bước Bước:
| Bước | Dữ Liệu | Mô Tả |
|---|---|---|
| 1 | "U2FsdGVkX1+ZzO2jNxNKbvHJmVaFptBWc0p..." |
Lấy từ localStorage |
| 2 | ✅ Tồn tại | Kiểm tra có dữ liệu |
| 3 | '{"email":"john@example.com","password":"SecurePass2024!"}' |
AES-256 decrypt |
| 4 | ✅ Valid JSON | Kiểm tra hợp lệ |
| 5 | { email: "john@example.com", password: "SecurePass2024!" } |
JSON parse |
| 6 | Trả về object | ✅ |
Output:
{
email: "john@example.com",
password: "SecurePass2024!"
}
⚠️ Xử Lý Lỗi
Trường Hợp 1: Không Có Dữ Liệu Lưu
loadCredentials()
↓
Kiểm tra localStorage
↓
encrypted === null
↓
return null ← Không có gì để tải
Trường Hợp 2: Dữ Liệu Bị Hỏng
loadCredentials()
↓
Lấy được dữ liệu
↓
Giải mã thất bại (decrypt() trả về '')
↓
clearCredentials() ← Xóa dữ liệu lỗi
↓
return null ← Không thể tải
Trường Hợp 3: JSON Parse Lỗi
loadCredentials()
↓
Giải mã thành công
↓
JSON.parse() thất bại (dữ liệu không phải JSON)
↓
catch block → clearCredentials()
↓
return null ← Lỗi JSON
Trường Hợp 4: LocalStorage Không Khả Dụng
Các hàm try-catch sẽ bắt lỗi
↓
Trả về false / null / log error
↓
Ứng dụng vẫn hoạt động bình thường (graceful degradation)
📊 So Sánh Bảo Mật
XOR Cipher (Cũ) vs AES-256 (Mới)
╔════════════════════════════════════════════════════════════════╗
║ XOR CIPHER (CŨ) │ AES-256 (MỚI) ║
╠════════════════════════════════════════════════════════════════╣
║ Mã Hóa Kiểu │ XOR từng ký tự │ Mã hóa khối ║
║ Độ An Toàn │ ⚠️ Rất Thấp │ ✅ Cực Cao ║
║ Key Length │ Xoay vòng bằng khóa │ 256-bit (32 bytes) ║
║ IV │ ❌ Không có │ ✅ Ngẫu nhiên ║
║ Brute Force │ ⚠️ Dễ (2^key_length) │ ✅ Không Thể ║
║ Pattern │ ⚠️ Có pattern │ ✅ Ẩn pattern ║
║ │ (cùng text → lại) │ (random IV) ║
║ Block Mode │ N/A │ ✅ CBC Mode ║
║ Salt │ ❌ Không │ ✅ Có PBKDF2 ║
║ Performance │ ✅ Cực Nhanh │ ✅ Nhanh (HW opt) ║
║ Standard │ ❌ Custom/Weak │ ✅ NIST/Military ║
╚════════════════════════════════════════════════════════════════╝
Ví Dụ Tấn Công:
XOR Cipher:
Nếu biết:
- Ciphertext = "abc123"
- Plaintext đối với ciphertext khác = "password"
Có thể dễ dàng suy ra SECRET_KEY bằng XOR:
plaintext XOR ciphertext = key
AES-256:
Ngay cả biết:
- Ciphertext = "U2FsdGVkX1+ZzO2jNxNKbvH..."
- Plaintext = "password"
- IV được sử dụng
Vẫn không thể suy ra SECRET_KEY (cần brute force 2^256 khả năng)
≈ 10^77 năm với máy tính hiện đại
🔧 Các Hàm Hỗ Trợ
1. clearCredentials()
export function clearCredentials(): void {
localStorage.removeItem(REMEMBER_ME_KEY);
// Xóa hoàn toàn dữ liệu khỏi localStorage
// Dùng khi: Người dùng logout hoặc "Forget Me"
}
2. hasSavedCredentials()
export function hasSavedCredentials(): boolean {
try {
const encrypted = localStorage.getItem(REMEMBER_ME_KEY);
return encrypted !== null;
// Kiểm tra xem có dữ liệu lưu hay không
// Dùng khi: Hiển thị checkbox "Remember Me"
} catch (error) {
return false;
}
}
🎯 Luồng Sử Dụng Trong Ứng Dụng
Sơ Đồ Hoàn Chỉnh:
LOGIN PAGE
│
├─→ User nhập email + password
│
├─→ Tick checkbox "Remember Me"?
│ │
│ ├─→ YES: saveCredentials(email, pwd)
│ │ └─→ Mã hóa + Lưu localStorage ✅
│ │
│ └─→ NO: Không lưu gì
│
├─→ Gửi login request lên server
│
└─→ Server xác thực OK ✅
│
└─→ Redirect tới Dashboard
SUBSEQUENT LOGIN (Lần Đăng Nhập Tiếp Theo)
│
├─→ Check: hasSavedCredentials()?
│ │
│ ├─→ YES: loadCredentials()
│ │ └─→ Giải mã + Parse ✅
│ │ └─→ Auto-fill form
│ │
│ └─→ NO: Form trống (User nhập thủ công)
│
└─→ User xác nhận + submit
│
└─→ Login
📝 Tóm Tắt
| Chức Năng | Hàm | Mô Tả |
|---|---|---|
| Lưu | saveCredentials(email, pwd) |
Mã hóa AES-256 + lưu localStorage |
| Tải | loadCredentials() |
Lấy từ localStorage + giải mã |
| Xóa | clearCredentials() |
Xóa hoàn toàn từ localStorage |
| Kiểm Tra | hasSavedCredentials() |
Kiểm tra có dữ liệu hay không |
🛡️ Kết Luận
✅ AES-256 được sử dụng để mã hóa thông tin sensitive
✅ Random IV đảm bảo mã hóa khác lần nữa
✅ PBKDF2 làm việc với SECRET_KEY để tạo key mạnh
✅ Try-Catch xử lý tất cả lỗi gracefully
✅ localStorage là vị trí lưu phù hợp (with encrypted data)
Bảo mật hiện tại: ⭐⭐⭐⭐⭐ (Military Grade)