{"record":{"id":"63dd6fb6a5e61599","repo":"stablyai/orca","slug":"invalid-public-key-expected-32-bytes-got-key-l","errorCode":null,"errorMessage":"Invalid public key: expected 32 bytes, got ${key.length} from \"${b64.slice(0, 20)}...\"","messagePattern":"Invalid public key: expected 32 bytes, got (.+?) from \"(.+?)\\.\\.\\.\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"mobile/src/transport/e2ee.ts","lineNumber":54,"sourceCode":"  for (let i = 0; i < bytes.length; i++) {\n    binary += String.fromCharCode(bytes[i]!)\n  }\n  return btoa(binary)\n}\n\nfunction base64ToUint8(b64: string): Uint8Array {\n  const binary = atob(b64)\n  const bytes = new Uint8Array(binary.length)\n  for (let i = 0; i < binary.length; i++) {\n    bytes[i] = binary.charCodeAt(i)\n  }\n  return bytes\n}\n\nexport function publicKeyFromBase64(b64: string): Uint8Array {\n  const key = base64ToUint8(b64)\n  if (key.length !== 32) {\n    throw new Error(\n      `Invalid public key: expected 32 bytes, got ${key.length} from \"${b64.slice(0, 20)}...\"`\n    )\n  }\n  return key\n}\n\nexport function publicKeyToBase64(key: Uint8Array): string {\n  return uint8ToBase64(key)\n}\n\nexport function encrypt(plaintext: string, sharedKey: Uint8Array): string {\n  const messageBytes = u8(new TextEncoder().encode(plaintext))\n  return uint8ToBase64(encryptBytes(messageBytes, sharedKey))\n}\n\nexport function decrypt(encrypted: string, sharedKey: Uint8Array): string | null {\n  const bundle = base64ToUint8(encrypted)\n  const plaintext = decryptBytes(bundle, sharedKey)","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/e2ee.ts#L36-L72","documentation":"publicKeyFromBase64 decoded a base64 string whose byte length is not exactly 32 — the Curve25519 public key size required by tweetnacl's box.keyPair / box.before. The function base64-decodes via atob, builds a Uint8Array, and checks length. A wrong length means the pinned desktop public key is corrupt, truncated, or encoded with the wrong scheme.","triggerScenarios":"The publicKeyB64 from pairing was truncated or padded wrong; a hex-encoded key was passed where base64 was expected; the key string has whitespace/newlines that atob mishandled; the stored host profile's publicKeyB64 was corrupted in AsyncStorage.","commonSituations":"Pairing QR/deeplink delivered a partial key; key copied with a stray newline or trailing '='; migration from a hex-key format; AsyncStorage corruption after an app crash mid-write; a relay key (16-byte) was passed instead of a Curve25519 key.","solutions":["Trim whitespace and strip any data: prefix from the base64 string before decoding.","Re-pair the host to get a fresh 32-byte (44 base64 chars) public key.","Validate the base64 length before calling: 44 chars without padding = 32 bytes.","If migrating from hex, convert hex→bytes first (64 hex chars = 32 bytes), then re-encode as base64."],"exampleFix":"// before\nexport function publicKeyFromBase64(b64: string): Uint8Array {\n  const key = base64ToUint8(b64)\n  if (key.length !== 32) {\n    throw new Error(`Invalid public key: expected 32 bytes, got ${key.length} ...`)\n  }\n  return key\n}\n\n// after — sanitize + validate before decode\nexport function publicKeyFromBase64(b64: string): Uint8Array {\n  const clean = b64.trim().replace(/^data:.*?;base64,/, '')\n  const key = base64ToUint8(clean)\n  if (key.length !== 32) {\n    throw new Error(`Invalid public key: expected 32 bytes, got ${key.length}`)\n  }\n  return key\n}","handlingStrategy":"validation","validationCode":"function isValidPublicKeyB64(b64: string): boolean {\n  try {\n    const clean = b64.trim().replace(/\\s/g, '')\n    const bytes = Uint8Array.from(atob(clean), (c) => c.charCodeAt(0))\n    return bytes.length === 32\n  } catch {\n    return false\n  }\n}\n\n// Use before calling connect()\nif (!isValidPublicKeyB64(serverPublicKeyB64)) {\n  throw new Error('Pairing key is corrupt — re-pair the host')\n}","typeGuard":"function isBase64Key32(b64: string): boolean {\n  try {\n    const bytes = Uint8Array.from(atob(b64.trim()), (c) => c.charCodeAt(0))\n    return bytes.length === 32\n  } catch {\n    return false\n  }\n}","tryCatchPattern":"try {\n  const client = connect(endpoint, deviceToken, serverPublicKeyB64, options)\n} catch (e) {\n  if (e.message.startsWith('Invalid public key')) {\n    triggerRePair()\n  }\n}","preventionTips":["Validate the base64 key length (44 chars no padding = 32 bytes) before pairing.","Trim whitespace and strip data: prefixes from the key string.","Re-pair if the stored publicKeyB64 in the host profile is corrupt.","Do not pass hex-encoded keys — convert to base64 first."],"tags":["e2ee","crypto","pairing","validation","mobile"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}