onlymega's avatar

Migrate to Laravel with old hashed user passwords.

I am trying to migrate an old PHP project to Laravel where user passwords were stored using MCRYPT_RIJNDAEL_128 cipher and a 43 symbols key (which is not supported in php 5.6 that is required for the latest Laravel version). What is the best way to keep existing user passwords while migrating to Laravel?

0 likes
13 replies
ohffs's avatar

If you're looking to move the project forward then you might be best doing something like checking the password against the current rijndael encrypted - if it matches re-encrypt it as bcrypt and save the new password - maybe with a field of 'has_migrated' on the user set so you can skip the check next time?

1 like
martinbean's avatar

@onlymega Similarly to what @ohffs says, have a null password column, and the original passwords in an legacy_password column or similar. When a user logs in, check using the old hashing algorithm; if there’s a match re-hash it using the new algorithm and save it to the database.

I’d also set a timeframe for deprecating the old system. Say a couple of months or so. If no one’s logged into your application in that time then they’re not a frequently active user, so you could either send a mail-shot to those who haven’t logged in saying they’ll need to reset their password to use the new system. Or leave it, as they’ll probably use the reset password facility off their own bat if they can’t log in.

MikeHopley's avatar

I had a similar problem when porting my (spaghetti-code!) website to Laravel. All my users' passwords used MD5 hashes (shudder), and Laravel uses Bcrypt.

My solution involves extending Laravel's hashing class:

<?php namespace BadmintonBible\Core\Extensions;

use BadmintonBible\Users\Models\User;

class TransitionalHasher extends \Illuminate\Hashing\BcryptHasher {

    public function check($value, $hashedValue, array $options = array())
    {
        // If check fails, is it an old MD5 hash?
        if ( !password_verify($value, $hashedValue) )
        {
            $user = User::wherePassword( md5($value) )->first();

            if ($user)  // We found a user with a matching MD5 hash
            {
                // Update the password to Laravel's Bcrypt hash
                // If two users have matching passwords, we might update the
                // wrong user -- but it doesn't matter!
                $user->password = \Hash::make($value);
                $user->save();

                // Log in the user
                return true;
            }
        }

        return password_verify($value, $hashedValue);
    }

}

Then bind this to the Hash:: facade:

<?php namespace BadmintonBible\Core\Extensions;

class TransitionalHashProvider extends \Illuminate\Hashing\HashServiceProvider {

    public function boot()
    {
        \App::bind('hash', function()
        {
            return new TransitionalHasher;
        });

        parent::boot();
    }

}

...and add this to your list of service providers. Now all calls Hash::check will use the extended code, which also checks against the old hashing method and rehashes the password if needed.

5 likes
ohffs's avatar

@MikeHopley ah, I remember when we all thought we were so l33t using md5 ;-) Innocent days... ;-)

2 likes
MikeHopley's avatar

Yeah, kinda scary to see I could crack a lot of my users' passwords using a simple online tool D:

ohffs's avatar

@MikeHopley I remember many years ago running john the ripper on a NIS password table (hello crypt() 8-char limit!) and finding the the most common password for my users was a variation on the word 'panties'. Enlightening, and yet depressing ;-)

This is possibly getting a little off-topic mind you ;-)

1 like
cbj4074's avatar
cbj4074
Best Answer
Level 2

There is already some sound advice here (and cheers for the actual code-snippet, @MikeHopley ), but I'd like to underscore a couple of crucial points that are not Laravel-specific (even if that means they are slightly off-topic).

1.) At no time should the old hash be stored in a separate column (even if the plan is to destroy it within some reasonable time-frame); doing so introduces significant risk without justification. This is exactly how Ashley Madison passwords were cracked ( http://cynosureprime.blogspot.com/2015/09/how-we-cracked-millions-of-ashley.html ):

The recommended approach is to store the algorithm along with the hash e.g. MD5:hash:salt or bcrypt12:hash:salt. This allows you to easily identify what algorithm to use on a per-user basis. When you deem an encryption strategy obsolete you can still protect your existing users by wrapping their existing hash in a new algorithm; in this case that would be bcrypt-ing the existing md5 hashes and storing something like md52bcrypt:hash:salt.

2.) In an ideal implementation (and I haven't done the research required to know if Laravel qualifies), there is no need to have a separate has_migrated field, or similar. This is for two reasons: a) this type of migration should be an ongoing process that happens every time that significant risk against a given hashing algorithm emerges publicly, thereby rendering such a column illogical, and b) it is redundant, because in an ideal implementation, the algorithms that have been used to compute a hash are embedded in the stored value.

3.) There is no need to send out an email blast at any point, because in an ideal implementation, every user's password is wrapped in the newest/strongest hashing algorithm, even if there are several hashing "layers" for any given user's password. One might visualize this strategy with the following pseudo-code: scrypt(bcrypt(sha1(md5('theuserspassword')))).

One of the more comprehensive road-maps that I've seen regarding this subject may be found at:

@uther_bendragon/sustainable-password-hashing-8c6bd5de3844" target="_blank">https://medium.com/@uther_bendragon/sustainable-password-hashing-8c6bd5de3844

Stay safe out there! ;)

3 likes
Rocky's avatar

How would i implement @MikeHopley s solution in Laravel 5?

I've created app/Classes/MD5Hasher.php

<?php

namespace App\Classes;

class MD5Hasher extends \Illuminate\Hashing\BcryptHasher {

    public function check($value, $hashedValue, array $options = array())
    {
        // If check fails, is it an old MD5 hash?
        if ( !password_verify($value, $hashedValue) )
        {
            $user = User::wherePassword( md5($value) )->first();

            if ($user)  // We found a user with a matching MD5 hash
            {
                // Update the password to Laravel's Bcrypt hash
                // If two users have matching passwords, we might update the
                // wrong user -- but it doesn't matter!
                $user->password = \Hash::make($value);
                $user->save();

                // Log in the user
                return true;
            }
        }

        return password_verify($value, $hashedValue);
    }

}

and

app/Providers/MD5HashProvider.php

<?php

namespace App\Providers;

class MD5HashProvider extends \Illuminate\Hashing\HashServiceProvider {

    public function boot()
    {
        \App::bind('hash', function()
        {
            return new MD5Hasher;
        });

        parent::boot();
    }

}

but when accessing the page again it shows me the following error:

FatalErrorException in MD5HashProvider.php line 14:
Call to undefined method Illuminate\Hashing\HashServiceProvider::boot()

Is there a better way to check for md5 passwords

MikeHopley's avatar

Try removing the line parent::boot().

Back then I needed to call boot() on Illuminate\Support\ServiceProvider. I think this was necessary to get the binding registered. I can't remember exactly why.

Since then the boot() method has been removed, hence the error.

I didn't notice the change, since I already removed my TransitionalHasher. It was, after all, transitional. ;)

elbakly's avatar

@MikeHopley This is a refactor for duplicate passwords

<?php namespace App;

use App\User;
use Illuminate\Hashing\BcryptHasher;

class TransitionalHasher extends BcryptHasher {

    public function check($value, $hashedValue, array $options = array())
    {

        // If check fails, is it an old SHA1 hash?
        if ( !password_verify($value, $hashedValue) )
        {
            // Update the password to Laravel's Bcrypt hash
            // If more than one users have matching passwords, we will update the
            // other users as well 
            User::wherePassword( sha1($value) )
                      ->update(['password'=>\Hash::make($value)]);

        }

        return password_verify($value, $hashedValue);
    }

}
MikeHopley's avatar

@elbakly Thanks, that looks a little cleaner than my code and updates all users. Although sha1() and md5() produce different hashes, so which one you use would depend on which was originally used.

But I no longer need this stuff, as all my users have been migrated to bcrypt. :)

Please or to participate in this conversation.