{"record":{"id":"f9db62f55f51eb1e","repo":"bitwarden/server","slug":"accountkeys-are-only-supported-for-v2-encryption","errorCode":null,"errorMessage":"AccountKeys are only supported for V2 encryption.","messagePattern":"AccountKeys are only supported for V2 encryption\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/Auth/Controllers/AccountsController.cs","lineNumber":527,"sourceCode":"    public async Task<KeysResponseModel> PostKeys([FromBody] KeysRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!string.IsNullOrWhiteSpace(user.PrivateKey) || !string.IsNullOrWhiteSpace(user.PublicKey))\n        {\n            throw new BadRequestException(\"User has existing keypair\");\n        }\n\n        if (model.AccountKeys != null)\n        {\n            var accountKeysData = model.AccountKeys.ToAccountKeysData();\n            if (!accountKeysData.IsV2Encryption())\n            {\n                throw new BadRequestException(\"AccountKeys are only supported for V2 encryption.\");\n            }\n            // A client that predates the key id field sends none. The account then picks one up from\n            // the backfill endpoint on a later sync rather than here.\n            var userKeyId = KeyId.FromHexEncodedString(model.UserKeyId);\n            var updateUserDataTasks = userKeyId == null\n                ? null\n                : new UpdateUserData[] { _userRepository.SetUserKeyId(user.Id, userKeyId) };\n\n            await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, accountKeysData,\n                updateUserDataTasks);\n            return new KeysResponseModel(accountKeysData, user.Key);\n        }\n        else\n        {\n            // Todo: Drop this after a transition period. This will drop no-account-keys requests.\n            // The V1 check in the other branch should persist\n            // https://bitwarden.atlassian.net/browse/PM-27329\n            await _userService.SaveUserAsync(model.ToUser(user));","sourceCodeStart":509,"sourceCodeEnd":545,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/Auth/Controllers/AccountsController.cs#L509-L545","documentation":"In POST /accounts/keys, when model.AccountKeys is supplied but accountKeysData.IsV2Encryption() is false, the controller throws BadRequestException(\"AccountKeys are only supported for V2 encryption.\") → HTTP 400. The AccountKeys field exclusively carries V2 (account-keys / SSO-style) encryption data; V1 keypair submissions must omit it.","triggerScenarios":"A client sends an AccountKeys payload whose structure does not represent V2 encryption (e.g. a legacy/V1 keypair wrapped in the AccountKeys field), or passes the V1 public/private keypair through the V2 field.","commonSituations":"Mixed client version sending both V1 keypair fields and a non-V2 AccountKeys object; serialization bug putting PublicKeyEncryptionKeyPairData in the wrong place; client upgraded partially and sends an inconsistent payload.","solutions":["Ensure AccountKeys is only populated with V2-encryption data; for V1 keypair submission omit AccountKeys entirely (the else branch handles it).","Verify ToAccountKeysData().IsV2Encryption() returns true on the client before posting.","Upgrade the client fully to the V2 key model so the payload is internally consistent."],"exampleFix":"// before\nmodel.AccountKeys = legacyV1Keypair; // IsV2Encryption() == false\n// after — V1 path: omit AccountKeys\nmodel.AccountKeys = null;\nmodel.PublicKey = pub; model.PrivateKey = priv;","handlingStrategy":"type-guard","validationCode":"// Only send AccountKeys when it represents V2 encryption\nif (model.AccountKeys != null && !model.AccountKeys.ToAccountKeysData().IsV2Encryption())\n    model.AccountKeys = null; // fall back to V1 keypair path","typeGuard":"static bool IsV2(AccountKeysData? d) => d is not null && d.IsV2Encryption();","tryCatchPattern":"try { await client.PostAsync(\"accounts/keys\", content); }\ncatch (BadRequestException ex) when (ex.Message.Contains(\"V2 encryption\"))\n{ /* rebuild payload: omit AccountKeys or supply real V2 data */ }","preventionTips":["Keep the key payload internally consistent (V1 fields vs V2 AccountKeys).","Omit AccountKeys for V1 keypair submission.","Verify IsV2Encryption() client-side before posting."],"tags":["validation","bad-request","keys","encryption","v2-encryption"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}