RocketChat/Rocket.Chat · error · Meteor.Error

error-keys-already-set

error-keys-already-set

Error message

Keys already set

What it means

`e2e.setUserPublicAndPrivateKeys` throws `error-keys-already-set` when the user already has both a public and a private key stored (`Users.fetchKeysByUserId` returns both) and the request does not carry `force: true`. The guard prevents silently overwriting an existing E2E identity; regeneration must be explicit. Deprecated since 9.0.0 in favor of `/v1/e2e.setUserPublicAndPrivateKeys`.

Source

Thrown at apps/meteor/server/meteor-methods/platform/setUserPublicAndPrivateKeys.ts:27

	// eslint-disable-next-line @typescript-eslint/naming-convention
	interface ServerMethods {
		'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string; force?: boolean }): void;
	}
}

const isKeysResult = (result: any): result is { public_key: string; private_key: string } => {
	return result.private_key && result.public_key;
};

export const setUserPublicAndPrivateKeysMethod = async (
	userId: string,
	keyPair: { public_key: string; private_key: string; force?: boolean },
): Promise<void> => {
	if (!keyPair.force) {
		const keys = await Users.fetchKeysByUserId(userId);

		if (isKeysResult(keys)) {
			throw new Meteor.Error('error-keys-already-set', 'Keys already set', {
				method: 'e2e.setUserPublicAndPrivateKeys',
			});
		}
	}

	await Users.setE2EPublicAndPrivateKeysByUserId(userId, {
		private_key: keyPair.private_key,
		public_key: keyPair.public_key,
	});

	const subscribedRoomIds = await Rooms.getSubscribedRoomIdsWithoutE2EKeys(userId);
	await Rooms.addUserIdToE2EEQueueByRoomIds(subscribedRoomIds, userId);

	void notifyOnRoomChangedById(subscribedRoomIds);
};

Meteor.methods<ServerMethods>({
	async 'e2e.setUserPublicAndPrivateKeys'(keyPair) {

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Fetch the existing keys first (`e2e.getUserPublicAndPrivateKeys`) and reuse them instead of publishing new ones
  2. When regeneration is intended (e.g. after a password reset), pass `force: true`
  3. Make the client publish step idempotent — record completion and never re-run it

Example fix

// before
Meteor.call('e2e.setUserPublicAndPrivateKeys', { public_key, private_key });
// after
Meteor.call('e2e.setUserPublicAndPrivateKeys', {
  public_key,
  private_key,
  force: isIntentionalRegeneration,
});
Defensive patterns

Strategy: validation

Validate before calling

Meteor.call('e2e.getUserPublicAndPrivateKeys', (err, keys) => {
  if (keys?.public_key && keys?.private_key) {
    return useExistingKeys(keys); // reuse — do not publish
  }
  Meteor.call('e2e.setUserPublicAndPrivateKeys', { public_key, private_key });
});

Type guard

const hasKeyPair = (keys: unknown): keys is { public_key: string; private_key: string } =>
  !!keys && typeof keys === 'object' &&
  !!(keys as any).public_key && !!(keys as any).private_key;

Try / catch

try {
  await Meteor.callAsync('e2e.setUserPublicAndPrivateKeys', { public_key, private_key, force });
} catch (e) {
  if (e instanceof Meteor.Error && e.error === 'error-keys-already-set') {
    // fetch existing keys and reuse, or resend with force: true if regenerating
  }
}

Prevention

When it happens

Trigger: Publishing a key pair without `force` for a user whose keys already exist — a retried setup step, a second device generating fresh keys instead of fetching the existing pair, or re-running the post-login key setup.

Common situations: Retry after timeout where the first publish actually committed; new-device login re-running key generation; post-password-reset flows forgetting the force flag.

Related errors


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