can1357/oh-my-pi · error · Error

Sealed frame too short

Error message

Sealed frame too short

What it means

open() decrypts a sealed collaboration frame, which is laid out as IV || ciphertext. A frame must be strictly longer than the IV (IV_LENGTH bytes) to contain any ciphertext; anything shorter is malformed (truncated in transit or never produced by seal). Throwing early avoids a confusing WebCrypto 'operation error' from a zero-length ciphertext.

Source

Thrown at packages/coding-agent/src/collab/crypto.ts:48

	}
	return crypto.subtle.importKey("raw", asStrict(raw), AES_ALGORITHM, false, ["encrypt", "decrypt"]);
}

export async function seal(key: CryptoKey, frame: CollabFrame): Promise<Uint8Array> {
	const iv = new Uint8Array(IV_LENGTH);
	crypto.getRandomValues(iv);
	const plaintext = TEXT_ENCODER.encode(JSON.stringify(frame));
	const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: AES_ALGORITHM, iv }, key, plaintext));
	const out = new Uint8Array(IV_LENGTH + ciphertext.byteLength);
	out.set(iv, 0);
	out.set(ciphertext, IV_LENGTH);
	return out;
}

/** Inverse of {@link seal}. Throws on auth failure or malformed input. */
export async function open(key: CryptoKey, data: Uint8Array): Promise<CollabFrame> {
	if (data.byteLength <= IV_LENGTH) {
		throw new Error("Sealed frame too short");
	}
	const iv = asStrict(data.subarray(0, IV_LENGTH));
	const ciphertext = asStrict(data.subarray(IV_LENGTH));
	const plaintext = new Uint8Array(await crypto.subtle.decrypt({ name: AES_ALGORITHM, iv }, key, ciphertext));
	return JSON.parse(TEXT_DECODER.decode(plaintext)) as CollabFrame;
}

function asStrict(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
	if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
		return bytes as Uint8Array<ArrayBuffer>;
	}
	const copy = new Uint8Array(bytes.byteLength);
	copy.set(bytes);
	return copy;
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify the websocket message is a binary frame from the peer before calling open; skip/handle text frames separately.
  2. Check data.byteLength > IV_LENGTH at the receive site and drop/ignore short frames as corrupt.
  3. Confirm both peers run compatible versions of the collab protocol and that no intermediary truncates binary messages.
  4. If frames are stored/relayed, ensure the full sealed buffer is transmitted (no slicing off the IV prefix).

Example fix

// before
const frame = await open(key, new Uint8Array(await msg.arrayBuffer()));
// after
const bytes = new Uint8Array(await msg.arrayBuffer());
if (bytes.byteLength <= IV_LENGTH) return; // ignore corrupt frame
const frame = await open(key, bytes);
Defensive patterns

Strategy: validation

Validate before calling

if (!(data instanceof Uint8Array) || data.byteLength <= IV_LENGTH) return; // skip corrupt/empty frame
const frame = await open(key, data);

Type guard

function isOpenableFrame(data: unknown): data is Uint8Array {
  return data instanceof Uint8Array && data.byteLength > IV_LENGTH;
}

Try / catch

try {
  const frame = await open(key, data);
} catch {
  // treat as corrupt frame: log and ignore; peers authenticate via GCM anyway
}

Prevention

When it happens

Trigger: Calling open(key, data) with data.byteLength <= IV_LENGTH: a relay delivers an empty or whitespace-only binary message, a peer's message was truncated by a proxy, or the caller passes raw JSON/plaintext text instead of the sealed binary frame.

Common situations: A flaky relay or load balancer drops the tail of a binary websocket message; a custom message handler feeds text frames (or JSON) into open() instead of binary frames; a version mismatch where one peer seals frames with a different layout; tests feed empty Uint8Array buffers.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/827c141266992f66. Report an issue: GitHub.