{"record":{"id":"c62d41824b66a348","repo":"bitwarden/server","slug":"all-existing-ciphers-must-be-included-in-the-rotat","errorCode":null,"errorMessage":"All existing ciphers must be included in the rotation.","messagePattern":"All existing ciphers must be included in the rotation\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/KeyManagement/Validators/CipherRotationValidator.cs","lineNumber":39,"sourceCode":"\n        var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id);\n        if (existingCiphers == null)\n        {\n            return result;\n        }\n\n        var existingUserCiphers = existingCiphers.Where(c => c.OrganizationId == null);\n        if (existingUserCiphers.Count() == 0)\n        {\n            return result;\n        }\n\n        foreach (var existing in existingUserCiphers)\n        {\n            var cipher = ciphers.FirstOrDefault(c => c.Id == existing.Id);\n            if (cipher == null)\n            {\n                throw new BadRequestException(\"All existing ciphers must be included in the rotation.\");\n            }\n            result.Add(cipher.ToCipher(existing));\n        }\n        return result;\n    }\n}\n","sourceCodeStart":21,"sourceCodeEnd":46,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/KeyManagement/Validators/CipherRotationValidator.cs#L21-L46","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-fetch the full cipher list immediately before submitting the rotation to minimize the race window.","Ensure the client sends the complete, untruncated cipher list including all personal ciphers.","If a cipher was added concurrently, abort the rotation, re-fetch, and retry.","Consider implementing an optimistic-concurrency check (e.g., a revision/version stamp) to detect stale cipher lists."],"exampleFix":"// before\nawait api.RotateAllCipherKeysAsync(staleCipherList); // throws if incomplete\n\n// after — refresh right before rotation\nvar currentCiphers = await api.ListCiphersAsync();\nawait api.RotateAllCipherKeysAsync(currentCiphers);","handlingStrategy":"validation","validationCode":"// Refresh cipher list immediately before rotation\nvar currentCiphers = await _cipherRepository.GetManyByUserIdAsync(userId);\nvar personalCiphers = currentCiphers.Where(c => c.OrganizationId == null);\nvar submittedIds = submittedCiphers.Select(c => c.Id).ToHashSet();\nif (!personalCiphers.All(c => submittedIds.Contains(c.Id)))\n    return BadRequest(\"Cipher list is stale — some personal ciphers are missing. Refresh and retry.\");\nawait _cipherRotationService.RotateAsync(submittedCiphers);","typeGuard":"public static bool AllPersonalCiphersIncluded(IEnumerable<Cipher> existing, IEnumerable<Cipher> submitted) =>\n    existing.Where(c => c.OrganizationId == null).All(e => submitted.Any(s => s.Id == e.Id));","tryCatchPattern":"try\n{\n    await _cipherService.RotateAllKeysAsync(submittedCiphers);\n}\ncatch (BadRequestException ex) when (ex.Message.Contains(\"All existing ciphers\"))\n{\n    // Re-fetch and retry once\n    var fresh = await _cipherRepository.GetManyByUserIdAsync(userId);\n    await _cipherService.RotateAllKeysAsync(fresh.Where(c => c.OrganizationId == null));\n}","preventionTips":["Re-fetch the complete cipher list immediately before submitting a key rotation.","Prevent concurrent cipher creation during the rotation window using a client-side lock.","Validate that the submitted cipher count matches the fetched personal cipher count before sending."],"tags":["encryption","key-rotation","ciphers","validation","concurrency","csharp","aspnet"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}