{"record":{"id":"a65058e0703acd9a","repo":"bitwarden/server","slug":"all-existing-trusted-devices-must-be-included-in-t","errorCode":null,"errorMessage":"All existing trusted devices must be included in the rotation.","messagePattern":"All existing trusted devices must be included in the rotation\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/KeyManagement/Validators/DeviceRotationValidator.cs","lineNumber":40,"sourceCode":"        _deviceRepository = deviceRepository;\n    }\n\n    public async Task<IEnumerable<Device>> ValidateAsync(User user, IEnumerable<OtherDeviceKeysUpdateRequestModel> devices)\n    {\n        var result = new List<Device>();\n\n        var existingTrustedDevices = (await _deviceRepository.GetManyByUserIdAsync(user.Id)).Where(d => d.IsTrusted()).ToList();\n        if (existingTrustedDevices.Count == 0)\n        {\n            return result;\n        }\n\n        foreach (var existing in existingTrustedDevices)\n        {\n            var device = devices.FirstOrDefault(c => c.DeviceId == existing.Id);\n            if (device == null)\n            {\n                throw new BadRequestException(\"All existing trusted devices must be included in the rotation.\");\n            }\n\n            if (device.EncryptedUserKey == null || device.EncryptedPublicKey == null)\n            {\n                throw new BadRequestException(\"Rotated encryption keys must be provided for all devices that are trusted.\");\n            }\n\n            result.Add(device.ToDevice(existing));\n        }\n\n        return result;\n    }\n}\n","sourceCodeStart":22,"sourceCodeEnd":54,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/KeyManagement/Validators/DeviceRotationValidator.cs#L22-L54","documentation":"Thrown by DeviceRotationValidator during a user encryption-key rotation (e.g. master password change). The validator loads every device the user has marked as trusted (devices holding an encrypted copy of the user key for SSO/trust flows) and requires the rotation request to include every one of them, matched by DeviceId. If even one trusted device is absent from the submitted list, the whole rotation is rejected so that no device is left encrypted under the old (now-rotated) key.","triggerScenarios":"PUT/POST to the user key-rotation endpoint with an `OtherDeviceKeysUpdateRequestModel` collection that omits one or more DeviceIds returned by the devices repository for the user. A new device was trusted since the client last enumerated the device list; the client sent a stale/partial list; or a DeviceId was malformed/transposed.","commonSituations":"A client app cached the device list at session start and a device was trusted in another session; the rotation request was hand-built and only included 'known' devices; concurrent rotations or a race where a device trust completed mid-rotation.","solutions":["Re-fetch the user's full device list immediately before building the rotation payload so no trusted device is omitted.","Ensure every entry with IsTrusted()=true on the server is present in the request, keyed by its exact DeviceId GUID.","If a device should no longer be trusted, un-trust/delete it through its own endpoint before rotating keys, rather than dropping it from the rotation list.","Validate the submitted DeviceId set is a superset of the trusted-device set before sending the request."],"exampleFix":"// before: client sends only devices it knew about\nvar devices = [\"device-a\", \"device-b\"];\nrotate({ devices: devices.map(reencrypt) });\n\n// after: fetch the authoritative trusted set first, include all\nconst trusted = await api.getMyDevices();\nconst devices = trusted.filter(d => d.isTrusted).map(d => d.id);\nrotate({ devices: devices.map(reencrypt) });","handlingStrategy":"validation","validationCode":"// Before rotating, ensure every trusted device is represented\nconst trusted = (await api.getMyDevices()).filter(d => d.isTrusted);\nconst submitted = new Set(rotationPayload.devices.map(d => d.deviceId));\nconst missing = trusted.filter(d => !submitted.has(d.id));\nif (missing.length) {\n  throw new Error(`Rotation is missing trusted devices: ${missing.map(d => d.id).join(', ')}`);\n}","typeGuard":"function isCompleteDeviceRotation(existing: Device[], submitted: { deviceId: string }[]): boolean {\n  const have = new Set(submitted.map(s => s.deviceId));\n  return existing.filter(d => d.isTrusted).every(d => have.has(d.id));\n}","tryCatchPattern":"try {\n  await api.rotateKey(payload);\n} catch (e) {\n  if (e.status === 400 && /trusted devices must be included/i.test(e.message)) {\n    await refreshDevices();\n    payload.devices = trustedDevices.map(reencrypt);\n    return api.rotateKey(payload);\n  }\n  throw e;\n}","preventionTips":["Always re-fetch the user's devices immediately before building a rotation payload.","Sync the vault/account state right before a key rotation to avoid stale lists.","Add a client-side superset assertion comparing submitted ids against the trusted-device set."],"tags":["key-rotation","device","encryption","validation","bad-request"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}