Issue #10924: Adding salt and Modular Crypt Format to crypt library.
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 @@
 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 @@
    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 @@
        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 @@
         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 @@
 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 @@
 #
 # 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
similarity index 97%
rename from Modules/cryptmodule.c
rename to Modules/_cryptmodule.c
index d5a42ff..5100788 100644
--- a/Modules/cryptmodule.c
+++ b/Modules/_cryptmodule.c
@@ -45,7 +45,7 @@
 
 static struct PyModuleDef cryptmodule = {
     PyModuleDef_HEAD_INIT,
-    "crypt",
+    "_crypt",
     NULL,
     -1,
     crypt_methods,
@@ -56,7 +56,7 @@
 };
 
 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 @@
             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']) )