你的密碼安全嗎
如何判斷自己的密碼已經泄露?
一些安全廠家會定期收集互聯網上被拖庫的相關數據賬號信息,我使用了Avast提供的一個工具來查詢自己的密碼是否被泄露(https://www.avast.com/hackcheck/) ,這裡只需要輸入你的郵箱地址,就能查詢到你關聯地址的郵箱的登陸賬號或者密碼是否被泄漏。我在這裡輸入了我之前比較常用的QQ郵箱賬號,可以發現我的密碼已經遭到泄漏,泄露的來源分別是Adobe、CSDN和京東。其中Adobe和京東泄露的是加密後的密碼,因此還算可控,CSDN泄露的是明文密碼,所以非常危險。

如果你使用了Chrome瀏覽器保存密碼,在Chrome的安全設置中,也有提醒你密碼已經被泄露的選項。勾選這個選項,在你的密碼被泄露時就會給你發出警告。

除此之外,還有一些黑灰產的途徑可以查詢到自己的密碼和個人隱私信息是否被泄露(如Telegram社工庫之類的途徑),因為涉及到相關法律問題,因此不仔細分享。但是可以得到的結論是:我們的密碼並不安全。
為什麼你的密碼可能不安全 密碼是如何存儲的通常情況下,密碼都應該在數據庫中被加密存儲,使用明文存儲密碼是開發信息系統的大忌,但是事實上,能夠做到這一點的信息平台開發商都不多,對於一些「小作坊式」開發的廠家,密碼很多情況下都是明文保存的。
對於密碼的加密,通常會採用一些Hash算法,在註冊賬戶時,對賬號的密碼進行Hash計算後,再進行存儲,以MD5算法為例:
MD5 ("password") = 5f4dcc3b5aa765d61d8327deb882cf99
若用戶的密碼為password,在Hash計算後,得到的值為5f4dcc3b5aa765d61d8327deb882cf99,在進行存儲時,將得到的Hash值存入數據庫的密碼字段。
在用戶登陸時,只要將用戶密碼的輸入再做一次MD5計算,將得到的Hash值與數據庫存儲的值進行比較,就可以判斷用戶的密碼輸入是否正確了。
這樣做的好處是,即使數據庫中用戶密碼的相關信息字段被泄露,用戶真正的密碼明文是不知道的。這其中利用到了Hash算法的不可逆性:在有限算力的情況下,Hash算法只能從輸入得到輸出,無法從輸出反推得到輸入。因為這樣的特性,Hash算法通常用在保存密碼,信息和文件完整性校驗等地方。常用的Hash算法有MD5、SHA、HMAC等。
但是這樣的算法也會存在一些問題,雖然無法從輸出反推得到輸入,但是可以建立一個密碼到其哈希值的字典(即彩虹表),在破解時只需要查詢彩虹表的值,即可得到對應的密碼明文數據。
為了解決這樣的問題,我們可以在計算哈希值時採用加鹽的方式。對於每個用戶,鹽值都採用隨機數。在計算哈希時,將用戶輸入的密碼和鹽值進行組合之後再進行計算,最後將加鹽的Hash值存入密碼字段。如使用用戶的隨機UID32142作為鹽值,計算方法為:
MD5 ("password32142") = 970e6155f135079c9c1b9d3302b957b5
通過隨機加鹽的方式,可以有效地避免彩虹表攻擊造成密碼原文泄露。MD5是一種比較簡單的Hash算法,因此在不推薦在實際使用的時候採用MD5算法存儲密碼,通常情況下我們會有一些更適合密碼的哈希算法比如Bcrypt算法來對密碼計算Hash,Bcrypt算法在生成時就會生成一段隨機的鹽值,並且將鹽值保存在輸出的結果中,這樣的好處是即使是同樣的輸入,得到的輸出也是不同的,能夠有效避免密碼原文泄露的風險。
Bcrypt("password")=10$7ioawRWnYEBZwGE7QAGL0.oa5suk8/NjdAS0RSlqy8Kehplft8CBy
密碼過於簡單談到密碼的安全性,首先需要強調的是幾條密碼保護的基本規則:
但是實際上對於上述幾點,能夠完美堅持做到每一點的同學我相信一定不多。大家通常使用的密碼都是與自己相關的規律組合,因此存在着一定的隱私泄露的風險。
業務系統存在漏洞事實上,所有的業務系統都是由人來開發的。但是人並不是機器,因此總是會存在着犯錯的可能性,這是無法避免的,我們只能從流程上來規範以儘量降低發生錯誤的概率。但是就如Meta/Twitter這樣的大公司都存在着各種各樣的密碼泄露的問題,國內的BAT等公司也都曾爆出過密碼被泄漏的安全問題。除此之外,還有着XCode Ghost/Docker Hub數據泄露等供應鏈攻擊,就連流程詳盡規範的大公司都防不勝防,就更不用說你手機上那些不知名小公司的小App了。
就連我們自己的業務系統也不例外:在某日排查線上bug的時候,我也發現在我們業務使用到的某上游服務中,也有把用戶的密碼直接明文打印在日誌中的行為。根據公司的數據安全管理規定數據分類分級表,用戶操作行為日誌等屬於L3級別數據 ,用戶賬號的密碼等數據屬於L4級別數據,因此在日誌中,不可以出現用戶明文密碼相關的敏感字段。這樣任何擁有L3級別日誌查詢權限的用戶,都可以獲取到L4級別的保密數據,這樣的操作相當於把L4級別的數據範圍擴大到了L3級別上,因此是違法相關數據保護規定的行為。出現這樣的問題可能是無意為之,但是還是暴露了我們在代碼安全上的意識還有需要加強的地方。
基礎設施/硬件漏洞對於業務系統產生的一些安全漏洞,我們可以通過一些流程和規範儘量保障,但是對於一些基礎設施和硬件的漏洞,就很難能夠通過這樣方式來預防了,如以下的幾個例子:
上述這樣的一些漏洞都會造成服務端的風險,從而產生被黑灰產拖庫導致用戶密碼泄漏的風險。
如何解決密碼不安全的問題總的來說,實現絕對的安全是不可能的,所以我們只能在有限的條件下,尋求相對的安全。為了能夠實現這樣相對的安全,現有的密碼驗證方式矛盾的地方在於:安全等於麻煩,為了實現賬戶的安全,你需要記憶各種複雜的密碼和驗證方式,要想省事的話就只能使用一些簡單方便記憶的密碼,並且在不同平台使用相同的密碼,這樣雖然省事了,但是可能會帶來更多的麻煩:密碼泄露導致賬戶被盜造成個人隱私和財產的損失。
怎麼解決安全和麻煩這樣的問題呢,FIDO(Fast IDentity Online)聯盟想出了比較完美的解決方案:

既然密碼不安全,那麼沒有密碼不就可以解決密碼不安全的問題了嗎?
FIDO聯盟和W3C為了解決這樣的魚與熊掌的問題,希望可以制定相關的技術規範,定義一套開放的、可擴展的、能互用的機制,減少用戶 在認證時對密碼的依賴,於是WebAuthn應運而生。
WebAuthn是什麼WebAuthn全稱為Web Authentication,是一個使用非對稱加密的方式,在Web應用上替代密碼/短信驗證碼等方式在Web應用中進行註冊、登陸、雙重認證(2FA) 的API。能夠解決釣魚攻擊、數據泄漏的安全問題,同時也能提高應用的用戶體驗(不必記憶複雜的密碼)。
通過使用WebAuthn的API,我們可以方便實現指紋、人臉等生物信息的認證或者是加密硬件如USB Key(如網銀使用的U盾)、藍牙設備等來驗證身份信息,在方便的同時能夠保障安全和隱私。
兼容性
從CanIUse中可以看到,Web Authentication在桌面端瀏覽器的兼容性較好,絕大多數的桌面端瀏覽器都能支持,但是在移動端的兼容性較差:iOS僅在14.5以上的版本完全支持,安卓支持Android5以上版本,除此之外,由於依賴TPM模塊和指紋/人臉識別等傳感器,硬件配置也會影響WebAuthn的可用性。
需要注意的是,WebAuthn只能在HTTPS或者localhost中使用,不支持HTTP環境,即只有在保證當前的環境是安全的前提下,WebAuthn的這些實現才是有意義的。
一些概念 2FA2FA的全稱為2 Factor Authentication,即雙因子認證,通常是結合密碼以及其他標誌(如OTP、USB Key、短信驗證碼、指紋等生物特徵)進行認證的方式。現在2FA的使用已經比較普遍,許多廠家會結合風控系統在密碼驗證的同時結合其他認證方式,保證授權是可信的。比如說在新的設備上登錄社交App時,通常除了密碼之外,還會需要其他的認證方式。
OTPOTP即一次性密碼(One-time password),OTP的計算通常是基於時間戳的,密碼的生成具有有效期(通常是30s)。使用OTP能夠有效地避免「重放攻擊」,一般的靜態密碼,在使用時如果設備上有可以監聽鍵盤輸入的木馬等程序,很容易造成密碼的泄露。OTP解決了這樣的問題,即保證了泄露的密碼也是沒有用的(因為只能使用一次,且在極短時間內有效)。通常會在安全性要求比較高的情況下使用:如網遊賬號(左圖,盛大密寶,我小時候玩盛大遊戲時,使用到的OTP密碼器)或企業認證(右圖,Seal VPN)。OTP的缺點在於每次都要打開對應的軟件或者相關的OTP設備來獲取一次性密碼,使用的便捷程度上存在一定的不便。

因為OTP的計算是基於時間戳的,之前我有遇到過一種情況,OTP怎麼輸入都不對,後來發現是我本地設備的NTP存在問題,沒有和NTP Server進行同步導致客戶端獲取到的時間不對,因此計算得到的OTP也不對。如果下次大家有遇到類似的情況,記得先檢查一下本地設備的時間是否正確。
對稱加密對稱加密算法的特點是,在加密和解密的過程中,使用的密鑰是相同的,因此通訊的雙方需要交換並持有相同的密鑰。常見的對稱加密算法有AES/DES等。
優點:
缺點:
和對稱加密不同的是,在非對稱加密中,需要一組密鑰對來進行加密和解密。這一組密鑰對我們稱為公鑰(可以公開的密鑰)和私鑰(需要保密的密鑰)。除了在WebAuthn中有用到之外,其他地方也非常常見,比如說JWT/HTTPS(HTTPS既有非對稱加密,也有對稱加密)/SSH等。非對稱加密有這樣的一些特點:
常用的非對稱加密算法有RSA/ECC等。
優點
缺點

我們在這裡所指的FIDO協議,通常指UAF/U2F/FIDO2這三種協議中的任意一種,因為這三種協議的工作原理都是類似的,區別只在於結構的不同。FIDO要實現在沒有其他信息的情況下證明使用者的身份,需要引入一種零知識證明的方式來解決。FIDO基於一種挑戰應答方式(Challenge-Response)機制的方式進行工作:每次認證時,服務端給客戶端發送一個隨機的挑戰字符串,客戶端接收到這個挑戰後,做出相對應的應答。其工作流程如下:
依賴方(服務端)向客戶端發送challenge和憑證 ID,客戶端再附加依賴方的信息將其發送給身份認證器。身份認證器會首先檢查用戶是否存在,然後通過彈窗等請求用戶按下按鈕或者使用指紋傳感器等進行完整的用戶身份的認證。驗證完成後,身份認證器使用憑證ID所標識的對應私鑰對challenge進行加密,再將斷言返回給客戶端,客戶端將相關身份認證器的提供信息轉發給依賴方。然後依賴方會檢查返回的信息,確保響應包含符合預期的來源和challenge,然後用服務端已經存儲的公鑰來驗證簽名。這個工作流中的任何一個環節失敗,即說明存在釣魚攻擊等場景,認證就會失敗。
Challenge-Response 的認證方式使用非常廣泛,很多協議如Kerberos/SSH也採用了類似的方式。
過程WebAuthn使用的工作流一般分為兩種:註冊(Registration)和驗證(Authentication),註冊用來給系統添加用戶相關的身份認證信息,驗證用來校驗和檢查用戶的身份是否正確。
註冊
註冊的流程如下:
0⃣️ 瀏覽器向依賴方發送註冊請求:
如點擊一個Button開始註冊用戶,這裡的協議和格式都不在WebAuthn 的標準範圍之內,可以由客戶端自行實現
1.依賴方向瀏覽器發送挑戰、依賴方信息和用戶信息:
這裡的協議和格式也不在WebAuthn的標準範圍內,可以是基於HTTPS的XMLHttpRequest/Fetch實現,也可以使用任意其他協議(如ProtoBuf),只要客戶端能夠和服務端發送對應的請求消息即可,但是需要保證環境是安全的(HTTPS)
2.瀏覽器向認證器發送用戶信息、依賴方信息和客戶端信息的Hash值
在瀏覽器內部,瀏覽器將驗證參數並用默認值補全缺少的參數,然後這些參數會變為clientDataJSON。調用 create() 的參數與clientDataJSON 的 SHA-256 哈希一起傳遞到認證器(只有哈希被發送是因為與認證器的連接可能是低帶寬的 NFC 或藍牙連接,之後認證器只需對哈希簽名以確保它不會被篡改)
3.認證器請求用戶確認,然後創建一對公私鑰,用私鑰創建證明
在進行這一步時,認證器通常會以某種形式要求用戶確認,如輸入 PIN、使用指紋、人臉掃描等,以證明用戶在場並同意註冊。之後,認證器將創建一個新的非對稱密鑰對,並安全地存儲私鑰以供將來驗證使用。公鑰則將成為證明的一部分,被在製作過程中燒錄於認證器內的私鑰進行簽名。這個私鑰會具有可以被驗證的證書鏈。
4.認證器把簽名和公鑰以及憑證ID發送給瀏覽器
新的公鑰、全局唯一的憑證 ID 和其他的證明數據會被返回到瀏覽器,成為 attestationObject。
5.瀏覽器將證明數據和客戶端信息發送給依賴方
這裡和前面一樣,可以使用任意的格式或協議,前提是需要保證環境安全
6.依賴方用公鑰驗證發送的challenge,如果成功則將對應的公鑰與用戶綁定存儲在數據庫中,完成註冊流程
在這一步,服務器需要執行一系列檢查以確保註冊完成且數據未被篡改。步驟包括:

在驗證中,瀏覽器向依賴方發送某個用戶的驗證請求後的行為如下:
1.依賴方向瀏覽器發送挑戰
2.瀏覽器向認證器發送依賴方ID、挑戰和客戶端信息的Hash值,
3.認證器請求用戶確認,然後通過依賴方ID找到對應的私鑰,使用私鑰簽名挑戰(斷言)
4.認證器將斷言信息和簽名後發送給瀏覽器
5.瀏覽器將簽名、驗證器數據以及客戶端信息發送給服務器
6.服務器使用用戶綁定的公鑰驗證挑戰是否與發送的一致,如果驗證通過則表示認證成功
區別註冊和驗證之間的主要區別在於:
要使用 WebAuthn,需要先了解Credentials Management API,因為Web Authn API繼承自Credentials Management API。Credentials Management API允許網站與用戶代理的密碼系統進行交互,以便網站能夠以統一的方式處理站點憑證,而用戶代理為憑證管理提供更好的幫助。通常用來存儲和檢索用戶、聯合賬戶憑證(FederatedCredential,如OpenID Connect)、和非對稱密鑰對憑證(WebAuthn使用的就是非對稱密鑰對的憑證)。
註冊:navigator.credentials.create(options)通過這個API,使用publicKey選項時, 創建一個新的憑據,無論是用於註冊新賬號還是將新的非對稱密鑰憑據與已有的賬號關聯。其中options的結構如下:
constoptions={publicKey:{rp:{id:"example.com",name:"example.com",},user:{name:"username@example.com",id:userIdBuffer,displayName:"UserName",},pubKeyCredParams:[{type:"public-key",alg:-7}],challenge:challengeBuffer,authenticatorSelection:{authenticatorAttachment:"platform"},},}rp: 依賴方
user:用戶信息
pubKeyCredParams(Array):公鑰算法的list,指明依賴方可以接受哪些加密算法
對於 pubKeyCredParams,通常我們只需添加 ES256 (alg: -7) 算法即可兼容大部分外部認證器,此外,再添加 RS256 (alg: -257) 算法即可兼容大部分平台內置認證器(如 Windows Hello/TouchID)。前端添加算法之後,後端也需要相應的算法支持進行簽名校驗。
authenticatorSelection?:用於過濾想要使用的認證器
authenticatorAttachment?:"platform" | "cross-platform"
userVerification?:指定認證器是否需要驗證「用戶為本人」,否則只須「用戶在場」。具體驗證過程取決於認證器(不同認證器的認證方法不同,也有認證器不支持用戶驗證),而對驗證結果的處理情況則取決於依賴方。該參數可以為以下三個值之一:
residentKey?:客戶端密鑰駐留,創建可發現憑證的選項,用於實現無用戶名登錄
excludeCredentials?(Array): 包含已向用戶註冊的憑據列表。然後將此列表提供給驗證器,如果驗證器識別其中任何一個,它會取消操作並返回錯誤 CREDENTIAL_EXISTS,從而防止同一驗證器的重複註冊。數組中的每一項都是一個公鑰憑證對象,包含以下屬性:
type(String):值只能為"public-key"
id(Uint8Array):需要排除的憑證ID
transports?(String[]):指定認證器和用戶代理的通訊方式,可以是以下的取值
調用完創建憑證的API後,瀏覽器會彈出一個認證器的選擇彈窗(具體取決於authenticatorAttachment 這個字段的設置,如果指定了可能就不會有認證器選擇彈窗的這個環節),在這個彈窗中,用戶可以選擇使用哪個認證器來生成憑證:

以選擇「本設備」為例,會調用系統內置的認證器(在macOS上即表現為使用Touch ID/Windows上為Windows Hello)進行認證:

使用指紋認證成功後,WebAuthn API會通過Promise返回PublicKeyCredential對象,包含其對應的公鑰憑證信息如下:

PublicKeyCredential的結構如下:
response:響應,認證器的結果,包含憑證創建和證明信息
CBOR是一種提供良好壓縮性,擴展性強,不需要進行版本協商的二進制數據交換形式。

rpIdHash:依賴方ID的SHA-256哈希值,32bytes
flags:標誌位
signCount:簽名計數
fmt:證明的格式
authData:證明數據:包含公鑰、憑證id等信息
認證器應實現簽名計數器功能。這些計數器在概念上認證器為每個憑據存儲,或為整個認證器全局存儲。憑據的簽名計數器的初始值在認證器註冊返回的認證器數據的值中指定。對於每個成功的認證器驗證操作,簽名計數器都會遞增一些正值,並且後續值將再次返回到認證器數據中的 WebAuthn 信賴方。簽名計數器的目的是幫助信賴方檢測克隆的認證器。克隆檢測對於保護措施有限的認證器更為重要。
信賴方存儲最新認證器驗證操作的簽名計數器。(或者來自認證器的計數器註冊操作,如果沒有認證器驗證曾經對憑據執行過。在後續認證器驗證操作中,信賴方將存儲的簽名計數器值與斷言的認證器數據中返回的新值進行比較。如果任一值為非零,並且新值小於或等於存儲的值,則可能存在克隆的認證器,或者認證器可能出現故障。
attestedCredentialData

其中最重要的參數之一是 origin,它是 clientData 的一部分,同時服務器將能在稍後驗證它。
實際上在默認情況下,註冊時認證器並不會對挑戰進行簽名(首次使用時信任模型),attestationObject 並不會包含簽名後的挑戰。只有依賴方明確要求證明且用戶同意後認證器才會對挑戰進行簽名(具體實現據情況會有所不同)。
得到上面的數據後,我們需要把上面的數據回傳給依賴方即後端進行校驗,後端的操作至少如下:
1.校驗rpIdHash
2.檢查 UV 和 UP
3.存儲證明的計數
4.存儲公鑰
驗證:**navigator.credentials.get(options)**這個API也需要傳入一個options對象,options的結構和註冊的時候比較類似:
constoptions={publicKey:{challenge:challengeBuffer,//rpId:'',userVerification:'',}}allowCredentials?(Array):使用用戶帳戶註冊的憑據標識符列表。此列表將發送到所有可用設備。如果驗證器能夠識別列表中的憑證,它將通過提示用戶操作(例如按下按鈕或指紋)來啟動斷言生成過程。如果認證器無法識別憑證,它將返回錯誤,從而通知平台它不是正確的設備。第一個成功的設備結果將完成獲取斷言請求。數組中的每一項都是對象,包含以下屬性:
和create方法一樣,調用get這個方法也會返回一個Promise,可以得到認證器返回PublicKeyCredential的對象,結構如下:

response:響應,認證器的結果,包含憑證創建和證明信息
認證器的信息和上方的attestedCredentialData的數據結構類似,只是這裡不包含公鑰的信息了。
得到這些後,我們只要把PublicKeyCredential對象的內容發送給依賴方即服務端驗證即可,依賴方至少要做這樣一些的校驗:
1.驗證 rpIdHash
2.檢查 UV 和 UP
3.驗證簽名的計數,並更新數據庫中簽名的計數
4.公鑰校驗簽名
可發現憑據實現完上述的流程後,我們就可以使用指紋等進行登錄了,但是這時我們還是會需要使用到用戶名,然後才能進行身份認證。如何在不輸入用戶名的情況下進行認證呢?許多驗證器提供了這個功能,我們稱為可發現憑據(Discoverable Credentials)。
為什麼普通的 WebAuthn 為什麼不能實現無用戶名登錄?大部分認證器為了實現無限對公私鑰,會將私鑰通過加密後包含在憑證 ID 中發送給依賴方,這樣認證器本身就不用存儲任何信息。不過,這就導致需要身份認證時,依賴方必須通過用戶名找到對應的憑證 ID,將其發送給認證器以供其算出私鑰。要不輸入用戶名,則需要認證器將私鑰在自己的存儲中也存儲一份。這樣,依賴方無需提供憑證 ID,認證器就可以通過依賴方 ID 找到所需的私鑰並簽名。因此這個特性需要認證器能夠儲存用戶ID,即上面的userHandle字段。
為了實現安全隔離,認證器的存儲通常會和平台隔離,擁有獨立的存儲空間(如在Mac平台下,TouchID有獨立的安全芯片T1/T2,運行獨立的操作系統BridgeOS),一般情況下認證器能夠永久存儲的私鑰數量是有限的,所以只有在真正需要需要無用戶名登錄時才啟用這個特性

實現:
[1] MDN: Web Authentication API
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Authentication_API
[2] W3C文檔:Web Authentication API Spec
https://www.w3.org/TR/webauthn-3/
[3] WebKit 文檔:Meet Face ID and Touch ID for the web
https://webkit.org/blog/11312/meet-face-id-and-touch-id-for-the-web/
[4] Medium: Introduction to WebAuthn API
https://medium.com/webauthnworks/introduction-to-webauthn-api-5fd1fb46c285
[5] 談談WebAuthn
https://flyhigher.top/develop/2160.html
[6] WebAuthn介紹與使用
https://obeta.me/posts/2019-03-01/WebAuthn%E4%BB%8B%E7%BB%8D%E4%B8%8E%E4%BD%BF%E7%94%A8
[7] WebAuthn Guide
https://webauthn.io/
[8] Apple 安全隔離介紹
https://support.apple.com/zh-cn/guide/security/sec59b0b31ff/web
- END -❤️ 謝謝支持
以上便是本次分享的全部內容,希望對你有所幫助^_^
喜歡的話別忘了分享、點讚、收藏三連哦~。
歡迎關注公眾號ELab團隊收貨大廠一手好文章~
我們來自字節跳動,是旗下大力教育前端部門,負責字節跳動教育全線產品前端開發工作。
我們圍繞產品品質提升、開發效率、創意與前沿技術等方向沉澱與傳播專業知識及案例,為業界貢獻經驗價值。包括但不限於性能監控、組件庫、多端技術、Serverless、可視化搭建、音視頻、人工智能、產品設計與營銷等內容。
歡迎感興趣的同學在評論區或使用內推碼內推到作者部門拍磚哦 🤪
字節跳動校/社招投遞鏈接:https://job.toutiao.com/s/YK7wdrk
內推碼:DYJ95U9