更多資訊請參考 rubyonrails.org:

Active Record 加密

本指南涵蓋如何使用 Active Record 加密您的資料庫資訊。

閱讀本指南後,您將了解

  • 如何使用 Active Record 設定資料庫加密。
  • 如何遷移未加密的資料。
  • 如何使不同的加密方案共存。
  • 如何使用 API。
  • 如何設定函式庫以及如何擴充它。

Active Record 支援應用程式層級加密。它的運作方式是宣告哪些屬性應該被加密,並在必要時無縫地加密和解密它們。加密層位於資料庫和應用程式之間。應用程式將存取未加密的資料,但資料庫將儲存加密的資料。

1 為何要在應用程式層級加密資料?

Active Record 加密的存在是為了保護您應用程式中的敏感資訊。一個典型的例子是來自使用者的個人識別資訊。但是,如果您已經在靜態加密資料庫,為何還需要應用程式層級加密?

作為一個直接的實際好處,加密敏感屬性會增加額外的安全層。例如,如果攻擊者存取了您的資料庫、其快照或您的應用程式日誌,他們將無法理解加密的資訊。此外,加密可以防止開發人員在應用程式日誌中意外暴露使用者的敏感資料。

但更重要的是,透過使用 Active Record 加密,您可以在程式碼層級定義應用程式中構成敏感資訊的內容。Active Record 加密可以對應用程式中的資料存取進行精細控制,並允許您的應用程式取用資料的服務。例如,請考慮保護加密資料的可稽核 Rails 主控台或檢查內建系統以自動篩選控制器參數

2 基本用法

2.1 設定

執行 bin/rails db:encryption:init 以產生一組隨機金鑰

$ bin/rails db:encryption:init
Add this entry to the credentials of the target environment:

active_record_encryption:
  primary_key: EGY8WhulUOXixybod7ZWwMIL68R9o5kC
  deterministic_key: aPA5XyALhf75NNnMzaspW7akTfZp0lPY
  key_derivation_salt: xEY0dt6TZcAMg52K7O84wYzkjvbA62Hz

這些值可以透過複製和貼上產生的值到您現有的Rails 憑證中來儲存。或者,這些值可以從其他來源設定,例如環境變數

config.active_record.encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"]
config.active_record.encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"]
config.active_record.encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"]

這些產生的值長度為 32 個位元組。如果您自己產生這些值,則您應該使用的最小長度為主金鑰 12 個位元組(這將用於導出 AES 32 位元組金鑰)和鹽 20 個位元組。

2.2 宣告加密屬性

可加密的屬性在模型層級定義。這些是具有相同名稱的欄位支援的一般 Active Record 屬性。

class Article < ApplicationRecord
  encrypts :title
end

函式庫將在將這些屬性儲存到資料庫之前透明地加密它們,並在檢索時解密它們

article = Article.create title: "Encrypt it all!"
article.title # => "Encrypt it all!"

但是,在幕後,執行的 SQL 看起來像這樣

INSERT INTO `articles` (`title`) VALUES ('{\"p\":\"n7J0/ol+a7DRMeaE\",\"h\":{\"iv\":\"DXZMDWUKfp3bg/Yu\",\"at\":\"X1/YjMHbHD4talgF9dt61A==\"}}')

2.2.1 重要事項:關於儲存和欄位大小

加密需要額外的空間,因為 Base64 編碼和與加密的酬載一起儲存的中繼資料。當使用內建的信封加密金鑰供應商時,您可以估計最壞情況下的額外負擔約為 255 個位元組。這種額外負擔在較大的尺寸上可以忽略不計。不僅因為它被稀釋了,而且因為函式庫預設使用壓縮,這可以為較大的酬載提供比未加密版本最多節省 30% 的儲存空間。

關於字串欄位大小有一個重要的問題:在現代資料庫中,欄位大小決定了它可以分配的字元數,而不是位元組數。例如,對於 UTF-8,每個字元最多可以佔用四個位元組,因此,在使用 UTF-8 的資料庫中,一個欄位在位元組數方面最多可以儲存其大小的四倍。現在,加密的酬載是序列化為 Base64 的二進位字串,因此它們可以儲存在一般的 string 欄位中。由於它們是一系列 ASCII 位元組,因此加密的欄位最多可以佔用其明文版本大小的四倍。因此,即使儲存在資料庫中的位元組相同,欄位也必須大四倍。

實際上,這表示

  • 當加密以西方字母(主要是 ASCII 字元)編寫的短文字時,您在定義欄位大小時應考慮到額外的 255 個額外負擔。
  • 當加密以非西方字母(例如西里爾字母)編寫的短文字時,您應該將欄位大小乘以 4。請注意,儲存額外負擔最多為 255 個位元組。
  • 當加密長文字時,您可以忽略欄位大小問題。

一些範例

要加密的內容 原始欄位大小 建議的加密欄位大小 儲存額外負擔(最壞情況)
電子郵件地址 string(255) string(510) 255 個位元組
簡短的表情符號序列 string(255) string(1020) 255 個位元組
以非西方字母編寫的文字摘要 string(500) string(2000) 255 個位元組
任意長文字 text text 可忽略不計

2.3 確定性加密和非確定性加密

預設情況下,Active Record 加密使用非確定性方法進行加密。在此上下文中,非確定性表示使用相同的密碼加密相同的內容兩次將產生不同的密文。這種方法透過使密文的密碼分析更加困難並使查詢資料庫變得不可能來提高安全性。

您可以使用 deterministic: 選項以確定性的方式產生初始化向量,從而有效地啟用查詢加密資料。

class Author < ApplicationRecord
  encrypts :email, deterministic: true
end

Author.find_by_email("some@email.com") # You can query the model normally

除非您需要查詢資料,否則建議使用非確定性方法。

在非確定性模式下,Active Record 使用 AES-GCM,其中包含 256 位元金鑰和隨機初始化向量。在確定性模式下,它也使用 AES-GCM,但初始化向量是作為金鑰和要加密內容的 HMAC-SHA-256 摘要產生的。

您可以透過省略 deterministic_key 來停用確定性加密。

3 功能

3.1 Action Text

您可以透過在其宣告中傳遞 encrypted: true 來加密 Action Text 屬性。

class Message < ApplicationRecord
  has_rich_text :content, encrypted: true
end

目前不支援將個別的加密選項傳遞給 Action Text 屬性。它將使用已設定的全域加密選項進行非確定性加密。

3.2 Fixtures

您可以透過將此選項新增到您的 test.rb 來自動加密 Rails fixtures

config.active_record.encryption.encrypt_fixtures = true

啟用後,所有可加密的屬性都將根據模型中定義的加密設定進行加密。

3.2.1 Action Text Fixtures

若要加密 Action Text fixtures,您應該將它們放置在 fixtures/action_text/encrypted_rich_texts.yml 中。

3.3 支援的類型

active_record.encryption 會在使用底層類型加密值之前將其序列化,但是,除非使用自訂的 message_serializer它們必須可序列化為字串。像 serialized 這樣的結構化類型開箱即用。

如果您需要支援自訂類型,建議的方法是使用序列化的屬性。序列化屬性的宣告應在加密宣告之前

# CORRECT
class Article < ApplicationRecord
  serialize :title, type: Title
  encrypts :title
end

# INCORRECT
class Article < ApplicationRecord
  encrypts :title
  serialize :title, type: Title
end

3.4 忽略大小寫

當查詢確定性加密的資料時,您可能需要忽略大小寫。有兩種方法可以更輕鬆地實現此目的。

您可以在宣告加密屬性時使用 :downcase 選項,在加密發生之前將內容轉換為小寫。

class Person
  encrypts :email_address, deterministic: true, downcase: true
end

當使用 :downcase 時,原始大小寫會遺失。在某些情況下,您可能只想在查詢時忽略大小寫,同時也儲存原始大小寫。對於這些情況,您可以使用 :ignore_case 選項。這需要您新增一個名為 original_<column_name> 的新欄位來儲存未變更大小寫的內容。

class Label
  encrypts :name, deterministic: true, ignore_case: true # the content with the original case will be stored in the column `original_name`
end

3.5 支援未加密資料

為了簡化未加密資料的遷移,此函式庫包含 config.active_record.encryption.support_unencrypted_data 選項。當設定為 true

  • 嘗試讀取未加密的加密屬性將正常運作,而不會引發任何錯誤。
  • 具有確定性加密屬性的查詢將包含它們的「明文」版本,以支援尋找加密和未加密的內容。您需要設定 config.active_record.encryption.extend_queries = true 來啟用此功能。

此選項旨在於過渡期間使用,在此期間,明文資料和加密資料必須共存。兩者預設都設定為 false,這是任何應用程式的建議目標:在處理未加密資料時會引發錯誤。

3.6 支援先前的加密方案

變更屬性的加密屬性可能會破壞現有的資料。例如,假設您想將確定性屬性變為非確定性。如果您僅變更模型中的宣告,讀取現有的密文將會失敗,因為加密方法現在不同了。

為了支援這些情況,您可以宣告先前的加密方案,這些方案將在兩種情況下使用

  • 當讀取加密資料時,如果目前的方案不起作用,Active Record Encryption 將嘗試先前的加密方案。
  • 當查詢確定性資料時,它將使用先前的方案新增密文,以便查詢可以與使用不同方案加密的資料無縫運作。您必須設定 config.active_record.encryption.extend_queries = true 來啟用此功能。

您可以設定先前的加密方案

  • 全域
  • 以每個屬性為基礎

3.6.1 全域先前的加密方案

您可以透過在 application.rb 中使用 previous 設定屬性將它們新增為屬性列表來新增先前的加密方案。

config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]

3.6.2 每個屬性的加密方案

宣告屬性時使用 :previous

class Article
  encrypts :title, deterministic: true, previous: { deterministic: false }
end

3.6.3 加密方案和確定性屬性

當新增先前的加密方案時

  • 對於非確定性加密,新資訊將始終使用最新(目前)的加密方案進行加密。
  • 對於確定性加密,預設情況下,新資訊將始終使用最舊的加密方案進行加密。

通常,使用確定性加密時,您希望密文保持不變。您可以透過設定 deterministic: { fixed: false } 來變更此行為。在這種情況下,它將使用最新的加密方案來加密新資料。

3.7 唯一約束

唯一約束只能與確定性加密的資料一起使用。

3.7.1 唯一驗證

只要啟用擴充查詢 (config.active_record.encryption.extend_queries = true),就正常支援唯一驗證。

class Person
  validates :email_address, uniqueness: true
  encrypts :email_address, deterministic: true, downcase: true
end

當結合加密和未加密的資料,以及設定先前的加密方案時,它們也將運作。

如果您想忽略大小寫,請確保在 encrypts 宣告中使用 downcase:ignore_case:。在驗證中使用 case_sensitive: 選項將不起作用。

3.7.2 唯一索引

為了支援確定性加密欄位上的唯一索引,您需要確保它們的密文永遠不會變更。

為了鼓勵這樣做,當設定多個加密方案時,確定性屬性預設將始終使用最舊的可用的加密方案。否則,您有責任確保這些屬性的加密屬性不會變更,否則唯一索引將無法運作。

class Person
  encrypts :email_address, deterministic: true
end

3.8 篩選命名為加密欄位的參數

依預設,加密欄位設定為在 Rails 記錄檔中自動篩選。您可以透過將以下內容新增至您的 application.rb 來停用此行為。

config.active_record.encryption.add_to_filter_parameters = false

如果已啟用篩選,但您想要排除特定欄位自動篩選,請將它們新增至 config.active_record.encryption.excluded_from_filter_parameters

config.active_record.encryption.excluded_from_filter_parameters = [:catchphrase]

當產生篩選參數時,Rails 將使用模型名稱作為前綴。例如:對於 Person#name,篩選參數將是 person.name

3.9 編碼

此函式庫將保留非確定性加密的字串值的編碼。

由於編碼與加密的酬載一起儲存,因此確定性加密的值預設將強制使用 UTF-8 編碼。因此,當加密時,具有不同編碼的相同值將產生不同的密文。您通常希望避免這種情況,以保持查詢和唯一性約束正常運作,因此此函式庫會自動為您執行轉換。

您可以設定確定性加密所需的預設編碼

config.active_record.encryption.forced_encoding_for_deterministic_encryption = Encoding::US_ASCII

您可以停用此行為,並在所有情況下保留編碼

config.active_record.encryption.forced_encoding_for_deterministic_encryption = nil

3.10 壓縮

此函式庫預設會壓縮加密的酬載。這可以為較大的酬載節省高達 30% 的儲存空間。您可以透過為加密的屬性設定 compress: false 來停用壓縮

class Article < ApplicationRecord
  encrypts :content, compress: false
end

您也可以設定用於壓縮的演算法。預設的壓縮器是 Zlib。您可以透過建立一個響應 #deflate(data)#inflate(data) 的類別或模組來實作您自己的壓縮器。

require "zstd-ruby"

module ZstdCompressor
  def self.deflate(data)
    Zstd.compress(data)
  end

  def self.inflate(data)
    Zstd.decompress(data)
  end
end

class User
  encrypts :name, compressor: ZstdCompressor
end

您可以全域設定壓縮器

config.active_record.encryption.compressor = ZstdCompressor

4 金鑰管理

金鑰提供者實作金鑰管理策略。您可以全域或以每個屬性為基礎設定金鑰提供者。

4.1 內建金鑰提供者

4.1.1 DerivedSecretKeyProvider

金鑰提供者將使用 PBKDF2 從提供的密碼衍生金鑰。

config.active_record.encryption.key_provider = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(["some passwords", "to derive keys from. ", "These should be in", "credentials"])

依預設,active_record.encryption 會使用在 active_record.encryption.primary_key 中定義的金鑰設定 DerivedSecretKeyProvider

4.1.2 EnvelopeEncryptionKeyProvider

實作簡單的 信封加密 策略

  • 它為每個資料加密操作產生一個隨機金鑰
  • 它將資料金鑰與資料本身一起儲存,並使用憑證 active_record.encryption.primary_key 中定義的主要金鑰進行加密。

您可以透過將此內容新增至您的 application.rb 來設定 Active Record 使用此金鑰提供者

config.active_record.encryption.key_provider = ActiveRecord::Encryption::EnvelopeEncryptionKeyProvider.new

與其他內建金鑰提供者一樣,您可以在 active_record.encryption.primary_key 中提供主要金鑰的列表,以實作金鑰輪換方案。

4.2 自訂金鑰提供者

對於更進階的金鑰管理方案,您可以在初始化器中設定自訂金鑰提供者

ActiveRecord::Encryption.key_provider = MyKeyProvider.new

金鑰提供者必須實作此介面

class MyKeyProvider
  def encryption_key
  end

  def decryption_keys(encrypted_message)
  end
end

這兩種方法都會傳回 ActiveRecord::Encryption::Key 物件

  • encryption_key 會傳回用於加密某些內容的金鑰
  • decryption_keys 會傳回用於解密指定訊息的潛在金鑰列表

金鑰可以包含任意標籤,這些標籤將與訊息一起以未加密的形式儲存。您可以使用 ActiveRecord::Encryption::Message#headers 在解密時檢查這些值。

4.3 屬性特定的金鑰提供者

您可以使用 :key_provider 選項以每個屬性為基礎設定金鑰提供者

class Article < ApplicationRecord
  encrypts :summary, key_provider: ArticleKeyProvider.new
end

4.4 屬性特定的金鑰

您可以使用 :key 選項以每個屬性為基礎設定給定的金鑰

class Article < ApplicationRecord
  encrypts :summary, key: "some secret key for article summaries"
end

Active Record 使用該金鑰來衍生用於加密和解密資料的金鑰。

4.5 輪換金鑰

active_record.encryption 可以使用金鑰列表來支援實作金鑰輪換方案

  • 最後一個金鑰將用於加密新內容。
  • 當解密內容時,將會嘗試所有金鑰,直到有一個金鑰起作用為止。
active_record_encryption:
  primary_key:
    - a1cc4d7b9f420e40a337b9e68c5ecec6 # Previous keys can still decrypt existing content
    - bc17e7b413fd4720716a7633027f8cc4 # Active, encrypts new content
  key_derivation_salt: a3226b97b3b2f8372d1fc6d497a0c0d3

這可以啟用以下工作流程:您透過新增金鑰、重新加密內容和刪除舊金鑰來保留一個簡短的金鑰列表。

目前不支援確定性加密的金鑰輪換。

Active Record Encryption 尚未提供金鑰輪換流程的自動管理。所有部分都已就緒,但尚未實作。

4.6 儲存金鑰參考

您可以設定 active_record.encryption.store_key_references 來使 active_record.encryption 在加密訊息本身中儲存對加密金鑰的參考。

config.active_record.encryption.store_key_references = true

這樣做可以提高解密效能,因為系統現在可以直接找到金鑰,而不是嘗試金鑰列表。代價是儲存空間:加密的資料會稍微大一些。

5 API

5.1 基本 API

ActiveRecord 加密旨在聲明式使用,但它提供了用於進階使用案例的 API。

5.1.1 加密和解密

article.encrypt # encrypt or re-encrypt all the encryptable attributes
article.decrypt # decrypt all the encryptable attributes

5.1.2 讀取密文

article.ciphertext_for(:title)

5.1.3 檢查屬性是否已加密

article.encrypted_attribute?(:title)

6 設定

6.1 設定選項

您可以在 application.rb(最常見的情況)或在特定的環境設定檔 config/environments/<env name>.rb 中設定 Active Record Encryption 選項,如果您想要以每個環境為基礎設定它們。

建議使用 Rails 內建的憑證支援來儲存金鑰。如果您偏好透過設定屬性手動設定它們,請確保您不會將它們與您的程式碼一起提交(例如,使用環境變數)。

6.1.1 config.active_record.encryption.support_unencrypted_data

當為 true 時,可以正常讀取未加密的資料。當為 false 時,它將引發錯誤。預設值:false

6.1.2 config.active_record.encryption.extend_queries

當為 true 時,如果需要,將修改引用確定性加密屬性的查詢以包含其他值。如果 config.active_record.encryption.support_unencrypted_data 為 true,則那些附加值將是該值的清理版本,以及使用先前加密方案加密的值(如果有的話,如 previous: 選項所提供)。預設值:false(實驗性)。

6.1.3 config.active_record.encryption.encrypt_fixtures

當為 true 時,載入時將自動加密 fixtures 中的可加密屬性。預設值:false

6.1.4 config.active_record.encryption.store_key_references

當為 true 時,加密訊息的標頭中會儲存對加密金鑰的參考。當使用多個金鑰時,這可以加快解密速度。預設值:false

6.1.5 config.active_record.encryption.add_to_filter_parameters

當為 true 時,加密的屬性名稱會自動新增至 config.filter_parameters,且不會顯示在記錄檔中。預設值:true

6.1.6 config.active_record.encryption.excluded_from_filter_parameters

您可以設定當 config.active_record.encryption.add_to_filter_parameters 為 true 時不會篩選掉的參數列表。預設值:[]

6.1.7 config.active_record.encryption.validate_column_size

根據欄位大小新增驗證。建議這樣做以防止使用高度可壓縮的酬載儲存龐大的值。預設值:true

6.1.8 config.active_record.encryption.primary_key

用於衍生根資料加密金鑰的金鑰或金鑰列表。它們的使用方式取決於設定的金鑰提供者。建議透過 active_record_encryption.primary_key 憑證進行設定。

6.1.9 config.active_record.encryption.deterministic_key

用於確定性加密的金鑰或金鑰列表。建議透過 active_record_encryption.deterministic_key 憑證進行設定。

6.1.10 config.active_record.encryption.key_derivation_salt

用於衍生金鑰的鹽值。建議透過 active_record_encryption.key_derivation_salt 憑證進行設定。

6.1.11 config.active_record.encryption.forced_encoding_for_deterministic_encryption

用於確定性加密的屬性的預設編碼。您可以將此選項設定為 nil 來停用強制編碼。預設值為 Encoding::UTF_8

6.1.12 config.active_record.encryption.hash_digest_class

用於衍生金鑰的摘要演算法。預設值為 OpenSSL::Digest::SHA256

6.1.13 config.active_record.encryption.support_sha1_for_non_deterministic_encryption

支援使用摘要類別 SHA1 解密以非確定性方式加密的資料。預設值為 false,表示僅支援在 config.active_record.encryption.hash_digest_class 中設定的摘要演算法。

6.1.14 config.active_record.encryption.compressor

用於壓縮加密有效負載的壓縮器。它應該回應 deflateinflate。預設值為 Zlib。您可以在 壓縮 章節中找到有關壓縮器的更多資訊。

6.2 加密上下文

加密上下文定義了在給定時刻使用的加密元件。有一個基於您的全域設定的預設加密上下文,但您可以為給定的屬性或在執行特定的程式碼區塊時設定自訂上下文。

加密上下文是一種彈性但進階的設定機制。大多數使用者應該不必關心它們。

加密上下文的主要元件是:

  • encryptor:公開用於加密和解密資料的內部 API。它與 key_provider 互動以建立加密訊息並處理其序列化。加密/解密本身由 cipher 完成,序列化由 message_serializer 完成。
  • cipher:加密演算法本身 (AES 256 GCM)
  • key_provider:提供加密和解密金鑰。
  • message_serializer:序列化和反序列化加密的有效負載 (Message)。

如果您決定建立自己的 message_serializer,請務必使用無法反序列化任意物件的安全機制。一個常見的支援情境是加密現有的未加密資料。攻擊者可以利用這一點在加密發生之前輸入被篡改的有效負載並執行 RCE 攻擊。這表示自訂序列化器應避免使用 MarshalYAML.load(請改用 YAML.safe_load)或 JSON.load(請改用 JSON.parse)。

6.2.1 全域加密上下文

全域加密上下文是預設使用的上下文,並且像其他設定屬性一樣在您的 application.rb 或環境設定檔案中進行設定。

config.active_record.encryption.key_provider = ActiveRecord::Encryption::EnvelopeEncryptionKeyProvider.new
config.active_record.encryption.encryptor = MyEncryptor.new

6.2.2 每個屬性的加密上下文

您可以透過在屬性宣告中傳遞加密上下文參數來覆寫它們。

class Attribute
  encrypts :title, encryptor: MyAttributeEncryptor.new
end

6.2.3 執行程式碼區塊時的加密上下文

您可以使用 ActiveRecord::Encryption.with_encryption_context 為給定的程式碼區塊設定加密上下文。

ActiveRecord::Encryption.with_encryption_context(encryptor: ActiveRecord::Encryption::NullEncryptor.new) do
  # ...
end

6.2.4 內建加密上下文

6.2.4.1 停用加密

您可以執行不加密的程式碼。

ActiveRecord::Encryption.without_encryption do
  # ...
end

這表示讀取加密文字將返回密文,而儲存的內容將以未加密的方式儲存。

6.2.4.2 保護加密資料

您可以執行不加密的程式碼,但阻止覆寫加密的內容。

ActiveRecord::Encryption.protecting_encrypted_data do
  # ...
end

如果您想在仍然對加密資料執行任意程式碼(例如在 Rails 主控台中)時保護加密資料,這會很方便。



回到頂端