Python Portfolio Project - File Encrypter and Decrypter for Local System
Cryptography is the process of encoding information into a format that cannot be decoded without its respective, cryptographic key. In cybersecurity, cryptography utilizes two, vital functions: encryption and decryption. Encryption transforms plain-text data into ciphertext (randomized, unreadable text) using a cryptographic, encryption key. Decryption transforms ciphertext back into plain text using a cryptographic, decryption key.
Encryption keeps data at rest and data in transit confidential, prevents information from being altered, and ensures that the data exchanged between authorized parties is authentic.
This Python program allows for the encryption and decryption of files in your current working directory on your local, computer system using symmetric encryption. Symmetric encryption uses a single, cryptographic key to encrypt and decrypt data. This program will utilize the "Fernet" symmetric key algorithm to generate cryptographic keys for the encryption and decryption of files.
Python Program Components
The file encrypter/decrypter incorporates the following Python modules:
A great portion of this file encrypter is nested within a "while" loop, mainly to provide an opportunity for the user to run the program again in certain circumstances. The program begins by presenting the user with their current working directory, and gives the user the choice to change their current directory in the variable "change_dir_option":
In order to accommodate the potential inputs of the user from "change_dir_option", I nested a "match-case" statement; a "match" statement takes an expression and compares its value to successive patterns given as one or more "case" blocks. Case blocks are code blocks that will run different processes, according to an expression matching a certain pattern/case. Only the first pattern that matches gets executed.
In this case, the pattern to match is the input from the user in the variable "change_dir_option". If the input matches case "a", the program will allow the user to manually change their current working directory before any cryptography begins. If the input matches case "b", the program will not change the current working directory, and will be where all the cryptography functions will occur. If the user's input is neither case "a" or "b", the program will run the code block for case "_". This "_" case is a "catch-all" case for all other case inputs/patterns that are not "a" or "b".
In the code block for case "_", the program will notify the user that none of the listed options were selected, and give the user a choice to restart the file encrypter, or exit the program based on their input. The user's input will be stored in the variable "restart". If "restart" is "a", the nested "if" statement will restart the program (hence, the "while" loop) from the beginning. If "restart" is "b", the "if" statement will exit the program. If "restart" is neither "a" nor "b", the "if" statement will also terminate the Python program:
Defining Functions for Symmetric Encryption
Up until now, this program has only utilized the "time" and "os" modules, but at this point, the cryptographic functions of the "fernet" module will be showcased.
Applying symmetric encryption (SE) to files on our local system involves multiple steps and components; therefore, we need an efficient way to break down these components to achieve symmetric encryption. After extensive searching and research, I learned that storing the SE components into task-related, Python functions would be the most effective method for the stepwise process of encrypting and decrypting data.
Since symmetric encryption needs a cryptographic key available before it can encrypt and decrypt data, the first function we should define is one that will generate a cryptographic key and save it in our current working directory. This function will be defined as "write_key()":
Within the function, a cryptographic key is generated by the "generate.key()" method of the "Fernet" class, and is stored in the variable "key". The cryptographic key is generated through the "Fernet" symmetric key algorithm. Next, we combine the "with" statement with the "open()" function to create and open a new file known as "enc_key.key"; with the "as" statement, this new file will now be recognized under the variable alias "key_file" within our program. As a final step, the ".write()" method writes the newly-generated, cryptographic key into the open file "enc_key.key" and saves the file in our current directory.
Now we need a function that will load the newly-generated, cryptographic key from "enc_key.key" back into this Python program so we can persistently encrypt and decrypt our current directory files. This function will be defined as "load_key()".
The load_key() function simply consists of another combined "with-open()-as" function statement, where the "enc_key.key" is reopened within the encryption program under the variable alias "load_enc_key" and the reopened, cryptographic key will stored inside the "loaded" variable. Lastly, a return statement is used to return the value of the "loaded" variable, which is our integrated symmetric key, and ensures that our encryption key is available within the program prior to encryption and decryption:
So we have a function to generate a cryptographic key [write_key()] and a function to load that key back into our program [load_key()] — all we need are functions that encrypt and decrypt our data. Let's start with our "encrypt()" function:
The encrypt() function uses two positional parameters: "filename" and "key". The "filename" parameter will hold the literal name of the file we want to encrypt from our current directory; the "key" parameter will be the symmetric key that was created and is currently loaded into our Python program for use.
To encrypt a message, we must first create a "Fernet object" using the key created previously. An object is an instance of a class, and when an object is created from a class, the object will now inherit all the capabilities of their associated class. In this case, "Fernet" is the class, and the "key" parameter/variable is the object. Think of the cryptographic key we created as powerless ciphertext that gets supercharged with encryption and decryption capabilities once it is attached to the "Fernet" class. So we create a Fernet object in the form of "Fernet(key)", and store this in a variable called "f" (representing the "fernet" symmetric key algorithm).
The encrypt() function also applies the "with-open()-as" function statement, where the file in the "filename" parameter is opened within the encryption program under the variable alias "file" and and through the "read()" method, returns all of the specified file's data to prepare it for encryption, which is then stored inside "file_data" variable.
Next, we create a variable called "encrypted_data" and store the Fernet object (variable "f") along with the ".encrypt()" method that will be applied to the "file_data" variable. This means we are taking our loaded key and using it to encrypt the data for the file within the "file_data" variable. This process encrypts our data.
Lastly, we close the function with our last "with-open()-as" function statement, where the newly-encrypted contents of the file are saved, and all the plain text in the file are overwritten with ciphertext. This will result in the full encryption of the specified file within our current directory.
Now, let's focus on our "decrypt()" function:
Recommended by LinkedIn
The "decrypt()" function follows the same format as the "encrypt()" function: two positional parameters of "filename" and "key"; the creation of a Fernet object to give our give our loaded key cryptographic functionality; two "with-open()-as" function statements that open and store our file, and overwrites and save our file, respectively.
The main difference is we do not apply the ".encrypt()" method to the data in this function. Since the "decrypt()" function is presumed to decrypt already-encrypted data, we will utilize the ".decrypt()" method with the Fernet object ("f"), and apply it to the "encrypted_data" variable. This means we are taking our loaded key and using it to decrypt the data for the file within the "encrypted_data" variable. This process decrypts our data. The result of this function is the newly-decrypted contents of the file are saved, and all the ciphertext in the file are overwritten with its original, plain text. This will result in the full decryption of the specified file within our current directory.
We finally have all the functional components to achieve symmetric encryption on our local system, so can we look at the rest of the source code for our file encrypter.
Encryption in Action: File Encryption in Current Directory
Once the current working directory is established, the user will be prompted for input on whether to encrypt or decrypt a file, and the user's input will stored in the "crypt_option" variable for future use.
If "crypt_option" is "a", the "if" statement will call the "write_key()" function to generate the cryptographic key, and call the "load_key()" function to load the key into the program and store it in the variable "key". These functions will run in the background prior to file encryption.
The "if" statement will then execute a nested "for" loop to iterate through each file in the current working directory, which is made possible by "os.listdir(".")" as the iterable object. "os.listdir()" from the "os" module is used to get the list of all files and directories in the specified directory, and the only parameter for this function is the path of directory from which files will be listed. In our case, the directory path is our current directory, so the parameter for "os.listdir()" will be "." because it represents the current working directory.
Nested within the "for" loop is another "if" statement; the conditional statement executes with the "fnmatch" method from the "fnmatch" module to iterate through each file in the current working directory, and locates files that include various, text patterns. Each instance of the "fnmatch.fnmatch()" method will match a specific file extension; the file extension pattern will be preceded with a "wildcard" (*) that will accommodate different filenames. Some of the file extensions include ".txt", ".csv", ".log", ".rtf", ".xml" and ".html". If a file in the current directory has one of these file extensions, the program will list the file within its output. Essentially, the "for" loop acts as a filter for the plain text files we want to retrieve.
Once the "for" loop populates a list of plain text files in our current directory, we can select the file we want to encrypt by entering it into the prompt below. For demonstrative purposes, we will select "example03.txt" for encryption:
The encryption process will now begin by calling the "encrypt()" function with our "file" variable input as the first parameter, and our loaded key from our "key" variable as the second parameter. Once encryption is complete, the program will notify us of successful encryption:
After encryption, the user will be prompted for input on whether to immediately decrypt the recently-encrypted file, and the user's input will stored in the "decrypt_option" variable for future use. If "decrypt_option" is "a", the "if" statement will call the "load_key()" function to load the cryptographic key into the "key" variable, decrypt the file with "decrypt()" function, and exit the program. If "decrypt_option" is "b", the "if" statement will execute and exit the program:
Decryption in Action: File Decryption in Current Directory
If "crypt_option" is "b", the "if" statement will call the "load_key()" function to load the key into the program and store it in the variable "key". This function will run in the background prior to file decryption. Unlike file encryption, the "write_key()" function does not need to be called, for the cryptographic key is presumed to have already been generated at encryption, and the same key for encryption must be available for decryption to happen:
The "if" statement will then execute a similar code block to the "a" option in the "crypt_option" variable: a list of plain text files will be populated, and the user can enter the file they want to decrypt among the listed options. For expediency, we will select "example03.txt" again since it is already encrypted:
After decryption, the user will be prompted for input on whether to immediately encrypt the recently-decrypted file, and the user's input will stored in the "encrypt_option" variable for future use. If "encrypt_option" is "a", the "if" statement will call the "load_key()" function to load the cryptographic key into the "key" variable, encrypt the file with "encrypt()" function, and exit the program. If "encrypt_option" is "b", the "if" statement will execute and exit the program:
Conclusion
I am very proud of this file encrypter. Thank you so much for reading through this portfolio project! I know it was a long one. If you enjoyed my take with this Python program, please feel to reach out and connect. I'm always looking to network with people in the industry.
Until next time!
© 2023, Enoch Deboss