can1357/oh-my-pi · error · Error

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

Error message

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

What it means

importRoomKey validates that the raw room key material is exactly KEY_LENGTH bytes (AES-256 → 32 bytes) before importing it via WebCrypto. Any other length is rejected with an explicit message showing actual byte length, since importKey with a wrong-length key would fail opaquely.

Source

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

 * Sealed layout: `[12B IV][ciphertext+tag]`.
 */
import type { WireFrame } from "@oh-my-pi/pi-wire";

const AES_ALGORITHM = "AES-GCM";
const IV_LENGTH = 12;
const KEY_LENGTH = 32;
const TEXT_ENCODER = new TextEncoder();
const TEXT_DECODER = new TextDecoder();

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

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

View on GitHub (pinned to 9690622007)

Solutions

  1. Decode the full key segment of the link and confirm it yields exactly 32 bytes
  2. Split a composite secret (key ∥ writeToken) with subarray(0, 32) before importing
  3. Regenerate the invite link from the host with createRoomKey instead of hand-crafting key bytes
  4. Check the base64url decoding step for padding/alphabet mishandling

Example fix

// before
const key = await importRoomKey(decodedSecret); // may be key+writeToken
// after
const raw = decodedSecret.subarray(0, KEY_LENGTH);
if (raw.byteLength !== KEY_LENGTH) throw new Error("bad link");
const key = await importRoomKey(raw);
Defensive patterns

Strategy: validation

Validate before calling

import { KEY_LENGTH } from "./codec";
if (raw.byteLength !== KEY_LENGTH) {
  throw new Error(`bad room key: expected ${KEY_LENGTH} bytes, got ${raw.byteLength}`);
}

Type guard

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

Try / catch

try {
  const key = await importRoomKey(raw);
} catch (e) {
  if (e.message.includes("Room key must be")) {
    // request a fresh invite link from the host
  }
}

Prevention

When it happens

Trigger: Passing a Uint8Array whose byteLength !== KEY_LENGTH to importRoomKey — e.g. a key decoded from a truncated or hand-edited collab link, or a key concatenated with a write token without splitting first.

Common situations: Users mangling invite links (base64url segment cut off), decoding with a non-standard base64 decoder, or mistakenly passing key+writeToken composite secret into importRoomKey instead of the bare key.

Related errors


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