RocketChat/Rocket.Chat · critical · Error

Failed to persist keys as they are not strings.

Error message

Failed to persist keys as they are not strings.

What it means

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.

Source

Thrown at apps/meteor/client/lib/e2ee/rocketchat.e2e.ts:285

		) {
			// KeyID was changed, update instance with new keyID and put room in waiting keys status
			this.instancesByRoomId[rid].onRoomKeyReset(room.e2eKeyId);
		}

		return this.instancesByRoomId[rid] ?? null;
	}

	removeInstanceByRoomId(rid: IRoom['_id']): void {
		delete this.instancesByRoomId[rid];
	}

	private async persistKeys(
		{ public_key, private_key }: KeyPair,
		password: string,
		{ force }: { force: boolean } = { force: false },
	): Promise<void> {
		if (typeof public_key !== 'string' || typeof private_key !== 'string') {
			throw new Error('Failed to persist keys as they are not strings.');
		}

		const encodedPrivateKey = await this.keychain.encryptKey(private_key, password);

		if (!encodedPrivateKey) {
			throw new Error('Failed to encode private key with provided password.');
		}

		await sdk.rest.post('/v1/e2e.setUserPublicAndPrivateKeys', {
			public_key,
			private_key: JSON.stringify(encodedPrivateKey),
			force,
		});
	}

	async acceptSuggestedKey(rid: string): Promise<void> {
		await sdk.rest.post('/v1/e2e.acceptSuggestedGroupKey', {
			rid,

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Log/inspect the KeyPair right before persistKeys to see which field is not a string.
  2. Regenerate the key pair (the E2EE setup flow) instead of persisting the malformed one.
  3. If it originated from stored key recovery, reset E2EE state and re-create keys with force: true.
  4. Ensure the page runs in a secure context so key export succeeds.

Example fix

// before
await e2e.persistKeys(keyPair, password, { force });

// after
if (typeof keyPair.public_key !== 'string' || typeof keyPair.private_key !== 'string') {
  keyPair = await e2e.generateKeys(); // regenerate the corrupted pair
}
await e2e.persistKeys(keyPair, password, { force });
Defensive patterns

Strategy: type-guard

Validate before calling

if (!isStringKeyPair(keyPair)) {
  keyPair = await e2e.generateKeys(); // do not persist malformed material
}
await e2e.persistKeys(keyPair, password, { force });

Type guard

const isStringKeyPair = (kp: KeyPair): kp is KeyPair & { public_key: string; private_key: string } =>
  typeof kp.public_key === 'string' &&
  typeof kp.private_key === 'string' &&
  kp.public_key.length > 0 &&
  kp.private_key.length > 0;

Try / catch

try {
  await e2e.persistKeys(keyPair, password);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Failed to persist keys')) {
    // regenerate and retry once with force
    await e2e.persistKeys(await e2e.generateKeys(), password, { force: true });
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/648cfff8b6951be5. Report an issue: GitHub.