【譯】使用PHP和SQL構建可搜尋的加密資料庫

雷語發表於2017-06-13

譯者按:原文鋪墊較多,但對於理解作者的心情有一定益處。
基於 PHP 和 普通SQL資料庫只要肯琢磨,系統的安全也能大幅提升。
閱讀本文,你將瞭解:

  1. 針對類似身份證此類敏感資訊的儲存和查詢方案。
  2. 對 PHP 擴充套件庫Libsodium 有感性的瞭解

我們總被詢問一個相同的問題。這個問題時不時的出現在* open source encryption libraires` bug trackers 。它曾是一個怪異的問題,在my talk at B-Sides Orlando*(名為 針對怪異問題構建防禦性解決方案)已有涉及,我們也在一本白皮書中用一章節來描述說明。

這個問題就是:“我們是如何安全加密資料庫欄位,並且還可以在搜尋查詢中使用這些欄位?

我們的安全解決方案是相當的簡單明瞭,但在這些提問的團隊到發現我們簡單的解決方案之間卻是充滿了危險:糟糕的設計、學院派的搜尋工程、誤導的市場以及貧乏的威脅建模。

看到這如果你已經急不可耐,可以直接跳到解決方案。

關於可搜尋的加密#

讓我們從一個簡單的場景開始,它可能與政府、醫療應用有一些特殊的關聯:

  • 你在建立一個新系統,它需要從使用者那裡收集社會安全號(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 in2hex(
            openssl_encrypt(
                $plaintext,
                `aes-128-ecb`,
                $this->key,
                OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
            )
        );
    }
}

在上面的片段中,當使用相同的祕鑰時,相同原文總會產生相同的密文。但更值得關注的是ECB 模式,它每16個位元組塊獨立加密,這將產生一些極其不幸的後果。

形式上,這些建構函式並不是語義上安全的:如果你加密了一條內容很多的訊息,你會看到一些塊重複出現在密文中。

為了安全,對於沒有解密祕鑰的其他人,加密後的資訊和隨機噪音必須沒有明顯的區別。非安全的模式包括 ECB 模式和使用靜態 (或空)IV的 CBC 模式。

你想要非確定性的加密,這意味著每個訊息使用唯一的隨機數(nonce,Number Used Once)或者給定祕鑰但不會重複初始化向量。

實驗性學院派設計##

現在有很多學術研究也在關注這個主題,如 同態(hommorphic)漏序(order-revealing)保序(order-preserving)等技術。

這個工作有趣的地方是,現有的設計如果用於生產環境,沒有一處是足夠安全的。

例如,漏序加密洩露了太多原文的資料
同態加密的設計通常要將易受攻擊部分(實用選擇)重新打包作為特點。

  • 非填充 RSA 有關乘法是同態的。
  • 如果將密文乘以整數,你得到的明文會等於原始訊息乘以相同的整數。有很多可能的針對非填充RSA 的攻擊,這是為什麼線上索中 RSA 使用填充(雖然總是非安全的填充模式)。
  • AES 在計數模式下,關於 XOR 也是同態的。
  • 這也是為什麼在 CTR 模式下重用 nonce 將威脅資訊的機密性(通常非 NMR 流式加密也類似)。

如之前博文提到的,當涉及現實世界加密時,沒有完整性的機密性相當於沒有機密性。如果攻擊者獲得了資料庫的訪問許可權,修改了密文,研究了應用關於解密的行為,那將會有什麼樣的後果呢?

在研的這些密碼學也許某天能產生創新加密設計,並且不打破密碼學幾十年的研究成果及協議設計。然而,我們不關注這些,為了解決這個問題,你不用耗費財力、精力在沒有必要的複雜的研究原型標準上。

不光彩:解密每行##

我不期望大多數工程師沒看到這一串的諷刺而直接看到解決方案。壞主意是,因為你需要安全加密(見下面),你唯一的資源是在資料庫中查詢每個密文,然後遍歷它們,一個接一個解密它們,最後在應用程式碼中執行你的搜尋操作。

如果你完成了這個流程,你將使你的應用面臨拒絕服務攻擊。你的合法使用者響應會變慢。這是犬儒主義的回答,但你可以做到比這更好,下面我們將具體說。

輕鬆實現安全可搜尋加密#

讓我們開始一舉解決非安全/欠考慮的節所列的問題:所有密文將是一個認證加密組合的結果,如能結合大 Nonce 則更好(Nonce 由安全隨機數生成器產生)。

使用認證加密組合,密文是不可確定的(相同訊息、祕鑰,不同 Nonce,產生不同密文),同時由認證標籤保護。一些合適的選項如下:XSalsa20-Poly1305, XChacha20-Poly1305,NORX64-4-1。如果你使用 NaCI(Networking and Cryptography library)或 libsodium,你可以直接使用crypto_secretbox

於是,我們的密文與隨機噪音是很難區分的,可防止選擇密文攻擊。這就是安全而無趣的加密技術應該的樣子。

然而,這引出了新的挑戰:我們不能為了匹配密文,而只加密任意訊息和資料庫查詢。幸運的是,有個巧妙的變通方案。

重要:威脅塑造了加密技術的應用

在開始前,確保加密是在實實在在讓你的資料更安全。需要著重強調一點,加密儲存並不能讓 CRUD 應用變得更安全(Create, Read, Update and Delete),這樣的應用一般都容易受到 SQL 注入攻擊。要解決實際問題(如阻止 SQL 注入)只有一條路可走。

如果加密技術是一種適合執行的安全控制,這就暗示了用於加解密資料的加密祕鑰對於資料庫軟體是不能訪問的。大多數情況下,很有意義把應用服務和資料庫服務部署在獨立的硬體。

我們的威脅模型##

本文後續都假定以下3點成立:

  1. 你的資料庫服務和 Web 服務部署在不同實體物理硬體上(避開VM)。
  2. 你的資料庫服務不知道 Web 服務持有的祕鑰。
  3. 我們保護資料抵抗實彈攻擊,而不是危害Web 伺服器。危害Web伺服器是一場全域性的遊戲。
  • 實彈攻擊 vs 線下攻擊:你可以簡單使用全盤加密,來應對包含硬體被物理偷竊的威脅模型,但這種方法對於來自解密的線上伺服器的攻擊毫無價值。

威脅模型的其餘部分有意的晦澀難懂。只要上面的假設成立,我們的解決方案對於你的威脅模型將是可應用的。

實施加密資料的文字檢索#

可能的用例:儲存社會安全號碼,但對它們進行檢索。

為了儲存加密後的資訊且仍能使用明文在 SELECT 查詢中,我們將使用名為盲索引(blind indexing)策略。總體的思路是將明文的帶有祕鑰的雜湊(像 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 的盲索引可以存入* human.ssn_bidx*。簡單的實現可能會如下:

<?php
/* 這並不是滿足生產環境質量的程式碼。
 * 它為了可讀性和易於理解做了優化,但未考慮安全性。
 */

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);
}

更綜合的概念驗證在supplemental material for my B-Sides Orlando 2017 talk。它是基於知識共享 CC0許可釋出的,對於大多數人而言等同於公共的。

安全分析及限制##

基於你抽象的威脅模型,這個解決方案遺留了兩個在解決前必須注意的問題:

  1. 它真的使用安全還是像不能保守祕密的人一樣會洩露資料?
  2. 它的使用限制是什麼?(這是已經回答的問題)

基於上面的例子,假定你的加密祕鑰和盲索引祕鑰是分離的,這兩個祕鑰都儲存在 Web 伺服器中,資料庫伺服器就沒有方法獲得這些祕鑰,這樣任何危害資料庫伺服器的攻擊者只能知道有一些行記錄了社會安全號碼,但不知共享的 SSN 是什麼。加倍的實體洩露是必要的,目的是為了索引,它反過來允許快速的 SELECT 查詢使用者提供的值。

此外,如果攻擊者能像正常應用的使用者那樣觀察到或改變明文,而且觀察到儲存在資料庫裡的忙索引,他們可用利用這個進行選擇明文攻擊,他們以使用者的身份遍歷每一個可能的值,並與對應結果的盲索引值關聯起來。相比較例如 Argon2的方案,這在 使用HMAC 方案時更加可行。對於高加密或低敏感值(不是 SSN 之類),物理的暴力攻擊有利於我們。

對於犯罪分子,更可行的攻擊是,用其他行的值來替換,然後正常訪問應用程式,後者將返回明文,除非每行都採用不同的祕鑰(如hash_hmac(`sha256`, $rowID, $masterKey, true)能有效減輕,雖然還有更適合的方法)。這裡最好的防禦是使用 AEAD 模式(傳遞主祕鑰作為附加關聯資料),以便於密文與特定的資料庫行繫結。(這不能阻止攻擊者刪除資料,這也是更大的挑戰)。

相比較其他方案洩露資訊的總量,大部分應用威脅模型都會覺得這個方案是一個可接受的權衡。只要你使用整整加密用於加密,不論 HMAC (針對盲索引非敏感資料)還是密碼HASH 演算法(盲索引敏感資料),它都很容易得出應用系統的安全性。

然後,它有一個很嚴格的限制:它只能用於精確匹配。如果兩個字串不同點並沒什麼意義,但總會產生不同的加密的HASH,此時搜尋一個就不會返回另一個。如果你需要做更高階的查詢,但仍希望保持你的解密祕鑰和明文值不在資料庫伺服器的範圍內,我們就必須更多的創新。

它仍是最佳的方案,當 HMAC/Argon2 可以阻止沒有祕鑰的攻擊者學習儲存在資料庫中的明文值,它可能洩露真實世界的後設資料(如兩個無關的人可能共享同一個街道地址)。

實施加密資料的模糊查詢#

可能的用例:加密人的合法姓名,支援部分匹配的檢索
讓我們在前面的章節基礎上繼續,我們已經建立了一個忙索引,支援精確匹配的方式查詢資料庫。

下面,不再增加列到已經存在的表,我們將儲存額外的索引值到一個 join 表。

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位,並按布隆過濾器來處理他們:

  • 如果查詢觸發的盲索引匹配了給定的行,資料可能匹配。
    • 你應用程式碼需要為每個候選行執行解密運算,然後返回實際匹配的結果。
  • 如果查詢觸發的盲索引沒有匹配給定的行,那麼資料確實沒有匹配的。

有時值得把這些值從字串轉化為整型,如果你的資料庫伺服器最終可以更高效的儲存它。

結論#

我希望我已經充分說明了,不僅能建立一個使用安全加密的系統同時允許快速檢索(最小的資訊洩露,對抗高特權的攻擊),而且可能很簡單的建立一個系統,而且僅使用現代的加密庫及較小的耦合。

如果你有興趣在你的軟體中實現加密的資料庫儲存,我們很樂意提供你和你的公司諮詢服務。如果有興趣,請聯絡我們!

作者:Scott Arciszewski
開發總監
15年軟體開發、應用安全、系統管理經驗,Scott 希望通過解決難題和自動化瑣碎的任務,以幫助他人獲得快樂的工作和生活平衡。

原文


相關文章