can1357/oh-my-pi · error · Error

Room key must be ${ROOM_KEY_BYTES} bytes, got ${raw.byteLeng

Error message

Room key must be ${ROOM_KEY_BYTES} bytes, got ${raw.byteLength}

What it means

importRoomKey wraps a raw symmetric AES-GCM key for the collaboration room. The library enforces that the raw key bytes are exactly ROOM_KEY_BYTES (32 bytes); anything else is rejected before reaching WebCrypto so a mistyped or truncated secret fails loudly instead of producing a key that cannot decrypt host frames. This guards the room-key wire format produced by parseCollabLink and generateRoomKey.

Source

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

const IV_LENGTH = 12;
const TEXT_ENCODER = new TextEncoder();
const TEXT_DECODER = new TextDecoder();

export function generateRoomKey(): Uint8Array {
	const key = new Uint8Array(ROOM_KEY_BYTES);
	crypto.getRandomValues(key);
	return key;
}

export function generateWriteToken(): Uint8Array {
	const token = new Uint8Array(WRITE_TOKEN_BYTES);
	crypto.getRandomValues(token);
	return token;
}

export function importRoomKey(raw: Uint8Array): Promise<CryptoKey> {
	if (raw.byteLength !== ROOM_KEY_BYTES) {
		throw new Error(`Room key must be ${ROOM_KEY_BYTES} bytes, got ${raw.byteLength}`);
	}
	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) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Decode the key strictly from the link's base64url key fragment via parseCollabLink, which validates the 32-byte (or 48-byte with token) length before you call importRoomKey.
  2. Check raw.byteLength === ROOM_KEY_BYTES before calling and regenerate/re-obtain the key if it differs.
  3. If the key came from hex or a password, derive/re-encode it to exactly 32 bytes (e.g. Buffer.from(hex, 'hex') must yield 32 bytes) before importing.
  4. For custom keys, use crypto.getRandomValues(new Uint8Array(ROOM_KEY_BYTES)) as generateRoomKey does.

Example fix

// before
const key = await importRoomKey(Buffer.from(secretHex, "hex")); // 16 or 64 bytes for many hex inputs
// after
const raw = Buffer.from(secretB64url, "base64url");
if (raw.byteLength !== ROOM_KEY_BYTES) throw new Error("bad room key length");
const key = await importRoomKey(new Uint8Array(raw));
Defensive patterns

Strategy: validation

Validate before calling

if (raw.byteLength !== ROOM_KEY_BYTES) throw new Error(`expected ${ROOM_KEY_BYTES}-byte room key, got ${raw.byteLength}`);
const key = await importRoomKey(raw);

Type guard

function isValidRoomKey(raw: Uint8Array): boolean {
  return raw instanceof Uint8Array && raw.byteLength === ROOM_KEY_BYTES;
}

Prevention

When it happens

Trigger: Calling importRoomKey(raw) with a Uint8Array whose byteLength !== ROOM_KEY_BYTES: a key decoded from a truncated/corrupted collab link, a hand-rolled hex string decoded to 16/64 bytes, a key padded or stripped by base64 vs base64url confusion, or a key sliced to the wrong subarray bounds.

Common situations: A guest pastes a collab link that was mangled by a terminal or chat client (truncated base64url); a developer constructs the key from a hex-encoded secret instead of the base64url fragment; an older client produced 16-byte keys before the format moved to 32; tests hardcode short dummy keys.

Related errors


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