稱(chēng)加密與非對(duì)稱(chēng)加密:從算法原理到混合加密實(shí)戰(zhàn))
對(duì)稱(chēng)加密和非對(duì)稱(chēng)加密這兩個(gè)詞幾乎是搞開(kāi)發(fā)的人都繞不開(kāi)的坎。剛?cè)胄心菚?huì)兒我一度以為它們只是兩種可以替換著用的加密套路直到真正做接口加密、文件加密、配合HTTP協(xié)議調(diào)優(yōu)時(shí)才意識(shí)到背后其實(shí)是兩套完全不同的安全模型。選錯(cuò)方案或者混用算法輕則性能崩盤(pán)重則業(yè)務(wù)數(shù)據(jù)直接裸奔。這篇文章我會(huì)從底層邏輯、主流算法選型、混合加密實(shí)戰(zhàn)一直聊到高頻報(bào)錯(cuò)和排查套路把這兩條技術(shù)路線一次講透。新手能照著做老手也能查漏補(bǔ)缺。1. 兩套加密體系的底層邏輯拆解1.1 對(duì)稱(chēng)加密一把鑰匙走到底對(duì)稱(chēng)加密的核心思想非常簡(jiǎn)單加密和解密用的是同一個(gè)密鑰。你拿這個(gè)密鑰把明文變成密文接收方拿同一個(gè)密鑰把密文還原成明文全程沒(méi)有任何第二把鑰匙參與。舉個(gè)例子你把一份文件鎖進(jìn)保險(xiǎn)箱用一把鑰匙鎖上再把箱子寄給對(duì)方對(duì)方用同一把鑰匙打開(kāi)。這個(gè)模型的關(guān)鍵假設(shè)是這把鑰匙必須在通信之前就安全地送到對(duì)方手里。如果鑰匙在傳輸過(guò)程中被截獲那加密就形同虛設(shè)。在工程實(shí)現(xiàn)上對(duì)稱(chēng)加密算法普遍依賴(lài)兩類(lèi)操作混淆和擴(kuò)散。混淆讓密文和密鑰、明文之間的關(guān)系變得極其復(fù)雜攻擊者看不出規(guī)律擴(kuò)散則讓明文中哪怕一個(gè)bit的變化也能擴(kuò)散到密文的大部分位這就是所謂雪崩效應(yīng)。對(duì)稱(chēng)加密最大的優(yōu)勢(shì)是快。硬件上普遍有AES指令集加速一塊支持AES-NI的處理器做AES加解密吞吐量能做到每秒幾個(gè)GB級(jí)別。所以凡是涉及大量數(shù)據(jù)加密的場(chǎng)景比如文件加密、數(shù)據(jù)庫(kù)加密、TLS數(shù)據(jù)傳輸?shù)讓訋缀醵际菍?duì)稱(chēng)加密在干活。它最大的痛點(diǎn)也寫(xiě)在明面上密鑰怎么安全分發(fā)。兩個(gè)人從來(lái)沒(méi)碰過(guò)面沒(méi)有任何安全通道怎么把同一把密鑰同時(shí)送到雙方手上在互聯(lián)網(wǎng)場(chǎng)景里這是個(gè)致命問(wèn)題。1.2 非對(duì)稱(chēng)加密公鑰私鑰分工協(xié)作非對(duì)稱(chēng)加密的模型和對(duì)稱(chēng)加密完全不同。它生成的不是一把鑰匙而是一對(duì)數(shù)學(xué)上關(guān)聯(lián)的密鑰公鑰和私鑰。公鑰可以公開(kāi)分發(fā)私鑰只有自己持有。這里有兩組經(jīng)典操作很多人容易記混。第一組是公鑰加密、私鑰解密。任何人拿到你的公鑰都能把消息加密后發(fā)給你但只有你的私鑰能解開(kāi)。這解決的是機(jī)密性問(wèn)題??梢灶?lèi)比成一個(gè)帶投信口的郵箱任何人都能從投信口把信塞進(jìn)去但只有拿鑰匙的郵遞員能打開(kāi)箱門(mén)取信。第二組是私鑰簽名、公鑰驗(yàn)簽。你用私鑰對(duì)一段數(shù)據(jù)做簽名別人用你的公鑰驗(yàn)證簽名是否有效。這解決的是身份認(rèn)證和完整性防篡改問(wèn)題。類(lèi)比的話就像你在文件上蓋了一個(gè)只有你能蓋的私章別人拿著你的公開(kāi)印模來(lái)核對(duì)真假。非對(duì)稱(chēng)加密的數(shù)學(xué)基礎(chǔ)通常是某種單向函數(shù)難題。經(jīng)典RSA依賴(lài)的是大整數(shù)分解的困難性兩個(gè)大素?cái)?shù)相乘很容易但把一個(gè)很大的合數(shù)分解回兩個(gè)素?cái)?shù)極難。橢圓曲線密碼則依賴(lài)橢圓曲線離散對(duì)數(shù)問(wèn)題在同樣安全強(qiáng)度下密鑰長(zhǎng)度可以做到比RSA短得多計(jì)算開(kāi)銷(xiāo)也更小。非對(duì)稱(chēng)加密的優(yōu)點(diǎn)是密鑰分發(fā)壓力小公鑰隨便公開(kāi)私鑰不出本地。缺點(diǎn)是慢而且慢得很明顯。RSA做一次私鑰解密可能比AES做一次分組加密慢兩到三個(gè)數(shù)量級(jí)。所以它只適合加密少量數(shù)據(jù)比如密鑰本身、哈希值不可能用于海量業(yè)務(wù)數(shù)據(jù)的直接加密。1.3 為什么現(xiàn)實(shí)方案幾乎都是混合加密很多人會(huì)問(wèn)既然非對(duì)稱(chēng)加密能解決密鑰分發(fā)問(wèn)題那所有數(shù)據(jù)都用非對(duì)稱(chēng)加密不就行了答案是不行非對(duì)稱(chēng)加密太慢了。反過(guò)來(lái)如果只用對(duì)稱(chēng)加密密鑰分發(fā)問(wèn)題無(wú)解。于是工程界普遍采用混合加密方案用非對(duì)稱(chēng)加密協(xié)商或傳輸一個(gè)隨機(jī)會(huì)話密鑰再用這個(gè)會(huì)話密鑰配合對(duì)稱(chēng)加密去加密真正的業(yè)務(wù)數(shù)據(jù)。密鑰分發(fā)問(wèn)題交給非對(duì)稱(chēng)加密數(shù)據(jù)加密問(wèn)題交給對(duì)稱(chēng)加密兩者各干各擅長(zhǎng)的事。你在瀏覽器里訪問(wèn)任何HTTPS站點(diǎn)背后都是這么工作的。TLS握手階段用證書(shū)和密鑰交換算法完成身份驗(yàn)證與會(huì)話密鑰協(xié)商后續(xù)所有HTTP請(qǐng)求體、響應(yīng)體全是用對(duì)稱(chēng)加密來(lái)保護(hù)的。這個(gè)設(shè)計(jì)思路是理解整個(gè)加密體系的關(guān)鍵。當(dāng)你自己設(shè)計(jì)接口加密方案時(shí)也應(yīng)該沿用混合思路而不是天真地拿RSA去加密幾百KB的報(bào)文。2. 算法選型實(shí)戰(zhàn)哪些加密算法值得用2.1 對(duì)稱(chēng)加密的主流選擇現(xiàn)在最值得關(guān)注的對(duì)稱(chēng)加密算法我首推AES。它已經(jīng)標(biāo)準(zhǔn)化幾十年硬件加速覆蓋極其廣泛安全分析也足夠充分。AES支持128、192、256位三種密鑰長(zhǎng)度一般業(yè)務(wù)場(chǎng)景用AES-128就夠涉及長(zhǎng)期敏感數(shù)據(jù)建議直接AES-256。AES屬于分組密碼每16字節(jié)一塊處理。單獨(dú)使用分組密碼會(huì)暴露數(shù)據(jù)規(guī)律所以必須搭配工作模式。模式選不好算法再?gòu)?qiáng)也白搭。ECB模式最不該用。同一個(gè)明文塊永遠(yuǎn)產(chǎn)生同一個(gè)密文塊數(shù)據(jù)量大一點(diǎn)就會(huì)暴露結(jié)構(gòu)特征直接放棄。CBC模式需要IV加密每個(gè)分組前先和上一個(gè)密文塊異或安全性遠(yuǎn)好于ECB但它不提供完整性保護(hù)密文被篡改了檢測(cè)不到。GCM模式是目前我最推薦的它是AEAD模式一份輸出同時(shí)搞定機(jī)密性和完整性校驗(yàn)內(nèi)部生成的認(rèn)證標(biāo)簽可以識(shí)別密文是否被改動(dòng)過(guò)。CTR模式把分組密碼變成流式加密也能用但同樣要額外做認(rèn)證。ChaCha20也是一款很優(yōu)秀的流式對(duì)稱(chēng)加密算法在手機(jī)等無(wú)AES硬件加速的環(huán)境下表現(xiàn)不錯(cuò)TLS 1.3里也把它列為推薦套件。合規(guī)敏感、有國(guó)產(chǎn)化要求的項(xiàng)目還會(huì)考慮SM4。選型時(shí)可以按這個(gè)原則默認(rèn)AES-256-GCM嵌入式或弱硬件環(huán)境考慮ChaCha20有合規(guī)要求再看SM4。2.2 非對(duì)稱(chēng)加密的主流選擇RSA是最常見(jiàn)的非對(duì)稱(chēng)算法核心優(yōu)勢(shì)是普及度高幾乎任何語(yǔ)言、任何加密庫(kù)都支持。但它的密鑰長(zhǎng)度要求越來(lái)越苛刻2048位是底線我建議新項(xiàng)目直接上3072位或4096位。密鑰變長(zhǎng)帶來(lái)的直接代價(jià)是加解密速度進(jìn)一步變慢RSA-4096的一次私鑰操作會(huì)比RSA-2048慢好幾倍做批量簽名時(shí)CPU壓力很明顯。ECC類(lèi)算法是更現(xiàn)代的選擇。同樣安全強(qiáng)度下256位的橢圓曲線密鑰約等于3072位RSA的安全性密鑰和簽名都短一大截計(jì)算效率也更高。像ECDHE、ECDSA都是ECC的具體應(yīng)用形式SM2也屬于橢圓曲線體系。使用ECC時(shí)要避免自己定義曲線直接使用成熟的標(biāo)準(zhǔn)曲線比如常見(jiàn)的P-256。這里必須強(qiáng)調(diào)一個(gè)高頻誤區(qū)簽名和加密是兩個(gè)不同的操作不能混用。RSA加密應(yīng)該使用OAEP填充RSA簽名應(yīng)該使用PSS填充。不少初學(xué)者圖省事用PKCS1v15填充一把梭或者拿著加密的接口去驗(yàn)簽結(jié)果不是報(bào)錯(cuò)就是存在嚴(yán)重安全隱患。簽名驗(yàn)簽的方向也別搞反私鑰簽名、公鑰驗(yàn)簽這是證明消息確實(shí)來(lái)自私鑰持有者公鑰加密、私鑰解密才是保證只有私鑰持有者能讀內(nèi)容。2.3 不同類(lèi)型業(yè)務(wù)的選型建議業(yè)務(wù)場(chǎng)景推薦方案關(guān)鍵理由Web/App服務(wù)端TLSECDHE AES-256-GCM前向保密性能與安全均衡接口報(bào)文加密接收方RSA-2048公鑰加密會(huì)話密鑰 AES-GCM加密業(yè)務(wù)體混合加密避免非對(duì)稱(chēng)性能瓶頸數(shù)據(jù)簽名ECDSA P-256 或 RSA-3072 PSS驗(yàn)簽效率高簽名長(zhǎng)度短文件存儲(chǔ)加密AES-256-GCM 獨(dú)立密鑰管理大文件吞吐量要求高AEAD防篡改國(guó)內(nèi)合規(guī)項(xiàng)目SM2 SM4符合指定標(biāo)準(zhǔn)要求選型沒(méi)有唯一答案但有一條通用原則盡量選擇密碼學(xué)界廣泛接受、有持續(xù)安全分析的算法避免自研算法和過(guò)時(shí)協(xié)議。算法本身不是越復(fù)雜越好能被全世界安全專(zhuān)家反復(fù)審過(guò)的算法才更值得信任。3. 混合加密方案與HTTPS握手過(guò)程拆解3.1 TLS為什么選擇混合加密TLS協(xié)議從設(shè)計(jì)之初就決定了它必須采用混合加密。公開(kāi)網(wǎng)絡(luò)上不可能預(yù)先給每個(gè)客戶端分發(fā)對(duì)稱(chēng)密鑰直接使用非對(duì)稱(chēng)加密又扛不住海量HTTP流量。于是TLS的做法是先通過(guò)證書(shū)體系驗(yàn)證服務(wù)器身份再通過(guò)密鑰交換算法讓客戶端和服務(wù)器各自算出一個(gè)只有雙方知道的會(huì)話密鑰最后進(jìn)入對(duì)稱(chēng)加密階段。理解了這個(gè)邏輯你就可以解釋很多工程現(xiàn)象。比如為什么TLS握手階段比數(shù)據(jù)傳輸階段更慢因?yàn)槲帐掷锷婕胺菍?duì)稱(chēng)運(yùn)算。為什么抓包看到的是密文因?yàn)檎嬲龢I(yè)務(wù)數(shù)據(jù)全部走了對(duì)稱(chēng)加密。為什么推薦ECDHE這類(lèi)臨時(shí)密鑰交換算法因?yàn)槊看挝帐稚傻呐R時(shí)密鑰用完即棄即使服務(wù)器長(zhǎng)期私鑰泄露歷史流量也解不開(kāi)這就是前向保密的價(jià)值。3.2 一次完整握手的核心流程以TLS 1.3為例我把它簡(jiǎn)化成三個(gè)關(guān)鍵階段來(lái)看。第一階段是ClientHello??蛻舳松梢粋€(gè)臨時(shí)的橢圓曲線密鑰對(duì)把公鑰放進(jìn)Key Share字段發(fā)給服務(wù)器同時(shí)附上支持的密碼套件列表。第二階段是ServerHello。服務(wù)器選一個(gè)雙方都支持的套件返回自己的臨時(shí)公鑰、證書(shū)鏈并對(duì)當(dāng)前握手消息做簽名??蛻舳四玫阶C書(shū)后要驗(yàn)證證書(shū)鏈?zhǔn)欠窨尚?、域名是否匹配、證書(shū)是否過(guò)期然后用服務(wù)器臨時(shí)公鑰和自己的臨時(shí)私鑰算出同樣的會(huì)話密鑰。第三階段從此刻起雙方已經(jīng)完成密鑰協(xié)商。后面發(fā)送的所有數(shù)據(jù)都用會(huì)話密鑰配合對(duì)稱(chēng)加密保護(hù)TLS 1.3還引入了單獨(dú)的Finished消息用來(lái)校驗(yàn)握手過(guò)程的真實(shí)性防止中間人篡改握手消息。簡(jiǎn)化成一句口訣證書(shū)確立身份臨時(shí)密鑰交換建立會(huì)話密鑰對(duì)稱(chēng)加密保護(hù)數(shù)據(jù)。3.3 握手過(guò)程中最容易忽略的安全點(diǎn)證書(shū)校驗(yàn)是第一道坎。很多開(kāi)發(fā)者在聯(lián)調(diào)時(shí)為了省事直接把證書(shū)校驗(yàn)關(guān)閉這在生產(chǎn)環(huán)境是巨大的災(zāi)難。一旦跳過(guò)證書(shū)驗(yàn)證客戶端就無(wú)法確認(rèn)正在通信的是不是真正的服務(wù)器中間人可以拿著任意證書(shū)截獲并轉(zhuǎn)發(fā)流量。密碼套件配置也很關(guān)鍵。老舊的TLS 1.0、TLS 1.1協(xié)議以及RC4、CBC類(lèi)弱套件應(yīng)該直接禁用。TLS 1.3已經(jīng)只保留少數(shù)幾個(gè)AEAD套件這是趨勢(shì)。如果你的業(yè)務(wù)還在維護(hù)老協(xié)議至少要保證默認(rèn)走TLS 1.2以上并優(yōu)先選擇ECDHEAES-GCM。會(huì)話密鑰的熵也要關(guān)注??蛻舳撕头?wù)器的隨機(jī)數(shù)質(zhì)量直接決定會(huì)話密鑰強(qiáng)度任何一方使用了弱隨機(jī)數(shù)生成器整個(gè)加密鏈路都不可信。這也是為什么服務(wù)端一定要配置足夠的熵源不能依賴(lài)系統(tǒng)默認(rèn)的劣質(zhì)隨機(jī)算法。4. 實(shí)操一條龍密鑰生成、文件加密、混合傳輸4.1 生成并檢查RSA與ECC密鑰實(shí)際操作中離不開(kāi)命令行工具。生成RSA私鑰并導(dǎo)出公鑰兩條命令就能做完openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out rsa_private.pem openssl pkey -in rsa_private.pem -pubout -out rsa_public.pem生成的私鑰文件默認(rèn)是PKCS#8格式已經(jīng)帶上了Base64外殼。查看密鑰細(xì)節(jié)可以用openssl pkey -in rsa_private.pem -text -nooutECC密鑰類(lèi)似只是指定曲線參數(shù)openssl ecparam -genkey -name prime256v1 -out ecc_private.pem openssl ec -in ecc_private.pem -pubout -out ecc_public.pem生成之后建議立刻做兩件事一是給私鑰設(shè)置強(qiáng)口令保護(hù)二是把公鑰單獨(dú)導(dǎo)出交給對(duì)接方私鑰永遠(yuǎn)不要出現(xiàn)在任何日志或代碼倉(cāng)庫(kù)里。我見(jiàn)過(guò)太多次私鑰被當(dāng)普通文本提交到倉(cāng)庫(kù)的翻車(chē)事件這一類(lèi)安全事故的后果基本都是災(zāi)難性的。4.2 用對(duì)稱(chēng)加密加密文件加密單個(gè)文件可以先用OpenSSL命令行快速驗(yàn)證鏈路openssl enc -aes-256-cbc -pbkdf2 -salt -in plain.txt -out encrypted.bin執(zhí)行時(shí)會(huì)要求輸入口令這個(gè)口令通過(guò)PBKDF2派生為真正的AES密鑰。加上鹽可以避免相同口令產(chǎn)生相同密鑰從而提高安全性。不過(guò)生產(chǎn)環(huán)境我更推薦直接用代碼庫(kù)來(lái)處理尤其是GCM這類(lèi)AEAD模式。在Python里使用cryptography庫(kù)from cryptography.hazmat.primitives.ciphers.aead import AESGCM import os key AESGCM.generate_key(bit_length256) nonce os.urandom(12) aesgcm AESGCM(key) plaintext bconfidential file content ciphertext aesgcm.encrypt(nonce, plaintext, None) decrypted aesgcm.decrypt(nonce, ciphertext, None) print(decrypted)這段代碼里nonce必須隨機(jī)生成并且絕對(duì)不能重復(fù)使用。重復(fù)使用同一個(gè)nonce加密兩個(gè)不同消息會(huì)直接破壞GCM的安全性。encrypt返回的密文其實(shí)已經(jīng)自動(dòng)拼接了16字節(jié)的認(rèn)證標(biāo)簽解密時(shí)原樣傳入即可。4.3 一個(gè)可直接套用的混合加密demo真實(shí)業(yè)務(wù)中更常見(jiàn)的需求是把消息加密后傳輸給對(duì)方。我用一段Python代碼演示完整混合加密流程。from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.ciphers.aead import AESGCM import os # 接收方生成密鑰對(duì)實(shí)際項(xiàng)目中從密鑰文件加載 private_key rsa.generate_private_key(public_exponent65537, key_size2048) public_key private_key.public_key() # 1. 生成隨機(jī)會(huì)話密鑰 session_key os.urandom(32) # 2. 用接收方公鑰加密會(huì)話密鑰OAEP填充 encrypted_session_key public_key.encrypt( session_key, padding.OAEP( mgfpadding.MGF1(algorithmhashes.SHA256()), algorithmhashes.SHA256(), labelNone ) ) # 3. 用會(huì)話密鑰和AES-GCM加密業(yè)務(wù)數(shù)據(jù) aesgcm AESGCM(session_key) nonce os.urandom(12) ciphertext aesgcm.encrypt(nonce, bbusiness data, None) # 4. 組裝包加密會(huì)話密鑰 nonce 密文 packet encrypted_session_key nonce ciphertext # 接收方反解 decrypted_session_key private_key.decrypt( encrypted_session_key, padding.OAEP( mgfpadding.MGF1(algorithmhashes.SHA256()), algorithmhashes.SHA256(), labelNone ) ) aesgcm_receiver AESGCM(decrypted_session_key) plaintext aesgcm_receiver.decrypt(nonce, ciphertext, None) print(plaintext)這個(gè)demo的思路和HTTPS的混合加密本質(zhì)上是完全一致的。會(huì)話密鑰隨機(jī)生成長(zhǎng)度32字節(jié)AES-GCM完成高效加密RSA-OAEP負(fù)責(zé)會(huì)話密鑰的安全傳輸。實(shí)際落地時(shí)建議把公鑰存成PEM文件從外部加載不要每次都現(xiàn)場(chǎng)生成密鑰對(duì)非對(duì)稱(chēng)密鑰應(yīng)該是穩(wěn)定的不是每次請(qǐng)求都換一把。4.4 參數(shù)選擇的幾個(gè)硬性建議密鑰長(zhǎng)度不能將就。RSA低于2048位我建議直接放棄AES-128可以接受但敏感數(shù)據(jù)優(yōu)先AES-256SM4密鑰長(zhǎng)度固定128位合規(guī)場(chǎng)景下按標(biāo)準(zhǔn)執(zhí)行就好。nonce和IV必須隨機(jī)且唯一。AES-GCM的nonce標(biāo)準(zhǔn)長(zhǎng)度是12字節(jié)也就是96位推薦由密碼學(xué)安全隨機(jī)數(shù)生成器產(chǎn)生。不要用時(shí)間戳、自增ID這類(lèi)可預(yù)測(cè)值當(dāng)nonce這會(huì)打開(kāi)災(zāi)難性的口子。填充模式必須和算法匹配。AES-CBC需要PKCS7填充RSA加密用OAEPRSA簽名用PSS。把各種模式混著配程序大概率直接拋異常就算不報(bào)錯(cuò)也埋下了安全隱患。編碼也是一個(gè)容易翻車(chē)的環(huán)節(jié)。密文是二進(jìn)制網(wǎng)絡(luò)傳輸、JSON序列化時(shí)要用Base64或Hex編碼不能直接把二進(jìn)制塞進(jìn)字符串。解碼時(shí)也要注意Base64是否包含換行、URL安全字符替換這類(lèi)細(xì)節(jié)。5. 高頻報(bào)錯(cuò)與排查技巧實(shí)錄5.1 五類(lèi)高頻報(bào)錯(cuò)及處理報(bào)錯(cuò)信息典型原因處理方式pem routines:get_name:no start line密鑰文件不是合法PEM或內(nèi)容被截?cái)?錯(cuò)誤解碼檢查文件首尾是否有完整BEGIN/END行確認(rèn)沒(méi)有混入換行或字符unsupported paddingRSA填充模式不匹配統(tǒng)一使用OAEP加密、PSS簽名并確認(rèn)兩端算法一致Wrong final block lengthCBC模式密文長(zhǎng)度不是分組的整數(shù)倍或被截?cái)鄼z查密文是否完整Base64解碼后長(zhǎng)度是否對(duì)得上Tag verification failedGCM認(rèn)證標(biāo)簽校驗(yàn)失敗密文被篡改或nonce錯(cuò)誤確認(rèn)密文、nonce、關(guān)聯(lián)數(shù)據(jù)是否被改動(dòng)優(yōu)先檢查解密輸入key size must be 128 or 256 bitsAES密鑰長(zhǎng)度不符合要求檢查密鑰是不是被錯(cuò)誤編碼Base64串是否忘了解碼成原始字節(jié)這些報(bào)錯(cuò)里GCM的Tag驗(yàn)證失敗最值得警惕。它不一定是代碼寫(xiě)錯(cuò)了也可能是密文在傳輸過(guò)程中被別人動(dòng)過(guò)手腳。遇到這個(gè)錯(cuò)誤先把日志拉出來(lái)檢查密文包是否完整再?gòu)逆溌分虚g件排查有沒(méi)有被篡改不要急著改代碼。5.2 問(wèn)題定位的常用套路加密問(wèn)題排查和普通業(yè)務(wù)Bug不太一樣它不是看一眼堆棧就能解決的核心難點(diǎn)在于你處理的大多是二進(jìn)制數(shù)據(jù)日志打不出來(lái)中間過(guò)程也很難直觀檢查。我的經(jīng)驗(yàn)是先分層驗(yàn)證。第一步用命令行工具獨(dú)立驗(yàn)證硬件和算法鏈路比如用openssl先生成密鑰、加密、解密確認(rèn)這條鏈路是通的。第二步寫(xiě)最小化代碼腳本只做內(nèi)存中加解密排除網(wǎng)絡(luò)、序列化、存儲(chǔ)干擾。第三步再接入完整業(yè)務(wù)流程這個(gè)時(shí)候如果還有問(wèn)題基本就能鎖定是某一段數(shù)據(jù)裝配或編碼環(huán)節(jié)出了問(wèn)題。調(diào)試時(shí)還要善于用十六進(jìn)制視圖。二進(jìn)制數(shù)據(jù)用print打出來(lái)是一堆亂碼完全沒(méi)法判斷。正確做法是把每一段關(guān)鍵中間結(jié)果轉(zhuǎn)成Hex輸出一次就能看出長(zhǎng)度對(duì)不對(duì)、數(shù)據(jù)有沒(méi)有被截?cái)?。加日志也有講究。密鑰、IV、nonce這些敏感信息絕對(duì)不能直接打到日志里但可以記錄它們各自的長(zhǎng)度、來(lái)源、生成時(shí)間以及長(zhǎng)度校驗(yàn)結(jié)果。長(zhǎng)度對(duì)不上往往比內(nèi)容對(duì)不上更容易定位問(wèn)題。5.3 幾個(gè)我自己踩過(guò)的坑有一次做接口聯(lián)調(diào)對(duì)方反饋解密一直失敗報(bào)的是padding錯(cuò)誤。我檢查了代碼RSA填充模式確實(shí)是OAEP密鑰也完全一致。折騰了半天最后發(fā)現(xiàn)是對(duì)方在把密鑰從JSON解析出來(lái)時(shí)Base64字符串里的換行符被JSON解析吃掉了導(dǎo)致整個(gè)公鑰內(nèi)容不完整解析出來(lái)的密鑰根本是殘缺的。還有一次AES-GCM在測(cè)試環(huán)境一直正常一上生產(chǎn)就偶發(fā)解密失敗。后來(lái)定位到是生產(chǎn)環(huán)境緩存層對(duì)key做了序列化處理把密文的尾字節(jié)截?cái)嗔?。GCM的認(rèn)證標(biāo)簽在密文末尾少一個(gè)字節(jié)都會(huì)導(dǎo)致Tag驗(yàn)證失敗。從那以后我養(yǎng)成了一個(gè)習(xí)慣凡是經(jīng)過(guò)緩存、消息隊(duì)列、數(shù)據(jù)庫(kù)的二進(jìn)制字段必須檢查底層存儲(chǔ)類(lèi)型是否支持完整二進(jìn)制內(nèi)容不能默認(rèn)所有中間件都能原樣搬運(yùn)byte數(shù)組。另外要提醒大家注意一個(gè)數(shù)學(xué)層面的坑用RSA加密時(shí)能加密的明文長(zhǎng)度上限是密鑰字節(jié)數(shù)減掉填充開(kāi)銷(xiāo)2048位密鑰只能加密約190字節(jié)。很多人沒(méi)意識(shí)到這一點(diǎn)直接拿RSA去加密幾百字節(jié)的業(yè)務(wù)數(shù)據(jù)結(jié)果程序報(bào)錯(cuò)或者發(fā)明各種奇怪的拆分方案。正確的做法永遠(yuǎn)是用RSA保護(hù)會(huì)話密鑰用會(huì)話密鑰保護(hù)業(yè)務(wù)數(shù)據(jù)。6. 做加密方案設(shè)計(jì)時(shí)容易忽略的邊界6.1 密鑰生命周期管理很多開(kāi)發(fā)者把精力都花在算法和模式選擇上卻忽略了密鑰本身是需要管理的。密鑰從生成、存儲(chǔ)、使用、輪換到銷(xiāo)毀每一個(gè)環(huán)節(jié)都可能成為安全短板。密鑰不應(yīng)該出現(xiàn)在源碼里更不應(yīng)該出現(xiàn)在配置文件里并跟隨倉(cāng)庫(kù)一起分發(fā)。正確的做法是放在獨(dú)立的密鑰管理系統(tǒng)、硬件安全模塊或云上的密鑰管理服務(wù)里應(yīng)用運(yùn)行時(shí)通過(guò)權(quán)限受控的接口獲取。密鑰還要定期輪換一旦懷疑泄露就要立即吊銷(xiāo)并簽發(fā)新密鑰。證書(shū)有效期到期的場(chǎng)景大家都熟悉但業(yè)務(wù)自定義的非對(duì)稱(chēng)密鑰對(duì)往往沒(méi)人管過(guò)期一用就是好幾年這其實(shí)非常危險(xiǎn)。6.2 認(rèn)證、機(jī)密性、完整性三者缺一不可加密并不等于安全。單純的加密算法只解決機(jī)密性問(wèn)題它不能防止數(shù)據(jù)被篡改也不能證明數(shù)據(jù)來(lái)自合法發(fā)送方。很多方案里開(kāi)發(fā)者費(fèi)了半天勁加密了數(shù)據(jù)卻沒(méi)有做完整性校驗(yàn)結(jié)果攻擊者隨便翻轉(zhuǎn)幾個(gè)bit接收方解出來(lái)的內(nèi)容已經(jīng)面目全非程序卻完全感知不到。這就是我對(duì)AEAD模式情有獨(dú)鐘的原因。AES-GCM一份代碼同時(shí)解決機(jī)密性和完整性附帶的認(rèn)證標(biāo)簽直接暴露數(shù)據(jù)是否被改動(dòng)。而在混合加密方案里如果還需要確認(rèn)發(fā)送方身份就應(yīng)該再用發(fā)送方私鑰對(duì)數(shù)據(jù)做一次簽名形成簽名加密的組合。簽名確認(rèn)來(lái)源加密保護(hù)內(nèi)容認(rèn)證標(biāo)簽防篡改三者各司其職缺一不可。6.3 最后聊幾點(diǎn)體會(huì)做了幾年加密相關(guān)工作最大的感受是永遠(yuǎn)不要自己發(fā)明加密協(xié)議。密碼學(xué)發(fā)展到現(xiàn)在經(jīng)過(guò)全世界頂尖研究者反復(fù)分析過(guò)的算法和標(biāo)準(zhǔn)協(xié)議已經(jīng)足夠好用了普通業(yè)務(wù)真正要做的不是設(shè)計(jì)算法而是正確組裝組合方案并保證密鑰管理安全。那些宣稱(chēng)不可破解的自研算法最后幾乎都成了安全界的笑話。隨機(jī)數(shù)質(zhì)量也是經(jīng)常被低估的一環(huán)。很多語(yǔ)言默認(rèn)的隨機(jī)數(shù)生成器不適合用于加密一定要使用專(zhuān)門(mén)的密碼學(xué)安全隨機(jī)數(shù)生成接口比如Python里的os.urandom和加密庫(kù)提供的生成函數(shù)。隨機(jī)性不是看起來(lái)隨機(jī)而是不可預(yù)測(cè)這兩者有著本質(zhì)區(qū)別。最后一個(gè)小建議把常用的加解密流程封裝成統(tǒng)一的標(biāo)準(zhǔn)模塊對(duì)外只暴露加密、解密、簽名、驗(yàn)簽四個(gè)接口。這樣業(yè)務(wù)方不需要關(guān)心底層RSA還是ECC、GCM還是CBC安全團(tuán)隊(duì)也可以集中精力在一個(gè)受控的模塊里做審計(jì)和加固。加密是手段不是目的能讓人用對(duì)、用好才是有價(jià)值的工程實(shí)踐。