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
- Fetch the existing keys first (`e2e.getUserPublicAndPrivateKeys`) and reuse them instead of publishing new ones
- When regeneration is intended (e.g. after a password reset), pass `force: true`
- 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
- Always fetch existing keys before generating and publishing new ones
- Mark the publish step done so retries never re-send it
- Reserve force: true for deliberate regeneration (password reset flows)
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
- error-invalid-user
- error-room-e2e-key-already-exists
- error-invalid-user
- Failed to persist keys as they are not strings.
- error-not-allowed
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/ca22f9bd996e1974.
Report an issue: GitHub.