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
- Only pass frames produced by seal() through open(); check the sender path
- Log data.byteLength on failure to confirm whether the transport truncated the message
- Handle empty socket messages before calling open()
- 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
- Only feed open() output of seal() from the same codec version
- Drop empty socket messages before decryption
- Log byteLength on decrypt failures to detect transport truncation
- Keep IV_LENGTH/AES_ALGORITHM identical on all peers (same package version)
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
- Room key must be ${ROOM_KEY_BYTES} bytes, got ${raw.byteLeng
- Room key must be ${KEY_LENGTH} bytes, got ${raw.byteLength}
- Sealed frame too short
- error
- Collab replica activation was cancelled
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/e21066118fdfd29e.
Report an issue: GitHub.