CSC232 Informaton Security Lab Manual SP24 V2.0
CSC232 Informaton Security Lab Manual SP24 V2.0
i
Table of Contents
Lab # Main Topic Page #
Lab 01 Python Programming-Part I 1
Lab 02 Python Collections-Part II 14
Lab 03 Classical Ciphers 38
Lab 04 Stream Ciphers and PRG 49
Lab 05 Block Ciphers 65
Lab 06 Secure Hash Function 84
Lab 07 Blockchain Technology 96
Lab 08 Midterm-Exam 115
Lab 09 Key distribution problem 118
Lab 10 Cryptographic Math 126
Lab 11 ElGamal encryption 135
Lab 12 RSA 141
Lab 13 Elliptic Curve Cryptography 145
Lab 14 Digital signatures 149
Lab 15 Public-Key Certificates (PKC) 164
ii
Lab 01
Python Introduction
Objective:
This lab is an introductory session on Python. The lab will equip students with necessary
concepts needed to build algorithms in Python.
Activity Outcomes:
The lab will teach students to:
Basic Operations on strings and numbers
Basic use of conditionals
Basic use of loops
Instructor Note:
As a pre-lab activity, read Chapters 3-5 from the book (Introduction to Programming Using
Python - Y.Liang (Pearson, 2013)) to gain an insight about python programming and its
fundamentals.
1
1) Useful Concepts
Python refers to the Python programming language (with syntax rules for writing what is
considered valid Python code) and the Python interpreter software that reads source code
(written in the Python language) and performs its instructions.
Python is a general-purpose programming language. That means you can use Python to write
code for any programming task. Python is now used in the Google search engine, in mission-
critical projects at NASA, and in transaction processing at the New York Stock Exchange.
Python's most obvious feature is that it uses indentation as a control structure. Indentation is
used in Python to delimit blocks. The number of spaces is variable, but all statements within
the same block must be indented the same amount. The header line for compound statements,
such as if, while, def, and class should be terminated with a colon ( : )
Variables
Python is dynamically typed. You do not need to declare variables! The declaration
happens automatically when you assign a value to a variable. Variables can change type, simply
by assigning them a new value of a different type. Python allows you to assign a single value to
several variables simultaneously. You can also assign multiple objects to multiple variables.
Integers
In Python 3, there is effectively no limit to how long an integer value can be. Of course, it is
constrained by the amount of memory your system has, as are all things, but beyond that an
integer can be as long as you need it to be:
>>> print(123123123123123123123123123123123123123123123123 + 1)
123123123123123123123123123123123123123123123124
Python interprets a sequence of decimal digits without any prefix to be a decimal number:
>>> print(10)
10
The following strings can be prepended to an integer value to indicate a base other than 10:
2
Prefi Interpretation Base
x
0b (zero + lowercase letter 'b') Binary 2
0B (zero + uppercase letter 'B')
0o (zero + lowercase letter 'o') Octal 8
0O (zero + uppercase letter 'O')
0x (zero + lowercase letter 'x') Hexadecimal 16
0X (zero + uppercase letter 'X')
For example:
>>> print(0o10)
8
>>> print(0x10)
16
>>> print(0b10)
2
The underlying type of a Python integer, irrespective of the base used to specify it, is called int:
>>> type(10)
<class 'int'>
>>> type(0o10)
<class 'int'>
>>> type(0x10)
<class 'int'>
Floating-Point Numbers
The float type in Python designates a floating-point number. float values are specified with a
decimal point. Optionally, the character e or E followed by a positive or negative integer may
be appended to specify scientific notation:
>>> 4.2
4.2
>>> type(4.2)
<class 'float'>
>>> 4.
4.0
>>> .2
0.2
>>> .4e7
4000000.0
>>> type(.4e7)
<class 'float'>
>>> 4.2e-4
0.00042
3
Floating-Point Representation
The following is a bit more in-depth information on how Python represents floating-point
numbers internally. You can readily use floating-point numbers in Python without understanding
them to this level, so don’t worry if this seems overly complicated. The information is
presented here in case you are curious. Almost all platforms represent Python float values as
64-bit “double-precision” values, according to the IEEE 754 standard. In that case, the
maximum value a floating-point number can have is approximately 1.8 ⨉ 10308. Python will
indicate a number greater than that by the string inf:
>>> 1.79e308
1.79e+308
>>> 1.8e308
Inf
The closest a nonzero number can be to zero is approximately 5.0 ⨉ 10-324. Anything
closer to zero than that is effectively zero:
>>> 5e-324
5e-324
>>> 1e-325
0.0
Floating point numbers are represented internally as binary (base-2) fractions. Most decimal
fractions cannot be represented exactly as binary fractions, so in most cases the internal
representation of a floating-point number is an approximation of the actual value. In practice,
the difference between the actual value and the represented value is very small and should not
usually cause significant problems.
Complex Numbers :
Complex numbers are specified as <real part>+<imaginary part>j. For example:
>>> 2+3j
(2+3j)
>>> type(2+3j)
<class 'complex'>
Strings
Strings are sequences of character data. The string type in Python is called str.
String literals may be delimited using either single or double quotes. All the characters
between theopening delimiter and matching closing delimiter are part of the string:
>>> print("I am a string.")
I am a string.
>>> type("I am a string.")
<class 'str'>
>>> print('I am too.')
I am too.
>>> type('I am too.')
<class 'str'>
4
A string in Python can contain as many characters as you wish. The only limit is your
machine’s memory resources. A string can also be empty:
What if you want to include a quote character as part of the string itself? Your first impulse might be to
>>> ''
''
try something like this:
5
Specifying a backslash in front of the quote character in a string “escapes” it and causes
Python tosuppress its usual special meaning. It is then interpreted simply as a literal single
quote character:
>>> print('This string contains a single quote (\') character.')
This string contains a single quote (') character.
The following is a table of escape sequences which cause Python to suppress the
usual special interpretation of a character in a string:
Escape Usual Interpretation of “Escaped” Interpretation
Sequence Character(s) After
Backslash
\' Terminates string with single quote opening Literal single quote (') character
delimiter
\" Terminates string with double quote opening Literal double quote (")
delimiter character
\<newline> Terminates input line Newline is ignored
\\ Introduces escape sequence Literal backslash (\) character
Ordinarily, a newline character terminates line input. So Enter in the middle of a string will
pressing cause Python to think it is incomplete:
>>> print('a
SyntaxError: EOL while scanning string literal
To break up a string over more than one line, include a backslash before each newline,
and the newlines will be ignored:
>>> print('a\
... b\
... c')
Abc
>>> print('foo\tbar')
foo bar
The escape sequence \t causes the t character to lose its usual meaning, that of a literal t.
Instead, the combination is interpreted as a tab character.
Here is a list of escape sequences that cause Python to apply special meaning instead of
interpreting literally:
Escape Sequence “Escaped”
Interpretation
\a ASCII Bell (BEL) character
\b ASCII Backspace (BS) character
\f ASCII Formfeed (FF) character
\n ASCII Linefeed (LF) character
\N{<name>} Character from Unicode database with given <name>
\r ASCII Carriage Return (CR) character
\t ASCII Horizontal Tab (TAB) character
\uxxxx Unicode character with 16-bit hex value xxxx
\Uxxxxxxxx Unicode character with 32-bit hex value xxxxxxxx
\v ASCII Vertical Tab (VT) character
\ooo Character with octal value ooo
\xhh Character with hex value hh
Examples:
>>> print("a\tb")
a b
>>> print("a\141\x61")
aaa
>>> print("a\nb")
a
b
>>> print('\u2192 \N{rightwards arrow}')
→→
This type of escape sequence is typically used to insert characters that are not readily
generated from the keyboard or are not easily readable or printable.
7
Raw Strings
A raw string literal is preceded by r or R, which specifies that escape sequences in the
associated string are not translated. The backslash character is left in the string:
>>> print('foo\nbar')
8
foo
bar
>>> print(r'foo\nbar')
foo\nbar
>>> print('foo\\bar')
foo\bar
>>> print(R'foo\\bar')
foo\\bar
Triple-Quoted Strings
There is yet another way of delimiting strings in Python. Triple-quoted strings are delimited
by matching groups of three single quotes or three double quotes. Escape sequences still
work in triple- quoted strings, but single quotes, double quotes, and newlines can be included
without escaping them. This provides a convenient way to create a string with both single and
double quotes in it:
>>> print('''This string has a single (') and a double (") quote.''')
This string has a single (') and a double (") quote.
Because newlines can be included without escaping them, this also allows for multiline strings:
>>> print("""This is a
string that spans
across several lines""")
This is a
string that spans
across several lines
You will see in the upcoming tutorial on Python Program Structure how triple-quoted
strings can be used to add an explanatory comment to Python code.
As you will see in upcoming tutorials, expressions in Python are often evaluated in Boolean
context, meaning they are interpreted to represent truth or falsehood. A value that is true in
Boolean context is sometimes said to be “truthy,” and one that is false in Boolean context is
said to be “falsy.” (You may also see “falsy” spelled “falsey.”)
The “truthiness” of an object of Boolean type is self-evident: Boolean objects that are equal to
True are truthy (true), and those equal to False are falsy (false). But non-Boolean objects can
be evaluated in Boolean context as well and determined to be true or false.
9
2) Solved Lab Activites
Activity 1:
Let us take an integer from user as input and check whether the given value is even or not. If
the given value is not even then it means that it will be odd. So here we need to use if-else
statement an demonstrated below
A. Create a new Python file from Python Shell and type the following code.
B. Run the code by pressing F5.
Output
Activity 2:
Write a Python code to keep accepting integer values from user until 0 is entered. Display
sum of the given values.
Solution:
10
Output
Activity 3:
Write a Python code to accept an integer value from user and check that whether the given
value is prime number or not.
Solution:
Activity 4:
Accept 5 integer values from user and display their sum. Draw flowchart
before coding in python.
Solution:
Create a new Python file from Python Shell and type the following code. Run the code by
pressing F5.
11
You will get the following output.
Activity 5:
Calculate the sum of all the values between 0-10 using while loop.
Solution:
Create a new Python file from Python Shell and type the following code.
Run the code by pressing F5.
Activity 6:
Take input from the keyboard and use it in your program.
Solution:
In Python and many other programming languages you can get user input. In
Python the input() function will ask keyboard input from the user.The input
function prompts text if a parameter is given. The function reads input from the
keyboard, converts it to a string and removes the newline (Enter).Type and
experiment with the script below.
#!/usr/bin/env python3
12
Activity 7:
Generate a random number between 1 and 9 (including 1 and 9). Ask the user to guess the
number, then tell them whether they guessed too low, too high, or exactly right. (Hint:
remember to use the user input lessons from the very first exercise)
Extras:
Keep the game going until the user types “exit”
Keep track of how many guesses the user has taken, and when the game ends, print this out.
Solution:
import random
# Awroken
MINIMUM = 1
MAXIMUM = 9
NUMBER = random.randint(MINIMUM, MAXIMUM)
GUESS = None
ANOTHER = None
TRY = 0
RUNNING = True
print "Alright..."
while RUNNING:
GUESS = raw_input("What is your lucky number? ")
if int(GUESS) < NUMBER:
print "Wrong, too low."
elif int(GUESS) > NUMBER:
print "Wrong, too high."
elif GUESS.lower() == "exit":
print "Better luck next time."
elif int(GUESS) == NUMBER:
print "Yes, that's the one, %s." % str(NUMBER)
if TRY < 2:
print "Impressive, only %s tries." % str(TRY)
elif TRY > 2 and TRY < 10:
print "Pretty good, %s tries." % str(TRY)
else:
print "Bad, %s tries." % str(TRY)
RUNNING = False
TRY += 1
13
Lab Task 1:
Write a program that prompts the user to input an integer and then outputs the number with
the digits reversed. For example, if the input is 12345, the output should be 54321.
Lab Task 2:
Write a program that reads a set of integers, and then prints the sum of the even and odd integers.
Lab Task 3:
Fibonacci series is that when you add the previous two numbers the next number is formed.
You have to start from 0 and 1.
E.g. 0+1=1 → 1+1=2 → 1+2=3 → 2+3=5 → 3+5=8 → 5+8=13
Steps: You have to take an input number that shows how many terms to be displayed. Then
use loops for displaying the Fibonacci series up to that term e.g. input no is =6 the output
should be
011235
Lab Task 4:
Write a Python code to accept marks of a student from 1-100 and display the grade
according to the following formula.
Lab Task 5:
Write a program that takes a number from user and calculate the factorial of that number.
14
Lab 02
Python Lists and Dictionaries
Objective:
This lab will give you practical implementation of different types of sequences including lists,
tuples, sets and dictionaries. We will use lists alongside loops in order to know about indexing
individual items of these containers. This lab will also allow students to write their own
functions.
Activity Outcomes:
This lab teaches you the following topics:
How to use lists, tuples, sets and dictionaries How to use loops with lists
How to write customized functions
Instructor Note:
As a pre-lab activity, read Chapters 6, 10 and 14 from the book (Introduction to Programming
Using Python - Y. Liang (Pearson, 2013)) to gain an insight about python programming and its
fundamentals.
15
1) Useful Concepts
Python provides different types of data structures as sequences. In a sequence, there are more
than one values and each value has its own index. The first value will have an index 0 in python,
the second value will have index 1 and so on. These indices are used to access a particular value
in the sequence.
Python Lists:
Lists are just like dynamically sized arrays, declared in other languages (vector in C++ and
ArrayList in Java). Lists need not be homogeneous always which makes it the most powerful
tool in Python. A single list may contain DataTypes like Integers, Strings, as well as Objects.
Lists are mutable, and hence, they can be altered even after their creation.
List in Python are ordered and have a definite count. The elements in a list are indexed
according to a definite sequence and the indexing of a list is done with 0 being the first index.
Each element in the list has its definite place in the list, which allows duplicating of
elements in the list, with each element having its own distinct place and credibility.
Creating a List
Lists in Python can be created by just placing the sequence inside the square brackets[]. Unlike
Sets, a list doesn’t need a built-in function for the creation of a list.
# Creating a List
List = []
print("Blank List: ")
print(List)
16
Output:
Blank List:
[]
List of numbers:
[10, 20, 14]
List Items
Geeks
Geeks
Multi-Dimensional List:
[['Geeks', 'For'], ['Geeks']]
Output:
List with the use of Numbers:
[1, 2, 4, 4, 3, 3, 3, 6, 5]
# Creating a List
List1 = []
17
print(len(List1))
Output:
0
3
# Creating a List
List = []
print("Initial blank List: ")
print(List)
# Addition of Elements
# in the List
List.append(1)
List.append(2)
List.append(4)
print("\nList after Addition of Three elements: ")
print(List)
18
print(List)
Output:
Initial blank List:
[]
# Creating a List
List = [1,2,3,4]
print("Initial List: ")
print(List)
# Addition of Element at
# specific Position
# (using Insert Method)
List.insert(3, 12)
List.insert(0, 'Geeks')
print("\nList after performing Insert Operation: ")
print(List)
19
Output:
Initial List:
[1, 2, 3, 4]
# Creating a List
List = [1, 2, 3, 4]
print("Initial List: ")
print(List)
Output:
Initial List:
[1, 2, 3, 4]
20
# Python program to demonstrate
# accessing of element from list
Negative indexing
In Python, negative sequence indexes represent positions from the end of the array. Instead of
having to compute the offset as in List[len(List)-3], it is enough to just write List[-3]. Negative
indexing means beginning from the end, -1 refers to the last item, -2 refers to the second-last
item, etc.
21
# Python program to demonstrate
# Removal of elements in a List
# Creating a List
List = [1, 2, 3, 4, 5, 6,
7, 8, 9, 10, 11, 12]
print("Initial List: ")
print(List)
Output:
Initial List:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
List = [1,2,3,4,5]
# Removing element from the
22
# Set using the pop() method
List.pop()
print("\nList after popping an element: ")
print(List)
# Removing element at a
# specific location from the
# Set using the pop() method
List.pop(2)
print("\nList after popping a specific element: ")
print(List)
Output:
List after popping an element:
[1, 2, 3, 4]
Slicing of a List
In Python List, there are multiple ways to print the whole List with all the elements, but to print
a specific range of elements from the list, we use the Slice operation. Slice operation is
performed on Lists with the use of a colon(:). To print elements from beginning to a range use
[: Index], to print elements from end-use [:-Index], to print elements from specific Index till the
end use [Index:], to print elements within a range, use [Start Index:End Index] and to print the
whole List with the use of slicing operation, use [:]. Further, to print the whole List in reverse
order, use [::-1].
Note – To print elements of List from rear-end, use Negative Indexes.
Figure 1 - List
23
# Python program to demonstrate
# Removal of elements in a List
# Creating a List
List = ['G', 'E', 'E', 'K', 'S', 'F',
'O', 'R', 'G', 'E', 'E', 'K', 'S']
24
print("Initial List: ")
print(List)
Output:
Initial List:
['G', 'E', 'E', 'K', 'S', 'F', 'O', 'R', 'G', 'E', 'E', 'K', 'S']
# Creating a List
List = ['G', 'E', 'E', 'K', 'S', 'F',
'O', 'R', 'G', 'E', 'E', 'K', 'S']
print("Initial List: ")
print(List)
25
Sliced_List = List[:-6]
print("\nElements sliced till 6th element from last: ")
print(Sliced_List)
Output:
Initial List:
['G', 'E', 'E', 'K', 'S', 'F', 'O', 'R', 'G', 'E', 'E', 'K', 'S']
List Comprehension
List comprehensions are used for creating new lists from other iterables like tuples, strings,
arrays, lists,
etc
.
A list comprehension consists of brackets containing the expression, which is executed
for each element along with the for loop to iterate over each element.
Syntax:
newList = [ expression(element) for element in oldList if condition ]
26
Example:
27
For better understanding, the above code is similar to –
print(odd_square)
Output:
[1, 9, 25, 49, 81]
Dictionary in Python is an unordered collection of data values, used to store data values like a
map, which, unlike other Data Types that hold only a single value as an element,
Dictionary holds key:value pair. Key-value is provided in the dictionary to make it more
optimized.
# Creating a Dictionary
# with Integer Keys
Dict = {1: 'Geeks', 2: 'For', 3: 'Geeks'}
print("\nDictionary with the use of Integer Keys: ")
print(Dict)
# Creating a Dictionary
# with Mixed keys
Dict = {'Name': 'Geeks', 1: [1, 2, 3, 4]}
print("\nDictionary with the use of Mixed Keys: ")
print(Dict)
Output:
Dictionary with the use of Integer Keys:
{1: 'Geeks', 2: 'For', 3: 'Geeks'}
Dictionary with the use of Mixed Keys:
{1: [1, 2, 3, 4], 'Name': 'Geeks'}
28
Dictionary can also be created by the built-in function dict(). An empty dictionary can be
created byjust placing to curly braces{}.
# Creating a Dictionary
# with dict() method
Dict = dict({1: 'Geeks', 2: 'For', 3:'Geeks'})
print("\nDictionary with the use of dict(): ")
print(Dict)
# Creating a Dictionary
# with each item as a Pair
Dict = dict([(1, 'Geeks'), (2, 'For')])
print("\nDictionary with each item as a pair: ")
print(Dict)
Output:
Empty Dictionary:
{}
Dictionary with the use of dict():
{1: 'Geeks', 2: 'For', 3: 'Geeks'}
Dictionary with each item as a pair:
{1: 'Geeks', 2: 'For'}
Nested Dictionary:
Figure 2 - Dictionary
29
# Creating a Nested Dictionary
# as shown in the below image
Dict = {1: 'Geeks', 2: 'For',
3:{'A' : 'Welcome', 'B' : 'To', 'C' : 'Geeks'}}
print(Dict)
Output:
{1: 'Geeks', 2: 'For', 3: {'A': 'Welcome', 'B': 'To', 'C': 'Geeks'}}
30
print("\nAdding a Nested Key: ")
print(Dict)
Output:
Empty Dictionary:
{}
# Creating a Dictionary
Dict = {1: 'Geeks', 'name': 'For', 3: 'Geeks'}
Output:
Accessing a element using key:
For
Accessing a element using key:
Geeks
There is also a method called get() that will also help in accessing the element from a dictionary.
31
# Creating a Dictionary
Dict = {1: 'Geeks', 'name': 'For', 3: 'Geeks'}
# Creating a Dictionary
Dict = {'Dict1': {1: 'Geeks'},
'Dict2': {'Name': 'For'}}
Output:
{1: 'Geeks'}
Geeks
For
# Initial Dictionary
Dict = { 5 : 'Welcome', 6 : 'To', 7 : 'Geeks',
'A' : {1 : 'Geeks', 2 : 'For', 3 : 'Geeks'},
'B' : {1 : 'Geeks', 2 : 'Life'}}
print("Initial Dictionary: ")
print(Dict)
32
print("\nDeleting a specific key: ")
print(Dict)
# Creating a Dictionary
Dict = {1: 'Geeks', 'name': 'For', 3: 'Geeks'}
# Deleting a key
# using pop() method
pop_ele = Dict.pop(1)
print('\nDictionary after deletion: ' + str(Dict))
print('Value associated to poped key is: ' + str(pop_ele))
Output:
Dictionary after deletion: {3: 'Geeks', 'name': 'For'}
Value associated to poped key is: Geeks
# Creating Dictionary
Dict = {1: 'Geeks', 'name': 'For', 3: 'Geeks'}
# Deleting an arbitrary key
# using popitem() function
pop_ele = Dict.popitem()
print("\nDictionary after deletion: " + str(Dict))
33
print("The arbitrary pair returned is: " + str(pop_ele))
Output:
Dictionary after deletion: {3: 'Geeks', 'name': 'For'}
The arbitrary pair returned is: (1, 'Geeks')
# Creating a Dictionary
Dict = {1: 'Geeks', 'name': 'For', 3: 'Geeks'}
Dictionary Methods
Methods Description
copy() They copy() method returns a shallow copy of the dictionary.
clear() The clear() method removes all items from the dictionary.
Removes and returns an element from a dictionary having the
pop() given
key.
Removes the arbitrary key-value pair from the dictionary and
popitem() returns it
as tuple.
get() It is a conventional method to access a value for a key.
dictionary_name.value returns a list of all the values available in a given dictionary.
s()
str() Produces a printable string representation of a dictionary.
update() Adds dictionary dict2’s key-values pairs to dict
setdefault() Set dict[key]=default if key is not already in dict
keys() Returns list of dictionary dict’s keys
items() Returns a list of dict’s (key, value) tuple pairs
has_key() Returns true if key in dictionary dict, false otherwise
fromkeys() Create a new dictionary with keys from seq and values set to value.
34
type() Returns the type of the passed variable.
cmp() Compares elements of both dict.
Activity 1
Accept two lists from user and display their join.
35
Solution:
36
Activity 2:
A palindrome is a string which is same read forward or backwards.
For example: "dad" is the same in forward or reverse direction. Another example is
"aibohphobia" which literally means, an irritable fear of palindromes.
Write a function in python that receives a string and returns True if that string is a palindrome
and False otherwise. Remember that difference between upper and lower case characters are
ignored during this determination.
Solution:
Activity 3:
Imagine two matrices given in the form of 2D lists as under; a = [[1, 0, 0], [0, 1, 0], [0, 0, 1] ]
b = [[1, 2, 3], [4, 5, 6], [7, 8, 9] ]
Write a python code that finds another matrix/2D list that is a product of and b, i.e., C=a*b
Solution:
Activity 4:
A closed polygon with N sides can be represented as a list of tuples of N connected
coordinates, i.e., [ (x1,y1), (x2,y2), (x3,y3), . . . , (xN,yN) ]. A sample polygon with 6
sides (N=6) is shown below.
37
Figure 3 - Polygon
38
Write a python function that takes a list of N tuples as input and returns the
perimeter of the polygon. Remember that your code should work for any value of N.
Activity 5:
Imagine two sets A and B containing numbers. Without using built-in set functionalities, write
your own function that receives two such sets and returns another set C which is a symmetric
difference of the two input sets. (A symmetric difference between A and B will return a set C
which contains only those items that appear in one of A or B. Any items that appear in both
sets are not included in C). Now compare the output of your function with the following built-
in functions/operators.
A.symmetric_difference(B)
B.symmetric_difference(A)
A^B
B^A
Solution:
39
Activity 6:
Create a Python program that contains a dictionary of names and phone numbers. Use a tuple
of separate first and last name values for the key field. Initialize the dictionary with at least
three names and numbers. Ask the user to search for a phone number by entering a first and
last name. Display the matching number if found, or a message if not found.
Solution:
Lab Task 1:
Create two lists based on the user values. Merge both the lists and display in sorted order.
Lab Task 2:
Repeat the above activity to find the smallest and largest element of the list. (Suppose all the
elements are integer values)
Lab Task 3:
For this exercise, you will keep track of when our friend’s birthdays are, and be able to find that
information based on their name. Create a dictionary (in your file) of names and birthdays.
When you run your program it should ask the user to enter a name, and return the birthday of
that person back to them. The interaction should look something like this:
Lab Task 4:
Create a dictionary by extracting the keys from a given dictionary
Write a Python program to create a new dictionary by extracting the mentioned keys from
the below dictionary.
Given
dictionary:
sample_dict = {
"name": "Kelly",
"age": 25,
"salary": 8000,
"city": "New york"}
# Keys to extract
keys = ["name", "salary"]
Expected output:
41
Lab 03
Classical Ciphers
L
In this lab students will get an understanding of some common classical cryptography
schemes, and learn the Python code that will bring all the topics together. Specifically,
students will gain cryptographic knowledge about Substitution Ciphers, Caesar Cipher,
Vigenère Cipher, Playfair, Column Transposition, Affine Cipher.
Lab Outcomes:
Explore the basics of encryption schemes
Explore the use of historical ciphers and their cryptanalysis
Gain an understanding of why it is critical to use well-established encryption
algorithms
Instructor Note:
Perform an understanding practice of given different ciphers codes and after execution
write line wise code description
42
Practice -Lab Activity 1: Substitution Ciphers
The substitution cipher simply substitutes one letter in the alphabet for another based
upon a cryptovariable. The substitution involves shifting positions in the alphabet. This
includes the Caesar cipher and ROT-13, which will be covered shortly. Examine the
following example:
Plaintext: WE HOLD THESE TRUTHS TO BE SELF-EVIDENT, THAT ALL MEN
ARE CREATED EQUAL.
Ciphertext: ZH KROG WKHVH WUXWKV WR EH VHOI-HYLGHQW, WKDW
DOO PHQ DUH FUHDWHG HTXDO.
The Python syntax to both encrypt and decrypt a substitution cipher is presented next.
This example shows the use of
key = 'abcdefghijklmnopqrstuvwxyz'
origtext = 'We hold these truths to be self-evident, that all men are
created equal.'
ciphertext = enc_substitution(13, origtext)
plaintext = dec_substitution(13, ciphertext)
print("Original Text:", origtext)
print("Ciphertext:", ciphertext)
print("Decrypted Text:", plaintext)
CODE OUTPUT:
try:
print(x)
except:
print("An exception occurred")
The plain text characters are placed horizontally and the cipher text is created with
vertical format as: holewdlo lr. Now, the receiver has to use the same table to decrypt
the cipher text to plain text.
Code
The following program code demonstrates the basic implementation of columnar
transposition technique:
def split_len(seq, length):
return [seq[i:i + length] for i in range(0, len(seq),
length)]
print(encode('3214', 'HELLO'))
Code Output
LEHOL
Explanation
Using the function split_len(), we can split the plain text characters, which can be
placed in columnar or row format.
encode method helps to create cipher text with key specifying the number of columns
and prints the cipher text by reading characters through each column.
Practice -Lab Activity 3: Caesar Cipher
The Caesar cipher is one of the oldest recorded ciphers. De Vita Caesarum, Divus Iulis
(“The Lives of the Caesars, the Deified Julius), commonly known as The Twelve
Caesars, was written in approximately 121 CE. In The Twelve Caesars, it states that if
someone has a message that they want to keep private, they can do so by changing the
order of the letters so that the original word cannot be determined. When the recipient
of the message receives it, the reader must sub- stitute the letters so that they shift by
four positions.
Simply put, the cipher shifted letters of the alphabet three places forward so that the
letter A was replaced with the letter D, the letter B was replaced with E, and so on. Once
the end of the alphabet was reached, the letters would start over: \
The Caesar cipher is an example of a mono-alphabet substitution. This type of
substitution substitutes one character of the ciphertext from a character in plaintext.
Other examples that include this type of substitution are Atbash, Affine, and the ROT-
13 cipher. There are many flaws with this type of cipher, the most obvious of which is
that the encryption and decryption methods are fixed and require no shared key. This
would allow anyone who knew this method to read Caesar’s encrypted messages with
ease. Over the years, there have been several variations that include ROT-13, which
shifts the letters 13
places instead of 3. We will explore how to encrypt and decrypt Caesar cipher and
ROT-13 codes using Python.
For example, given that x is the current letter of the alphabet, the Caesar cipher
function adds three for encryption and subtracts three for decryption. While this could
be a variable shift, let’s start with the original shift of 3: Enc(x) = (x + 3) % 26 Dec(x)
= (x - 3) % 26
These functions are the first use of modular arithmetic; there are other ways to get the
same result, but this is the cleanest and fastest method. The encryption formula adds 3
to the numeric value of the number. If the value exceeds 26, which is the final position
of Z, then the modular arithmetic wraps the value back to the beginning of the alphabet.
While it is possible to get the ordinal (ord) of a number and convert it back to ASCII,
the use of the key simplifies the alphabet indexing. You will learn how to use the ord()
function when we explore the Vigenère cipher in the next section. In the following
Python recipe, the enc_caesar function will access a variable index to encrypt the
plaintext that is passed in.
key = 'abcdefghijklmnopqrstuvwxyz'
def enc_caesar(n, plaintext):
result = ''
for l in plaintext.lower():
try:
i = (key.index(l) + n) % 26
result += key[i]
except ValueError:
result += l
return result
plaintext = 'We hold these truths to be self-evident, that all men are
created equal.'
ciphertext = enc_caesar(3, plaintext)
print(ciphertext)
Decryption
The reverse in this case is straightforward. Instead of adding, we subtract. The
decryption would look like the following:
key = 'abcdefghijklmnopqrstuvwxyz'
def dec_caesar(n, ciphertext):
result = ''
for l in ciphertext:
try:
i = (key.index(l) - n) % 26
Code Output result += key[i]
except ValueError:
result += l
return result
ciphertext = 'zh krog wkhvh wuxwkv wr eh vhoi-hylghqw, wkdw doo phq duh
fuhdwhg htxdo.'
plaintext = dec_caesar(3, ciphertext)
Practice -Lab Activity 4: ROT-13
Now that you understand the Caesar cipher, take a look at the ROT-13 cipher. The
unique construction of the ROT-13 cipher allows you to encrypt and decrypt using the
same method. The reason for this is that since ROT-13 moves the letter of the alphabet
exactly halfway, when you run the process again, the letter goes back to its original
value.
To see the code behind the cipher, take a look at the following:
key = 'abcdefghijklmnopqrstuvwxyz'
def enc_dec_ROT13(n, plaintext):
result = ''
for l in plaintext.lower():
try:
i = (key.index(l) + n) % 26
result += key[i]
except ValueError:
result += l
return result
plaintext = 'We hold these truths to be self-evident, that all men are created equal.'
ciphertext = enc_dec_ROT13(13, plaintext)
print(ciphertext)
# Decrypt the ciphertext by running the same function with the same shift of 13
plaintext = enc_dec_ROT13(13, ciphertext)
print(plaintext)
Code output:
jr ubyq gurfr gehguf gb or frys-rivqrag, gung nyy zra ner perngrq rdhny.
we hold these truths to be self-evident, that all men are created equal.
Whether we use a Caesar cipher or the ROT-13 variation, brute-forcing an attack would
take at most 25 tries, and we could easily decipher the plaintext results when we see a
language we understand. This will get more complex as we explore the other historical
ciphers; the cryptanalysis requires frequency analysis and language detectors. We will
focus on these concepts in upcoming chapters.
Plaintext: We hold these truths to be self-evident, that all men are created equal.
Ciphertext: zi jzlu tamgr wvwehj th js fhph-pvzdxvh, gkev llc mxv oeh gtpakew
mehdp.
To create a numeric key such as the one shown, use the following syntax. You should
see the output
[3, 4, 2, 11, 0, 17, 0, 19, 8, 14, 13]:
def key_vigenere(key):
keyArray = []
for i in range(0, len(key)):
keyElement = ord(key[i].upper()) - 65 # Ensure uppercase
keyArray.append(keyElement)
return keyArray
secretKey = 'DECLARATION'
key = key_vigenere(secretKey)
print(key)
Code Output:
DPN MVN IFR GTPAKEW SDXEN
When you know the key, such as in this case, you can decrypt the Vigenère
cipher with the following:
def key_vigenere(key):
keyArray = []
for i in range(0, len(key)):
keyElement = ord(key[i].upper()) - 65 # Ensure uppercase
keyArray.append(keyElement)
return keyArray
secretKey = 'DECLARATION'
key = key_vigenere(secretKey)
ciphertext = 'DNP RYR OIU OIKORO VZZIV' # You may replace this with the actual ciphertext
decoded = dec_vigenere(ciphertext, key)
print(decoded)
Code Output:
AJN RHR GUH KGZOAO NLMFR
We will perform cryptanalysis by creating a random key that will use the same
encryption function, and then we will use frequency analysis to help find the appropriate
key. For now, it is more important to understand how the Python code works with this
cryptography scheme.
Lab Activity 5: One-Time Pad Function
Now that you have seen how XOR works, it will be easier to understand the ne-time
pad. OTP takes a random sequence of 0s and 1s as the secret key and will then XOR
the key with your plaintext message to produce the ciphertext:
GEN: choose a random key uniformly from {0,1}ℓ {0,1}ℓ (the set of binary stringsof
length ℓℓ)
ENC: given k∈{0,1}ℓ k∈{0,1}ℓ and m∈{0,1} ℓ m∈{0,1}ℓ then output is c:=k⊕m
c:=k⊕m
DEC: given k∈{0,1} ℓ k∈{0,1}ℓ and c∈{0,1} ℓ c∈{0,1}ℓ, the output message is
m:=k⊕c m:=k⊕c
The output given by the OTP satisfies Claude Shannon’s notion of perfect secrecy (see
“Shannon’s Theorem”). Imagine all possible messages, all possible keys, and all
possible ciphertexts. For every message and ciphertext pair, there is one key that causes
that message to encrypt to that ciphertext. This is really saying that each key gives you
a one-to-one mapping from messages to ciphertexts, and changing the key shuffles the
mapping without ever repeating a pair.
The OTP remains unbreakable as long as the key meets the following criteria:
When the key is the same length as the encrypted message, each plaintext letter’s subkey
is unique, meaning that each plaintext letter could be encrypted to any ciphertext letter
with equal probability. This removes the ability to use frequency analysis against the
encrypted text to learn anything about the cipher. Brute-forcing the OTP would take an
incredible amount of time and would be computationally unfeasible, as the number of
keys would equal 26 raised to the power of the total number of letters in the message.
In Python 3.6 and later, you will have the option to use the secrets module, which will
allow you to generate random numbers. The function secrets.randbelow() will return
random numbers between zero and the argument passed to it:
import secrets
msg = "helloworldthisistheonetimepad"
key = ''
for i in range(len(msg)):
key += secrets.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ') # Use
uppercase letters
print(key)
Code Output:
TWPIRWVUSCGQPYTAHDNDGPIDTKCTB
import secrets
def key_generation(length):
"""
Generates a random binary key of a given length.
"""
return ''.join(secrets.choice('01') for _ in range(length))
# Encryption
ciphertext = encrypt(key, message)
# Decryption
decrypted_message = decrypt(key, ciphertext)
Code Output:
Message: 0111101111
Key: 0010010001
Ciphertext: 0101111110
Decrypted Text: 0111101111
Task 1
Affine Cipher is the combination of Multiplicative Cipher and Caesar Cipher
algorithm. The basic implementation of affine cipher is as shown in the image below:
Write python code for above mentioned Affine Cipher with code output.
Task 2
While using Caesar cipher technique, encrypting and decrypting symbols involves
converting the values into numbers with a simple basic procedure of addition or
subtraction. If multiplication is used to convert to cipher text, it is called a wrap-around
situation. Consider the letters and the associated numbers to be used as shown below:
The numbers will be used for multiplication procedure and the associated key is 7.
The basic formula to be used in such a scenario to generate a multiplicative cipher is
Write Python code for multiplicative cipher and also provide how it can be hacked.
Your programs should include code output mechanism in any form.
Task 3
CrypTool: CrypTool is one of the most comprehensive open-source cryptography
tools. It includes tutorials, simulations, and visualizations of classical and modern
cryptographic algorithms .You can analyze ciphers, break them using various
techniques (e.g., frequency analysis, brute force), and experiment with encryption and
decryption. Practice encrypting plaintext using different ciphers and then try decrypting
it.
Task 4
print(encode('3214', 'HELLO'))
Note: Please note that from learning perspective try to work on you own
implementation so that in Midterm assessments you will be able to solve challenging
task yourself.
Note: The instructor can design graded lab activities according to the level of
difficult and complexity of the practice lab activities. The lab tasks assigned by the
instructor should be evaluated in the same lab
Lab 04
Streams Ciphers and Pseudo Random Generator (PRG)
This lab will introduce students to Streams Ciphers and Pseudo Random Generator
(PRG). We will first learn the working principles of stream ciphers, including how they
use a key and a pseudo-random generator to produce a keystream. Implement a basic
stream cipher to see how XOR-based encryption works. In this lab students will also
learn how PRGs generate keystreams for stream ciphers.
Activity Outcomes:
This lab teaches you the following topics:
The RC4 stream cipher was created by Ron Rivest in 1987. RC4 was classified as a
trade secret by RSA Security but was eventually leaked to a message board in 1994.
RC4 was originally trademarked by RSA Security so it is often referred to as
ARCFOUR or ARC4 to avoid trademark issues. ARC4 would later become commonly
used in a number of encryption protocols and standards such as SSL, TLS, WEP, and
WPA. In 2015, it was prohibited for all versions of TLS by RFC 7465. ARC4 has been
used in many hardware and software implementations. One of the main advantages of
ARC4 is its speed and simplicity, which you will notice in the following code:
"""
Implement the ARC4 stream cipher.
"""
def arc4crypt(data, key):
x=0
box = list(range(256)) # Ensure box is a list, not range object
# Key-scheduling algorithm (KSA)
for i in range(256):
x = (x + box[i] + ord(key[i % len(key)])) % 256
box[i], box[x] = box[x], box[i] # Swap values
x=0
y=0
out = []
# Pseudo-random generation algorithm (PRGA)
for char in data:
x = (x + 1) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x] # Swap values
out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256])) # XOR with
keystream
return ''.join(out)
The Vernam cipher was developed by Gilbert Vernam in 1917. It is a type of onetime
pad for data streams and is considered to be unbreakable. The algorithm is symmetrical,
and the plaintext is combined with a random stream of data of the same length using the
Boolean XOR function; the Boolean XOR function is also known as the Boolean
exclusive OR function. Claude Shannon would later mathematically prove that it is
unbreakable. The characteristics of the Vernam cipher include:
XOR Operation:
o XOR is a bitwise operation, and it's symmetric. This means:
a ^ b will encrypt a with b.
Applying XOR again with b reverses the operation: (a ^ b) ^ b = a.
This is why using the same function for both encryption and decryption works in the
Vernam cipher.
One of the disadvantages of using an OTP is that the keys must be as long as the
message it is trying to conceal; therefore, for long messages, you will need a long key:
CODE
def VernamEncDec(text, key):
result = ""
ptr = 0
for char in text:
result += chr(ord(char) ^ ord(key[ptr])) # XOR each character with the key
ptr += 1
if ptr == len(key): # Reset the key pointer if it reaches the end of the key
ptr = 0
return result
while True:
input_text = input("\nEnter Text To Encrypt:\t")
OUTPUT
The Salsa20 cipher was developed in 2005 by Daniel Bernstein, and submitted to
eSTREAM. The Salsa20/20 (Salsa20 with 20 rounds) is built on a pseudorandom
function that is based on add-rotate-xor (ARX) operations. ARX algorithms are
designed to have their round function support modular addition, fixed rotation, and
XOR. These ARX operations are popular because they are relatively fast and cheap in
hardware and software, and because they run in constant time, and are therefore immune
to timing attacks. The rotational cryptanalysis technique attempts to attack such round
functions.
The core function of Salsa20 maps a 128-bit or 256-bit key, a 64-bit nonce/IV, and a
64-bit counter to a 512-bit block of the keystream. Salsa20 provides speeds of around
4–14 cycles per byte on modern x86 processors and is considered acceptable hardware
performance. The numeric indicator in the Salsa name specifies the number of
encryption rounds. Salsa20 has 8, 12, and 20 variants. One of the biggest benefits of
Salsa20 is that Bernstein has written several implementations that have been released
to the public domain, and the cipher is not patented.
Salsa20 is composed of sixteen 32-bit words that are arranged in a 4×4 matrix. The
initial state is made up of eight words of key, two words of the stream position, two
words for the nonce/IV, and four fixed words or constants. The initial state would look
like the following:
The Salsa20 core operation is the quarter-round that takes a four-word input and
produces a four-word output. The quarter-round is denoted by the following function:
QR(a, b, c, d). The odd-numbered rounds apply QR(a, b, c, d) to each of the four
columns in the preceding 4×4 matrix; the even-numbered rounds apply the rounding to
each of the four rows. Two consecutive rounds (one for a column and one for a row)
operate together and are known as a double-round. To help understand how the rounds
work, let us first examine a 4×4 matrix with labels from 0 to 15:
The first double-round starts with a quarter-round on column 1 and row 1. The first
QR round examines column 1, which contains 0, 4, 8, and 12. The second QR round
examines row 1, which contains 0, 1, 2, 3. The second doubleround picks up starting
at the second column and second row position (5, 9, 13, 1). The even round picks up
5, 5, 7, 4. Notice that the starting cell is the same for each double-round:
A couple libraries are available that will help simplify the Salsa20 encryption scheme.
You can access the salsa20 library by doing a pip install salsa20.
Once you have the library installed, you can use the XSalsa20_keystream to generate
a keystream of the desired length, or you can pass any message plaintext or ciphertext)
to have it XOR'd with the keystream. All values must be binary strings that include str
for Python 2 or the byte for Python 3. Here, you will see a Python implementation of
the salsa20 library:
from salsa20 import XSalsa20_xor
from os import urandom
IV = urandom(24)
KEY = b'*secret**secret**secret**secret*'
ciphertext = XSalsa20_xor(b"IT'S A YELLOW SUBMARINE", IV, KEY)
print(XSalsa20_xor(ciphertext, IV, KEY).decode())
from nacl.secret import SecretBox
from nacl.utils import random
Code Output
IT'S A YELLOW SUBMARINE
One of the reasons why you should be familiar with Salsa20 is that it is consistently
faster than AES. It is recommended to use Salsa20 for encryption in typical
cryptographic applications.
1. Write python code for your designed stream cipher approach for encryption
decryption, you can use approach from more than one already developed ciphers as
given in lab practice exercises.
2. Design and implement an adversarial attack approach for your proposed stream cipher
approach.
Lab 05
Block Ciphers
Objective:
Stream ciphers work by generating pseudorandom bits and XORing them with your
message. Block ciphers take in a fixed-length message, a private key, and they produce
a ciphertext that is the same length as the fixed-length plaintext message. In this Lab
students will understand working of DES, Triple DES AES.
Activity Outcomes:
How to learn coding of cryptographic block ciphers like DES, and AES.
How to code and analyze attack scenarios for complex block ciphers.
57
Practice -Lab Activity 1: DES
AES and Triple DES are the most common block ciphers in use today. From the
student’s point of view, DES is still interesting to study, but due to its small 56-bit key
size, it is considered insecure. In 1999, two partners, Electronic Frontier Foundation
and distributed.net collaborated to publicly break a DES key in 22 hours and 15 minutes.
Here, we will use the PyCrypto library to demonstrate
how to use DES to encrypt a message. The following code is using the ECB block mode;
you will learn about the various modes later in this chapter. To execute the following
recipe, perform a pip install PyCrypto:
from Crypto.Cipher import DES
key = b'shhhhhh!'
origText = b'The US Navy has submarines in Kingsbay!!'
des = DES.new(key, DES.MODE_ECB)
ciphertext = des.encrypt(origText)
plaintext = des.decrypt(ciphertext)
print('The original text is {}'.format(origText))
print('The ciphertext is {}'.format(ciphertext))
print('The plaintext is {}'.format(plaintext))
print()
The key was 'shhhhhh!' and the message was 'The US Navy has submarines in
Kingsbay!!'. The ciphertext was 40 bytes long; 40 mod 8 = 0, so there is no need to pad
this example. If you were to implement a block cipher in reality, you should use a
padding function that ensures the block length.
58
What is DES?
Data Encryption Standard (DES) is a block cipher with a 56-bit key length that has
played a significant role in data security. Data encryption standard (DES) has been
found vulnerable to very powerful attacks therefore, the popularity of DES has been
found slightly on the decline. DES is a block cipher and encrypts data in blocks of size
of 64 bits each, which means 64 bits of plain text go as the input to DES, which
produces 64 bits of ciphertext. The same algorithm and key are used for encryption
and decryption, with minor differences. The key length is 56 bits.
The basic idea is shown below:
We have mentioned that DES uses a 56-bit key. Actually, The initial key consists of 64
bits. However, before the DES process even starts, every 8th bit of the key is discarded
to produce a 56-bit key. That is bit positions 8, 16, 24, 32, 40, 48, 56, and 64 are
discarded.
Thus, the discarding of every 8th bit of the key produces a 56-bit key from the
original 64-bit key.
DES is based on the two fundamental attributes of cryptography: substitution (also
called confusion) and transposition (also called diffusion). DES consists of 16 steps,
each of which is called a round. Each round performs the steps of substitution and
transposition. Let us now discuss the broad-level steps in DES.
In the first step, the 64-bit plain text block is handed over to an initial Permutation (IP)
function.
The initial permutation is performed on plain text.
Next, the initial permutation (IP) produces two halves of the permuted block; saying
Left Plain Text (LPT) and Right Plain Text (RPT).
Now each LPT and RPT go through 16 rounds of the encryption process.
In the end, LPT and RPT are rejoined and a Final Permutation (FP) is performed on
the combined block
The result of this process produces 64-bit ciphertext.
62
Initial Permutation (IP)
As we have noted, the initial permutation (IP) happens only once and it happens before
the first round. It suggests how the transposition in IP should proceed, as shown in the
figure. For example, it says that the IP replaces the first bit of the original plain text
block with the 58th bit of the original plain text, the second bit with the 50th bit of the
original plain text block, and so on. This is nothing but jugglery of bit positions of the
original plain text block. The same rule applies to all the other bit positions shown in
the figure.
As we have noted after IP is done, the resulting 64-bit permuted text block is divided
into two half blocks. Each half-block consists of 32 bits, and each of the 16 rounds, in
turn, consists of the broad-level steps outlined in the figure.
63
Step 1: Key transformation
We have noted initial 64-bit key is transformed into a 56-bit key by discarding every
8th bit of the initial key. Thus, for each a 56-bit key is available. From this 56-bit key,
a different 48-bit Sub Key is generated during each round using a process called key
transformation. For this, the 56-bit key is divided into two halves, each of 28 bits. These
halves are circularly shifted left by one or two positions, depending on the round.
For example: if the round numbers 1, 2, 9, or 16 the shift is done by only one position
for other rounds, the circular shift is done by two positions. The number of key bits
shifted per round is shown in the figure.
After an appropriate shift, 48 of the 56 bits are selected. From the 48 we might obtain
64 or 56 bits based on requirement which helps us to recognize that this model is very
versatile and can handle any range of requirements needed or provided. for selecting 48
of the 56 bits the table is shown in the figure given below. For instance, after the shift,
bit number 14 moves to the first position, bit number 17 moves to the second position,
and so on. If we observe the table , we will realize that it contains only 48-bit positions.
Bit number 18 is discarded (we will not find it in the table), like 7 others, to reduce a
56-bit key to a 48-bit key. Since the key transformation process involves permutation
as well as a selection of a 48-bit subset of the original 56-bit key it is called Compression
Permutation.
64
Because of this compression permutation technique, a different subset of key bits is
used in each round. That makes DES not easy to crack.
Recall that after the initial permutation, we had two 32-bit plain text areas called Left
Plain Text(LPT) and Right Plain Text(RPT). During the expansion permutation, the
RPT is expanded from 32 bits to 48 bits. Bits are permuted as well hence called
expansion permutation. This happens as the 32-bit RPT is divided into 8 blocks, with
each block consisting of 4 bits. Then, each 4-bit block of the previous step is then
expanded to a corresponding 6-bit block, i.e., per 4-bit block, 2 more bits are added.
This process results in expansion as well as a permutation of the input bit while creating
output. The key transformation process compresses the 56-bit key to 48 bits. Then the
expansion permutation process expands the 32-bit RPT to 48-bits. Now the 48-bit key
is XOR with 48-bit RPT and the resulting output is given to the next step, which is
the S-Box substitution.
def hex2bin(s):
mp = {'0': "0000",
'1': "0001",
'2': "0010",
'3': "0011",
65
'4': "0100",
'5': "0101",
'6': "0110",
'7': "0111",
'8': "1000",
'9': "1001",
'A': "1010",
'B': "1011",
'C': "1100",
'D': "1101",
'E': "1110",
'F': "1111"}
bin = ""
for i in range(len(s)):
bin = bin + mp[s[i]]
return bin
def bin2hex(s):
mp = {"0000": '0',
"0001": '1',
"0010": '2',
"0011": '3',
"0100": '4',
"0101": '5',
"0110": '6',
"0111": '7',
"1000": '8',
"1001": '9',
"1010": 'A',
"1011": 'B',
"1100": 'C',
"1101": 'D',
"1110": 'E',
"1111": 'F'}
hex = ""
for i in range(0, len(s), 4):
ch = ""
ch = ch + s[i]
ch = ch + s[i + 1]
ch = ch + s[i + 2]
ch = ch + s[i + 3]
hex = hex + mp[ch]
return hex
66
def bin2dec(binary):
binary1 = binary
decimal, i, n = 0, 0, 0
while(binary != 0):
dec = binary % 10
decimal = decimal + dec * pow(2, i)
binary = binary//10
i += 1
return decimal
def dec2bin(num):
res = bin(num).replace("0b", "")
if(len(res) % 4 != 0):
div = len(res) / 4
div = int(div)
counter = (4 * (div + 1)) - len(res)
for i in range(0, counter):
res = '0' + res
return res
67
def xor(a, b):
ans = ""
for i in range(len(a)):
if a[i] == b[i]:
ans = ans + "0"
else:
ans = ans + "1"
return ans
# S-box Table
sbox = [[[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7],
[0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8],
[4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0],
[15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]],
# Initial Permutation
pt = permute(pt, initial_perm, 64)
print("After initial permutation", bin2hex(pt))
# Splitting
left = pt[0:32]
right = pt[32:64]
69
for i in range(0, 16):
# Expansion D-box: Expanding the 32 bits data into 48 bits
right_expanded = permute(right, exp_d, 48)
# Swapper
if(i != 15):
left, right = right, left
print("Round ", i + 1, " ", bin2hex(left),
" ", bin2hex(right), " ", rk[i])
# Combination
combine = left + right
pt = "123456ABCD132536"
key = "AABB09182736CCDD"
# Key generation
# --hex to binary
key = hex2bin(key)
# Splitting
left = key[0:28] # rkb for RoundKeys in binary
right = key[28:56] # rk for RoundKeys in hexadecimal
rkb = []
rk = []
for i in range(0, 16):
# Shifting the bits by nth shifts by checking from shift table
left = shift_left(left, shift_table[i])
right = shift_left(right, shift_table[i])
rkb.append(round_key)
rk.append(bin2hex(round_key))
print("Encryption")
cipher_text = bin2hex(encrypt(pt, rkb, rk))
print("Cipher Text : ", cipher_text)
print("Decryption")
71
rkb_rev = rkb[::-1]
rk_rev = rk[::-1]
text = bin2hex(encrypt(cipher_text, rkb_rev, rk_rev))
print("Plain Text : ", text)
Output:
...60AF7CA5
Round 12 FF3C485F 22A5963B C2C1E96A4BF3
Round 13 22A5963B 387CCDAA 99C31397C91F
Round 14 387CCDAA BD2DD2AB 251B8BC717D0
Round 15 BD2DD2AB CF26B472 3330C5D9A36D
Round 16 19BA9212 CF26B472 181C5D75C66D
Decryption
Output:
Encryption:
After initial permutation: 14A7D67818CA18AD
After splitting: L0=14A7D678 R0=18CA18AD
Round 1 18CA18AD 5A78E394 194CD072DE8C
Round 2 5A78E394 4A1210F6 4568581ABCCE
Round 3 4A1210F6 B8089591 06EDA4ACF5B5
Round 4 B8089591 236779C2 DA2D032B6EE3
Round 5 236779C2 A15A4B87 69A629FEC913
Round 6 A15A4B87 2E8F9C65 C1948E87475E
Round 7 2E8F9C65 A9FC20A3 708AD2DDB3C0
Round 8 A9FC20A3 308BEE97 34F822F0C66D
Round 9 308BEE97 10AF9D37 84BB4473DCCC
Round 10 10AF9D37 6CA6CB20 02765708B5BF
Round 11 6CA6CB20 FF3C485F 6D5560AF7CA5
Round 12 FF3C485F 22A5963B C2C1E96A4BF3
Round 13 22A5963B 387CCDAA 99C31397C91F
Round 14 387CCDAA BD2DD2AB 251B8BC717D0
Round 15 BD2DD2AB CF26B472 3330C5D9A36D
Round 16 19BA9212 CF26B472 181C5D75C66D
Cipher Text: C0B7A8D05F3A829C
Decryption
After initial permutation: 19BA9212CF26B472
After splitting: L0=19BA9212 R0=CF26B472
Round 1 CF26B472 BD2DD2AB 181C5D75C66D
Round 2 BD2DD2AB 387CCDAA 3330C5D9A36D
Round 3 387CCDAA 22A5963B 251B8BC717D0
Round 4 22A5963B FF3C485F 99C31397C91F
Round 5 FF3C485F 6CA6CB20 C2C1E96A4BF3
Round 6 6CA6CB20 10AF9D37 6D5560AF7CA5
Round 7 10AF9D37 308BEE97 02765708B5BF
Round 8 308BEE97 A9FC20A3 84BB4473DCCC
Round 9 A9FC20A3 2E8F9C65 34F822F0C66D
Round 10 2E8F9C65 A15A4B87 708AD2DDB3C0
Round 11 A15A4B87 236779C2 C1948E87475E
Round 12 236779C2 B8089591 69A629FEC913
Round 13 B8089591 4A1210F6 DA2D032B6EE3
Round 14 4A1210F6 5A78E394 06EDA4ACF5B5
Round 15 5A78E394 18CA18AD 4568581ABCCE
73
Round 16 14A7D678 18CA18AD 194CD072DE8C
Plain Text: 123456ABCD132536
Graded Task 1
You have implemented DES there is built in implemented DES in python in
crypto cipher module use it for encryption/decryption and provide output sample
example
# Example usage
if __name__ == "__main__":
# Input data (must be bytes)
data = b"Secret123"
1. Guess K1
P --[Encrypt with K1]--> Intermediate Value 1 (I1)
2. Guess K2
C --[Decrypt with K2]--> Intermediate Value 2 (I2)
Assumptions:
The attacker knows or can guess some plaintext-ciphertext pairs (this is called a
known-plaintext attack).
The DES encryption and decryption processes can be divided into independent stages,
allowing the attacker to perform partial encryption and decryption.
1. Intercept the Ciphertext: The attacker intercepts a known plaintext (i.e., a piece of
the original message that is known or can be guessed) and the corresponding
ciphertext (i.e., the encrypted version of that message).
2. Divide the DES Algorithm into Two Stages:
o DES operates in multiple rounds of encryption, but the MITM attack divides this into
two stages:
1. First encryption stage (Encrypt a known plaintext).
2. Second decryption stage (Decrypt a known ciphertext).
The attack leverages the fact that the encryption can be split into these stages, and the
intermediate value after one encryption round should match with the decrypted
intermediate value after one decryption round.
3. Guess the First Half of the Key: The attacker guesses the first half of the key (let’s
call it K1). The attacker encrypts the known plaintext using K1 and stores the result
(intermediate encryption value).
4. Guess the Second Half of the Key: The attacker guesses the second half of the key
(let’s call it K2). The attacker decrypts the intercepted ciphertext using K2 and stores
the result (intermediate decryption value).
5. Matching Intermediate Values:
o The attacker compares the intermediate values from the first stage (encrypting with
K1) and the second stage (decrypting with K2).
75
o If the intermediate values match, the attacker has likely found the correct combination
of the two keys (K1 and K2), which together form the full DES key.
o Since DES uses a single 56-bit key, this is broken down into halves for this type of
attack.
AES stands for Advanced Encryption Standard, and it is the only public encryption
scheme that the NSA approves for confidential information. We focus on its use as our
main block cipher from now on. AES is the current de facto block cipher, and it works
on 16 bytes at a time. It has three possible key lengths: 16-byte, 24-byte, or 32-byte. We
know that a block cipher is effectively a deterministic permutation on binary strings,
like a fixed-length reversible hash. Given a proper-length key and a 16-byte input we
should always get the same 16-byte output. Note that there are typically three ways to
work with bytes: plain ASCII, hex digest, and base64 (we haven’t played with this yet
but we will). A good chunk of your bugs come from transferring between hex and raw.
You explore AES in the next chapter as you manipulate images.
Using AES with Python Earlier in this chapter, you were introduced to PyCrypto as a
Python module that enables block ciphers using DES; it also has methods for encrypting
AES. The PyCrypto module is similar to the Java Cryptography Extension (JCE) that
is used in Java. The first step we will take in our AES encryption is to generate a strong
key. As you know, the stronger the key, the stronger the encryption. The key we use for
our encryption is oftentimes the weakest link in our encryption chain. The key we select
should not be guessable and should provide sufficient entropy, which simply means that
the key should lack order or predictability. The following Python code will create a
random key that is 16 bytes:
import os
import binascii
key = binascii.hexlify(os.urandom(16))
print ('key', [x for x in key] )
key [97, 53, 99, 97, 102, 99, 102, 102, 50, 101, 98, 57, 97, 51, 50, 50,
51, 52, 102, 49, 101, 51, 102, 52, 100, 49, 48, 51, 51, 49, 56, 51]
Now that you have generated a key, you will need an initialization vector. The IV
should be generated for each message to ensure a different encrypted text each time the
message is encrypted. The IV adds significant protection in case the message is
intercepted; it should mitigate the use of cryptanalysis to infer message or key data. The
IV is required to be transmitted to the message receiver to ensure proper decryption, but
unlike the message key, the IV does not need to be kept secret. You can add the IV to
process the encrypted text. The message receiver will need to know where the IV is
located inside the mes sage. You can create a random IV by using the following snippet;
note the use of random.randint. This method of generating random numbers is less
effective and it has a lower entropy, but in this case we are using it to create the IV that
will be used in the encryption process so there is less concern with the use of
randint here:
To decrypt the ciphertext, you will need the key that was used for the encryption.
Transporting the key, inside itself, can be a challenge. You will learn about key
exchange in a later chapter. In addition to the key, you will also need the IV. The IV
can be transmitted over any line of communication as there are no requirements to
encrypt it. You can safely send the IV along with the encrypted file and embed it in
plaintext, as shown here:
77
File Decryption Using AES
To decrypt the previous example, use the following code to reverse the process:
with open(verfile, 'w') as fout:
while True:
data = fin.read(sz)
n = len(data)
if n == 0:
break
decd = aes.decrypt(data)
n = len(decd)
if fsz > n:
fout.write(decd)
else:
fout.write(decd[:fsz]) # <- remove padding on last block
fsz -= n
78
Lab 06
Secure Hash Function
Objective
Activity Outcomes:
This lab teaches you the following topics:
How to c r e a t e : H M A C M D 5 d i g e s t i n P y t h o n
How to a p p l y S H A d i f f e r e n t v e r s i o n s t o m e s s a g e .
How to Simulating a Birthday Attack
79
1) Useful concepts:
For a one-way hash to be used in cryptographic systems, the algorithm must provide
preimage resistance, secondary resistance, and collision resistance:
■ Preimage resistance means that an attempt to find the original message that
produces a hash is computationally unrealistic or for a given h in the output space of
the hash function, it is hard to find any message x with H(x) = h.
■ Secondary resistance means that an attempt to find a second message that produces
the same hash is computationally unrealistic or for a given message x2≠x1 with H(x1)
=H(x2).
■ Collision resistance means that finding any two messages that will produce the same
hash is computationally unrealistic for the message pair or s x1≠x2 with H(x1)=H(x2).
In examining the rules, while the secondary resistance and collision resistance may
appear very similar, they are slightly different. From a (second) preimage attack we also
get a collision attack. The other direction doesn’t work as easily, though some collision
attacks on broken hash functions seem to be extensible to be almost as useful as second
preimage attacks (i.e., we find collisions where
most parts of the message can be arbitrarily fixed by the attacker).
The strength of the hash function does not equal the hash length. The strength of the
hash is about half the length of the hash due to the probability produced by the Birthday
Attack. The birthday attack exploits the mathematics behind the birthday problem in
probability theory.
Consider the scenario in which a teacher with a class of 30 students (n = 30) asks for
everybody’s birthday to determine whether any two students have the same birthday.
The birthday attack treats our birthdays as uniformly distributed values out of 365 days.
The general intuition is that it takes √N samples from a space of size N to have 50%
chance of collision. Imagine selecting some value (k) at random from N. Then out of
the k values you picked there are k(k − 1)/2 pairs. For any given pair there is a 1/N
chance of collision. This gives k(k − 1)/2N chance of collision. Therefore, k ~√N will
lead to around 50% chance of collision.
The birthday attack relies on any match coming from within a set and not a specific
match to a specific value. That intuition should guide us as we approach Message
Authentication Codes (MACs). This birthday attack gives us a generic approach for
finding two messages that hash to the same value in far less time than brute force. The
size that matters is the output size of the hash function, too.
HMAC can use a variety of hashing algorithms, like MD5, SHA1, SHA256, etc. The
HMAC function is not process intensive, so it has been widely accepted, and it is easy
to implement in mobile and embedded devices while maintaining decent security. The
following code example shows how to generate an HMAC MD5 digest with Python:
80
Practice -Lab Activity 1: HMAC MD5 digest generation in Python:
import hmac # Import the hmac module to use the HMAC algorithm
from hashlib import md5 # Import the md5 function from hashlib to use as the
hashing algorithm
# Create a new HMAC object using the secret key and MD5 as the hashing algorithm.
# The second argument is an optional initial message (here we provide an empty byte
string b'').
h = hmac.new(key, b'', md5)
Note:
The md5 library was a Python library that provided a simple interface for generating
MD5 hashes.
This library has been deprecated in favor of the hashlib library, which provides a more
flexible and secure interface for generating hashes.
The below code demonstrates the working of MD5 hash accepting bytes and output as
bytes.
81
# Python 3 code to demonstrate the
# working of MD5 (byte - byte)
# Encoding the string 'GeeksforGeeks' as bytes and hashing it using the MD5 hash
function
# MD5 requires a byte input, so we prefix the string with 'b' to convert it to bytes.
result = hashlib.md5(b'GeeksforGeeks')
Code Output:
Explanation: The above code takes byte and can be accepted by the hash function. The
md5 hash function encodes it and then using digest (), byte equivalent encoded string is
printed.
Below code demonstrated how to take string as input and output hexadecimal equivalent
of the encoded value.
Solution
SHA, (Secure Hash Algorithms) are set of cryptographic hash functions defined by the
language to be used for various applications such as password security etc. Some
variants of it are supported by Python in the “hashlib” library. These can be found using
“algorithms guaranteed” function of hashlib.
Output:
The available algorithms are: {'sha256', 'sha384', 'sha224', 'sha512', 'sha1', 'md5'}
To proceed with, lets first discuss the functions going to be used in this article.
Functions associated:
encode() : Converts the string into bytes to be acceptable by hash function.
hexdigest() : Returns the encoded data in hexadecimal format.
SHA Hash
import hashlib
# initializing string
str = "GeeksforGeeks"
print ("\r")
# initializing string
str = "GeeksforGeeks"
print ("\r")
# initializing string
str = "GeeksforGeeks"
84
print("The hexadecimal equivalent of SHA224 is : ")
print(result.hexdigest())
print ("\r")
# initializing string
str = "GeeksforGeeks"
print ("\r")
# initializing string
str = "GeeksforGeeks"
Output:
The hexadecimal equivalent of SHA256 is :
f6071725e7ddeb434fb6b32b8ec4a2b14dd7db0d785347b2fb48f9975126178f
Explanation: The above code takes string and converts it into the byte equivalent using
encode() so that it can be accepted by the hash function. The SHA hash functions encode
it and then using hexdigest(), hexadecimal equivalent encoded string is printed.
Step 1: Use a reduced hash size (e.g., truncate the output of SHA-256 to 16 bits).
Step 2: Generate random strings and compute truncated hashes.
Step 3: Find two different strings that produce the same truncated hash (collision).
python
Copy code
import hashlib
def birthday_attack(iterations=10000):
hashes = {}
for _ in range(iterations):
random_string = generate_random_string()
sha256_hash =
hashlib.sha256(random_string.encode()).hexdigest()
truncated_hash = truncate_hash(sha256_hash)
if truncated_hash in hashes:
print(f"Collision found: {random_string} and
{hashes[truncated_hash]} have the same truncated hash!")
return
hashes[truncated_hash] = random_string
birthday_attack()
86
Graded Task 2: Exploring Hash Stretching (PBKDF2)
python
Copy code
import hashlib
import time
python
Copy code
import hashlib
import random
import string
def generate_random_string(length=10):
return ''.join(random.choice(string.ascii_letters +
string.digits) for _ in range(length))
def find_collision(iterations=100000):
87
hashes = {}
for _ in range(iterations):
random_string = generate_random_string()
sha256_hash =
hashlib.sha256(random_string.encode()).hexdigest()
if sha256_hash in hashes:
print(f"Collision found: {random_string} and
{hashes[sha256_hash]} have the same hash!")
return
hashes[sha256_hash] = random_string
find_collision()
Objective: Implement password hashing with a salt to demonstrate how salt improves
security.
python
Copy code
import hashlib
import os
def hash_password_with_salt(password):
# Generate a random 16-byte salt
salt = os.urandom(16)
hash_obj = hashlib.pbkdf2_hmac('sha256',
password.encode(), salt, 100000)
return salt, hash_obj
88
# Simulate password verification
password_check = input("Re-enter the password to verify:
")
if verify_password(password_check, salt, password_hash):
print("Password verified!")
else:
print("Password verification failed.")
89
90
Lab 07
Blockchain Technology
Activity Outcomes:
This lab teaches you the following topics:
How to implement blockchain concept.
How to find a solution of real world problems using Blockchain Technology.
91
What is blockchain?
Blockchain is a record-keeping technology designed to make it impossible to hack the
system or forge the data stored on the blockchain, thereby making it secure and
immutable. It's a type of distributed ledger technology (DLT), a digital record-keeping
system for recording transactions and related data in multiple places at the same time.
Each computer in a blockchain network maintains a copy of the ledger where
transactions are recorded to prevent a single point of failure. Also, all copies are updated
and validated simultaneously.
Blockchain is also considered a type of database, but it differs substantially from
conventional databases in how it stores and manages information. Instead of storing
data in rows, columns, tables and files as traditional databases do, blockchain stores
data in blocks that are digitally chained together. In addition, a blockchain is a
decentralized database managed by computers belonging to a peer-to-peer network
instead of a central computer like in traditional databases.
Bitcoin, launched in 2009 on the Bitcoin blockchain, was the first cryptocurrency and
popular application to successfully use blockchain. As a result, blockchain has been
most often associated with Bitcoin and alternatives such as Dogecoin and Bitcoin Cash,
which both use public ledgers.
A blockchain ledger consists of two types of records, individual transactions and blocks.
The first block has a header and data that pertain to transactions taking place within a
set time period. The block's timestamp is used to help create an alphanumeric string
called a hash. After the first block has been created, each subsequent block in the ledger
uses the previous block's hash to calculate its own hash.
Before a new block can be added to the chain, its authenticity must be verified by a
computational process called validation or consensus. At this point in the blockchain
process, a majority of nodes in the network must agree the new block's hash has been
calculated correctly. Consensus ensures that all copies of the blockchain distributed
ledger share the same state.
Once a block has been added, it can be referenced in subsequent blocks, but it can't be
changed. If someone attempts to swap out a block, the hashes for previous and
subsequent blocks will also change and disrupt the ledger's shared state.
When consensus is no longer possible, other computers in the network are aware that a
problem has occurred, and no new blocks will be added to the chain until the problem
is solved. Typically, the block causing the error will be discarded and the consensus
process will be repeated. This eliminates a single point of failure.
92
Key features of blockchain technology
Blockchain technology is built on a foundation of unique characteristics that
differentiate it from traditional databases. The following are its most important and
defining characteristics:
94
95
Here are several blockchain-related programming tasks that will introduce
fundamental concepts like hashing, proof of work, and block validation, helping
students understand how blockchains operate at a technical level.
Example
Create a class named Person, use the __init__() function to assign values for name and
age:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person("John", 36)
print(p1.name)
print(p1.age)
Note: The __init__() function is called automatically every time the class is being
used to create a new object.
Step 1: Define a Block class that includes properties like index, timestamp,
data, previous_hash, and hash.
Step 2: Compute the block’s hash by combining the block’s properties (excluding
hash) and hashing them using SHA-256.
Example:
96
self.index = index # Index or
position of the block in the chain
self.timestamp = time.time() # Timestamp of
block creation
self.data = data # Data stored in
the block (e.g., transactions)
self.previous_hash = previous_hash # Hash of the
previous block, linking to it
self.hash = self.compute_hash() # Hash of the
current block, generated using compute_hash method
def compute_hash(self):
"""
Method to calculate the SHA-256 hash of the block's
contents.
The hash is generated using the block's index,
timestamp, data, and the previous block's hash.
"""
# Combine the block's properties into a single string
block_string =
f"{self.index}{self.timestamp}{self.data}{self.previous_hash}"
# Compute and return the SHA-256 hash of the block
string
return
hashlib.sha256(block_string.encode()).hexdigest()
def create_genesis_block(self):
"""
Creates the first block in the blockchain (the genesis
block).
The genesis block has an index of 0, default data
"Genesis Block",
and a previous hash of "0" since it's the first block.
"""
return Block(0, "Genesis Block", "0")
def print_blockchain(self):
"""
Prints out each block in the blockchain.
Displays the block's index, data, hash, and the hash of
the previous block.
"""
# Iterate over all blocks in the chain and print their
details
for block in self.chain:
print(f"Index: {block.index}, Data: {block.data},
Hash: {block.hash}, Previous Hash: {block.previous_hash}")
98
blockchain = Blockchain() # Create a new
blockchain with a genesis block
blockchain.add_block("Block 1 Data") # Add a block with
data "Block 1 Data"
blockchain.add_block("Block 2 Data") # Add a block with
data "Block 2 Data"
blockchain.print_blockchain() # Print the details of
the blockchain
Purpose of PoW
The purpose of a consensus mechanism is to bring all the nodes in agreement, that is,
trust one another, in an environment where the nodes don’t trust each other.
All the transactions in the new block are then validated and the new block is then
added to the blockchain.
The block will get added to the chain which has the longest block height
(see blockchain forks to understand how multiple chains can exist at a point in time).
Miners(special computers on the network) perform computation work in solving a
complex mathematical problem to add the block to the network, hence named, Proof-
of-Work.
With time, the mathematical problem becomes more complex.
Step 1: Modify the Block class to include a nonce and a difficulty target.
99
Step 2: Implement the PoW mechanism that requires the block’s hash to start with a
certain number of zeros.
def compute_hash(self):
"""
Compute the SHA-256 hash of the block's contents.
The hash includes the block's index, timestamp, data,
previous hash, and nonce.
"""
# Combine the block's attributes into a single string
block_string =
f"{self.index}{self.timestamp}{self.data}{self.previous_hash}{s
elf.nonce}"
# Compute and return the SHA-256 hash of the block
string
return
hashlib.sha256(block_string.encode()).hexdigest()
100
PoW requires finding a hash that starts with a certain
number of leading zeros,
defined by the difficulty parameter.
"""
# The required hash prefix is a string of '0' repeated
difficulty times (e.g., "00" for difficulty=2)
prefix = '0' * difficulty
# Loop until we find a hash that starts with the
required number of leading zeros
while True:
self.hash = self.compute_hash() # Compute the
block's hash
if self.hash.startswith(prefix): # Check if the
hash satisfies the difficulty requirement
return self.hash # Return the valid
hash if it meets the condition
self.nonce += 1 # Increment the
nonce and try again (to find a new hash)
Step 1: Ensure that each block’s previous_hash matches the hash of the previous
block.
Step 2: Recompute the hash of each block and verify it matches the stored hash.
def compute_hash(self):
"""
Compute the SHA-256 hash of the block.
The hash is generated from the block's index,
timestamp, data, previous hash, and nonce.
"""
# Concatenate the block's attributes into a string
block_string =
f"{self.index}{self.timestamp}{self.data}{self.previous_hash}{s
elf.nonce}"
# Return the SHA-256 hash of the concatenated string
return
hashlib.sha256(block_string.encode()).hexdigest()
def create_genesis_block(self):
"""
Creates the genesis block (the first block in the
chain).
The genesis block has an index of 0, default data
"Genesis Block", and a previous hash of "0".
"""
return Block(0, "Genesis Block", "0") # Return the
first block with index 0
def is_chain_valid(self):
"""
Validates the entire blockchain by ensuring the hashes
and block linking are correct.
"""
# Loop through the blockchain from the second block
(index 1) onward
for i in range(1, len(self.chain)):
current_block = self.chain[i]
previous_block = self.chain[i-1]
A 51% attack refers to a scenario in which a single entity or group of attackers gains
control over more than 50% of the computational power (hashing power) in a
blockchain network, particularly in proof-of-work (PoW) blockchains like Bitcoin. This
gives the attacker the ability to manipulate the blockchain by performing actions that
compromise its integrity. Some of the risks associated with a 51% attack include:
Step 1: Create two versions of a blockchain: one valid and one modified by an
attacker.
104
Step 2: The attacker rewrites history by modifying the data in an earlier block and
recalculating all subsequent blocks’ hashes.
def compute_hash(self):
"""
Compute the SHA-256 hash of the block.
This method concatenates the block's attributes and
returns the hash of the concatenated string.
"""
# Combine block properties into a string and hash it
block_string =
f"{self.index}{self.timestamp}{self.data}{self.previous_hash}{s
elf.nonce}"
return
hashlib.sha256(block_string.encode()).hexdigest()
def create_genesis_block(self):
"""
Creates the genesis block, the first block in the
blockchain.
The genesis block has an index of 0, fixed data, and no
previous block (previous hash = "0").
"""
return Block(0, "Genesis Block", "0") # The first
block with index 0 and default previous hash
def is_chain_valid(self):
"""
Validates the integrity of the blockchain by checking
each block's hash and previous hash linkage.
"""
# Loop through the chain starting from the second block
(index 1)
for i in range(1, len(self.chain)):
current_block = self.chain[i]
106
previous_block = self.chain[i-1]
Objective: Implement a simple reward system where a miner gets rewarded for
successfully mining a block.
class Blockchain:
def __init__(self):
self.chain = [self.create_genesis_block()]
self.mining_reward = 50
self.pending_rewards = {}
def create_genesis_block(self):
return Block(0, "Genesis Block", "0")
def print_rewards(self):
for miner, reward in self.pending_rewards.items():
print(f"Miner: {miner}, Reward: {reward} coins")
# Example usage
blockchain = Blockchain()
blockchain.mine_block("Miner1")
blockchain.mine_block("Miner2", difficulty=4)
blockchain.print_rewards()
108
Home Task 7: Basic Blockchain Peer-to-Peer (P2P) Network Simulation
Step 1: Define multiple nodes that maintain their own copy of the blockchain.
Step 2: Implement a method to synchronize the blockchain among the nodes.
class Node:
def __init__(self, name):
self.name = name
self.blockchain = Blockchain()
# Example usage
node1 = Node("Node1")
node2 = Node("Node2")
These tasks introduce various blockchain concepts like block creation, proof of work,
blockchain integrity validation, mining rewards, and P2P synchronization. Students can
further build on these tasks by adding more complex features like transaction handling,
consensus algorithms, or smart contracts.
109
Lab 08
Midterm-Exam
Lab manual, any code generating online site (chatgpt) are strictly prohibited during
examination.
Mobile /WhatsApp is not allowed during examination.
Attempt all questions.
CLO-4 Implement a cryptographic algorithm to ensure information security.
Question 1. [5 Marks]
Write a Python program that demonstrates how Caesar Cipher encryption and
decryption work using modular arithmetic.
Steps:
110
Task 2-a: Correct following Vigenère cipher code also provide explanation of
mistake you have corrected.
def key_vigenere(key):
keyArray = []
for i in range(0, len(key)
keyElement = ord(key[i].upper) – 65
keyArray.append(keyElemnt)
return keyArray
secretKey = 'DECLARATION'
key = key_vigenere(secretKey)
print(keys)
Lab Task 2b: Write a program to simulate a brute-force attack on DES using a small
key space (for learning purposes, use a smaller key size like 8 bits). This will
demonstrate how easily DES can be broken with modern computing power when the
key space is limited.
Steps:
1. Ask the user for a plaintext message and a small key size (e.g., 4-bit or 8-bit keys for
the simulation).
2. Encrypt the message using DES.
3. Write a brute-force algorithm that tries all possible keys within the small key space.
4. Print the correct key when found and the decrypted message.
Question 3 [5 Marks]
Implement password hashing with a salt to demonstrate how salt improves security.
Step 1: Generate a random salt for each password.
Step 2: Hash the concatenation of the password and the salt.
Step 3: Store both the salt and the hash for future verification.
Question 4. [5 Marks]
Write code to apply an HMAC digest to a signed message. Using HMAC to Sign
Message The file that we are creating the message digest for is a simple text file that
contains only Hello. When run, the code reads a data file and computes an HMAC
signature for it
Steps:
Define Secret Key: Set a secret key to use in the HMAC process.
111
Initialize HMAC Object: Create an HMAC object with the specified key and hash
function.
Open the Target File: Open the file you want to generate an HMAC for, usually in
binary mode for accurate data reading.
Read the File in Chunks: Use a loop to read the file in manageable chunks (e.g.,
1024 bytes) for memory efficiency.
Process Each Chunk: For each chunk, update the HMAC object with the current
data block.
Finalize and Retrieve Digest: After reading all chunks, compute the final HMAC
digest, typically in hexadecimal format.
Output the Digest: Display or return the resulting digest, representing the HMAC of
the entire file.
Question 5. [3 +2 = 5 Marks]
Lab Task 5-b: Write a Python program that simulates a secure messaging system
using the Vernam Cipher. The key is shared securely between two users and is only
used once (One-Time Pad).
Steps:
------------------------Best of Luck-------------------------
112
Lab 09
Key distribution problem
Objective:
This lab will introduce students into Key distribution problem. The key distribution
problem was solved by Diffie and Hellman as that in which they introduced public key
cryptography. Their protocol for key distribution, called Diffie–Hellman Key
Exchange, allows two parties to agree a secret key over an insecure channel.
Activity Outcomes:
This lab teaches you the following topics:
How to program key distribution problem.
Diffie-Hellman algorithm coding and attacks description.
The Diffie-Hellman algorithm is being used to establish a shared secret that can be used
for secret communications while exchanging data over a public network using the
elliptic curve to generate points and get the secret key using the parameters.
For the sake of simplicity and practical implementation of the algorithm, consider only
4 variables, one prime P and G (a primitive root of P) and two private values a and b.
P and G are both publicly available numbers. Users (say Alice and Bob) pick private
values a and b and they generate a key and exchange it publicly. The opposite person
receives the key and that generates a secret key, after which they have the same secret
key to encrypt.
Alice Bob
113
Alice Bob
Example:
Implementation:
# Diffie-Hellman Code
if __name__ == "__main__":
main()
Output
The value of P : 23
The value of G : 9
The private key a for Alice : 4
The private key b for Bob : 3
Secret key for the Alice is : 9
115
Secret key for the Bob is : 9
Let’s assume that the eavesdropper EVE knows the public values p and g like everyone
else, and from her eavesdropping, she learns the values exchanged by Alice and Bob,
gᵃ mod p and gᵇ mod p, as well. With all her knowledge, she still can’t compute the
secret key S, as it turns out, if p and g are properly chosen, it’s very, very hard for her
to do.
For instance, you could brute force it and try all the options, but The calculations (mod
p) make the discrete log calculation super slow when the numbers are large. If p and g
have thousands of bits, then the best-known algorithms to compute discrete logs,
although faster than plain brute force, will still take millions of years to compute.
Even with its immunity to brute force, it’s vulnerable to MITM (man in the middle
position).
Step 1: Selected public numbers p and g, p is a prime number, called the “modulus”
and g is called the base.
Step 2: Selecting private numbers.
Let Alice pick a private random number a and let Bob pick a private random number
b, Malory picks 2 random numbers c and d.
116
Step 3: Intercepting public values,
Malory intercepts Alice’s public value (ga(mod p)), block it from reaching Bob, and
instead sends Bob her own public value (gc(modp)) and Malory intercepts Bob’s public
value (gb(mod p)), block it from reaching Alice, and instead sends Alice her own public
value (gd (modp))
Step 5: If Alice uses S1 as a key to encrypt a later message to Bob, Malory can decrypt
it, re-encrypt it using S2, and send it to Bob. Bob and Alice won’t notice any problem
and may assume their communication is encrypted, but in reality, Malory can decrypt,
read, modify, and then re-encrypt all their conversations.
Implementation Code:
import random
117
class A:
def __init__(self):
# Generating a random private number selected by alice
self.n = random.randint(1, p)
def publish(self):
# generating public values
return (g**self.n)%p
class B:
def __init__(self):
# Generating a random private number selected for alice
self.a = random.randint(1, p)
# Generating a random private number selected for bob
self.b = random.randint(1, p)
self.arr = [self.a,self.b]
alice = A()
bob = A()
eve = B()
118
print(f'Bob published (gb): {gb}')
print(f'Eve published value for Alice (gc): {gea}')
print(f'Eve published value for Bob (gd): {geb}')
Output:
Enter a prime number (p) : 227
Enter a number (g) : 14
Code output
119
Graded Home Task
Search about latest attack on key exchange algorithms and provide its implementation
in python.
120
Lab 10
Cryptographic Math
This lab introduces basic number theory, Fermat’s, Euclidean algorithm, Euler’s and
the Chinese remainder theorem, and gives a more in-depth look at modular arithmetic.
Activity Outcomes:
In this lab, students will gain following mathematical cryptographic knowledge:
Gain an understanding of modular arithmetic
Understand the importance of the greatest common divisor (GCD)
Gain an understanding of Fermat’s Theorem Euclidean algorithm and Euler’s
Theorem
Gain an understanding of Chinese remainder Theorem.
The logic of modular arithmetic began with the quotient-remainder theorem. The
Quotient-remainder theorem states that for every integer A and positive B there exist
different integers Q and R such that: A = B * Q + R, 0 = < r = < b. When a = 95 and b
= 10, what is the unique value of q (quotient) and r (remainder)? You find that the
quotient equals 9 and the remainder equals 5. Once you understand the quotient-
remainder theorem, it is easier to understand our first bit of cryptographic math:
modular arithmetic. Here is an example: 23 ≡ 2(mod7), which reads as “23 is equivalent
to 2 mod 7.” You can also type it into a search engine as 23 mod 7 to see the answer.
You can further examine the modulo by stating that a ≡ b (mod q) when a minus b is a
multiple of q. Another way to state it numerically would be: 123 ≡ 13 (mod 11) because
123 – 13 = 110 = 11 * 10. An alternative way to think about it is to examine 53 (mod
121
13), which would be to say that 53 is equivalent to 53 − 13 = 40, which is equivalent to
27, which is equivalent to 14, which is equivalent to 1, which is equivalent to −12,
which is equivalent to −25, and so on. In fact, 53 ≡ {53 + k⋅13|∀ k ∈Z} (you can read
that as “is equivalent to the set of all numbers of form 53 plus an integer multiple of
13”). the modulus is represented by the % sign. To illustrate this example in Python,
type the following:
>>> 53 % 13
1
>>> 40 % 13
1
>>> 27 % 13
1
>>> 14 % 13
1
>>> -12 % 13
1
Now that you understand modular arithmetic, we turn our attention to the greatest
common divisor (GCD). The GCD is the largest number that perfectly divides two
integers: a and b. For example, the GCD of 12 and 18 is 6. This is an excellent
opportunity to introduce Euclid’s algorithm, which is a technique for finding the GCD
of two integers with negligible effort. To find the GCD of two integers A and B, use
the following rules:
If A = 0 then GCD(A, B) = B
If B = 0 then GCD(A, B) = A
A = B * Q + R and B ≠ 0 then GCD (A, B) = GCD (B,R)
Therefore, write A using the quotient remainder form: A = B * Q + R
Find GCD(B, R)
Euclid’s algorithm works by continuously dividing one number into another and
calculating the quotient and remainder at each step. Each phase produces a decreasing
sequence of remainders, which terminate at zero, and the last non-zero remainder in the
sequence is the GCD. You will revisit Euclid’s algorithm shortly when you examine the
modular inverses; for now, you can use the Algorithm to write a GCD function in
Python:
def gcd(a,b):
if b == 0:
return a
else:
return gcd(b, a % b)
print(gcd(12,18))
122
Now that you know how to create your own GCD function, note that it is very inefficient
due to its use of recursion. Prefer using Python’s built-in GCD function, which is part
of the standard Python math library.
Visit site to explore in detail: https://www.geeksforgeeks.org/gcd-in-python/
Prime Numbers
Prime numbers in cryptography are vital to the security of our encryption schemes.
Prime factorization, also known as integer factorization, is a mathematical problem that
is used to secure public-key encryption schemes. This is achieved by using extremely
large semiprime numbers that are a result of the multiplication of two prime numbers.
As you may remember, a prime number is any number that is only divisible by 1 and
itself. The first prime number is 2. Additional prime numbers include 3, 5, 7, 11, 13, 17,
19, 23, and so on. An infinite number of prime numbers exist, and all numbers have one
prime factorization. A semiprime number, also known as biprime, 2-almost prime, or a
pq number, is a natural number that is the product of two prime numbers. The
semiprimes less than 50 are 4, 6, 9, 10, 14, 15, 21, 22, 25, 26, 33, 34, 35, 38, 39, 46, and
49. Prime numbers are significant in cryptography. Here is a simple Python script that
tests if an integer value is prime:
def isprime(x):
x = abs(int(x)) # Ensure x is a non-negative integer
if x < 2:
return False # Numbers less than 2 are not prime
elif x == 2:
return True # 2 is the only even prime number
elif x % 2 == 0:
return False # Other even numbers are not prime
else:
for n in range(3, int(x ** 0.5) + 1, 2):
if x % n == 0:
return False # Found a factor, thus not prime
123
return True # No factors found, thus prime
# Example usage
print(isprime(100000007)) # It will return True or False
Fermat’s Little Theorem Fermat’s little theorem is used in number theory to compute
the powers of inte gers modulo prime numbers. The theorem is a special case of Euler’s
theorem. Fermat’s little theorem states let p be a prime number, and a be any integer. If
n is a prime number, then for every a, 1 < a < p − 1,
a p − 1 ≡ 1 (mod p) or a p − 1 % p = 1 To ensure this makes sense, let’s look at an example:
p = prime integer number a = integer which is not a multiple of p According to Fermat’s
little theorem, 2(17 − 1) ≡ 1 mod (17) 65,536 % 17 ≡ 1 This means (65,536 – 1) is a
multiple of 17. This is proven by multiplying 17 * 3,855, which equals (65,536 – 1) or
65,535. If you know the modulo m is prime, then you can also use Fermat’s little
theorem to find the inverse. Here is a quick and easy function that will return whether
an integer is prime or not:
def CheckIfProbablyPrime(x):
if x < 2:
return False
return pow(2, x-1, x) == 1
Example Usage
>>> CheckIfProbablyPrime(19)
True
>>> CheckIfProbablyPrime(31)
True
>>> CheckIfProbablyPrime(589)
False
The Euclidean algorithm is a way to find the greatest common divisor of two
positive integers. GCD of two numbers is the largest number that divides both of
them. A simple way to find GCD is to factorize both numbers and multiply common
prime factors.
124
Basic Euclidean Algorithm for GCD:
The algorithm is based on the below facts.
If we subtract a smaller number from a larger one (we reduce a larger number),
GCD doesn’t change. So if we keep subtracting repeatedly the larger of two, we end
up with GCD.
Now instead of subtraction, if we divide the larger number, the algorithm stops
when we find the remainder 0.
return gcd(b % a, a)
# Driver code
if __name__ == "__main__":
a = 10
b = 15
print("gcd(", a, ",", b, ") = ", gcd(a, b))
a = 35
b = 10
print("gcd(", a, ",", b, ") = ", gcd(a, b))
a = 31
b=2
print("gcd(", a, ",", b, ") = ", gcd(a, b))
Output
GCD(10, 15) = 5
GCD(35, 10) = 5
GCD(31, 2) = 1
Earlier in this lab, you were introduced to Fermat’s little theorem. We will now examine
125
a generalization of Fermat’s theorem known as Euler’s theorem. Both Fermat’s and
Euler’s theorems play an important role in public-key cryptography, which will be
explored in greater detail in next lab. In number theory, Euler’s theorem, also known as
Euler’s totient theorem or the Fermat–Euler theorem, states that if n and a are coprime
positive integers, then aφ(n) ≡ 1 mod n where φ(n) is Euler’s totient function. In 1736,
Leonhard Euler published his proof of Fermat’s little theorem, which Fermat had
presented without proof. Subsequently, Euler presented other proofs of the theorem,
culminating with “Euler’s theorem” in his paper of 1763, in which he attempted to find
the smallest exponent for which Fermat’s little theorem was always true.
Euler investigated the properties of numbers; he specifically studied the distribution of
prime numbers. One crucial function he defined is named the PHI function; the PHI
function measures the breakability of a number. Assume you have the number n; the
function calculates the number of integers that are less than or equal to n and do not
share any common factor with n; you see it in the following notation: ɸ [n].
For example, if you wanted to examine ɸ [8], you would examine all values from 1 to
8 and count all integers with which 8 does not share a factor greater than 1; the numbers
are 1, 3, 5, 7. The function produces 4. As it turns out, calculating the PHI of a prime
number P is simple. ɸ [P] = P − 1. To calculate ɸ [7], you count all integers except 7
since none of the integers share a factor with 7; therefore, ɸ [7] = 6. Assume a larger
prime such as 21,377. ɸ [21,377] = 21,376. The equation looks like the following:
ap − 1 ≡ 1 mod p
Euler totient functions offer benefits to speed up modular inverse computations. Euler’s
totient function Φ(n) for an input n is the count of numbers in the format of {1, 2, 3, 4,
5, n} that are relatively prime to n, i.e., the numbers whose GCD with n is 1. Examine
the following six examples, which calculate the Euler’s totient function Φ (n) in respect
to the inputs 1 through 6. The output will be the number of positive integers that do not
exceed n and also have no common divisors with n other than the common divisor 1: Φ
(1) = 1 gcd (1, 1) is 1 Φ(2) = 1 gcd (1, 2) is 1, but gcd (2, 2) is 2 Φ (3) = 2 gcd (1, 3) is
1 and gcd (2, 3) is 1 Φ (4) = 2 gcd (1, 4) is 1 and gcd (3, 4) is 1 Φ (5) = 4 gcd (1, 5) is
1, gcd (2, 5) is 1, gcd (3, 5) is 1, and gcd (4, 5) is 1 Φ (6) = 2 gcd (1, 6) is 1 and gcd (5,
6) is 1
Use the following Python to test Euler’s totient function on integers 1 through 20. The
output should resemble Figure 4.3.
import math
def phi(n):
amount = 0
for k in range(1, n + 1):
if math.gcd(n, k) == 1:
amount += 1
return amount
for n in range(1,20) :
print("Φ(",n,") = ",phi(n))
in above Python program, you can use the matplotlib library and create a graph, as
shown here:
126
import math
import numpy as np
from matplotlib import pyplot as plt
def phi(n):
amount = 0
for k in range(1, n + 1):
if math.gcd(n, k) == 1:
amount += 1
return amount
for i in range (500):
phi_n = phi(i)
#print (i ,phi_n)
plt . plot (i ,phi_n , 'o ')
plt.xlabel("Value of x")
plt.ylabel("Value of y")
plt.title("Euler's Theorem")
plt.show()
Figure 4.4 shows the output of using Euler’s theorem and the MatPlot library to
create a graph. Figure 4.3: Euler.py tes
127
Figure 4.4: EulerPlot.py tes
128
def find_min_x(nums, rems):
# Initialize result
x=1
while True:
# Check if remainder of x % nums[j] is rem[j] for all j from 0 to k-1
for j in range(len(nums)):
if x % nums[j] != rems[j]:
break
return x
# Example Usage
nums = [3, 4, 5]
rems = [2, 3, 1]
print(find_min_x(nums, rems))
Output
11
129
Lab 11
ElGamal encryption
This lab will introduce students to ElGamal encryption (1984), T. ElGamal announced
a public-key scheme based on discrete logarithms, closely related to the Diffie–Hellman
technique [ELGA84, ELGA85]. The ElGamal cryptosystem is used in some form in a
number of standards including the digital signature standard (DSS), which is covered
in Chapter 13, and the S/MIME email standard (Chapter 21).
Activity Outcomes:
.Instructor Note:
As pre-lab activity, read Chapter 10 from the book (Learn cryptography and Network
Security Principles and Practice by William Stallings.,) for concept understanding.
130
1) Idea of ElGamal Cryptosystem:
El-Gamal encryption is a public-key cryptosystem.El-Gamal uses asymmetric key
encryption for communicating between two parties and encrypting the message. This
cryptosystem is based on the difficulty of finding a discrete logarithm in a cyclic group.
That is, even if we know ga and gk, it is extremely difficult to compute gak. In this
section, we will examine the basic idea of the cryptosystem with an example using
cryptography’s: Alice and Bob.
131
The following Python code will help you gain an understanding of these steps.
132
import random
from math import pow, gcd
def main():
msg = 'Please do not let the enemy know our position.'
print("Original Message :", msg)
print()
133
q = random.randint(pow(10, 20), pow(10, 50))
g = random.randint(2, q)
key = gen_key(q) # Private key for receiver
h = power(g, key, q)
en_msg, p = encrypt(msg, q, h, g)
dr_msg = decrypt(en_msg, p, key, q)
dmsg = ''.join(dr_msg)
print()
print("The encrypted message :", en_msg)
print()
print("Decrypted Message :", dmsg)
print()
if __name__ == '__main__':
main()
The preceding code should take the message “Please do not let the enemy know our
position.” and generate an ElGamal key that is used to encrypt and decrypt the
message. Examine Figure. The program displays the ga and gk that is produced along
with the gak.
134
Code Output
135
Lab 12
RSA
The pioneering paper by Diffie and Hellman [DIFF76b] introduced a new approach to
cryptography and, in effect, challenged cryptologists to come up with a cryptographic
algorithm that met the requirements for public-key systems. One of the first successful
responses to the challenge was developed in 1977 by Ron Rivest, Adi Shamir, and Len
Adleman at MIT and first published in 1978 [RIVE78]. The Rivest-Shamir-Adleman
(RSA) scheme has since that time reigned supreme as the most widely accepted and
implemented general-purpose approach to public-key encryption.
.
Activity Outcomes:
Instructor Note:
As pre-lab activity, read Chapter 9, page 297-300 from the book (Cryptography and
Network Securi Principles and Practic Eighth Editi Global Editi William Stallings) to
know the basics of RSA.
136
1) Solved lab Activity:
This example provide simple implementation of the RSA algorithm that can encrypt
and decrypt a message. Note that the keys here are far too small to be of practical use
since they are still relatively easy to factor:
import random
# Euclid's algorithm to find the greatest common divisor (GCD) of two numbers
def gcd(a, b):
while b != 0:
a, b = b, a % b
return a
if __name__ == '__main__':
print("Chapter 8 - Understanding RSA")
while True:
try:
p = int(input("Enter a prime number: "))
q = int(input("Enter a second distinct prime number: "))
break # Exit the loop if both inputs are valid
except ValueError:
print("Invalid input. Please enter valid integers.")
Code Output
To produce the output in Figure 8.1, enter 11 as your first prime and 17 as your second.
139
Lab 13
Elliptic Curve Cryptography
This lab will introduce students to Elliptic Curve Cryptography in Python. Students
will also get in depth of ECC mechanism with the process of key generation.
Activity Outcomes:
This lab teaches you the following topics:
Basic concept understanding of ECC.
ECC key generation process.
Implementing Basic Operations for ECC
Instructor Note:
As pre-lab activity, read Chapter 10 from the book (Learn cryptography and
Network Security Principles and Practice by William Stallings.,) for concept
understanding.
140
1) Elliptic Curve Cryptography: Concept
Now that you have a better understanding of how the more traditional algorithms work
using Python, we will examine an alternative approach that is considered a more
efficient type of public-key cryptography: elliptic curve cryptography, or as it is more
simply known, ECC. The security of the cryptosystem lies within the difficulty of
solving discrete logarithms on the field defined by specific equations computed over a
curve; the group of cryptographic algorithms were introduced in 1985 and were based
on the esoteric branch of mathematics called elliptic curves. Although the system was
introduced in the mid ’80s, it took another twenty years for the cryptosystem to gain
wide acceptance. Several factors are contributing to its increasing popularity. First, the
security of 1024-bit RSA encryption is degrading due to faster computing and a better
understanding and analysis of encryption methods. While brute force is still unlikely to
crack 1024-bit RSA keys, other approaches, including highly intensive parallel
computing in distributed computing arrays, are resulting in more sophisticated attacks.
These attacks have reduced the effectiveness of this level of security. Even 2,048-bit
encryption is estimated by the RSA Security to be effective only until 2030. A second
factor that is contributing to the adoption of ECC is that many government entities have
started to accept ECC as anncryption method. Third, the authentication speed of ECC
is faster than RSA in terms of server authentication. Finally, certificate authorities have
started embedding ECC algorithms into their SSL certificates.
ECC was independently suggested by Neal Koblitz (University of Washington) and
Victor S. Miller (IBM) in 1985. After the introduction of Diffie-Hellman and RSA,
cryptographers started exploring other mathematics-based cryptographic solutions
looking for other algorithms that would offer easy one-way calculations that were hard
to find an inverse for; these types of functions are referred to as trapdoors. A trapdoor
function is a function that is easy to perform one way but has a secret that is required to
perform the inverse calculation efficiently. That is, if f is a trapdoor function, then y =
f(x) is easy to compute, but x = f − 1(y) is hard to compute without some special
knowledge k. Unless you have a mathematical background, elliptic curves may be new
to you; so what exactly is an elliptic curve and how does the elliptic curve trapdoor
function work?
An elliptic curve is the set of points that satisfy a specific mathematical equation. The
equation for an elliptic curve looks something like this:
141
That graphs to something that looks like Figure.
The most important takeaway for this section is that you understand that ECC produces
encryption keys based on using points on a curve to define the public and private keys.
An ECC key is very helpful for the current generation
as more people are moving to the smartphone. As the utilization of smartphones
continues to grow, there is an emerging need for a more flexible encryption for business
to meet with increasing security requirements.
The elliptic curve cryptography certificates allow key size to remain small while
providing a higher level of security. The ECC certificate key creation method is entirely
different from previous algorithms, while relying on the use of a public key for
encryption and a private key for decryption. By starting small and with a slow growth
potential, ECC has a longer potential life span. Elliptic curves are likely to be the next
generation of cryptographic algorithms, and we are seeing the beginning of their use
now.
When you compare ECC with other algorithms like RSA, you will find the ECC key is
significantly smaller yet offers the same level of security. One notable instance is that
a 3,072-bit RSA key takes 768 bytes, whereas the equally strong NIST P-256 private
key only takes 32 bytes (that is, 256 bits). PyCryptodome offers us an ECC module that
provides mechanisms for generating new ECC keys, exporting and importing them
using widely supported formats like PEM or DER.
ECC private keys are integers that represent the curve’s field size; the typical size is
256 bits. A 256-bit private key would look like the following:
0x51897b64e85c3f714bba707e867914295a1377a7463a9dae8ea6a8b914246319
Generating an ECC key requires generating a random integer within a spec ified
range.
The public keys in the ECC are EC points—pairs of integer coordinates {x, y}, lying
on the curve. Due to their special properties, EC points can be compressed to just one
coordinate + 1 bit (odd or even). Thus the compressed public key, corresponding to a
256-bit ECC private key, is a 257-bit integer. An example of an ECC public key
(corresponding to the preceding private key, encoded in the Ethereum format, as hex
with prefix 02 or 03) is
0x02f54ba86dc1ccb5bed0224d23f01ed87e4a443c47fc690d7797a13d41d2340e1.
In this format, the public key takes 33 bytes (66 hex digits), which can be optimized to
142
exactly 257 bits.
The following example demonstrates how to generate a new ECC key, export it, and
reload it back into your program. The code uses the NIST P-256 algorithm, which is the
most-used elliptic curve, and there are no reasons to believe it’s Insecure:
EccKey(curve='NIST P-256',
point_x=85511317925193091591538005554467283386311991513737248626228047
21 0045098335773,
point_y=62834027958545080347454491553206133116570260879291164814224928
59 9382610602892,
d=2063641786698337143130043788498991558397573514836970341582277689437
768 1606808)
Digital Signatures
Objective:
This lab will enable students to understand Digital signature generation process, along
with different types cryptographic mechanisms used in Digital signatures.
Activity Outcomes:
Instructor Note:
As pre-lab activity, read Chapter 13, from the book (Cryptography and Network Securi
Principles and Practic Eighth Editi Global Editi William Stallings) to know the basics
of Digital signatures
144
Practice -Lab Activity 1:
Lamport One Time Signature is a method for constructing a digital signature and
typically involved the use of a cryptographic hash function. As it is a one-time signature
scheme, it can only be used to securely sign one message.
Suppose Alice wants to digitally sign her message to Bob, the process can be explained
in 3 steps:
1. Key Generation
2. Signature Generation
3. Signature Verification.
1. Key Generation:
Alice first needs to create a Lamport key pair, a private key and a corresponding public
key.
In order to create the private key, a secure random number generator is used to
generate 256 pairs of random numbers. Each number consists of 256 bits. Alice will
store this private key securely. Remember that the private key is not meant to be shared
with anyone.
In order to create the public key, Alice hashes each of the 512 numbers of her private
key. This will produce another 512 numbers, each consisting of 256 bits. This is the
public key that will be shared with anyone.
2. Signature Generation:
Alice hashes her message using a 256-bit cryptographic hash function, eg SHA 256, to
obtain a 256-bit digest.
For each bit, depending on whether the bit value is 1 or 0, Alice will pick the
corresponding number from the pair of numbers of her private key i.e. if the bit is 0, the
first number is chosen, and if the bit is 1, the second number is chosen. This results in
a sequence of 256 numbers which is her signature.
145
Alice sends the message along with her signature to Bob.
3. Signature Verification:
Bob hashes the message using the same 256-bit cryptographic hash function, to obtain
a 256-bit digest.
For each bit, depending on whether the bit value is 1 or 0, Bob will pick the
corresponding number from Alice’s public key i.e if the first bit of the message hash is
0, he picks the first hash in the first pair, and so on. This is done in the same manner as
shown in the diagram above. This results in a sequence of 256 numbers.
Bob hashes each of the numbers in Alice’s signature to obtain a 256-bit digest. If this
matches the sequence of 256 numbers that Bob had previously picked out, the signature
is valid.
import hashlib
import secrets
# Creation of keys
def keygen():
skey = [[0] * 255, [1] * 255]
for i in range(len(skey)):
for j in range(len(skey[i])):
skey[i][j] = bin(secrets.randbits(255))[2:]
skey[i][j] = '0' * (255 - len(skey[i][j])) + skey[i][j]
for i in range(255):
k = (mhash >> i) & 1 # Directly extract the bit
signature[i] = skey[k][i]
return signature
# Verification of signature
def verification(message, pkey, signature):
mhash = int(hashlib.sha256(message.encode()).hexdigest(), 16)
for i in range(255):
k = (mhash >> i) & 1 # Directly extract the bit
146
verify = hashlib.sha256(signature[i].encode()).hexdigest() # Get hash of the
signature
if pkey[k][i] != verify:
return False
return True
# Example usage
keypair = keygen()
message = "I am god."
signature = signgen(message, keypair[0])
print("Signature valid:", verification(message, keypair[1], signature))
147
Practice -Lab Activity 2:
M = Message or Plaintext
H = Hash Function
|| = bundle the plantext and hash function (hash digest)
E = Encryption Algorithm
D = Decryption Algorithm
PUa = Public key of sender
PRa = Private key of sender
Sig = Signature function
Ver = Verification function
PUG = Global public Key
DSA Approach
Primary Termologies
User’s Private Key (PR): This key is publicly known and can be shared with anyone.
It’s used to verify digital signatures created with a corresponding private key.
User’s Public Key (PU): A top-secret cryptographic key only possessed by the user is
used in DSA algorithm’s digital signature generation. As it is, the private key must be
kept secret and secure because it proves that a given user is genuine.
Signing (Sig): Signing involves creating a digital signature with the help of a user’s
private key. In case of DSA, this process requires mathematical operations to be
performed on the message that should be signed using a given private key in order to
generate a unique signature for that message.
148
Verifying (Ver): Verifying is the process of verifying whether or not a digital signature
has been forged using its corresponding public key. In DSA, this involves comparing
the messages hash against the verification value through mathematical operations
between two binary strings – one representing an encrypted data and another one
representing plain-text original message.
Steps to Perform DSA
The Digital Signature Algorithm (DSA) is a public-key technique (i.e., assymetric
cryptography) and it is used to provide only the digital signature function, and it cannot
be used for encryption or key exchange.
1. Global Public-Key Components
There are three parameters that are public and can be shared to a set of users.
A prime number p is chosen with a length between 512 and 1024 bits such that q divides
(p – 1). So, p is prime number where 2L-1 < p <2L for 512<= L<=1024 and L is a
multiple of 64; i.e., bit length of between 512 and 1024 bits in increments of 64 bits.
Next, an N-bit prime number q is selected. So, q is prime divisor of (p – 1), where 2N-
1 < q < 2N i.e., bit length of N bits.
Finally, g is selected to be of the form h(p-1)/q mod p, where h is an integer between 1
and (p – 1) with the limitation that g must be greater than 1. So, g is = h(p – 1)/q mod
p, where h is any integer with 1 < h < (p – 1) such that h(p-1)/q mod p > 1.
If a user has these numbers, then it can selectsprivate key and generates a public key.
2. User’s Private Key
The private key x should be chosen randomly or pseudorandomly and it must be a
number from 1 to (q – 1), so x is random or pseudorandom integer with 0 < x < q.
3. User’s Public Key
The public key is computed from the private key as y = gx mod p. The computation of
y given x is simple. But, given the public key y, it is believed to be computationally
infeasible to choose x, which is the discrete logarithm of y to the base g, mod p.
4. Signing
If a user want to develop a signature, a user needs to calculates two quantities, r and s,
that are functions of the public key components (p, q, g), the hash code of the message
H(M, the user’s private key (x), and an integer k that must be generated randomly or
pseudorandomly and be unique for each signing. k is generated randomly or
pseudorandomly integer such that 0<k < q.
Signing
5. Verification
Let M, r′, and s′ be the received versions of M, r, and s, respectively.
149
Verification is performed using the formulas shown in below:
w = (s′)-1 mod q
u1 = [H(M′)w] mod q
u2 = (r′)w mod q
v = [(gu1 yu2) mod p] mod q
The receiver needs to generate a quantity v that is a function of the public key
components, the sender’s public key, and the hash code of the message. If this value
matches the r value of the signature, then the signature is considered as valid.
TEST: v = r′
Verification
Now, at the end it will test on the value r, and it does not depend on the message or
plaintext as, r is the function of k and the three global public-key components as
mentioned above. The multiplicative inverse of k (mod q) when passed to the function
that also has as inputs the message hash code and the user’s private key. The structure
of this function is such that the receiver can recover r using the incoming message and
signature, the public key of the user, and the global public key.
It is given that there is difficulty in taking discrete logarithms, it is not feasible for an
attacker to recover k from r or to recover x from s. The only computationally demanding
task in signature generation is the exponential calculation gk mod p. Because this value
does not depend on the message to be signed, it can be computed ahead of time. Indeed,
a user could precalculate a number of values of r to be used to sign documents as needed.
The only other somewhat demanding task is the determination of a multiplicative
inverse, k-1 .
Key generation
from miller import *
from fractions import gcd
def loopIsPrime(number):
#looping to reduce probability of rabin miller false +
isNumberPrime = True
for i in range(0,20):
isNumberPrime*=isPrime(number)
if(isNumberPrime == False):
return isNumberPrime
return isNumberPrime
def modexp( base, exp, modulus ):
return pow(base, exp, modulus)
150
def squareAndMultiply(x,c,n):
z=1
#getting value of l by converting c into binary representation and getting its
length
c="{0:b}".format(c)[::-1] #reversing the binary string
l=len(c)
for i in range(l-1,-1,-1):
z=pow(z,2)
z=z%n
if(c[i] == '1'):
z=(z*x)%n
return z
def keyGeneration():
t = random.randint(1,p-1)
g = squareAndMultiply(t, (p-1)//q, p)
a = random.randint(2,q-1)
h = squareAndMultiply(g,a,p)
#print("p = ",p)
#print("q = ",q)
#print("g = ",g)
#print("h = ",h)
#print("a = ",a)
file1 = open("key.txt","w")
151
file1.write(str(p))
file1.write("\n")
file1.write(str(q))
file1.write("\n")
file1.write(str(g))
file1.write("\n")
file1.write(str(h))
file1.close()
file2 = open("secretkey.txt","w")
file2.write(str(a))
file2.close()
import random
def rabinMiller(num):
# Returns True if num is a prime number.
s = num - 1
t=0
while s % 2 == 0:
# keep halving s while it is even (and use t
# to count how many times we halve s)
s = s // 2
t += 1
152
def isPrime(num):
lowPrimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61,
67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157,
163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251,
257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353,
359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457,
461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571,
577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673,
677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797,
809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911,
919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]
if num in lowPrimes:
return True
def generateLargePrime(keysize):
# Return a random prime number of keysize bits in size.
while True:
num = random.randrange(2**(keysize-1), 2**(keysize))
if isPrime(num):
return num
while r > 0 :
temp = (tL[0] - (q*bL[0]))
tL[0] = t
t = temp
temp = (sL[0] - (q*s))
sL[0] = s
s = temp
aL[0] = bL[0]
bL[0] = r
q = math.floor(aL[0]/bL[0])
r = (aL[0] - (q*bL[0]))
r = bL[0]
inverse = s % in2
return inverse
def squareAndMultiply(x,c,n):
z=1
#getting value of l by converting c into binary representation and getting its
length
c="{0:b}".format(c)[::-1] #reversing the binary string
l=len(c)
for i in range(l-1,-1,-1):
z=pow(z,2)
z=z%n
if(c[i] == '1'):
z=(z*x)%n
return z
def shaHash(fileName):
BLOCKSIZE = 65536
hasher = hashlib.sha1()
with open(fileName, 'rb') as afile:
buf = afile.read(BLOCKSIZE)
while len(buf) > 0:
hasher.update(buf)
buf = afile.read(BLOCKSIZE)
#print(hasher.hexdigest())
hex = "0x"+hasher.hexdigest()
154
#print(int(hex,0))
return int(hex,0) #returns int value of hash
def sign():
if(len(sys.argv) < 2):
print("Format: python sign.py filename")
elif(len(sys.argv) == 2):
print("Signing the file...")
fileName = sys.argv[1]
file1 = open("key.txt","r")
file2 = open("secretkey.txt","r")
p=int(file1.readline().rstrip())
q=int(file1.readline().rstrip())
g=int(file1.readline().rstrip())
h=int(file1.readline().rstrip())
a=int(file2.readline().rstrip())
loop = True
while loop:
r = random.randint(1,q-1)
c1 = squareAndMultiply(g,r,p)
c1 = c1%q
c2 = shaHash(fileName) + (a*c1)
Rinverse = computeInverse(r,q)
c2 = (c2*Rinverse)%q
#print(shaHash(fileName))
#print(c1)
#print(c2)
file = open("signature.txt","w")
file.write(str(c1))
file.write("\n")
file.write(str(c2))
print("cipher stored at signature.txt")
sign()
while r > 0 :
temp = (tL[0] - (q*bL[0]))
tL[0] = t
t = temp
temp = (sL[0] - (q*s))
sL[0] = s
s = temp
aL[0] = bL[0]
bL[0] = r
q = math.floor(aL[0]/bL[0])
r = (aL[0] - (q*bL[0]))
r = bL[0]
inverse = s % in2
return inverse
def squareAndMultiply(x,c,n):
z=1
#getting value of l by converting c into binary representation and getting its
length
c="{0:b}".format(c)[::-1] #reversing the binary string
l=len(c)
for i in range(l-1,-1,-1):
z=pow(z,2)
z=z%n
if(c[i] == '1'):
z=(z*x)%n
return z
def shaHash(fileName):
BLOCKSIZE = 65536
hasher = hashlib.sha1()
with open(fileName, 'rb') as afile:
buf = afile.read(BLOCKSIZE)
while len(buf) > 0:
hasher.update(buf)
buf = afile.read(BLOCKSIZE)
#print(hasher.hexdigest())
hex = "0x"+hasher.hexdigest()
156
#print(int(hex,0))
return int(hex,0) #returns int value of hash
def verification():
if(len(sys.argv) < 2):
print("Format: python sign.py filename")
elif(len(sys.argv) == 2):
print("Checking the signature...")
fileName = sys.argv[1]
file1 = open("key.txt","r")
file2 = open("signature.txt","r")
p=int(file1.readline().rstrip())
q=int(file1.readline().rstrip())
g=int(file1.readline().rstrip())
h=int(file1.readline().rstrip())
c1=int(file2.readline().rstrip())
c2=int(file2.readline().rstrip())
#print(c1)
#print(c2)
t1=shaHash(fileName)
#print(t1)
inverseC2 = computeInverse(c2,q)
t1 = (t1*inverseC2)%q
t2 = computeInverse(c2,q)
t2 = (t2*c1)%q
valid1 = squareAndMultiply(g,t1,p)
valid2 = squareAndMultiply(h,t2,p)
valid = ((valid1*valid2)%p)%q
#print(valid)
if(valid == c1):
print("Valid signature")
else:
print("Invalid signature")
verification()
The error message you're seeing indicates that Python is unable to find a module
157
named miller that you're trying to import in your script (sgnature.py). Here are some
steps you can follow to resolve this issue:
1. Check for Typographical Errors: Make sure that you have spelled the module name
correctly in your import statement.
2. Install the Module: If miller is a third-party library, you may need to install it. You
can install it using pip. Open your command prompt or terminal and run:
(Note: Replace miller with the actual package name if it's different.)
3. Check Your Python Environment: Ensure that you are using the correct Python
environment where the module is installed. You can check your current environment
by running:
.\venv\Scripts\activate
o On macOS/Linux:
source venv/bin/activate
6. Updating Python Path: If you have the miller.py file in a different directory, you can
add that directory to your Python path in your script:
import sys
sys.path.append('path_to_directory')
from miller import *
7. Check for Compatibility: If miller is a package that requires a specific version of
Python, make sure you're using a compatible version.
158
Lab 15
Public-Key Certificates (PKC)
Objective:
This lab will enable students to utilize their knowledge of Public-Key Certificates and
will enable students to generate their own digital certificates in python.
Activity Outcomes:
This lab teaches you the following topics:
• Gain an understanding of the importance of PKI
• Learn how to implement a PKI solution in Python
Instructor Note:
As pre-lab activity, read Chapter 13, page 418-435 from the book (Cryptography and
Network Securi Principles and Practic Eighth Editi Global Editi William Stallings).
Practice -Lab Activity 1:
Public-Key Certificates
Public-key certificates essentially act as a passport that certifies that a public-key
belongs to a specific name or organization. Certificates are issued by certificate
authorities, more commonly known as CAs. One of the properties of using public-key
certificates is that they allow all users to know without question that the public-key of
the CA can be checked by each user. In addition, certifi- cates do not require the online
participation of a TTP. One thing that you must remember is that the security of the
private key is crucial to the security of all users. The following represents the notation
of a certificate binding a public key
+KA to user A issued by a certificate authority CA using its private key -CKCA:
Cert-CKCA(+KA) = CA[V, SN, AI, CA, TCA, A, +KA] where:
V = rsion number SN = serial number
AI = algorithm identifier of signature algorithm used CA = name of certification
authority
TCA = period of validity of this certificate
A = name to which the public key in this certificate is bound
+KA = public to be bound to a name
Certificate Revocation
If we continue to examine the situation presented, you can see that communications
between Alice and Bob rely on the trust of the certificate authority and each party must
keep their private key secure. Should one of their keys become compromised, the
certificate needs to be nullified or revoked. If Alice’s key was compromised in an attack,
the attacker (Trent) can continue to impersonate Alice up to the end of the certificate’s
validity period. If Alice detects the com- promise, she can ask for revocation of the
corresponding public-key certificate. Certificate revocation is performed by
maintaining a list of compromised certificates; these lists are known as certificate
revocation lists, or CRLs. CRLs are stored in the X.500 directory; when a user or
process is checking a certificate, it must not only confirm that the certificate exists but
also make sure the certificate is not on a CRL. The certificate revocation process is quite
slow and can be costly and ineffective.
If you’ve used the previous example to generate a key, you will be able to load it
using the following code. Examine the use of load_pem_private_key(), as shown here:
# Generate CSR
csr = x509.CertificateSigningRequestBuilder().subject_name(
x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"CA"),
x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Python
Cryptography"),
x509.NameAttribute(NameOID.COMMON_NAME, u"8gwifi.org"),
])
).add_extension(
x509.SubjectAlternativeName([
x509.DNSName(u"mysite.com"),
]),
critical=False
).sign(private_key, hashes.SHA256(), default_backend())