{"record":{"id":"648cfff8b6951be5","repo":"RocketChat/Rocket.Chat","slug":"failed-to-persist-keys-as-they-are-not-strings","errorCode":null,"errorMessage":"Failed to persist keys as they are not strings.","messagePattern":"Failed to persist keys as they are not strings\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"apps/meteor/client/lib/e2ee/rocketchat.e2e.ts","lineNumber":285,"sourceCode":"\t\t) {\n\t\t\t// KeyID was changed, update instance with new keyID and put room in waiting keys status\n\t\t\tthis.instancesByRoomId[rid].onRoomKeyReset(room.e2eKeyId);\n\t\t}\n\n\t\treturn this.instancesByRoomId[rid] ?? null;\n\t}\n\n\tremoveInstanceByRoomId(rid: IRoom['_id']): void {\n\t\tdelete this.instancesByRoomId[rid];\n\t}\n\n\tprivate async persistKeys(\n\t\t{ public_key, private_key }: KeyPair,\n\t\tpassword: string,\n\t\t{ force }: { force: boolean } = { force: false },\n\t): Promise<void> {\n\t\tif (typeof public_key !== 'string' || typeof private_key !== 'string') {\n\t\t\tthrow new Error('Failed to persist keys as they are not strings.');\n\t\t}\n\n\t\tconst encodedPrivateKey = await this.keychain.encryptKey(private_key, password);\n\n\t\tif (!encodedPrivateKey) {\n\t\t\tthrow new Error('Failed to encode private key with provided password.');\n\t\t}\n\n\t\tawait sdk.rest.post('/v1/e2e.setUserPublicAndPrivateKeys', {\n\t\t\tpublic_key,\n\t\t\tprivate_key: JSON.stringify(encodedPrivateKey),\n\t\t\tforce,\n\t\t});\n\t}\n\n\tasync acceptSuggestedKey(rid: string): Promise<void> {\n\t\tawait sdk.rest.post('/v1/e2e.acceptSuggestedGroupKey', {\n\t\t\trid,","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/client/lib/e2ee/rocketchat.e2e.ts#L267-L303","documentation":"E2EE key persistence (persistKeys) throws this error when the KeyPair passed in has a public_key or private_key that is not a string (typeof check fails) — i.e. the key material is undefined, an object, or otherwise malformed before it is ever encrypted and POSTed to /v1/e2e.setUserPublicAndPrivateKeys. It is an early integrity guard for the E2EE setup flow.","triggerScenarios":"Passing a KeyPair whose keys came back malformed from generation or decryption (a failed WebCrypto export returning undefined, a decode step that produced an object instead of a string), or custom code constructing KeyPair from raw crypto primitives without serializing them.","commonSituations":"Corrupted locally-stored E2EE key material being re-persisted; password reset / key recovery flows passing the wrong shape; refactors of the KeyPair type; browser WebCrypto unavailability (non-secure context) making key export return nothing.","solutions":["Log/inspect the KeyPair right before persistKeys to see which field is not a string.","Regenerate the key pair (the E2EE setup flow) instead of persisting the malformed one.","If it originated from stored key recovery, reset E2EE state and re-create keys with force: true.","Ensure the page runs in a secure context so key export succeeds."],"exampleFix":"// before\nawait e2e.persistKeys(keyPair, password, { force });\n\n// after\nif (typeof keyPair.public_key !== 'string' || typeof keyPair.private_key !== 'string') {\n  keyPair = await e2e.generateKeys(); // regenerate the corrupted pair\n}\nawait e2e.persistKeys(keyPair, password, { force });","handlingStrategy":"type-guard","validationCode":"if (!isStringKeyPair(keyPair)) {\n  keyPair = await e2e.generateKeys(); // do not persist malformed material\n}\nawait e2e.persistKeys(keyPair, password, { force });","typeGuard":"const isStringKeyPair = (kp: KeyPair): kp is KeyPair & { public_key: string; private_key: string } =>\n  typeof kp.public_key === 'string' &&\n  typeof kp.private_key === 'string' &&\n  kp.public_key.length > 0 &&\n  kp.private_key.length > 0;","tryCatchPattern":"try {\n  await e2e.persistKeys(keyPair, password);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Failed to persist keys')) {\n    // regenerate and retry once with force\n    await e2e.persistKeys(await e2e.generateKeys(), password, { force: true });\n    return;\n  }\n  throw e;\n}","preventionTips":["Never construct KeyPair manually — always use the E2EE module's generation/decryption paths.","Run in a secure context (https/localhost) so WebCrypto exports return strings.","Treat non-string key material as corruption: regenerate rather than coerce."],"tags":["e2ee","encryption","key-management"],"backgroundTag":"e2ee-key-persistence","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}