can1357/oh-my-pi · error · Error

Sealed frame too short

Error message

Sealed frame too short

What it means

open() decrypts a sealed frame produced by seal(), which prefixes an IV before the ciphertext. If the input is not longer than IV_LENGTH there is no room for any ciphertext, so the frame is malformed and decryption cannot proceed; the function throws before touching WebCrypto.

Source

Thrown at packages/collab-web/src/lib/codec.ts:43

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

export async function seal(key: CryptoKey, frame: WireFrame): 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<WireFrame> {
	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 WireFrame;
}

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. Only pass frames produced by seal() through open(); check the sender path
  2. Log data.byteLength on failure to confirm whether the transport truncated the message
  3. Handle empty socket messages before calling open()
  4. Ensure both peers use the same codec version (IV_LENGTH, AES_ALGORITHM)

Example fix

// before
const frame = await open(key, data); // throws on short buffer
// after
if (data.byteLength > IV_LENGTH) {
  const frame = await open(key, data);
} else {
  ignoreMalformedFrame(data);
}
Defensive patterns

Strategy: validation

Validate before calling

import { IV_LENGTH } from "./codec";
if (data.byteLength <= IV_LENGTH) {
  return; // ignore malformed/empty frame
}

Type guard

function isSealedFrame(data: Uint8Array): boolean {
  return data instanceof Uint8Array && data.byteLength > IV_LENGTH;
}

Try / catch

try {
  const frame = await open(key, data);
  handleFrame(frame);
} catch (e) {
  logger.warn("dropping undecryptable frame", { byteLength: data.byteLength });
}

Prevention

When it happens

Trigger: Calling open() with a zero-length or IV-only buffer: receiving an empty WebSocket message, feeding open() an already-decrypted frame, or corrupted transport data truncated to the IV length.

Common situations: Relay or socket delivering empty/short frames on reconnect, a peer sending plaintext that is passed to open() by mistake, or version drift where a peer omits the IV prefix.

Related errors


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