{"record":{"id":"c12e3ae6ef33ce7d","repo":"can1357/oh-my-pi","slug":"room-key-must-be-key-length-bytes-got-raw-by","errorCode":null,"errorMessage":"Room key must be ${KEY_LENGTH} bytes, got ${raw.byteLength}","messagePattern":"Room key must be (.+?) bytes, got (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/collab-web/src/lib/codec.ts","lineNumber":24,"sourceCode":" * Sealed layout: `[12B IV][ciphertext+tag]`.\n */\nimport type { WireFrame } from \"@oh-my-pi/pi-wire\";\n\nconst AES_ALGORITHM = \"AES-GCM\";\nconst IV_LENGTH = 12;\nconst KEY_LENGTH = 32;\nconst TEXT_ENCODER = new TextEncoder();\nconst TEXT_DECODER = new TextDecoder();\n\nexport function generateRoomKey(): Uint8Array {\n\tconst key = new Uint8Array(KEY_LENGTH);\n\tcrypto.getRandomValues(key);\n\treturn key;\n}\n\nexport function importRoomKey(raw: Uint8Array): Promise<CryptoKey> {\n\tif (raw.byteLength !== KEY_LENGTH) {\n\t\tthrow new Error(`Room key must be ${KEY_LENGTH} bytes, got ${raw.byteLength}`);\n\t}\n\treturn crypto.subtle.importKey(\"raw\", asStrict(raw), AES_ALGORITHM, false, [\"encrypt\", \"decrypt\"]);\n}\n\nexport async function seal(key: CryptoKey, frame: WireFrame): Promise<Uint8Array> {\n\tconst iv = new Uint8Array(IV_LENGTH);\n\tcrypto.getRandomValues(iv);\n\tconst plaintext = TEXT_ENCODER.encode(JSON.stringify(frame));\n\tconst ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: AES_ALGORITHM, iv }, key, plaintext));\n\tconst out = new Uint8Array(IV_LENGTH + ciphertext.byteLength);\n\tout.set(iv, 0);\n\tout.set(ciphertext, IV_LENGTH);\n\treturn out;\n}\n\n/** Inverse of {@link seal}. Throws on auth failure or malformed input. */\nexport async function open(key: CryptoKey, data: Uint8Array): Promise<WireFrame> {\n\tif (data.byteLength <= IV_LENGTH) {","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/collab-web/src/lib/codec.ts#L6-L42","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst key = await importRoomKey(decodedSecret); // may be key+writeToken\n// after\nconst raw = decodedSecret.subarray(0, KEY_LENGTH);\nif (raw.byteLength !== KEY_LENGTH) throw new Error(\"bad link\");\nconst key = await importRoomKey(raw);","handlingStrategy":"validation","validationCode":"import { KEY_LENGTH } from \"./codec\";\nif (raw.byteLength !== KEY_LENGTH) {\n  throw new Error(`bad room key: expected ${KEY_LENGTH} bytes, got ${raw.byteLength}`);\n}","typeGuard":"function isRoomKeyMaterial(raw: Uint8Array): boolean {\n  return raw instanceof Uint8Array && raw.byteLength === KEY_LENGTH;\n}","tryCatchPattern":"try {\n  const key = await importRoomKey(raw);\n} catch (e) {\n  if (e.message.includes(\"Room key must be\")) {\n    // request a fresh invite link from the host\n  }\n}","preventionTips":["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"],"tags":["crypto","validation","collaboration","webrtc"],"backgroundTag":"invalid-room-key-length","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}