(This issue has been usurped to address a slightly different issue now)
Problem/Motivation
Current 8.x-1.x and 7.x-1.x branches genpass_password($length) is now near identical to user_password($length) function except that it uses a potentially larger pool of source characters.
The 7.x-2.x branch has an alternative password generation algorithm which guarantees that the password will contain at least one character from each of the following character sets.
// This array contains the list of allowable characters for the
// password. Note that the number 0 and the letter 'O' have been
// removed to avoid confusion between the two. The same is true
// of 'I', 1, and 'l'.
$character_sets = array(
'lower_letters' => 'abcdefghijkmnopqrstuvwxyz',
'upper_letters' => 'ABCDEFGHJKLMNPQRSTUVWXYZ',
'digits' => '23456789',
'special' => '@#$%^&()=/|[]{};<>/',
);
https://git.drupalcode.org/project/genpass/blob/7.x-2.x/genpass.module#L84
This is a simple method of guaranteeing that the password will always have at least one character from each set, to create a more complex password.
The current code will always produce a password which is ($lengh + 4) characters long, and with the first 4 characters being one of each of the character sets in the order above.
Possible password space size is 25 x 24 x 8 x 18 x (25 + 24 + 8 + 18)^$length which is several orders of magnitude smaller that a password that is the same final length but can be any character. (Ignoring the double up of character / as this is a bug)
Due to the difference in the size of the character sets, there is not an even distribution of character use over the generation of passwords when each is forced to be included at least once. eg The digits set is normally usually has a probably of 8 / 75 = 0.10666.. of being included in each position. If it is forced to be included it has a probably of 1 of being included at least once and 0.10666 of being included for every additional character. It was much less likely to be included randomly and so ends up being more prevalent in generated passwords using this method.
The increase in length of the returned password to $length + count($character_sets) does increase the password space which depending on the length of requested password, creates a vastly higher entropy password the was asked for. It does however break the expected result of the function by not returning a password of the same length as any of the other hook_password functions.
Proposed resolution
Include this method of password generation in 8.x-1.x branch.
Update the method to make passwords that are less predictable for their first 4 characters by shuffling the password characters before returning. This should be back-ported.
In keeping with user_password($length) and the hook_password() implemented by the 8.x-1.x branch of this module, always return a password that is $length characters long. Users will need to be informed that they should increase the length of the password by N (~2) to reach the same password space size as that of a random password. This could be calculated for the default source characters.
Increase the total number of special/symbol characters that can be included as part of a password to all of those which can be entered into a password box, excepting those which look too similar to another character.
Allow developers to alter the source character sets via code in case they have some specific need to change them.
Remove the ability to set the source characters via UI.
Remaining tasks
- Remove "Generated password entropy" admin setting.
- Add hook_genpass_character_sets_alter() which is called just before password generation begins. NB: Without using random_int, the limit for any one character set length, and the combined length must not exceed 256 characters; throw an exception.
- hook_update_N update to remove defunct setting.
Wrap random_int to deal with older PHP versions.- Document hook_password in api file if not present elsewhere
User interface changes
- Admin interface has been changed; "Generated password entropy" setting is now gone.
API changes
- The
$lengthparameter is now required and expected for all implementations ofhook_password().
Data model changes
None.
Release notes snippet
@todo
Original issue summary
Hi,
thank you for this great module, it's a life saver.
Before I generate all of the passwords, I'm curious what's the difference between the two password generation algorithms provided - genpass and user?
Are both of the passwords then compatible with the new password hasing mechanism in Drupal 7, i. e. will they be transferable to Drupal 8?
| Comment | File | Size | Author |
|---|---|---|---|
| #15 | interdiff_13-15.txt | 3.57 KB | greggles |
| #15 | genpass-update-genpass-password-1811780-15-d8.patch | 15.12 KB | greggles |
| #13 | interdiff_12-13.txt | 634 bytes | elc |
| #13 | genpass-update-genpass-password-1811780-13-d8.patch | 14.48 KB | elc |
| #12 | interdiff_7-12.txt | 12.1 KB | elc |
Comments
Comment #1
gregglesThe code comments on the genpass function say 'Based on the original D6 user_password function (with more characters)' so it seems the original intention was to user more characters so it would be more secure.
The current comparison to user_password shows that genpass has a shorter default length of 8 vs. 10 (less secure) uses mt_rand (less secure) but also has a longer set of allowable characters (more secure, but also can cause some support problems if people confuse lower case L, capital I and the number one - l, I and 1)
The Drupal 7 user_password function is a bit different, so I think this is an important thing to update so that genpass_password is at least as strong as Drupal 7's function. I've updated the issue to be about that.
Comment #2
elc commentedPassword generation has been updated for both supported branches by the commits for sa-contrib-2018-042 #2982180: Address sa-contrib-2018-042 in the 8.x branch.
The 7.x-1.x branch code is now effectively based off the D7 user_password function. The 7.x-2.x branch is basically the same.
The 8.x-1.x branch code is now effectively based off the D8 user_password function.
All that remains is the comment incorrectly stating that the function is based off the D6 version. It was removed from the D7 version with commit adb924c335f1c494f9c4e53ba4e24a15164c33a5.
Attached is patch to remove it from 8.x-1.x branch. This will likely fail testing for some unknown reason, but it should be ready to go.
Comment #3
gregglesThe 7.x-2.x branch is basically the same, but a bit stronger to help satisfy password checking. For example, the 7.x-1.x branch might randomly choose all letters some of the time, which doesn't satisfy a password strength requirement that requires at least 1 number and special character. The 7.x-2.x branch will always include a number, lower case letter, upper case letter, and special character.
I think the 7.x-2.x strategy is the better one to copy for 8.x. What do you think?
Comment #4
gregglesUpdating title to reflect what I see as the goal.
Comment #5
elc commentedYup, it makes sense to use an improved password generation method for the D8 release.
I have since spent far more time looking into this than I probably should have. Random numbers and cryptography are always full of gotchas, and I ran into a pile of 'em. I've ended up re-writing the summary to include a lot more information.
I've had a look at the 7.x-2.x method of generating a password and it looks pretty good in that it guarantees to use characters from all four sets.
It does have an issue in that the first 4 characters have reduced entropy since they each is guaranteed to always use a more limited set of characters in the same order for every password. It's still quite a random password, but it's not as random a password where every character could be any one character. I've addressed this by limitation by adding a random sort (using shuffle) just prior to returning the new password. It is safe to use shuffle here because the source is already random. The increase in length does vastly over-compensate for any reduction.
The length being param + 4 is also unexpected unless someone is being really careful about reading the docs or the code. The user_password function this is meant to be compatible with (as in 8.x-1.x version where it assumes a hook of hook_password) does return a password where
$length= returned length. Min length ends up being 4 no matter what, which is again unexpected. Can't win them all.While testing the randomness of the resulting passwords, I found that the simplistic password rules of always ensuring there's a character from each set resulting in a much higher probability of a digit being included in the password. It ends up not being a truly random password, but one with a bias. With a sufficiently large length of password, this can be made insignificant. As such, the minimum length of password should probably be increased to this extent. Doing so then vastly reduces the chances of a password not including a character from each set in the first place and making a forced inclusion less needed.
The coded Character Sets removes the need for the "Password Entropy" setting. It might be worth still allowing that to be configurable (via code) if an advanced user really knew what they were doing and wanted less secure passwords. It has been an option since the module's inception which is the reason I'd have thought it might be kept as an option. Who know how many people want poor passwords? :P It's certainly easier to just not given people the option to shoot themselves in the foot. They can patch the code if they're that keen.
So, not attached is a re-working of the D8 genpass_password function which keeps track of character sets usage and if the password fails to include one of the required sets, it just re-tries. It doesn't bother to check passwords that are too short (length <= number of character sets) as this would obviously fail. Alas, after testing a few tens of millions of passwords, I found this to end up spending quite a large amount of time amount of time generating passwords all the way up to around 20 char length (maxed out at 97 attempts on a 5 char password). Even with balancing the character sets to make it equally likely to come from any of them didn't improve performance enough. That idea is dead.
Patch attached is now your method from 7.x-2.x, with a random shuffle and a change to choosing index. This is the method used by KeePassX, which does a very good job. Tested on 20 million passwords which ended up being nicely random. This seems to fulfil what is needed for a truly random password.
Should probably port the shuffle back to 7.x-2.x.
Also ended up changing the characters included in the 'special' group to all of those from the original. I don't think that should cause issues. There was also a double up of the
/character. Old is first line, new is 2nd line.@#$%^&()=/|[]{};<>/@#$%^&()=/|[]{};<>\!*+,-.:?~I haven't tested to see if any of those will cause issues other than typing them all in as a password and confirming I can log in with it.
This patch does also diverge from what Drupal Core is doing in user_password() with the looping on
$index = ord(Crypt::randomBytes(1));. With the core's source char string there this only returns a valid index around 1/5 of the time:57÷256=0.2226. This looping is done to avoid modulo bias of cutting a random number down to fit a smaller range where[rand_max] mod [range] != 0. A better method is now available inrandom_int($min, $max)[1]; this is cryptographically secure and modulo bias safe, returning an int from $min to $max inclusive. It can deal with any size of source characters which is useful if we allow people to change the size of source chars arrays. This function is available from PHP7 and also has a back-ported version to PHP 5.x in paragonie/random_compat[2] which is included in my default drupal projects (is it in everyone's?). Failing that, a locally implemented version could be included.[1] https://www.php.net/manual/en/function.random-int.php
[2] https://github.com/paragonie/random_compat
(Further review may be needed; paragonie's random_int just uses range looping too instead of bias compensation which is faster.)
In an ideal world, support for random_int would be added to Drupal\Component\Utility\Crypt. The call to randomBytes is already a wrapper around the newer PHP random functions.
Still @todo
Comment #6
gregglesWow, lots of very cool and interesting work! Thanks for the code and explanations.
That hadn't really occurred to me and it's a great point! I do think that the extra variety of characters offsets this a bit, but I agree that shuffling the characters of the resulting string is an even better solution.
One area I'm not sure about:
I'm hesitant to deviate from what core does. I think it would be good to get those changes into core first and then bring them over here. Core gets way more reviews (both as part of committing and from security researchers) so it would be good for this module to "do what core does" to choose a random string to benefit from that research.
Comment #7
elc commentedAgreed about random index generation matching core. Alas I don't have the drive to go push a better solution into core. It's not called constantly so a few more loops while picking a random number inside the index isn't harming anything. I'm sure a modulo bias safe and fast random_int would be quite useful to be added to core.
Attached is the version that essentially just does the following
$lengthadmin setting intogenpass_generate()so it is used for all modules implementing hook_passwordStill @todo everything in the summary.
Added to summary:
- Need to throw an exception if altered character_sets (from hook_genpass_character_sets_alter) has a single set length or combined length which exceeds 256 characters as this is outside the bounds of a single random byte for the random index. random_int call avoided this problem as it calculated how many random bytes it needed based on the range.
Comment #8
rajab natshahAgreed on the elements
Thanks for all your work
Could we support elements from Password Policy if we want to have them both in the same site.
To ignore password policy rules when we generate, but let it take over after that
or obey its rules when we generate
The current status, we could have number of right generated passwords, but some time the form want save as it did not pass the password policy.
Comment #9
greggles@RajabNatshah have you tried the logic in this patch? It should help to always pass basically any password policy since it's longer and has more variation in the kinds of characters that are included.
Comment #10
rajab natshahThank you Greg for following up
We had a random failed cases 1 in 10 when we
Issue #3060823: Change Automated Functional Acceptance Testing to work with changes after enabling [Generate Password] module in the 8.x-7.x branch
https://git.drupalcode.org/project/varbase/commit/c1b959f
The robot test will have issues
https://travis-ci.org/Vardot/varbase/builds/549324872
Then we had to go back to
Issue #3060823: Revert [ Feature: Create default testing users ] Automated Functional Acceptance Testing to the old normal way
https://git.drupalcode.org/project/varbase/commit/518abea
Then it passed https://travis-ci.org/Vardot/varbase/builds/553735926
Sure we should test with the new patch
to full test we could run about 10 to 20 times to make sure it will not fail in random cases
Comment #11
rajab natshahI hope that we could move foreword with this issue
#10
Comment #12
elc commentedBeen a bit silent on this project sorry - I now have a little sprog who has taken over my life.
It appears that more recent discussion in here has diverged off just fixing up password generation to possible integration with password policy module. I think this should be forked off into a separate issue as that's quite a bit of unrelated code. There is actually already an issue opened on that front: #1964974: Integrate with Password Policy module so the generated password meets policy requirement
Attached is a patch to do all of the remaining tasks on this issue in addition to path #7.
- Remove genpass_entropy ui, test and config elements
- hook_update_N to remove the configuration item from existing installs
- Add genpass.api.php describing hook_password and hook_genpass_character_sets_alter
- Move settings into their own details fieldset to separate them out from "registration and cancellation"
- Implement genpass_character_sets alter to allow other modules to change the character_sets if they're really really keen
- Throw InvalidCharacterSetsException when resulting character_set doesn't quite make it
Provided this works for people, I think this fix is complete.
Comment #13
elc commentedNeglected to also sanity check the maximum length of altered character sets. The length cannot exceed the possible range of random indexes provided by Crypt::randomBytes(1).
Comment #14
gregglesThanks for your continued work on this, ELC. And congratulations on the sprog :)
Here are some minor bits of feedback and questions. I'd be happy to reroll with these changes included or have you do that. Just say the word if you'd like me to do it.
In InvalidCharacterSetsException.php:
The package seems like some copy/paste that could be removed.
In genpass.module:
I think moving this to its own fieldset makes sense. I'm not sure the word Genpass is used in user-facing text, much though. On the modules page the name of the module is shown as "Generate Password" so maybe "Generate Password Configuration" though that sounds pretty awkward. What do you think about calling this "Password Generation Configuration" or something similar to that?
It seems Crypt::randomBytes is deprecated and for a module that runs only on php7 we should use random_bytes(). What do you think?
I also noticed that the core user_password function is now a bit different:
We could move to that style of generation to "do what core does" and also remove a deprecated function.
Comment #15
gregglesOK, here's an updated patch with the changes I suggested in #14 and I made 3 other adjustments:
* Using random_int means that the length of the character sets can't (reasonably) exceed the max int returned, so we can stop throwing that exception.
* I updated the test to use BrowserTestBase as we aren't testing any javascript and it's easier and faster to run BrowserTestBase
* In running the tests I noticed some deprecation issues related to the theme so I fixed those as well.
I think this could be ready to commit and then we'd be ready for the 8.x-1.0 release!
Let me know what you think about these changes.
Comment #16
elc commentedThanks for re-rolling it and fixing it up.
Ironically, the call to random_int was what I was talking about in #5. Core is using the pretty much exactly the code I wrote. I'd patched it in and then removed it after #6 :P It's good to see that core has updated to use this crypto secure function, and good that we're using it here now. Heck, we could even update the subject.
The length exception is now quite irrelevant so good riddance. It was an ugly hack to deal with the lack of random_int in a weird way anyway.
I did want to call the settings group something less crap. The developer in me got in the way and wanted people to know the settings were from a particular module. The front end user in me wants to know what the settings are actually for. So yes, the name of the fieldgroup is much better.
Looks good to me.
Comment #18
gregglesThanks for the feedback and review.
I totally get how it probably feels a bit circular for us to have arrived back at random_int - you were right before, clearly. I'm just always hesitant for a module to make a move faster than core given how many more people review core changes than they do in a random contrib.
Committed/pushed now.
I also created a new release and will likely make an 8.x-1.0 in the next few weeks.