From 83a9eb301c89843ea7970fde27fa7a16dba751dc Mon Sep 17 00:00:00 2001 From: Hykilpikonna Date: Sun, 7 Nov 2021 13:31:08 -0500 Subject: [PATCH] [+] Prep9 --- practice/prep9.py | 268 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 practice/prep9.py diff --git a/practice/prep9.py b/practice/prep9.py new file mode 100644 index 0000000..4b589ad --- /dev/null +++ b/practice/prep9.py @@ -0,0 +1,268 @@ +"""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'])