Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Migrating legacy passwords to CakePHP 3

I'm reading the Auth documentation on Cake 3's website here, and I'm still slightly confused on how to migrate my users' passwords to my new application.

It says to include the FallbackPasswordHasher class, but if that's the case, where do I place the salt for the legacy password (as it's different for the new site)? Other than that, everything seems fairly self-explanatory.

I have a few different but related sites that I'm consolidating into one website that will provide the same services across multiple businesses, so I need to import users' passwords from a variety of sites with different salts.

like image 431
Isaac Askew Avatar asked Dec 06 '14 21:12

Isaac Askew


2 Answers

It depends on the old hashing mechanism used

Depending on the mechanism and algorithm used, you may not need the old salt, as it's already attached to the hash, this is for example the case for passwords that were hashed using the Blowfish password hasher, or generally passwords hashed using crypt(), they can be validated by the Default password hasher.

So if that is the case for all your passwords, then you won't need to use the Fallback password hasher at all, but instead you can use just the Default password hasher, and rehash the passwords if necessary as shown in the docs.


Hashes without salt attached

For passwords hashed using mechanisms that do not attach the salt to the hash, like for example md5(), sha1(), mhash(), etc., you may need a custom legacy password hasher that incorporates the salt that has been used for the old passwords.

In most cases the WeakPassword hasher should do the job, which uses the old Cake 2 mechanisms and salt configuration, that is the global Security.salt option and the hashType hasher option.

See Cookbook > Authentication > Changing Hashing Algorithms

In case the weak hasher doesn't work for you because you've for example used a custom hasher in your old app, then you'll need a custom legacy hasher. An example for a legacy password hasher can be found in the docs, incorporating a salt should be as simple as for example

namespace App\Auth;

use Cake\Auth\AbstractPasswordHasher;

class LegacyPasswordHasher extends AbstractPasswordHasher {

    public function hash($password) {
        throw new \LogicException('You really should not use me!');
    }

    public function check($password, $hashedPassword) {
        // compare using the legacy salt
        return sha1($password . 'legacy-salt-here') === $hashedPassword;
    }

}

Note that in case of using the Fallback hasher there is no need for the legacy hasher to implement hash() to actually hash something, as you shouldn't use it for new hashes.


Different salts and/or algorithms per app

Having passwords originating from different sources where individual salts were used, can make things a little more complex.

There are various ways to solve this, and the most obvious one (as this is how the Fallback hasher is intended to work) would be to create a separate custom legacy hasher for every app, where the different hashers will use the appropriate salt and maybe also algorithm for the specific app.

All you need to do then is to rely on the Fallback hasher iterating over all your legacy hashers until one of them successfully validates the password.

'passwordHasher' => [
    'className' => 'Fallback',
    'hashers' => ['Default', 'LegacyApp1', 'LegacyApp2', 'LegacyApp3', /* ... */]
]

And that's all?

Basically, yes, the only problem here might be collisions, ie where the hasher for App X would successfully verify a password that would normally be evaluated as false by the hasher for App Y where the password originates from.

In case this is a scenario that may be likely for whatever reason (it should normally be rather unlikely even for the oh so weak MD5), you'd have to go for a solution that is more strict.

There are probably 10^Cake ways to solve such a problem, so since collisions should be rather unlikely, I'll stick with the in my opinion most basic one, which would be to modify the passwords before merging them so that they have an identifier added that identifies the old application, which can then be used by the legacy password hasher to choose the appropriate salt and algorithm.

An example

If for example the old password of App 1 would be

de1566cc82d9fda1ac39a28a45afe3671d9ef880

prepend app1 (using a unique separator that cannot occur in any of the old as well as the new password hashes) to make the password column contain something like

app1:::de1566cc82d9fda1ac39a28a45afe3671d9ef880

and in your legacy password hasher check for it doing something like

public function check($password, $hashedPassword) {
    if(strpos($hashedPassword, ':::') === false) {
        // this is not the password you are looking for
        return false;
    }
    // separate the identifier from the password
    list($app, $hashedPassword) = explode(':::', $hashedPassword, 2);

    // create comparision hash using app specific legacy algorithms and salts
    switch($app) {
        case 'app1':
            $compare = sha1($password . '375828236784563245364');
            break;

        case 'app2':
            $compare = md5('legacy-salt-for-app-2' . $password);
            break;,

        // etc

        default:
            return false;
    }

    return $compare === $hashedPassword;
}

As mentioned initially, there are a lot of ways to solve this problem, the one shown being only one of them, however this should give a hint what you would generally need to accomplish such a task.


ps, please note that all sample code here is untested!

like image 158
ndm Avatar answered Oct 31 '22 16:10

ndm


For anybody curious as to how this resolved: here's what I did, based on ndm's answer.

  1. Create a folder in src called 'Auth' (the same directory controller, model, etc. is in)
  2. Inside that folder, create a file called 'LegacyPasswordHasher.php'. Use code analogous to the code below:

    namespace App\Auth;
    
    use Cake\Auth\AbstractPasswordHasher;
    
    class LegacyPasswordHasher extends AbstractPasswordHasher {
    
        public function hash($password) {
            throw new \LogicException('You really should not use me!');
            // shamelessly copied from ndm's result - a 'hash' function is necessary for this class
        }
    
        public function check($password, $hashedPassword) {
            // compare using the legacy salt
            return sha1('myAwesomeAndStrangeSaltFromPreviousSite'.$password) === $hashedPassword;
        }
    
    }
    

Keep in mind that some people prepend the salt and others append the salt. Make sure you know which it is or your Legacy login won't work.

After that, go into your src/AppController.php file and add this under your public $components array:

 'authenticate' => [
      'Form' => [
           'passwordHasher' => [
                'className' => 'Fallback',
                'hashers' => ['Default', 'Legacy']
                // 'Default' is your main one - I added 'Legacy' here
                // 'Legacy is called if 'Default' fails - refers to our php file we just made
            ]
       ]
 ]

After this, I built a function that redirects the user to a password reset page if their password is expired. I imported all of the old users to my website, mass set all of their password expiration dates to years ago, then tested one out by logging in with a password encrypted using my old site's salt. It logged me in perfectly, detected my expired password, redirected me to my 'set new password' page, and after they set their new password it encrypted it using the current encryption method for CakePHP 3 (sha256, I think).

Perfect! Thanks again to StackOverflow, ndm, and the internet.

like image 31
Isaac Askew Avatar answered Oct 31 '22 15:10

Isaac Askew