# Crypto [Crypto](https://developer.mozilla.org/en-US/docs/Web/API/Crypto) Crypto.subtle 是 Web Crypto API 的一部分,是 JavaScript 中用於執行加密和解密操作的接口。它提供了一組強大的加密算法,包括 * 對稱密鑰加密算法、 * 非對稱密鑰加密算法、 * 哈希函數和消息身份驗證代碼(MAC)算法等。 Crypto.subtle 的主要作用是提供一種安全的方式來執行加密操作,以便在 Web 應用程序中保護用戶數據的隱私和安全。例如,可以使用 Crypto.subtle 實現以下任務: * 對稱密鑰加密和解密 * 非對稱密鑰加密和解密 * 計算哈希值 * 計算消息身份驗證代碼 * 生成隨機值 * 密鑰派生和密鑰導出等操作。 Crypto.subtle 支持的加密算法和操作會因實現和瀏覽器而異。例如,可能支持的對稱密鑰加密算法包括 AES、DES、3DES 等。可能支持的非對稱密鑰加密算法包括 RSA、ECDSA 等。 需要注意的是,雖然 Crypto.subtle 提供了一組強大的加密算法和操作,但是在實際使用時需要格外小心。一些加密算法和操作可能會對性能產生重大影響,而一些錯誤的使用方式可能會導致加密漏洞和安全問題。因此,在使用 Crypto.subtle 時應該仔細閱讀相關文檔和指南,並且遵循最佳實踐來確保安全性和可靠性。 ## dev ```typescript // Generate a random 256-bit key const key = await window.crypto.subtle.generateKey( {name: 'AES-CBC', length: 256}, true, // Extractable ['encrypt', 'decrypt'] // Key usages ); // Convert the message to an ArrayBuffer const message = new TextEncoder().encode('Hello, world!').buffer; // Encrypt the message using AES-CBC const iv = window.crypto.getRandomValues(new Uint8Array(16)); const ciphertext = await window.crypto.subtle.encrypt( {name: 'AES-CBC', iv}, key, message ); // Decrypt the ciphertext using AES-CBC const decryptedMessage = await window.crypto.subtle.decrypt( {name: 'AES-CBC', iv}, key, ciphertext ); // Convert the decrypted message to a string const decryptedText = new TextDecoder().decode(decryptedMessage); console.log(decryptedText); // "Hello, world!" ``` ## prod ```typescript // Generate a random key for HMAC SHA-256 const key = await window.crypto.subtle.generateKey( {name: 'HMAC', hash: 'SHA-256', length: 256}, true, // Extractable ['sign', 'verify'] // Key usages ); // Convert the message to an ArrayBuffer const message = new TextEncoder().encode('Hello, world!').buffer; // Calculate the HMAC of the message const hmac = await window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, message ); // Verify the HMAC of the message const isValid = await window.crypto.subtle.verify( {name: 'HMAC', hash: 'SHA-256'}, key, hmac, message ); console.log(isValid); // true ``` 在 prod 使用中,需要考慮更多的安全性問題,例如密鑰管理、密鑰交換、密碼學協議等,以確保數據的隱私和安全。 crypto.sign主要用於數據完整性、來源認證和數字證書驗證,而crypto.encrypt主要用於數據保密性和安全通信。在實際應用中,這兩種技術可以結合使用,以提供更全面的數據保護和安全性。 上面的例子: `Extractable` : 用意是讓生成的key能夠讓之後的crypto api `import` `export` 使用你生成的key `new TextEncoder()`: 將string轉乘Uint8Array 或 ArrayBuffer ,兩者都是二進制資料格式,差別在於ArrayBuffer 日後無法修,Uint8Array 則是 ArrayBuffer 的視圖日後可以調整資料 ```typescript // importKey 用來 sign 使用,generateKey 則是用來作 decode/encode不能混用 const signkey = crypto.subtle.importKey( 'raw', new TextEncoder().encode('sign_secret'), { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign', 'verify'] ) const decodeKey = crypto.subtle.generateKey( { name: 'AES-CBC', length: 256 }, true, ['decrypt', 'encrypt'] ) // sign 用來驗證雙向資料的正確性 const sign = async (data: string) => { const message_in = new TextEncoder().encode(data) const message_out = new TextEncoder().encode(data) const hmc = crypto.subtle.sign( { name: 'HMAC', hash: 'SHA-256' }, await signkey, message_in ) const isValidate = crypto.subtle.verify( { name: 'HMAC', hash: 'SHA-256' }, await signkey, await hmc, message_out ) // true console.log(await isValidate) } // encrypt 增加一層加密讓 TextEncoder 轉換需要多一把鑰匙才能 decode const decode = async (data: string) => { try { const message = new TextEncoder().encode(data) const iv = crypto.getRandomValues(new Uint8Array(16)) const encodeMessage = crypto.subtle.encrypt( { name: 'AES-CBC', iv }, await decodeKey, message ) const decryptedMessage = crypto.subtle.decrypt( { name: 'AES-CBC', iv }, await decodeKey, await encodeMessage ) const result = new TextDecoder().decode(await decryptedMessage) // data console.log(result) } catch (e) { console.log(e) } } ``` ```typescript const data = "Hello, world!"; const uint8array = new TextEncoder().encode(data); const ArrayBuffer = new TextEncoder().encode(data).buffer; ``` ### getRandomValues 如果你只是需要做隨機亂數生成,使用key沒有安全疑慮的話可以用他,因為 getRandomValues 他使用的生成器是 pseudo-random number generator 而不像 subtle generateKey 是採用 truly random number generator,這對效能來說會提升不少。 ```javascript const array = new Uint32Array(10); self.crypto.getRandomValues(array); console.log("Your lucky numbers:"); for (const num of array) { console.log(num); } ``` hint : getRandomValues 生成的key不保證他的加密算法沒有安全疑慮的,所以使用前要`注意一下`。 ### randomUUID 另一個更輕量的隨機生成數就是 randomUUID ,生成的結構是跟 v4 UUID 是一樣的,所以如果你需要用在data 的 id 剛好格式需要 UUID 的形式就用他吧!! ```typescript /* Assuming that self.crypto.randomUUID() is available */ let uuid = self.crypto.randomUUID(); console.log(uuid); // for example "36b8f84d-df4e-4d49-b662-bcde71a8764f" ``` ## Math.random() 機率分佈不均勻 因為真正的隨機事件其實際上是要不可預測的結果,Math.random採用的是PRNG算法生成隨機seed來達隨機亂數之目的,由PRNG的生成算法是確定的,因此生成的隨機順序列表在理論上是可以的預測的。因此,在某種情況下,使用Math.random() 隨機生成的隨機數分佈可能不均。時間段內,生成的隨機分配可能會偏離均分佈。 ```typescript function randomWithCrypto() { const array = new Uint32Array(1) crypto.getRandomValues(array) return Math.floor(array[0] / Math.pow(2, 32) * 100) // 1-100 } function randomWithMathRandom() { return Math.floor(Math.random() * 100) } function crypto1000() { let result = {} let key for (let i = 0; i < 10000; i++) { key = randomWithCrypto() if (result[key]) { result[key]++ } else { result[key] = 1 } } return result } function mathRandom1000() { let result = {} for (let i = 0; i < 10000; i++) { key = randomWithMathRandom() if (result[key]) { result[key]++ } else { result[key] = 1 } } return result } console.time('crypto') crypto1000() console.timeEnd('crypto') //crypto: 16.324ms console.time('Math') mathRandom1000() console.timeEnd('Math') // Math: 0.865ms ``` | 方法 | Math.random() | getRandomValues()| | ---------------- | --------- | --------------------- | | 生成數分布均勻度 | 不均勻 | 均勻 | | 安全性 | 不安全 | 安全 | | 10000 次生成時間 (ms) | 0.8 ~ 1 | 15 以上 | `生成數分布均勻度` 是指生成的隨機數在 0 到 100 之間的分布情況。由demo可知,Math.random() 生成的隨機數分布不均勻,而 getRandomValues() 生成的隨機數分布均勻。(可以貼範例跑跑看結果XD) 而 `安全性` 則是指生成的隨機數是否足夠安全,能否用於加密等安全需求場合。`Math.random()`不安全,因為它的生成數容易被猜測。而 `getRandomValues()` 則相對安全,因為它的生成數具有高度的隨機性和不可預測性。 ### 額外補充 generateKey VS importKey,兩者都是密鑰提供方式,差別在於importKey 可以將已有的密鑰在做加密提供crypto api去做解析。 ```typescript const key = crypto.subtle.importKey( 'raw', new TextEncoder().encode('my_secret'), { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'], ); const key = await window.crypto.subtle.generateKey( {name: 'HMAC', hash: 'SHA-256', length: 256}, true, // Extractable ['sign', 'verify'] // Key usages ); ``` ## library hmac 他是一個特定obj資料格式,createHmac 用於生成實例,update 用時存放data到 hmac object中,digest 則是將hmac object data轉譯成你想要的資料格式,例如hex或是base64 ```typescript import { createHmac } from 'node:crypto'; function getToken(id: string): string { const hmac = createHmac('sha256', 'my_secret') hmac.update(JSON.stringify({ id })) const token = hmac.digest('hex') return token; } ``` ## arraybuffer to string (decode) ```typescript const key = crypto.subtle.importKey( 'raw', new TextEncoder().encode('Kjhg2365987'), { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign', 'verify'] ) const message = new TextDecoder().encoding(JSON.stringify({ id: '1' })) function toHex(arrayBuffer) { return Array.prototype.map .call(new Uint8Array(arrayBuffer), (n) => n.toString(16).padStart(2, '0')) .join(''); } const hmac = crypto.subtle.sign( 'HMAC', await key, message ) const token = toHex(hmac) ```Uint16Array ### compare hash ```typescript import crypto from 'crypto' const secret = 'alpha' const string = 'bacon' const compareHmac = (text) => { // from validate data const hash = crypto.createHmac('SHA256', secret).update(string).digest('base64') const result = crypto.createHmac('SHA256', secret).update(text).digest('base64') if (hash == result) { console.log('match') } else { console.log('no match') } } ``` ## Uint8Array vs Uint16Array Uint16Array 跟 Uint8Array 都是一種二進制的格式,差別在於字元中儲存的值大小與記憶體儲存的差異: * Uint8Array : array 中每個字元佔用 8 bytes ,所以值得範圍會是 0 - 255 (2 的 8 次方) * Uint16Array : array 中每個字元佔用 8 bytes ,所以值得範圍會是 0 - 65535 (2 的 16 次方) 用途: * Uint8Array:需要減少內存的情況或是處理值的範圍在 0 - 256內推薦使用,詳情可以查看 查看[utf-8 編碼表](http://www.mytju.com/classcode/tools/encode_utf8.asp) 使用字元的範圍 ,通常簡易圖像數據、音樂資料都會採用以及英文字母轉換。 * Uint16Array : 用於相對複查的演算與大量圖像演算法會去做使用。 ### 使用 Uint8Array 或是 Uint16Array 可以透過 array 用 index方式去修改內容,值得提的是 Uint8Array 跟 Uint16Array 長度是固定的不能做 dynamic length,使用上會是透過 ArrayBuffer去指定長度。 ```typescript let buffer = new ArrayBuffer(16); // 創建一個長度 16 的 buffer let view = new Uint32Array(buffer); // 將 buffer 當成 32位整數的序列 alert(Uint32Array.BYTES_PER_ELEMENT); // 每個整數 4 個字節 alert(view.length); // 總共存 4 個整數 alert(view.byteLength); // 16,每個字節大小 // 让我们写入一个值 view[0] = 123456; for(let num of view) { alert(num); // 123456,然後 0,0,0(4 個值) } ``` ### 範例 但有時候我們並不會去查看 utf-8 編碼去對照,這是我們可以用 TextEncoder 跟 TextDecoder 用來輸入我們想放的 data就好就不用一個一個字元去對照編碼表,預設情況下 TextEncoder encode出來的結果會是 uint8Arrays ,所以 example 1 跟 example 2 結果一樣 ```typescript let data = 'name' const encoder = new TextEncoder(); const decoder = new TextDecoder(); <!-- example 1 --> const uint8Arrays = encoder.encode(data) <!-- example 2 --> const uint8Arrays2 = new.Uint8Array(encoder.encode(data)) const decode = new TextDecoder().decode(uint8Arrays) //name ``` ### unit8ToString unit8可以透過 FileReader 去解析,但 FileReader 他是 browser 的 api ,所以在node runtime 中會報 error 要注意。 ```typescript const largeuint8ArrToString = (uint8arr: Uint8Array, callBack: (e: ProgressEvent<FileReader>) => void) => { let bb = new Blob([uint8arr]) // FileReader 是 webApi所以只能在 client component 中的 useEffect 中使用 const reader = new FileReader() reader.onload = (e) => { callBack(e) } reader.readAsText(bb) } largeuint8ArrToString(unitArray8, (e) => console.log(e.target?.result)) ``` unit16ToString 待解