Skip to content

Commit cf7ac0c

Browse files
committed
Fix comlink serialization by converting Buffer to Uint8Array at worker boundary
Fix comlink serialization by returning Uint8Array from aesUtils aesUtils now returns plain Uint8Array instead of Buffer, fixing 'Unserializable return value' errors on Linux CI where Buffer objects don't serialize correctly through comlink's structured clone.
1 parent f80db24 commit cf7ac0c

File tree

6 files changed

+37
-17
lines changed

6 files changed

+37
-17
lines changed

packages/sdk/src/encryption/EncryptionService.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ export class EncryptionService {
3636
* Note: The input data buffer is transferred to the worker and becomes unusable after this call.
3737
*/
3838
async encryptWithAES(data: Uint8Array, cipherKey: Uint8Array): Promise<Uint8Array> {
39+
// Ensure we have plain Uint8Array instances for worker communication (not Buffer subclass)
40+
const dataArray = new Uint8Array(data)
41+
const keyArray = new Uint8Array(cipherKey)
3942
const result = await this.getWorkerApi().encrypt(
40-
transfer({ data, cipherKey }, [data.buffer])
43+
transfer({ data: dataArray, cipherKey: keyArray }, [dataArray.buffer])
4144
)
4245
if (result.type === 'error') {
4346
throw new Error(`AES encryption failed: ${result.message}`)
@@ -49,10 +52,11 @@ export class EncryptionService {
4952
* Encrypt the next group key using the current group key.
5053
*/
5154
async encryptNextGroupKey(currentKey: GroupKey, nextKey: GroupKey): Promise<EncryptedGroupKey> {
55+
// Convert Buffer to Uint8Array for worker communication
5256
const result = await this.getWorkerApi().encryptGroupKey({
5357
nextGroupKeyId: nextKey.id,
54-
nextGroupKeyData: nextKey.data,
55-
currentGroupKeyData: currentKey.data
58+
nextGroupKeyData: new Uint8Array(nextKey.data),
59+
currentGroupKeyData: new Uint8Array(currentKey.data)
5660
})
5761
if (result.type === 'error') {
5862
throw new Error(`Group key encryption failed: ${result.message}`)
@@ -67,10 +71,11 @@ export class EncryptionService {
6771
* Decrypt an encrypted group key using the current group key.
6872
*/
6973
async decryptNextGroupKey(currentKey: GroupKey, encryptedKey: EncryptedGroupKey): Promise<GroupKey> {
74+
// Convert Buffer to Uint8Array for worker communication
7075
const result = await this.getWorkerApi().decryptGroupKey({
7176
encryptedGroupKeyId: encryptedKey.id,
72-
encryptedGroupKeyData: encryptedKey.data,
73-
currentGroupKeyData: currentKey.data
77+
encryptedGroupKeyData: new Uint8Array(encryptedKey.data),
78+
currentGroupKeyData: new Uint8Array(currentKey.data)
7479
})
7580
if (result.type === 'error') {
7681
throw new Error(`Group key decryption failed: ${result.message}`)
@@ -88,12 +93,13 @@ export class EncryptionService {
8893
groupKey: GroupKey,
8994
encryptedNewGroupKey?: EncryptedGroupKey
9095
): Promise<[Uint8Array, GroupKey?]> {
96+
// Convert Buffer to Uint8Array for worker communication
9197
const request = {
9298
content,
93-
groupKeyData: groupKey.data,
99+
groupKeyData: new Uint8Array(groupKey.data),
94100
newGroupKey: encryptedNewGroupKey ? {
95101
id: encryptedNewGroupKey.id,
96-
data: encryptedNewGroupKey.data
102+
data: new Uint8Array(encryptedNewGroupKey.data)
97103
} : undefined
98104
}
99105
const result = await this.getWorkerApi().decryptStreamMessage(

packages/sdk/src/encryption/EncryptionUtil.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export class EncryptionUtil {
118118
const wrappingAESKey = await this.deriveAESWrapperKey(sharedSecret, kdfSalt)
119119

120120
// Encrypt plaintext with the AES wrapping key
121-
const aesEncryptedPlaintext = encryptWithAES(plaintextBuffer, Buffer.from(wrappingAESKey))
121+
const aesEncryptedPlaintext = encryptWithAES(plaintextBuffer, wrappingAESKey)
122122

123123
// Concatenate the deliverables into a binary package
124124
return Buffer.concat([kemCipher, kdfSalt, aesEncryptedPlaintext])
@@ -140,6 +140,6 @@ export class EncryptionUtil {
140140
const wrappingAESKey = await this.deriveAESWrapperKey(sharedSecret, kdfSalt)
141141

142142
// Decrypt the aesEncryptedPlaintext
143-
return decryptWithAES(aesEncryptedPlaintext, Buffer.from(wrappingAESKey))
143+
return Buffer.from(decryptWithAES(aesEncryptedPlaintext, wrappingAESKey))
144144
}
145145
}

packages/sdk/src/encryption/EncryptionWorker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import {
2121
const workerApi = {
2222
encrypt: async (request: AESEncryptRequest): Promise<AESEncryptResult> => {
2323
try {
24-
const result = encryptWithAES(request.data, request.cipherKey)
25-
return transfer({ type: 'success', data: result }, [result.buffer])
24+
const data = encryptWithAES(request.data, request.cipherKey)
25+
return transfer({ type: 'success', data }, [data.buffer])
2626
} catch (err) {
2727
return { type: 'error', message: String(err) }
2828
}

packages/sdk/src/encryption/aesUtils.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,36 @@ import { createCipheriv, createDecipheriv } from '@streamr/utils'
77

88
export const INITIALIZATION_VECTOR_LENGTH = 16
99

10+
/**
11+
* Concatenate multiple Uint8Arrays into a single Uint8Array.
12+
*/
13+
function concatBytes(...arrays: Uint8Array[]): Uint8Array {
14+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0)
15+
const result = new Uint8Array(totalLength)
16+
let offset = 0
17+
for (const arr of arrays) {
18+
result.set(arr, offset)
19+
offset += arr.length
20+
}
21+
return result
22+
}
23+
1024
/**
1125
* Encrypt data using AES-256-CTR.
1226
* Returns IV prepended to ciphertext.
1327
*/
1428
export function encryptWithAES(data: Uint8Array, cipherKey: Uint8Array): Uint8Array {
1529
const iv = randomBytes(INITIALIZATION_VECTOR_LENGTH) // always need a fresh IV when using CTR mode
1630
const cipher = createCipheriv('aes-256-ctr', cipherKey, iv)
17-
return Buffer.concat([iv, cipher.update(data), cipher.final()])
31+
return concatBytes(iv, cipher.update(data), cipher.final())
1832
}
1933

2034
/**
2135
* Decrypt AES-256-CTR encrypted data.
2236
* Expects IV prepended to ciphertext.
2337
*/
24-
export function decryptWithAES(cipher: Uint8Array, cipherKey: Uint8Array): Buffer {
38+
export function decryptWithAES(cipher: Uint8Array, cipherKey: Uint8Array): Uint8Array {
2539
const iv = cipher.slice(0, INITIALIZATION_VECTOR_LENGTH)
2640
const decipher = createDecipheriv('aes-256-ctr', cipherKey, iv)
27-
return Buffer.concat([decipher.update(cipher.slice(INITIALIZATION_VECTOR_LENGTH)), decipher.final()])
41+
return concatBytes(decipher.update(cipher.slice(INITIALIZATION_VECTOR_LENGTH)), decipher.final())
2842
}

packages/sdk/test/unit/EncryptionUtil.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('EncryptionUtil', () => {
1717
it('returns the initial plaintext after decrypting the ciphertext', async () => {
1818
const key = await RSAKeyPair.create(512)
1919
const ciphertext = await EncryptionUtil.encryptForPublicKey(plaintext, key.getPublicKey(), AsymmetricEncryptionType.RSA)
20-
expect(await EncryptionUtil.decryptWithPrivateKey(ciphertext, key.getPrivateKey(), AsymmetricEncryptionType.RSA)).toStrictEqual(plaintext)
20+
expect(await EncryptionUtil.decryptWithPrivateKey(ciphertext, key.getPrivateKey(), AsymmetricEncryptionType.RSA)).toEqualBinary(plaintext)
2121
})
2222

2323
it('produces different ciphertexts upon multiple encrypt() calls', async () => {
@@ -40,7 +40,7 @@ describe('EncryptionUtil', () => {
4040
const ciphertext = await EncryptionUtil.encryptForPublicKey(plaintext, key.getPublicKey(), AsymmetricEncryptionType.ML_KEM)
4141
expect(await EncryptionUtil.decryptWithPrivateKey(
4242
ciphertext, key.getPrivateKey(), AsymmetricEncryptionType.ML_KEM
43-
)).toStrictEqual(plaintext)
43+
)).toEqualBinary(plaintext)
4444
})
4545

4646
it('produces different ciphertexts upon multiple encrypt() calls', async () => {

packages/sdk/test/unit/aesUtils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('aesUtils', () => {
2020
it('returns the initial plaintext after decrypting the ciphertext', () => {
2121
const key = GroupKey.generate()
2222
const ciphertext = encryptWithAES(plaintext, key.data)
23-
expect(decryptWithAES(ciphertext, key.data)).toStrictEqual(plaintext)
23+
expect(decryptWithAES(ciphertext, key.data)).toEqualBinary(plaintext)
2424
})
2525

2626
it('preserves size (plaintext + iv)', () => {

0 commit comments

Comments
 (0)