如何正確區(qū)分并使用加密與認(rèn)證技術(shù)?
在密碼學(xué)專家之中,“加密并不是認(rèn)證”是一個(gè)簡(jiǎn)單的共識(shí)。但很多不了解密碼學(xué)的開(kāi)發(fā)者,并不知道這句話的意義。如果這個(gè)知識(shí)更廣為人知和深入理解,那么將會(huì)避免很多的設(shè)計(jì)錯(cuò)誤。
這一概念本身并不困難,但在表面之下,還有更多豐富的細(xì)節(jié)和玄妙之處有待發(fā)現(xiàn)。本文就是講述開(kāi)發(fā)者對(duì)于加密和認(rèn)證二者的混淆與誤用,并附上了優(yōu)秀的解決方案。
0x01 加密與認(rèn)證之間有哪些區(qū)別?
加密是呈現(xiàn)信息,使其在沒(méi)有正確的密鑰情況下,變得難以卒讀的過(guò)程。在簡(jiǎn)單的對(duì)稱加密中,同一個(gè)密鑰被用于加密和解密。在非對(duì)稱加密中,可以使用用戶的公鑰對(duì)信息加密,使得只有對(duì)應(yīng)私鑰的擁有者才能讀取它。
認(rèn)證是呈現(xiàn)信息,使其抗篡改(通常在某一非常低的概率之內(nèi),小于1除以已知宇宙中粒子的數(shù)量),同時(shí)也證明它起源于預(yù)期發(fā)送者的過(guò)程。
注意:當(dāng)本文提及真實(shí)性時(shí),是專門指的信息真實(shí)性,而不是身份真實(shí)性。這是一個(gè)PKI和密鑰管理問(wèn)題,我們可能在未來(lái)的博客中詳細(xì)說(shuō)明。
就CIA triad而言:加密提供機(jī)密性,認(rèn)證提供完整性。
加密不提供完整性;被篡改的信息(通常)還能解密,但結(jié)果通常會(huì)是垃圾。單獨(dú)加密也不抑制惡意第三方發(fā)送加密信息。
認(rèn)證不提供機(jī)密性;可以為明文信息提供抗篡改。
在程序員中,常見(jiàn)的錯(cuò)誤是混淆這兩個(gè)概念。你能很容易找到這樣的一個(gè)庫(kù)或者框架:加密cookie數(shù)據(jù),然后在僅僅解密它之后就無(wú)條件地信任與使用之。
0x02 加密
我們之前定義了加密,并且詳細(xì)說(shuō)明了它是提供機(jī)密性,但不提供完整性和真實(shí)性的。你可以篡改加密信息,并將產(chǎn)生的垃圾給予接收者。而且你甚至可以利用這種垃圾產(chǎn)生機(jī)制,來(lái)繞過(guò)安全控制。
考慮在加密cookie的情況下,有如下代碼:
- function setUnsafeCookie($name, $cookieData, $key)
- {
- $iv = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
- return setcookie(
- $name,
- base64_encode(
- $iv.
- mcrypt_encrypt(
- MCRYPT_RIJNDAEL_128,
- $key,
- json_encode($cookieData),
- MCRYPT_MODE_CBC,
- $iv
- )
- )
- );
- }
- function getUnsafeCookie($name, $key)
- {
- if (!isset($_COOKIE[$name])) {
- return null;
- }
- $decoded = base64_decode($_COOKIE[$name]);
- $iv = mb_substr($decoded, 0, 16, '8bit');
- $ciphertext = mb_substr($decoded, 16, null, '8bit');
- $decrypted = rtrim(
- mcrypt_decrypt(
- MCRYPT_RIJNDAEL_128,
- $key,
- $ciphertext,
- MCRYPT_MODE_CBC,
- $iv
- ),
- "\0"
- );
- return json_decode($decrypted, true);
- }
上面的代碼提供了在密碼段鏈接模塊的AES加密,如果你傳入32字節(jié)的字符串作為$key,你甚至可以聲稱,為你的cookie提供了256位的AES加密,然后人們可能被誤導(dǎo)相信它是安全的。
0x03 如何攻擊未經(jīng)認(rèn)證的加密
比方說(shuō),在登錄到這個(gè)應(yīng)用程序之后,你會(huì)發(fā)現(xiàn)你收到一個(gè)會(huì)話cookie,看起來(lái)就像
kHv9PAlStPZaZJHIYXzyCnuAhWdRRK7H0cNVUCwzCZ4M8fxH79xIIIbznxmiOxGQ7td8LwTzHFgwBmbqWuB+sQ==
讓我們改變一個(gè)字節(jié)的第一塊(初始化向量),并反復(fù)發(fā)送我們的新的cookie,直到出現(xiàn)一些變化。應(yīng)該采取共4096次HTTP請(qǐng)求,以嘗試變量IV所有可能的單字節(jié)變化。在上面的例子中,經(jīng)過(guò)2405次請(qǐng)求后,我們得到一個(gè)看起來(lái)像這樣的字符串:
kHv9PAlStPZaZZHIYXzyCnuAhWdRRK7H0cNVUCwzCZ4M8fxH79xIIIbznxmiOxGQ7td8LwTzHFgwBmbqWuB+sQ==
相比之下,在base64編碼的cookie中只有一個(gè)字符不同(kHv9PAlStPZaZ J vs kHv9PAlStPZaZ Z):
- kHv9PAlStPZaZJHIYXzyCnuAhWdRRK7H0cNVUCwzCZ4M8fxH79xIIIbznxmiOxGQ7td8LwTzHFgwBmbqWuB+sQ==
+ kHv9PAlStPZaZZHIYXzyCnuAhWdRRK7H0cNVUCwzCZ4M8fxH79xIIIbznxmiOxGQ7td8LwTzHFgwBmbqWuB+sQ==
我們存儲(chǔ)在這個(gè)cookie里的原始數(shù)據(jù),是看起來(lái)像這樣的數(shù)組:
- array(2) {
- ["admin"]=>
- int(0)
- ["user"]=>
- "aaaaaaaaaaaaa"
- }
但在僅僅改變初始化向量的一個(gè)字節(jié)之后,我們就能夠改寫(xiě)我們的閱讀信息:
- array(2) {
- ["admin"]=>
- int(1)
- ["user"]=>
- "aaaaaaaaaaaaa"
- }
根據(jù)底層應(yīng)用程序的設(shè)置方法,你或許可以翻轉(zhuǎn)一位進(jìn)而提升成為一名管理員。即使你的cookie是加密的。 如果你想再現(xiàn)我們的結(jié)果,我們的加密密鑰是十六進(jìn)制下的:000102030405060708090a0b0c0d0e0f
#p#
0x04 認(rèn)證
如上所述,認(rèn)證旨在提供信息的完整性(我們指顯著抗篡改能力),而這證明它來(lái)自預(yù)期的源(真實(shí)性)。這樣做的典型方法是,為信息計(jì)算一個(gè)密鑰散列消息認(rèn)證碼(HMAC的簡(jiǎn)稱),并將信息與它連結(jié)。
- function hmac_sign($message, $key)
- {
- return hash_hmac('sha256', $message, $key) . $message;
- }
- function hmac_verify($bundle, $key)
- {
- $msgMAC = mb_substr($bundle, 0, 64, '8bit');
- $message = mb_substr($bundle, 64, null, '8bit');
- return hash_equals(
- hash_hmac('sha256', $message, $key),
- $msgMAC
- );
- }
重要的是,這里使用一個(gè)適當(dāng)?shù)墓9ぞ?,如HMAC,而不僅僅是一個(gè)簡(jiǎn)單的散列函數(shù)。
- function unsafe_hash_sign($message, $key)
- {
- return md5($key.$message) . $message;
- }
- function unsafe_hash_verify($bundle, $key)
- {
- $msgHash = mb_substr($bundle, 0, 64, '8bit');
- $message = mb_substr($bundle, 64, null, '8bit');
- return md5($key.$message) == $msgHash;
- }
我在這兩個(gè)函數(shù)名前面加了unsafe,是因?yàn)樗鼈冞€是易受到一些缺點(diǎn)的危害:
Timing Attacks
Chosen Prefix Attacks on MD5 (PDF)
Non-strict equality operator bugs (largely specific to PHP)
現(xiàn)在,我們有點(diǎn)接近我們強(qiáng)大的對(duì)稱加密認(rèn)證的目標(biāo)。目前仍有幾個(gè)問(wèn)題,如:
如果我們的原始信息以空字節(jié)結(jié)尾會(huì)發(fā)生什么?
有沒(méi)有一個(gè)比mcrypt擴(kuò)展庫(kù)默認(rèn)使用的更好的填充策略?
由于使用AES,有哪些通信方面是易受攻擊的?
幸運(yùn)的是,這些問(wèn)題在現(xiàn)有的加密函數(shù)庫(kù)中已有了解答。我們強(qiáng)烈推薦你使用現(xiàn)有的庫(kù),而不是寫(xiě)自己的加密功能。對(duì)于PHP開(kāi)發(fā)人員來(lái)說(shuō),你應(yīng)該使用defuse/php-encryption(或者libsodium)。
0x05 用Libsodium安全加密Cookies
- /*
- // At some point, we run this command:
- $key = Sodium::randombytes_buf(Sodium::CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES);
- */
- /**
- * Store ciphertext in a cookie
- *
- * @param string $name - cookie name
- * @param mixed $cookieData - cookie data
- * @param string $key - crypto key
- */
- function setSafeCookie($name, $cookieData, $key)
- {
- $nonce = Sodium::randombytes_buf(Sodium::CRYPTO_SECRETBOX_NONCEBYTES);
- return setcookie(
- $name,
- base64_encode(
- $nonce.
- Sodium::crypto_secretbox(
- json_encode($cookieData),
- $nonce,
- $key
- )
- )
- );
- }
- /**
- * Decrypt a cookie, expand to array
- *
- * @param string $name - cookie name
- * @param string $key - crypto key
- */
- function getSafeCookie($name, $key)
- {
- $hexSize = 2 * Sodium::Sodium::CRYPTO_SECRETBOX_NONCEBYTES;
- if (!isset($_COOKIE[$name])) {
- return array();
- }
- $decoded = base64_decode($_COOKIE[$name]);
- $nonce = mb_substr($decoded, 0, $hexSize, '8bit');
- $ciphertext = mb_substr($decoded, $hexSize, null, '8bit');
- $decrypted = Sodium::crypto_secretbox_open(
- $ciphertext,
- $nonce,
- $key
- );
- if (empty($decrypted)) {
- return array();
- }
- return json_decode($decrypted, true);
- }
對(duì)于沒(méi)有l(wèi)ibsodium庫(kù)的開(kāi)發(fā)人員,我們的一個(gè)博客讀者,提供了一個(gè)安全cookie實(shí)現(xiàn)的例子,其使用了defuse/php-encryption(我們推薦的PHP庫(kù))。
0x06 使用關(guān)聯(lián)數(shù)據(jù)的認(rèn)證加密
在我們前面的示例中,我們集中精力于,同時(shí)使用加密和認(rèn)證,使其作為必須小心使用的單獨(dú)組件,以避免加密的悲劇。具體而言,我們專注于密碼段鏈接模塊的AES加密。
然而,密碼學(xué)家已經(jīng)開(kāi)發(fā)出更新,更具有彈性的加密模型,其加密和認(rèn)證信息在同一操作。這些模型被稱為AEAD模型(Authenticated Encryption with Associated Data)。關(guān)聯(lián)數(shù)據(jù)意味著,無(wú)論你的應(yīng)用程序需要什么進(jìn)行認(rèn)證,都不加密。
AEAD模型通常用于有狀態(tài)的目的,如網(wǎng)絡(luò)通信中,其中一個(gè)隨機(jī)數(shù)可以很容易地管理。
AEAD兩個(gè)可靠的實(shí)現(xiàn)是AES-GCM和ChaCha20-Poly1305。
AES-GCM是Galois/Counter模式中的高級(jí)加密標(biāo)準(zhǔn)(又名Rijndael算法加密)。這種模式在OpenSSL的最新版本中加入,但它目前在PHP中還不被支持。
ChaCha20-Poly1305結(jié)合了ChaCha20流密碼與Poly1305消息認(rèn)證碼。這種模式在libsodium PHP擴(kuò)展可用。Sodium::crypto_aead_chacha20poly1305_encrypt() Sodium::crypto_aead_chacha20poly1305_decrypt()
總結(jié)一下,你該記住的
加密不是認(rèn)證
加密提供機(jī)密性
認(rèn)證提供完整性
將兩者混為一談你就得自擔(dān)風(fēng)險(xiǎn)
為了完成CIA triad,你需要單獨(dú)解決可用性。這通常不是一個(gè)加密問(wèn)題。
更重要的是:在密碼學(xué)專家的監(jiān)督下,使用具有韌性被證實(shí)記錄的庫(kù),而不是自己在那里閉門造車,你會(huì)好得多。
原文:https://paragonie.com/blog/2015/05/using-encryption-and-authentication-correctly