27. Strong Password Generation#

Choosing a strong password is crucial for protecting your data. Moreover, it is often advisable to use different passwords for different online services. This reduces the risks in case of a data breach. One critical aspect to consider is the password for your email account, which is often used to reset all other passwords.

During this lab session, we will learn how to generate good passwords. We will work with strings and read data from files.

27.1. Classic Method#

27.1.1. Password Strength#

he strength (or robustness) of a password refers to its ability to resist a brute-force attack, which attempts every possible password combination. The strength depends on the length LL of the password and the size NN of the alphabet used. If the password is chosen randomly, this measure corresponds to its entropy and is calculated by the formula: $\(F = \log_2(N^L)\)$

Where:

  • \(F\) is the password strength,

  • \(L\) is the length of the password,

  • \(N\) is the size of the alphabet.

The strength of a password is considered:

  • Very weak if \(F<64\),

  • Weak if \(F<80\),

  • Moderate if \(F<100\),

  • Strong if \(F \geq 100\).

By using a Python function, you can see how long a password needs to be depending on the size of the alphabet. For example, a password based only on numbers requires 25 characters to be considered moderate strength. A password using numbers, lowercase letters, uppercase letters, and punctuation symbols only needs 13 characters to achieve moderate strength.

Here is the Python code to calculate password strength:

from math import pow, log2

def strength(L, N):
    return int(log2(pow(N, L)))

27.1.2. Test criteria#

Most websites require that a password contains at least one lowercase letter, one uppercase letter, one digit, and one punctuation symbol. Therefore, at least 13 characters are needed for a randomly chosen password to be of moderate strength.

Write a function is_strong_enough(password) that takes a password as a parameter and returns a boolean indicating whether the password meets these criteria.

Here are the punctuation symbols we will consider: punct = ".:;!?/()&#@&_-*%".

Hint: Remember the isxxx() methods in Python strings, which can help you test if certain characters are present in a password (see section “Testing the characters”).

# Simple Method
def is_strong_enough(password):
    pass

            
is_strong_enough("tT1;") # False
is_strong_enough("fjhkqsgdfhqgsdhfgq") # False
is_strong_enough("fjhkqsgdfhqgsX2;") # True

27.1.3. Password Generation#

To generate passwords, we would like to randomly select symbols from a set of characters. The choice function from the random module allows us to do this. It takes a sequence as a parameter and returns a random element from it.

# Select a random character from "abc"
c = random.choice("abc")

However, generating random values can be tricky because our machines are deterministic, meaning they follow set processes that make true randomness difficult to achieve. In practice, instead of generating truly random sequences, we generate pseudo-random sequences—sequences that appear random but are generated by deterministic algorithms. The random module provides fast pseudo-random functions suitable for simulations but not ideal for cryptographic purposes.

For our needs, we will use the choice function from the secrets module (see documentation). It works similarly to the one in the random module. It is slower but less predictable and therefore more secure for password generation.

Write a function generate_password(size) that returns a randomly generated password that meets the length and strength criteria.

The simplest solution, while maintaining the most entropy, is to keep generating random passwords until one meets the strength constraint.

Hint: The string module includes useful sequences such as digits and ascii_letters that will make writing the function easier.

import string
print(string.digits, string.ascii_letters)
import secrets
import string
def generate_password(size):
    pass

generate_password(13) # ex: 'pBq51YXM83X!S'

27.2. Diceware Method#

The password generation method we discussed earlier is robust but has two drawbacks:

  • It requires trusting the computer to generate randomness.

  • It produces passwords that are difficult to remember.

One might be tempted to create passwords by other means, such as using common words and introducing substitutions to meet strength criteria. Unfortunately, these passwords have low entropy and should be avoided.

The comic from XKCD illustrates this well and suggests an alternative method for password generation: the Diceware method.

xkcd picture

This method relies on a list of approximately 8000 words. A password, or passphrase, is a random selection of several words from this list.

A word chosen randomly from a list of 8000 words corresponds to about 13 bits of entropy. To get a password with moderate strength, you need to choose at least 6 words.

This method can be performed without computer assistance. In fact, each word on the list is identified by a quintuplet of values between 1 and 6. In other words, each word can be selected by rolling 5 dice.

Today, there are alternative word lists available. For example, the Electronic Frontier Foundation has developed lists based on pop culture themes (Star Wars, Harry Potter, Game of Thrones, Star Trek). The principle is the same, but here you select a word using 3 dice with 20 faces.

Here a file containing words from Star Wars universe.

27.2.1. Reading the Word List#

Write a function read_diceware(file_path) that takes the path to a file containing the word list and returns a list of words from the file. Each line in the file contains one word.

Hint: Remember to remove the newline characters that exist at the end of each line using the strip function. The course section dedicated to file reading my be useful.

def read_diceware(file_path):
    pass

#words = read_diceware("starwars_8k_2018.txt")
#print(words[0], words[-1])   # print "aided ghost"
#print(len(words))            # print 8000

27.2.2. Generating a Diceware Password#

Write a function diceware that generates a password using the Diceware method. The number of words chosen is passed as a parameter, along with the list of words.

Hint: The choice function from the secrets module works on lists as well. It works on any sequence.

def diceware(words, size):
    pass

#diceware(words, 6) # Ex: 'finishfrustratedwarnedtranslatorrunninginfiltrate'

27.2.3. Strength Criteria#

These passwords are robust, but they are often rejected by systems because they do not contain uppercase letters, numbers, or special characters. We suggest the following modification:

  • The words that are concatenated will be capitalized, meaning each will start with an uppercase letter.

  • Some letters will be replaced by numbers and symbols (a:4, e:3, i:!, o:(), y:'/).

Modify the diceware function so that it returns capitalized words.

Write a function that takes a password as input and modifies it according to our substitution scheme.

Finally, write a function advanced_diceware that returns an altered Diceware password. If, by bad luck, the password does not meet the requirements (i.e., does not contain at least one uppercase letter, number, and special character), generate a new one.

def diceware(words, size):
    pass

#diceware(words, 6) # Ex: 'SpeechlessReprogrammedDismayAskingLedListens'
def alter_password(password):
    pass

alter_password("Testy") #"T3st'/"
def advanced_diceware(words, size):
    pass

#advanced_diceware(words, 6) # Ex: "D3n!3sT3chn()S!3g3B4rg4!n!ngD!s()b3'/!ngK!ll"

A simpler approach might have been to simply add the same information at the end of each password, such as ‘1!’. This would keep our passwords readable, but it’s probably less educational for learning Python!