Repository navigation
Web chunk(): copy the caller's bytes once, straight into the frame #76
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 |
|---|---|---|
|
|
@@ -280,6 +280,11 @@ const signWith = async (key, text) => toHex(await subtle.sign('HMAC', key, utf8. | |
| */ | ||
| const copyOf = (data) => Uint8Array.prototype.slice.call(data); | ||
|
|
||
| /** @param {unknown} data */ | ||
| const isBytes = (data) => | ||
| ArrayBuffer.isView(data) && /** @type {any} */ (data).BYTES_PER_ELEMENT === 1; // a Uint8Array of any realm or a Node Buffer; not a DataView or a wider array | ||
| const blankSignature = '0'.repeat(64); // a signature is 64 hex digits | ||
|
|
||
| /** | ||
| * Runs the signing steps of a stream one after the other. A step that throws rejects its own call only: the next one still runs, as on the | ||
| * synchronous entries, where a refused call throws and the signer stays usable. A step moves the chain only once it has succeeded. | ||
|
|
@@ -326,8 +331,17 @@ export function chunkSigner(authHeaders, credentials) { | |
| if (data.length === 0) { | ||
| return data; | ||
| } | ||
| const own = copyOf(data); // the signature and the frame are made when the chunk's turn comes; the caller may reuse its buffer once this returns | ||
| return frameChunk(await sign(own), own); | ||
| // The one copy of the bytes is the frame itself, made now, so the caller may reuse its buffer once this returns. The data is hashed where it sits in the | ||
| // frame and the signature is written into the blank the frame has for it, when the chunk's turn comes. | ||
| if (!isBytes(data)) { | ||
| throw new TypeError('data must be a Uint8Array'); | ||
| } | ||
| const frame = frameChunk(blankSignature, data); | ||
| const body = frame.length - 2 - data.length; // the data ends the frame, followed by CR LF; the signature ends the line before it | ||
|
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. medium: Use one fixed length when framing a growable view ·
Failure: If another worker grows a SharedArrayBuffer while a length-tracking Uint8Array over it is being framed, those length reads can differ. The signature can then overwrite part of Suggested fix: Capture the length once, create a fixed-length view for framing, and use that same length to calculate the body and signature offsets. |
||
| const at = body - 2 - blankSignature.length; | ||
| const signature = await sign(frame.subarray(body, frame.length - 2)); | ||
| utf8.encodeInto(signature, frame.subarray(at, at + signature.length)); | ||
|
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. medium: Avoid requiring encodeInto after advancing the chain ·
Failure: In a WebCrypto environment that provides Suggested fix: Write the ASCII signature directly or use the already-required |
||
| return frame; | ||
| }, | ||
| async end(trailers) { | ||
| const text = trailers === undefined ? undefined : canonicalTrailers(trailers); // before sign(), so a refused trailer leaves the chain where it was | ||
|
|
||
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.
medium: Validate the view's actual element type ·
ref 850dd2The check accepts any
ArrayBuffer.isView(data)whose ordinarydata.BYTES_PER_ELEMENTproperty equals 1.Failure: A DataView can have its own
BYTES_PER_ELEMENT: 1andlength: 3properties. Passing such a view over bytes[1, 2, 3]succeeds and produces a correctly signed frame containing[0, 0, 0], becauseframeChunktreats the DataView as an array-like object with no indexed elements. The previous copy operation rejected it, and the signing chain now advances.Suggested fix: Use an intrinsic typed-array brand check that accepts the one-byte typed-array kinds and rejects DataView and wider views, without trusting an overridable property.