{"record":{"id":"1ef39d107e14b31f","repo":"bitwarden/server","slug":"all-existing-emergency-access-keys-must-be-include","errorCode":null,"errorMessage":"All existing emergency access keys must be included in the rotation.","messagePattern":"All existing emergency access keys must be included in the rotation\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/KeyManagement/Validators/EmergencyAccessRotationValidator.cs","lineNumber":41,"sourceCode":"    public async Task<IEnumerable<EmergencyAccess>> ValidateAsync(User user,\n        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)\n    {\n        var result = new List<EmergencyAccess>();\n\n        var existing = await _emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(user.Id);\n        if (existing == null || existing.Count == 0)\n        {\n            return result;\n        }\n        // Exclude any emergency access that has not been confirmed yet.\n        existing = existing.Where(ea => ea.KeyEncrypted != null).ToList();\n\n        foreach (var ea in existing)\n        {\n            var emergencyAccess = emergencyAccessKeys.FirstOrDefault(c => c.Id == ea.Id);\n            if (emergencyAccess == null)\n            {\n                throw new BadRequestException(\"All existing emergency access keys must be included in the rotation.\");\n            }\n\n            if (emergencyAccess.KeyEncrypted == null)\n            {\n                throw new BadRequestException(\"Emergency access keys cannot be set to null during rotation.\");\n            }\n\n            result.Add(emergencyAccess.ToEmergencyAccess(ea));\n        }\n\n        return result;\n    }\n}\n","sourceCodeStart":23,"sourceCodeEnd":55,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/KeyManagement/Validators/EmergencyAccessRotationValidator.cs#L23-L55","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","solutions":["Fetch all confirmed emergency-access grants for the user immediately before constructing the rotation payload.","Include every grant with KeyEncrypted set, keyed by its exact Id.","Revoke any grant you do not want to rotate before starting the key rotation.","Verify the submitted Id set is a superset of the server's confirmed-grant set before sending."],"exampleFix":"// before\nconst ea = locallyKnownGrants.map(g => ({ id: g.id, keyEncrypted: reencrypt(g.key) }));\n\n// after\nconst all = await api.getEmergencyAccessGrants();\nconst ea = all.filter(g => g.keyEncrypted).map(g => ({ id: g.id, keyEncrypted: reencrypt(g.key) }));","handlingStrategy":"validation","validationCode":"const confirmed = (await api.getEmergencyAccessGrants()).filter(g => g.keyEncrypted);\nconst submitted = new Set(payload.emergencyAccessKeys.map(g => g.id));\nconst missing = confirmed.filter(g => !submitted.has(g.id));\nif (missing.length) {\n  throw new Error(`Rotation is missing emergency access grants: ${missing.map(g => g.id).join(', ')}`);\n}","typeGuard":"function isCompleteEmergencyAccessRotation(existing: { id: string; keyEncrypted: string | null }[], submitted: { id: string }[]): boolean {\n  const have = new Set(submitted.map(s => s.id));\n  return existing.filter(e => e.keyEncrypted != null).every(e => have.has(e.id));\n}","tryCatchPattern":"try {\n  await api.rotateKey(payload);\n} catch (e) {\n  if (e.status === 400 && /emergency access keys must be included/i.test(e.message)) {\n    await refreshEmergencyAccess();\n    payload.emergencyAccessKeys = confirmed.map(g => ({ id: g.id, keyEncrypted: reencrypt(g.keyEncrypted) }));\n    return api.rotateKey(payload);\n  }\n  throw e;\n}","preventionTips":["Re-fetch confirmed emergency-access grants before building the rotation payload.","Revoke grants you do not want to rotate before starting the rotation.","Assert the submitted id set is a superset of the confirmed-grant set."],"tags":["key-rotation","emergency-access","encryption","validation","bad-request"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}