Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unbreak password_hash('blowfish') #25347

Closed
wants to merge 1 commit into from
Closed

Conversation

jasperla
Copy link
Contributor

@jasperla jasperla commented Jun 5, 2017

SUMMARY

The used salt buffer was of incorrect length (16 instead of 22). Also the
passlib documentation recommends against generating your own salt. So unless
the salt was provided, let passlib generate it.

Also set the default bcrypto algorithm to '2b' which is the recommended format.

ISSUE TYPE
  • Bugfix Pull Request
COMPONENT NAME

password_hash filter

ANSIBLE VERSION
ansible 2.3.0.0
  config file = /home/jasper/.ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, May 24 2017, 16:43:59) [GCC 4.2.1 20070719 ]
ADDITIONAL INFORMATION

The upstream documentation for passlib can be found here.

About salt it writes:

salt (str) – Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 22 characters, drawn from the regexp range [./0-9A-Za-z].

and about the algorithm:

"2a" - some implementations suffered from rare security flaws, replaced by 2b.
[..]
"2b" - latest revision of the official BCrypt algorithm, current default.

Before:

fatal: [192.168.2.1]: FAILED! => {"failed": true, "msg": "the field 'args' has an invalid value ([]), and could not be converted to an dict. Error was: salt too small (bcrypt
+requires exactly 22 chars)\n\nThe error appears to have been in '/home/jirib/ansible/test.yml': line 11, column 7, but may\nbe elsewhere in the file depending on the exact syntax
+problem.\n\nThe offending line appears to be:\n\n  tasks:\n    - name: Create users from secret.yml\n      ^ here\n"}

After:

changed: [192.168.2.1]

More informatively I've ran a debug task (debug: msg="{{ 'emiel' | password_hash('blowfish') }}") that used to return:

TASK [debug] ************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"failed": true, "msg": "the field 'args' has an invalid value ([]), and could not be converted to an dict. Error was: salt too small (bcrypt requires exactly 22 chars)\n\nThe error appears to have been in '/private/tmp/ansible-blowfish/users.yml': line 8, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n  - debug: msg=\"{{ 'emiel' | password_hash('blowfish') }}\"\n    ^ here\nWe could be wrong, but this one looks like it might be an issue with\nmissing quotes.  Always quote template expression brackets when they\nstart a value. For instance:\n\n    with_items:\n      - {{ foo }}\n\nShould be written as:\n\n    with_items:\n      - \"{{ foo }}\"\n"}

and now returns:

ok: [localhost] => {
    "changed": false,
    "msg": "$2b$12$yhShz4JEDMGWCZO3AFpBq..gn6TIq9ydFcxOtqlFcIxdQFJc4zHk2"
}

The used salt buffer was of incorrect length (16 instead of 22). Also the
passlib documentation recommends against generating your own salt. So unless
the salt was provided, let passlib generate it.

Also set the default bcrypto algorithm to '2b' which is the recommended format.
@ansibot ansibot added affects_2.4 This issue/PR affects Ansible v2.4 bugfix_pull_request c:plugins/filter needs_triage Needs a first human triage before being processed. labels Jun 5, 2017
@alikins alikins requested a review from jimi-c June 5, 2017 15:01
@@ -271,13 +271,17 @@ def get_encrypted_password(password, hashtype='sha512', salt=None):
# TODO: find a way to construct dynamically from system
cryptmethod = {
'md5': '1',
'blowfish': '2a',
'blowfish': '2b',
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't match documentation. Why is it being changed?

Copy link
Contributor

Choose a reason for hiding this comment

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

          2a  | Blowfish (not in mainline glibc; added in some
              | Linux distributions)

from the glibc man page for crypt.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I see where the passlib documentation specifies a 2b. Since cryptmethod is only used in the crypt.crypt() path, I don't think we should change this code here.

# below contains incorrectly set padding bits. Also the length used to
# be incorrect (16 instead of 22). Besides, Passlib recommends NOT
# generating a salt string manually.
if salt is None and hashtype is not 'blowfish':
Copy link
Contributor

Choose a reason for hiding this comment

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

Making this change here doesn't work because we aren't guaranteed to have passlib installed. The crypt.crypt() method will require that we have a salt set. You could move salt generation into the conditional for not HAS_PASSLIB.

@abadger
Copy link
Contributor

abadger commented Jun 5, 2017

I also noticed that the passlib import check we're doing is no longer sufficient... apparently there are now separate backend packages and passlib needs at least one of those installed to function.

Perhaps we need a try: except passlib.exc.MissingBackendError: in the passlib code and fallback to crypt if that fails as well.

@abadger
Copy link
Contributor

abadger commented Jun 5, 2017

Something like this perhaps:

encrypted = None

if HAS_PASSLIB:
    try:
        # passlib based code
    except passlib.exc.MissingBackendError:
        pass

if encrypted is None:
     if sys.platform.startswith('darwin'):
         raise errors.AnsibleFilterError('|password_hash requires the passlib python module to generate password hashes on Mac OS X/Darwin')
     saltstring = "$%s$%s" % (cryptmethod[hashtype], salt)
     encrypted = crypt.crypt(password, saltstring)

@alikins alikins removed the needs_triage Needs a first human triage before being processed. label Jun 5, 2017
@abadger
Copy link
Contributor

abadger commented Jun 5, 2017

Note: bcoca recommends a different error message if a passlib backend is missing rather than the same message as missing passlib altogether.

@eli-collins
Copy link

Passlib author here - I just incidentally ran across this thread, and wanted to point out that crypt.crypt() is one of the backends that passlib tests for when bcrypt.hash() is called.

So if passlib throws MissingBackendError, it's reasonably certain that crypt.crypt() doesn't support bcrypt... or even worse, has broken support [1]. Because of that, if passlib is present but throws MissingBackendError, I'd recommend not using crypt.crypt() as a bcrypt fallback.

[1] For example, in cases like linux/glibc, crypt() may not even throw an error, but fall back to the super-insecure des-crypt algorithm: crypt.crypt("$2b$foo", "bar") --> "bac8SdYfKlb5."

@abadger
Copy link
Contributor

abadger commented Jun 6, 2017

Hmm... On my linux/glibc box, crypt.crypt does not fallback. It returns None (Would be better to raise an exception but a sentinel like None can also be programmed against).

@eli-collins
Copy link

For reference, I'm using Linux Mint 18.1, python 3.5.2; but I'm not at all surprised you have different behavior.

That's one of the things that started me coding passlib -- crypt()'s error/fallback behavior is hugely inconsistent at the C level, and python's wrapper just reflects that. Given the same input of crypt.crypt("$2b$foo", "bar"), other platforms may even return things like "*0" or ":" to indicate an error, rather than None.

Passlib's internal safe_crypt() helper is an example of the number of edge cases I've had to normalize for :(

@ansibot ansibot added stale_ci This PR has been tested by CI more than one week ago. Close and re-open this PR to get it retested. support:core This issue/PR relates to code supported by the Ansible Engineering Team. labels Jun 23, 2017
@calfonso calfonso added the needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR. label Aug 18, 2017
@ansibot ansibot added bug This issue/PR relates to a bug. and removed bugfix_pull_request labels Mar 2, 2018
mfuchs added a commit to mfuchs/ansible that referenced this pull request Aug 24, 2018
…_hash.

* vars_prompt with encrypt does not require passlib for the algorithms
  supported by crypt.
* Additional checks ensure that there is always a result.
  This works around issues in the crypt.crypt python function that returns
  None for algorithms it does not know.
  Some modules (like user module) interprets None as no password at all,
  which is misleading.
* The password_hash filter supports all parameters of passlib.
  This allows users to provide a rounds parameter, fixing ansible#15326.
* password_hash is not restricted to the subset provided by crypt.crypt,
  fixing one half of ansible#17266.
* Updated documentation fixes other half of ansible#17266.
* password_hash does not hard-code the salt-length, which fixes bcrypt
  in connection with passlib.
  bcrypt requires a salt with length 22, which fixes ansible#25347
* Salts are only generated by ansible when using crypt.crypt.
  Otherwise passlib generates them.
* Avoids deprecated functionality of passlib with newer library versions.
* When no rounds are specified for sha256/sha256_crypt and sha512/sha512_crypt
  always uses the default values used by crypt, i.e. 5000 rounds.
  Before when installed passlibs' defaults were used.
  passlib changes its defaults with newer library versions, leading to non
  idempotent behavior.

  NOTE: This will lead to the recalculation of existing hashes generated
        with passlib and without a rounds parameter.
        Yet henceforth the hashes will remain the same.
        No matter the installed passlib version.
        Making these hashes idempotent.

Fixes ansible#15326
Fixes ansible#17266
Fixes ansible#25347 except bcrypt still uses 2a, instead of the suggested 2b.
@abadger
Copy link
Contributor

abadger commented Aug 27, 2018

#21215 has been merged which implements all of this except for the switch to 2b iff we know we're using passllib instead of crypt. If you'd like to make a PR for that aspect, feel free to create a new one which does that. Thanks!

@abadger abadger closed this Aug 27, 2018
abadger pushed a commit that referenced this pull request Aug 27, 2018
…_hash (#21215)

* Share the implementation of hashing for both vars_prompt and password_hash.
* vars_prompt with encrypt does not require passlib for the algorithms
  supported by crypt.
* Additional checks ensure that there is always a result.
  This works around issues in the crypt.crypt python function that returns
  None for algorithms it does not know.
  Some modules (like user module) interprets None as no password at all,
  which is misleading.
* The password_hash filter supports all parameters of passlib.
  This allows users to provide a rounds parameter, fixing #15326.
* password_hash is not restricted to the subset provided by crypt.crypt,
  fixing one half of #17266.
* Updated documentation fixes other half of #17266.
* password_hash does not hard-code the salt-length, which fixes bcrypt
  in connection with passlib.
  bcrypt requires a salt with length 22, which fixes #25347
* Salts are only generated by ansible when using crypt.crypt.
  Otherwise passlib generates them.
* Avoids deprecated functionality of passlib with newer library versions.
* When no rounds are specified for sha256/sha256_crypt and sha512/sha512_crypt
  always uses the default values used by crypt, i.e. 5000 rounds.
  Before when installed passlibs' defaults were used.
  passlib changes its defaults with newer library versions, leading to non
  idempotent behavior.

  NOTE: This will lead to the recalculation of existing hashes generated
        with passlib and without a rounds parameter.
        Yet henceforth the hashes will remain the same.
        No matter the installed passlib version.
        Making these hashes idempotent.

Fixes #15326
Fixes #17266
Fixes #25347 except bcrypt still uses 2a, instead of the suggested 2b.

* random_salt is solely handled by encrypt.py.
  There is no _random_salt function there anymore.
  Also the test moved to test_encrypt.py.
* Uses pytest.skip when passlib is not available, instead of a silent return.
* More checks are executed when passlib is not available.

* Moves tests that require passlib into their own test-function.

* Uses the six library to reraise the exception.

* Fixes integration test.

When no rounds are provided the defaults of crypt are used.
In that case the rounds are not part of the resulting MCF output.
@ansible ansible locked and limited conversation to collaborators Jul 22, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
affects_2.4 This issue/PR affects Ansible v2.4 bug This issue/PR relates to a bug. c:plugins/filter needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR. plugins/filter stale_ci This PR has been tested by CI more than one week ago. Close and re-open this PR to get it retested. support:core This issue/PR relates to code supported by the Ansible Engineering Team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants