You Might Have Misused Python Random

Image for post
Image for post
Photo by Lucas Santos on Unsplash

Introduction

Basic usage of Python random module

#Generate a random integer between 1 to 10 
random.randint(1,10)
#2
#generate a random floating point number between 0 to 1 random.random()
#0.3103975786510934
#Generate random number between 1 to 2 in uniform distribution
random.uniform(1, 2)
#1.9530600469459607
#Generate random number between 1 to 100, with step as 2
random.randrange(1, 100, 2)
#43
#Generate random bytes, available in Python version 3.9 random.randbytes(8)
#b'v\xf7\xb2v`\xc8U]'
#Generate a integer within 8 bits
random.getrandbits(8)
#68

And shuffling or sampling the items in a sequence can be achieved easily with below:

slangs = ["Boomer", "Cap", "Basic", "Retweet", "Fit", "Fr", "Clout"]random.shuffle(slangs) 
#['Fit', 'Basic', 'Fr', 'Clout', 'Cap', 'Retweet', 'Boomer']

random.sample(slangs, k=5)
#['Fit', 'Fr', 'Clout', 'Retweet', 'Basic']

You can also use the choice function to choose a random option from a given sequence, for instance:

random.choice(["Boomer", "Cap", "Basic", "Retweet", "Fit", "Fr", "Clout"])
#'Retweet'

With this function, It’s easy to implement a lucky draw logic where all the participants’ name are passed in and a winner is selected randomly which seems to be the perfect use case of it. The problem comes when developers try to use this function to generate password or security related tokens which they believe it’s random enough and resistant to predication. But it is indeed wrong.

Why the random numbers generated are not random enough?

Actually random module does provide a SystemRandom class which uses the sources from operation system to generate random numbers without relying on the software state and the result is not reproducible. For instance, depends on the actual implementation, some OS uses the atmospheric noise or the exact time of the key presses as the source for generating unpredictable result which is more suitable for cryptography.

You can use it in the same way as the random class except the state is not available:

sys_random = random.SystemRandom()sys_random.random() 
#0.04883551877802528
sys_random.randint(1, 100)
#72
sys_random.choice(["Boomer", "Cap", "Basic", "Retweet", "Fit", "Fr", "Clout"])
#'Clout'

Unfortunately, this class has been overlooked for many years and some developers continue using the functions from random module for generating password or security tokens which exposed a lot of security vulnerability. To address these concerns, an enhancement proposal raised to add a new secrets module for some common security-related functions, so that people won’t mix up it with the random module.

Python secrets module

#Generate random integer between 0 to 50 
secrets.randbelow(50)
#2
#Generate random integer with 8 bits
secrets.randbits(8)
#125
#Generate random bytes
secrets.token_bytes(20)
#b'\x8a\xb3\xa92)S:\xf2\xac\x90\xaf\xb1\xb3Q\xc5\xfe\x80\xdb\xc2`'
#Generate random string in hexadecimal with default 32 bytes
secrets.token_hex()
#'cf4f964edf810ca7f6ad6b533038fdcf538c73bd59e11d3340a838e7fd88fdf9'
#Generate URL-safe text string
secrets.token_urlsafe(10)
#z9zQQsxcq6SRug
import string secrets.choice(string.ascii_letters + string.digits + string.punctuation)
#'k'

The functions are pretty similar to what we have seen in random module, just that results are generated from os.urandom function which meant to be unpredictable enough for cryptographic applications.

Implementing a strong password generator with Python secrets

  • Length between 8 to 16
  • At least 1 lowercase
  • At least 1 uppercase
  • At least 1 number
  • At least 1 special characters among !#$%&@_~

We can use the choice function to randomly choose 1 character each time for a random iteration between 8 to 16 times, then test if all the requirements are met for the generated password. Below the sample code:

def generate_strong_password():    special_characters = '!#$%&@_~' 
password_choices = string.ascii_letters + string.digits + special_characters
while True:
password = ''.join(secrets.choice(password_choices) for _ in range(random.randint(8, 16)))
if (any(c.islower() for c in password)
and any(c.isupper() for c in password)
and sum(special_characters.find(c) > -1 for c in password) == 1
and any(c.isdigit() for c in password)):
break

return password

To check the randomness, you can try the below:

[generate_strong_password() for _ in range(10)] # sample output 
[
'C9A&PbswcLMpJ',
'DHrbuzZU&io7Io',
'B1zx4YqKUS@01m',
'Q!F8tsZJsw',
'9QVF8oASt_jceq2',
'2~EKErrHAc9jvbyZ',
'8ceU_XZbYdqv8b',
'h8oS@tZ6',
'3w@OX33tw12J6',
'FX~mkO0aNatqp'
]

Conclusion

Originally published at https://www.codeforests.com on December 27, 2020.

Resources and tutorials for python, data science and automation solutions

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store