使用PHP與SQL搭建可搜尋的加密資料庫

玄學醬發表於2017-09-18
本文講的是使用PHP與SQL搭建可搜尋的加密資料庫我們從一個簡單的場景開始(這可能與很多地方政府或醫療保健應用非常相關):

您正在建立一個新系統,需要從其使用者收集社會保障號碼(SSN)。

規則和常識都規定使用者的SSN應該在儲存時加密。

鑑於他們的SSN,員工需要能夠檢視使用者帳號。

我們先來看看這個問題的一些比較明顯的答案。不安全(或其他不明智)的答案是非隨機加密,大多數團隊(特別是沒有安全或加密專家的團隊)最明顯的答案將是這樣做:

<?php
class InsecureExampleOne
{
    protected $db;
    protected $key;
 
    public function __construct(PDO $db, string $key = ``)
    {
        $this->db = $db;
        $this->key = $key;
    }
 
    public function searchByValue(string $query): array
    {
        $stmt = $this->db->prepare(`SELECT * FROM table WHERE column = ?`);
        $stmt->execute([
            $this->insecureEncryptDoNotUse($query)
        ]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
 
    protected function insecureEncryptDoNotUse(string $plaintext): string
    {
        return bin2hex(
            openssl_encrypt(
                $plaintext,
                `aes-128-ecb`,
                $this->key,
                OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
            )
        );
    }
}

在上面的程式碼片段中,當使用相同的金鑰加密時,相同的明文總是產生相同的密文。但更多的關於ECB模式是每隔16位元組的塊被單獨加密,這可能會產生一些非常不好的後果。

事實上正式的看的話,這些結構不是語義上的安全:如果加密一個大的訊息,你會看到密文中的塊會出現重複。

為了安全起見,加密必須與隨機噪聲無法區分給任何不儲存解密金鑰的人。不安全模式包括ECB模式和CBC模式,靜態(或空)IV。

您需要的應該是非確定性加密,這意味著每個訊息都會使用一個不重複給定金鑰的唯一的隨機數或初始化向量。

學術設計

有許多學術研究涉及諸如同態,秩序揭示和訂單儲存加密技術等主題。

與這項工作一樣有趣的是,目前的設計無法在生產環境中使用。

例如,訂單顯示加密會洩漏足夠的資料來推斷明文。

同態加密方案通常將重新打包漏洞(實際的選擇密文攻擊)作為特徵。

相對於乘法,未貼合的 RSA是同態的。

如果您將密文乘以整數,則您獲得的明文將等於原始訊息乘以相同的整數。有幾種可能的攻擊是無條件的RSA,這就是為什麼現實世界中的RSA使用填充(儘管通常是不安全的填充模式)。

計數器模式下的AES是相同XOR的同態。

這就是為什麼nonce-reuse在CTR模式下消除了你的訊息的機密性(通常是非NMR流密碼)。

正如我們在之前的部落格文章中所述,當涉及真實世界的加密技術時,沒有完整性的機密性與沒有保密性相同。如果攻擊者獲得訪問資料庫,改變密文,並在解密時研究應用程式的行為,會發生什麼?

也許有一天,正在進行的加密研究有可能產生一種創新的加密設計,不會對數十年來對安全加密原語和加密協議設計的研究取消任何進展。但是,我們目前還沒有,因此您不需要投資一個不必要的複雜的研究原型來解決問題。

我不希望大多數工程師能夠在沒有受到任何挫折的情況下得到這個解決方案。這裡有一個壞主意是,你需要安全的加密(見下文),你唯一的方法就是查詢資料庫中的每個密文,然後對它們進行迭代,逐個解密,並在應用程式程式碼中執行搜尋操作。

如果您被迫接受了這種方法,那麼實際上你做的將會開啟你的應用程式去進行DOS攻擊。對您的合法使用者來說,速度可能會慢一些。這顯然是一個憤世嫉俗的答案,你可以做得比這更好,我們將在下面展示。

安全加密搜尋將變得簡單

我們首先避免在不安全/不明智的部分中概述的所有問題:所有密文將是經認證的加密方案的結果,優選具有大的隨機數(由安全的隨機數生成器生成)。

通過認證加密方案,密文是非確定性的(相同的訊息和金鑰,但是不同的隨機數,產生不同的密文)並且被認證標籤保護。一些合適的選項包括:XSalsa20-Poly1305,XChacha20-Poly1305和(假設在CAESAR得出結論之前沒有破壞)NORX64-4-1。如果你使用NaCl或者libsodium,你可以crypto_secretbox在這裡使用。

因此,我們的密文與隨機噪聲無法區分,並且可以防止選擇密文攻擊。

然而,這裡有一個迫在眉睫的挑戰:我們不能僅僅加密任意訊息,我們還要查詢資料庫以匹配密文。幸運的是,有一個聰明的解決方法。

在開始之前,請確保加密實際上使您的資料更安全。非常重要的一點是,“加密儲存”不是保護易受SQL隱碼攻擊的CRUD應用程式的解決方案。解決實際問題(即防止SQL隱碼攻擊)是唯一的辦法。

如果加密是實現的合適的安全控制,這意味著用於加密/解密資料的加密金鑰對於資料庫軟體是不可訪問的。在大多數情況下,將應用程式伺服器和資料庫伺服器保留在單獨的硬體上是有意義的。

實現加密資料的字元搜尋

可能的用途:儲存社會保險號,但仍然可以進行查詢。

為了儲存加密資訊並仍然在SELECT查詢中使用明文,我們將遵循一種我們稱之為盲索引的策略。一般的想法是將明文的金鑰雜湊(例如HMAC)儲存在單獨的列中。重要的是,盲索引鍵與加密金鑰不同,資料庫伺服器未知。

對於非常敏感的資訊,而不是簡單的HMAC,您將需要使用鍵作為靜態鹽的鍵的拉伸演算法(PBKDF2-SHA256,scrypt,Argon2)來減緩列舉嘗試。我們不用擔心任何一種情況下的離線暴力攻擊,除非攻擊者可以獲取金鑰(不能儲存在資料庫中)。

所以如果你的表格模式看起來像這樣(在PostgreSQL中):

CREATE TABLE humans (
    humanid BIGSERIAL PRIMARY KEY,
    first_name TEXT,
    last_name TEXT,
    ssn TEXT, /* encrypted */
    ssn_bidx TEXT /* blind index */
);
CREATE INDEX ON humans (ssn_bidx);

您將儲存加密的值humans.ssn。明文SSN的盲目索引將進入humans.ssn_bidx。看起來天真的想法實現的話可能如下所示:

<?php
/* This is not production-quality code.
 * It`s optimized for readability and understanding, not security.
 */
 
function encryptSSN(string $ssn, string $key): string
{
    $nonce = random_bytes(24);
    $ciphertext = sodium_crypto_secretbox($ssn, $nonce, $key);
    return bin2hex($nonce . $ciphertext);
}
 
function decryptSSN(string $ciphertext, string $key): string
{
    $decoded = hex2bin($ciphertext);
    $nonce = mb_substr($decoded, 0, 24, `8bit`);
    $cipher = mb_substr($decoded, 24, null, `8bit`);
    return sodium_crypto_secretbox_open($cipher, $nonce, $key);
}
 
function getSSNBlindIndex(string $ssn, string $indexKey): string
{
    return bin2hex(
        sodium_crypto_pwhash(
            32,
            $ssn,
            $indexKey,
            SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE,
            SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE
        )
    );
}
 
function findHumanBySSN(PDO $db, string $ssn, string $indexKey): array
{
    $index = getSSNBlindIndex($ssn, $indexKey);
    $stmt = $db->prepare(`SELECT * FROM humans WHERE ssn_bidx = ?`);
    $stmt->execute([$index]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

對於我的B-Sides Orlando 2017年談話的補充材料,包含更全面的POC。它是根據知識共享CC0許可證釋出的,對於大多數人來說,這個許可證與“公共領域”相同。

安全分析與限制

根據你的確切威脅模型,這個解決方案留下了兩個必須回答的問題:

是否真的可以安全使用,還是說可能仍然會洩漏資料,就像濾網一樣?
它的用途有什麼侷限性?(這一個其實已經回答了。)

以上我們的例子中,假設您的加密金鑰和盲索引鍵是分開的,兩個金鑰都儲存在Web伺服器中,而資料庫伺服器沒有任何方法來獲取這些金鑰,那麼任何攻擊者只會破壞資料庫伺服器但不是Web伺服器)只能夠了解若干行是否共享一個社會安全號碼,而不是共享SSN是什麼。為了使索引成為可能,這種重複的條目洩漏是必要的,這又允許從使用者提供的值進行快速的SELECT查詢。

此外,如果攻擊者能夠在遵守儲存在資料庫中的盲目索引的同時觀察/更改明文作為應用程式的普通使用者,則可以將其利用為選擇的明文攻擊,在這些攻擊中,它們以使用者身份迭代每個可能的值然後與所得的盲目索引值相關聯。這在HMAC方案中比在Argon2方案中更實際。而對於高熵或低靈敏度值(而不是SSN)來說,我只能說物理學真的就在我們身邊。

對於現在這樣的情況,犯罪分子要想進行更實際的攻擊就要將值從一行替換到另一行,然後正常訪問應用程式,而這就將會揭示明文,除非採用了獨特的每行金鑰(例如hash_hmac(`sha256`, $rowID, $masterKey, true)甚至可以在這裡有效緩解)其他人會更喜歡)。這裡最好的防禦是使用AEAD模式(將主鍵作為附加關聯資料傳遞),以便將密文連線到特定的資料庫行。(這樣做並不會阻止攻擊者刪除資料,這是一個更大的挑戰。)

與其他解決方案洩露的資訊量相比,大多數應用程式的威脅模型應該將其視為可接受的權衡。只要您使用身份驗證加密進行加密,以及HMAC(用於盲目索引非敏感資料)或密碼雜湊演算法(盲目索引敏感資料),就很容易理解應用程式的安全性。

然而,它確實有一個非常嚴重的限制:它只適用於完全匹配。如果兩個字串以無意義的方式不同,但總是會產生不同的加密雜湊,則搜尋一個不會產生另一個。如果您需要執行更多高階查詢,但是仍然希望將解密金鑰和明文值保留在資料庫伺服器的手中,我們將必須獲得創造性。

還值得注意的是,雖然HMAC / Argon2可以防止不具有金鑰的攻擊者學習資料庫中儲存的明文值,但它可能在現實世界會顯示後設資料(例如兩個看似無關的人共享一個街道地址)。

實現加密資料的模糊搜尋

可能的用途:加密人們的法定名稱,並能夠僅搜尋部分匹配內容。

我們在上一節的基礎上構建一個盲目索引,讓您可以查詢資料庫的精確匹配。

這一次,我們將不會在現有表中新增列,而是將額外的索引值儲存到連線表中。

CREATE TABLE humans (
    humanid BIGSERIAL PRIMARY KEY,
    first_name TEXT, /* encrypted */
    last_name TEXT, /* encrypted */
    ssn TEXT, /* encrypted */
);
CREATE TABLE humans_filters (
    filterid BIGSERIAL PRIMARY KEY,
    humanid BIGINT REFERENCES humans (humanid),
    filter_label TEXT,
    filter_value TEXT
);
/* Creates an index on the pair. If your SQL expert overrules this, feel free to omit it. */
CREATE INDEX ON humans_filters (filter_label, filter_value);

這種變化的原因是規範化我們的資料結構。你可以通過新增列到現有的表,但它可能會變得混亂。

接下來的改變是,我們將為每個不同型別的查詢(每個都有自己的金鑰)為每列儲存一個單獨的,不同的盲目索引。例如:

需要忽略空格的不區分大小寫的查詢?
儲存盲目索引preg_replace(`/[^a-z]/`, “, strtolower($value))。
需要查詢他們姓氏的第一個字母?
儲存盲目索引strtolower(mb_substr($lastName, 0, 1, $locale))。
需要匹配“這個字母,並在那個字母結束”?
儲存盲目索引strtolower($string[0] . $string[-1])。
需要查詢他們姓氏的前三個字母和他們的名字的第一個字母?
你猜對了!基於部分資料來構建另一個索引。

每個指標都需要有一個獨特的關鍵,應該大力刺激明文隱含的盲目索引,將真正的明文值洩露給一個犯罪分子,用謎語填字遊戲。只能為嚴重的業務需求建立索引,並且積極地記錄對應用程式這些部分的訪問。

時間與儲存的交易

到目前為止,所有設計命題都贊成允許開發人員仔細閱讀SELECT查詢,同時最小化解密子例程的次數。一般來說,這樣已經能做到讓大多數人滿意了。

但是,有些情況下,如果意味著節省大量的磁碟空間,搜尋查詢中的輕微效能也可以接受。

這裡的技巧很簡單:將您的盲目索引截斷為16,32或64位,並將其視為 Bloom過濾器:

如果查詢中涉及到的盲目索引與給定行匹配,則資料可能是一個匹配項。

您的應用程式程式碼將需要為每個候選行執行解密,然後僅提供實際匹配。

如果查詢中涉及的盲指數與給定行不匹配,則資料絕對不匹配。

如果您的資料庫伺服器最終會更有效地儲存它,也可能會將這些值從字串轉換為整數。

原文釋出時間為:2017年5月28日
本文作者:Change 
本文來自雲棲社群合作伙伴嘶吼,瞭解相關資訊可以關注嘶吼網站。


相關文章