Learn how to upgrade password security in legacy Symfony 1.4 projects by migrating from outdated SHA-1 hashing to bcrypt or Argon2.

Migrating sfGuardPlugin from SHA1 to bcrypt or Argon2 in Symfony 1.4

What was once considered strong encryption might now be outdated and vulnerable. Password security has evolved significantly over the past decade, so if you're the maintainer of a legacy project, you definitely need to pay attention to the security of your applications. In a Symfony 1.4 project, many developers relied on the sfGuardPlugin, which by default used SHA-1 for password hashing. While it was sufficient in its time, modern standards have shown that SHA1 is no longer secure against brute-force attacks and should be replaced with more robust algorithms.

Migrating from SHA1 to bcrypt or Argon2 is not just a best practice; it’s a necessity to ensure the protection of user data. Both algorithms are designed to resist modern attack vectors, with bcrypt offering proven reliability and Argon2 providing state-of-the-art memory-hard defenses. In this article, I’ll walk you through the steps required to upgrade sfGuardPlugin from SHA1 to bcrypt or Argon2 in Symfony 1.4, highlighting the necessary changes for a smooth migration that does not disrupt your existing users.

A. If you're still using Symfony 1.4 with PHP 5.6 (use bcrypt)

If you’re using Symfony 1.4 with PHP 5.6, you can still implement secure password hashing by integrating bcrypt. This means you can replace old sha1 or md5 password checks with bcrypt, ensuring stronger protection against brute-force attacks. By doing so, your legacy project gains modern password security without requiring a full framework upgrade.

A.1. Why bcrypt works and why it’s safer than salt + SHA-1

Bcrypt is a password hashing algorithm specifically designed for security. Unlike general-purpose hash functions such as SHA-1, bcrypt is computationally expensive and includes a built-in, automatic salt. When a password is hashed with bcrypt, it generates a random salt and applies a slow, adaptive key stretching function. This makes each hash unique, even if two users choose the same password, and allows the cost (work factor) to be increased over time as hardware gets faster.

In contrast, simply combining a salt with SHA-1 is much weaker. SHA-1 is designed for speed, so attackers with modern hardware (like GPUs or ASICs) can test billions of guesses per second, even with salts. Bcrypt is safer for storing passwords because it’s slow on purpose, making brute-force attacks harder, and it automatically adds a salt to each password so attackers can’t use precomputed hashes.

To proceed with the upgrade, you will need to modify your sfGuardUser class, which should be located at lib/model/doctrine/sfDoctrineGuardPlugin/sfGuardUser.class.php. In this class, you will need to implement 2 new methods and a single constant:

<?php

/**
 * sfGuardUser
 * 
 * This class has been auto-generated by the Doctrine ORM Framework
 */
class sfGuardUser extends PluginsfGuardUser
{
    /**
     * Define the cost for bcrypt hashing.
     * 
     * @var int
     */
    const BCRYPT_COST = 12;

    /**
     * Define the password for the user using bcrypt.
     *
     * @param $plain
     * @return void
     * @throws Doctrine_Exception
     * @throws Doctrine_Table_Exception
     */
    public function setPassword($plain)
    {
        $hash = password_hash($plain, PASSWORD_BCRYPT, ['cost' => self::BCRYPT_COST]);
        $this->_set('password', $hash);
        $this->_set('algorithm', 'bcrypt');
        $this->_set('salt', null);
    }

    /**
     * Verifies the user's password.
     * If the password is in the old format (sha1 with salt), it updates it to bcrypt.
     *
     * @param string $plain
     * @return bool
     * @throws Doctrine_Exception
     * @throws Doctrine_Table_Exception
     */
    public function checkPassword($plain)
    {
        if ($this->getAlgorithm() === 'bcrypt') {
            $ok = password_verify($plain, $this->getPassword());

            if ($ok && password_needs_rehash($this->getPassword(), PASSWORD_BCRYPT, ['cost' => self::BCRYPT_COST])) {
                $this->setPassword($plain);
                $this->save();
            }

            return $ok;
        }

        // Verifies the old password (sha1 with salt)
        $legacyHash = sha1($this->getSalt() . $plain);
        if (hash_equals($this->getPassword(), $legacyHash)) {
            // Updates to bcrypt
            $this->setPassword($plain);
            $this->save();
            return true;
        }

        return false;
    }
}

This change will introduce a migration of password handling in your Symfony 1.4 project’s sfGuardUser model (with the Doctrine version).

  • The new constant for bcrypt cost defines the computational cost factor (work factor) for bcrypt.
  • The new setPassword($plain) method replaces the legacy hashing system with PHP’s password_hash function, using the bcrypt algorithm. When a password is set, it stores the bcrypt hash, marks the algorithm as "bcrypt", and eliminates the need for a separate salt, as bcrypt handles salting internally.
  • The checkPassword($plain) method is also updated to support both bcrypt and the old SHA1 + salt scheme. If the user’s password is already in bcrypt format, it verifies it with password_verify and, if necessary, rehashes it with the updated cost. If the password is still in the legacy format, it validates it using the old sha1(salt + plain) approach, and upon success, automatically upgrades and saves it as a bcrypt hash. In both cases, the method ensures backward compatibility while progressively migrating users to the more secure bcrypt system, returning true only if the provided password is correct.

B. If you're still using Symfony 1.4 with PHP >=7.2 (use Argon2)

If you’re using Symfony 1.4 with PHP 7.2, you can implement modern password hashing by integrating Argon2, which is supported natively in this version of PHP. This allows you to replace old sha1 or md5 password checks with Argon2, providing stronger resistance against brute-force and GPU-based attacks compared to bcrypt, but it's only available from PHP 7.2 or greater.

To proceed with the upgrade, you will need to modify your sfGuardUser class, which should be located at lib/model/doctrine/sfDoctrineGuardPlugin/sfGuardUser.class.php:

<?php

/**
 * sfGuardUser
 *
 * Extends the default sfGuardUser with modern password hashing support.
 * Supports Argon2id (preferred), falls back to bcrypt if Argon2 is unavailable.
 * Transparently upgrades legacy SHA1+salt hashes to Argon2id/bcrypt on login.
 */
class sfGuardUser extends PluginsfGuardUser
{
    /**
     * Bcrypt fallback cost (used only if Argon2id is not available).
     *
     * @var int
     */
    const BCRYPT_COST = 12;

    /**
     * Argon2id tuning parameters.
     *
     * @return array<string,int> Options for memory, time, and threads.
     */
    protected function getArgon2Options(): array
    {
        return [
            'memory_cost' => 131072, // ~128 MiB
            'time_cost'   => 4,
            'threads'     => 2,
        ];
    }

    /**
     * Checks if Argon2id is available in this PHP build.
     *
     * @return bool True if Argon2id is supported.
     */
    protected function argon2idAvailable(): bool
    {
        return in_array('argon2id', password_algos(), true);
    }

    /**
     * Hashes a password using Argon2id if available, otherwise bcrypt.
     *
     * @param string $plain The plain-text password.
     * @return string The hashed password.
     */
    protected function hashPassword(string $plain): string
    {
        if ($this->argon2idAvailable()) {
            return password_hash($plain, PASSWORD_ARGON2ID, $this->getArgon2Options());
        }

        return password_hash($plain, PASSWORD_BCRYPT, ['cost' => self::BCRYPT_COST]);
    }

    /**
     * Determines if a stored hash needs rehashing according to current options.
     *
     * @param string $hash The stored hash.
     * @return bool True if the hash should be upgraded.
     */
    protected function needsRehash(string $hash): bool
    {
        if ($this->argon2idAvailable()) {
            return password_needs_rehash($hash, PASSWORD_ARGON2ID, $this->getArgon2Options());
        }

        return password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => self::BCRYPT_COST]);
    }

    /**
     * Sets the password for the user.
     * Uses Argon2id when available, otherwise bcrypt.
     *
     * @param string $plain The plain-text password.
     * @return void
     * @throws Doctrine_Exception
     * @throws Doctrine_Table_Exception
     */
    public function setPassword($plain)
    {
        $hash = $this->hashPassword($plain);
        $this->_set('password', $hash);
        $this->_set('algorithm', $this->argon2idAvailable() ? 'argon2id' : 'bcrypt');
        $this->_set('salt', null); // modern algorithms handle salts internally
    }

    /**
     * Verifies a plain-text password against the stored hash.
     * - If Argon2id/bcrypt is stored, verifies with password_verify.
     * - If legacy SHA1+salt is stored, verifies and upgrades on success.
     * - On success, automatically rehashes and upgrades if needed.
     *
     * @param string $plain The plain-text password.
     * @return bool True if the password is valid, false otherwise.
     * @throws Doctrine_Exception
     * @throws Doctrine_Table_Exception
     */
    public function checkPassword($plain)
    {
        $algo = $this->getAlgorithm();
        $stored = $this->getPassword();

        // Modern hashes (Argon2id or bcrypt)
        if ($algo === 'argon2id' || $algo === 'bcrypt' || str_starts_with($stored, '$2y$') || str_starts_with($stored, '$argon2')) {
            $ok = password_verify($plain, $stored);

            if ($ok && $this->needsRehash($stored)) {
                $this->setPassword($plain); // upgrade to current preferred algorithm/params
                $this->save();
            }
            return $ok;
        }

        // Legacy SHA1+salt
        $legacyHash = sha1($this->getSalt() . $plain);
        if (hash_equals($stored, $legacyHash)) {
            $this->setPassword($plain); // upgrade to Argon2id/bcrypt
            $this->save();
            return true;
        }

        return false;
    }
}

Conclusions

By applying any of those changes to your old Symfony 1.4 project, you will immediately improve the security of your application and current users won't be affected at all:

  • Security improvement: bcrypt is much stronger than SHA1 with salt, resistant to brute-force attacks.
  • Backward compatibility: Users with old SHA1+salt passwords can still log in. On the first successful login, their password is migrated automatically to bcrypt.
  • Future-proofing: By checking password_needs_rehash, the system can seamlessly upgrade bcrypt hashes if you decide to increase the cost factor later.

Happy coding ❤️!


Senior Software Engineer at Software Medico. Interested in programming since he was 14 years old, Carlos is a self-taught programmer and founder and author of most of the articles at Our Code World.

Sponsors