ChromeOS基於eCryptfs的用戶數據安全保護機制
概述
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_name、global_default_cipher_key_size、flags、global_default_fnek_sig、global_default_fn_cipher_name、global_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() 函數完成最終的加密文件的創建,關鍵代碼的調用流程如圖七所示,以代碼做為視圖,分為三大步驟:一、通過ecryptfs_do_create()函數創建eCryptfs 文件的inode並初始化;二、通過函數ecryptfs_initialize_file()將新創建的文件初始化成eCryptfs加密文件的格式,添加入諸如加密演算法、密鑰信息等,為後續的讀寫操作初始化好crypto介面;三、通過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()完成。
4. 最後通過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技術,相關技術在另外的文章中進行介紹。
最後,文中必有未及細看而自以為是的東西,望大家能夠去偽存真,更求不吝賜教。
參考資料
1. 企業級加密文件系統 eCryptfs 詳解
2. eCryptfs: a Stacked Cryptographic Filesystem
3. Linux kernel-V4.4.79 sourcecode
4. chromiumos platform-9653 sourcecode


※Authenticode簽名偽造——PE文件的簽名偽造與簽名驗證劫持
※「中國菜刀」出海:有人用它從澳大利亞軍方偷了30GB絕密數據
※警惕出現下一個「WannaCry」,安天緊急發布CVE-2017-11780漏洞免疫工具
※神漏洞!Outlook會將你的加密郵件明文再發送一遍
※識別驗證新花樣:如何用心跳進行身份識別
TAG:嘶吼RoarTalk |