bitwarden/server · error · BadRequestException

Emergency access keys cannot be set to null during rotation.

Error message

Emergency access keys cannot be set to null during rotation.

What it means

Thrown by EmergencyAccessRotationValidator when a rotation entry matched a confirmed emergency-access grant (matched by Id) but its KeyEncrypted is null. Rotation must re-encrypt every existing grant; sending a matched entry with a null key would erase access, which the validator forbids.

Source

Thrown at src/Api/KeyManagement/Validators/EmergencyAccessRotationValidator.cs:46

        var existing = await _emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(user.Id);
        if (existing == null || existing.Count == 0)
        {
            return result;
        }
        // Exclude any emergency access that has not been confirmed yet.
        existing = existing.Where(ea => ea.KeyEncrypted != null).ToList();

        foreach (var ea in existing)
        {
            var emergencyAccess = emergencyAccessKeys.FirstOrDefault(c => c.Id == ea.Id);
            if (emergencyAccess == null)
            {
                throw new BadRequestException("All existing emergency access keys must be included in the rotation.");
            }

            if (emergencyAccess.KeyEncrypted == null)
            {
                throw new BadRequestException("Emergency access keys cannot be set to null during rotation.");
            }

            result.Add(emergencyAccess.ToEmergencyAccess(ea));
        }

        return result;
    }
}

View on GitHub (pinned to e93b962371)

Solutions

  1. Re-encrypt the grant's KeyEncrypted with the new user key for every included grant.
  2. To remove an emergency-access grant, call the revoke endpoint before rotating rather than nulling the key in the rotation payload.
  3. Assert every included grant entry has a non-null KeyEncrypted before submitting.

Example fix

// before: trying to drop a grant by nulling
{ id: g.id, keyEncrypted: null }

// after: revoke separately, then rotate the rest
await api.revokeEmergencyAccess(g.id);
rotate({ emergencyAccessKeys: remaining.map(g => ({ id: g.id, keyEncrypted: reencrypt(g.key) })) });
Defensive patterns

Strategy: validation

Validate before calling

const nulled = payload.emergencyAccessKeys.filter(g => g.keyEncrypted == null);
if (nulled.length) {
  throw new Error(`Emergency access keys cannot be null: ${nulled.map(g => g.id).join(', ')}`);
}

Type guard

function hasEmergencyAccessKey(g: { keyEncrypted?: string | null }): boolean {
  return g.keyEncrypted != null && g.keyEncrypted.length > 0;
}

Try / catch

try {
  await api.rotateKey(payload);
} catch (e) {
  if (e.status === 400 && /cannot be set to null/i.test(e.message)) {
    payload.emergencyAccessKeys = payload.emergencyAccessKeys.map(g => ({ ...g, keyEncrypted: reencrypt(g.keyEncrypted ?? oldKeyFor(g.id)) }));
    return api.rotateKey(payload);
  }
  throw e;
}

Prevention

When it happens

Trigger: The emergency-access array includes the grant Id but the KeyEncrypted field was left null or stripped out during request construction.

Common situations: Client attempted to 'remove' a grant by nulling its key instead of revoking it through the revoke endpoint; serialization omitted the field; the re-encryption of that grant's key failed silently.

Related errors


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