v7.1.3.2
更多資訊,請造訪 rubyonrails.org: 更多 Ruby on Rails

Active Record 加密

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

閱讀本指南後,您將知道

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("[email protected]") # 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 固定裝置

您可以透過將此選項新增至 test.rb 來自動加密 Rails 固定裝置。

config.active_record.encryption.encrypt_fixtures = true

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

3.2.1 Action Text 固定裝置

若要加密 Action Text 固定裝置,您應將它們置於 fixtures/action_text/encrypted_rich_texts.yml

3.3 支援的類型

active_record.encryption 會在加密值之前使用基礎類型序列化它們,但它們必須可以序列化為字串。結構化類型(例如 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

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 會設定一個 DerivedSecretKeyProvider,其金鑰定義在 active_record.encryption.primary_key 中。

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 加密尚未提供金鑰輪換程序的自動管理。所有部分都已存在,但尚未實作。

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(最常見的場景)中設定 Active Record 加密選項,或是在特定環境設定檔 config/environments/<env name>.rb 中設定,如果您想要依環境為基礎設定它們。

建議使用 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::SHA1

6.1.13 config.active_record.encryption.support_sha1_for_non_deterministic_encryption

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

6.2 加密內容

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

加密內容是一種彈性但進階的設定機制。大多數使用者不需在意它們。

加密內容的主要元件為

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

如果您決定建立自己的 message_serializer,使用無法反序列化任意物件的安全機制非常重要。常見的支援情境是加密現有的未加密資料。攻擊者可以在加密發生前利用此漏洞輸入竄改的 payload,並執行 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 主控台中),這會非常方便。

回饋

我們鼓勵您協助提升本指南的品質。

如果您發現任何錯字或事實錯誤,請協助我們修正。若要開始,您可以閱讀我們的 文件貢獻 部分。

您也可能會發現不完整或過時的內容。請務必針對 main 新增任何遺漏的文件。務必先查看 Edge Guides,以驗證問題是否已在 main 分支修正。查看 Ruby on Rails Guides Guidelines,了解風格和慣例。

如果您發現需要修復但無法自行修補的任何原因,請 開啟一個問題

最後但並非最不重要的一點,歡迎在 官方 Ruby on Rails 論壇 上針對 Ruby on Rails 文件進行任何類型的討論。