-
Notifications
You must be signed in to change notification settings - Fork 7
refactor(amsg): 单用户/Cloudflare 加密改用 Web Crypto,免 nodejs_compat #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@rei-standard/amsg-server": minor | ||
| --- | ||
|
|
||
| 单用户 / Cloudflare 入口(`@rei-standard/amsg-server/cloudflare`)的载荷加密改用 Web Crypto(`globalThis.crypto.subtle`),不再依赖 Node 的 `crypto`。整条子路径现在只用 WHATWG 标准 API,Worker bundle 打包免开 `nodejs_compat` 兼容开关,可直接粘进 Cloudflare Dashboard。 | ||
|
|
||
| 加密线格式(AES-256-GCM、载荷 base64、入库 hex `iv:authTag:data`)保持不变,旧数据照常解密。因实现改为异步,`@rei-standard/amsg-server/cloudflare` 重新导出的 `deriveUserEncryptionKey`、`decryptPayload`、`encryptForStorage`、`decryptFromStorage` 现在返回 Promise,直接调用需 `await`。 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,70 +1,80 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Encryption utility library (SDK version) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ReiStandard SDK v2.0.1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Encryption utility library (Web Crypto version) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ReiStandard SDK | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Wraps AES-256-GCM operations for request/response and storage encryption. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * AES-256-GCM for request/response and storage encryption. Built on | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `globalThis.crypto.subtle` so it runs on any Web Crypto runtime (Cloudflare | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Workers, Vercel/Netlify Edge, Deno, Bun, Node ≥ 19) with no Node builtins. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * That lets the single-user / Cloudflare entry bundle without `nodejs_compat`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Wire format is byte-for-byte identical to the previous node:crypto version: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * payload uses standard base64 with a separate `authTag`; storage uses hex in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `iv:authTag:encryptedData`. Web Crypto appends the GCM tag to the ciphertext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * so these functions split it off on encrypt and re-join it on decrypt. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utf8, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utf8Decode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| randomBytes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| concatBytes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bytesToHex, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hexToBytes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bytesToBase64, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| base64ToBytes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from './webcrypto-utils.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const subtle = globalThis.crypto.subtle; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const TAG_LEN = 16; // AES-GCM auth tag length in bytes (tagLength: 128) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Import a 64-char hex key as an AES-256-GCM CryptoKey. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function importAesKey(hexKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return subtle.importKey('raw', hexToBytes(hexKey), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Derive a user-specific encryption key from the master key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} userId - Unique user identifier. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} masterKey - 64-char hex master key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {string} 64-char hex key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {Promise<string>} 64-char hex key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function deriveUserEncryptionKey(userId, masterKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return createHash('sha256') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .update(masterKey + userId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .digest('hex') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .slice(0, 64); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function deriveUserEncryptionKey(userId, masterKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const digest = await subtle.digest('SHA-256', utf8(masterKey + userId)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return bytesToHex(new Uint8Array(digest)).slice(0, 64); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Decrypt a client-encrypted request body (AES-256-GCM, base64 encoded). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {{ iv: string, authTag: string, encryptedData: string }} encryptedPayload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} encryptionKey - 64-char hex key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {Object} Decrypted JSON object. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {Promise<Object>} Decrypted JSON object. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function decryptPayload(encryptedPayload, encryptionKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function decryptPayload(encryptedPayload, encryptionKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { iv, authTag, encryptedData } = encryptedPayload; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const decipher = createDecipheriv( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'aes-256-gcm', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Buffer.from(encryptionKey, 'hex'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Buffer.from(iv, 'base64') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decipher.setAuthTag(Buffer.from(authTag, 'base64')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const decrypted = Buffer.concat([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decipher.update(Buffer.from(encryptedData, 'base64')), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decipher.final() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.parse(decrypted.toString('utf8')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const key = await importAesKey(encryptionKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sealed = concatBytes(base64ToBytes(encryptedData), base64ToBytes(authTag)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const plain = await subtle.decrypt({ name: 'AES-GCM', iv: base64ToBytes(iv), tagLength: 128 }, key, sealed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.parse(utf8Decode(plain)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The export async function decryptPayload(encryptedPayload, encryptionKey) {
if (!encryptedPayload || typeof encryptedPayload !== 'object') {
throw new Error('Invalid encrypted payload: must be an object');
}
const { iv, authTag, encryptedData } = encryptedPayload;
if (!iv || !authTag || !encryptedData) {
throw new Error('Invalid encrypted payload: missing iv, authTag, or encryptedData');
}
const key = await importAesKey(encryptionKey);
const sealed = concatBytes(base64ToBytes(encryptedData), base64ToBytes(authTag));
const plain = await subtle.decrypt({ name: 'AES-GCM', iv: base64ToBytes(iv), tagLength: 128 }, key, sealed);
return JSON.parse(utf8Decode(plain));
} |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Encrypt a JSON payload for API transfer (AES-256-GCM, base64 encoded). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string|Object} payload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} encryptionKey - 64-char hex key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {{ iv: string, authTag: string, encryptedData: string }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {Promise<{ iv: string, authTag: string, encryptedData: string }>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function encryptPayload(payload, encryptionKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function encryptPayload(payload, encryptionKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const plaintext = typeof payload === 'string' ? payload : JSON.stringify(payload); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const iv = randomBytes(12); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cipher = createCipheriv('aes-256-gcm', Buffer.from(encryptionKey, 'hex'), iv); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const authTag = cipher.getAuthTag(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const key = await importAesKey(encryptionKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sealed = new Uint8Array(await subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, key, utf8(plaintext))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| iv: iv.toString('base64'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authTag: authTag.toString('base64'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| encryptedData: encrypted.toString('base64') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| iv: bytesToBase64(iv), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authTag: bytesToBase64(sealed.slice(sealed.length - TAG_LEN)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| encryptedData: bytesToBase64(sealed.slice(0, sealed.length - TAG_LEN)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -73,30 +83,28 @@ export function encryptPayload(payload, encryptionKey) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} text - Plaintext string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} encryptionKey - 64-char hex key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {string} Format: iv:authTag:encryptedData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {Promise<string>} Format: iv:authTag:encryptedData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function encryptForStorage(text, encryptionKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function encryptForStorage(text, encryptionKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const iv = randomBytes(16); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cipher = createCipheriv('aes-256-gcm', Buffer.from(encryptionKey, 'hex'), iv); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const authTag = cipher.getAuthTag(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const key = await importAesKey(encryptionKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sealed = new Uint8Array(await subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, key, utf8(text))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = sealed.slice(0, sealed.length - TAG_LEN); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tag = sealed.slice(sealed.length - TAG_LEN); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${bytesToHex(iv)}:${bytesToHex(tag)}:${bytesToHex(data)}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Decrypt data from database storage format. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} encryptedText - Format: iv:authTag:encryptedData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} encryptionKey - 64-char hex key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {string} Plaintext string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {Promise<string>} Plaintext string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function decryptFromStorage(encryptedText, encryptionKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function decryptFromStorage(encryptedText, encryptionKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(':'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const decipher = createDecipheriv( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'aes-256-gcm', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Buffer.from(encryptionKey, 'hex'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Buffer.from(ivHex, 'hex') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decipher.setAuthTag(Buffer.from(authTagHex, 'hex')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return decipher.update(encryptedDataHex, 'hex', 'utf8') + decipher.final('utf8'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const key = await importAesKey(encryptionKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sealed = concatBytes(hexToBytes(encryptedDataHex), hexToBytes(authTagHex)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const plain = await subtle.decrypt({ name: 'AES-GCM', iv: hexToBytes(ivHex), tagLength: 128 }, key, sealed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return utf8Decode(plain); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+104
to
110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,45 @@ export function jsonToBase64Url(value) { | |
| return bytesToBase64Url(utf8(JSON.stringify(value))); | ||
| } | ||
|
|
||
| /** Encode bytes as standard base64 (with `=` padding). */ | ||
| export function bytesToBase64(buf) { | ||
| const bytes = toUint8(buf); | ||
| let bin = ''; | ||
| for (let i = 0; i < bytes.length; i++) { | ||
| bin += String.fromCharCode(bytes[i]); | ||
| } | ||
| return btoa(bin); | ||
| } | ||
|
|
||
| /** Decode a standard base64 string into a Uint8Array. */ | ||
| export function base64ToBytes(str) { | ||
| const bin = atob(str); | ||
| const out = new Uint8Array(bin.length); | ||
| for (let i = 0; i < bin.length; i++) { | ||
| out[i] = bin.charCodeAt(i); | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| /** Encode bytes as lowercase hex. */ | ||
| export function bytesToHex(buf) { | ||
| const bytes = toUint8(buf); | ||
| let hex = ''; | ||
| for (let i = 0; i < bytes.length; i++) { | ||
| hex += bytes[i].toString(16).padStart(2, '0'); | ||
| } | ||
| return hex; | ||
| } | ||
|
|
||
| /** Decode a hex string into a Uint8Array. */ | ||
| export function hexToBytes(hex) { | ||
| const out = new Uint8Array(hex.length / 2); | ||
| for (let i = 0; i < out.length; i++) { | ||
| out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); | ||
| } | ||
| return out; | ||
| } | ||
|
Comment on lines
+80
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The export function hexToBytes(hex) {
if (typeof hex !== 'string' || hex.length % 2 !== 0) {
throw new Error('Invalid hex string: must be an even-length string');
}
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) {
const val = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
if (Number.isNaN(val)) {
throw new Error('Invalid hex character');
}
out[i] = val;
}
return out;
} |
||
|
|
||
| /** | ||
| * Constant-time byte comparison. Returns true iff `a` and `b` are equal-length | ||
| * sequences with the same bytes. Length is intentionally NOT secret — early | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
importAesKeyfunction does not validate thathexKeyis a valid 64-character hex string (representing a 256-bit AES key). If an invalid key length is passed,subtle.importKeywill throw a genericDOMException. Adding a validation check here improves error clarity and robustness.