來源:IceSword Lab
作者:suezi@IceSword Lab

一、概述

Chromebook 的使用場景模式是允許多人分享使用同一臺設備,但是同時也要保護每個用戶數據的私密性,使得每個使用者都不允許訪問到對方的隱私數據,包括:賬戶信息、瀏覽歷史記錄和 cache、安裝的應用程序、下載的內容以及用戶自主在本地產生的文本、圖片、視頻等。本文試圖從較高的角度闡述 ChromeOS 是如何通過 eCryptfs 機制保護用戶數據隱私。

二、eCryptfs 簡介

eCryptfs 在 Linux kernel 2.6.19 由 IBM 公司的 Halcrow,Thompson 等人引入,在 Cryptfs 的基礎上實現,用于企業級的文件系統加密,支持文件名和文件內容的加密。本質上 eCryptfs 就像是一個內核版本的 Pretty Good Privacy(PGP)服務,插在 VFS和下層物理文件系統之間,充當一個“過濾器”的角色。用戶應用程序對加密文件的寫請求,經系統調用層到達 VFS 層,VFS 轉給 eCryptfs 文件系統組件處理,處理完畢后,再轉給下層物理文件系統;讀請求流程則相反。

eCryptfs 的設計受到 OpenPGP 規范的影響,核心思想:eCryptfs 通過一種對稱密鑰加密算法來加密文件的內容或文件名,如 AES-128,密鑰 FEK(File Encryption Key)隨機產生。而 FEK 通過用戶口令或者公鑰進行保護,加密后的 FEK 稱EFEK(Encrypted File Encryption Key),口令/公鑰稱為 FEFEK(File Encryption Key Encryption Key)。在保存文件時,將包含有 EFEK、加密算法等信息的元數據(metadata)放置在文件的頭部或者 xattr 擴展屬性里(本文默認以前者做為講解),打開文件前再解析 metadata。

圖一 eCryptfs的系統架構

eCryptfs 的系統架構如圖一所示,eCryptfs 堆疊在 EXT4 文件系統之上,工作時需要用戶程序和內核同時配合,用戶程序主要負責獲取密鑰并通過(add_key/keyctl/request_key)系統調用傳送到內核的 keyring,當某個應用程序發起對文件的讀寫操作前,由 eCryptfs 對其進行加/解密,加/解密的過程中需要調用 Kernel 的 Crypto API(AES/DES etc)來完成。以對目錄 eCryptfs-test 進行加密為例,為方便起見,在 Ubuntu 系統下測試eCryptfs的建立流程,如圖二所示,通過mount指令發起eCryptfs的建立流程,然后在用戶應用程序 eCryptfs-utils 的輔助下輸入用于加密FEK的用戶口令及選擇加密算法等,完成掛載后意味著已經開始對測試目錄 eCryptfs-test 的所有內容進行加密處理。測試中在 eCryptfs-test 目錄下增加需要加密的文件或目錄的內容,當用戶 umount 退出對 eCryptfs-test 目錄的掛載后再次查看該目錄時,發現包括文件名和文件內容都進行了加密,如圖三所示。

圖二 eCryptfs使用時的建立流程

圖三 eCryptfs加密后的文件

圖四 eCryptfs對文件的加解密流程

實現上,eCryptfs 對數據的加/解密流程如圖四所示,對稱密鑰加密算法以塊為單位進行加密/解密,如AES-128。eCryptfs 將加密文件分成多個邏輯塊,稱為 extent,extent 的大小可調,但是不能大于實際物理頁,默認值等于物理頁的大小,如32位的系統下是 4096 字節。加密文件的頭部存放元數據,包括元數據長度、標志位、旗標、EFEK 及相應的 signature,目前元數據的最小長度為 8192 字節。加/解密開始前,首先解密FEKEK取出FEK。當讀入一個 extent 中的任何部分的密文時,整個 extent 被讀入 Page Cache,通過 Kernel Crypto API 進行解密;當 extent 中的任何部分的明文數據被寫回磁盤時,需要加密并寫回整個 extent。

三、eCryptfs 詳述

eCryptfs 在內核中的實現代碼位于 kernel/fs/ecryptfs,下面以 eCryptfs 使用到的關鍵數據結構、eCryptfs init、eCryptfs mount、file creat、file open、file read、file write 的順序分別介紹eCryptfs是如何工作。另外,eCryptfs 還實現了/dev/ecryptfs的 misc 設備,用于內核與應用程序間的消息傳遞,如密鑰請求與響應,屬于非必選項,因此這里不對其進行介紹。

eCryptfs 相關的數據結構

eCryptfs 關鍵的數據結構包括 eCryptfs 文件系統相關 file、dentry、inode、superblock、file_system_type 描述、auth token 認證令牌描述、eCryptfs 加密信息描述等。

eCryptfs 文件系統相關的數據結構如清單一所示,下文將會重點介紹 file_system_type 中的 mount 函數,即 ecryptfs_mount。

清單一 eCryptfs文件系統相關的數據結構
/* ecryptfs file_system_type */
static struct file_system_type ecryptfs_fs_type = {
    .owner = THIS_MODULE,
    .name = "ecryptfs",
    .mount = ecryptfs_mount,
    .kill_sb = ecryptfs_kill_block_super,
    .fs_flags = 0
};
/* superblock private data. */
struct ecryptfs_sb_info {
    struct super_block *wsi_sb;
    struct ecryptfs_mount_crypt_stat mount_crypt_stat;
    struct backing_dev_info bdi;
};
/* inode private data. */
struct ecryptfs_inode_info {
    struct inode vfs_inode;
    struct inode *wii_inode;
    struct mutex lower_file_mutex;
    atomic_t lower_file_count;
    struct file *lower_file;
    struct ecryptfs_crypt_stat crypt_stat;
};
/* dentry private data. Each dentry must keep track of a lower vfsmount too. */
struct ecryptfs_dentry_info {
    struct path lower_path;
    union {
        struct ecryptfs_crypt_stat *crypt_stat;
        struct rcu_head rcu;
    };
};
/* file private data. */
struct ecryptfs_file_info {
    struct file *wfi_file;
    struct ecryptfs_crypt_stat *crypt_stat;
};

eCryptfs 支持對文件名(包括目錄名)進行加密,因此特意使用了 struct ecryptfs_filename 的結構封裝文件名,如清單二所示。

清單二 文件名的數據結構
struct ecryptfs_filename {
    struct list_head crypt_stat_list;
    u32 flags;
    u32 seq_no;
    char *filename;
    char *encrypted_filename;
    size_t filename_size;
    size_t encrypted_filename_size;
    char fnek_sig[ECRYPTFS_SIG_SIZE_HEX];
    char dentry_name[ECRYPTFS_ENCRYPTED_DENTRY_NAME_LEN + 1];
};

struct ecryptfs_auth_tok用于記錄認證令牌信息,包括用戶口令和非對稱加密兩種類型,每種類型都包含有密鑰的簽名,用戶口令類型還包含有算法類型和加鹽值等,如清單三所示。為了方便管理,使用時統一將其保存在struct ecryptfs_auth_tok_list_item鏈表中。

清單三 認證令牌信息的數據結構
struct ecryptfs_auth_tok {
    u16 version; /* 8-bit major and 8-bit minor */
    u16 token_type;
    u32 flags;
    struct ecryptfs_session_key session_key;
    u8 reserved[32];
    union {
        struct ecryptfs_password password;  //用戶口令類型
        struct ecryptfs_private_key private_key; //非對稱加密類型
    } token;
}
struct ecryptfs_password {
    u32 password_bytes;
    s32 hash_algo;
    u32 hash_iterations;
    u32 session_key_encryption_key_bytes;
    u32 flags;
    /* Iterated-hash concatenation of salt and passphrase */
    u8 session_key_encryption_key[ECRYPTFS_MAX_KEY_BYTES];
    u8 signature[ECRYPTFS_PASSWORD_SIG_SIZE + 1];
    /* Always in expanded hex */
    u8 salt[ECRYPTFS_SALT_SIZE];
};
struct ecryptfs_private_key {
    u32 key_size;
    u32 data_len;
    u8 signature[ECRYPTFS_PASSWORD_SIG_SIZE + 1];
    char pki_type[ECRYPTFS_MAX_PKI_NAME_BYTES + 1];
    u8 data[];
};

eCryptfs 在 mount 時會傳入全局加解密用到密鑰、算法相關數據,并將其保存在 struct ecryptfs_mount_crypt_stat,如清單四所示

清單四 mount 時傳入的密鑰相關數據結構
struct ecryptfs_mount_crypt_stat {
    u32 flags;
    struct list_head global_auth_tok_list;
    struct mutex global_auth_tok_list_mutex;
    size_t global_default_cipher_key_size;
    size_t global_default_fn_cipher_key_bytes;
    unsigned char global_default_cipher_name[ECRYPTFS_MAX_CIPHER_NAME_SIZE + 1];
    unsigned char global_default_fn_cipher_name[
        ECRYPTFS_MAX_CIPHER_NAME_SIZE + 1];
    char global_default_fnek_sig[ECRYPTFS_SIG_SIZE_HEX + 1];
};

eCryptfs 讀寫文件時首先需要進行加/解密,此時使用的密鑰相關數據保存在struct ecryptfs_crypt_stat結構中,其具體數值在 open 時初始化,部分從 mount 時的 ecryptfs_mount_crypt_stat 復制過來,部分從分析加密文件的 metadata 獲取,該數據結構比較關鍵,貫穿 eCryptfs 的文件 open、read、write、close 等流程,如清單五所示。

清單五 ecryptfs_crypt_stat 數據結構
struct ecryptfs_crypt_stat {
    u32 flags;
    unsigned int file_version;
    size_t iv_bytes;
    size_t metadata_size;
    size_t extent_size; /* Data extent size; default is 4096 */
    size_t key_size;
    size_t extent_shift;
    unsigned int extent_mask;
    struct ecryptfs_mount_crypt_stat *mount_crypt_stat;
    struct crypto_ablkcipher *tfm;
    struct crypto_hash *hash_tfm; /* Crypto context for generating
                       * the initialization vectors */
    unsigned char cipher[ECRYPTFS_MAX_CIPHER_NAME_SIZE + 1];
    unsigned char key[ECRYPTFS_MAX_KEY_BYTES];
    unsigned char root_iv[ECRYPTFS_MAX_IV_BYTES];
    struct list_head keysig_list;
    struct mutex keysig_list_mutex;
    struct mutex cs_tfm_mutex;
    struct mutex cs_hash_tfm_mutex;
    struct mutex cs_mutex;
};
eCryptfs init過程

使用 eCryptfs 前,首先需要通過內核的配置選項“CONFIG_ECRYPT_FS=y”使能 eCryptfs,因為加解密時使用到內核的 crypto 和 keystore 接口,所以要確保“CONFIG_CRYPTO=y”,“CONFIG_KEYS=y”,“CONFIG_ENCRYPTED_KEYS=y”,同時使能相應的加解密算法,如 AES 等。重新編譯內核啟動后會自動注冊 eCryptfs,其 init 的代碼如清單六所示。

清單六 eCryptfs init 過程
static int __init ecryptfs_init(void)
{
    int rc;
    //eCryptfs的extent size不能大于page size
    if (ECRYPTFS_DEFAULT_EXTENT_SIZE > PAGE_CACHE_SIZE) {
        rc = -EINVAL;  ecryptfs_printk(KERN_ERR,…);     goto out;
    }
    /*為上文列舉到的eCryptfs重要的數據結構對象申請內存,如eCryptfs的auth token、superblock、inode、dentry、file、key    等*/
    rc = ecryptfs_init_kmem_caches(); 
    …
    //建立sysfs接口,該接口中的version各bit分別代表eCryptfs支持的能力和屬性
    rc = do_sysfs_registration(); 
    …
    //建立kthread,為后續eCryptfs讀寫lower file時能借助內核函數得到rw的權限
    rc = ecryptfs_init_kthread();
    …
    //在chromeos中該函數為空,直接返回0 
    rc = ecryptfs_init_messaging();
    …
    //初始化kernel crypto
    rc = ecryptfs_init_crypto();
    …
    //注冊eCryptfs文件系統
    rc = register_filesystem(&ecryptfs_fs_type);
    …
    return rc;
}
eCryptfs mount 過程

在使能了 eCryptfs 的內核,當用戶在應用層下發“mount –t ecryptfs src dst options”指令時觸發執行上文清單一中的 ecryptfs_mount 函數進行文件系統的掛載安裝并初始化 auth token,成功執行后完成對 src 目錄的 eCryptfs 屬性的指定,eCryptfs 開始正常工作,此后任何在src目錄下新建的文件都會被自動加密處理,若之前該目錄已有加密文件,此時會被自動解密。

ecryptfs_mount 涉及的代碼比較多,篇幅有限,化繁為簡,函數調用關系如圖五所示。

圖五 eCryptfs mount的函數調用關系圖

從圖五可看到mount時首先利用函數ecryptfs_parse_options()對傳入的option參數做解析,完成了如下事項:

  1. 調用函數ecryptfs_init_mount_crypt_stat()初始化用于保存 auth token 相關的 struct ecryptfs_mount_crypt_stat 對象;

  2. 調用函數ecryptfs_add_global_auth_tok()將從 option 傳入的分別用于 FEK 和 FNEK(File Name Encryption Key,用于文件名加解密)的 auth token的 signature 保存到 struct ecryptfs_mount_crypt_stat 對象;

  3. 分析 option 傳入參數,初始化 struct ecryptfs_mount_crypt_stat 對象的成員,如 global_default_cipher_nameglobal_default_cipher_key_sizeflags、global_default_fnek_sigglobal_default_fn_cipher_nameglobal_default_fn_cipher_key_bytes等;

  4. 調用函數ecryptfs_add_new_key_tfm()針對 FEK 和 FNEK 的加密算法分別初始化相應的 kernel crypto tfm 接口;

  5. 調用函數ecryptfs_init_global_auth_toks()將解析 option 后得到 key sign 做為參數利用 keyring 的 request_key 接口獲取上層應用傳入的 auth token,并將 auth token 添加入 struct ecryptfs_mount_crypt_stat 的全局鏈表中,供后續使用。

接著為 eCryptfs 創建 superblock 對象并初始化,具體如下:通過函數 sget()創建 eCryptfs 類型的 superblock;調用bdi_setup_and_register()函數為 eCryptfs 的 ecryptfs_sb_info 對象初始化及注冊數據的回寫設備bdi;初始化 eCryptfs superblock 對象的各成員,如 s_fs_info、s_bdi、s_op、s_d_op 等;然后獲取當前掛載點的 path 并判斷是否已經是 eCryptfs,同時對執行者的權限做出判斷;再通過 ecryptfs_set_superblock_lower()函數將 eCryptfs 的 superblock 和當前掛載點上底層文件系統對應的 VFS superblock 產生映射關系;根據傳入的 mount option 參數及VFS映射點 superblock 的值初始化 eCryptfs superblock 對象flag成員,如關鍵的 MS_RDONLY 屬性;根據 VFS 映射點 superblock 的值初始化 eCryptfs superblock 對象的其他成員 ,如 s_maxbytes、s_blocksize、s_stack_depth;最后設置 superblock 對象的 s_magic 為 ECRYPTFS_SUPER_MAGIC。這可看出 eCryptfs 在 Linux kernel 的系統架構中,其依賴于 VFS 并處于 VFS 之下層,實際文件系統之上層。

下一步到創建 eCryptfs 的 inode 并初始化,相應工作通過函數ecryptfs_get_inode()完成,具體包括:首先獲取當前掛載點對應的 VFS 的 inode;然后調用函數 iget5_locked() 在掛載的fs中獲取或創建一個 eCryptfs 的 inode,并將該 inode 與掛載點對應的 VFS 的 inode 建立映射關系,與 superblock 類似,eCryptfs 的 inode 對象的部分初始值從其映射的 VFS inode 中拷貝,inode operation 由函數 ecryptfs_inode_set() 發起初始化,根據inode是符號鏈接還是目錄文件還是普通文件分別進行不同的i_op 賦值,如ecryptfs_symlink_iops/ecryptfs_dir_iops/ecryptfs_main_iops;同時對 i_fop file_operations 進行賦值,如ecryptfs_dir_fops/ecryptfs_main_fops

然后調用d_make_root()函數為之前創建的 superblock 設置 eCryptfs 的根目錄 s_root。

最后通過ecryptfs_set_dentry_private()函數為 eCryptfs 設置 dentry。

加密文件creat過程

creat 過程特指應用層通過 creat 系統調用創建一個新的加密文件的流程。以應用程序通過 creat() 函數在以 eCryptfs 掛載的目錄下創建加密文件為例,其函數調用流程如圖六所示,creat()通過系統調用進入 VFS,后經過層層函數調用,最終調用到 eCryptfs 層的ecryptfs_create()函數,該部分不屬于 eCryptfs 的重點,不詳述。

圖六 create經由VFS調用ecryptfs_create的流程

圖七 eCryptfs創建加密文件的函數調用過程

eCryptfs 層通過 ecryptfs_create() 函數完成最終的加密文件的創建,關鍵代碼的調用流程如圖七所示,以代碼做為視圖,分為三大步驟:

1、通過 ecryptfs_do_create() 函數創建 eCryptfs 文件的 inode 并初始化;

2、通過函數ecryptfs_initialize_file()將新創建的文件初始化成 eCryptfs 加密文件的格式,添加入諸如加密算法、密鑰信息等,為后續的讀寫操作初始化好 crypto 接口;

3、通過d_instantiate()函數將步驟一生成的 inode 信息初始化相應的 dentry。具體如下:

一.為新文件創建 inode

首先借助ecryptfs_dentry_to_lower()函數根據 eCryptfs 和底層文件系統(在 chromeos 里就是 ext4)的映射關系獲取到底層文件系統的 dentry 值。然后調用vfs_create()函數在底層文件系統上創建inode,緊接著利用__ecryptfs_get_inode()函數創建 eCryptfs 的 inode 對象并初始化以及建立其與底層文件系統inode間的映射關系,之后通過fsstack_copy_attr_times()fsstack_copy_inode_size()函數利用底層文件系統的 inode 對象的值初始化 eCryptfs inode 的相應值。

二.初始化 eCryptfs 新文件

經過步驟一完成了在底層文件系統上新建了文件,現在通過函數ecryptfs_initialize_file()將該文件設置成 eCryptfs 加密文件的格式。

  1. ecryptfs_new_file_context()函數完成初始化文件的 context,主要包括加密算法 cipher、auth token、生成針對文件加密的隨機密鑰等,這里使用的關鍵數據結構是 struct ecryptfs_crypt_stat,具體如清單五所示,初始化文件的 context 基本可以看成是初始化struct ecryptfs_crypt_stat對象,該對象的 cipher、auth token、key sign 等值從 mount eCryptfs 傳入的 option 并保存在struct ecryptfs_mount_crypt_stat (詳見清單四)對象中獲取。具體是:首先由ecryptfs_set_default_crypt_stat_vals()函數完成 flags、extent_size、metadata_size、cipher、key_size、file_version、mount_crypt_stat 等 ecryptfs_crypt_stat對象的缺省值設置;然后再通過ecryptfs_copy_mount_wide_flags_to_inode_flags()函數根據mount時設置的ecryptfs_mount_crypt_stat的 flags 重新設置 ecryptfs_crypt_stat 對象 flags;接著由ecryptfs_copy_mount_wide_sigs_to_inode_sigs()函數將 mount 時保存的 key sign 賦值給 ecryptfs_crypt_stat 對象的 keysig_list 中的節點對象中的 keysig;然后繼續將ecryptfs_mount_crypt_stat的 cipher、key_size 等值賦給 ecryptfs_crypt_stat 對象中的相應值;再調用函數ecryptfs_generate_new_key()生成 key 并保存到 ecryptfs_crypt_stat 對象的 key;最后通過ecryptfs_init_crypt_ctx() 函數完成 kernel crypto context 的初始化,如 tfm,為后續的寫操作時的加密做好準備。

  2. ecryptfs_get_lower_file()通過調用底層文件系統的接口打開文件,需要注意的是ecryptfs_privileged_open(),該函數喚醒了上文清單六提到 kthread,借助該內核線程,eCryptfs 巧妙避開了底層文件的讀寫權限的限制。

  3. ecryptfs_write_metadata() 完成關鍵的寫入 eCryptfs 文件格式到新創建的文件中。

關鍵函數ecryptfs_write_headers_virt()的代碼如清單七所示,eCryptfs 保存格式如清單七的注釋(也可參考上文的圖四),其格式傳承自OpenPGP,最后在ecryptfs_generate_key_packet_set()完成 EFEK 的生成,并根據 token_type 的類型是 ECRYPTFS_PASSWORD 還是 ECRYPTFS_PRIVATE_KEY 生成不同的 OpenPGP 的 Tag,之后保存到 eCryptfs 文件頭部 bytes 26 開始的地方。這里以 ECRYPTFS_PASSWORD 為例,因此 bytes 26 地址起存放的內容是 Tag3 和 Tag11,對應著 EFEK 和 Key sign。否則保存的是 Tag1,即 EFEK。Tag3 或 Tag1 的具體定義詳見 OpenPGP 的描述文檔 RFC2440.

之后將生成的 eCryptfs 文件的頭部數據保存到底層文件系統中,該工作由ecryptfs_write_metadata_to_contents()完成。

  1. 最后通過ecryptfs_put_lower_file()將文件改動的所有臟數據回寫入磁盤。

三.最后通過 d_instantiate() 函數將步驟一生成的 inode 信息初始化相應的 dentry,方便后續的讀寫操作。

清單七 寫入eCryptfs格式文件的關鍵函數
/* Format version: 1
*   Header Extent:
 *     Octets 0-7:        Unencrypted file size (big-endian)
 *     Octets 8-15:       eCryptfs special marker
 *     Octets 16-19:      Flags
 *      Octet 16:         File format version number (between 0 and 255)
 *      Octets 17-18:     Reserved
 *      Octet 19:         Bit 1 (lsb): Reserved
 *                        Bit 2: Encrypted?
 *                        Bits 3-8: Reserved
 *     Octets 20-23:      Header extent size (big-endian)
 *     Octets 24-25:      Number of header extents at front of file (big-endian)
 *     Octet  26:        Begin RFC 2440 authentication token packet set
 *   Data Extent 0:        Lower data (CBC encrypted)
 *   Data Extent 1:        Lower data (CBC encrypted)
 *   ...
*/
static int ecryptfs_write_headers_virt(char *page_virt, size_t max,
                       size_t *size,
                       struct ecryptfs_crypt_stat *crypt_stat,
                       struct dentry *ecryptfs_dentry)
{
    int rc;
    size_t written;
    size_t offset;
    offset = ECRYPTFS_FILE_SIZE_BYTES;
    write_ecryptfs_marker((page_virt + offset), &written);
    offset += written;
    ecryptfs_write_crypt_stat_flags((page_virt + offset), crypt_stat,
                    &written);
    offset += written;
    ecryptfs_write_header_metadata((page_virt + offset), crypt_stat,
                       &written);
    offset += written;
    rc = ecryptfs_generate_key_packet_set((page_virt + offset), crypt_stat,
                          ecryptfs_dentry, &written,
                          max - offset);
    …
    return rc;
}
加密文件open過程

這里 open 過程主要指通過 open 系統調用打開一個已存在的加密文件的流程。當應用程序在已完成 eCryptfs 掛載的目錄下 open一個已存在的加密文件時(這里以普通文件為例),其系統調用流程如圖八所示,經由層層調用后進入ecryptfs_open()函數,由其完成加密文件的 metadata 分析,然后取出EFEK并使用 kernel crypto 解密得到 FEK。另外在文中”create過程”分析時,著重介紹了創建 eCryptfs 格式文件的過程,省略了在完成lookup_open()函數調用后的vfs_open()的分析,它與這里介紹的vfs_open()流程是一樣的。需要特別指出的是在do_dentry_open函數里初始化了struct file的f_mapping成員,讓其指向inode->i_mapping;而在上圖五的 inode 的創建函數ecryptfs_inode_set中存在“inode->i_mapping->a_ops = &ecryptfs_aops”的賦值語句,這為后續的加密文件的頁讀寫時使用的關鍵對象struct address_space_operations a_ops做好了初始化。

下面重點介紹 ecryptfs_open() 函數,其主要的函數調用關系如圖九所示。eCryptfs 支持 Tag3 和 Tag1 的形式保存 EFEK,這里的分析默認是采用了 Tag3 的方式。

圖八 create 經由 VFS 調用 ecryptfs_create 的流程

圖九 eCryptfs 創建加密文件的函數調用過程

ecryptfs_open()函數的完成的主要功能包括讀取底層文件,分析其文件頭部的 metadata,取出關鍵的 EFEK 及 key sign,之后根據key sign從ecryptfs_mount_crypt_stat對象中匹配到相應的 auth token,再調用 kernel crypto 解密 EFEK 得到 FEK,最后將 FEK 保存到ecryptfs_crypt_stat的 key 成員中,完成 ecryptfs_crypt_stat 對象的初始化,供后續的文件加解密使用。具體如下:

  1. ecryptfs_set_file_private()巧妙的將 struct ecryptfs_file_info 保存到 struct file 的 private_data 中,完成 VFS 和 eCryptfs 之間的鏈式表達及映射;

  2. ecryptfs_get_lower_file()借助 kthread 內核線程巧妙的獲取到底層文件的RW權限;

  3. ecryptfs_set_file_lower() 完成 struct ecryptfs_file_info的 wfi_file 和底層文件系統文件 lower_file 之間的映射;

  4. read_or_initialize_metadata() 完成了 ecryptfs_open 的大部分功能,首先通過ecryptfs_copy_mount_wide_flags_to_inode_flags()從文件對應的 ecryptfs_mount_crypt_stat 中拷貝 flags 對ecryptfs_crypt_stat的 flags 進行初始化;之后使用函數 ecryptfs_read_lower() 讀取文件的頭部數據,緊接著利用ecryptfs_read_headers_virt() 進行數據分析和處理,包括:

1) 利用ecryptfs_set_default_sizes()初始化ecryptfs_crypt_stat對象的 extent_size、iv_bytes、metadata_size 等成員的默認值;

2) 使用ecryptfs_validate_marker()校驗文件的 marker 標記值是否符合 eCryptfs 文件格式;

3) 通過ecryptfs_process_flags()取出文件 metadata 保存的 flag 并修正 ecryptfs_crypt_stat 對象成員 flags 的值,同時初始化對象成員 file_version;

4) 在parse_header_metadata()分析文件的 metadata 的大小并保存到 ecryptfs_crypt_stat 對象成員 metadata_size;

5) 通過 ecryptfs_parse_packet_set() 解析 Tag3 和 Tag11 的 OpenPGP 格式包,獲取 EFEK 及 key sign,后根據 key sign 匹配到 auth token,再調用 kernel crypto 解密 EFEK 得到 FEK。對應的代碼實現邏輯是:parse_tag_3_packet()解析 Tag3,獲取 EFEK 和 cipher,同時將 cipher 保存到 ecryptfs_crypt_stat 對象成員 cipher;parse_tag_11_packet() 解析出 key sign,保存到 auth_tok_list 鏈表中;ecryptfs_get_auth_tok_sig()從 auth_tok_list 鏈表中獲取到 key sign;然后通過ecryptfs_find_auth_tok_for_sig()根據 key sign 從ecryptfs_mount_crypt_stat對象中匹配到相應的 auth token;再利用 decrypt_passphrase_encrypted_session_key()使用分析得到的 auth token、cipher 解密出 FEK,并將其保存在 ecryptfs_crypt_stat 的 key 成員;之后在ecryptfs_compute_root_iv() 函數里初始化 ecryptfs_crypt_stat的 root_iv 成員,在 ecryptfs_init_crypt_ctx()函數里初始化 ecryptfs_crypt_stat 的 kernel crypto 接口 tfm。至此,ecryptfs_crypt_stat 對象初始化完畢,后續文件在讀寫操作時使用到的加解密所需的所有信息均在該對象中獲取。

加密文件 read 過程

read 過程指應用程序通過 read()函數在 eCryptfs 掛載的目錄下讀取文件的過程。因為掛載點在掛載 eCryptfs 之前可能已經存在文件,這些已存在的文件屬于非加密文件,只有在完成 eCryptfs 掛載后的文件才自動保存成 eCryptfs 格式的加密文件,所以讀取文件時需要區分文件是否屬于加密文件。從應用程序發起read()操作到eCryptfs層響應的函數調用關系流程圖如十所示,讀取時采用page read的機制,涉及到page cache的問題,圖中以首次讀取文件,即文件內容還沒有被讀取到page cache的情況為示例。自ecryptfs_read_update_atime()起進入到 eCryptfs 層,由此函數完成從底層文件系統中讀取出文件內容,若是加密文件則利用 kernel crypto 和 open 時初始化好的 ecryptfs_crypt_stat 對象完成內容的解密,之后將解密后的文件內容拷貝到上層應用程序,同時更新文件的訪問時間,其中 touch_atime()完成文件的訪問時間的更新;generic_file_read_iter() 函數調用內核函數do_generic_file_read(),完成內存頁的申請,并借助 mapping->a_ops->readpage() 調用真正干活的主力 ecryptfs_readpage() 來完成解密工作,最后通過 copy_page_to_iter() 將解密后的文件內容拷貝到應用程序。到了關鍵的解密階段,描述再多也不如代碼來的直觀,ecryptfs_readpage() 的核心代碼如清單八、九、十所示。

圖十 create 經由 VFS 調用 ecryptfs_create 的流程

清單八 ecryptfs_readpage()關鍵代碼
static int ecryptfs_readpage(struct file *file, struct page *page)
{
    struct ecryptfs_crypt_stat *crypt_stat =
        &ecryptfs_inode_to_private(page->mapping->host)->crypt_stat;
    int rc = 0;
    if (!crypt_stat || !(crypt_stat->flags & ECRYPTFS_ENCRYPTED)) {

    //讀取非加密文件
        rc = ecryptfs_read_lower_page_segment(page, page->index, 0,
                              PAGE_CACHE_SIZE,
                              page->mapping->host);
    } else if (crypt_stat->flags & ECRYPTFS_VIEW_AS_ENCRYPTED) {
    //直接讀取密文給上層,此時應用程序讀到的是一堆亂碼
        if (crypt_stat->flags & ECRYPTFS_METADATA_IN_XATTR) {
            rc = ecryptfs_copy_up_encrypted_with_header(page, crypt_stat);
            …
        } else {
            rc = ecryptfs_read_lower_page_segment(
                page, page->index, 0, PAGE_CACHE_SIZE,
                page->mapping->host);
            …
        }
    } else {
    //讀取密文并調用kernel crypto解密
        rc = ecryptfs_decrypt_page(page);
        …
    }
    …
    return rc;
}
清單九 ecryptfs_decrypt_page()核心代碼
int ecryptfs_decrypt_page(struct page *page)
{
    …
    ecryptfs_inode = page->mapping->host;

    //獲取包含有FEK、cipher、crypto context tfm信息的ecryptfs_crypt_stat
    crypt_stat = &(ecryptfs_inode_to_private(ecryptfs_inode)->crypt_stat);

    //計算加密文件內容在底層文件中的偏移
    lower_offset = lower_offset_for_page(crypt_stat, page);
    page_virt = kmap(page);

    //利用底層文件系統的接口讀取出加密文件的內容
    rc = ecryptfs_read_lower(page_virt, lower_offset, PAGE_CACHE_SIZE, ecryptfs_inode);
    kunmap(page);
    …
    for (extent_offset = 0;
         extent_offset < (PAGE_CACHE_SIZE / crypt_stat->extent_size);
         extent_offset++) {

        //解密文件內容
        rc = crypt_extent(crypt_stat, page, page,
                  extent_offset, DECRYPT);
        …
    }
    …
}
清單十 crypt_extent()核心加解密函數的關鍵代碼
static int crypt_extent(struct ecryptfs_crypt_stat *crypt_stat,
            struct page *dst_page,
            struct page *src_page,
            unsigned long extent_offset, int op)
{
    //op 指示時利用該函數進行加密還是解密功能
    pgoff_t page_index = op == ENCRYPT ? src_page->index : dst_page->index;
    loff_t extent_base;
    char extent_iv[ECRYPTFS_MAX_IV_BYTES];
    struct scatterlist src_sg, dst_sg;
    size_t extent_size = crypt_stat->extent_size;
    int rc;
    extent_base = (((loff_t)page_index) * (PAGE_CACHE_SIZE / extent_size));
    rc = ecryptfs_derive_iv(extent_iv, crypt_stat,
                (extent_base + extent_offset));
    …
    sg_init_table(&src_sg, 1);
    sg_init_table(&dst_sg, 1);
    sg_set_page(&src_sg, src_page, extent_size,
            extent_offset * extent_size);
    sg_set_page(&dst_sg, dst_page, extent_size,
            extent_offset * extent_size);
    //調用kernel crypto API進行加解密
    rc = crypt_scatterlist(crypt_stat, &dst_sg, &src_sg, extent_size, extent_iv, op);
    …
    return rc;
}

理順了 mount、open 的流程,知道 FEK、cipher、kernel crypto context 的值及存放位置,同時了解了加密文件的格式,解密的過程顯得比較簡單,感興趣的同學可以繼續查看 crypt_scatterlist()的代碼,該函數純粹是調用 kernel crypto API 進行加解密的過程,跟 eCryptfs 已經沒有關系。

加密文件 write 過程

eCryptfs 文件 write 的流程跟 read 類似,在寫入 lower file 前先通過 ecryptfs_writepage() 函數進行文件內容的加密,這里不再詳述。

四、ChromeOS 使用 eCryptfs 的方法及流程

Chromeos 在保護用戶數據隱私方面可謂不遺余力,首先在系統分區上專門開辟出專用于存儲用戶數據的 stateful partition,當用戶進行正常和開發者模式切換時,該分區的數據將會被自動擦除;其次該 stateful partition 的絕大部分數據采用 dm-crypt 進行加密,在系統啟動時用戶登錄前由 mount-encrypted 完成解密到/mnt/stateful_partition/encrypted,另外完成以下幾個mount工作:將/Chromeos/mnt/stateful_partition/home bind mount/home;將/mnt/stateful_partition/encrypted/var bind mount/var目錄;將/mnt/stateful_partition/encrypted/chromos bind mount/home/chronos。最后在用戶登錄時發起對該用戶私有數據的 eCryptfs 加解密的流程,具體工作由 cryptohomed 守護進程負責完成,eCryptfs 加密文件存放在/home/.shadow/[salted_hash_of_username]/vault目錄下,感興趣的讀者可通過 ecryptfs-stat 命令查看其文件狀態和格式,mount 點在/home/.shadow/[salted_hash_of_username]/mount,之后對/home/.shadow/[salted_hash_of_username]/mount下的 user 和 root 建立 bind mount 點,方便用戶使用,如將/home/.shadow/[salted_hash_of_username]/mount/user bind mount/home/user/[salted_hash_of_username]/home/chronos/u-[salted_hash_of_username] ;將/home/.shadow/[salted_hash_of_username]/mount/root bind mount/home/root/[salted_hash_of_username]。用戶在存取數據時一般是對目錄/home/chronos/u-[salted_hash_of_username]進行操作。

eCryptfs 在 Chromeos 中的應用架構如圖十所示。系統啟動后開啟 cryptohomed 的守護進程,由該進程來響應 eCryptfs 的掛載和卸載等,進程間采用D-Bus的方式進行通信,cryptohome應用程序主用于封裝用戶的動作命令,后通過 D-Bus 向 cryptohomed 發起請求。如可通過cryptohome命令“cryptohome -–action=mount -–user=[account_id]”來發起 eCryptfs 的掛載;通過命令“cryptohome -–action=unmount”卸載eCryptfs的掛載,執行成功此命令后,用戶的所有個人數據將無法訪問,如用戶先前下載的文件內容不可見、安裝的應用程序不可使用,/home/.shadow/[salted_hash_of_username]/mount內容為空。

圖十一 eCryptfs 在 Chromeos 中的架構圖

cryptohomed 特色的 mount 流程如下:

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

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

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

b. 生成vault keyset /home/.shadow/[salted_hash_of_username]/master.0/home/.shadow/[salted_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”字段

圖十二中的 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,那么問題來了,Chromeos 的 FEK 的保存及解密流程與上文介紹的 eCryptfs 時不一致,FEK 不應該是 open 時從加密文件的頭部 metadata 里的 EFEK 中解密出來的么?不過一次解密出FEK,全局使用,效率的確比每次讀取文件時解析FEK高很多,之后通過 key 的系統調用將 key 傳入內核的 keyring,使用時通過key sign匹配。最后跟上文所述實屬異曲同工。

4.通過 mount 系統調用傳入 option 完成掛載。

該部分與正常的 Linux 做法一致,在 mount 的 option 里傳入關鍵的 cipher、key sign、key bytes 等信息。

圖十二 TPM解密VK的流程

五、結語

ecryptfs 建立在系統安全可信的基礎上,保護用戶數據的安全,核心基礎組件是加密密鑰,若在內核被攻破后密鑰被通過某些手段竊取,ecryptfs 的安全性將同樣被攻破。另外 page cache 中加密文件的明文頁有可能被交換到 swap 區,在 chromeos 中已經禁用了swap,因此不會產生影響,但是其他版本的 Linux 系統需要注意該問題。

eCryptfs 首次實現到現在已經十年有余,直到近幾年才在 chromeos 和 Ubuntu 上使用,個人認為除了之前人們的安全意識不如現在強烈外,更重要的是隨著處理器性能的增強,eCryptfs 加解密引起的文件讀寫性能下降的問題已經得到緩解。但實際的性能損耗如何,有待繼續研究。或許出于性能的原因,年初的時候 Google 在 chromeos 實現了基于ext4 crypto 的 dircrypto,用于實現跟 eCryptfs 同樣的功能,目前 chromeos 同時支持 eCryptfs 和 dircrypto,但在60版本后優先采用 dircrypto 技術,相關技術在另外的文章中進行介紹。

最后,文中必有未及細看而自以為是的東西,望大家能夠去偽存真,更求不吝賜教。

六、參考資料


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