# 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 待解