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.

Multi-Layer Tenant Isolation
Incoming Request
JWT tenant_id Claim — Extracted by TenantContextMiddleware
IDataScopeService — Sets current tenant context
EF Global Query Filter — WHERE TenantId = @currentTenantId
Tenant-Scoped Data Only
TenantContextMiddleware.csC#
/// <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);
    }
}
DataScopeService — Scoping MechanismC#
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.

ID Encryption & Decryption Pipeline
Guid ID (16B)
Random Nonce (12B)
IdEncryptionService — AES-GCM Authenticated Encryption
Wire Format: [Nonce 12B][Tag 16B][Ciphertext]
Base64Url Encoding
URL-Safe String ID
IdEncryptionService.csC#
/// <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.

RestrictedFieldsAuthorizationFilter.csC#
/// <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 omitted

Tenant-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.

ServicePurposeKey Methods
ITenantGuardianServiceValidates tenant hierarchy operationsValidateHierarchy(), CanAccessChild(), CanCreateSubTenant()
ITenantQuotaServiceEnforces resource limits per tenantCheckAdminQuota(), CheckUserQuota(), CheckStorageQuota()
ITenantPasswordValidatorTenant-level password policy enforcementValidateAsync() — checks min length, complexity, history
IAdminSecurityServiceProtected admin operationsIsProtected(), CanTransfer(), ValidateTransfer()
TenantGuardianService — Hierarchy ValidationC#
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.

Database Column Level Encryption & Hashing Architecture
Sensitive Inputs
Passwords / Backup Codes
Refresh Tokens
OIDC Client Secrets
2FA TOTP Secrets
GDPR Erasure PII
BCrypt Hash — Work Factor = 12 slow hash
SHA-256 Hash — One-way secure hashing
AES-GCM Encrypted — Symmetric key encryption
AES-256 Encrypted
SHA-256 Truncated (16 char) — Stable anonymized token
Secure Database Store
Data TypeProtection MethodSymmetric/Hash AlgorithmStorage / Location
PasswordsOne-Way Salting & HashingBCrypt (Work Factor 12)Database (IdentityDb)
OTP CodesEager One-Way HashingBCrypt (never stored in plaintext)Database (IdentityDb)
Refresh TokensOne-Way HashingSHA-256Database (IdentityDb)
2FA SecretsSymmetric Encryption at RestAES-256Database (IdentityDb)
Backup CodesOne-Way Salting & HashingBCrypt (hashed individually)Database (IdentityDb)
OIDC Client SecretsSymmetric Encryption at RestAES-GCM (IdEncryptionService)Database (IdentityDb)
JWT Signing KeyHMAC Signature VerificationHMAC-SHA256 or RSA keyappsettings.json / Environment Variable
Connection StringsSymmetric Data Protection / Secret VaultDPAPI / Environment Variablesappsettings.Production.json / Env vars
File UploadsTenant Isolation + Strict ACLsEncrypted Blob PathsBlob Storage (S3 / Azure Blob)
GDPR Erasure PIIStable Irreversible AnonymizationANONYMIZED- + 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.