<jupyter_start><jupyter_text>Breaking Codes with Python**Digital Skills Week - University of Hertfordshire**<NAME> and <NAME> **Welcome to this session on basic Python and utilising Python to break codes!**The aims of this session are:*   Describe what Python is *   Create a short "Hello World" program in Google Colab* Use Python Libraries to crack a code* Find the solution to the Puzzle! **What is Python?**Python was first developed in the 1980s by <NAME>. Python is a popular general-purpose programming language that can be used for a wide variety of applications. It includes high-level data structures, dynamic typing, dynamic binding, and many more features that make it as useful for complex application development as it is for scripting or "glue code" that connects components together. It can also be extended to make system calls to almost all operating systems and to run code written in C or C++. Due to its ubiquity and ability to run on nearly every system architecture, Python is a universal language found in a variety of different applications.It is one of the simplest but powerful programming languages in the world. Today we are going to take your first steps in to creating your first programs! **What is Google Colaboratory?**What is Colab?Colab, or ‘Colaboratory’, allows you to write and execute Python in your browser, with:*   Zero configuration required*   Free access to GPUs* Easy sharing **What is Cryptography?**Everybody is intrested in Cryptography at the moment, mainly due to the surge in interest around Cryptocurrencies.Cryptography is the study of secure communications techniques that allow only the sender and intended recipient of a message to view its contents. The term is derived from the Greek word kryptos, which means hidden. It is closely associated to encryption, which is the act of scrambling ordinary text into what's known as ciphertext and then back again upon arrival. In addition, cryptography also covers the obfuscation of information in images using techniques such as microdots or merging. Ancient Egyptians were known to use these methods in complex hieroglyphics, and Roman Emperor Julius Caesar is credited with using one of the first modern ciphers.  Hello World! **Lets get Started!**As is obligatory with all programming courses, our first step is to create a "Hello World" program to find our feet with the process. Hello World programs are used to illustrate how the process of coding works, as well as to ensure that a language or system is operating correctly. They are usually the first programs that new coders learn, because even those with little or no experience can execute Hello World both easily and correctly.<jupyter_code>#In programming, we often use comments within programs to explain what we are doing. In Python, if you start a line with a hash symbol "#" anything after the hash will be ignored by the program.
#I will be using the # to give you instructions on how to complete these tasks.

#"Hello World" in Python is very simple.
#the "print" command will print whatever is in brackets after it.
#Brackets are usied to show the command is a function ***EXPLAIN WHAT A FUNCTION IS***

#Below this line, write the following: print("my first program") and press the play button to the left hand side to run the script.

print("my first program")
#Amazing! You've successfully run your first code in Python! 

#You might be wondering why the statement to print is in speech marks. 
#There are many different types of data in the world, for example numbers (known as integers), Text (known as string), true/false (known as booleans)
#Sometimes we have to tell the computer what kind of data we will be giving it - in a lot of programming languages, you have to be really explicit and declare your variables. This generally is not the case
#with python and it will figure it out for you!
#Lets have a play with some data types - you've already learnt how to use strings i.e. you place them within ("")
#How about a number? 
#Type print(1) and run the program.

print(1)
#At this rate, you'll be breaking codes in no time at all!

#Lets have a conversation about indentation! A lot of programming languages use "curly braces" to indicate, essentially, that all this bit of the code belongs together - a bit like grouping things together.

#In this section, I am also going to introduce some maths functions. Most programs are a series of maths functions and "if" this happens "then" do this. It does get very complex when we start combining all of these functions
#together but just like real life, you don't have to re-invent the wheel. Pythong has many reference libraries that you can use with simple commands to acheive complex outcomes!

#Lets start by declaring a variable called "x" and giving that variable a value.
#Type: x=1 
#then, using your knowledge above, ask python to print the value of X
#HINT in this situation X is used a mathmatical number, NOT a string so you do not need to us "" around the X

x=1
print(x)

#Great! But lets say we only want to print X if X is equal to 1 - this is where we need to add Indentations to indicate the series of commands are related

#Try running this:

x = 1
if x == 1:
    # indented four spaces
    print("x is 1.")

#Now, change X to 2 and run the code again.
#You'll see that nothing is printed but no error messages are sent either, which is what we expected!
#You'll also note that on the second line, we use == to indicate "is exactly equal to"
%reset
#Let's add another bit to our code so that when X is not 1, Python lets us know about this.

#Try running this:

x = 2
if x == 1:
    # indented four spaces
    print("x is 1.")
else:
    print("x is not 1")

#Try changing the value of X to 1 and re-running the code
%reset
#Beautiful! A lot of cryptography relies on manipulating data, so lets practice with some maths functions. Try the code below:

x=27
y=5
print(x+y)

#Woohoo! We've got a programatic calculator! You already have the skills to produce a program that can complete complex maths problems!
#Try changing the symbol in print(x+y) from a + to other maths functions. The simplest in python are: + (addition), - (subtraction), * (multiply), / (divide)
%reset
#There are some more complex maths functions such as: ** (to the power of), % (modulus i.e "return the remainder") for example and also comparison operators, such as: < (less than), > (greater than), == (equal to)
#!= (not equal to), Less than or Equal to (<=), Greater than or equal to (>=)

#We've now almost learnt enough code to create a program to tell us if a number is odd or even
#The key to odd or even programs is the % operator. Any number that can be divided by 2 WITHOUT a remainder is even. Lets try this out first before we put it in to a program:

x=4
y=7

#We know that 4 is even, so if we use the modulus operator % by 2, the answer should be 0 because 2 divides in to 4 with no remainder
#We also know 7 is odd, so if we use the modulus operator % by 2, the answer should be 1 because 2 divides in to 7 with 1 remainder
#we are also going to instroduce an naming variables based on what they are. 
#Try running this code and see what happens. You can change the values of x and y and re-run this code.

answerOne = x%2
print (answerOne)
answerTwo = y%2
print (answerTwo)



%reset
#Looking good! Although, if we were making an app, we would probably want the user to input their number and then the program to assess this to be odd or even and then return the answer to the user in plain text.

#Thinking about the steps in creating programs, its often useful to plan out your steps in plain language, like so:

#User inputs number
#Program uses modulus by two to determine if there is a remainder
#If modulus equals zero, then print "this number is even"
#If modulus equals NOT zero, then print "This number is Even"

#See how stating what we want to acheive, makes the process a lot clearer. You already know how to find the modulus, make a statement based on the output and return this to the user.

#The only thing we are not certain of, is how to take a user input, but don't worry - lets tackle that now, then you can have a go at creating this useful little program.

#Its really easy to do this in python, the command is "input" Try running this code:

username = input("Enter username:")
print("Username is: " + username)


%reset
#You may notice that this returns a string - this is no good for maths as we want our data to be an integer (a number). In python, we have to be explicit about this so we add an "int" function before the input function.

#Try this: 

x = int(input("Enter a number: "))
y = int(input("Enter a number: "))
print (x+y)
%reset<jupyter_output><empty_output><jupyter_text>The Odd-Even Machine  **You've made it this far - WELL DONE!**This is your first test - you have all the code you need to create an odd or even machine!Remember to break it down and code each bit seperately. You need to:* User inputs number* Program uses modulus by two to determine if there is a remainder* If modulus equals zero, then print "this number is even"* If modulus equals NOT zero, then print "This number is Even"Try your best to create this program in the space below:<jupyter_code>#Enter your program here


%reset<jupyter_output><empty_output><jupyter_text>Encoding and Decoding **Amazing Work**In order to break codes, we will need to introduce more complex functions and actions, but now you have a basic understanding, we can cover these as we break some codes!The art of concealing information to induce secrecy in the communication and transmission of sensitive data is termed cryptography. Diving deep into the etymology of the word ‘cryptography’ shows that this word finds its origin in ancient Greek. Derived from words kryptos meaning “hidden” or “secret” and graphy meaning “writing”, cryptography literally means writing something secretly.There are lots of real life examples of this and why it is needed. Many people think of the military and examples such as the enigma machine but everyday your normal routine actions are facilitated by modern cryptography - you wouldn't want your online banking or card details to be boradcast across the internet in plain text!Common terminologies that are encountered in the word of cryptography are:* A simple text, easily perceived by a human, is called plaintext or cleartext.* The process of using mathematical algorithms to disguise sensitive information in plaintext is called encryption.* These algorithms, also known as ciphers, are a series of well-defined steps to make the secret message actually unbreakable for any adversary. After encryption, you get a ciphertext that does not make sense at all. This is the step where you have your message hidden.* To make the algorithm work, you need a key unique to that algorithm and message.* Now, in order to decrypt the encrypted text, the key and the name of the algorithm has to be known. This conversion of ciphertext back to plaintext is called decryption.In order to get the same plaintext from the decryption algorithm, we must always provide the same key. If the key is tampered, the output would be unexpected, undesirable, or usually unwanted.**If the above sounds complex, don't worry - we will take it step by step and break everything down. We will start with very simple cryptography and move on from there.**<jupyter_code>#Let's create our first encryption-decryption program!

#In this excercise we will:
# *Take a users input
# *Encode this by reversing the letters
#return the encoded message

#Obviously, simply reversing the text is very easy to break but lets not worry at the moment how secure our program is, lets just get the basic structure together as we can always improve the encoding later

#since we already know how to take a users input, do something with it and then return something based upon that input - we just need to figure out how to reverse our text to encode it.

#There is no built-in function to reverse a String in Python.

#The fastest (and easiest?) way is to use a slice that steps backwards, -1.

#Try running this code

txt = "Hello World"[::-1]
print(txt)

#you'll notice there are a couple of things in this code we haven't covered yet, mainly the [::-1] part
#The sqaure brackets denote an ARRAY. Arrays are super useful for holding lists or tables of data. up until this point, we've just used variables which relate to a single piece of data i.e x=1
#But often this is too simplistic for many data types so we need to hold each seperate piece of data in an array so we can manipulate it.

#The slice notation in python has the following syntax: list[<start>:<stop>:<step>]
# means that for a given string/list/tuple, you can slice the said object using the format <object_name>[<start_index>, <stop_index>, <step>]

#Try adjusting the -1 and re-running the code to see the impact it has. You can even remove the -1 all together! 

%reset<jupyter_output><empty_output><jupyter_text>**OK! We have our first algorithm to encode a message**Admittedly, its really easy to break but with everything in programming, we always start simply and build up to our final task.So now we have this element of our code, we can start to build our decoding and encoding program. In this example, we don't need to tell the program if we are encoding or decoding because the algorithm will simply reverse whatever we give it.So lets create the program! Remember, these are the steps:* Let a user input a string to be coded* Encode the input* Retun the encoded stringWe already have all the code to do this, so lets buid this piece by piece together.<jupyter_code>#Right, our first task is to accept a user input
#In this case, we will be using string data (or letters) so we can simply do the following:

secretMsg = input("Enter string to be encoded or decoded:")

#Now we need to process this message with our backwards algorythm

encodedMsg = secretMsg[::-1]

#Now we just need to return the encoded message back to the user

print(encodedMsg)

%reset<jupyter_output><empty_output><jupyter_text>**THIS IS AN EMERGENCEY BROADCAST**We are sorry to interrupt this training session withe following message:43-D53BHoIx=v?hctaw/moc.ebutuoy.www//:sptth :metsys habogad eht ot levart tsum uoY**Decode this message** **OK, so that was pretty easy**We probaby want to make our encryption a little more complex so its a bit harder to crack!We refer to the above cipher as a Reverse Cipher. The next encoding method we will use is the Ceaser Cipher. Whilst a reverse Cipher on its own is easy to crack, adding a reverse cipher to another cipher adds a layer of additional security.To begin with we will create a program to encode with a Ceaser Cipher and then add a reverse cipher to make it even harder for someone to crack.**The Ceaser Cipher**The algorithm of Caesar cipher holds the following features −Caesar Cipher Technique is the simple and easy method of encryption technique.It is simple type of substitution cipher.Each letter of plain text is replaced by a letter with some fixed number of positions down with alphabet.![algorithm_caesar_cipher.jpg](data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAD7AisDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKR<KEY>DQ//ddH/D/T/qyv9v8A/wDDQ/8A<KEY>RQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAf/2Q==)<jupyter_code>#Ok, so this is not quite as easy as the reverse cipher!

#First thing that is important to note is computers don't recognise letters - each letter on your keyboard is stored in computer memory in something called Unicode (originally this was something called ASCII but was too limited in its
#scope to encode all the characters of the world as it was only 8 bit). 
#This is a 16-bit number and encodes almost all the English language’s characters, digits, and punctuations.

#For instance, the uppercase ‘A’ is represented by the number 65, ‘B’ by 66, and so on. Similarly, lowercase characters’ representation begins with the number 97.

#For our purposes, its going to be a lot easier and make more sense if we use the built in functions within Python to convert our messages to Unicode before we manipulate them

#You can use the ord() method to convert a character to its numeric representation in Unicode. It accepts a single character and returns the number representing its Unicode. eg:

letter_unicode=ord("d")
print ("Unicode =", letter_unicode)

#Try changing the "c" to another letter like "d"

%reset
#Just like how we could convert a character into its numeric Unicode using ord() method, we do the inverse i.e., find the character represented by a number using chr() method.

#Using the below code, decode the Unicode "64" in to normal letters:

letter_normal=chr(45)
print ("Decoded Unicode =", letter_normal)
%reset<jupyter_output><empty_output><jupyter_text>Now that we understand the two fundamental methods we’ll use, let’s implement the encryption technique for capital letters in Python. We shall encrypt only the uppercase characters in the text and will leave the remaining ones unchanged - remember all keyboard text has a unicode equivalent but we don't want to encrypt the space at this point.Let’s first look at the step-by-step process of encrypting the capital letters:1. Define the shift value i.e., the number of positions we want to shift from each character.2. Iterate over each character of the plain text:3. If the character is upper-case:* Calculate the position/index of the character in the 0-25 range.* Perform the positive shift using the modulo operation.* Find the character at the new position.* Replace the current capital letter by this new character.4. Else, If the character is not upper-case, keep it with no change.Couple of things to note in this plan:Iterate means go through all the letters in the message and apply until you run out of letterThere are 26 letters in the Alphabet however arrays start at 0, this is why the charactyer range is from 0-25<jupyter_code>shift = 5 # defining the shift count - Try changing this to see the impact on the encoded message

text = "HELLO WORLD"

encryption = ""

#the next line means, for each individual letter in the variable "text", do the following. We use "c" as this variable but it could be any letter or word etc. It's sort of tradition to use X, Y, Z as Integer variable names
#
for c in text:

    # check if character is an uppercase letter
    if c.isupper():

        # find the position in 0-25
        c_unicode = ord(c)

        c_index = ord(c) - ord("A")

        # perform the shift
        new_index = (c_index + shift) % 26

        # convert to new character
        new_unicode = new_index + ord("A")

        new_character = chr(new_unicode)

        # append to encrypted string
        encryption = encryption + new_character

    else:

        # since character is not uppercase, leave it as it is
        encryption += c

       
print("Plain text:",text)

print("Encrypted text:",encryption)

%reset<jupyter_output><empty_output><jupyter_text>**CHALLENGE**Using the above code and what you have learnt, can you adjust this code to accept user input?**BONUS** Can you add the backwards cipher as well to this encryption program?<jupyter_code>shift = 5 # defining the shift count - Try changing this to see the impact on the encoded message. If you have an encoded message and know the shift number, you can decode the message by turning this in to a negative number i.e -5

text = input("Enter string in capitals to be encoded or decoded:")

encryption = ""

for c in text:

    # check if character is an uppercase letter
    if c.isupper():

        # find the position in 0-25
        c_unicode = ord(c)

        c_index = ord(c) - ord("A")

        # perform the shift
        new_index = (c_index + shift) % 26

        # convert to new character
        new_unicode = new_index + ord("A")

        new_character = chr(new_unicode)

        # append to encrypted string
        encryption = encryption + new_character

    else:

        # since character is not uppercase, leave it as it is
        encryption += c
        
encryptionBackwards = #ADD THE CORRECT CODE HERE TO REVERSE THE ANSWER

print("Plain text:",text)

print("Encrypted text:",encryption)

print("Reversed Encrypted Text:", encryptionBackwards)
%reset<jupyter_output><empty_output><jupyter_text>As you might have guessed, the key to cracking a ceaser cipher is knowing the shift number. This is fairly limited as there are only 26 possible solutions.This means we can crack a ceaser cipher using a technique called "Brute Force"Brute Forcing is a method where a hacker will iterate through all possible combinations of a key in order to break a password or code. In real life, this rarely works as password fields normally limit the number attempts of guessing and cracking dictionaries are huge. Although, if you have a common password, it *might* just work (HINT: change your passwords regularly!!)Lets write a program that uses brute force to hack a password that was encrypted using a Ceaser Cipher.Try cracking this: UDYMTSNXFBJXTRJ<jupyter_code>#ok, we are going to stick with our upper case only letters.
#First we need to write a function that accepts a ciphertext and a key, and decrypts all its uppercase letters. This is basically the same as before. This is just a function the computer will use on the text we give it.

def cipher_decrypt_upper(ciphertext, key):

    decrypted = ""

    for c in ciphertext:

        if c.isupper(): 

            c_index = ord(c) - ord('a') 

            c_og_pos = (c_index - key) % 26 + ord('a')

            c_og = chr(c_og_pos)

            decrypted += c_og

        else:

            decrypted += c

    return decrypted

#The thing we want to know is, what is the key i.e. the number of shifts the text has been put through. We wre going to write a bit of code that uses the above function to translate text using all 26 availble positions.

cryptic_text = input("Enter text in capitals to be cracked:") #This is our user input

for i in range(0,26):

    plain_text = cipher_decrypt_upper(cryptic_text, i)

    print("For key {}, decrypted text: {}".format(i, plain_text))

#The output will be a list of all the possible decryptions - one of them will look more like a word than the others
%reset<jupyter_output><empty_output><jupyter_text>**Emergency Broadcast**An operative deep inside a competitor's complex is attempting to hack this OneNote page in order to discover the secrets within. On the desk of the rival is a post it note with the following written down: UBKHIDEWQHTUsing the above programs and the knowledge you have gained through this session, hack the OneNote and discover the secret documents.https://herts365-my.sharepoint.com/:o:/g/personal/kw20abg_herts_ac_uk/Ej0UskWx7kVDmq-LqCphGG0BQk_EZFoju3zHqgVJshkbSw?e=rgOye4**Hint** This password might be additionally encoded . . . maybe it is backwards . . . **You've got the document, but . . .**This looks really complex . . . One of the great things about Python is we can use already created libraries to aide us. This file is encrypted using something called FERNET, other complex ciphers its what Bitcoin uses for it's secure transactions and it's the mathmatical function it's miners are trying to solve.A hash is defined as a mathematical function that takes input of arbitrary length and turns it into output with a fixed length. Let's build one.Take the number 123Now take the letters of your text, and assign numbers to them (just like a computer naturally does with Unicode), lets go with the simple A=1, B=2, ... scheme, with 27 for space. This translates "Hello" to 8 5 12 12 15. Now multiply the numer you currently have (123) with the first letter's number. If the result is longer than 3 digits, only remember the last three. Repeat for the other letters:123 * 8 = 984984 * 5 = 4920920 * 12 = 11040040 * 12 = 480480 * 15 = 7200Congratulations, you turned "Hello" into 200. This is a horrible scheme (for starters, it reacts really really badly to the letter J because J equals 10, the above scheme results in any word starting with J always equaling 000), but it should explain the principle.A real hash function that is often used is DJBX33A: Start with 5381, take the current number, multiply it by 33, and add the number of the letter. However, that is not a cryptographic hash like SHA2. For that, the art is to make sure certain additional properties are met, e.g. an attacker cannot find two sentences that result in the same number. This is the art of modern cryptography. The maths are horrible, but thanks to Python, we can use these forms of encryption without ever having to tackle this level of maths! Programs like 7Zip allow you to encrypt your files using SHA-256 without code.**For the Brave amongst you**The best way to appreciate the complexity of modern encryption is to compare our encryptions to a real one. Both our encryption methods were pretty weak, they didn't take much to crack because the mathmatical functions we applied to them were simple. You could add much more complex mathmatics to what we have here by adding more operations.SHA256 works by splitting the input into 512bit sized blocks (if the input is not a multiple of it, the input will be padded i.e. extra stuff added to make it the correct length), resulting in **n** 512bit blocks with 64bytes each, called **M1** to **Mn**. Then a value **X0** is initialized to a default 256bit value defined by the standard, which are the first 32 bits of the decimal place of the square root of the first 8 prime numbers.Then, the algorithm will process each block in sequence, calculating **Xi** as **Xi = f(Mi, Xi-1)**, and after the last round the final hash value is in **Xn**.The true magic of SHA256 happens in the function **f**, which takes a 512bit block and a 256bit block (**Mi** and **Xi-1** respectively), and compresses them down to a 256bit block. Each round works the following:Expand the 512bit block to a 64word array **W**Copy the 16 words of the input into **W0** to **W16**For the remaining 48words, apply the following method: **W[t] = MIX(W[t - 2], W[t - 7], W[t - 15], W[t - 16])**. The operation goes over the remaining 48 words and applies a series of binary operations on them. Then, go over all 64words and compress them down to 256bit, using more binary operations.Yeah . . . ;DWell, here's a much easier way to encrypt the document . . . if you have the key . . .<jupyter_code>#We are going to import someone elses library here to help us decrypt our message. We need to run the below command to download the library and install it on this virtual machine
#In Google Colab, we use the ! to show that this bit of code is not in python but is a command line prompt.

!python -m pip install cryptography

#Import the library that does all the magic
from cryptography.fernet import Fernet

#There are two main ways to get a key, we can either generate a new one or use one that has previously been generated. These keys need to be in a particular format so make sure to get this right.
#To generate a new random key, we can simply use
key = Fernet.generate_key()
print (key)


#To encrypt a message, you will need a key (as previously discussed) and your message as type bytes (you can convert strings to bytes using .encode()).

from cryptography.fernet import Fernet
message = "My Secret Message".encode()

f = Fernet(key)
encrypted = f.encrypt(message)  # Encrypt the bytes. The returning object is of type bytes

print (encrypted)
from cryptography.fernet import Fernet
encrypted = b"...encrypted bytes..."

f = Fernet(key)
decrypted = f.decrypt(encrypted)  # Decrypt the bytes. The returning object is of type bytes

print (decrypted)<jupyter_output><empty_output><jupyter_text>Lets decrypt the final message in the document!So you have a key (note that keeping your key in the same place as your data is a really bad idea!)Replace the below code with values from the document to win the final puzzle!<jupyter_code>from cryptography.fernet import Fernet

#replace b"...encrypted bytes..." with your message
encrypted = b"...encrypted bytes..."

#replace key with the private key i.e: b'gcDpjPpXee3B2JtpbnF0CGTaowGgbFMS46eU2tKXm_M='
f = Fernet(key)
decrypted = f.decrypt(encrypted)  # Decrypt the bytes. The returning object is of type bytes

print (decrypted)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.05445935280189424, 'fraction_numerical': 0.010466353248001098, 'mean_word_length': 4.358955498344979, 'pattern_counts': {'":': 0, '<': 55, '<?xml version=': 0, '>': 55, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 3, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '105616', 'license': 'MIT', 'n_tokens_mistral': 8231, 'n_tokens_neox': 7695, 'n_words': 4451, 'path': 'SESSION_VERSION_Code_Breaking_with_Python.ipynb', 'repo_name': 'KVengeance/CodeBreakingWithPython-', 'size': '71630'}