Mã hóa và giải mã trong Node.js
Mã hóa dữ liệu là một phần cực kì quan trọng trong các Web App thời nay, chắc tôi không cần nói nhiều về sự quan trọng của tính bảo mật trong các hệ thống nữa rồi. Cứ lâu lâu lại nổ ra một vụ hack vài trăm, vài triệu đô của các dự án Crypto cũng đủ cho bạn thấy nếu bạn làm phần bảo mật không tốt thì hậu quả cho dự án của bạn sẽ to lớn thế nào đúng không. Việc mã hóa dữ liệu để đảm bảo tính bảo mật và tính toàn vẹn của thông tin nhạy cảm được truyền và lưu trữ vào cơ sở dữ liệu thực sự là một thách thức không hề dễ dàng. Node.js – một runtime enviroment (môi trường thực thi) nổi tiếng của Javascript cung cấp một số modul của bên thứ ba để tích hợp sẵn để mã hóa và giải mã dữ liệu một cách an toàn.
Trong bài viết này chúng ta sẽ khám phá các kỹ thuật mã hóa khác nhau, từ cơ bản tới nâng cao và thảo luận về cách chúng triển khai như thế nào với Node.js nhé
Mã hóa cơ bản
Node.js đi kèm với một modul mật mã tích hợp cung cấp chức năng mã hóa, bao gồm các thuật toán băm, mã hóa và giải mã. Modul này hỗ trợ nhiều thuật toán mã hóa khác nhau như AES (Tiêu chuẩn của mã hóa nâng cao), DES (Tiêu chuẩn mã hóa dữ liệu) v.v…
Mã hóa khóa đối xứng
Mã hóa khóa đối xứng còn được gọi là mã hóa khóa bí mật, sử dụng một khóa duy nhất cho cả quá trình mã hóa và giải mã. Các bên tham gia giao tiếp với nhau phải được chia sẻ cùng một mã hóa giống hệt nhau.
Dưới đây là một ví dụ về các sử dụng module crypto
và giải mã bằng khóa đối xứng với thuật toán AES:
const crypto = require('crypto');
// Tạo mã khóa 32 byte ngẫu nhiên
const encryptionKey = crypto.randomBytes(32);
// Hàm mã hóa
function encryptData(plaintext) {
const iv = crypto.randomBytes(16); // Generate a new Initialization Vector (IV) for each encryption
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}`;
}
// Hàm giải mã
function decryptData(ciphertext) {
const [ivHex, encrypted] = ciphertext.split(':');
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', encryptionKey, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Gọi hàm + ví dụ
const plaintext = 'This is a secret message';
const ciphertext = encryptData(plaintext);
console.log('Encrypted data:', ciphertext);
const decryptedData = decryptData(ciphertext);
console.log('Decrypted data:', decryptedData);
Ở ví dụ này, chúng tôi xác định một mã khóa và 2 hàm: Mã hóa dữ liệu (encryptData) và Giải mã dữ liệu (decryptData).
Hàm mã hóa lấy chuỗi văn bản gốc làm đầu vào, tạo một Vector khởi tạo ngẫu nhiên (IV), tạo đối tượng mật mã bằng thuật toán AES-256-CBC và mã hóa văn bản gốc. Dữ liệu được mã hóa cùng với IV được trả về dưới dạng chuỗi ở định dạng iv:encryptedData.
Hàm decryptData lấy chuỗi dữ liệu được mã hóa làm đầu vào, tách IV và dữ liệu được mã hóa, tạo đối tượng giải mã bằng cách sử dụng cùng khóa và thuật toán mã hóa, đồng thời giả mã dữ liệu.
Lưu ý rằng mã hóa khóa đối xứng yêu cầu việc chia sẻ và lưu trữ khóa mã hóa một cách an toàn giữa các bên liên quan, Nếu khóa bị xâm phạm, toàn bộ hệ thống mã hóa sẽ dễ bị tấn công.
Mã hóa khóa bất đối xứng
Mã hóa khóa bất đối xứng hay còn gọi là mã hóa khóa chung, sử dụng hai khóa khác nhau:
Khóa chung để mã hóa và khóa riêng để giải mã. Khóa chung có thể được chia sẻ tự do trong khi khóa riêng phải giữ bí mật.
Module crypto
hỗ trợ mã hóa khóa không đối xứng bằng các thuật toán như RSA. Dưới đây là một ví dụ minh họa cho việc sử dụng module crypto
:
const crypto = require('crypto');
// Tạo key
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048, // Key size in bits
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
// Hàm mã hóa
function encryptData(plaintext) {
const buffer = Buffer.from(plaintext, 'utf8');
const encrypted = crypto.publicEncrypt(publicKey, buffer);
return encrypted.toString('hex');
}
/// Hàm giải mã
function decryptData(ciphertext) {
const buffer = Buffer.from(ciphertext, 'hex');
const decrypted = crypto.privateDecrypt(privateKey, buffer);
return decrypted.toString('utf8');
}
// Gọi hàm + ví dụ
const plaintext = 'This is a secret message';
const ciphertext = encryptData(plaintext);
console.log('Encrypted data:', ciphertext);
const decryptedData = decryptData(ciphertext);
console.log('Decrypted data:', decryptedData);
Trong ví dụ này, chúng tôi tạo ra một cặp khóa bằng thuật toán RSA bằng phương thức của crypto.generateKeyPairSync
. Hàm encryptData
sử dụng chuỗi đầu vào làm đầu vào và biến đổi nó thành dạng bộ đệm (buffer), và mã hóa nó bằng cách sử dụng khóa công cộng với phương thức crypto.publicEncrypt. Dữ liệu đã được mã hóa sẽ được trả về dưới dạng số hex.
Hàm decryptData lấy dữ liệu đã được mã hóa làm đầu vào, sau đó chuyển đổi nó từ dạng chuỗi Hex về dạng bộ đệm. Sự giải mã này sử dụng khóa bí mật cùng với phương thức crypto.privateDecryp. Dữ liệu đã được giải mã sẽ được trả về ở dạng chuỗi.
Mã hóa bất đối xứng được cho là an toàn hơn mã hóa đối xứng bởi vì chúng chia ra khóa dành cho việc mã hóa và khóa dành cho việc giải mã, khiến cho kẻ tấn công khó khăn hơn trong việc xử lí cả 2 loại khóa này. Tuy vậy, mã hóa khóa không đối xứng yêu cầu tính toán nhiều hơn và thường chậm hơn so với mã hóa khóa đối xứng với đối tượng đầu vào là dữ liệu lớn.
Kĩ thuật nâng cao: Sử dụng thư viện của bên thứ ba
Mặc dù module crypto
đã tích hợp sẵn các chức năng mã hóa và giải mã cơ bản, nhưng rất nhiều yêu cầu khó yêu cầu những chức năng bổ sung hoặc sự trợ giúp từ các thuật toán mã hóa phức tạp. Trong những trường hợp này, một thư viện từ bên thứ 3 có thể xử lí được vấn đề này.
Dưới đây là các thư viện mã hóa của Nodejs từ bên thứ 3 thường được sử dụng :
- crypto-js: Thư viện JavaScript cung cấp các thuật toán mã hóa và băm thông tin. Nó cho phép thực hiện mã hóa đối xứng và không đối xứng, băm thông tin để tạo checksum, và hỗ trợ nhiều chế độ mã hóa khác nhau.
- node-forge: Một phiên bản JavaScript thuần túy của các tiêu chuẩn mật mã hóa, cho phép mã hóa và giải mã dữ liệu, tạo và xác minh chữ ký số. Thư viện này hỗ trợ các thuật toán mã hóa phổ biến như AES, DES, RSA và nhiều thuật toán khác.
- jsonwebtoken: Thư viện cho phép tạo và xác minh JSON Web Tokens (JWT), một phương tiện phổ biến để xác thực và truyền dữ liệu an toàn trong ứng dụng web.
- bcrypt: Thư viện này được sử dụng chủ yếu để băm mật khẩu. Nó sử dụng thuật toán bcrypt, được thiết kế để chậm và chống lại các cuộc tấn công vét cạn bằng cách tăng thời gian tính toán.
- openpgp: Đây là một triển khai của tiêu chuẩn mã hóa OpenPGP, cho phép mã hóa và giải mã dữ liệu, tạo và xác minh chữ ký số, cũng như quản lý khóa mã hóa. Thư viện này thường được sử dụng cho việc giao tiếp an toàn qua email hoặc các kênh truyền thông khác.
Dưới đây là một ví dụ cho việc sử dụng thư viện crypto-js
để mã hóa và giải mã nâng cao nhé:
const CryptoJS = require('crypto-js');
// Khai báo khóa
const encryptionKey = 'ThisIsASecretKey';
// Hàm mã hóa
function encryptData(plaintext) {
const ciphertext = CryptoJS.AES.encrypt(plaintext, encryptionKey).toString();
return ciphertext;
}
// Hàm giải mã
function decryptData(ciphertext) {
const bytes = CryptoJS.AES.decrypt(ciphertext, encryptionKey);
const decryptedData = bytes.toString(CryptoJS.enc.Utf8);
return decryptedData;
}
// Gọi hàm + ví dụ
const plaintext = 'This is a secret message';
const ciphertext = encryptData(plaintext);
console.log('Encrypted data:', ciphertext);
const decryptedData = decryptData(ciphertext);
console.log('Decrypted data:', decryptedData);
Các phương pháp tốt nhất trong Mã hóa
Khi triển khai việc mã hóa trong các ứng dụng Node.js, điều thiết yếu là tuân thủ các phương pháp tốt nhất là rất quan trọng để đảm bảo an toàn và tính toàn vẹn dữ liệu của bạn.
Dưới đây là một số đề xuất của tôi về các phương pháp tốt nhất khu thực hiện mã hóa nhé:
Quản lý khóa
Việc quản lý khóa thích hợp rất quan trọng để duy trì tính bảo mật của hệ thống mã hóa của bạn. Bạn có thể thực hiện theo các gợi ý sau:
Tạo khóa mạnh: Sử dụng trình tạo chuỗi và số ngẫu nhiên để tạo khóa mã hóa mạnh. Tránh sử dụng các khóa yếu hoặc có thể đoán trước được vì chúng có thể làm tổn hại tới toàn bộ hệ thống mã hóa nếu bị đoán ra.
Lưu trữ khóa an toàn: Không bao giờ lưu trữ khóa mã hóa ở dạng văn bản thần túy hoặc nhúng chúng vào phần code của bạn (cái này nhiều người mắc phải). Thay vào đó hãy lưu trữ chúng ở một vị trí an toàn, chẳng hạn như biến môi trường hoặc hệ thống quản lý khóa bảo mật.
Thay đổi khóa thường xuyên: Hãy định kỳ cập nhật và thay đổi khóa mã hóa của bạn để giảm thiếu nguy cơ bị dò ra mã khóa. Thiết lập chính sách luân chuyển khóa dựa trên yêu cầu trên hệ thống của bạn.
Truyền khóa an toàn: Khi truyền mã khóa giữa các bên, hãy sử dụng các kênh bảo mật như HTTPS hoặc các phương thức liên lạc được mã hóa để tăng cường tính bảo mật.
Thuật toán mã hóa an toàn
Chọn các thuật toán mã hóa an toàn và được áp dụng rộng rãi dã được “cộng đồng mã hóa” xem xét và kiểm tra kỹ lưỡng. Tránh sử dụng các thuật toán mã hóa độc quyền hoặc tùy chỉnh vì chúng có thể có những lỗ hổng hoặc điểm yếu chưa được phát hiện.
Md5 đã được confirm là không an toàn, vậy nên bạn có thể sử dụng một số thuật toán mã hóa được đề xuất bao gồm:
AES (Tiêu chuẩn mã hóa nâng cao) để mã hóa đối xứng
RSA (Rivest-Shamir-Adleman) hoặc ECC (Mật mã đường cong Elliptic) để mã hóa bất đối xứng
SHA-256 hoặc SHA-3 để băm và xác thực tin nhắn
Ngoài ra, hãy luôn cập nhật các thông tin và cập nhật bảo mật mới nhất về thuật toán mã hóa cũng như cách triển khai chúng để giải quyết mọi lỗ hổng mới được phát hiện.
Chế độ mã hóa và lược đồ đệm an toàn
Sử dụng các chế độ mã hóa và các lược đồ đệm an toàn để ngăn chặn các loại tấn công khác nhau, như tấn công oracle đệm và tấn công văn bản đã chọn. Một số chế độ mã hóa và các lược đồ đệm được khuyến nghị bao gồm:
- Chế độ CBC (Cipher Block Chaining) với lược đồ đệm PKCS#7 cho mã hóa đối xứng.
- OAEP (Optimal Asymmetric Encryption Padding) cho mã hóa không đối xứng.
Kiểm tra và làm sạch dữ liệu đầu vào
Trước khi mã hóa và giải mã dữ liệu, luôn kiểm tra và làm sạch dữ liệu đầu vào để ngăn chặn các cuộc tấn công injection hoặc các lỗ hổng khác. Đảm bảo rằng dữ liệu đầu vào có đúng định dạng đúng như bạn mong muốn và không chứa bất kì nội dung độc hại nào.
Xử lí lỗi và lưu log lỗi
Hãy luôn triển khai cơ chế xử lí và lưu log lỗi trong các chức năng mã hóa và giải mã của bạn. Tránh rò rỉ thông tin nhạy cảm như mã khóa hoặc dữ liệu văn bản gốc, các thông tin trong thông báo lỗi hoặc nhật kí lỗi. Chỉ ghi lại các thông tin cần thiết cho việc debug và kiểm tra (Đừng in ra bất kì thông tin nào như key, OTP, password… trong phần logs kể cả trong môi trường dev)
Code review và kiểm thử
Thường xuyên xem xét lại các hàm mã hóa của bạn và các chức năng liên quan để phát hiện các lỗ hổng tiềm ẩn và thực hiện kiểm tra kỹ lưỡng, bao gồm cả các trường hợp kiểm tra tiêu cực (negative test cases) và các kịch bản biên (edge scenarios). Cân nhắc mời thêm các chuyên gia mã hóa hoặc các chuyên gia bảo mật có kinh nghiệm để xem xét mã và kiểm tra độ an toàn cho dự án của bạn.
Việc mã hóa trong thực tế
Mã hóa là một khía cạnh quan trọng của nhiều ứng dụng thực tế, từ việc truyền thông an toàn và lưu trữ dữ liệu đến xác thực và chữ ký số. Dưới đây là một số trường hợp sử dụng phổ biến trong các ứng dụng Node.js:
- Giao tiếp An Toàn: Mã hóa những dữ liệu được truyền qua mạng, chẳng hạn như APIs, WebSockets, hoặc các kênh giao tiếp khác, để ngăn chặn việc nghe trộm và tấn công trung gian.
- Lưu Trữ Dữ Liệu An Toàn: Mã hóa dữ liệu nhạy cảm, chẳng hạn như mật khẩu, thông tin cá nhân hoặc dữ liệu tài chính, trước khi lưu trữ vào cơ sở dữ liệu hoặc hệ thống tệp, để bảo vệ khỏi việc truy cập không được ủy quyền hoặc việc vi phạm dữ liệu.
- Xác Thực và Phân Quyền: Sử dụng các kỹ thuật mã hóa, chẳng hạn như JSON Web Tokens (JWT) hoặc các thuật toán băm an toàn như bcrypt, cho các cơ chế xác thực và phân quyền người dùng.
- Chữ Ký Số: Triển khai các chữ ký số bằng cách sử dụng các thuật toán mã hóa không đối xứng như RSA hoặc ECC để đảm bảo tính toàn vẹn và không thể chối bỏ của dữ liệu hoặc tài liệu.
- Truyền Tệp An Toàn: Mã hóa các tệp hoặc dữ liệu trước khi chuyển qua mạng không an toàn hoặc các kênh, chẳng hạn như email hoặc FTP, để duy trì tính bảo mật.
- Mã Hóa Điểm Cuối Điểm Cuối: Triển khai mã hóa điểm cuối điểm cuối trong các ứng dụng nhắn tin hoặc nền tảng truyền thông, nơi dữ liệu được mã hóa trên thiết bị của người gửi và chỉ có thể được giải mã bởi người nhận dự định.
Trong suốt bài viết này, chúng ta đã tìm hiểu về các kỹ thuật mã hóa khác nhau, từ mã hóa đối xứng và không đối xứng cơ bản bằng cách sử dụng module crypto
tích hợp đến các phương pháp mã hóa tiên tiến hơn sử dụng các thư viện bên thứ ba như crypto-js
. Chúng ta cũng đã thảo luận về các phương pháp tốt nhất trong việc mã hóa, như quản lý khóa, lựa chọn thuật toán an toàn, kiểm tra và làm sạch dữ liệu đầu vào, và xử lý lỗi.
Bằng cách triển khai mã hóa trong các ứng dụng Node.js của bạn và tuân thủ các phương pháp tốt nhất ngành công nghiệp, bạn có thể cải thiện đáng kể tính bảo mật và quyền riêng tư của dữ liệu của mình, bảo vệ chúng khỏi việc bị truy cập, nghe trộm hoặc can thiệp không được ủy quyền. Hãy nhớ rằng mã hóa là một phần quan trọng của một chiến lược bảo mật toàn diện, và nó nên được kết hợp với các biện pháp bảo mật khác, chẳng hạn như kiểm soát truy cập, thực hành mã hóa an toàn và kiểm tra bảo mật định kỳ, để cung cấp một hệ thống phòng thủ đa lớp và mạnh mẽ chống lại các mối đe dọa tiềm ẩn.