Skip to content

Commit 309b177

Browse files
committed
Merge branch 'release/v0.26.3'
2 parents 04e6b0d + 5545914 commit 309b177

File tree

7 files changed

+145
-93
lines changed

7 files changed

+145
-93
lines changed

.github/copilot-instructions.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Test files should always end on `.spec.ts`.
2+
3+
Indentation is 2 chars.
4+
5+
Prefer private variable to start with `_`.

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zeed",
33
"type": "module",
4-
"version": "0.26.2",
4+
"version": "0.26.3",
55
"description": "🌱 Simple foundation library",
66
"author": {
77
"name": "Dirk Holtwick",
@@ -71,16 +71,16 @@
7171
"devDependencies": {
7272
"@antfu/eslint-config": "^3.11",
7373
"@antfu/ni": "^0.23.1",
74-
"@types/node": "^22.10.1",
74+
"@types/node": "^22.10.2",
7575
"@vitejs/plugin-vue": "^5.2.1",
7676
"@vitest/browser": "^2.1.8",
7777
"@vitest/coverage-v8": "^2.1.8",
7878
"esbuild": "^0.24.0",
7979
"eslint": "^9.16.0",
80-
"playwright": "^1.49.0",
80+
"playwright": "^1.49.1",
8181
"tsup": "^8.3.5",
8282
"typescript": "^5.7.2",
83-
"vite": "^6.0.2",
83+
"vite": "^6.0.3",
8484
"vitest": "^2.1.8"
8585
},
8686
"pnpm": {

src/common/crypto.spec.ts

+6-89
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { DefaultLogger } from '.'
55
import { decrypt, deriveKeyPbkdf2, digest, encrypt, randomUint8Array } from './crypto'
6-
import { equalBinary, toHex } from './data/bin'
6+
import { equalBinary, fromBase64, toBase64 } from './data/bin'
77

88
const log = DefaultLogger('crypto.spec')
99

@@ -16,54 +16,15 @@ describe('crypto', () => {
1616
let id: Uint8Array | undefined
1717
while ((id = list.pop())) {
1818
// console.log(id)
19-
expect(equalBinary(id, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]))).toBe(
20-
false,
21-
)
19+
expect(equalBinary(id, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]))).toBe(false)
2220
expect(id?.length).toBe(8)
2321
expect(list).not.toContain(id)
2422
}
2523
})
2624

2725
it('should digest', async () => {
28-
expect(toHex(await digest('abc'))).toBe(
29-
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
30-
)
31-
expect(await digest(new Uint8Array([1, 2, 3]))).toMatchInlineSnapshot(`
32-
Uint8Array [
33-
3,
34-
144,
35-
88,
36-
198,
37-
242,
38-
192,
39-
203,
40-
73,
41-
44,
42-
83,
43-
59,
44-
10,
45-
77,
46-
20,
47-
239,
48-
119,
49-
204,
50-
15,
51-
120,
52-
171,
53-
204,
54-
206,
55-
213,
56-
40,
57-
125,
58-
132,
59-
161,
60-
162,
61-
1,
62-
28,
63-
251,
64-
129,
65-
]
66-
`)
26+
expect(toBase64(await digest('abc'))).toBe('ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=')
27+
expect(toBase64(await digest(new Uint8Array([1, 2, 3])))).toMatchInlineSnapshot(`"A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc+4E="`)
6728
})
6829

6930
// it("should derive key", async () => {
@@ -92,56 +53,12 @@ Uint8Array [
9253
})
9354
const sample = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
9455
const cipher = await encrypt(sample, key)
95-
// log("cipher", cipher)
56+
log('cipher', toBase64(cipher))
9657

9758
const bin = await decrypt(cipher, key)
9859
expect(equalBinary(sample, bin)).toBe(true)
9960

100-
const binFix = await decrypt(
101-
new Uint8Array([
102-
1,
103-
1,
104-
27,
105-
108,
106-
252,
107-
31,
108-
238,
109-
192,
110-
61,
111-
168,
112-
45,
113-
29,
114-
128,
115-
212,
116-
215,
117-
222,
118-
205,
119-
105,
120-
178,
121-
193,
122-
150,
123-
36,
124-
24,
125-
216,
126-
180,
127-
75,
128-
168,
129-
133,
130-
37,
131-
25,
132-
124,
133-
137,
134-
221,
135-
103,
136-
214,
137-
97,
138-
218,
139-
232,
140-
248,
141-
93,
142-
]),
143-
key,
144-
)
61+
const binFix = await decrypt(fromBase64('AQELynGCxvLXKwLM/oHjOaM4R6d7oAzxJpgpCZnKmWwhkwIDzpPMUQ=='), key)
14562
expect(binFix).toEqual(sample)
14663
})
14764

src/common/crypto.ts

+32
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,38 @@ export async function deriveKeyPbkdf2(
6666
)
6767
}
6868

69+
export async function deriveKeyPbkdf2CBC(
70+
secret: BinInput,
71+
opt: {
72+
iterations?: number
73+
salt?: BinInput
74+
} = {},
75+
): Promise<CryptoKey> {
76+
const secretBuffer = toUint8Array(secret)
77+
const keyMaterial = await crypto.subtle.importKey(
78+
'raw',
79+
secretBuffer,
80+
CRYPTO_DEFAULT_DERIVE_ALG,
81+
false,
82+
['deriveKey'],
83+
)
84+
return await crypto.subtle.deriveKey(
85+
{
86+
name: CRYPTO_DEFAULT_DERIVE_ALG,
87+
salt: opt.salt ? toUint8Array(opt.salt) : new Uint8Array(0),
88+
iterations: opt.iterations ?? CRYPTO_DEFAULT_DERIVE_ITERATIONS,
89+
hash: CRYPTO_DEFAULT_HASH_ALG,
90+
},
91+
keyMaterial,
92+
{
93+
name: CRYPTO_DEFAULT_ALG,
94+
length: 256,
95+
},
96+
true,
97+
['encrypt', 'decrypt'],
98+
)
99+
}
100+
69101
function getMagicId() {
70102
return new Uint8Array([1, 1])
71103
}

src/common/crypto/aes-sealed.spec.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { beforeAll, describe, expect, it } from 'vitest'
2+
import { hxDecrypt, hxEncrypt } from './aes-sealed'
3+
4+
describe('aes Encryption and Decryption', () => {
5+
let key: CryptoKey
6+
7+
beforeAll(async () => {
8+
key = await crypto.subtle.generateKey(
9+
{
10+
name: 'AES-GCM',
11+
length: 256,
12+
},
13+
true,
14+
['encrypt', 'decrypt'],
15+
)
16+
})
17+
18+
it('should encrypt and decrypt data correctly', async () => {
19+
const data = new TextEncoder().encode('Hello, World!')
20+
const encryptedData = await hxEncrypt(data, key)
21+
const decryptedData = await hxDecrypt(encryptedData, key)
22+
expect(new TextDecoder().decode(decryptedData)).toBe('Hello, World!')
23+
})
24+
25+
it('should produce different ciphertexts for the same plaintext', async () => {
26+
const data = new TextEncoder().encode('Hello, World!')
27+
const encryptedData1 = await hxEncrypt(data, key)
28+
const encryptedData2 = await hxEncrypt(data, key)
29+
expect(encryptedData1).not.toEqual(encryptedData2)
30+
})
31+
32+
it('should fail to decrypt with a different key', async () => {
33+
const data = new TextEncoder().encode('Hello, World!')
34+
const encryptedData = await hxEncrypt(data, key)
35+
const differentKey = await crypto.subtle.generateKey(
36+
{
37+
name: 'AES-GCM',
38+
length: 256,
39+
},
40+
true,
41+
['encrypt', 'decrypt'],
42+
)
43+
await expect(hxDecrypt(encryptedData, differentKey)).rejects.toThrow()
44+
})
45+
46+
// it('should decrypt a sample that was generated by Swift code', async () => {
47+
// const key = await deriveKeyPbkdf2CBC(new Uint8Array([1, 2, 3]), {
48+
// salt: new Uint8Array([1, 2, 3]),
49+
// })
50+
// // expect(toBase64(key)).toMatchInlineSnapshot()
51+
// const sample = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
52+
// const encryptedData = fromBase64('br6sc+pnZaIXcV1fTygAs/UJlDZIIBY50i56MMGNampZTcSakt0=')
53+
// const decryptedData = await hxDecrypt(encryptedData, key)
54+
// expect(decryptedData).toMatchInlineSnapshot()
55+
// })
56+
})

src/common/crypto/aes-sealed.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export async function hxEncrypt(data: Uint8Array, key: CryptoKey, tag?: Uint8Array): Promise<Uint8Array> {
2+
const iv = crypto.getRandomValues(new Uint8Array(12)) // AES-GCM requires a 12-byte IV
3+
if (!tag) {
4+
tag = crypto.getRandomValues(new Uint8Array(16))
5+
}
6+
7+
const encrypted = await crypto.subtle.encrypt(
8+
{
9+
name: 'AES-GCM',
10+
iv,
11+
tagLength: 128,
12+
additionalData: tag,
13+
},
14+
key,
15+
data,
16+
)
17+
18+
const encryptedArray = new Uint8Array(encrypted)
19+
const combined = new Uint8Array(iv.length + encryptedArray.length + tag.length)
20+
combined.set(iv)
21+
combined.set(encryptedArray, iv.length)
22+
combined.set(tag, encryptedArray.length + iv.length)
23+
return combined
24+
}
25+
26+
export async function hxDecrypt(data: Uint8Array, key: CryptoKey): Promise<Uint8Array> {
27+
// The data layout of the combined representation is nonce, ciphertext, then tag.
28+
// The nonce is 12 bytes, the tag is 16 bytes, and the ciphertext is the rest of the data.
29+
const iv = data.slice(0, 12) // nonce is the first 12 bytes
30+
const encrypted = data.slice(12, -16) // The ciphertext is everything between the nonce and the tag.
31+
const tag = data.slice(-16) // The authentication tag has a length of 16 bytes.
32+
// console.log({ iv, encrypted, tag })
33+
34+
const decrypted = await crypto.subtle.decrypt({
35+
name: 'AES-GCM',
36+
iv,
37+
tagLength: 128,
38+
additionalData: tag,
39+
}, key, encrypted)
40+
return new Uint8Array(decrypted)
41+
}

src/common/data/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export * from './signal'
2828
export * from './sortable'
2929
export * from './sorted'
3030
export * from './string-deburr'
31+
export * from './string-hash-fnv'
3132
export * from './string-hash-pool'
3233
export * from './url'
3334
export * from './utils'

0 commit comments

Comments
 (0)