如何使用 PostgreSQL 安全地存儲密碼

介紹

管理用戶帳戶的應用程序需要在允許(授權)用戶在應用程序內執行不同任務之前對用戶進行身份驗證(建立身份)。 密碼是用於對用戶進行身份驗證的常用且行之有效的方法。 在典型的基於密碼的身份驗證中,用戶憑證由登錄 ID(用戶名、電子郵件等)和密碼組成。 這些憑據存儲在數據庫中。 對於每次登錄嘗試,用戶輸入的憑據都會根據數據庫中存儲的憑據進行檢查。

在數據庫中存儲用戶憑據時,切勿(永遠)將密碼存儲為純文本(未加密的可讀文本)。 明文的對立面是密文。

  1. 所有的計算機系統都是可以破解的。 萬一您的服務器遭到破壞並且攻擊者甚至獲得了對數據庫(或數據庫的轉儲)的只讀訪問權限,您可以獲取用戶的登錄憑據,包括密碼。

  2. 用戶通常會忽略安全最佳實踐,並為多項服務使用相同的密碼。 在您的服務上洩露用戶密碼可能會導致該用戶的帳戶在其他服務上遭到破壞。

  3. 如果服務提供商可以讀取密碼,用戶就可以責怪服務提供商,以防發生意外。 此外,不道德的開發人員和系統管理員可能會濫用用戶密碼。

  4. 許多國家和超國家法規(例如 GDPR)規定將密碼存儲為純文本是非法的。

範圍

本文的範圍僅限於用戶密碼的安全存儲。 本文不涉及 PostgreSQL 中的一般數據加密,也不涉及數據庫憑據(即用於登錄數據庫本身的密碼)或連接的加密。

以前的要求

要從本指南中獲益,之前接觸過 PostgreSQL 是必要的。 為了測試示例,假設您已經在獨立服務器上運行 PostgreSQL 或作為 PostgreSQL 的 Vultr 託管數據庫實例。

熟悉基本密碼學概念(如加密和散列)很有幫助,但不是必需的。

請注意,本指南中的所有代碼示例都是 SQL 語句。 另請注意,SQL 語句中的文本字符串用單引號括起來。

第一原則——密碼散列

如果用戶密碼不以純文本形式存儲,則應以加密形式存儲。 但是,加密算法有相應的解密算法,用於恢復加密數據。 加密的目的是暫時混淆數據並在需要時將其解密回原始狀態。 但這在密碼的情況下是不可取的。 存儲易於破解的加密密碼沒有額外的好處。 因此,密碼不是加密的,而是加密的。

hash 是從輸入字符串生成的隨機字符串。 生成散列的算法稱為散列函數。 計算輸入字符串的哈希值很容易,但幾乎不可能根據給定的哈希值計算字符串的值。 散列被認為是一種單向計算。

因此,針對散列密碼的最實用的攻擊媒介是暴力破解。 蠻力嘗試通常基於字典。 一般的想法是從最常用密碼的列表(字典)開始,然後按順序散列每個可能的密碼,直到找到匹配項。

散列是一項計算密集型任務; 這使得蠻力變得困難。 此外,密碼散列函數使用諸如關鍵伸展進一步減緩蠻力嘗試。 示例 關鍵的擴展技術之一是重複對字符串進行哈希處理(首先計算哈希值,然後計算哈希值的哈希值,依此類推)。

PostgreSQL 中的密碼散列

pgcrypto 擴大

在 PostgreSQL 中, pgcrypto 該擴展程序具有加密密碼的必要功能。 從 pgcrypto 它是一個內置擴展,您無需下載或安裝任何其他軟件。 啟用擴展:

                      
                        CREATE EXTENSION pgcrypto ;

                      
                    

哈希函數總是為給定的輸入字符串生成相同的哈希值。 這導致兩個主要問題:

  1. 如果兩個用戶有相同的密碼,那麼他們的哈希值是相同的。 最好所有散列都是唯一的。

  2. 蠻力在計算上是昂貴的。 因此,攻擊者經常使用彩虹桌而不是蠻力。 這些表包含常用密碼的預計算哈希值。

這兩個問題都通過使用兩個輸入值計算散列來解決:輸入字符串(密碼)和鹽。 salt 是一個偽隨機生成的字符串,有助於確保散列的唯一性。 即使兩個用戶密碼相同,他們的salt值也會不同。 因此,所有哈希值都是唯一的。

gen_salt() 功能

鹽是使用 gen_salt() 功能。 通常,此函數採用一個參數: type . 語法是:

                      
                        -- pseudocode

gen_salt(type) ;

                      
                    

type 該參數表示用於散列的加密算法的類型。 它可以採用四個可能值之一:

  • des – 用於數據加密標準 (DES) 算法

  • xdes – 對於擴展的 DES 算法

  • md5 – 用於 MD5 消息摘要算法

  • bf – 對於 Blowfish 算法

例如,使用擴展 DES 算法生成鹽:

                      
                        SELECT gen_salt('xdes') ;

                      
                    

同樣,要使用 MD5 算法生成鹽:

                      
                        SELECT gen_salt('md5') ;

                      
                    

請注意,salt 還編碼有關用於哈希的加密算法類型的信息,以及(如果有)哈希參數。 嘗試反復生成相同類型的鹽; 請注意,相同類型的鹽遵循一致的模式。

保存新密碼

當用戶創建新密碼(或更改其現有密碼)時,數據庫需要存儲密碼(的哈希值)。 哈希是使用 crypt() 功能。

crypt() 功能

crypt() 功能是基於Unix 地穴庫. 根據以下參數生成哈希:

  1. 密碼字符串

  2. 鹽的價值

的語法 crypt() 功能是:

                      
                        -- pseudocode

crypt(password_string, salt_string) ;

                      
                    

salt_string 該參數是使用 gen_salt() 功能。 例如使用 md5 計算哈希的算法:

                      
                        -- pseudocode

SELECT crypt(password_string, gen_salt('md5')) ;

                      
                    

無鹽散列

模擬使用 crypt() 沒有隨機鹽的函數,使用常量字符串, salt , 為鹽。 生成密碼的哈希值, supersecurepassword :

                      
                        SELECT crypt('supersecurepassword', 'salt') ;

                      
                    

以上命令生成密文 saUkChKIZTKFs . 非隨機化(或無鹽)鹽會導致可預測的哈希值,並使系統容易受到彩虹表攻擊。 因此,有必要使用隨機鹽。

使用隨機鹽對密碼進行哈希處理

為密碼字符串生成哈希 supersecurepassword 例如,使用 MD5 算法:

                      
                        SELECT crypt('supersecurepassword', gen_salt('md5')) ;

                      
                    

複製上述命令生成的哈希值。 您將在下一節中使用它。

檢查輸入的密碼是否正確

當現有用戶登錄時,他們輸入用戶名和密碼。 服務器需要檢查輸入的密碼是否與存儲的密碼匹配。 這是通過調用相同的 crypt() 具有以下參數的函數:

  1. 輸入的密碼

  2. 實際密碼的存儲散列(而不是生成的鹽)

如果輸入的密碼與實際密碼相同,則輸入密碼的哈希值與存儲的(實際密碼的)哈希值匹配。

一般語法是:

                      
                        --pseudocode

crypt(entered_password, stored_hash_of_actual_password) ;

                      
                    

假設用戶輸入了正確的密碼, supersecurepassword . 要對此進行測試,請致電 crypt() 工作如下:

                      
                        SELECT crypt('supersecurepassword', 'generated_hash_value_from_actual_password') ;

                      
                    

上面的第二個參數是上一個命令生成的散列值——粘貼你之前複製的散列值。 此命令生成與生成實際密碼相同的散列。

現在,假設用戶輸入了錯誤的密碼, wrong_password . 撥電至 crypt() 工作如下:

                      
                        SELECT crypt('wrong_password', 'generated_hash_value_from_actual_password') ;

                      
                    

輸出哈希值與實際密碼哈希不同。

換句話說,調用 crypt() 帶有字符串的函數 ( string1 ) 和散列 ( hash1 )那個字符串( string1 ) 生成相同的散列 ( hash1 ).

實際使用

在實際使用中,哈希是從表中存儲和查詢的。 本節中的示例展示瞭如何執行此操作:

創建一個 user_account 包含三列的表:自動生成的 ID、用戶名和用戶的密碼哈希值:

                      
                        CREATE table user_account (user_id SERIAL, user_name VARCHAR(10), password_hash VARCHAR(100)) ;

                      
                    

在表中插入一行測試數據:

                      
                        INSERT INTO user_account (user_name, password_hash) 

VALUES ('user1', crypt('user1_password', gen_salt('md5'))) ;

                      
                    

上面的命令插入用戶名 user1 和密碼的 MD5 散列, user1_password .

要更改用戶密碼的(哈希值),請使用 SQL UPDATE 領域:

                      
                        UPDATE user_account 

SET password_hash = crypt('user1_new_password', gen_salt('md5')) 

WHERE user_name="user1" ;

                      
                    

要將輸入的密碼與正確的密碼相匹配,請調用 crypt() 使用輸入的密碼和正確密碼的哈希作為鹽來運行。

如果用戶嘗試使用正確的密碼登錄, user1_new_password :

                      
                        SELECT (password_hash = crypt('user1_new_password', password_hash)) 

    AS password_match 

FROM user_account 

WHERE user_name="user1" ;

                      
                    

這應該輸出 t 為了 true 作為價值 password_match .

如果登錄嘗試使用不正確的密碼, user1_wrong_password :

                      
                        SELECT (password_hash = crypt('user1_wrong_password', password_hash)) 

    AS password_match 

FROM user_account 

WHERE user_name="user1" ;

                      
                    

這應該輸出 f 為了 false 作為價值 password_match .

高級使用

當使用擴展 DES 或 Blowfish 算法計算散列時,可以自定義(調整)算法生成散列所經歷的迭代次數。 在這種情況下, gen_salt() 函數可以接受一個額外的參數, iter_count , 為迭代次數。 語法是:

                      
                        -- pseudocode

gen_salt(type, iter_count)

                      
                    

在上面的聲明中, type 或者是 xdes 任何一個 bf .

  1. 擴展 DES 的迭代次數 ( xdes ) 算法可以是 1 到 16777215 之間的奇數。默認為 725。

  2. 河豚的迭代次數 ( bf ) algorithm 可以是 4 到 31 之間的整數。默認值為 6。

如果在 N 次迭代後生成了哈希,則任何暴力嘗試也必須生成 N 次密碼猜測哈希。 N 的值越大,暴力破解密碼哈希的難度就越大。 然而,使散列計算太慢對於常規使用來說是不切實際的。 作為演示,散列密碼 supersecurepassword 使用具有默認迭代次數 6 的 Blowfish 算法:

                      
                        SELECT crypt('supersecurepassword', gen_salt('bf', 6)) ;

                      
                    

注意大約需要多長時間(使用計算機或手機上的時鐘)。 現在運行具有更多迭代次數的相同散列函數:

                      
                        SELECT crypt('supersecurepassword', gen_salt('bf', 30)) ;

                      
                    

這需要更長的時間。 如果花費的時間太長,請使用取消操作 CTRL + C 並用較少的迭代次數重試。

如果您輸入無效的迭代次數,它會拋出如下錯誤:

                      
                        ERROR:  gen_salt: Incorrect number of rounds

                      
                    

哈希函數設置

選擇迭代次數是可用性和安全性之間的平衡。 迭代次數越多,計算哈希所需的時間就越長。 這使它的用戶友好性降低,但也通過阻止暴力嘗試來提高安全性。 一個常見的建議是選擇迭代次數,以便標準服務器硬件每秒可以計算 4 到 100 個哈希值。

結論

實際上,密碼是次優的解決方案。 在服務器上存儲密碼(即使是散列格式)將所有責任都推給了應用程序開發人員和管理員。 身份驗證是一個複雜的話題,建議使用專門從事它的專門服務提供商,例如OpenID. 允許用戶使用一組標準憑據(例如他們的 Google 帳戶)登錄對用戶和應用程序開發人員都有好處。

但是,基於密碼的身份驗證在很多情況下都很方便,並且被廣泛使用。 因此,必須小心謹慎地安全存儲用戶密碼並保護用戶安全和應用程序完整性。

警告

  1. 本文僅討論如何在數據庫中安全地存儲密碼。 這也稱為靜態數據保護。 同樣重要的是保護傳輸中的數據。 通過 Internet 傳輸密碼時(例如從 Web 前端)始終確保 1) 使用 HTTPS 和 2) 將登錄憑據作為表單數據發送,而不是 URL 參數。

  2. 在密碼被加密之前,它以純文本形式提供。 這使您容易受到有權訪問數據庫和/或 Web 服務器的不法員工的攻擊。 因此,上述系統是建立在信任應用程序的開發者和管理員的基礎上的。 如果無法建立這種信任,請考慮使用客戶端加密方法。 這樣,純文本密碼永遠不會出現在服務器上。

  3. 此外,密碼散列僅對獲得對數據庫(或數據庫轉儲)的只讀訪問權限的攻擊者有效。 對數據庫具有寫入權限的攻擊者會發現更容易簡單地覆蓋密碼哈希。

最終,沒有任何安全措施是真正萬無一失的。 增加的安全措施意味著更複雜的系統設計或更不友好的系統。 您需要根據特定應用程序的安全需求取得平衡。

文章標題 名稱(可選) 電子郵件(可選) 描述

發送建議

相關文章