bitwarden/server · error · BadRequestException

All existing ciphers must be included in the rotation.

Error message

All existing ciphers must be included in the rotation.

What it means

Thrown by CipherRotationValidator when the set of ciphers submitted for a key rotation does not include every existing personal (non-organization) cipher the user currently has. The validator iterates over existing user ciphers and requires a matching submitted cipher by Id for each one. BadRequestException returns HTTP 400.

Source

Thrown at src/Api/KeyManagement/Validators/CipherRotationValidator.cs:39

        var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id);
        if (existingCiphers == null)
        {
            return result;
        }

        var existingUserCiphers = existingCiphers.Where(c => c.OrganizationId == null);
        if (existingUserCiphers.Count() == 0)
        {
            return result;
        }

        foreach (var existing in existingUserCiphers)
        {
            var cipher = ciphers.FirstOrDefault(c => c.Id == existing.Id);
            if (cipher == null)
            {
                throw new BadRequestException("All existing ciphers must be included in the rotation.");
            }
            result.Add(cipher.ToCipher(existing));
        }
        return result;
    }
}

View on GitHub (pinned to e93b962371)

Solutions

  1. Re-fetch the full cipher list immediately before submitting the rotation to minimize the race window.
  2. Ensure the client sends the complete, untruncated cipher list including all personal ciphers.
  3. If a cipher was added concurrently, abort the rotation, re-fetch, and retry.
  4. Consider implementing an optimistic-concurrency check (e.g., a revision/version stamp) to detect stale cipher lists.

Example fix

// before
await api.RotateAllCipherKeysAsync(staleCipherList); // throws if incomplete

// after — refresh right before rotation
var currentCiphers = await api.ListCiphersAsync();
await api.RotateAllCipherKeysAsync(currentCiphers);
Defensive patterns

Strategy: validation

Validate before calling

// Refresh cipher list immediately before rotation
var currentCiphers = await _cipherRepository.GetManyByUserIdAsync(userId);
var personalCiphers = currentCiphers.Where(c => c.OrganizationId == null);
var submittedIds = submittedCiphers.Select(c => c.Id).ToHashSet();
if (!personalCiphers.All(c => submittedIds.Contains(c.Id)))
    return BadRequest("Cipher list is stale — some personal ciphers are missing. Refresh and retry.");
await _cipherRotationService.RotateAsync(submittedCiphers);

Type guard

public static bool AllPersonalCiphersIncluded(IEnumerable<Cipher> existing, IEnumerable<Cipher> submitted) =>
    existing.Where(c => c.OrganizationId == null).All(e => submitted.Any(s => s.Id == e.Id));

Try / catch

try
{
    await _cipherService.RotateAllKeysAsync(submittedCiphers);
}
catch (BadRequestException ex) when (ex.Message.Contains("All existing ciphers"))
{
    // Re-fetch and retry once
    var fresh = await _cipherRepository.GetManyByUserIdAsync(userId);
    await _cipherService.RotateAllKeysAsync(fresh.Where(c => c.OrganizationId == null));
}

Prevention

When it happens

Trigger: POST to the key-rotation endpoint (e.g., /ciphers/rotate-all) with a cipher list that is missing one or more of the user's existing personal ciphers. This is a safety check — rotating keys requires re-encrypting every cipher with the new key; omitting any would leave ciphers undecryptable.

Common situations: Client fetches the cipher list, user creates a new cipher in another session/tab, then the rotation is submitted with the now-stale list; network error caused one cipher to be dropped from the payload; client-side bug truncating the list; concurrent modification between fetch and rotate.

Related errors


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