summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteven D'Aprano <steve@pearwood.info>2016-04-14 15:51:31 (GMT)
committerSteven D'Aprano <steve@pearwood.info>2016-04-14 15:51:31 (GMT)
commit95702725ff7ce93c332757fec6ba00a59f5ded94 (patch)
tree247895b372c300b90b47aa79da62dd82bf7cbe30
parent15d2d49ce685e10deb93a18248d8605d74315e6c (diff)
downloadcpython-95702725ff7ce93c332757fec6ba00a59f5ded94.zip
cpython-95702725ff7ce93c332757fec6ba00a59f5ded94.tar.gz
cpython-95702725ff7ce93c332757fec6ba00a59f5ded94.tar.bz2
Add secrets module and tests.
-rw-r--r--Lib/secrets.py149
-rw-r--r--Lib/test/test_secrets.py120
2 files changed, 269 insertions, 0 deletions
diff --git a/Lib/secrets.py b/Lib/secrets.py
new file mode 100644
index 0000000..ed018ad
--- /dev/null
+++ b/Lib/secrets.py
@@ -0,0 +1,149 @@
+"""Generate cryptographically strong pseudo-random numbers suitable for
+managing secrets such as account authentication, tokens, and similar.
+See PEP 506 for more information.
+
+https://www.python.org/dev/peps/pep-0506/
+
+
+Random numbers
+==============
+
+The ``secrets`` module provides the following pseudo-random functions, based
+on SystemRandom, which in turn uses the most secure source of randomness your
+operating system provides.
+
+
+ choice(sequence)
+ Choose a random element from a non-empty sequence.
+
+ randbelow(n)
+ Return a random int in the range [0, n).
+
+ randbits(k)
+ Generates an int with k random bits.
+
+ SystemRandom
+ Class for generating random numbers using sources provided by
+ the operating system. See the ``random`` module for documentation.
+
+
+Token functions
+===============
+
+The ``secrets`` module provides a number of functions for generating secure
+tokens, suitable for applications such as password resets, hard-to-guess
+URLs, and similar. All the ``token_*`` functions take an optional single
+argument specifying the number of bytes of randomness to use. If that is
+not given, or is ``None``, a reasonable default is used. That default is
+subject to change at any time, including during maintenance releases.
+
+
+ token_bytes(nbytes=None)
+ Return a random byte-string containing ``nbytes`` number of bytes.
+
+ >>> secrets.token_bytes(16) #doctest:+SKIP
+ b'\\xebr\\x17D*t\\xae\\xd4\\xe3S\\xb6\\xe2\\xebP1\\x8b'
+
+
+ token_hex(nbytes=None)
+ Return a random text-string, in hexadecimal. The string has ``nbytes``
+ random bytes, each byte converted to two hex digits.
+
+ >>> secrets.token_hex(16) #doctest:+SKIP
+ 'f9bf78b9a18ce6d46a0cd2b0b86df9da'
+
+ token_urlsafe(nbytes=None)
+ Return a random URL-safe text-string, containing ``nbytes`` random
+ bytes. On average, each byte results in approximately 1.3 characters
+ in the final result.
+
+ >>> secrets.token_urlsafe(16) #doctest:+SKIP
+ 'Drmhze6EPcv0fN_81Bj-nA'
+
+
+(The examples above assume Python 3. In Python 2, byte-strings will display
+using regular quotes ``''`` with no prefix, and text-strings will have a
+``u`` prefix.)
+
+
+Other functions
+===============
+
+ compare_digest(a, b)
+ Return True if strings a and b are equal, otherwise False.
+ Performs the equality comparison in such a way as to reduce the
+ risk of timing attacks.
+
+ See http://codahale.com/a-lesson-in-timing-attacks/ for a
+ discussion on how timing attacks against ``==`` can reveal
+ secrets from your application.
+
+
+"""
+
+__all__ = ['choice', 'randbelow', 'randbits', 'SystemRandom',
+ 'token_bytes', 'token_hex', 'token_urlsafe',
+ 'compare_digest',
+ ]
+
+
+import base64
+import binascii
+import os
+
+try:
+ from hmac import compare_digest
+except ImportError:
+ # Python version is too old. Fall back to a pure-Python version.
+
+ import operator
+ from functools import reduce
+
+ def compare_digest(a, b):
+ """Return ``a == b`` using an approach resistant to timing analysis.
+
+ a and b must both be of the same type: either both text strings,
+ or both byte strings.
+
+ Note: If a and b are of different lengths, or if an error occurs,
+ a timing attack could theoretically reveal information about the
+ types and lengths of a and b, but not their values.
+ """
+ # For a similar approach, see
+ # http://codahale.com/a-lesson-in-timing-attacks/
+ for T in (bytes, str):
+ if isinstance(a, T) and isinstance(b, T):
+ break
+ else: # for...else
+ raise TypeError("arguments must be both strings or both bytes")
+ if len(a) != len(b):
+ return False
+ # Thanks to Raymond Hettinger for this one-liner.
+ return reduce(operator.and_, map(operator.eq, a, b), True)
+
+
+
+from random import SystemRandom
+
+_sysrand = SystemRandom()
+
+randbits = _sysrand.getrandbits
+choice = _sysrand.choice
+
+def randbelow(exclusive_upper_bound):
+ return _sysrand._randbelow(exclusive_upper_bound)
+
+DEFAULT_ENTROPY = 32 # number of bytes to return by default
+
+def token_bytes(nbytes=None):
+ if nbytes is None:
+ nbytes = DEFAULT_ENTROPY
+ return os.urandom(nbytes)
+
+def token_hex(nbytes=None):
+ return binascii.hexlify(token_bytes(nbytes)).decode('ascii')
+
+def token_urlsafe(nbytes=None):
+ tok = token_bytes(nbytes)
+ return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii')
+
diff --git a/Lib/test/test_secrets.py b/Lib/test/test_secrets.py
new file mode 100644
index 0000000..a3d1a8c
--- /dev/null
+++ b/Lib/test/test_secrets.py
@@ -0,0 +1,120 @@
+"""Test the secrets module.
+
+As most of the functions in secrets are thin wrappers around functions
+defined elsewhere, we don't need to test them exhaustively.
+"""
+
+
+import secrets
+import unittest
+import string
+
+
+# === Unit tests ===
+
+class Compare_Digest_Tests(unittest.TestCase):
+ """Test secrets.compare_digest function."""
+
+ def test_equal(self):
+ # Test compare_digest functionality with equal (byte/text) strings.
+ for s in ("a", "bcd", "xyz123"):
+ a = s*100
+ b = s*100
+ self.assertTrue(secrets.compare_digest(a, b))
+ self.assertTrue(secrets.compare_digest(a.encode('utf-8'), b.encode('utf-8')))
+
+ def test_unequal(self):
+ # Test compare_digest functionality with unequal (byte/text) strings.
+ self.assertFalse(secrets.compare_digest("abc", "abcd"))
+ self.assertFalse(secrets.compare_digest(b"abc", b"abcd"))
+ for s in ("x", "mn", "a1b2c3"):
+ a = s*100 + "q"
+ b = s*100 + "k"
+ self.assertFalse(secrets.compare_digest(a, b))
+ self.assertFalse(secrets.compare_digest(a.encode('utf-8'), b.encode('utf-8')))
+
+ def test_bad_types(self):
+ # Test that compare_digest raises with mixed types.
+ a = 'abcde'
+ b = a.encode('utf-8')
+ assert isinstance(a, str)
+ assert isinstance(b, bytes)
+ self.assertRaises(TypeError, secrets.compare_digest, a, b)
+ self.assertRaises(TypeError, secrets.compare_digest, b, a)
+
+ def test_bool(self):
+ # Test that compare_digest returns a bool.
+ self.assertTrue(isinstance(secrets.compare_digest("abc", "abc"), bool))
+ self.assertTrue(isinstance(secrets.compare_digest("abc", "xyz"), bool))
+
+
+class Random_Tests(unittest.TestCase):
+ """Test wrappers around SystemRandom methods."""
+
+ def test_randbits(self):
+ # Test randbits.
+ errmsg = "randbits(%d) returned %d"
+ for numbits in (3, 12, 30):
+ for i in range(6):
+ n = secrets.randbits(numbits)
+ self.assertTrue(0 <= n < 2**numbits, errmsg % (numbits, n))
+
+ def test_choice(self):
+ # Test choice.
+ items = [1, 2, 4, 8, 16, 32, 64]
+ for i in range(10):
+ self.assertTrue(secrets.choice(items) in items)
+
+ def test_randbelow(self):
+ # Test randbelow.
+ errmsg = "randbelow(%d) returned %d"
+ for i in range(2, 10):
+ n = secrets.randbelow(i)
+ self.assertTrue(n in range(i), errmsg % (i, n))
+ self.assertRaises(ValueError, secrets.randbelow, 0)
+
+
+class Token_Tests(unittest.TestCase):
+ """Test token functions."""
+
+ def test_token_defaults(self):
+ # Test that token_* functions handle default size correctly.
+ for func in (secrets.token_bytes, secrets.token_hex,
+ secrets.token_urlsafe):
+ name = func.__name__
+ try:
+ func()
+ except TypeError:
+ self.fail("%s cannot be called with no argument" % name)
+ try:
+ func(None)
+ except TypeError:
+ self.fail("%s cannot be called with None" % name)
+ size = secrets.DEFAULT_ENTROPY
+ self.assertEqual(len(secrets.token_bytes(None)), size)
+ self.assertEqual(len(secrets.token_hex(None)), 2*size)
+
+ def test_token_bytes(self):
+ # Test token_bytes.
+ self.assertTrue(isinstance(secrets.token_bytes(11), bytes))
+ for n in (1, 8, 17, 100):
+ self.assertEqual(len(secrets.token_bytes(n)), n)
+
+ def test_token_hex(self):
+ # Test token_hex.
+ self.assertTrue(isinstance(secrets.token_hex(7), str))
+ for n in (1, 12, 25, 90):
+ s = secrets.token_hex(n)
+ self.assertEqual(len(s), 2*n)
+ self.assertTrue(all(c in string.hexdigits for c in s))
+
+ def test_token_urlsafe(self):
+ # Test token_urlsafe.
+ self.assertTrue(isinstance(secrets.token_urlsafe(9), str))
+ legal = string.ascii_letters + string.digits + '-_'
+ for n in (1, 11, 28, 76):
+ self.assertTrue(all(c in legal for c in secrets.token_urlsafe(n)))
+
+
+if __name__ == '__main__':
+ unittest.main()