Search code examples
pythondebiancrypt

crypt.crypt raises an OSError: Invalid Argument


I use an up to date Debian 10. I have unexpected behavior with python crypt.crypt, some calls raise an OSError: Invalid Argument exception, without further explanation, so I am not really sure what is happening.

>>> # debian - python3.9
>>> import crypt
>>> crypt.crypt("foo", '$2a$10$Ud3Zhyb1M3I1PMDXWOFBp.81LKFwD5mZo33jnVHbPPrQY4cKSqoI7')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.9/crypt.py", line 82, in crypt
    return _crypt.crypt(word, salt)
OSError: [Errno 22] Invalid argument

>>> # archlinux - python3.9
>>> import crypt
>>> crypt.crypt("foo", '$2a$10$Ud3Zhyb1M3I1PMDXWOFBp.81LKFwD5mZo33jnVHbPPrQY4cKSqoI7')
'$2a$10$Ud3Zhyb1M3I1PMDXWOFBp.8U7GsUrAN2JZZbKcxHSc.cTrK6oEA/.'

This code fails silently on my debian with the system python3.7 (it returns None until python3.9) and raises this OSError on my manually built python3.9. It works perfectly well on my archlinux on any python version.

edit1: I suspect this is due to the crypt methods maybe being not supported anymore with debian?

# On arch
>>> crypt.methods
[<crypt.METHOD_SHA512>, <crypt.METHOD_SHA256>, <crypt.METHOD_BLOWFISH>, <crypt.METHOD_MD5>, <crypt.METHOD_CRYPT>]

# On Debian
>>> crypt.methods
[<crypt.METHOD_SHA512>, <crypt.METHOD_SHA256>, <crypt.METHOD_MD5>, <crypt.METHOD_CRYPT>]

edit2: The crypt man page shows this interesting piece of information:

If  salt  is  a character string starting with the characters "$id$" followed by a string
optionally terminated by "$", then the result has the form:

       $id$salt$encrypted

id identifies the encryption method used instead of DES and this then determines how
the rest of the password  string  is  interpreted.

The following values of id are supported:
ID  | Method
─────────────────────────────────────────────────────────
1   | MD5
2a  | Blowfish (not in mainline glibc; added in some
    | Linux distributions)
5   | SHA-256 (since glibc 2.7)
6   | SHA-512 (since glibc 2.7)

So the $2a at the start of the salt would mean that blowfish is needed for crypt to work properly, but it is not available on debian according to crypt.method.

edit3: I can confirm this with this piece of C code

// gcc crypt.c -o foobar -lcrypt; ./foobar
#include <crypt.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>


int main() {
    char *crypt_result;
    char* word = "foo";
    char* salt = "$2a$10$Ud3Zhyb1M3I1PMDXWOFBp.81LKFwD5mZo33jnVHbPPrQY4cKSqoI7";

    crypt_result = crypt(word, salt);
    if (crypt_result == NULL) {
        printf("%s\n", strerror(errno));
    }
    else {
        printf("%s\n", crypt_result);
    }

    return 0;
}

It displays the expected output on archlinux, but displays Invalid argument on debian.

How can I solve this situation?


Solution

  • More context

    The $2a at the beginning of the salt is a marker of the blowfish/bcrypt method.

    Blowfish is not supported by all Linux distros, like Ubuntu:

    How to make Ubuntu's crypt(3) support Blowfish?

    https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1349252

    Debian does not support blowfish in crypt as of Debian 10 but should be able to do it in the next release, moving the dependency of crypt from glibc to libxcrypt:

    https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=149452#42

    https://blog.bofh.it/debian/id_458

    How to solve the situation

    Attempt with passlib

    I made an attempt to use passlib with the verify context whose prototype is like crypt.crypt:

    >>> from passlib.apps import custom_app_context as pwd_context
    >>> pwd_context.verify("foo", '$2a$10$Ud3Zhyb1M3I1PMDXWOFBp.81LKFwD5mZo33jnVHbPPrQY4cKSqoI7')
    

    but unfortunately this snippet code seems to be bugged.

    Attempt with bcrypt

    For now I will stick to crypt.crypt and use bcrypt as a fallback:

    import crypt
    import bcrypt
    
    def verify(password, hash):
        try:
            return crypt.crypt(password, hash) == hash
        except OSError:
            return bcrypt.checkpw(password.encode("utf-8"), hash.encode("utf-8"))
    
    verify("foo", "$2a$10$Ud3Zhyb1M3I1PMDXWOFBp.81LKFwD5mZo33jnVHbPPrQY4cKSqoI7")