diff --git a/modules/sdk-coin-ada/src/lib/messages/cip8/utils.ts b/modules/sdk-coin-ada/src/lib/messages/cip8/utils.ts index 04353738e12..919eed3dae6 100644 --- a/modules/sdk-coin-ada/src/lib/messages/cip8/utils.ts +++ b/modules/sdk-coin-ada/src/lib/messages/cip8/utils.ts @@ -7,6 +7,27 @@ export function bytesToHex(bytes: Uint8Array | Buffer): string { return Buffer.from(bytes).toString('hex'); } +/** + * Helper function to encode a value to CBOR bytes. + * + * Do not use `cbor`'s synchronous statics (`Encoder.encode`/`encodeOne`/`encodeCanonical`) here: + * they internally return a single `read()` chunk, which truncates the encoding on Node.js 26 + * (the resulting bytes then fail to decode with "unexpected end of input"). + * + * Keep the `data` listener: it puts the stream in flowing mode, so the encoder's chunks are + * delivered (and released) synchronously as it writes. Draining buffered output with `read()` + * instead does not work — once the readable buffer passes its high-water mark `push()` returns + * false and the encoder's chunked `push()` chains short-circuit, truncating large values. + */ +function encodeCbor(value: unknown): Buffer { + const encoder = new Encoder(); + const chunks: Buffer[] = []; + encoder.on('data', (chunk: Buffer) => chunks.push(chunk)); + encoder.write(value); + encoder.end(); + return Buffer.concat(chunks); +} + export interface CSLSigStructureOutput { sigStructureCborBytes: Uint8Array; protectedHeaderCborBytes: Uint8Array; @@ -33,7 +54,7 @@ export function createCSLSigStructure(addressCborBytes: Uint8Array, message: str const protectedHeaderMap = new Map(); protectedHeaderMap.set(1, -8); // Algorithm ID: EdDSA protectedHeaderMap.set('address', Buffer.from(addressCborBytes)); - const protectedHeaderCborBytes = Encoder.encode(protectedHeaderMap); + const protectedHeaderCborBytes = encodeCbor(protectedHeaderMap); // Sig_structure const sigStructureArray: any[] = [ @@ -42,7 +63,7 @@ export function createCSLSigStructure(addressCborBytes: Uint8Array, message: str Buffer.from([]), // Empty external_aad Buffer.from(payloadBytes), ]; - const sigStructureCborBytes = Encoder.encode(sigStructureArray); + const sigStructureCborBytes = encodeCbor(sigStructureArray); return { sigStructureCborBytes, protectedHeaderCborBytes, payloadBytes }; } @@ -63,9 +84,9 @@ export function constructCSLCoseObjects( Buffer.from(payloadBytes), Buffer.from(cslSignatureBytes), ]; - const finalCoseSign1CborBytes = Encoder.encode(coseSign1Array); + const finalCoseSign1CborBytes = encodeCbor(coseSign1Array); /* // directly encoding the coseSign1Array without prepending the 0xD2 tag. - * const coseSign1PayloadBytes = Encoder.encode(coseSign1Array); + * const coseSign1PayloadBytes = encodeCbor(coseSign1Array); * const coseSign1Tag = Buffer.from([0xD2]); // Tag 18 for COSE_Sign1 * const finalCoseSign1CborBytes = Buffer.concat([coseSign1Tag, coseSign1PayloadBytes]); */ @@ -77,14 +98,14 @@ export function constructCSLCoseObjects( coseKeyMap.set(3, -8); // alg: EdDSA coseKeyMap.set(-1, 6); // crv: Ed25519 coseKeyMap.set(-2, Buffer.from(paymentPubKey.as_bytes())); // x: public_key_bytes (Ed25519 public key) - const finalCoseKeyCborBytes = Encoder.encode(coseKeyMap); + const finalCoseKeyCborBytes = encodeCbor(coseKeyMap); const manualCoseKeyHex = bytesToHex(finalCoseKeyCborBytes); return { manualCoseSign1Hex, manualCoseKeyHex }; } export function coseObjectsOutputToBuffer(output: CSLCoseObjectsOutput): Buffer { - return Buffer.from(Encoder.encode(output)); + return encodeCbor(output); } export async function bufferToCoseObjectsOutput(buffer: Buffer): Promise { diff --git a/modules/sdk-coin-ada/test/unit/messages/cip8/utils.ts b/modules/sdk-coin-ada/test/unit/messages/cip8/utils.ts index 7a1c24982e7..a442f33ff16 100644 --- a/modules/sdk-coin-ada/test/unit/messages/cip8/utils.ts +++ b/modules/sdk-coin-ada/test/unit/messages/cip8/utils.ts @@ -113,5 +113,25 @@ describe('CIP8 Utils', function () { should.equal(reconstructedObjects.manualCoseSign1Hex, originalObjects.manualCoseSign1Hex); should.equal(reconstructedObjects.manualCoseKeyHex, originalObjects.manualCoseKeyHex); }); + + it('should round-trip a payload larger than the cbor encoder chunk size', async function () { + // A truncated encoding (only the first chunk) or a payload dropped by the encoder would + // either fail to decode or come back different, so the round-trip pins the encoding down. + const message = 'a'.repeat(64 * 1024); + const { protectedHeaderCborBytes, payloadBytes } = createCSLSigStructure(addressCborBytes, message); + + const originalObjects = constructCSLCoseObjects( + protectedHeaderCborBytes, + payloadBytes, + cip8TestResources.createTestSignature(message), + cip8TestResources.createTestPublicKey() + ); + + const buffer = coseObjectsOutputToBuffer(originalObjects); + const reconstructedObjects = await bufferToCoseObjectsOutput(buffer); + + should.equal(reconstructedObjects.manualCoseSign1Hex, originalObjects.manualCoseSign1Hex); + should.equal(reconstructedObjects.manualCoseKeyHex, originalObjects.manualCoseKeyHex); + }); }); });