Data Protection
Tenant isolation, data encryption at rest and in transit, restricted fields, ID encryption, and GDPR compliance.
SCRIPE protects data at every layer from network encryption (TLS 1.2+) and database encryption (TDE), through row-level tenant isolation and field-level access control, to GDPR-compliant data portability and right-to-delete mechanisms.
Tenant Data Isolation
Every query is automatically scoped to the current tenant via EF Core global query filters. The ITenantAwareEntity interface marks entities that must be tenant-scoped, and the TenantContextMiddleware extracts the tenant ID from the JWT token.
/// <summary>
/// Extracts tenant_id from JWT claims and sets it in IDataScopeService.
/// All subsequent DB queries are automatically scoped to this tenant.
/// </summary>
public class TenantContextMiddleware
{
public async Task InvokeAsync(HttpContext context, IDataScopeService dataScopeService)
{
if (context.User.Identity?.IsAuthenticated == true)
{
var tenantClaim = context.User.FindFirst("tenant_id");
if (tenantClaim != null && Guid.TryParse(tenantClaim.Value, out var tenantId))
{
dataScopeService.SetCurrentTenant(tenantId);
}
}
await _next(context);
}
}public class DataScopeService : IDataScopeService
{
private Guid? _currentTenantId;
public Guid? CurrentTenantId => _currentTenantId;
public void SetCurrentTenant(Guid tenantId)
=> _currentTenantId = tenantId;
// Used by EF Core global query filter
public Expression<Func<T, bool>> GetTenantFilter<T>()
where T : ITenantAwareEntity
{
var tenantId = _currentTenantId
?? throw new UnauthorizedAccessException("No tenant context");
return entity => entity.TenantId == tenantId;
}
}ID Encryption
SCRIPE can encrypt Guid entity IDs in API responses using AES-256. This prevents enumeration attacks and hides internal database identifiers from external consumers.
/// <summary>
/// AES-GCM authenticated encryption service for IDs.
/// Encrypts Guid and int IDs to URL-safe base64 strings.
///
/// Uses AES-GCM (Galois/Counter Mode) which provides both confidentiality
/// and integrity — immune to padding oracle attacks unlike AES-CBC.
///
/// Wire format: [Nonce 12B][Tag 16B][Ciphertext]
/// </summary>
public sealed class IdEncryptionService : IIdEncryptionService
{
private const int NonceSize = 12; // AES-GCM standard nonce size
private const int TagSize = 16; // AES-GCM standard tag size
private readonly byte[] _key; // 32-byte key derived from config
public IdEncryptionService(IOptions<IdEncryptionOptions> options, IHostEnvironment? environment = null)
{
var keyString = options.Value.Key;
var minLength = options.Value.MinimumKeyLength; // Min 32 chars
// Startup safety checks - fail-closed model
if (string.IsNullOrWhiteSpace(keyString) || keyString.Length < minLength)
throw new InvalidOperationException("Key is missing or too weak.");
// Rejects placeholder keys ("change-this") in Production environments
if (options.Value.RejectPlaceholderKeysInProduction && environment?.IsProduction() == true && IsPlaceholder(keyString))
throw new InvalidOperationException("Do not use placeholders in production!");
_key = Encoding.UTF8.GetBytes(keyString[..32]);
}
public string Encrypt(Guid id)
{
var plainBytes = id.ToByteArray();
var nonce = new byte[NonceSize];
RandomNumberGenerator.Fill(nonce);
var ciphertext = new byte[plainBytes.Length];
var tag = new byte[TagSize];
using var aesGcm = new AesGcm(_key, TagSize);
aesGcm.Encrypt(nonce, plainBytes, ciphertext, tag);
// Combined wire format: [nonce][tag][ciphertext]
var combined = new byte[NonceSize + TagSize + ciphertext.Length];
Buffer.BlockCopy(nonce, 0, combined, 0, NonceSize);
Buffer.BlockCopy(tag, 0, combined, NonceSize, TagSize);
Buffer.BlockCopy(ciphertext, 0, combined, NonceSize + TagSize, ciphertext.Length);
return Convert.ToBase64String(combined).Replace('+', '-').Replace('/', '_').TrimEnd('=');
}
public Guid Decrypt(string encryptedId)
{
var combined = Convert.FromBase64String(encryptedId.Replace('-', '+').Replace('_', '/') + GetPadding(encryptedId));
if (combined.Length < NonceSize + TagSize)
throw new CryptographicException("Ciphertext is too short.");
var nonce = new byte[NonceSize];
var tag = new byte[TagSize];
var ciphertext = new byte[combined.Length - NonceSize - TagSize];
Buffer.BlockCopy(combined, 0, nonce, 0, NonceSize);
Buffer.BlockCopy(combined, NonceSize, tag, 0, TagSize);
Buffer.BlockCopy(combined, NonceSize + TagSize, ciphertext, 0, ciphertext.Length);
var plaintext = new byte[ciphertext.Length];
using var aesGcm = new AesGcm(_key, TagSize);
aesGcm.Decrypt(nonce, ciphertext, tag, plaintext);
return new Guid(plaintext);
}
}Restricted Fields (Field-Level Security)
Roles can restrict access to specific entity fields. When a role has restricted fields configured, the FieldProjectionMiddleware automatically removes those fields from API responses, preventing unauthorized data exposure.
/// <summary>
/// ASP.NET Core authorization filter that checks if the current user's role
/// has permission to view specific fields in the response.
/// Fields are configured per-role in the RolePermission entity.
/// </summary>
public class RestrictedFieldsAuthorizationFilter : IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var roleId = context.HttpContext.User.FindFirst("role_id")?.Value;
if (roleId is null) return;
var restrictedFields = await _rolePermRepo
.GetRestrictedFieldsAsync(Guid.Parse(roleId));
if (restrictedFields.Any())
{
// Store in HttpContext.Items for response serializer
context.HttpContext.Items["RestrictedFields"] = restrictedFields;
}
}
}
// In response serialization:
// Fields listed in RestrictedFields are replaced with "***" or omittedTenant-Aware Services
Services that need to access tenant-specific data inject IDataScopeService to get the current tenant ID. This service reads the tenant_id claim from the JWT token and makes it available throughout the request pipeline.
| Service | Purpose | Key Methods |
|---|---|---|
| ITenantGuardianService | Validates tenant hierarchy operations | ValidateHierarchy(), CanAccessChild(), CanCreateSubTenant() |
| ITenantQuotaService | Enforces resource limits per tenant | CheckAdminQuota(), CheckUserQuota(), CheckStorageQuota() |
| ITenantPasswordValidator | Tenant-level password policy enforcement | ValidateAsync() — checks min length, complexity, history |
| IAdminSecurityService | Protected admin operations | IsProtected(), CanTransfer(), ValidateTransfer() |
public class TenantGuardianService : ITenantGuardianService
{
/// <summary>
/// Validates that a tenant operation doesn't violate hierarchy rules:
/// - Cannot create circular parent references
/// - Cannot delete a tenant with active children
/// - Cannot move a tenant under its own descendant
/// </summary>
public async Task<Result> ValidateHierarchy(
Guid tenantId, Guid? newParentId, CancellationToken ct)
{
if (newParentId is null) return Result.Success();
// Check for circular reference
var ancestors = await GetAllAncestorsAsync(newParentId.Value, ct);
if (ancestors.Contains(tenantId))
return Result.Failure(new AppError(
"Tenant.CircularReference",
"Cannot set a descendant as parent — circular reference"
));
return Result.Success();
}
}Data at Rest Encryption
Database-level Transparent Data Encryption (TDE) encrypts data files. Application-level encryption using ASP.NET Core Data Protection API secures sensitive fields like HMAC secrets and backup codes.
| Data Type | Protection Method | Symmetric/Hash Algorithm | Storage / Location |
|---|---|---|---|
| Passwords | One-Way Salting & Hashing | BCrypt (Work Factor 12) | Database (IdentityDb) |
| OTP Codes | Eager One-Way Hashing | BCrypt (never stored in plaintext) | Database (IdentityDb) |
| Refresh Tokens | One-Way Hashing | SHA-256 | Database (IdentityDb) |
| 2FA Secrets | Symmetric Encryption at Rest | AES-256 | Database (IdentityDb) |
| Backup Codes | One-Way Salting & Hashing | BCrypt (hashed individually) | Database (IdentityDb) |
| OIDC Client Secrets | Symmetric Encryption at Rest | AES-GCM (IdEncryptionService) | Database (IdentityDb) |
| JWT Signing Key | HMAC Signature Verification | HMAC-SHA256 or RSA key | appsettings.json / Environment Variable |
| Connection Strings | Symmetric Data Protection / Secret Vault | DPAPI / Environment Variables | appsettings.Production.json / Env vars |
| File Uploads | Tenant Isolation + Strict ACLs | Encrypted Blob Paths | Blob Storage (S3 / Azure Blob) |
| GDPR Erasure PII | Stable Irreversible Anonymization | ANONYMIZED- + SHA-256 (first 16 chars) | Database (Identifiable Fields) |
Data in Transit Encryption
All communication uses TLS 1.2 or higher. HSTS headers enforce HTTPS in production. Internal service communication between YARP gateway and backend modules also uses TLS.
- HTTPS enforced via HSTS (Strict-Transport-Security: max-age=31536000)
- TLS 1.2+ required — older protocols rejected
- Certificate pinning supported for mobile clients
- SignalR WebSocket connections use WSS (encrypted WebSocket)
- Inter-service communication uses mTLS in Kubernetes
- Cookie attributes: Secure, HttpOnly, SameSite=Strict
GDPR Compliance
SCRIPE provides mechanisms for GDPR compliance including data portability (export user data as JSON), right to delete (anonymize or purge user data), consent tracking, and configurable data retention policies.
Right to Delete
Enforce permanent erasure of user records and PII across all tenant databases upon request.
Data Portability
Export comprehensive machine-readable archives of all user-associated data in JSON format.
Consent Management
Immutable append-only tracking of consent timestamps, policy versions, and user agents.
Audit Trail for Compliance
Tamper-evident audit logging for every read, write, export, and deletion of sensitive personal data.
Query Filter Scoping
Strict multi-tenant isolation ensuring personal data never leaks across tenant boundaries.
Data Retention Policies
Automated background pruning and anonymization based on regulatory and corporate retention rules.