{"record":{"id":"2fe958a681782313","repo":"schollz/croc","slug":"invalid-stored-transfer-key","errorCode":null,"errorMessage":"Invalid stored-transfer key","messagePattern":"Invalid stored-transfer key","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/protocol/stored.ts","lineNumber":152,"sourceCode":"  if (\n    (parsed.protocol !== \"https:\" && !(parsed.protocol === \"http:\" && loopback)) ||\n    parsed.username ||\n    parsed.password ||\n    (parsed.pathname !== \"/\" && parsed.pathname !== \"\") ||\n    parsed.search ||\n    parsed.hash\n  ) {\n    throw new Error(\"Stored-transfer origin must contain only an HTTPS scheme and host\");\n  }\n  return parsed.origin;\n}\n\nfunction validateShare(share: StoredShare) {\n  if (!/^[A-Za-z0-9_-]{22}$/.test(share.id)) {\n    throw new Error(\"Invalid stored-transfer id\");\n  }\n  if (share.key.byteLength !== storedKeyBytes) {\n    throw new Error(\"Invalid stored-transfer key\");\n  }\n  share.origin = normalizeOrigin(share.origin);\n  return share;\n}\n\nexport function formatStoredBrowserURL(share: StoredShare) {\n  validateShare(share);\n  return `${share.origin}/s/${share.id}#v1.${base64URL(share.key)}`;\n}\n\nexport function formatStoredCLIToken(share: StoredShare) {\n  validateShare(share);\n  return [\n    storedProtocol,\n    base64URL(textEncoder.encode(share.origin)),\n    share.id,\n    base64URL(share.key),\n  ].join(\".\");","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/stored.ts#L134-L170","documentation":"validateShare() requires the share key to be exactly 32 bytes (storedKeyBytes). The key comes from base64url-decoding the URL fragment (#v1....) or the token's last segment; a decode that yields any other length — from truncation, padding characters, or unicode mangling — is rejected before any decryption is attempted with it.","triggerScenarios":"parseStoredShare where fromBase64URL(parsed.hash.slice(4)) or parts[3] decodes to != 32 bytes; also createStoredUpload if wasm().storeGenerateKey() ever returned a different length (it does not in a consistent build).","commonSituations":"Share links mangled by messengers that strip or truncate the fragment; users copying only part of the URL and dropping the #v1. key fragment (usually caught as invalid URL first); mixing client versions where the key format changed.","solutions":["Copy the entire share URL including the #v1.<key> fragment; many chat clients require explicit 'copy link'","If generating URLs yourself, base64URL-encode exactly the 32-byte key with no padding","Pre-validate with a regex on the fragment length (43 unpadded base64url chars encode 32 bytes) before calling parseStoredShare"],"exampleFix":"// validation before parse\nconst keyPart = url.hash.slice(4);\nif (!/^[A-Za-z0-9_-]{43}$/.test(keyPart)) throw new Error(\"Share link is missing its key fragment\");","handlingStrategy":"validation","validationCode":"const isValidShareKey = (key: Uint8Array): boolean => key.byteLength === 32;\n// for the URL fragment: /^[A-Za-z0-9_-]{43}$/.test(hash.slice(4))","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Check the #v1. fragment decodes to 43 unpadded base64url chars before parsing","Copy share links whole; fragments are easy to lose in chat apps"],"tags":["validation","key","crypto","token"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}