Skip to content

Conversation

bwesterb
Copy link
Contributor

argon2 is the winner of the Password Hashing Competition.

  1. There are two python bindings available for argon2: argon2 and argon2-cffi. The first uses ctypes and the latter cffi. As cffi works for most environments, I choose that one.
  2. argon2 has four parameters: time_cost, memory_cost, parallelism and type. I choose to copy the defaults of argon2-cffi. Section 9 of the argon2 specification discusses recommended parameters.

https://code.djangoproject.com/ticket/26033#ticket

@timgraham
Copy link
Member

Thanks for the patch. Design decisions (such as the choice of what algorithms to support in core) are usually made on the django-developers mailing list. I would post this idea there to get feedback.

@bwesterb
Copy link
Contributor Author

Ok thanks. Post.

@hynek
Copy link

hynek commented Jan 2, 2016

Since Florian mentioned he’d like some feedback from me or @dstufft on the mailing list:

Sadly I can’t really comment on the Django portions but the usage of Argon2 libraries is rather straight forward as long as you stay away from the argon2.low_level.core() binding. The authors mention in the paper that no parameter value is actually insecure.

I have two minor general comments:

  • I wouldn’t test with a time cost of 8. :) If you want to check whether arguments get propagated, I would recommend to go down to 1 and not up lest you wanna slow down your test suite.
  • If you want to keep using argon2-cffi’s defaults, you can also just use the relevant constants.

@bwesterb
Copy link
Contributor Author

bwesterb commented Jan 3, 2016

  • I decreased the time cost for the test from 8 to 1 as suggested by @hynek.
  • I am ok with using argon2 constants directly. However, this would be different from the other PasswordHashers that hardcode their default. We can always change this after it has been merged.

This PR is ready to be merged.

@apollo13
Copy link
Member

apollo13 commented Jan 3, 2016

Is there a specific reason to go with the low_level api?
Edit:// Nevermind, looks as if the highlevel API doesn't allow custom salts…

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use argon2.DEFAULT_HASH_LENGTH here,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. We do have to be careful: if argon2.DEFAULT_HASH_LENGTH is increased significantly in the future, encode can return a string longer than the maximum 128.

@bwesterb
Copy link
Contributor Author

bwesterb commented Jan 4, 2016

@apollo13 argon2-cffi's high-level API (argon2.PasswordHasher) does not allow one to specify a salt (on purpose). It generates a new random salt itself. As Django's API wants to generate a salt itself, we use the lower-level API.

@timgraham timgraham changed the title Add argon2 password hasher Fixed #26033 -- Added argon2 password hasher. Jan 4, 2016
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use 4 space hanging indent:

argon2.low_level.hash_secret(
    force_bytes(password),
    ...,
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@bwesterb
Copy link
Contributor Author

bwesterb commented Jan 7, 2016

@timgraham I saw you set the "missing documentation" label on the issue tracker. Could you help me out and tell me which documentation I should write?

@timgraham
Copy link
Member

docs/topics/auth/passwords.txt -- see the similar docs for bcrypt there. Also a mention in the 1.10 release notes. p.s. You can squash commits too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, please use hanging indent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@bwesterb
Copy link
Contributor Author

bwesterb commented Jan 7, 2016

@timgraham Thanks. I added the documentation and release notes. Please feel free to change the wording, tone and contents as you like.

Before you merge, I will squash the commits. (For now it is easier to keep track of what changed in the PR.)

By the way, UnsaltedSHA1PasswordHasher and UnsaltedMD5PasswordHasher were missing in the documentation. Was this on purpose. I've added them for the moment, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the other entries" isn't entirely accurate; very few websites well need to keep anything other than PBKDF2PasswordHasher, the default hasher since Django got its current password hashing mechanism.

I would say:

  PASSWORD_HASHERS = [
      'django.contrib.auth.hashers.Argon2PasswordHasher',
      'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]

If your database contains password encoded with other hashers, you need to add them to the list, or else the corresponding users won't be able to log in. See the documentation of :setting:PASSWORD_HASHERS for details.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some people might blindly copy and paste the displayed code to enable Argon2, without reading the remark after it. It does not hurt to list all hashers, so that seems to me to be the safe choice.

This is also how the documentation is written to enable BCrypt. See topics/auth/passwords.txt.

Off course, I could change it everywhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it consistent is a reasonable choice for now. Leave it as is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would link to "argon2_usage" instead of repeating the example usage parts of it in the release notes.

Copy link
Contributor Author

@bwesterb bwesterb Feb 3, 2016 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to suggest some edits to move argon2 discussion to a separate section with a few todos that I hope can complete: https://dpaste.de/aaF5 You can apply the patch with patch -p1 -i raw.diff. Thanks, this is coming along!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I added some text on choosing parameters.

@bwesterb bwesterb force-pushed the argon2 branch 2 times, most recently from 2d50fa6 to 57cf060 Compare February 4, 2016 19:27
@bwesterb bwesterb force-pushed the argon2 branch 2 times, most recently from 93c8ed5 to abc48a3 Compare February 4, 2016 19:36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we say what the defaults are? Should we consider increasing the defaults for each major release of Django like we do for PBKDF2? https://docs.djangoproject.com/en/dev/internals/howto-release-django/#new-stable-branch-tasks
Please use 1 space between sentences.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a good idea to review the defaults for each major release. The analogue of the current rule would be to increase memory_cost with approximately 20% for each major release. Maybe it would be a better idea to use actual hardware as a reference: adjust the parameters such that the hash takes a certain amount of milliseconds on the cheapest Amazon EC2 instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe coordinate changing the defaults with @hynek ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The defaults aren’t too likely to change very frequently since even the lowest settings are “safe enough”. Choosing the target verification time is highly individual so I guess the best approach for Django is to decide on a time and tune all checkers to meet it so people don't have to think about it? My own target was roughly 0.5ms–1ms “on my notebook”.

@timgraham
Copy link
Member

Could you add a harden_runtime() method based on the patch in today's security release? 67b46ba

@bwesterb
Copy link
Contributor Author

bwesterb commented Mar 2, 2016

The problem. The runtime of PBKDF2 is practically linear in the number of iterations. For argon2, it is more complex. If I am not mistaken, it's runtime is proportional to

time_cost * memory_cost / max(available_threads, parallelism) + memory_cost * constant1 + constant2

The first term is for the main iteration of argon2, which goes over all blocks of memory time_cost times. The second is for the compression of all memory into one final block (B_final in the spec.) at the end whose hash will be the result of argon2. The third is for the computation of the initial hash of the parameters (H_0 in the spec.)

Suppose we double the time_cost. Then argon2 will run just shy of twice as fast: the second and the third term are the difference. Here is some simple code that verifies this: https://gist.github.com/bwesterb/fe0e8be93c978aaa911d The difference is even bigger than I expected: 10%.

What to do?
I see a few possibilities

  1. Use a no-op harden_runtime and live with the issue.
  2. Let Django compute the expected runtime of the default password hash beforehand and let the harden_runtime sleep (or spinwait) the difference.
  3. Instead of changing the parameters of argon2, simply iterate argon2 itself. (In other words: use PBKDF2(argon2))

The last would be the best mitigation of the issue at hand, but would over time give us a hash which is a lot weaker than it could have been. Option 2 would also work if a hash is replaced by a different, but slower hash (instead of just when the parameters are changed).

@timgraham

@apollo13
Copy link
Member

apollo13 commented Mar 2, 2016

I am leaning towards option one. The security fix is mainly for pbkdf2
where we constantly increase iterations. I do not think that algos like
argon 2 need such an aggressive approach (I have to read up on argon
though, but for bcrypt adding 1 to the work factor means doubling the
workload -- not something you'd do lightly)

On Wed, Mar 2, 2016, 14:54 Bas Westerbaan notifications@github.com wrote:

The problem. The runtime of PBKDF2 is practically linear in the number
of iterations. For argon2, it is more complex. If I am not mistaken, it's
runtime is proportional to

time_cost * memory_cost / max(available_threads, parallelism) + memory_cost * constant1 + constant2

The first term is for the main iteration of argon2, which goes over all
blocks of memory time_cost times. The second is for the compression of
all memory into one final block (B_final in the spec.) at the end whose
hash will be the result of argon2. The third is for the computation of the
initial hash of the parameters (H_0 in the spec.)

Suppose we double the time_cost. Then argon2 will run just shy of twice
as fast: the second and the third term are the difference. Here is some
simple code that verifies this:
https://gist.github.com/bwesterb/fe0e8be93c978aaa911d The difference is
even bigger than I expected: 10%.

What to do?
I see a few possibilities

  1. Use a no-op harden_runtime and live with the issue.
  2. Let Django compute the expected runtime of the default password
    hash beforehand and let the harden_runtime sleep (or spinwait) the
    difference.
  3. Instead of changing the parameters of argon2, simply iterate argon2
    itself. (In other words: use PBKDF2(argon2))

The last would be the best mitigation of the issue at hand, but would over
time give us a hash which is a lot weaker than it could have been. Option 2
would also work if a hash is replaced by a different, but slower hash
(instead of just when the parameters are changed).


Reply to this email directly or view it on GitHub
#5876 (comment).

Argon2 is the winner of the Password Hashing Competition.

    https://password-hashing.net

There are two python bindings available for Argon2: argon2 and
argon2-cffi. The first uses ctypes and the latter cffi. As cffi works
for most environments, I choose that one.

Argon2 has four parameters: time_cost, memory_cost, parallelism and
type. I choose to copy the defaults of argon2-cffi. Section 9 of the
argon2 specification discusses recommended parameters.

    https://password-hashing.net/argon2-specs.pdf
@bwesterb
Copy link
Contributor Author

bwesterb commented Mar 5, 2016

@timgraham I added a no-op harden_runtime as suggested by @apollo13

@timgraham
Copy link
Member

merged in b4250ea, thanks!

@timgraham timgraham closed this Mar 8, 2016
@timgraham
Copy link
Member

timgraham commented Apr 19, 2016

We have a test failure with the release of argon2_cffi 16.1 which changes the hash and adds an extra dollar sign which causes problems with Django trying to split it. How do you think we should proceed? I guess updating Django and requiring the latest version of argon2_cffi might be okay. I just hope this won't be an ongoing issue where argon2_cffi makes changes that aren't compatible?

@hynek
Copy link

hynek commented Apr 19, 2016

Argon2 has a new version 1.3 that introduced a version tag.

My suggestions:

  • fix the hash in your tests.
  • you can check what version the hash is and use that information for opportunistic re-hashing

JFTR Hashes are backward compatible. You can verif old hashes with the new release.

@bwesterb
Copy link
Contributor Author

I will write a patch for this this week.

@hynek
Copy link

hynek commented Apr 19, 2016

(Oh and I didn't change anything; it's the standard/official bindings that evolved.)

@bwesterb
Copy link
Contributor Author

I created a PR: #6489

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants