summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/crypt.rst106
-rw-r--r--Lib/crypt.py61
-rw-r--r--Lib/test/test_crypt.py17
-rw-r--r--Misc/NEWS4
-rw-r--r--Modules/Setup.dist2
-rw-r--r--Modules/_cryptmodule.c (renamed from Modules/cryptmodule.c)4
-rw-r--r--setup.py2
7 files changed, 183 insertions, 13 deletions
diff --git a/Doc/library/crypt.rst b/Doc/library/crypt.rst
index 0be571e..a5556cf 100644
--- a/Doc/library/crypt.rst
+++ b/Doc/library/crypt.rst
@@ -15,9 +15,9 @@
This module implements an interface to the :manpage:`crypt(3)` routine, which is
a one-way hash function based upon a modified DES algorithm; see the Unix man
-page for further details. Possible uses include allowing Python scripts to
-accept typed passwords from the user, or attempting to crack Unix passwords with
-a dictionary.
+page for further details. Possible uses include storing hashed passwords
+so you can check passwords without storing the actual password, or attempting
+to crack Unix passwords with a dictionary.
.. index:: single: crypt(3)
@@ -26,15 +26,67 @@ the :manpage:`crypt(3)` routine in the running system. Therefore, any
extensions available on the current implementation will also be available on
this module.
+Hashing Methods
+---------------
-.. function:: crypt(word, salt)
+The :mod:`crypt` module defines the list of hashing methods (not all methods
+are available on all platforms):
+
+.. data:: METHOD_SHA512
+
+ A Modular Crypt Format method with 16 character salt and 86 character
+ hash. This is the strongest method.
+
+.. versionadded:: 3.3
+
+.. data:: METHOD_SHA256
+
+ Another Modular Crypt Format method with 16 character salt and 43
+ character hash.
+
+.. versionadded:: 3.3
+
+.. data:: METHOD_MD5
+
+ Another Modular Crypt Format method with 8 character salt and 22
+ character hash.
+
+.. versionadded:: 3.3
+
+.. data:: METHOD_CRYPT
+
+ The traditional method with a 2 character salt and 13 characters of
+ hash. This is the weakest method.
+
+.. versionadded:: 3.3
+
+Module Functions
+----------------
+
+The :mod:`crypt` module defines the following functions:
+
+.. function:: crypt(word, salt=None)
*word* will usually be a user's password as typed at a prompt or in a graphical
- interface. *salt* is usually a random two-character string which will be used
- to perturb the DES algorithm in one of 4096 ways. The characters in *salt* must
- be in the set ``[./a-zA-Z0-9]``. Returns the hashed password as a string, which
- will be composed of characters from the same alphabet as the salt (the first two
- characters represent the salt itself).
+ interface. The optional *salt* is either a string as returned from
+ :func:`mksalt`, one of the ``crypt.METHOD_*`` values (though not all
+ may be available on all platforms), or a full encrypted password
+ including salt, as returned by this function. If *salt* is not
+ provided, the strongest method will be used (as returned by
+ :func:`methods`.
+
+ Checking a password is usually done by passing the plain-text password
+ as *word* and the full results of a previous :func:`crypt` call,
+ which should be the same as the results of this call.
+
+ *salt* (either a random 2 or 16 character string, possibly prefixed with
+ ``$digit$`` to indicate the method) which will be used to perturb the
+ encryption algorithm. The characters in *salt* must be in the set
+ ``[./a-zA-Z0-9]``, with the exception of Modular Crypt Format which
+ prefixes a ``$digit$``.
+
+ Returns the hashed password as a string, which will be composed of
+ characters from the same alphabet as the salt.
.. index:: single: crypt(3)
@@ -42,6 +94,34 @@ this module.
different sizes in the *salt*, it is recommended to use the full crypted
password as salt when checking for a password.
+.. versionchanged:: 3.3
+ Before version 3.3, *salt* must be specified as a string and cannot
+ accept ``crypt.METHOD_*`` values (which don't exist anyway).
+
+.. function:: methods()
+
+ Return a list of available password hashing algorithms, as
+ ``crypt.METHOD_*`` objects. This list is sorted from strongest to
+ weakest, and is guaranteed to have at least ``crypt.METHOD_CRYPT``.
+
+.. versionadded:: 3.3
+
+.. function:: mksalt(method=None)
+
+ Return a randomly generated salt of the specified method. If no
+ *method* is given, the strongest method available as returned by
+ :func:`methods` is used.
+
+ The return value is a string either of 2 characters in length for
+ ``crypt.METHOD_CRYPT``, or 19 characters starting with ``$digit$`` and
+ 16 random characters from the set ``[./a-zA-Z0-9]``, suitable for
+ passing as the *salt* argument to :func:`crypt`.
+
+.. versionadded:: 3.3
+
+Examples
+--------
+
A simple example illustrating typical use::
import crypt, getpass, pwd
@@ -57,3 +137,11 @@ A simple example illustrating typical use::
else:
return 1
+To generate a hash of a password using the strongest available method and
+check it against the original::
+
+ import crypt
+
+ hashed = crypt.crypt(plaintext)
+ if hashed != crypt.crypt(plaintext, hashed):
+ raise "Hashed version doesn't validate against original"
diff --git a/Lib/crypt.py b/Lib/crypt.py
new file mode 100644
index 0000000..dc62dba
--- /dev/null
+++ b/Lib/crypt.py
@@ -0,0 +1,61 @@
+'''Wrapper to the POSIX crypt library call and associated functionality.
+'''
+
+import _crypt
+
+saltchars = 'abcdefghijklmnopqrstuvwxyz'
+saltchars += saltchars.upper()
+saltchars += '0123456789./'
+
+
+class _MethodClass:
+ '''Class representing a salt method per the Modular Crypt Format or the
+ legacy 2-character crypt method.'''
+ def __init__(self, name, ident, salt_chars, total_size):
+ self.name = name
+ self.ident = ident
+ self.salt_chars = salt_chars
+ self.total_size = total_size
+
+ def __repr__(self):
+ return '<crypt.METHOD_%s>' % self.name
+
+
+# available salting/crypto methods
+METHOD_CRYPT = _MethodClass('CRYPT', None, 2, 13)
+METHOD_MD5 = _MethodClass('MD5', '1', 8, 34)
+METHOD_SHA256 = _MethodClass('SHA256', '5', 16, 63)
+METHOD_SHA512 = _MethodClass('SHA512', '6', 16, 106)
+
+
+def methods():
+ '''Return a list of methods that are available in the platform ``crypt()``
+ library, sorted from strongest to weakest. This is guaranteed to always
+ return at least ``[METHOD_CRYPT]``'''
+ method_list = [ METHOD_SHA512, METHOD_SHA256, METHOD_MD5 ]
+ ret = [ method for method in method_list
+ if len(crypt('', method)) == method.total_size ]
+ ret.append(METHOD_CRYPT)
+ return ret
+
+
+def mksalt(method = None):
+ '''Generate a salt for the specified method. If not specified, the
+ strongest available method will be used.'''
+ import random
+
+ if method == None: method = methods()[0]
+ s = '$%s$' % method.ident if method.ident else ''
+ s += ''.join([ random.choice(saltchars) for x in range(method.salt_chars) ])
+ return(s)
+
+
+def crypt(word, salt = None):
+ '''Return a string representing the one-way hash of a password, preturbed
+ by a salt. If ``salt`` is not specified or is ``None``, the strongest
+ available method will be selected and a salt generated. Otherwise,
+ ``salt`` may be one of the ``crypt.METHOD_*`` values, or a string as
+ returned by ``crypt.mksalt()``.'''
+ if salt == None: salt = mksalt()
+ elif isinstance(salt, _MethodClass): salt = mksalt(salt)
+ return(_crypt.crypt(word, salt))
diff --git a/Lib/test/test_crypt.py b/Lib/test/test_crypt.py
index 2adb28d..cb4234b 100644
--- a/Lib/test/test_crypt.py
+++ b/Lib/test/test_crypt.py
@@ -10,6 +10,23 @@ class CryptTestCase(unittest.TestCase):
if support.verbose:
print('Test encryption: ', c)
+ def test_salt(self):
+ self.assertEqual(len(crypt.saltchars), 64)
+ for method in crypt.methods():
+ salt = crypt.mksalt(method)
+ self.assertEqual(len(salt),
+ method.salt_chars + (3 if method.ident else 0))
+
+ def test_saltedcrypt(self):
+ for method in crypt.methods():
+ pw = crypt.crypt('assword', method)
+ self.assertEqual(len(pw), method.total_size)
+ pw = crypt.crypt('assword', crypt.mksalt(method))
+ self.assertEqual(len(pw), method.total_size)
+
+ def test_methods(self):
+ self.assertTrue(len(crypt.methods()) > 1)
+
def test_main():
support.run_unittest(CryptTestCase)
diff --git a/Misc/NEWS b/Misc/NEWS
index 564f879..a3a246c 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -27,6 +27,10 @@ Core and Builtins
Library
-------
+- Issue #10924: Adding salt and Modular Crypt Format to crypt library.
+ Moved old C wrapper to _crypt, and added a Python wrapper with
+ enhanced salt generation and simpler API for password generation.
+
- Issue #11074: Make 'tokenize' so it can be reloaded.
- Issue #11085: Moved collections abstract base classes into a separate
diff --git a/Modules/Setup.dist b/Modules/Setup.dist
index e1c8fe4..56f3314 100644
--- a/Modules/Setup.dist
+++ b/Modules/Setup.dist
@@ -207,7 +207,7 @@ _symtable symtablemodule.c
#
# First, look at Setup.config; configure may have set this for you.
-#crypt cryptmodule.c # -lcrypt # crypt(3); needs -lcrypt on some systems
+#_crypt _cryptmodule.c # -lcrypt # crypt(3); needs -lcrypt on some systems
# Some more UNIX dependent modules -- off by default, since these
diff --git a/Modules/cryptmodule.c b/Modules/_cryptmodule.c
index d5a42ff..5100788 100644
--- a/Modules/cryptmodule.c
+++ b/Modules/_cryptmodule.c
@@ -45,7 +45,7 @@ static PyMethodDef crypt_methods[] = {
static struct PyModuleDef cryptmodule = {
PyModuleDef_HEAD_INIT,
- "crypt",
+ "_crypt",
NULL,
-1,
crypt_methods,
@@ -56,7 +56,7 @@ static struct PyModuleDef cryptmodule = {
};
PyMODINIT_FUNC
-PyInit_crypt(void)
+PyInit__crypt(void)
{
return PyModule_Create(&cryptmodule);
}
diff --git a/setup.py b/setup.py
index 79d9f62..643b6cc 100644
--- a/setup.py
+++ b/setup.py
@@ -636,7 +636,7 @@ class PyBuildExt(build_ext):
libs = ['crypt']
else:
libs = []
- exts.append( Extension('crypt', ['cryptmodule.c'], libraries=libs) )
+ exts.append( Extension('_crypt', ['_cryptmodule.c'], libraries=libs) )
# CSV files
exts.append( Extension('_csv', ['_csv.c']) )