SubtleCrypto 的非加密用途
本文將重點介紹 digest 方法的用法,該方法屬於 SubtleCrypto 介面。在 Web Crypto API 中的許多其他方法都有非常具體的加密用例,但建立內容的雜湊(這正是 digest 方法所做的)有許多非常有用的用途。
本文不討論 SubtleCrypto 介面 的加密用途。本文要強調的最重要的一點是:不要在生產環境的加密用途中使用此 API,因為它功能強大且底層。要正確使用它,您需要採取許多特定於上下文的步驟來正確完成加密任務。如果其中任何一個步驟出錯,最壞的情況是您的程式碼無法執行,最好是程式碼會執行,而您卻會在不知不覺中讓使用者面臨不安全產品的風險。
您甚至可能根本不需要使用 Web Crypto API。您想要用加密來解決的許多問題,Web 平臺已經提供瞭解決方案。例如,如果您擔心中間人攻擊(例如 Wi-Fi 熱點讀取客戶端和伺服器之間的資訊),透過確保正確使用 HTTPS 即可解決。您想安全地在使用者之間傳送資訊嗎?那麼您可以透過 WebRTC 資料通道 建立使用者之間的資料連線,該連線在標準中是加密的。
SubtleCrypto 介面 提供了用於處理加密的底層原語,但使用這些工具實現系統是一項複雜的任務。錯誤很難被發現,結果可能意味著您的使用者資料不像您想象的那麼安全。如果您的使用者正在共享敏感或有價值的資料,這可能會帶來災難性的後果。
如有疑問,請不要自己嘗試,聘請有經驗的人員,並確保您的軟體經過安全專家的審計。
雜湊檔案
這是您可以使用 Web Crypto API 完成的最簡單的有用操作。它不涉及生成金鑰或證書,並且只有一個步驟。
雜湊 是一種將一長串位元組轉換為一串較短字串的技術,其中長字串的微小變化會導致短字串的巨大變化。此技術對於在不檢查兩個檔案的每個位元組的情況下標識兩個相同的檔案非常有用。這非常有用,因為您有一個簡單的字串進行比較。需要明確的是,雜湊是一個單向操作。您無法從雜湊值生成原始位元組字串。
如果生成的兩個雜湊相同,但用於生成它們的檔案不同,則稱為雜湊衝突,這是一種偶然發生的機率極低的事件,對於 SHA256 這樣的安全雜湊函式,幾乎不可能人為製造。因此,如果兩個字串相同,您可以合理地確信兩個原始檔案是相同的。
在本文釋出時,SHA256 是雜湊檔案的常用選擇,但在 SubtleCrypto 介面中還有 更高階的雜湊函式 可用。SHA256 雜湊最常見的表示形式是 64 個十六進位制數字組成的字串。十六進位制意味著它只使用 0-9 和 a-f 這些字元,代表 4 位資訊。簡而言之,SHA256 雜湊將任意長度的資料轉換為幾乎唯一的 256 位資料。
這項技術通常被允許您下載可執行檔案的網站使用,以確保下載的檔案與作者意圖的檔案匹配。這可以確保您的使用者不會安裝惡意軟體。最常見的方法是:
- 記下檔名和網站提供的 SHA256 校驗和。
- 下載可執行檔案。
- 在終端中執行
sha256sum /path/to/the/file來生成您自己的程式碼。如果您使用的是 Mac,可能需要 單獨安裝。 - 比較這兩個字串 - 它們應該匹配,除非檔案已被篡改。

SubtleCrypto 的 digest() 方法對此很有用。要生成檔案的校驗和,可以這樣做:
首先,我們新增一些 HTML 元素來載入檔案並顯示 SHA-256 輸出。
<h3>Demonstration of hashing a file with SHA256</h3>
<label
>Choose file(s) to hash <input type="file" id="file" name="file" multiple
/></label>
<output></output>
接下來,我們使用 SubtleCrypto 介面來處理它們。這透過以下方式工作:
- 使用
File物件的arrayBuffer()方法將檔案讀取到ArrayBuffer中。 - 使用
crypto.subtle.digest('SHA-256', arrayBuffer)來處理 ArrayBuffer。 - 將生成的雜湊(另一個 ArrayBuffer)轉換為字串,以便顯示。
const output = document.querySelector("output");
const file = document.getElementById("file");
// Run the hashing function when the user selects one or more file
file.addEventListener("change", hashTheseFiles);
// The digest function is asynchronous, it returns a promise
// We use the async/await syntax to simplify the code.
async function fileHash(file) {
const arrayBuffer = await file.arrayBuffer();
// Use the subtle crypto API to perform a SHA256 Sum of the file's
// Array Buffer. The resulting hash is stored in an array buffer
const hashAsArrayBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);
// To display it as a string we will get the hexadecimal value of
// each byte of the array buffer. This gets us an array where each byte
// of the array buffer becomes one item in the array
const uint8ViewOfHash = new Uint8Array(hashAsArrayBuffer);
// We then convert it to a regular array so we can convert each item
// to hexadecimal strings, where characters of 0-9 or a-f represent
// a number between 0 and 15, containing 4 bits of information,
// so 2 of them is 8 bits (1 byte).
const hashAsString = Array.from(uint8ViewOfHash)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hashAsString;
}
async function hashTheseFiles(e) {
let outHTML = "";
// iterate over each file in file select input
for (const file of this.files) {
// calculate its hash and list it in the output element.
outHTML += `${file.name} ${await fileHash(file)}\n`;
}
output.innerText = outHTML;
}
您會在哪裡使用它?
此時,您可能會想:“我可以在自己的網站上使用它,當用戶下載檔案時,我們可以確保雜湊值匹配,以向用戶保證他們的下載是安全的”。不幸的是,這有兩個立即想到的問題:
-
可執行檔案的下載始終應該透過 HTTPS 進行。這可以防止中間方執行此類攻擊,因此是多餘的。
-
如果攻擊者能夠替換原始伺服器上的下載檔案,那麼他們也可以簡單地替換呼叫 SubtleCrypto 介面的程式碼來繞過它,並聲稱一切正常。可能是一些偷偷摸摸的替換,例如 嚴格相等性,這在您自己的程式碼中可能很難發現。
diff--- if (checksum === correctCheckSum) return true; +++ if (checksum = correctCheckSum) return true;
一個可能值得嘗試的地方是,如果您想測試來自您不控制的第三方下載源的檔案。前提是下載位置具有 CORS 標頭,允許您在將檔案提供給使用者之前對其進行掃描。不幸的是,預設情況下很少有伺服器啟用 CORS。
什麼是“雜湊加鹽”?
您可能以前聽過一句話:“給雜湊加鹽”。它與我們當前的主題關係不大,但瞭解一下是好的。
注意:本節討論的是密碼安全,SubtleCrypto 提供的雜湊函式不適用於此用例。對於這些目的,您需要昂貴且緩慢的雜湊函式,如 scrypt 和 bcrypt。SHA 的設計初衷是相當快速和高效的,這使其不適合密碼雜湊。本節僅供您參考 — 請勿在客戶端使用 Web Crypto API 來雜湊密碼。
雜湊的一個流行用例是密碼。您絕不應該以明文形式儲存使用者的密碼,這簡直是個糟糕的主意。相反,您儲存使用者密碼的雜湊值,這樣,如果駭客獲取了您的使用者名稱和密碼資料庫,也無法恢復原始密碼。眼尖的讀者可能會注意到,透過將已知密碼列表中的雜湊值與獲取的密碼雜湊列表進行比較,仍然可以推斷出原始密碼。將一個字串連線到密碼會改變雜湊值,使其不再匹配。這被稱為加鹽。另一個棘手的問題是,如果您為每個密碼使用相同的鹽,那麼具有匹配雜湊值的密碼也具有相同的原始密碼。因此,如果您知道一個,那麼您就知道所有匹配的密碼。
為了解決這個問題,您執行所謂的給雜湊加鹽。對於每個密碼,您生成一個鹽(隨機字元字串),然後將其與密碼字串連線起來。然後,您將雜湊值和鹽儲存在同一個資料庫中,以便在使用者稍後登入時進行匹配檢查。這意味著,如果兩個使用者使用相同的密碼,它們的雜湊值也會不同。因此,您需要一個昂貴的加密函式,因為使用常用密碼列表來找出原始密碼會耗費大量時間。
使用 SHA 的雜湊表
您可以使用 SHA1 快速生成非加密安全的雜湊。這些對於將任意資料轉換為稍後可用於查詢的鍵非常有用。
例如,如果您想擁有一個數據庫,其中一行中的一個欄位包含大量資料塊。這會降低資料庫的效率,因為其中一個欄位必須是可變長度的,或者足夠大以儲存可能的最大資料塊。另一種解決方案是生成資料塊的雜湊值,並將其儲存在一個單獨的查詢表中,使用雜湊值作為索引。然後,您可以在原始資料庫中只儲存雜湊值,這是一個固定長度的好方法。
SHA1 雜湊的可能變體數量極其龐大。多到偶然產生兩個具有相同 SHA1 雜湊值的資料塊幾乎是不可能的。因為 SHA1 不是加密安全的,所以可能有意產生兩個具有相同 SHA1 雜湊值的檔案。因此,理論上,惡意使用者可以生成一個數據塊來替換資料庫中的原始資料塊,而由於雜湊值相同,因此不會被檢測到。這是一個值得注意的攻擊向量。
Git 如何儲存檔案
Git 使用 SHA1 雜湊,這裡是一個很好的例子,它在兩個有趣的方式中使用雜湊。當檔案儲存在 git 中時,它們透過它們的 SHA1 雜湊進行引用。這使得 git 能夠快速查詢資料並恢復檔案。
它不只是使用檔案內容進行雜湊,還會先加上 UTF8 字串 "blob ",然後是檔案大小(以位元組為單位,以十進位制表示),最後是 null 字元(在 JavaScript 中可以寫成 "\0")。您可以使用 Encoding API 的 TextEncoder 介面 來編碼 UTF8 文字,因為 JavaScript 中的字串是 UTF16。
下面的程式碼與我們的 SHA256 示例一樣,可以用來從檔案中生成這些雜湊。用於上傳檔案的 HTML 保持不變,但我們會做一些額外的工作來像 git 一樣新增大小資訊。
<h3>Demonstration of how git uses SHA1 for files</h3>
<label
>Choose file(s) to hash <input type="file" id="file" name="file" multiple
/></label>
<output></output>
const output = document.querySelector("output");
const file = document.getElementById("file");
file.addEventListener("change", hashTheseFiles);
async function fileHash(file) {
const arrayBuffer = await file.arrayBuffer();
// Git prepends the null terminated text 'blob 1234' where 1234
// represents the file size before hashing so we are going to reproduce that
// first we work out the Byte length of the file
const uint8View = new Uint8Array(arrayBuffer);
const length = uint8View.length;
// Git in the terminal uses UTF8 for its strings; the Web uses UTF16.
// We need to use an encoder because different binary representations
// of the letters in our message will result in different hashes
const encoder = new TextEncoder();
// Null-terminated means the string ends in the null character which
// in JavaScript is '\0'
const view = encoder.encode(`blob ${length}\0`);
// We then combine the 2 Array Buffers together into a new Array Buffer.
const newBlob = new Blob([view.buffer, arrayBuffer], {
type: "text/plain",
});
const arrayBufferToHash = await newBlob.arrayBuffer();
// Finally we perform the hash this time as SHA1 which is what Git uses.
// Then we return it as a string to be displayed.
return hashToString(await crypto.subtle.digest("SHA-1", arrayBufferToHash));
}
function hashToString(arrayBuffer) {
const uint8View = new Uint8Array(arrayBuffer);
return Array.from(uint8View)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
// like before we iterate over the files
async function hashTheseFiles(e) {
let outHTML = "";
for (const file of this.files) {
outHTML += `${file.name} ${await fileHash(file)}\n`;
}
output.innerText = outHTML;
}
請注意,它如何使用 Encoding API 來生成頭部,該頭部與原始 ArrayBuffer 連線以生成要雜湊的字串。
Git 如何生成提交雜湊
有趣的是,git 也以類似的方式基於多個資訊生成提交雜湊。這些可能包括前一個提交雜湊和提交訊息,它們共同構成一個新的雜湊。這可以用來引用基於多個唯一識別符號的提交。
終端命令是: (printf "commit %s\0" $(git --no-replace-objects cat-file commit HEAD | wc -c); git cat-file commit HEAD) | sha1sum
本質上是 UTF8 字串(null 字元寫成 \0)
commit [size in bytes as decimal of this info]\0tree [tree hash] parent [parent commit hash] author [author info] [timestamp] committer [committer info] [timestamp] commit message
這很好,因為沒有單個欄位保證是唯一的,但組合在一起可以為單個提交提供一個唯一的指標。然而,整個字串太長且難以處理。因此,透過雜湊它,您可以得到一個新的唯一字串,它足夠短,可以方便地從多個欄位共享。
這就是為什麼如果您曾經修改過提交,即使您沒有對訊息進行任何更改,雜湊值也會改變。提交的時間戳已更改,即使只是一個字元的更改,也足以完全更改新的雜湊值。
由此得出的結論是,當您想為某些資料新增一個鍵,但任何單個資訊都不夠獨特時,將多個字串連線起來並對其進行雜湊處理是一種生成有用鍵的好方法。
希望這些示例能鼓勵您檢視這個強大而新的 API。請記住,不要自己重新建立加密的東西。瞭解這些工具的存在就足夠了,其中一些工具,如 crypto.digest() 函式,是您日常開發中有用的工具。