Mã hóa các dữ liệu "bí mật" với Rails

Đôi khi trong các dự án, chúng ta cần kết nối tới các api của bên thứ ba hoặc kết nối tới các service, để truy cập các API hoặc service nói trên, chúng ta cần lưu trữ các key API trong hệ thống, và vì các thông tin này thường là các thông tin nhạy cảm nên chúng ta không muốn lưu trữ chúng dưới dạng văn bản thuần túy trong cơ sở dữ liệu mà phải mã hóa chúng. Bài viết dưới đây sẽ hướng dẫn các bạn 1 trong các cách mã hóa những thông tin nhạy cảm như vậy.

I. Giới thiệu class ActiveSupport::MessageEncryptor

Rails cung cấp cho chúng ta class MessageEncryptor, nó sẽ lấy 1 key để mã hóa/giải mã string của chúng ta. Điều đó giúp tránh được việc chúng ta dùng 1 key duy nhất cho tất cả các string cần mã hóa. Chúng ta có đọan mã như sau:

class Encryptor
  def initialize(key, salt)
    passphrase = ActiveSupport::KeyGenerator.new(key).generate_key(salt)
    @encryptor = ActiveSupport::MessageEncryptor.new(passphrase)
  end

  def encrypt(plaintext)
    @encryptor.encrypt_and_sign(plaintext)
  end

  def decrypt(encrypted_data)
    @encryptor.decrypt_and_verify(encrypted_data)
  end
end

Do hàm encrypt dùng vector khởi tạo ngẫu nhiên nên mỗi lần mã hóa chúng ta sẽ được 1 chuỗi hash mới, nhưng khi giải mã thì chỉ cho ra 1 kết quả duy nhất thôi (chính là chuỗi mà chúng ta mã hóa ban đầu).

II. Key/salt generation

Ý tưởng của chúng ta là sau khi có API key của bên thứ 3, chúng ta mã hóa và lưu vào 1 chỗ nào đó (có thể là database), key dùng để mã hóa chúng ta lưu ở một nơi khác (thường là file env trên server đó, không được lưu trên các hệ thống codebase). Như vậy khi attaker lấy được 1 trong 2 thông tin, chúng vẫn tiếp tục phải lấy được key còn lại để giải mã. Đoạn mã sau là ví dụ có thể tạo ra 10 cặp key dùng để mã hóa, bạn có thể generate ra file yml để sử dụng.

def generate_random_values
  10.times.each_with_object({}) do |number, acc|
    index = sprintf "%02d", number

    acc[index] = SecureRandom.hex(64)
  end
end

generate_random_values

#=> {"00"=>"4419df013bb7bdfa2258fd31ccb15dd1e3e4c7c65994bf8fd13dd945d39d8c01d9354ec21d74a213181391bf0bde8e1f30bb4c377a117ca6e4967698898d870a",
 "01"=>"6a6988a79c01a6060242e1ac9b97363f3442c473a8c12c9633f500fca59be05ae9942ce81cd28c7f7ea5e7dc65c544b08dd86a0faefb4c270d549c5f8caf595d",
 "02"=>"dbcff6e9d53097329ac586fa6e5291dbbe23e0e6d61b6f1576b9c27ea8467bf977fc319ff6637e6df035107973fd38c10f7094bf705945bb3e37bed935322da0",
 "03"=>"4dbbe78c54144d77c9e5930433beacca8eb1077db5ed127a05439337db2729754ae421711bde9ed5500b16aa0b64247b50ef597c701d3014ae5eec5fd5f40261",
 "04"=>"8fb12b4d551af401bbf0651f0831519a54e8d23e3abaa8f6033ce8221a223d98438d30eadee7bbd21ca0d9d43403087277ee617c9aa3880cf1fc2261ee712a0f",
 "05"=>"5ca7e64c710258a0f395a6f1775819491a6f4f6cb73ddda45ee11aa41c1a364e7af49e4a0d6b160ccdde00fee045f381401ef18ee2ed6e6cae176a39f24d0b57",
 "06"=>"f7e182315575664212539fc67c6eefbb195545611a29290145bfb6f5cb7f56386a20cce892a327de4612cd040c327b8afe0b7e28b9152f67d32911600d241a09",
 "07"=>"e81375597f91d75661be035b812d6d4ae6f02d304801fe909215861a689b6537b0adb04cd5c837234d1c5e4e21771e10503ddec35ec5a699834ad48f230fd007",
 "08"=>"673d4e77e739c460c198874901d3b0f13fedf4041bb3f5b0853ae1e8359051d7c6941ef8ff52f6f2f15da88fae30c36c3ae7764c6932d544959b4ac714f0986e",
 "09"=>"9380df10b6dde2caa8cf93d83caaa1fe2caf9cf9633ebf2b0bd8b32e61c9f2c563b9f4bf5b1e47e8b505cd24cb81c8307d65696bbd42486c8611a49b9fcc49d3"}

Chúng ta cũng có thể tạo 1 task để sinh file yml và sử dụng với ENV như sau

["keys", "salts"].each do |filename|
  my_secrets = {
    development: generate_random_values,
    test:        generate_random_values
  }

  File.write Rails.root.join("config", "secret_#{filename}.yml"), my_secrets.to_yaml
end

Khi chạy task lần đầu tiên, chúng ta sẽ có 2 file secret_key.yml và secret_salt.yml.

Ở file initialize, khi khởi chạy hệ thống, chúng ta load các key và salt này vào 1 hằng để sử dụng.

SECRET_KEYS  = Rails.application.config_for("secret_keys")
SECRET_SALTS = Rails.application.config_for("secret_salts")

Hàm config_for là 1 hàm được giới thiệu trên phiên bản Rail 4.2+, nhận vào tên file config yml và trả về nội dung là list các key của môi trường tương ứng (giống như cách hoạt động của file database.yml).

Hệ thống chúng ta sẽ hoạt động như sau:

  • Khi nhận được API key, hệ thống sẽ lựa chọn 1 cặp key/salt để mã hóa chuỗi api và lưu vào database.
  • Trước khi sử dụng key, giải mã để lấy được API bằng cặp key/salt đã mã hóa.

Cặp key/salt được chọn để mã hóa/giải mã như thế nào tùy thuôc vào hệ thống của các bạn, tuy nhiên chúng ta cần dựa vào các dữ liệu không đổi và có tính đặc thù (ví dụ như created_at, user_id...) để đảm bảo luôn giải mã được các key đã lưu.

III. Tổng kết

Không có phương thức bảo mật nào hoàn hảo, nhưng ít nhất tách biệt việc lưu trữ các key / salt và thông tin cần mã hóa cũng có thể đảm bảo rằng một cuộc tấn công SQL injection sẽ không làm mất tất cả dữ liệu nhạy cảm của khách hàng. An toàn thông tin luôn luôn phát triển song hành với các phương thức tấn công, vì vậy chúng ta luôn cố gắng tìm cách để làm cho ứng dụng của mình an toàn hơn, sử dụng cách thức trên cũng là một trong số các cách đó.


All Rights Reserved