Note: see the link below for the English version of this article.
https://duongnt.com/leaked-password
Trong bài trước, chúng ta đã tìm hiểu cách tạo một mật khẩu mạnh. Như đã biết, tấn công từ điển là một trong những cách hack mật khẩu hiệu quả nhất, và từ điển này được tạo từ dữ liệu do các trang web làm lộ ra.
Những vụ rò rỉ dữ liệu như vậy nhiều đến mức có hẳn một trang web tên là haveibeenpwned được lập ra chỉ để thống kê chúng. Và trang web này còn cung cấp một API để người dùng có thể so sánh mật khẩu của mình với danh sách các mật khẩu đã bị lộ.
Hôm nay, chúng ta sẽ sử dụng passpwnedcheck, một package mà tôi viết bằng Python, để kiểm tra xem mật khẩu của ta đã bị lộ hay chưa.
Note: nếu ta dùng biện pháp mà tôi giới thiệu ở bài trước để tạo passphrase thì ta có thể tự tin là passphrase đó không hề tồn tại ở bất kỳ đâu khác, vì lúc này lượng entropy là đủ lớn.
Dùng k-anonymity để kiểm tra mật khẩu mà không cần tiết lộ nó
Có lẽ nhiều người sẽ e ngại khi nghĩ đến việc gửi mật khẩu của mình lên trang haveibeenpwned. Mối lo này là hoàn toàn có lý, đúng là ta không nên gửi mật khẩu của mình cho bất kỳ bên thứ ba nào. Thay vào đó, ta sẽ dùng một thuộc tính toán học gọi là k-anonymity để kiểm tra mật khẩu của mình mà không cần tiết lộ nó cho ai.
Giả sử mật khẩu của ta là thaiduong
, các bước để kiểm tra sẽ như sau.
- Tính hash SHA1 cho mật khẩu với format là hex, trong ví dụ của ta giá trị hash là
90CF82C8ABCBEB601EF133F2407C32A00E6AB7F9
. - Chia hash thành 2 phần, phần prefix chứa 5 ký tự
90CF8
(20 bit) và phần suffix chứa những ký tự còn lại là2C8ABCBEB601EF133F2407C32A00E6AB7F9
(140-bit). - Gửi một request GET tới URL
https://api.pwnedpasswords.com/range/90CF8
để lấy về tất cả các mật khẩu có hash bắt đầu là90CF8
. Có thể thấy là ta gửi phần prefix cho API haveibeenpwned thông qua query parameter. Tại thời điểm viết bài này, response từ API là như sau.2C8ABCBEB601EF133F2407C32A00E6AB7F9:328 2D48C2358A0B9705369E876C6204C275D2B:1 2D8D15EAB16342EFECA70776E143D55183A:1 2E78930667364DDE018A196EDE2AB6C39DE:3 2E850D600CC1A9336CC0CB7E1933875AF84:4 ... thêm 606 dòng nữa
- Tìm xem suffix của ta có nằm trong response hay không. Nếu có thì mật khẩu của ta đã bị lộ. Từ dòng
2C8ABCBEB601EF133F2407C32A00E6AB7F9:328
, ta biết được rằng mật khẩu này đã bị lộ328
lần (có lẽ tôi không nên dùng tên làm mật khẩu). - Ngược lại, nếu không tìm thấy suffix trong response thì có lẽ mật khẩu của ta vẫn an toàn.
k-anonymity an toàn đến mức nào?
Khái niệm k-anonymity được nhắc đến lần đầu tiên trong một bài báo khoa học vào năm 1998. Mục tiêu của nó là.
The objective is to release information freely but to do so in a way that the identity of any individual contained in the data cannot be recognized. In this way, information can be shared freely and used for many new purposes.
Tạm dịch:
Mục tiêu là công khai dữ liệu nhưng vẫn đảm bảo giữ bí mật danh tính của những người xuất hiện trong dữ liệu đó. Bằng cách này, dữ liệu của họ có thể được chia sẻ một cách tự do cho nhiều mục đích khác nhau.
Và định nghĩa của k-anonymity là như sau.
Let T(A₁,…, Aₙ) be a table and Q|ₜ be the quasi-identifiers associated with it. T is said to satisfy k-anonymity if and only if for each quasi-identifier QI ∈ Q|ₜ each sequence of values in T[QI] appears at least with k occurrences in T[QI].
Đoạn này có vẻ phức tạp, nhưng ý chính là với bất kỳ cá nhân nào trong tập dữ liệu, ta không thể phân biệt họ với ít nhất là k-1 cá nhân khác trong cùng dữ liệu đó.
Qua một số thử nghiệm, tôi nhận thấy rằng với mỗi prefix, response mà haveibeenpwned trả về có khoảng 600 dòng. Có nghĩa là kể cả trong trường hợp xấu nhất là mật khẩu của ta đã bị lộ thì API vẫn không thể biết được mật khẩu đó là cái nào trong số 600 mật khẩu khác nhau. Nhưng trong trường hợp này thì đằng nào ta cũng phải đổi mật khẩu ngay nên không còn nhiều điều để nói, ta quan tâm hơn tới trường hợp mật khẩu chưa bị lộ.
Vì mỗi hash SHA1 đều có độ dài là 160 bit và ta chỉ gửi 20 bit đầu cho API nên cho dù phần prefix này có bị lộ đi chăng nữa thì để có thể thực hiện tấn công vét cạn, hacker vẫn phải đoán được 140 bit còn lại. Và nếu ta dùng mật khẩu mạnh (ví dụ như passphrase gồm 8 từ như tôi đã giới thiệu trong bài trước) thì tấn công vét cạn sẽ là bất khả thi.
Giới thiệu package passpwnedcheck
Tôi đã viết một package Python gọi là passpwnedcheck để giúp việc dùng API haveibeenpwned trở nên dễ dàng hơn. Các bạn có thể download nó từ link sau.
https://github.com/duongntbk/passpwnedcheck
Hoặc các bạn có thể dùng pip để cài đặt, câu lệnh là như sau.
pip install passpwnedcheck
Gửi request có đồng bộ
Đây là biện pháp đơn giản hơn, tiện dụng trong trường hợp ta không quá đặt nặng hiệu năng. Đầu tiên ta tạo một object với kiểu là PassChecker
.
from passpwnedcheck.pass_checker import PassChecker
pass_checker = PassChecker()
Gọi hàm is_password_compromised
của lớp PassChecker
để gửi request tới API. Kết quả trả về có dạng tuple với 2 phần tử; phần tử thứ nhất là biến boolean cho biết mật khẩu đã bị lộ hay chưa, phần tử thứ hai là số lần mật khẩu đó đã bị lộ.
passwords = 'Password'
is_leaked, count = await pass_checker.is_password_compromised(password)
if is_leaked:
print(f'Your password has been leaked {count} times')
else:
print('Your password has not been leaked (yet)')
Các bạn cũng có thể chạy trực tiếp script pass_checker.py
từ command line, nhớ là phải cài package bằng pip trước.
C:\> python pass_checker.py password
Your password has been compromised xxxxxxx time(s)
Gửi request không đồng bộ
Từ bản 2.0.0 trở đi, package hỗ trợ cả request không đồng bộ, ta không block thread trong lúc chờ response từ API. Đầu tiên, ta cần tạo object với kiểu là PassCheckerAsync
, lúc này ta cần tạo sẵn session với package assyncio
.
from passpwnedcheck.pass_checker_async import PassCheckerAsync
# session = <Code để tạo object với kiểu assyncio.session>
pass_checker_async = PassCheckerAsync(session)
Cách kiểm tra một mật khẩu đơn đơn lẻ giống như khi ta dùng request có đồng bộ.
passwords = 'Password'
is_leaked, count = await pass_checker_async.is_password_compromised(password)
Ta cũng có thể kiểm tra nhiều mật khẩu một lúc. Ta sẽ gửi 1 request riêng cho từng mật khẩu, và tất cả các request đó sẽ chạy song song với nhau.
passwords = ['Password1', 'Password2', 'Password3', 'Password4']
results = await PassCheckerAsync.is_passwords_compromised(passwords)
Lúc này, results
sẽ là một từ điển, với key là mật khẩu còn giá trị là số lần mật khẩu đó đã bị lộ. Từ điển đó có dạng như sau.
{
'Password1': 19,
'Password2': 89,
'Password3': 123,
'Password4': 456
}
Để giảm tải cho API, ta sẽ gửi request theo từng batch, mỗi batch có 10 request. Kích cỡ của batch là có thể thay đổi, nhưng ta cần chú ý để không gửi quá nhiều request một lúc kẻo làm quá tải API.
my_batch_size = 15
# Gửi 15 request một lúc
results = await pass_checker_async.is_passwords_compromised(passwords=passwords, batch_size=my_batch_size)
Nếu ta không cần tái sử dụng session thì ta có thể dùng lớp SessionManager
đi kèm trong package, chỉ cần đặt tất cả đoạn code phía trên trong câu lệnh with
.
from passpwnedcheck.session_manager import SessionManager
async with SessionManager() as manager:
pass_checker_async = PassCheckerAsync(manager.get_session())
is_leaked, count = await pass_checker_async.is_password_compromised('Password')
Kết thúc
Nếu dịch vụ của ta có nhiều thông tin đáng giá thì ta nên kiểm tra mật khẩu của người dùng để xem nó đã bị lộ hay chưa trước khi cho phép họ sử dụng nó trên hệ thống. Trong trường hợp đó, tôi tin rằng passpwnedcheck
sẽ hữu ích.
One Thought on “Kiểm tra xem mật khẩu đã bị lộ hay chưa bằng passpwnedcheck”