作者:suezi@IceSword Lab

概述

自2015年開發的 EXT4 Encryption 經過兩年的驗證性使用,Google 終于在年初的時候將 EXT4 Encryption 合并入 Chrome OS 用于保護用戶的隱私數據,完成與 eCryptfs 同樣的功能,簡稱該技術為 Dircrypto。當前,Chrome OS 仍是 eCryptfs 和 Dircrypto 兩種技術并存,但優先采用 Dircrypto,這表明 Dircrypto 將成為以后的主流趨勢。本文試圖闡述該技術的實現原理。

與 eCryptfs 一樣,EXT4 Encryption 用于完成文件(包括目錄)和文件名的加密,以實現多用戶系統中各個用戶私有數據的安全,即使在設備丟失或被盜的情況下,用戶隱私數據也不會輕易被人窺見。本文著重介紹文件內容加解密,文件名加解密留給讀者自行研究,技術要點主要包括:加解密模型、密鑰管理、EXT4 Encrytion 功能的開/關及參數設定操作。

EXT4 Encryption 簡述

創立 eCryptfs 十年之后,其主要的作者 Michael Halcrow 已從之前的 IBM 轉向服務 Google。Google 在保護用戶數據隱私方面具有強烈的需求,應用在其旗下的 Android、Chrome OS 及數據中心,此時采用的文件系統都是 EXT4,eCryptfs 屬于堆疊在 EXT4 上的文件系統,性能必定弱于直接在 EXT4 實現加密,恰好 EXT4 的主要維護者是 Google 的 Theodore Ts’o ,因此由 Michael Halcrow 主導、Theodore Ts’o 協助開發完成 EXT4 Encryption,目標在于“Harder,Better,Faster,Stronger”。

相比 eCryptfs,EXT4 Encryption 在內存使用上有所優化,表現在 read page 時,直接讀入密文到 page cache 并在該 page 中解密;而 eCryptfs 首先需要調用 EXT4 接口完成讀入密文到 page cache,然后再解密該 page 到另外的 page cache 頁,內存花銷加倍。當然,write page 時,兩者都不能直接對當前 page cache 加密,因為cache的明文內容需要保留著后續使用。在對文件加密的控制策略上,兩者都是基于目錄,但相比 eCryptfs 使用的 mount 方法,EXT4 Encryption 采用 ioctl 的策略顯得更加方便和靈活。另外,在密鑰管理方面,兩者也不相同。

EXT4 Encryption 加/解密文件的核心思想是:每個用戶持有一個 64 Bytes 的 master key,通過 master key 的描述(master key descriptor,實際使用時一般采用 key signature 加上“ext4:”前綴)進行識別,每個文件單獨產生一個16 Bytes的隨機密鑰稱為nonce,之后以nonce做為密鑰,采用 AES-128-ECB 算法加密 master key,產生 derived key。加/解密文件時采用 AES-256-XTS 算法,密鑰是 derived key。存儲文件時,將包含有格式版本、內容加密算法、文件名加密算法、旗標、 master key 描述、nonce等信息在內的數據保存在文件的 xattr 擴展屬性中。而master key由用戶通過一些加密手段進行存儲,在激活 EXT4 Encryption 前通過keys的系統調用以“logon”類型傳入內核keyring,即保證master只能被應用程序創建及更新但不能被應用程序讀取。加密是基于目錄樹的形式進行,加密策略通過EXT4_IOC_SET_ENCRYPTION ioctl對某個目錄進行下發,其子目錄或文件自動繼承父目錄的屬性,ioctl 下發的內容包括策略版本號、文件內容加密模式、文件名加密模式、旗標、master key 的描述。文件 read 操作時,從磁盤 block 中讀入密文到 page cache 并在該 page 中完成解密,然后拷貝到應用程序;文件 write 時采用 write page 的形式寫入磁盤,但不是在當前 page cache 中直接加密,而是將加密后的密文保存在另外的 page 中。

和 eCryptfs 一樣,EXT4 Encryption 在技術實現時利用了 page cache 機制的 Buffered I/O,換而言之就是不支持 Direct I/O。其加/解密的流程如圖一所示。

圖一 EXT4 Encryption加/解密流程

圖一中,在創建加密文件時通過get_random_bytes函數產生 16 Bytes 的隨機數,將其做為 nonce 保存到文件的 xattr 屬性中;當打開文件時取出文件的 nonce 和 master key 的描述,通過 master key 描述匹配到應用程序下發的 master key;然后以 nonce 做為密鑰,采用 AES-128-ECB 算法加密 master key 后產生 derived key,加/解密文件時采用該derived key做為密鑰,加密算法由用戶通過 ioctl 下發并保存到 xattr 的“contents_encryption_mode”字段,目前版本僅支持 AES-256-XTS;加/解密文件內容時調用 kernel crypto API 完成具體的加/解密功能。

下面分別從 EXT4 Encryption 使用的數據結構、內核使能 EXT4 Encryption 功能、如何添加 master key 到 keyring、如何開啟 EXT4 Encryption 功能、創建和打開加密文件、讀取和解密文件、加密和寫入加密文件等方面詳細敘述。

EXT4 Encryption 詳述

EXT4 Encryption 的主要數據結構

通過數據結構我們可以窺視到 EXT4 Encryption 的密鑰信息的保存和使用方式,非常有利于理解該加密技術。涉及到主要數據結構如下:

master key 的 payload 的數據表示如清單一所示,應用程序通過 add_key 系統調用將其和 master key descriptor 傳入內核 keyring。

清單一 master key
/* This is passed in from userspace into the kernel keyring */
struct ext4_encryption_key {
        __u32 mode;
        char raw[EXT4_MAX_KEY_SIZE];
        __u32 size;
} __attribute__((__packed__));

EXT4 Encryption 的文件加密信息的數據存儲結構如清單二結構體struct ext4_encryption_context所示,每個文件都對應保存著這樣的一個數據結構在其 xattr 中,包含了加密版本、文件內容和文件名的加密算法、旗標、master key descriptor 和隨機密鑰 nonce。

清單二 加密信息存儲格式
/**
 * Encryption context for inode
 *
 * Protector format:
 *  1 byte: Protector format (1 = this version)
 *  1 byte: File contents encryption mode
 *  1 byte: File names encryption mode
 *  1 byte: Reserved
 *  8 bytes: Master Key descriptor
 *  16 bytes: Encryption Key derivation nonce
 */
struct ext4_encryption_context {
    char format;
    char contents_encryption_mode;
    char filenames_encryption_mode;
    char flags;
    char master_key_descriptor[EXT4_KEY_DESCRIPTOR_SIZE];
    char nonce[EXT4_KEY_DERIVATION_NONCE_SIZE];
} __attribute__((__packed__));

設置 EXT4 Encryption 開啟是通過對特定目錄進行EXT4_IOC_SET_ENCRYPTION ioctl完成,具體策略使用清單三所示的struct ext4_encryption_policy 數據結構進行封裝,包括版本號、文件內容的加密算法、文件名的加密算法、旗標、master key descriptor。每個加密文件保存的ext4_encryption_context信息均繼承自該數據結構,子目錄繼承父目錄的ext4_encryption_context

清單三 Encryption policy
/* Policy provided via an ioctl on the topmost directory */
struct ext4_encryption_policy {
    char version;
    char contents_encryption_mode;
    char filenames_encryption_mode;
    char flags;
    char master_key_descriptor[EXT4_KEY_DESCRIPTOR_SIZE];
} __attribute__((__packed__));
`

open 文件時將文件加密相關信息從 xattr 中讀出并保存在清單四的struct ext4_crypt_info數據結構中,成員 ci_ctfm 用于調用 kernel crypto,在文件 open 時做好 key 的初始化。從磁盤獲取到加密信息后,將該數據結構保存到 inode 的內存表示struct ext4_inode_info中的i_crypt_info字段,方便后續的 readpage、writepage 時獲取到相應數據進行加/解密操作。

清單四 保存加/解密信息及調用接口的數據結構
struct ext4_crypt_info {
    char        ci_data_mode;
    char        ci_filename_mode;
    char        ci_flags;
    struct crypto_ablkcipher *ci_ctfm;
    char        ci_master_key[EXT4_KEY_DESCRIPTOR_SIZE];
};
`

如清單五所示,采用 struct ext4_crypto_ctx 表示在 readpage、writepage 時進行 page 加/解密的 context。在 writepage 時因為涉及到 cache 機制,需要保存明文頁,所以專門申請單獨的 bounce_page 保存密文用于寫入磁盤,用 control_page 來指向正常的明文頁。在 readpage 時,通過 bio 從磁盤中讀出數據到內存頁,讀頁完成后通過 queue_work 的形式調用解密流程并將明文保存在當前頁,因此 context 中存在 work 成員。另外,為了提高效率,在初始化階段一次性申請了128個 ext4_crypto_ctx 的內存空間并通過 free_list 鏈表進行管理。

清單五 用于表示加/解密 page 的 context
struct ext4_crypto_ctx {
    union {
        struct {
            struct page *bounce_page;       /* Ciphertext page */
            struct page *control_page;      /* Original page  */
        } w;
        struct {
            struct bio *bio;
            struct work_struct work;
        } r;
        struct list_head free_list;     /* Free list */
    };
    char flags;                      /* Flags */
    char mode;                       /* Encryption mode for tfm */
};
`
使能 EXT4 Encryption

Linux kernel具有良好的模塊化設計,EXT4 Encryption屬于一個EXT4 FS中一個可選的模塊,在編譯kernel前需通過配置選項使能該功能,如下:

CONFIG_EXT4_FS_SECURITY=y
CONFIG_EXT4_FS_ENCRYPTION=y
添加 master key 的流程

將 master key 添加到內核 keyring 屬于 EXT4 Encryption 的第一步,該步驟通過 add_key 系統調用完成,master key 在不同的 Linux 發行版有不同的產生及保存方法,這里以 Chrome OS 為例。

Chrome OS 在 cryptohomed 守護進程中完成 master key 的獲取和添加到 keyring。因為兼容 eCryptfs 和 EXT4 Encryption(為了跟 Chrome OS 保持一致,后續以 Dircrypto 代替 EXT4 Encryption 的稱呼),而 eCryptfs 屬于前輩,eCryptfs 通過 mount 的方式完成加密文件的開啟,為了保持一致性,cryptohomed 同樣是在 mount 的準備過程中解密出 master key 和開啟 Dircrypto,此 master key 即 eCryptfs 加密模式時用的 FEK,master key descriptor 即 FEK 的 key signature,所以本節介紹 Dircrypto 流程時所謂的 mount 流程,望讀者能夠理解,在 Dircrypto 模式下,mount 不是真正“mount”,千萬不要混淆。cryptohomed 的 mount 流程如下:

1.cryptohomed 在 D-Bus 上接收到持(包含用戶名和密碼)有效用戶證書的 mount 請求,當然 D-Bus 請求也是有權限控制的;

2.假如是用戶首次登陸,將進行:

a. 建立/home/.shadow/[salt_hash_of_username]目錄,采用 SHA1 算法和系統的 salt 對用戶名進行加密,生成salt_hash_of_username,簡稱s_h_o_u;

b. 生成vault keyset /home/.shadow/[salt_hash_of_username]/master.0/home/.shadow/[salt_hash_of_username]/master.0.sum。master.0 加密存儲了包含有 FEK 和 FNEK 的內容以及非敏感信息如 salt、password rounds 等;master.0.sum 是對 master.0 文件內容的校驗和。

3.采用通過 mount 請求傳入的用戶證書解密 keyset。當 TPM 可用時優先采用 TPM 解密,否則采用 Scrypt 庫,當 TPM 可用后再自動切換回使用 TPM。cryptohome 使用 TPM 僅僅是為了存儲密鑰,由 TPM 封存的密鑰僅能被 TPM 自身使用,這可用緩解密鑰被暴力破解,增強保護用戶隱私數據的安全。TPM 的首次初始化由 cryptohomed 完成。這里默認 TPM 可正常使用,其解密機制如下圖二所示,其中:

UP:User Passkey,用戶登錄口令

EVKK:Ecrypted vault keyset key,保存在 master.0 中的“tpm_key”字段

IEVKK:Intermediate vault keyset key,解密過程生成的中間文件,屬于EVKK的解密后產物,也是RSA解密的輸入密文

TPM_CHK: TPM-wrapped system-wide Cryptohome key,保存在/home/.shadow/cryptohome.key,TPM init時加載到TPM

VKK:Vault keyset key

VK:Vault Keyset,包含FEK和FNEK

EVK:Encrypted vault keyset,保存在master.0里”wrapped_keyset”字段

圖二 TPM解密VK的流程

圖二中的UP(由發起mount的D-Bus請求中通過key參數傳入)做為一個AES key用于解密EVKK,解密后得到的IEVKK;然后將IEVKK做為RSA的密文送入TPM,使用TPM_CHK做為密鑰進行解密,解密后得到VKK;最后生成的VKK是一個AES key,用于解密master.0里的EVK,得到包含有FEK和FNEK明文的VK。經過三層解密,終于拿到關鍵的FEK,此FEK在Dircrypto模式下當做master key使用,FEK signature即做master key descriptor使用。

最后通過 add_key 系統調用將 master key 及 master key descriptor(在 keyring 中為了方便區分,master key descriptor 由 key sign 加上前綴“ext4:”組成)添加到 keyring,如下清單六代碼所示

清單六 Chrome OS 傳入 master key 的核心代碼
key_serial_t AddKeyToKeyring(const brillo::SecureBlob& key,
                             const brillo::SecureBlob& key_descriptor) {
  //參數中的key即是master key,key_descriptor即sig
  if (key.size() > EXT4_MAX_KEY_SIZE ||
      key_descriptor.size() != EXT4_KEY_DESCRIPTOR_SIZE) {
    LOG(ERROR) << "Invalid arguments: key.size() = " << key.size()
               << "key_descriptor.size() = " << key_descriptor.size();
    return kInvalidKeySerial;
  }
  //在upstart中已經通過add_key添加dircrypt的會話keyring
  key_serial_t keyring = keyctl_search(
      KEY_SPEC_SESSION_KEYRING, "keyring", kKeyringName, 0);
  if (keyring == kInvalidKeySerial) {
    PLOG(ERROR) << "keyctl_search failed";
    return kInvalidKeySerial;
  }
  //初始化struct ext4_encryption_key
  ext4_encryption_key ext4_key = {};
  ext4_key.mode = EXT4_ENCRYPTION_MODE_AES_256_XTS;
  memcpy(ext4_key.raw, key.char_data(), key.size());
  ext4_key.size = key.size();
  //key_name就是最后的master key description,由”ext4:”+sig兩部分組成
  //kernel在request_key時同樣是將”ext4:”+sig兩部分組成master key description
  std::string key_name = kKeyNamePrefix + base::ToLowerASCII(
      base::HexEncode(key_descriptor.data(), key_descriptor.size()));
  // kKeyType是“logon”,不允許應用程序獲取密鑰的內容
  key_serial_t key_serial = add_key(kKeyType, key_name.c_str(), &ext4_key,
                                    sizeof(ext4_key), keyring);
  if (key_serial == kInvalidKeySerial) {
    PLOG(ERROR) << "Failed to insert key into keyring";
    return kInvalidKeySerial;
  }
  return key_serial;
}
`
Set Encryption Policy 流程

通過對目標目錄的文件描述符進行 ioctl 的 EXT4_IOC_SET_ENCRYPTION_POLICY 操作即完成了 EXT4 Encryption 的加/解密功能的開啟,該步驟在完成添加 master key 后進行,Chrome OS 中的相關代碼如下清單七所示,通過 struct ext4_encryption_policy 指定了策略的版本號、文件內容和文件名的加密算法、旗標、master key 的識別描述符。

清單七 Chrome OS set encryption policy 的核心代碼
bool SetDirectoryKey(const base::FilePath& dir,
                     const brillo::SecureBlob& key_descriptor) {
  DCHECK_EQ(static_cast<size_t>(EXT4_KEY_DESCRIPTOR_SIZE),
            key_descriptor.size());
  /*這里的dir代表要開啟EXT4 Encryption的目錄 */
  base::ScopedFD fd(HANDLE_EINTR(open(dir.value().c_str(),
                                      O_RDONLY | O_DIRECTORY)));
  if (!fd.is_valid()) {
    PLOG(ERROR) << "Ext4: Invalid directory" << dir.value();
    return false;
  }
   /*初始化struct ext4_encryption_policy對象 
   * 指定文件內容的加密算法是AES_256_XTS
  */
  ext4_encryption_policy policy = {};
  policy.version = 0;
  policy.contents_encryption_mode = EXT4_ENCRYPTION_MODE_AES_256_XTS;
  policy.filenames_encryption_mode = EXT4_ENCRYPTION_MODE_AES_256_CTS;
  policy.flags = 0;
  // key_descriptor即FEK 的key sig
  memcpy(policy.master_key_descriptor, key_descriptor.data(),
         EXT4_KEY_DESCRIPTOR_SIZE);
  /*通過ioctl完成設置*/
  if (ioctl(fd.get(), EXT4_IOC_SET_ENCRYPTION_POLICY, &policy) < 0) {
    PLOG(ERROR) << "Failed to set the encryption policy of " << dir.value();
    return false;
  }
  return true;
}
`

內核對EXT4_IOC_SET_ENCRYPTION_POLICY的 ioctl 在 ext4_ioctl 函數中完成響應,從應用程序中接收ext4_encryption_policy,解析其參數,若是首次對該目錄進行加密設置則生成一個ext4_encryption_context 數據結構保存包括版本號、文件內容的加密算法、文件名的加密算法、旗標、master key descriptor、nonce 在內的所有信息到目錄對應 inode 的 xattr 中。從此開始,以該目錄做為 EXT Encryption 加密的根目錄,其下文件和子目錄的除了 nonce 需要再次單獨產生外,其余加密屬性均繼承自該目錄。若非首次對該目錄進行 EXT4 Encryption 設置,則重點比較當前設置是否與先前的設置一致。首先介紹首次設置的情形, ext4_ioctl 的函數調用關系如圖三所示。

圖三 首次進行EXT4 Encryption設置的函數調用關系

應用程序進行 ioctl 系統調用經過 VFS,最終調用 ext4_ioctl 函數,借助圖三的函數調用可看到進行 EXT4 Encryption policy 設置時都進行了什么操作。首先判斷目錄所在的文件系統是否支持 EXT4 Encryption 操作,具體在ext4_has_feature_encrypt 函數中通過判斷 superblock 的 s_es->s_feature_incompat 是否支持 ENCRYPT 屬性;然后利用 copy_from_user 函數從用戶空間拷貝 ext4_encryption_policy 到內核空間;緊接著在 ext4_process_policy 函數里將 ext4_encryption_policy 轉換成 ext4_encryption_context 保存到 inode 的 attr;最后將加密目錄對應的 inode 的修改保存到磁盤。重點部分在 ext4_process_policy 函數,主要分三大步驟,第一步還是進行照例檢查校驗,包括:訪問權限、ext4_encryption_policy的版本號、目標目錄是否為空目錄、目標目錄是否已經存在ext4_encryption_context;第二步為目標目錄生成ext4_encryption_context并保存到 xattr;最后提交修改的保存請求。第一步的具體操作表現在函數操作上如下:

  • inode_owner_or_capable() 完成 DAC 方面的權限檢查
  • ext4_encryption_policy的版本號 version 進行檢查,當前僅支持版本0
  • ext4_inode_has_encryption_context()嘗試讀取目標目錄對應的 inode 的 xattr 的 EXT4 Encryption 字段”c”,看是否存在內容,若存在內容,則說明目標目錄在先前已經進行過 EXT4 Encryption 設置
  • S_ISDIR()校驗目標目錄是否真的是目錄
  • ext4_empty_dir()判斷目標目錄是否為空目錄,在首次設置 EXT4 Encryption 時,僅支持對空目錄進行操作。這點有別于 eCryptfs,eCryptfs 加密文件所在的目錄下支持非加密和加密文件的同時存在;而 EXT4 Encryption 要么是全加密,要么是全非加密。

第二步在ext4_create_encryption_context_from_policy函數中完成,具體如下:

  • ext4_convert_inline_data()對inline data做處理
  • ext4_valid_contents_enc_mode()校驗ext4_encryption_policy的文件內容加密模式是否為AES_256_XTS,當前僅支持該算法的內容加密
  • ext4_valid_filenames_enc_mode()校驗ext4_encryption_policy的文件名加密模式是否為AES_256_CTS,當前僅支持該算法的內容名加密
  • ext4_encryption_policy的 flags 做檢驗
  • get_random_bytes()產生 16 Bytes 的隨機數,賦值給ext4_encryption_context的 nonce,其他如 master key descriptor、flags、文件內容加密模式、文件名加密模式等值,從ext4_encryption_policy中獲取,完成目標目錄對應的ext4_encryption_context的初始化
  • ext4_xattr_set()將用于目標目錄的ext4_encryption_context保存到 inode 的 xattr
  • ext4_set_inode_flag()將目標目錄對應 inode 的 i_flags 設置成 EXT4_INODE_ENCRYPT,表明其屬性。后續在文件open、read、write 時通過該標志進行判斷

最后使用ext4_journal_startext4_mark_inode_dirtyext4_journal_stop等函數完成 xattr 數據回寫到磁盤的請求。

若非首次對目標目錄進行 EXT4 Encryption 設置,請流程如圖四所示,通過 ext4_xattr_get 函數讀取對應 inode 的 xattr 的EXT4 Encryption字段”c”對應的內容,即保存的 ext4_encryption_context,將其與ext4_encryption_policy的相應值進行對比,若不一致返回-EINVAL。

圖四 非首次進行EXT4 Encryption設置的函數調用關系

相比 eCryptfs,此EXT4_IOC_SET_ENCRYPTION_POLICY的 ioctl 的作用類似 eCryptfs 的“mount –t ecryptfs ”操作。

creat file 流程

creat file 流程特指應用程序通過 creat()函數或 open( , O_CREAT, )在已經通過EXT4_IOC_SET_ENCRYPTION_POLICY ioctl完成 EXT4 Encryption 設置的目錄下新建普通文件的過程。希望通過介紹該過程,可以幫助讀者了解如何創建加密文件,如何利用 master key 和 nonce 生成 derived key。

應用程序使用creat()函數通過系統調用經由VFS,在申請到fd、初始化好 nameidata 、struct file 等等之后利用ext4_create()函數完成加密文件的創建,函數調用關系如圖五所示。

創建加密文件的核心函數ext4_create()的函數調用關系如圖六所示,函數主要功能是創建 ext4 inode 節點并初始化,這里只關注 EXT4 Encryption 部分。在創建時首先判斷其所在目錄 inode 的 i_flags 是否已經被設置了EXT4_INODE_ENCRYPT屬性(該屬性在EXT4_IOC_SET_ENCRYPTION_POLICY ioctl或者在 EXT4 Encryption 根目錄下的任何地方新建目錄/文件時完成i_flags設置),若是則表明需要進行 EXT4 Encryption;接著讀取新文件所在目錄,即其父目錄的 xattr 屬性獲取到ext4_encryption_context,再為新文件生成新的 nonce,將 nonce 替換父目錄的ext4_encryption_context中的 nonce 生成用于新文件的ext4_encryption_context并保存到新文件對應 inode 的 xattr 中;然后用ext4_encryption_context中的 master key descriptor 匹配到 keyring 中的 master key,將ext4_encryption_context中的nonce做為密鑰對 master key 進行 AES-128-ECB 加密,得到 derived key;最后使用 derived key 和 AES-256-XTS 初始化 kernel crypto API,將初始化好的 tfm 保存到 ext4_crypt_info 的ci_ctfm 成員中,再將ext4_crypt_info保存到ext4_inode_infoi_crypt_info,后續對新文件進行讀寫操作時直接取出 ci_ctfm 做具體的加/解密即可。

圖五 creat 和 open file 函數調用關系

圖六 ext4_create函數調用關系

具體到圖六中ext4_create函數調用關系中各個要點函數,完成的功能如下:

  • ext4_encrypted_inode()判斷文件父目錄的 inode 的 i_flags 是否已經被設置了EXT4_INODE_ENCRYPT屬性
  • ext4_get_encryption_info()讀取父目錄的 xattr 屬性獲取到ext4_encryption_context,并為父目錄生成 derived key,初始化好 tfm 并保存到其ext4_inode_info的i_crypt_info
  • ext4_encryption_info()確認父目錄的ext4_inode_infoi_crypt_info已經初始化好
  • ext4_inherit_context()為新文件創建ext4_encryption_context并保存到其 xattr 中,并為新文件生成 derived key,初始化好 tfm 并保存到其ext4_inode_infoi_crypt_info

從上可看到ext4_get_encryption_info()ext4_inherit_context()是最關鍵的部分,其代碼如清單八和清單九所示,代碼較長,但強烈建議耐心讀完。

清單八 ext4_get_encryption_info 函數
int ext4_get_encryption_info(struct inode *inode)
{
    struct ext4_inode_info *ei = EXT4_I(inode);
    struct ext4_crypt_info *crypt_info;
    char full_key_descriptor[EXT4_KEY_DESC_PREFIX_SIZE +
                 (EXT4_KEY_DESCRIPTOR_SIZE * 2) + 1];
    struct key *keyring_key = NULL;
    struct ext4_encryption_key *master_key;
    struct ext4_encryption_context ctx;
    const struct user_key_payload *ukp;
    struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb); 
    struct crypto_ablkcipher *ctfm;
    const char *cipher_str;
    char raw_key[EXT4_MAX_KEY_SIZE];
    char mode;
    int res;
    //若ext4_inode_info中的i_crypt_info有值,說明先前已經初始化好
    if (ei->i_crypt_info)
        return 0;
    if (!ext4_read_workqueue) {
    /*為readpage時解密初始化read_workqueue,為ext4_crypto_ctx預先創建128個
    *cache,為writepage時用的bounce page創建內存池,為ext4_crypt_info創建slab
    */
        res = ext4_init_crypto();
        if (res)
            return res;
    }
    /*從xattr中讀取加密模式、master key descriptor、nonce等加密相關信息到
    *ext4_encryption_context
    */
    res = ext4_xattr_get(inode, EXT4_XATTR_INDEX_ENCRYPTION,
                 EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
                 &ctx, sizeof(ctx));
    if (res < 0) {
        if (!DUMMY_ENCRYPTION_ENABLED(sbi))
            return res;
        ctx.contents_encryption_mode = EXT4_ENCRYPTION_MODE_AES_256_XTS;
        ctx.filenames_encryption_mode =
            EXT4_ENCRYPTION_MODE_AES_256_CTS;
        ctx.flags = 0;
    } else if (res != sizeof(ctx))
        return -EINVAL;
    res = 0;
    crypt_info = kmem_cache_alloc(ext4_crypt_info_cachep, GFP_KERNEL);
    if (!crypt_info)
        return -ENOMEM;
    //根據獲取到的ext4_encryption_context內容初始化ext4_crypt_info
    crypt_info->ci_flags = ctx.flags;
    crypt_info->ci_data_mode = ctx.contents_encryption_mode;
    crypt_info->ci_filename_mode = ctx.filenames_encryption_mode;
    crypt_info->ci_ctfm = NULL;
    memcpy(crypt_info->ci_master_key, ctx.master_key_descriptor,
           sizeof(crypt_info->ci_master_key));
    if (S_ISREG(inode->i_mode))
        mode = crypt_info->ci_data_mode;
    else if (S_ISDIR(inode->i_mode) || S_ISLNK(inode->i_mode))
        mode = crypt_info->ci_filename_mode;
    else
        BUG();

    switch (mode) {
    case EXT4_ENCRYPTION_MODE_AES_256_XTS:
        cipher_str = "xts(aes)";
        break;
    case EXT4_ENCRYPTION_MODE_AES_256_CTS:
        cipher_str = "cts(cbc(aes))";
        break;
    default:
        printk_once(KERN_WARNING
                "ext4: unsupported key mode %d (ino %u)\n",
                mode, (unsigned) inode->i_ino);
        res = -ENOKEY;
        goto out;
    }
    if (DUMMY_ENCRYPTION_ENABLED(sbi)) {
        memset(raw_key, 0x42, EXT4_AES_256_XTS_KEY_SIZE);
        goto got_key;
    }
    //實際使用時將master key descriptor加上”ext4:”的前綴用于匹配master key
    memcpy(full_key_descriptor, EXT4_KEY_DESC_PREFIX,
           EXT4_KEY_DESC_PREFIX_SIZE);
    sprintf(full_key_descriptor + EXT4_KEY_DESC_PREFIX_SIZE,
        "%*phN", EXT4_KEY_DESCRIPTOR_SIZE,
        ctx.master_key_descriptor);
    full_key_descriptor[EXT4_KEY_DESC_PREFIX_SIZE +
                (2 * EXT4_KEY_DESCRIPTOR_SIZE)] = '\0';
    //使用master key descriptor為匹配條件向keyring申請master key
    keyring_key = request_key(&key_type_logon, full_key_descriptor, NULL);
    if (IS_ERR(keyring_key)) {
        res = PTR_ERR(keyring_key);
        keyring_key = NULL;
        goto out;
    }

    //確保master key的type是logon類型,防止應用程序讀取到key的內容
    if (keyring_key->type != &key_type_logon) {
        printk_once(KERN_WARNING
                "ext4: key type must be logon\n");
        res = -ENOKEY;
        goto out;
    }
    down_read(&keyring_key->sem);
    //從keyring中取出master key的payload
    ukp = user_key_payload(keyring_key);
    if (ukp->datalen != sizeof(struct ext4_encryption_key)) {
        res = -EINVAL;
        up_read(&keyring_key->sem);
        goto out;
    }
    //取出master key的有效數據ext4_encryption_key
    master_key = (struct ext4_encryption_key *)ukp->data;
    BUILD_BUG_ON(EXT4_AES_128_ECB_KEY_SIZE !=
             EXT4_KEY_DERIVATION_NONCE_SIZE);
    if (master_key->size != EXT4_AES_256_XTS_KEY_SIZE) {
        printk_once(KERN_WARNING
                "ext4: key size incorrect: %d\n",
                master_key->size);
        res = -ENOKEY;
        up_read(&keyring_key->sem);
        goto out;
    }
    /*以nonce做為密鑰,采用AES_128_ECB算法,利用kernel crypto API加密master
    * key(master_key->raw),生成derived key保存在raw_key里
    */
    res = ext4_derive_key_aes(ctx.nonce, master_key->raw,
                  raw_key);
    up_read(&keyring_key->sem);
    if (res)
        goto out;
got_key:
    //為AES_256_XTS加密算法申請tfm
    ctfm = crypto_alloc_ablkcipher(cipher_str, 0, 0);
    if (!ctfm || IS_ERR(ctfm)) {
        res = ctfm ? PTR_ERR(ctfm) : -ENOMEM;
        printk(KERN_DEBUG
               "%s: error %d (inode %u) allocating crypto tfm\n",
               __func__, res, (unsigned) inode->i_ino);
        goto out;
    }
    crypt_info->ci_ctfm = ctfm;
    crypto_ablkcipher_clear_flags(ctfm, ~0);
    crypto_tfm_set_flags(crypto_ablkcipher_tfm(ctfm),
                 CRYPTO_TFM_REQ_WEAK_KEY);
    //向kernel crypto接口里設置加密用的key為derived key
    res = crypto_ablkcipher_setkey(ctfm, raw_key,
                       ext4_encryption_key_size(mode));
    if (res)
        goto out;
    /*將初始化好的ext4_crypt_info 實例crypt_info拷貝到inode的ext4_inode_info 的*i_crypt_info。
    *后續加/解密文件內容時直接取出ext4_inode_info的i_crypt_info,即可從中獲取
    *到已經初始化好的tfm接口c_ctfm,用其直接加/解密
    */
    if (cmpxchg(&ei->i_crypt_info, NULL, crypt_info) == NULL)
        crypt_info = NULL;
out:
    if (res == -ENOKEY)
        res = 0;
    key_put(keyring_key);
    ext4_free_crypt_info(crypt_info);
    memzero_explicit(raw_key, sizeof(raw_key));
    return res;
}
`
清單九 ext4_inherit_context 函數
int ext4_inherit_context(struct inode *parent, struct inode *child)
{
    struct ext4_encryption_context ctx;
    struct ext4_crypt_info *ci;
    int res;
    //確保其父目錄inode對應的i_crypt_info已經初始化好
    res = ext4_get_encryption_info(parent);
    if (res < 0)
        return res;
    //獲取父目錄的保存在i_crypt_info的ext4_crypt_info信息
    ci = EXT4_I(parent)->i_crypt_info;
    if (ci == NULL)
        return -ENOKEY;
    ctx.format = EXT4_ENCRYPTION_CONTEXT_FORMAT_V1;
    if (DUMMY_ENCRYPTION_ENABLED(EXT4_SB(parent->i_sb))) {
        ctx.contents_encryption_mode = EXT4_ENCRYPTION_MODE_AES_256_XTS;
        ctx.filenames_encryption_mode =
            EXT4_ENCRYPTION_MODE_AES_256_CTS;
        ctx.flags = 0;
        memset(ctx.master_key_descriptor, 0x42,
               EXT4_KEY_DESCRIPTOR_SIZE);
        res = 0;
    } else {
    /*使用父目錄的文件內容加密模式、文件名加密模式、master key descriptor、flags
    *初始化新文件的ext4_encryption_context
    */
        ctx.contents_encryption_mode = ci->ci_data_mode;
        ctx.filenames_encryption_mode = ci->ci_filename_mode;
        ctx.flags = ci->ci_flags;
        memcpy(ctx.master_key_descriptor, ci->ci_master_key,
               EXT4_KEY_DESCRIPTOR_SIZE);
    }
    //產生16 bytes的隨機數做為新文件的nonce
    get_random_bytes(ctx.nonce, EXT4_KEY_DERIVATION_NONCE_SIZE);

    //將初始化好的新文件的ext4_encryption_context保存到attr中
    res = ext4_xattr_set(child, EXT4_XATTR_INDEX_ENCRYPTION,
                 EXT4_XATTR_NAME_ENCRYPTION_CONTEXT, &ctx,
                 sizeof(ctx), 0);
    if (!res) {
        //設置新文件的inode的i_flags為EXT4_INODE_ENCRYPT
        ext4_set_inode_flag(child, EXT4_INODE_ENCRYPT);
        ext4_clear_inode_state(child, EXT4_STATE_MAY_INLINE_DATA);
        /*為新文件初始化好其inode對應的i_crypt_info,主要是完成其tfm的初始化
        *為后續的讀寫文件時調用kernel crypto進行加/解密做好準備
        */
        res = ext4_get_encryption_info(child);
    }
    return res;
}
`

簡單的說,creat 時完成兩件事:一是創建ext4_encryption_context保存到文件的 xattr;二是初始化好ext4_crypt_info 保存到 inode 的 i_crypt_info,后續使用時取出 tfm,利用 kernel crypto API 即完成了加/解密工作。

open file 流程

這里 open file 特指打開已存在的 EXT4 Encryption 加密文件。僅加密部分而言,該過程相比 creat 少了創建ext4_encryption_context保存到文件的 xattr 的操作,其余部分基本一致。從應用程序調用open()函數開始到最終調用到ext4_file_open()函數的函數調用關系如上圖五所示。本節主要描述ext4_file_open()函數,其函數調用關系如圖七。

圖七 ext4_file_open函數調用關系

圖七所示各函數主要完成的功能如下:

  • ext4_encrypted_inode() 判斷欲打開文件對應 inode 的 i_flags 是否設置成EXT4_INODE_ENCRYPT,若是,表明是加密文件
  • ext4_get_encryption_info() 從文件 inode 的 xattr 取出文件加密算法、文件名加密算法、master key descriptor、 隨機密鑰 nonce;之后生成加密文件內容使用的密鑰 derived key 并初始化好 kernel crypto 接口 tfm,將其以ext4_crypt_info 形式保存到 inode 的i_crypt_info。詳細代碼見清單八
  • ext4_encryption_info()確保文件對應inode在內存中的表示ext4_inode_info中的i_crypt_info已經做好初始化
  • ext4_encrypted_inode(dir)判斷判斷欲打開文件的父目錄inode的i_flags是否設置成EXT4_INODE_ENCRYPT
  • ext4_is_child_context_consistent_with_parent()判斷文件和其父目錄的加密 context 是否一致,關鍵是 master key descriptor 是否一致
  • dquost_file_open() 調用通用的文件打開函數完成其余的操作

簡單的說就是在 open file 的時候完成文件加/解密所需的所有 context。

read file 流程

加密文件的解密工作主要是在 read 的時候進行。正常的 Linux read 支持 Buffered I/O 和 Direct I/O 兩種模式,Buffered I/O利用內核的 page cache 機制,而 Direct I/O 需要應用程序自身準備和處理cache,當前版本的 EXT4 Encryption 不支持Direct I/O,其文件內容解密工作都在 page cache 中完成。自應用程序發起 read 操作到 kernel 對文件內容進行解密的函數調用關系如圖八所示。

圖八 read 加密文件的函數調用關系

ext4 文件讀的主要實現在 ext4_readpage 函數,文件內容的 AES-256-XTS 解密理所當然也在該函數里,這里主要介紹文件內容解密部分,其函數調用關系如圖九所示。ext4 讀寫通過bio進行封裝,描述塊數據傳送時怎樣進行填充或讀取塊給 driver,包括描述磁盤和內存的位置,其內部有一個函數指針bi_end_io,當讀取完成時會回調該函數,如圖九所示,ext4 將 bi_end_io 賦值為mpage_end_iompage_end_io通過 queue_work 的形式調用 completion_pages 函數,在該函數中再調用 ext4_decrypt 函數完成page的解密。ext4_decrypt函數的代碼非常簡單,如清單十所示。核心的加密和解密函數都在ext4_page_crypto()中完成,因為在open file的時候已經初始化好了 kernel crypto 接口,所以這里主要傳入表明是加密還是解密的參數以及密文頁和明文頁地址,代碼比較簡單,如清單十一所示。

圖九 ext4_readpage函數調用關系

清單十 ext4_decrypt 函數
int ext4_decrypt(struct page *page)
{
    BUG_ON(!PageLocked(page));
    return ext4_page_crypto(page->mapping->host, EXT4_DECRYPT,
                page->index, page, page, GFP_NOFS);
}
`
清單十一 ext4_page_crypto 函數
static int ext4_page_crypto(struct inode *inode, ext4_direction_t rw, pgoff_t index, struct page *src_page,
                struct page *dest_page, gfp_t gfp_flags) {
    u8 xts_tweak[EXT4_XTS_TWEAK_SIZE];
    struct ablkcipher_request *req = NULL;
    DECLARE_EXT4_COMPLETION_RESULT(ecr);
    struct scatterlist dst, src;
    struct ext4_crypt_info *ci = EXT4_I(inode)->i_crypt_info;
    struct crypto_ablkcipher *tfm = ci->ci_ctfm; //取出open時初始化好的tfm
    int res = 0;
    req = ablkcipher_request_alloc(tfm, gfp_flags);
    if (!req) {
        printk_ratelimited(KERN_ERR "%s: crypto_request_alloc() failed\n", __func__);
        return -ENOMEM;
    }
    ablkcipher_request_set_callback(
        req, CRYPTO_TFM_REQ_MAY_BACKLOG | CRYPTO_TFM_REQ_MAY_SLEEP,
        ext4_crypt_complete, &ecr);
    BUILD_BUG_ON(EXT4_XTS_TWEAK_SIZE < sizeof(index));
    memcpy(xts_tweak, &index, sizeof(index));
    memset(&xts_tweak[sizeof(index)], 0, EXT4_XTS_TWEAK_SIZE - sizeof(index));
    sg_init_table(&dst, 1);
    sg_set_page(&dst, dest_page, PAGE_CACHE_SIZE, 0);
    sg_init_table(&src, 1);
    sg_set_page(&src, src_page, PAGE_CACHE_SIZE, 0);
    ablkcipher_request_set_crypt(req, &src, &dst, PAGE_CACHE_SIZE, xts_tweak);
    if (rw == EXT4_DECRYPT)
        res = crypto_ablkcipher_decrypt(req);
    else
        res = crypto_ablkcipher_encrypt(req);
    if (res == -EINPROGRESS || res == -EBUSY) {
        wait_for_completion(&ecr.completion);
        res = ecr.res;
    }
    ablkcipher_request_free(req);
    if (res) {
        printk_ratelimited( KERN_ERR "%s: crypto_ablkcipher_encrypt() returned %d\n", __func__, res);
        return res;
    }
    return 0;
}
`
write file 流程

在寫入文件的時候會首先將 page cache 中的文件明文內容進行 AES-256-XTS 加密,再通過bio寫入磁盤,該工作主要在ext4_writepage()函數中完成,這里主要關注 EXT4 Encryption 部分,其函數調用關系如圖十所示。

圖十 ext4_writepage函數調用關系

圖十中,首先照例通過ext4_encrypted_inode()函數利用 i_flags 是否等于 EXT4_INODE_ENCRYPT 來判斷是否是加密文件;然后使用 ext4_encrypt() 函數申請新的內存頁用于保存密文,完成內容的加密,具體代碼見清單十二,函數返回密文頁的地址保存在data_page變量;緊著通過io_submit_add_bh()封裝寫入 buffer 頁到磁盤的請求,這里通過判斷 data_page 頁是否空來決定是寫入明文頁還是密文頁,巧妙的兼容了加密和非加密兩種模式;最后通過ext4_io_submit()提交 bio 寫盤請求。

清單十二 ext4_encrypt 函數
struct page *ext4_encrypt(struct inode *inode,
              struct page *plaintext_page,
              gfp_t gfp_flags)
{
    struct ext4_crypto_ctx *ctx;
    struct page *ciphertext_page = NULL;
    int err;
    BUG_ON(!PageLocked(plaintext_page));
    //從cache中獲取一個ext4_crypto_ctx內存空間
    ctx = ext4_get_crypto_ctx(inode, gfp_flags);
    if (IS_ERR(ctx))
        return (struct page *) ctx;
    //從內存池中申請一個內存頁,命名為bounce page,用于保存密文內容,同時將
    //ext4_crypto_ctx的w.bounce_page指向該bounce page
    /* The encryption operation will require a bounce page. */
    ciphertext_page = alloc_bounce_page(ctx, gfp_flags);
    if (IS_ERR(ciphertext_page))
        goto errout;
    ctx->w.control_page = plaintext_page;
    //調用kernel crypto加密,將密文保存在bounce page
    err = ext4_page_crypto(inode, EXT4_ENCRYPT, plaintext_page->index,
                   plaintext_page, ciphertext_page, gfp_flags);
    if (err) {
        ciphertext_page = ERR_PTR(err);
    errout:
        ext4_release_crypto_ctx(ctx);
        return ciphertext_page;
    }
    SetPagePrivate(ciphertext_page);
    set_page_private(ciphertext_page, (unsigned long)ctx);
    lock_page(ciphertext_page);
    //返回密文頁bounce page地址
    return ciphertext_page;
}
`

因為在 open file 的時候已經初始化好了 kernel crypto 所需的加密算法、密鑰設置,并保存了 tfm 到文件 inode 的內存表示ext4_inode_info的成員 i_crypt_info 中,所以在 readpage/writepage 時進行加/解密的操作變得很簡單。

結語

與eCryptfs類似,EXT4 Encryption建立在內核安全可信的基礎上,核心安全組件是master key,若內核被攻破導致密鑰泄露,EXT4 Encryption的安全性將失效。同樣需要注意page cache中的明文頁有可能被交換到磁盤的swap區。早期版本的Chrome OS禁用了swap功能,當前版本的swap采取的是zram機制,與傳統的磁盤swap有本質區別。相比eCryptfs做為一個獨立的內核加密模塊,現在EXT4 Encryption原生的存在于EXT4文件系統中,在使用的便利性和性能上都優于eCryptfs,相信推廣將會變得更加迅速。

參考資料


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/433/