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
- Decode the full key segment of the link and confirm it yields exactly 32 bytes
- Split a composite secret (key ∥ writeToken) with subarray(0, 32) before importing
- Regenerate the invite link from the host with createRoomKey instead of hand-crafting key bytes
- 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
- Split composite secrets (key ∥ writeToken) with subarray before importing
- Decode keys only with a base64url decoder matching the encoder
- Never hand-edit link key segments
- Regenerate keys via createRoomKey rather than reusing old material
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
- Room key must be ${ROOM_KEY_BYTES} bytes, got ${raw.byteLeng
- ${parsed.error}
- Sealed frame too short
- ${normalized.error}
- Unsupported language '{value}'. Supported: {}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/c12e3ae6ef33ce7d.
Report an issue: GitHub.