Skip to content

Add API Key Fields to Default Encryption Schema + Module-Level Encryption Configuration #500

@seanspeaks

Description

@seanspeaks

GitHub Issue for Frigg Core: API Key Encryption Enhancement

Repository: https://github.com/friggframework/frigg
Priority: High
Timeline: 72 hours


Title

Add API Key Fields to Default Encryption Schema + Module-Level Encryption Configuration

Issue Type

  • Feature Request
  • Security Enhancement

Summary

The Frigg framework's field-level encryption currently only auto-encrypts OAuth-related fields (access_token, refresh_token, id_token) in the Credential model. API key-based authentication is increasingly common, but api_key and apiKey fields are not automatically encrypted, creating a security gap for integrations using API key authentication.

Current State:

// @friggframework/core/database/encryption/encryption-schema-registry.js
const CORE_ENCRYPTION_SCHEMA = {
    Credential: {
        fields: [
            'data.access_token',   // ✅ Encrypted
            'data.refresh_token',  // ✅ Encrypted
            'data.id_token',       // ✅ Encrypted
            // ❌ data.api_key - NOT encrypted
            // ❌ data.apiKey - NOT encrypted
        ],
    },
};

Proposed Solutions

Solution 1: Add API Key Fields to Core Schema (Quick Fix)

File: packages/core/database/encryption/encryption-schema-registry.js

const CORE_ENCRYPTION_SCHEMA = {
    Credential: {
        fields: [
            'data.access_token',
            'data.refresh_token',
            'data.id_token',
            'data.api_key',        // ✅ NEW: Encrypt API keys
            'data.apiKey',         // ✅ NEW: Encrypt API keys (camelCase variant)
        ],
    },
    // ... rest unchanged
};

Pros:

  • Simple, one-line change
  • Immediate protection for all API key integrations
  • Consistent with OAuth token encryption
  • No breaking changes

Cons:

  • Still requires core code changes for new field types
  • Module developers can't customize encryption per-module

Solution 2: Module-Level Encryption Configuration (Recommended)

Allow API module developers to specify which fields should be encrypted directly in their module definition, without modifying core framework code.

File: packages/core/modules/module.js

Option A: encrypt flag in apiPropertiesToPersist

// In API module definition.js
const Definition = {
    requiredAuthMethods: {
        apiPropertiesToPersist: {
            credential: ['api_key', 'siteNumber'],
            entity: [],
            // NEW: Specify which credential fields to encrypt
            encrypt: {
                credential: ['api_key'],  // Encrypt only api_key, not siteNumber
                entity: []
            }
        },
    },
};

Option B: Separate apiPropertiesToEncrypt object (More Explicit)

// In API module definition.js
const Definition = {
    requiredAuthMethods: {
        apiPropertiesToPersist: {
            credential: ['api_key', 'siteNumber'],
            entity: [],
        },
        // NEW: Explicit encryption configuration
        apiPropertiesToEncrypt: {
            credential: ['api_key'],  // Only encrypt sensitive fields
            entity: []
        },
    },
};

Option C: Simple boolean flag (Encrypt All)

// In API module definition.js
const Definition = {
    requiredAuthMethods: {
        apiPropertiesToPersist: {
            credential: ['api_key', 'siteNumber'],
            entity: [],
        },
        // NEW: Encrypt all persisted credential fields
        encryptCredentials: true,  // Default: false
    },
};

Implementation Notes:

  1. Backward Compatibility: If no encryption config is provided, fall back to core schema
  2. Merge Strategy: Module-level encryption fields should MERGE with core schema, not replace
  3. Validation: Validate that encryption fields are subset of apiPropertiesToPersist
  4. Registration: Register module encryption fields with registerCustomSchema() during module initialization

Pros:

  • Module developers control their own encryption needs
  • No core code changes needed for new field types
  • Fine-grained control (encrypt some fields but not others)
  • Self-documenting (encryption requirements visible in module definition)

Cons:

  • Slightly more complex implementation
  • Module developers must remember to configure encryption

Solution 3: Hybrid Approach (Best of Both)

Implement both solutions:

  1. Add api_key and apiKey to core schema (immediate security for common case)
  2. Add module-level encryption configuration (flexibility for custom fields)

This provides:

  • Security by default for API key integrations
  • Flexibility for custom authentication fields (bearer tokens, session tokens, etc.)
  • No breaking changes for existing integrations

Use Cases

Use Case 1: API Key Integration (AxisCare, Quo)

// Currently requires storing as access_token (workaround)
// With fix: can use proper field name
const Definition = {
    requiredAuthMethods: {
        apiPropertiesToPersist: {
            credential: ['api_key', 'siteNumber'],
            entity: [],
        },
        // Option B example
        apiPropertiesToEncrypt: {
            credential: ['api_key'],  // siteNumber is public, don't encrypt
        },
    },
};

Use Case 2: Custom Bearer Token

const Definition = {
    requiredAuthMethods: {
        apiPropertiesToPersist: {
            credential: ['bearer_token', 'workspace_id'],
            entity: [],
        },
        apiPropertiesToEncrypt: {
            credential: ['bearer_token'],  // Custom field, auto-encrypted
        },
    },
};

Use Case 3: Multiple Secret Fields

const Definition = {
    requiredAuthMethods: {
        apiPropertiesToPersist: {
            credential: ['api_key', 'webhook_secret', 'signing_key', 'tenant_id'],
            entity: [],
        },
        apiPropertiesToEncrypt: {
            credential: ['api_key', 'webhook_secret', 'signing_key'],
            // tenant_id is not sensitive, don't encrypt
        },
    },
};

Impact

Affected Integrations:

  • Any API module using api_key or apiKey fields
  • Existing integrations: AxisCare, Quo (currently using workarounds)
  • Future integrations: Proper API key support out of the box

Security Risk Without Fix:

  • API keys stored in plain text in database (if not using access_token workaround)
  • Non-compliant with SOC2, HIPAA, PCI-DSS encryption requirements
  • Higher risk of credential exposure in database breach

Implementation Checklist

Quick Fix (Solution 1) - Estimated 30 minutes

  • Add 'data.api_key' to CORE_ENCRYPTION_SCHEMA.Credential.fields
  • Add 'data.apiKey' to CORE_ENCRYPTION_SCHEMA.Credential.fields
  • Add test case for API key encryption
  • Update encryption README documentation
  • Release as patch version (2.0.x)

Full Solution (Solution 3) - Estimated 4-6 hours

  • Implement Solution 1 (add to core schema)
  • Design module-level encryption API (decide on Option A, B, or C)
  • Implement module encryption registration in module.js
  • Update encryption-schema-registry.js to merge module schemas
  • Add validation for module encryption config
  • Write comprehensive tests for module-level encryption
  • Update documentation with examples
  • Add migration guide for existing integrations
  • Release as minor version (2.1.0)

Testing Requirements

// Test: Core API key encryption
describe('Core Encryption Schema', () => {
    it('should encrypt data.api_key field', async () => {
        const credential = await prisma.credential.create({
            data: {
                userId: testUser.id,
                data: { api_key: 'secret-key-123' }
            }
        });

        // Read directly from DB (bypassing decryption)
        const raw = await prisma.$queryRaw`SELECT data FROM Credential WHERE id = ${credential.id}`;
        expect(raw[0].data.api_key).toMatch(/^[A-Za-z0-9+/=]+:[A-Za-z0-9+/=]+:[A-Za-z0-9+/=]+$/); // Encrypted format

        // Read through Prisma (with decryption)
        const decrypted = await prisma.credential.findUnique({ where: { id: credential.id } });
        expect(decrypted.data.api_key).toBe('secret-key-123'); // Decrypted
    });
});

// Test: Module-level encryption configuration
describe('Module-Level Encryption', () => {
    it('should encrypt fields specified in apiPropertiesToEncrypt', async () => {
        // Test module with custom encryption config
        const testModule = {
            apiPropertiesToPersist: {
                credential: ['custom_token', 'workspace_id'],
            },
            apiPropertiesToEncrypt: {
                credential: ['custom_token'], // Only encrypt this one
            },
        };

        // Verify custom_token is encrypted, workspace_id is not
    });
});

Documentation Updates Needed

  1. README.md - Add API key encryption to feature list
  2. database/encryption/README.md - Document module-level encryption configuration
  3. API_MODULE_GUIDE.md - Add encryption configuration section
  4. MIGRATION.md - Guide for updating existing API key integrations

Related Issues/PRs

  • Related to field-level encryption implementation
  • Related to API key authentication support
  • Closes: (this issue will close the gap for API key encryption)

Questions for Maintainers

  1. Preferred solution? Quick fix (Solution 1), Full solution (Solution 2), or Hybrid (Solution 3)?
  2. API preference for module-level encryption? Option A (nested), Option B (separate object), or Option C (boolean flag)?
  3. Backward compatibility concerns? Should we require module encryption config or make it optional with smart defaults?
  4. Timeline? Can we target 72-hour turnaround for at least Solution 1?

Additional Context

This issue was discovered while implementing the Quo (OpenPhone) API integration, which uses API key authentication. The current workaround is storing the API key as access_token to get automatic encryption, but this is semantically incorrect and confusing for developers.

A proper fix would:

  1. ✅ Improve security posture for all API key integrations
  2. ✅ Reduce developer confusion about field naming
  3. ✅ Enable better encryption control for module developers
  4. ✅ Align with industry best practices for credential storage

Reporter: @lefnire (via Claude Code TDD implementation)
Assignee: @frigg-maintainers
Labels: enhancement, security, good-first-issue (for Solution 1)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestprereleaseThis change is available in a prerelease.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions