migrateLegacyPasswordHashes static method

Future<int> migrateLegacyPasswordHashes(
  1. Session session, {
  2. int batchSize = 100,
  3. int? maxMigratedEntries,
})

Migrates legacy password hashes to the latest hash algorithm.

batchSize is the number of entries to migrate in each batch.

maxMigratedEntries is the maximum number of entries that will be migrated. If null, all entries in the database will be migrated.

Returns the number of migrated entries.

Warning: This migration method is designed for password hashes generated by the framework's default algorithm. Hashes stored with a custom generator or different algorithm may produce unexpected results.

Implementation

static Future<int> migrateLegacyPasswordHashes(
  Session session, {
  int batchSize = 100,
  int? maxMigratedEntries,
}) async {
  if (AuthConfig.current.passwordHashGenerator.hashCode !=
          defaultGeneratePasswordHash.hashCode ||
      AuthConfig.current.passwordHashValidator.hashCode !=
          defaultValidatePasswordHash.hashCode) {
    throw Exception(
        'Legacy password hash migration not supported when using custom password hash algorithm.');
  }
  var updatedEntries = 0;
  int lastEntryId = 0;

  while (true) {
    var entries = await EmailAuth.db.find(
      session,
      where: (t) => t.hash.notLike(r'%$%') & (t.id > lastEntryId),
      orderBy: (t) => t.id,
      limit: batchSize,
    );

    if (entries.isEmpty) {
      return updatedEntries;
    }

    if (maxMigratedEntries != null) {
      if (maxMigratedEntries == updatedEntries) {
        return updatedEntries;
      }

      var entrySurplus =
          (updatedEntries + entries.length) - maxMigratedEntries;
      if (entrySurplus > 0) {
        entries = entries.sublist(0, entries.length - entrySurplus);
      }
    }

    lastEntryId = entries.last.id!;

    var migratedEntries = await Future.wait(entries.where((entry) {
      try {
        return PasswordHash(
          entry.hash,
          legacySalt: EmailSecrets.legacySalt,
        ).isLegacyHash();
      } catch (e) {
        session.log(
          'Error when checking if hash is legacy: $e',
          level: LogLevel.error,
        );
        return false;
      }
    }).map((entry) async {
      return entry.copyWith(
        hash: await PasswordHash.migratedLegacyToArgon2idHash(
          entry.hash,
          legacySalt: EmailSecrets.legacySalt,
          pepper: EmailSecrets.pepper,
          allowUnsecureRandom: AuthConfig.current.allowUnsecureRandom,
        ),
      );
    }).toList());

    try {
      await EmailAuth.db.update(session, migratedEntries);
      updatedEntries += migratedEntries.length;
    } catch (e) {
      session.log(
        'Failed to update migrated entries: $e',
        level: LogLevel.error,
      );
    }
  }
}