Skip to content

Commit 6150c7b

Browse files
committed
Merge branch 'release/v0.27.0'
2 parents 9f9de5f + d563252 commit 6150c7b

9 files changed

Lines changed: 85 additions & 64 deletions

File tree

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zeed",
33
"type": "module",
4-
"version": "0.26.4",
4+
"version": "0.27.0",
55
"description": "🌱 Simple foundation library",
66
"author": {
77
"name": "Dirk Holtwick",
@@ -69,18 +69,18 @@
6969
"watch": "nr build -- --watch src"
7070
},
7171
"devDependencies": {
72-
"@antfu/eslint-config": "^3.11",
73-
"@antfu/ni": "^0.23.1",
72+
"@antfu/eslint-config": "^3.12.1",
73+
"@antfu/ni": "^0.23.2",
7474
"@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",
78-
"esbuild": "^0.24.0",
79-
"eslint": "^9.16.0",
78+
"esbuild": "^0.24.2",
79+
"eslint": "^9.17.0",
8080
"playwright": "^1.49.1",
8181
"tsup": "^8.3.5",
8282
"typescript": "^5.7.2",
83-
"vite": "^6.0.3",
83+
"vite": "^6.0.6",
8484
"vitest": "^2.1.8"
8585
},
8686
"pnpm": {
Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { beforeAll, describe, expect, it } from 'vitest'
2-
import { hxDecrypt, hxEncrypt } from './aes-sealed'
2+
import { fromBase64, toBase64 } from '../data/bin'
3+
import { decryptAesGcm, encryptAesGcm } from './aes-sealed'
4+
import { deriveKeyPbkdf2 } from './crypto'
35

46
describe('aes Encryption and Decryption', () => {
57
let key: CryptoKey
@@ -17,21 +19,21 @@ describe('aes Encryption and Decryption', () => {
1719

1820
it('should encrypt and decrypt data correctly', async () => {
1921
const data = new TextEncoder().encode('Hello, World!')
20-
const encryptedData = await hxEncrypt(data, key)
21-
const decryptedData = await hxDecrypt(encryptedData, key)
22+
const encryptedData = await encryptAesGcm(data, key)
23+
const decryptedData = await decryptAesGcm(encryptedData, key)
2224
expect(new TextDecoder().decode(decryptedData)).toBe('Hello, World!')
2325
})
2426

2527
it('should produce different ciphertexts for the same plaintext', async () => {
2628
const data = new TextEncoder().encode('Hello, World!')
27-
const encryptedData1 = await hxEncrypt(data, key)
28-
const encryptedData2 = await hxEncrypt(data, key)
29+
const encryptedData1 = await encryptAesGcm(data, key)
30+
const encryptedData2 = await encryptAesGcm(data, key)
2931
expect(encryptedData1).not.toEqual(encryptedData2)
3032
})
3133

3234
it('should fail to decrypt with a different key', async () => {
3335
const data = new TextEncoder().encode('Hello, World!')
34-
const encryptedData = await hxEncrypt(data, key)
36+
const encryptedData = await encryptAesGcm(data, key)
3537
const differentKey = await crypto.subtle.generateKey(
3638
{
3739
name: 'AES-GCM',
@@ -40,17 +42,38 @@ describe('aes Encryption and Decryption', () => {
4042
true,
4143
['encrypt', 'decrypt'],
4244
)
43-
await expect(hxDecrypt(encryptedData, differentKey)).rejects.toThrow()
45+
await expect(decryptAesGcm(encryptedData, differentKey)).rejects.toThrow()
4446
})
4547

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-
// })
48+
it('should decrypt a sample that was generated by Swift code', async () => {
49+
const key = await deriveKeyPbkdf2(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), {
50+
salt: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
51+
iterations: 100000,
52+
})
53+
const dataFromKey = await crypto.subtle.exportKey('raw', key)
54+
expect(toBase64(dataFromKey)).toMatchInlineSnapshot(`"UDl7buu/Zn/UxCIEp55MOTOKcDHvb959P+eyozok7BA="`)
55+
56+
// Create for Swift sample
57+
// const sample = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
58+
// const encryptedData2 = await hxEncrypt(sample, key)
59+
// expect(toBase64(encryptedData2)).toMatchInlineSnapshot(`"Akjv9nbmkMUsbYg3TuJIXjafK9kUkZMdJ49XdapndkFtiuLVEyg="`)
60+
61+
// From Swift sample
62+
const encryptedData = fromBase64('pVzIX88DZNjLw1VLLEnsIaqt4/jq4ZGJc8qDU2YruZJr6N9V0i0=')
63+
const decryptedData = await decryptAesGcm(encryptedData, key)
64+
expect(decryptedData).toMatchInlineSnapshot(`
65+
Uint8Array [
66+
9,
67+
8,
68+
7,
69+
6,
70+
5,
71+
4,
72+
3,
73+
2,
74+
1,
75+
0,
76+
]
77+
`)
78+
})
5679
})

src/common/crypto/aes-sealed.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,33 @@
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-
}
1+
const AES_GCM_TAG_LENGTH_BITS = 128
62

3+
export async function encryptAesGcm(data: Uint8Array, key: CryptoKey, authenticating: Uint8Array = new Uint8Array()): Promise<Uint8Array> {
4+
const iv = crypto.getRandomValues(new Uint8Array(12)) // AES-GCM requires a 12-byte IV
75
const encrypted = await crypto.subtle.encrypt(
86
{
97
name: 'AES-GCM',
108
iv,
11-
tagLength: 128,
12-
additionalData: tag,
9+
tagLength: AES_GCM_TAG_LENGTH_BITS,
10+
additionalData: authenticating,
1311
},
1412
key,
1513
data,
1614
)
1715

1816
const encryptedArray = new Uint8Array(encrypted)
19-
const combined = new Uint8Array(iv.length + encryptedArray.length + tag.length)
17+
const combined = new Uint8Array(iv.length + encryptedArray.length)
2018
combined.set(iv)
2119
combined.set(encryptedArray, iv.length)
22-
combined.set(tag, encryptedArray.length + iv.length)
2320
return combined
2421
}
2522

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.
23+
export async function decryptAesGcm(data: Uint8Array, key: CryptoKey, authenticating: Uint8Array = new Uint8Array()): Promise<Uint8Array> {
2924
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-
25+
const encrypted = data.slice(12) // ciphertext and tag of 128 bits
3426
const decrypted = await crypto.subtle.decrypt({
3527
name: 'AES-GCM',
3628
iv,
37-
tagLength: 128,
38-
additionalData: tag,
29+
tagLength: AES_GCM_TAG_LENGTH_BITS,
30+
additionalData: authenticating,
3931
}, key, encrypted)
4032
return new Uint8Array(decrypted)
4133
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* eslint-disable prefer-spread */
22
/* eslint-disable no-cond-assign */
33

4-
import { DefaultLogger } from '.'
4+
import { equalBinary, fromBase64, toBase64 } from '../data/bin'
5+
import { DefaultLogger } from '../log/log'
56
import { decrypt, deriveKeyPbkdf2, digest, encrypt, randomUint8Array } from './crypto'
6-
import { equalBinary, fromBase64, toBase64 } from './data/bin'
77

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { BinInput } from './data/bin'
2-
import { equalBinary, toUint8Array } from './data/bin'
1+
import type { BinInput } from '../data/bin'
2+
import { equalBinary, toUint8Array } from '../data/bin'
33

44
/** Get random bytes using window.crypto if available. Else use a poor fallback solution. */
55
export function randomUint8Array(length = 16): Uint8Array {

src/common/crypto/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './aes-sealed'
2+
export * from './crypto'
3+
export * from './xaes'

src/common/crypto/xaes.spec.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// import { toBase64 } from '../data/bin.js'
2-
import { decrypt, encrypt, exportKey, generateKey, importKey } from './xaes'
2+
import { decryptXAES, encryptXAES, exportKeyXAES, generateKeyXAES, importKeyXAES } from './xaes'
33
// import { SHAKE128 } from "@stablelib/sha3";
44

55
describe('xaes.spec', () => {
@@ -28,55 +28,55 @@ describe('xaes.spec', () => {
2828
// })
2929

3030
it('xaes generateKey/encrypt/decrypt', async () => {
31-
const key = await generateKey()
31+
const key = await generateKeyXAES()
3232
const iv = new Uint8Array(24).fill(1)
3333
const data = new Uint8Array(122).fill(2)
3434
const additionalData = new Uint8Array(16).fill(3)
3535
const params = { iv, additionalData }
36-
const encrypted = await encrypt(params, key, data)
37-
const decrypted = await decrypt(params, key, encrypted)
36+
const encrypted = await encryptXAES(params, key, data)
37+
const decrypted = await decryptXAES(params, key, encrypted)
3838
expect(decrypted).toEqual(data.buffer)
3939
})
4040

4141
it('xaes importKey, exportKey', async () => {
4242
const key = new Uint8Array(32).fill(0x04)
43-
const imported = await importKey('raw', key, true)
44-
const exported = await exportKey('raw', imported)
43+
const imported = await importKeyXAES('raw', key, true)
44+
const exported = await exportKeyXAES('raw', imported)
4545
expect(exported).toEqual(key.buffer)
4646
})
4747

4848
it('xaes test vector', async () => {
4949
const nonce = new TextEncoder().encode('ABCDEFGHIJKLMNOPQRSTUVWX')
5050
const plaintext = new TextEncoder().encode('XAES-256-GCM')
51-
const key = await importKey(
51+
const key = await importKeyXAES(
5252
'raw',
5353
new Uint8Array(32).fill(0x01),
5454
false,
5555
)
56-
const ciphertext = await encrypt({ iv: nonce }, key, plaintext)
56+
const ciphertext = await encryptXAES({ iv: nonce }, key, plaintext)
5757
const got = Array.from(new Uint8Array(ciphertext), byte => byte.toString(16).padStart(2, '0')).join('')
5858
const expected = 'ce546ef63c9cc60765923609b33a9a1974e96e52daf2fcf7075e2271'
5959
expect(got).toEqual(expected)
6060

61-
const decrypted = await decrypt({ iv: nonce }, key, ciphertext)
61+
const decrypted = await decryptXAES({ iv: nonce }, key, ciphertext)
6262
expect(new Uint8Array(decrypted)).toEqual(plaintext)
6363
})
6464

6565
it('xaes test vector with additional data', async () => {
66-
const key = await importKey(
66+
const key = await importKeyXAES(
6767
'raw',
6868
new Uint8Array(32).fill(0x03),
6969
false,
7070
)
7171
const aad = new TextEncoder().encode('c2sp.org/XAES-256-GCM')
7272
const nonce = new TextEncoder().encode('ABCDEFGHIJKLMNOPQRSTUVWX')
7373
const plaintext = new TextEncoder().encode('XAES-256-GCM')
74-
const ciphertext = await encrypt({ iv: nonce, additionalData: aad }, key, plaintext)
74+
const ciphertext = await encryptXAES({ iv: nonce, additionalData: aad }, key, plaintext)
7575
const got = Array.from(new Uint8Array(ciphertext), byte => byte.toString(16).padStart(2, '0')).join('')
7676
const expected = '986ec1832593df5443a179437fd083bf3fdb41abd740a21f71eb769d'
7777
expect(got).toEqual(expected)
7878

79-
const decrypted = await decrypt({ iv: nonce, additionalData: aad }, key, ciphertext)
79+
const decrypted = await decryptXAES({ iv: nonce, additionalData: aad }, key, ciphertext)
8080
expect(new Uint8Array(decrypted)).toEqual(plaintext)
8181
})
8282

src/common/crypto/xaes.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async function deriveKeyNonce(key: CryptoKey, iv: BufferSource): Promise<{ key:
8787
* Encrypts data using XAES-256-GCM with the given key and iv.
8888
* Key must be a 256-bit AES-CBC CryptoKey with 'encrypt' usage.
8989
*/
90-
export async function encrypt(params: {
90+
export async function encryptXAES(params: {
9191
iv: BufferSource
9292
additionalData?: BufferSource
9393
}, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer> {
@@ -108,7 +108,7 @@ export async function encrypt(params: {
108108
* Decrypts data using XAES-256-GCM with the given key and iv.
109109
* Key must be a 256-bit AES-CBC CryptoKey with 'encrypt' and 'decrypt' usages.
110110
*/
111-
export async function decrypt(params: {
111+
export async function decryptXAES(params: {
112112
iv: BufferSource
113113
additionalData?: BufferSource
114114
}, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer> {
@@ -131,7 +131,7 @@ export async function decrypt(params: {
131131
*
132132
* This function is not necessary, as you can use crypto.subtle.generateKey with AES-CBC directly.
133133
*/
134-
export async function generateKey(extractable?: boolean): Promise<CryptoKey> {
134+
export async function generateKeyXAES(extractable?: boolean): Promise<CryptoKey> {
135135
return await crypto.subtle.generateKey(
136136
{
137137
name: 'AES-CBC',
@@ -149,7 +149,7 @@ export async function generateKey(extractable?: boolean): Promise<CryptoKey> {
149149
*
150150
* This function is not necessary, as you can use crypto.subtle.importKey with AES-CBC directly.
151151
*/
152-
export async function importKey(format: 'jwk' | 'raw' | 'pkcs8' | 'spki', keyData: BufferSource | JsonWebKey, extractable?: boolean): Promise<CryptoKey> {
152+
export async function importKeyXAES(format: 'jwk' | 'raw' | 'pkcs8' | 'spki', keyData: BufferSource | JsonWebKey, extractable?: boolean): Promise<CryptoKey> {
153153
return await crypto.subtle.importKey( // @ts-expect-error-next-line
154154
format,
155155
keyData,
@@ -165,6 +165,6 @@ export async function importKey(format: 'jwk' | 'raw' | 'pkcs8' | 'spki', keyDat
165165
*
166166
* This function is not necessary, as you can use crypto.subtle.exportKey directly.
167167
*/
168-
export async function exportKey(format: 'jwk' | 'pkcs8' | 'raw' | 'spki', key: CryptoKey): Promise<ArrayBuffer | JsonWebKey> {
168+
export async function exportKeyXAES(format: 'jwk' | 'pkcs8' | 'raw' | 'spki', key: CryptoKey): Promise<ArrayBuffer | JsonWebKey> {
169169
return await crypto.subtle.exportKey(format, key)
170170
}

tsup.config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ export const tsup: Options = {
88
clean: true,
99
dts: true,
1010
format: ['esm', 'cjs'],
11-
minify: false,
11+
minify: true,
1212
splitting: true,
1313
bundle: true,
1414
skipNodeModulesBundle: true,
1515
entryPoints: ['src/index.all.ts'],
1616
target: 'es2020',
1717
outDir: 'dist',
18-
entry: ['src/**/*.ts'],
18+
entry: [
19+
'src/**/*.ts',
20+
'!src/**/*.spec.ts',
21+
],
1922
}

0 commit comments

Comments
 (0)