Ở bài viết này mình sẽ trình bày về cơ chế lưu password vào trong database để đảm bảo tính an toàn của password, tìm hiểu qua một số cách tấn công password, và cuối cùng là tìm hiểu qua một vài thư viện để mã hóa password.

1. Kiến thức cơ sở

Bài viết sử dụng các kiến thức cơ bản về mã hóa, one-way function, two-way funtion:

+ One-way function: nhận input là plain text và output là message được mã hóa. Rất khó để có thể giải mã message này và thu được input ban đầu. Cách duy nhất là thử cho tất cả các trường hợp tới khi tìm ra được input mà cho ra kết quả giống với message.

+ Two-way function: nhận 2 input là plain text và secret key, cho ra output là message được mã hóa. Khác với one-way function, sử dụng giá trị của secret key cho phép ta giải mã được input đầu vào.

+ Hashing: là one-way function với output message có chiều dài cố định(fixed length).

Mỗi một loại hashing lại có thuật toán mã hóa của riêng nó, độ mạnh của thuật toán tương đương với độ khó của việc giải mã.

Nhìn chung. một thuật toán mã hóa phải đảm bảo 2 yêu cầu sau:

  • Rất khó để tồn tại 2 input khác nhau để cho cùng một output (tính đúng đắn)
  • Rất khó để giải mã thuật toán (tính an toàn)

2. Bài toán lưu mật khẩu vào database:

Khi lưu mật khẩu vào database, ta cần đảm bảo rằng mật khẩu của người dùng được an toàn nên sẽ không lưu trực tiếp plain-text mà sẽ lưu mã hóa của mật khẩu.

Vậy câu hỏi đặt ra là sử dụng loại mã hóa nào? Với two-way function, khi hacker có được secrect key sẽ dễ dàng giải mã được input, trong khi đó one-way function đảm bảo input được an toàn kể cả khi hacker có được password đã mã hóa. Việc sử dụng hashing để mã hóa password sẽ giúp đảm bảo tính an toàn cũng như data được lưu trữ trong database có độ lớn cố định.

Cụ thể với việc sử dụng hashing, quá trình lưu password và đăng nhập sẽ diễn ra như sau:

  • User tạo tài khoản với user name u và password p, call api để đăng kí
  • Server sẽ mã hóa password( sử dụng hashing function) và lưu user name u cùng message mã hóa h_p vào database:

h_p = hashing(p)

  • User đăng nhập vào hệ thống với username u và password p'
  • Server kiểm tra user này bằng cách:

Lấy password mã hóa trong database h_p và so sánh nếu:

h_p == hashing(p')

Nếu bằng nhau, dựa trên tính đúng đắn của thuật toán hashing (xem phần 1), ta có thể khẳng định rằng p'p là giống nhau (gần như chắc chắn), server cho phép user truy cập vào hệ thống. Ngược lại nếu phép so sánh cho kết quả khác nhau, chứng tỏ password p' không đúng, server từ chối truy cập của user.

Flow kiểm tra password sử dụng hash lưu trong DB

3. Các cách giải mã password và cách ngăn chặn

Trong phạm vi bài viết này, mình sẽ không đi sâu vào phần thuật toán mà chỉ đưa ra các khái niệm chung. Phần chi tiết thuật toán xin được phép trình bày trong một bài blog khác.

Tấn công password có thể chia ra là online attack và offline attack. Online attack yêu cầu sự tương tác với server (đối tượng bị tấn công), trong khi offline attack không tương tác với server nhưng yêu cầu attacker phải truy cập được vào database chứa password đã mã hóa.

Với online attack, server có thể nâng cao bảo mật bằng các cách như hạn chế số lần login (ví dụ tối đa là 5 lần), hay giới hạn khoảng thời gian giữa các lần login (ví dụ tối thiểu là 2 giây), ... Tuy vậy, vẫn có các lỗ hổng để rò rỉ thông tin, điển hình như các phương pháp tấn công padding, hay plaintext oracle attacks.

Sau đây mình xin phép trình bày các kĩ thuật tấn công với giả sử là diễn ra offline, tức attacker có thể truy cập được vào database lưu password mã hóa. Các kĩ thuật này gồm brute-force attack, dictionary attack, rainbow table attack.

  • Brute-force attack: Là kĩ thuật "thử" tất các khả năng có thể của password cho đến khi tìm được password đúng. Kĩ thuật này yêu cầu việc tính hash của password và so sánh với giá trị trong database. Thời gian thử tăng theo độ dài của password. Trong một số trường hợp, dựa trên giới hạn của password như chữ cái đầu viết hoa, gồm kí tự đặc biệt, ... mà attacker có thể giảm thiểu số trường hợp phải thử.
  • Dictionary attack: Cũng là kĩ thuật "thử", nhưng nó không thử tất cả các khả năng mà sử dụng một tập các giá trị password có sẵn để thử (lý do tại sao gọi là dictionary attack). Kiểu tấn công này dựa trên cơ sở rằng người dùng thường sử dụng các password dễ đoán và lặp lại.
  • Rainbow table attack là kĩ thuật tạo trước một table mapping giữa plain text password và hash của nó. Attacker sử dụng table và query hash trong table này, lặp lại quá trình cho tới khi tìm được password có mã hóa giống với giá trị lưu trong database. Ưu điểm của phương pháp này so với brute-force attack là giảm thiểu quá trình tính hash của password. Tuy nhiên ta có thể ngăn chặn kiểu tấn công này bằng cách sử dụng kĩ thuật salt (kĩ thuật thêm data vào password trước khi hash).

4. Thực hành

Có rất nhiều giải thuật mã hóa khác nhau như SHA(SHA-0, SHA-1, SHA-2, ...), MD(MD, MD2, MD4, MD5, MD6) ... nhưng chúng đều có các điểm hạn chế trong bài toán mã hóa password (mình xin phép giải thích chi tiết trong blog khác nếu các bạn có quan tâm đến phần này).

Các thuật toán mã hóa password phổ biến hiện nay có thể kể ra như PBKDF2, bcrypt, scrypt, ... Bcrypt là giải thuật mặc định được sử dụng cho OpenBSD, Linux distributions, có thể bảo vệ mật khẩu trước các hình thức tấn công như rainbow table, brute-force, ... và quan trọng nó được implement ở hầu hết các ngôn ngữ lập trình.

Sau đây ta sẽ thử sử dụng thư viện bcrypt của nodejs để tạo mã hóa cho mật khẩu.

Install thư viện bcrypt: npm i bcrypt

  • Bước 1: Import thư viện:
const bcrypt = require('bcrypt');
const saltRounds = 10;
const myPlaintextPassword = '$x*&5a#6%7=8';
const attackerPassword = 'attacker_password';
  • Bước 2: Tạo password mã hóa với random salt:
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
    if(!err) {
        console.log(hash);
        myPasswordHash = hash;
    }
});

Output: '$2b$10$xIGK9.3Rre7ZBWrAqh84neMaB0AC4ivilS7mUO5m4PbA.2dnm5bTG'

2b: version của giải thuật bcrypt

10: số vòng tính salt (salt round)

xIGK9.3Rre7ZBWrAqh84neMaB0AC4ivilS7mUO5m4PbA.2dnm5bTG: salt, text đã mã hóa

Chú thích: Thuật toán của bcrypt compress cả salt, salt round và text đã mã hóa trong cùng một output. Những dữ liệu này sẽ được sử dụng để kiểm tra password.

  • Bước 3: Kiểm tra xem input password có đúng hay không:
// check with correct password
let hash = '$2b$10$xIGK9.3Rre7ZBWrAqh84neMaB0AC4ivilS7mUO5m4PbA.2dnm5bTG';
bcrypt.compare(myPlaintextPassword, hash, function(err, res) {
    if(err) {
        console.log(err);
    }else{
        console.log(res);
    }
});

Output: true

// check with wrong password
bcrypt.compare(attackerPassword, hash, function(err, res) {
    if(err) {
        console.log(err);
    }else{
        console.log(res);
    }
});

Output: false

Như vậy trong blog này chúng ta đã tìm hiểu về bài toán mã hóa password, cách nó vận hành cũng như tạo thử ví dụ để kiểm tra. Ngoài ra ta cũng đã tìm hiểu về một vài kiểu tấn công password. Hi vọng với những kiến thức này, các bạn sẽ có thể định hình và thiết kế module lưu trữ, xử lý password của mình để đảm bảo tính an toàn cho người dùng.

Thank and Best regards,

Haupv.

Reference:

http://www.unixwiz.net/techtips/iguide-crypto-hashes.html

https://en.wikipedia.org/wiki/Bcrypt

https://www.npmjs.com/package/bcrypt

https://rietta.com/blog/bcrypt-not-sha-for-passwords/