269 lines
9.4 KiB
Python
269 lines
9.4 KiB
Python
"""CSC110 Fall 2021 Prep 9: Programming Exercises
|
|
|
|
Instructions (READ THIS FIRST!)
|
|
===============================
|
|
|
|
This Python module contains several function headers and descriptions.
|
|
We have marked each place you need to fill in with the word "TODO".
|
|
As you complete your work in this file, delete each TODO comment.
|
|
|
|
You do not need to include doctests for this prep, though we strongly encourage you
|
|
to check your work carefully!
|
|
|
|
Copyright and Usage Information
|
|
===============================
|
|
|
|
This file is provided solely for the personal and private use of students
|
|
taking CSC110 at the University of Toronto St. George campus. All forms of
|
|
distribution of this code, whether as given or with any changes, are
|
|
expressly prohibited. For more information on copyright for CSC110 materials,
|
|
please consult our Course Syllabus.
|
|
|
|
This file is Copyright (c) 2021 David Liu, Mario Badr, and Tom Fairgrieve.
|
|
"""
|
|
from dataclasses import dataclass
|
|
import math
|
|
|
|
|
|
####################################################################################################
|
|
# Part 1
|
|
#
|
|
# Most websites where you can create an account now ask you for a "strong password". The definition
|
|
# of strong password varies. In Part 1, you will implement 1 definition of a strong password and
|
|
# get some practice with functions that mutate their input.
|
|
#
|
|
# For those curious, these definitions of a "strong password" don't actually seem to be based on
|
|
# evidence. See: https://nakedsecurity.sophos.com/2014/10/24/do-we-really-need-strong-passwords/
|
|
#
|
|
# If you are still coming up with your own passwords (or, worse, reusing the same password for more
|
|
# than one account), please look into getting a password manager.
|
|
####################################################################################################
|
|
def is_strong_password(password: str) -> bool:
|
|
"""Return whether password is a strong password.
|
|
|
|
A strong password has at least 6 characters, contains at least one lowercase letter, at least
|
|
one uppercase letter, and at least one digit.
|
|
|
|
>>> is_strong_password('I<3csc110')
|
|
True
|
|
>>> is_strong_password('ilovelamp')
|
|
False
|
|
>>> is_strong_password('Ii0...')
|
|
True
|
|
>>> is_strong_password('Ii0..')
|
|
False
|
|
>>> is_strong_password('ii0...')
|
|
False
|
|
>>> is_strong_password('II0...')
|
|
False
|
|
>>> is_strong_password('Iii...')
|
|
False
|
|
"""
|
|
# Has at least 6 characters
|
|
if len(password) < 6:
|
|
return False
|
|
|
|
# Contains at least one lowercase letter
|
|
if all(chr(o) not in password for o in range(ord('a'), ord('z') + 1)):
|
|
return False
|
|
|
|
# Contains at least one uppercase letter
|
|
if all(chr(o) not in password for o in range(ord('A'), ord('Z') + 1)):
|
|
return False
|
|
|
|
# Contains at least one digit
|
|
if all(str(o) not in password for o in range(10)):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def remove_weak_passwords(passwords: list[str]) -> list[str]:
|
|
"""Remove and return the weak passwords in the given list of passwords.
|
|
|
|
A weak password is a password that is not strong.
|
|
|
|
This function both mutates the given list (to remove the weak passwords)
|
|
and returns a new list (the weak passwords that were removed).
|
|
"""
|
|
# NOTE: This implementation has an error, and does not do what the docstring claims.
|
|
# How would you explain the error? (Not to be handed in, but a good question for review!)
|
|
# After you have answered this question, comment out this incorrect code.
|
|
# weak_passwords = [p for p in passwords if not is_strong_password(p)]
|
|
# all_passwords = [p for p in passwords if is_strong_password(p)]
|
|
# return weak_passwords
|
|
|
|
# Next, in the space below, implement this function remove_weak_passwords correctly.
|
|
# Warning: do NOT attempt to mutate the passwords list while looping over it
|
|
# ("for password in passwords:"). This leads to unexpected errors in Python!
|
|
# Instead, first compute the passwords to remove, and then remove each of them
|
|
# from the list of passwords. (You can look up the "list.remove" method.)
|
|
weak_passwords = [pn for pn in passwords if not is_strong_password(pn)]
|
|
for p in weak_passwords:
|
|
passwords.remove(p)
|
|
return weak_passwords
|
|
|
|
|
|
def test_mutation() -> None:
|
|
"""Test that remove_weak_passwords correctly mutates its list argument.
|
|
|
|
Your test case should have at least one weak password in the input passwords.
|
|
"""
|
|
passwords = ['I<3csc110', 'ilovelamp']
|
|
remove_weak_passwords(passwords)
|
|
assert passwords == ['I<3csc110']
|
|
|
|
|
|
def test_return() -> None:
|
|
"""Test that remove_weak_passwords correctly returns a list of the weak passwords.
|
|
|
|
Your test case should have at least one weak password in the input passwords.
|
|
"""
|
|
passwords = ['I<3csc110', 'ilovelamp']
|
|
assert remove_weak_passwords(passwords) == ['ilovelamp']
|
|
|
|
|
|
####################################################################################################
|
|
# Part 2
|
|
#
|
|
# Ideally, the passwords we use when we create our accounts are not stored in plaintext. Otherwise,
|
|
# any hacker who finds their way into a company's server has just gained access to every password
|
|
# stored on that server, with no further hacking required. Here, you will implement some functions
|
|
# to encrypt passwords.
|
|
#
|
|
# Further reading (not required for this prep)
|
|
# ============================================
|
|
#
|
|
# Unfortunately, sometimes passwords are not stored securely.
|
|
# See: https://www.howtogeek.com/434930/why-are-companies-still-storing-passwords-in-plain-text/
|
|
#
|
|
# You may also find it interesting that encryption may not be the best method for storing passwords.
|
|
# Do a quick search for: hashing vs. encryption. Here are some relevant articles:
|
|
# - https://cheapsslsecurity.com/blog/hashing-vs-encryption/
|
|
# - https://cybernews.com/security/hashing-vs-encryption/
|
|
# - https://www.maketecheasier.com/password-hashing-encryption/
|
|
####################################################################################################
|
|
def divides(d: int, n: int) -> bool:
|
|
"""Return whether d divides n."""
|
|
if d == 0:
|
|
return n == 0
|
|
else:
|
|
return n % d == 0
|
|
|
|
|
|
def is_prime(p: int) -> bool:
|
|
"""Return whether p is prime."""
|
|
possible_divisors = range(2, math.floor(math.sqrt(p)) + 1)
|
|
return p > 1 and all({not divides(d, p) for d in possible_divisors})
|
|
|
|
|
|
def phi(n: int) -> int:
|
|
"""Return the number of positive integers between 1 and n inclusive that are coprime with n.
|
|
|
|
You can use the math.gcd function imported from the math module.
|
|
|
|
Preconditions:
|
|
- n >= 1
|
|
|
|
>>> phi(5)
|
|
4
|
|
>>> phi(15)
|
|
8
|
|
>>> phi(1)
|
|
1
|
|
"""
|
|
return sum(math.gcd(i, n) == 1 for i in range(1, n + 1))
|
|
|
|
|
|
@dataclass
|
|
class PublicKey:
|
|
"""A public key for the RSA cryptosystem.
|
|
|
|
This is good review of data classes: this data class is another way of representing
|
|
and RSA public key, rather than using tuples like we saw in class.
|
|
|
|
Done: Translate these representation invariants into Python expressions under
|
|
"Representation Invariants".
|
|
1. n is greater than 1
|
|
2. e is between 2 and phi(n) - 1, inclusive
|
|
3. e is coprime to phi(n) (again, you can use math.gcd)
|
|
|
|
Representation Invariants:
|
|
- 1 < n
|
|
- 2 <= e <= phi(n) - 1
|
|
- math.gcd(e, phi(n)) = 1
|
|
"""
|
|
n: int
|
|
e: int
|
|
|
|
|
|
@dataclass
|
|
class PrivateKey:
|
|
"""A private key for the RSA cryptosystem.
|
|
|
|
Done: translate these representation invariants into Python expressions under
|
|
"Representation Invariants".
|
|
1. p and q are both prime
|
|
2. p and q are not equal
|
|
3. d is between 2 and phi(p * q) - 1, inclusive
|
|
|
|
Representation Invariants:
|
|
- is_prime(p)
|
|
- is_prime(q)
|
|
- p != q
|
|
- 2 <= d <= (q - 1) * (p - 1) - 1
|
|
"""
|
|
p: int
|
|
q: int
|
|
d: int
|
|
|
|
|
|
def encrypt_password(public_key: PublicKey, password: str) -> str:
|
|
"""Return the password encrypted using public_key and the RSA cryptosystem.
|
|
|
|
Your implementation should be very similar to the one from class, except now
|
|
the public key is a data class rather than a tuple.
|
|
"""
|
|
return ''.join([chr(pow(ord(c), public_key.e, public_key.n)) for c in password])
|
|
|
|
|
|
def decrypt_password(private_key: PrivateKey, encrypted: str) -> str:
|
|
"""Return decrypt the given encrypted password using private_key and the RSA cryptosystem.
|
|
|
|
Your implementation should be very similar to the one from class, except now
|
|
the public key is a data class rather than a tuple.
|
|
"""
|
|
n = private_key.p * private_key.q
|
|
return ''.join([chr(pow(ord(c), private_key.d, n)) for c in encrypted])
|
|
|
|
|
|
def encrypt_passwords(public_key: PublicKey, passwords: list[str]) -> None:
|
|
"""Encrypt each password in passwords using the given public_key and the RSA cryptosystem.
|
|
|
|
This function mutates passwords, and does not return anything.
|
|
The encrypted passwords should appear in the same order as the
|
|
corresponding original passwords.
|
|
"""
|
|
for i in range(len(passwords)):
|
|
passwords[i] = encrypt_password(public_key, passwords[i])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import python_ta
|
|
|
|
python_ta.check_all(config={
|
|
'max-line-length': 100,
|
|
'extra-imports': ['math', 'dataclasses', 'python_ta.contracts'],
|
|
'disable': ['R1705', 'C0200']
|
|
})
|
|
|
|
import python_ta.contracts
|
|
python_ta.contracts.DEBUG_CONTRACTS = False
|
|
python_ta.contracts.check_all_contracts()
|
|
|
|
import doctest
|
|
doctest.testmod(verbose=True)
|
|
|
|
import pytest
|
|
pytest.main(['prep9.py', '-v'])
|