/** * Migration: Add encrypted storage for private keys * * Fixes: DeviceCertificate.privateKeyPem stored in plaintext - security risk * Changes: * - Add encrypted_private_key column for AES-256-GCM encrypted storage * - Add encryption_key_id for key management * - Add encryption_algorithm field * - Mark private_key_pem as deprecated (not removed for backwards compatibility) * * Note: Actual encryption/decryption should be handled at application level. * This migration only sets up the schema for encrypted storage. * * @type {import('node-pg-migrate').ColumnDefinitions | undefined} */ export const shorthands = undefined; /** * @param pgm {import('node-pg-migrate').MigrationBuilder} * @param run {() => void | undefined} * @returns {Promise | void} */ export const up = (pgm) => { console.log('Adding encrypted private key storage...'); // Add encryption-related columns pgm.addColumns('device_certificates', { encrypted_private_key: { type: 'text', notNull: false, comment: 'AES-256-GCM encrypted private key (base64 encoded ciphertext + IV + auth tag)', }, encryption_key_id: { type: 'varchar(100)', notNull: false, comment: 'Identifier for the encryption key used (for key rotation support)', }, encryption_algorithm: { type: 'varchar(50)', notNull: false, default: 'AES-256-GCM', comment: 'Encryption algorithm used', }, encrypted_at: { type: 'timestamptz', notNull: false, comment: 'Timestamp when the private key was encrypted', }, }); // Add deprecation comment to old column pgm.sql(` COMMENT ON COLUMN device_certificates.private_key_pem IS 'DEPRECATED: Plaintext private key. Will be removed in future version. Use encrypted_private_key instead for secure storage. This column is kept for backwards compatibility during migration.'; `); // Add index on encryption_key_id for key rotation queries pgm.createIndex('device_certificates', 'encryption_key_id', { name: 'idx_device_certificates_encryption_key_id', where: 'encryption_key_id IS NOT NULL', }); // Add CHECK constraint for encryption algorithm pgm.addConstraint('device_certificates', 'chk_device_certificates_encryption_algorithm', { check: "encryption_algorithm IS NULL OR encryption_algorithm IN ('AES-256-GCM', 'AES-256-CBC', 'ChaCha20-Poly1305')", }); // Create a view that excludes private key data for safer queries pgm.sql(` CREATE OR REPLACE VIEW device_certificates_public AS SELECT id, device_id, serial_number, fingerprint, certificate_type, status, subject_dn, issuer_dn, certificate_pem, public_key_pem, key_algorithm, key_size, signature_algorithm, issued_at, expires_at, revoked_at, revocation_reason, x509_extensions, usage_count, last_used_at, renewal_notified_at, created_at, updated_at, deleted_at, -- Indicate if encrypted key exists without exposing it CASE WHEN encrypted_private_key IS NOT NULL THEN true ELSE false END AS has_encrypted_key, encryption_key_id, encryption_algorithm, encrypted_at FROM device_certificates; COMMENT ON VIEW device_certificates_public IS 'Safe view of device_certificates excluding private key data'; `); console.log('Encrypted private key storage added.'); console.log(''); console.log('IMPORTANT: To complete the migration:'); console.log('1. Update application code to use encrypted_private_key'); console.log('2. Configure encryption key in environment'); console.log('3. Run data migration to encrypt existing private keys'); console.log('4. Once verified, clear private_key_pem column'); }; /** * @param pgm {import('node-pg-migrate').MigrationBuilder} * @param run {() => void | undefined} * @returns {Promise | void} */ export const down = (pgm) => { console.log('Rolling back encrypted private key storage...'); // Drop view pgm.sql('DROP VIEW IF EXISTS device_certificates_public;'); // Drop constraint pgm.dropConstraint('device_certificates', 'chk_device_certificates_encryption_algorithm', { ifExists: true, }); // Drop index pgm.dropIndex('device_certificates', 'encryption_key_id', { name: 'idx_device_certificates_encryption_key_id', ifExists: true, }); // Drop columns pgm.dropColumns('device_certificates', [ 'encrypted_private_key', 'encryption_key_id', 'encryption_algorithm', 'encrypted_at', ]); // Restore original comment pgm.sql(` COMMENT ON COLUMN device_certificates.private_key_pem IS 'Private key in PEM format (optional, for key recovery)'; `); console.log('Encrypted private key storage rollback complete.'); };