BitzOrcas uses ASP.NET Core Data Protection for small application-owned secrets that must later be recovered, including AI provider keys, sensitive Website lead fields, and experiment assignments. It does not replace password hashing, TLS, database encryption, or a managed KMS.
Protection path
A protected payload carries key and integrity information. A reader still needs the same application key ring and purpose chain; copying only the ciphertext or connection string is insufficient.
Current registration behavior
The API calls AddBitzOrcasDataProtection before building the container. It reads ConnectionStrings:redis first and falls back to Redis:ConnectionString.
// Register once at the composition root.builder.Services.AddBitzOrcasDataProtection(builder.Configuration);
// Modules consume IDataProtectionProvider with a stable purpose.var protector = provider.CreateProtector( "BitzOrcas.Website.Leads.SensitiveFields.v1");Without a Redis connection string, the registration uses framework-default Data Protection storage. Source comments call this an in-memory ring, but the actual default also depends on runtime hosting. This manual therefore promises only a non-shared default repository, not restart durability.
With a Redis connection string, KeyManagementOptions receives a RedisXmlRepository at bitzorcas:dataprotection:keys. The configuration resolves IConnectionMultiplexer at runtime to support Aspire’s delayed connection.
Understand the fallback
If a connection string exists but no IConnectionMultiplexer is registered, the options configurator returns and keeps the default repository. The Host does not fail. That makes local composition resilient, but proves that “Redis is configured” is not the same as “the replicas share a key ring.”
The current registration does not set an explicit application name and does not wrap Redis XML key material with KMS/HSM protection. Isolate deployments with separate Redis credentials, networks, databases, or a future configurable name/prefix.
Select the right mechanism
| Data | Mechanism | Reason |
|---|---|---|
| User password | slow one-way password hash | plaintext is never needed |
| API Key / SCIM validator | SHA-256 hash + fixed-time comparison | validation only |
| Provider secret | Data Protection or external reference | the application must recover it |
| JWT/Webhook root key | Secret Store/KMS/HSM | independent access and rotation |
| database/object data | platform encryption at rest | large persistent data |
| network traffic | TLS | transport confidentiality and integrity |
Do not use reversible protection as a password hash or use Data Protection to encrypt large files.
Design purpose chains
A purpose is part of cryptographic isolation. Include product, module, use, and a version.
// Readers and writers must reproduce the exact same purpose chain.var protector = provider .CreateProtector("BitzOrcas.AIManage") .CreateProtector("ProviderApiKey") .CreateProtector("v1");
// Persist the protected value; never log the plaintext or decrypted result.var protectedValue = protector.Protect(apiKey);Changing a purpose switches decryption domains. For a format upgrade, read the old version, write the new version, migrate active values, and only then remove the old path.
Manage key lifetime
New writes use the current key while old keys remain available for decryption. Rotation means activating a new key, not deleting history. Revoke an old key only after every dependent payload has expired or been reprotected.
Treat the Redis ring as critical data:
- grant the workload account only the required key prefix;
- protect Redis authentication and transport;
- back up, restore, and audit key changes;
- exclude this key from generic cache flush scripts;
- isolate development, test, staging, and production rings.
Rehearse a rolling deployment
Use two processes or versions, not a single provider round-trip.
old replica A Protect(v1) → new replica B Unprotect succeedsnew replica B Protect(v1) → old replica A Unprotect succeedsrestart A and B → historical probes still decrypttemporarily block Redis → alert and execute the chosen failure policyrestore Redis/key ring → historical data remains readableFor long-lived business values, sample historical ciphertexts using a read-only verification job. Use meaningless probe plaintext and never expose real secrets in test logs.
Handle failures explicitly
A CryptographicException can mean a purpose mismatch, missing key, different repository, corrupted payload, or incompatible application format. Do not swallow it and treat ciphertext as plaintext. Do not silently generate a replacement credential to hide data loss.
For an external connector, mark the connection unavailable and alert. For low-risk experiment assignment, a domain may deliberately reassign. The owning domain defines fallback; the generic protection layer must not invent it.
Tests and observability
- unit-test same-purpose success and different-purpose failure;
- integration-test two ServiceProviders against real Redis;
- cover old/new versions and process restart;
- count decrypt failures by safe use category, never by secret value;
- rehearse key-ring backup and restore;
- expose whether Redis repository is truly active rather than silently assuming it.
Current boundary
The code supplies a shared Redis repository and safe local fallback, but not a first-class key-ring health probe, explicit deployment isolation name, KMS wrapping, or production fail-fast for repository fallback. Until those are delivered, multi-replica GA evidence depends on cross-decryption and Redis controls.
Adoption decision
A single development instance may accept default storage. Any replica set, rolling deployment, or long-lived ciphertext requires shared storage and cross-decryption proof. Regulated or high-value secrets should additionally require KMS wrapping and key-access audit before release.