createEncryptionKeyValidationHook function

PVCacheHook createEncryptionKeyValidationHook({
  1. required String testKey,
  2. String testValue = '_pvcache_key_test',
  3. String? encryptionKey,
  4. String keyName = '_pvcache_encryption_key',
  5. Future<void> onKeyInvalid()?,
  6. int priority = -100,
})

Creates a hook that validates encryption key on first access.

Attempts to decrypt a test entry to verify key is valid. If test fails, calls onKeyInvalid callback.

NOTE: This hook accesses storage directly to avoid infinite recursion. The test entry must be created separately before using this hook.

Implementation

PVCacheHook createEncryptionKeyValidationHook({
  required String testKey,
  String testValue = '_pvcache_key_test',
  String? encryptionKey,
  String keyName = '_pvcache_encryption_key',
  Future<void> Function()? onKeyInvalid,
  int priority = -100,
}) {
  bool validated = false;

  return PVCacheHook(
    eventString: 'encryption_key_validation',
    eventFlow: EventFlow.preProcess,
    priority: priority,
    actionTypes: [ActionType.get, ActionType.put],
    hookFunction: (ctx) async {
      // Only validate once per cache instance
      if (validated) return;

      // Skip validation for the test key itself to avoid recursion
      if (ctx.resolvedKey == testKey) {
        validated = true;
        return;
      }

      // Access storage directly to avoid recursion
      final bridge = PVBridge();
      final entryDb = await bridge.getDatabaseForType(
        ctx.cache.entryStorageType,
        heavy: ctx.cache.heavy,
        env: ctx.cache.env,
      );
      final entryStore = bridge.getStore(
        ctx.cache.env,
        ctx.cache.entryStorageType,
      );

      // Read test entry directly from storage
      final encryptedTestValue = await entryStore.record(testKey).get(entryDb);

      if (encryptedTestValue == null) {
        // Test entry doesn't exist, this is first run - mark as validated
        // The test entry will be created by normal cache operations later
        validated = true;
        return;
      }

      // Read metadata to check if it's encrypted
      final metaDb = await bridge.getDatabaseForType(
        ctx.cache.metadataStorageType,
        heavy: ctx.cache.heavy,
        env: ctx.cache.env,
      );
      final metaStoreName = ctx.cache.metadataNameFunction!(ctx.cache.env);
      final metaStore = bridge.getStore(
        metaStoreName,
        ctx.cache.metadataStorageType,
      );
      final metadata = await metaStore.record(testKey).get(metaDb);

      // If test entry exists and is encrypted, try to decrypt it
      if (metadata?['_encrypted'] == true) {
        try {
          final key = encryptionKey ?? await getOrCreateEncryptionKey(keyName);
          final cipher = AESCipher(key);
          final decrypted = cipher.decryptString(encryptedTestValue as String);
          final value = jsonDecode(decrypted);

          if (value == testValue) {
            // Key is valid
            validated = true;
            return;
          }

          // Value doesn't match - key changed
          if (onKeyInvalid != null) {
            await onKeyInvalid();
          }
        } catch (e) {
          // Decryption failed - key changed
          if (onKeyInvalid != null) {
            await onKeyInvalid();
          }
        }
      }

      validated = true;
    },
  );
}