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.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Kiểm tra xem mật khẩu đã bị lộ hay chưa bằng passpwnedcheck”

Leave a Reply