bitwarden/server · error · BadRequestException

All existing emergency access keys must be included in the r

Error message

All existing emergency access keys must be included in the rotation.

What it means

Thrown by EmergencyAccessRotationValidator during key rotation. It loads every emergency-access grant the user has made (as grantor) that is already confirmed (KeyEncrypted != null), and requires the rotation request to include each one matched by Id. Omitting a confirmed emergency-access grant would leave it encrypted under the dead key, so the entire rotation is rejected.

Source

Thrown at src/Api/KeyManagement/Validators/EmergencyAccessRotationValidator.cs:41

    public async Task<IEnumerable<EmergencyAccess>> ValidateAsync(User user,
        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)
    {
        var result = new List<EmergencyAccess>();

        var existing = await _emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(user.Id);
        if (existing == null || existing.Count == 0)
        {
            return result;
        }
        // Exclude any emergency access that has not been confirmed yet.
        existing = existing.Where(ea => ea.KeyEncrypted != null).ToList();

        foreach (var ea in existing)
        {
            var emergencyAccess = emergencyAccessKeys.FirstOrDefault(c => c.Id == ea.Id);
            if (emergencyAccess == null)
            {
                throw new BadRequestException("All existing emergency access keys must be included in the rotation.");
            }

            if (emergencyAccess.KeyEncrypted == null)
            {
                throw new BadRequestException("Emergency access keys cannot be set to null during rotation.");
            }

            result.Add(emergencyAccess.ToEmergencyAccess(ea));
        }

        return result;
    }
}

View on GitHub (pinned to e93b962371)

Solutions

  1. Fetch all confirmed emergency-access grants for the user immediately before constructing the rotation payload.
  2. Include every grant with KeyEncrypted set, keyed by its exact Id.
  3. Revoke any grant you do not want to rotate before starting the key rotation.
  4. Verify the submitted Id set is a superset of the server's confirmed-grant set before sending.

Example fix

// before
const ea = locallyKnownGrants.map(g => ({ id: g.id, keyEncrypted: reencrypt(g.key) }));

// after
const all = await api.getEmergencyAccessGrants();
const ea = all.filter(g => g.keyEncrypted).map(g => ({ id: g.id, keyEncrypted: reencrypt(g.key) }));
Defensive patterns

Strategy: validation

Validate before calling

const confirmed = (await api.getEmergencyAccessGrants()).filter(g => g.keyEncrypted);
const submitted = new Set(payload.emergencyAccessKeys.map(g => g.id));
const missing = confirmed.filter(g => !submitted.has(g.id));
if (missing.length) {
  throw new Error(`Rotation is missing emergency access grants: ${missing.map(g => g.id).join(', ')}`);
}

Type guard

function isCompleteEmergencyAccessRotation(existing: { id: string; keyEncrypted: string | null }[], submitted: { id: string }[]): boolean {
  const have = new Set(submitted.map(s => s.id));
  return existing.filter(e => e.keyEncrypted != null).every(e => have.has(e.id));
}

Try / catch

try {
  await api.rotateKey(payload);
} catch (e) {
  if (e.status === 400 && /emergency access keys must be included/i.test(e.message)) {
    await refreshEmergencyAccess();
    payload.emergencyAccessKeys = confirmed.map(g => ({ id: g.id, keyEncrypted: reencrypt(g.keyEncrypted) }));
    return api.rotateKey(payload);
  }
  throw e;
}

Prevention

When it happens

Trigger: Key-rotation request whose emergency-access array omits an EmergencyAccess.Id that exists for the grantor and already has a KeyEncrypted set. A grant was confirmed in another session after the client cached the list; or the client only rotated grants it 'remembers'.

Common situations: User confirmed a new emergency-access contact on another device and then rotated keys on this one; client built the list from local state rather than the server; a grant Id was dropped during serialization.

Related errors


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/1ef39d107e14b31f. Report an issue: GitHub.