Skip to main content

Overview

Key rotation is a critical security practice that limits the lifetime of encryption keys. The Key Management Service implements automated key rotation to ensure that compromised keys have minimal impact and that encryption remains secure over time.
By default, encryption keys expire after 30 days and are retained for 90 days after expiration before permanent deletion.

Why Key Rotation Matters

Regular key rotation provides several security benefits:
  • Limits exposure window: If a key is compromised, only data encrypted during that key’s lifetime is at risk
  • Reduces crypto-analytic attacks: Limiting the amount of data encrypted with a single key makes cryptanalysis harder
  • Enables compliance: Many security standards require regular key rotation
  • Detects compromise: Forced rotation can reveal if attackers are using stolen keys
  • Supports forward secrecy: Old encrypted data cannot be decrypted even if current keys are compromised

Expiration Policies

Configurable Rotation Period

The service uses environment-based configuration for rotation intervals:
this.keyRotationIntervalDays = this.configService.get(
  'ENCRYPTION_KEY_ROTATION_DAYS',
  30,  // Default: 30 days
);
Reference: encryption.service.ts:24-27

Key Expiration Calculation

When a new key is generated, its expiration date is automatically calculated:
const expirationDate = new Date();
expirationDate.setDate(
  expirationDate.getDate() + this.keyRotationIntervalDays,
);

await this.prismaService.clientEncryptionKey.create({
  data: {
    id: keyId,
    deviceId,
    publicKey,
    privateKey,
    expiresAt: expirationDate,  // Automatic expiration
    createdAt: new Date(),
  },
});
Reference: encryption.service.ts:63-78

Automated Rotation Task

Daily Rotation Schedule

The service runs an automated task every day at midnight to manage key lifecycle:
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async handleKeyRotation() {
  this.logger.log('Running scheduled key rotation task');
  
  // 1. Mark expired keys as deprecated
  // 2. Delete old deprecated keys
  // 3. Identify and deprecate problematic keys
}
Reference: key-rotation.tasks.ts:27-29

Rotation Task Operations

The task identifies all non-deprecated keys that have passed their expiration date:
const expirationDate = new Date();
const deprecatedCount = await this.prismaService.clientEncryptionKey.updateMany({
  where: {
    deprecated: false,
    expiresAt: {
      lt: expirationDate,  // Less than current date
    },
  },
  data: {
    deprecated: true,
    updatedAt: new Date(),
  },
});

this.logger.log(
  `Marked ${deprecatedCount.count} keys as deprecated due to expiration`
);
Reference: key-rotation.tasks.ts:33-50
Keys that have been deprecated for longer than the retention period are permanently deleted:
this.keyRetentionDays = this.configService.get(
  'ENCRYPTION_KEY_RETENTION_DAYS',
  90,  // Default: 90 days after deprecation
);

const deleteThreshold = new Date();
deleteThreshold.setDate(
  deleteThreshold.getDate() - this.keyRetentionDays,
);

const deletedCount = await this.prismaService.clientEncryptionKey.deleteMany({
  where: {
    deprecated: true,
    expiresAt: {
      lt: deleteThreshold,
    },
  },
});

this.logger.log(
  `Deleted ${deletedCount.count} deprecated keys older than ${this.keyRetentionDays} days`
);
Reference: key-rotation.tasks.ts:52-70
The service monitors encryption operations and identifies keys with high failure rates:
const problematicKeys = await this.monitoringService.findProblematicKeys(
  15  // 15% failure threshold
);

if (problematicKeys.length > 0) {
  await this.prismaService.clientEncryptionKey.updateMany({
    where: {
      id: {
        in: problematicKeys,
      },
    },
    data: {
      deprecated: true,
      updatedAt: new Date(),
    },
  });
  
  this.logger.log(
    `Marked ${problematicKeys.length} problematic keys as deprecated`
  );
}
This proactive rotation helps maintain service reliability and security.Reference: key-rotation.tasks.ts:73-96

Manual Key Rotation

Rotating Keys for a Specific Device

The service provides an API to manually rotate keys for a specific device:
async rotateKeysForDevice(
  deviceId: string,
): Promise<{ publicKey: string; keyId: string }> {
  // Mark existing keys as deprecated
  await this.prismaService.clientEncryptionKey.updateMany({
    where: {
      deviceId,
      deprecated: false,
    },
    data: {
      deprecated: true,
      updatedAt: new Date(),
    },
  });
  
  // Generate new key
  return this.generateClientEncryptionKey(deviceId);
}
Reference: encryption.service.ts:185-210

GraphQL Mutation

Clients can trigger manual rotation through the GraphQL API:
mutation RotateKey {
  rotateClientEncryptionKey(input: {
    deviceId: "device-123"
    appVersion: "1.0.0"
  }) {
    publicKey
    keyId
  }
}
Reference: encryption.resolver.ts:66-93

Key Lifecycle States

Key Reuse Strategy

Avoiding Unnecessary Key Generation

When a client requests an encryption key, the service first checks for existing valid keys:
// Check for existing valid keys first
const existingKey = await this.getValidKeyForDevice(deviceId);
if (existingKey) {
  this.logger.log(
    `Using existing valid key for device: ${deviceId}, keyId: ${existingKey.id}`
  );
  return {
    publicKey: existingKey.publicKey,
    keyId: existingKey.id,
  };
}

// Only generate new key if no valid key exists
const { publicKey, privateKey } = await generateKeyPair();
Reference: encryption.service.ts:44-57

Valid Key Query

The service queries for non-deprecated, non-expired keys:
private async getValidKeyForDevice(
  deviceId: string,
): Promise<{ id: string; publicKey: string } | null> {
  const validKey = await this.prismaService.clientEncryptionKey.findFirst({
    where: {
      deviceId,
      deprecated: false,
      expiresAt: {
        gt: new Date(),  // Greater than current date
      },
    },
    orderBy: {
      createdAt: 'desc',  // Most recent first
    },
  });
  
  return validKey ? { id: validKey.id, publicKey: validKey.publicKey } : null;
}
Reference: encryption.service.ts:259-276

Monitoring Key Rotation

Failure Rate Analysis

The monitoring service analyzes decryption failures to identify problematic keys:
async findProblematicKeys(
  failureThresholdPercent: number = 10,
): Promise<string[]> {
  const problematicKeys: any = await this.prismaService.$queryRaw`
    WITH key_stats AS (
      SELECT 
        key_id,
        SUM(CASE WHEN status = 'failure' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as failure_rate,
        COUNT(*) as total_operations
      FROM "EncryptionMetric"
      WHERE key_id IS NOT NULL AND operation = 'decrypt'
      GROUP BY key_id
      HAVING COUNT(*) >= 5  -- Minimum operations threshold
    )
    SELECT key_id
    FROM key_stats
    WHERE failure_rate >= ${failureThresholdPercent}
    ORDER BY failure_rate DESC, total_operations DESC
  `;
  
  return problematicKeys.map((k) => k.key_id);
}
Reference: encryption-monitoring.service.ts:112-141
Keys with a failure rate of 15% or higher are automatically deprecated during the daily rotation task.

Configuration Options

Environment VariableDefaultDescription
ENCRYPTION_KEY_ROTATION_DAYS30Days until a key expires
ENCRYPTION_KEY_RETENTION_DAYS90Days to retain deprecated keys before deletion

Example Configuration

# .env file
ENCRYPTION_KEY_ROTATION_DAYS=30
ENCRYPTION_KEY_RETENTION_DAYS=90

Best Practices

Regular Rotation

Use the default 30-day rotation period unless compliance requires a different interval

Grace Period

Maintain a retention period to handle delayed requests with expired keys

Monitor Failures

Track key failure rates to identify potential security issues early

Document Rotation

Keep audit logs of all rotation events for compliance and troubleshooting

Encryption Keys

Learn about RSA key generation and usage

Security Measures

Explore rate limiting and validation features

Build docs developers (and LLMs) love