Instituto Superior Técnico, Universidade de Lisboa
Network and Computer Security
Cryptography is more complex than just using certain functions; it is a field where details matter greatly. Selecting the right algorithm and key size, and correctly applying encryption modes, are crucial for real security. Small mistakes in these areas can lead to vulnerabilities.
This lab guide emphasizes understanding security vulnerabilities and best practices in cryptographic implementations. It starts with a quick review of the Java Cryptography Architecture (JCA), covering essential cryptographic concepts like secure random number generation, key management, and encryption techniques. Through exercises, it explores symmetric and asymmetric encryption, block cipher modes (including ECB, CBC, and OFB), digital signatures with OpenSSL, and a file tampering exercise.
The goals are:
- Correctly use the cryptographic mechanisms available in the Java platform;
- Perform attacks that exploit vulnerabilities introduced by the bad use of cryptography.
The Java platform strongly emphasizes security, including language safety, cryptography, public key infrastructure, secure communication, authentication, and access control.
The Java Cryptography Architecture (JCA), which is a major piece of the Java platform, includes a large set of application programming interfaces (APIs), tools, and implementations of commonly-used security algorithms, mechanisms, and protocols.
It provides a comprehensive security framework for writing applications and also provides a set of tools to securely manage applications.
The JCA APIs include abstractions for secure random number generation, key generation and management, certificates and certificate validation, encryption (symmetric/asymmetric block/stream ciphers), message digests (hashes), and digital signatures.
Some examples are the MessageDigest, Signature, KeyFactory, KeyPairGenerator, and Cipher classes.
Implementation independence, in the Java platform, is achieved using a provider-based architecture.
The term Cryptographic Service Provider (CSP) refers to a package or set of packages that implement one or more cryptographic services, such as digital signature algorithms, message digest algorithms, and key conversion services.
A program may simply request an object, e.g., a MessageDigest object, implementing a particular service, e.g., the SHA-256 digest algorithm, and get an implementation from one of the installed providers.
A program may instead request, if necessary, an implementation from a specific provider.
To obtain a security service from an underlying provider, applications rely on the relevant getInstance() method.
The message digest creation, for example, represents one type of service available from providers.
To obtain an implementation of a specific message digest algorithm, an application invokes the getInstance() method in the java.security.MessageDigest class.
MessageDigest md = MessageDigest.getInstance("SHA-256");
Optionally, by indicating the provider name, the program may request an implementation from a specific provider as in the following:
MessageDigest md = MessageDigest.getInstance("SHA-256", "MyProvider");
Providers may be updated transparently to the application when faster or more secure versions are available.
In the Java platform, the java.security.Provider class is the base class for all security providers.
Each CSP contains an instance of this class which contains the provider's name and lists all the security services/algorithms it implements.
Multiple providers may be configured at the same time and are listed in order of preference.
The highest priority provider that implements that service is selected when a security service is requested.
For more information, please read the Java Cryptography Architecture (JCA) Reference Guide.
This laboratory assignment uses Java Development Kit (JDK) version 11 or later, running on Linux.
It is recommended that you follow this assignment using the virtual machine from previous classes.
First, check if you have the Java compiler and Maven available.
If not, install Maven and the Java compiler:
sudo apt update
sudo apt install maven
sudo apt install openjdk-11-jdk
To try the cryptographic mechanisms, the Java code needs to be compiled and executed.
Put the lab files in a working folder with write permissions, like /tmp/crypto, for example, and change your working directory to it:
cd /tmp/crypto
You can compile the code using Maven. To do so:
mvn clean compile
To compile everything and generate convenient scripts for running each class:
mvn install
The commands include:
aes-key-gen, a utility for generating AES keys;base64-decode, decodes Base64 encoded data;base64-encode, for encoding data into Base64 format;file-aes-cipher, applies AES Cipher to files;file-aes-decipher, used for deciphering files encrypted with AES;image-aes-cipher, encrypts images using AES Cipher;image-xor, applies the XOR operation on images;random-image-gen, a tool for creating random images; andrsa-key-gen, which generates RSA keys.
These commands are generated in target/appassembler/bin.
Let us add this folder to the current PATH environment variable, so we can call the commands in any directory.
The following commands find the absolute path of the work directory, export the variable for the current session, and test one of the commands:
pwd
export PATH=$PATH:/tmp/crypto/target/appassembler/bin
image-xor
Adjust /tmp/crypto to the directory you are actually using.
The image-xor command should be recognized and print information about the missing parameters.
The following commands are the Windows CMD equivalent:
cd
set PATH=%PATH%;C:\tmp\crypto\target\appassembler\bin
image-xor
The cryptographic operations will be applied to image files, so that its results can be "seen".
In the directory intro/inputs, you can find three different images:
- Técnico: *.png, the IST logo;
- Tux: *.png, Tux, the Linux penguin;
- Glider: *.png, the hacker emblem (http://www.catb.org/hacker-emblem/).
Each one is presented with three different dimensions: 480x480, 960x960, and 2400x2400.
The resolution number is part of the file name.
The ImageMixer class is available to facilitate the operations on images.
Different code examples are available, such as the RandomImageGenerator, ImageXor, and ImageAESCipher classes.
If they could be correctly used in practice, one-time pads would provide perfect security. One of the constraints to make them work as expected is that the key stream must never be reused. The following steps visually illustrate what happens if they are reused, even if just once:
Generate a new 480x480 random image:
random-image-gen intro/outputs/otp.png 480 480
Perform the bitwise eXclusive OR operation (XOR) with the generated key:
image-xor intro/inputs/tecnico-0480.png intro/outputs/otp.png intro/outputs/encrypted-tecnico.png
XOR tux-0480.png with the same generated key:
image-xor intro/inputs/tux-0480.png intro/outputs/otp.png intro/outputs/encrypted-tux.png
Watch the images encrypted-tecnico.png and encrypted-tux.png.
Switch between them and see the differences.
To make the differences obvious, XOR them together:
image-xor intro/outputs/encrypted-tecnico.png intro/outputs/encrypted-tux.png intro/outputs/tecnico-tux.png
You can see that the reuse of a one-time pad (or any stream cipher key at all) considerably weakens (or completely breaks) the security of the information. The reason is the following:
C1 = M1 ⊕ K
C2 = M2 ⊕ K
C1 ⊕ C2 = M1 ⊕ M2
Legend: C stands for cipher-text, M for plain-text, K for key, ⊕ for XOR
The result you get is the XOR of the images. You can experiment with other images and sizes.
Now that you know that keys should never be reused, remember that the way you use them is also important.
We will use a symmetric-key encryption algorithm working in blocks to encrypt the pixels from an image. We will use different modes, namely: ECB (Electronic Code Book), CBC (Cipher Block Chaining) and OFB (Output FeedBack).
In the ECB mode, each block is independently encrypted with the key:
C[i] = E_k(M[i])
Visual representation of ECB mode
Begin by generating a new AES Key:
aes-key-gen w intro/outputs/aes.key
Then, encrypt the glider image with it:
image-aes-cipher intro/inputs/glider-0480.png intro/outputs/aes.key ECB intro/outputs/glider-aes-ecb.png
Watch the output image.
Remember what you have just done: encrypted the image with AES, using ECB mode, and a key you generated yourself.
Try the same thing with the other images (especially with other sizes).
NOTE: The observable visual encryption pattern emitted by ECB mode can be observered independently of the chosen security provider and the generated AES key as it is a consequence of its operational design.
In CBC mode, each block M[i] is XORed with the ciphertext from the previous block, and then encrypted with key k:
C[i] = E_k (M[i] ⊕ C[i-1])
Visual representation of CBC mode
The encryption of the first block can be performed by means of a random and unique value known as the Initialization Vector (IV).
The AES key will be the same from the previous step.
Encrypt the glider image with it, this time replacing ECB with CBC:
image-aes-cipher intro/inputs/glider-0480.png intro/outputs/aes.key CBC intro/outputs/glider-aes-cbc.png
Watch the file glider-aes-cbc.png.
See the difference made by changing only the mode of operation.
Still in the CBC mode, you may have wondered why the IV is needed in the first block. Consider what happens when you encrypt two different images with similar beginnings, and with the same key: the initial cipher text blocks would also be similar!
The ImageAESCipher class provided has been deliberately weakened: instead of randomizing the IV, it is always the same.
This time, encrypt the other two images with AES/CBC, still using the same AES key:
image-aes-cipher intro/inputs/tux-0480.png intro/outputs/aes.key CBC intro/outputs/tux-aes-cbc.png
image-aes-cipher intro/inputs/tecnico-0480.png intro/outputs/aes.key CBC intro/outputs/tecnico-aes-cbc.png
Now watch the images glider-aes-cbc.png, tux-aes-cbc.png, and tecnico-aes-cbc.png.
Look to the first lines of pixels.
Can you see what is going on?
In the OFB mode, the IV is encrypted with the key to make a keystream that is then XORed with the plaintext to make the cipher text.
Visual representation of OFB mode
In practice, the keystream of the OFB mode can be seen as the one-time pad that is used to encrypt a message. This implies that in OFB mode, if the key and the IV are both reused, there is no security.
Encrypt the images with OFB:
image-aes-cipher intro/inputs/glider-0480.png intro/outputs/aes.key OFB intro/outputs/glider-aes-ofb.png
image-aes-cipher intro/inputs/tux-0480.png intro/outputs/aes.key OFB intro/outputs/tux-aes-ofb.png
image-aes-cipher intro/inputs/tecnico-0480.png intro/outputs/aes.key OFB intro/outputs/tecnico-aes-ofb.png
Remember that the ImageAESCipher implementation has been weakened, by having a null IV, and you are reusing the same AES key.
Watch the generated images and switch quickly between them.
Take two images (e.g., image1 and image2) and cipher them both.
XOR ciphered image1 with ciphered image2.
What did you obtain?
Why?
What is more secure to use: CBC or OFB?
The goal now is to use asymmetric ciphers, with separate private and public keys. RSA is the most well known of these algorithms.
Generate the key pair for the server:
openssl genrsa -out server.key
And for the user:
openssl genrsa -out user.key
Create a Certificate Signing Request for the server, using its key (you can leave the parameters empty):
openssl req -new -key server.key -out server.csr
Create a configuration file for the certificate:
echo "[v3_ca]\nbasicConstraints = CA:TRUE" > server_cert_config.cnf
Self-sign:
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt -extfile server_cert_config.cnf -extensions v3_ca
NOTE: Alternatively, since our configuration is very simple, we could immediately generate the certificate, skipping the creation of the CSR and the configuration file:
openssl req -x509 -days 365 -key server.key -out server.crt -addext "basicConstraints=CA:TRUE"
Generate a Certificate Signing Request for the user, using its key:
openssl req -new -key user.key -out user.csr
And sign it with the server's private key:
openssl x509 -req -days 365 -in user.csr -CA server.crt -CAkey server.key -out user.crt
Verify the user's certificate:
openssl verify -CAfile server.crt user.crt
user.crt: OK
Create a digest of the grades.txt file:
openssl dgst -sha256 -binary grades/inputs/grades.txt > grades/outputs/grades.sha256
Sign the digest with the user's private key:
openssl pkeyutl -sign -inkey user.key -in grades/outputs/grades.sha256 -out grades/outputs/grades.sig
Verify the signature with the user's public key:
openssl pkeyutl -verify -inkey user.crt -certin -sigfile grades/outputs/grades.sig -in grades/outputs/grades.sha256
Signature Verified Successfully
Try modifying the grades.sha256 file and rerunning the verification to see what happens.
To read the generated keys in Java it is necessary to convert them to the right format.
Convert server.key to the .pem format:
openssl rsa -in server.key -text > private_key.pem
Convert private Key to PKCS#8 format (so the Java library can read it):
openssl pkcs8 -topk8 -inform PEM -outform DER -in private_key.pem -out private_key.der -nocrypt
Output public key portion in .der format (so Java can read it):
openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der
Read the key files using the following command:
rsa-key-gen r private_key.der public_key.der
Generate a new pair of RSA Keys.
rsa-key-gen w intro/outputs/priv.key intro/outputs/pub.key
Based on the ImageAESCipher class create ImageRSACipher and ImageRSADecipher classes.
Encrypt the image with the public key and then decrypt it with the private key. Try the same thing with the other images - especially with other sizes.
Please consider that the RSA cipher, as implemented by Java, can only be applied to one block of data at a time, and its size depends on the size of the key. For a 1024-bit key, the block size is 117 bytes or 60 bytes, depending on the padding options. This is acceptable because the RSA cipher is mostly used to cipher keys or hashes that are small. For large data, hybrid cipher is most suited, combining RSA with AES, for example. For this exercise you can cipher one block at a time.
In the directory grades/inputs, you can find the file grades.txt, the plaintext of a file with the grades of a course.
This flat-file database has a rigid structure: 64 bytes for name, and 16 bytes for each of the other fields, number, age and grade.
Unfortunately, you happen to be Mr. Thomas S. Cook, and your grade was not on par with the rest of your class because you studied for a different exam...
Begin by encrypting this file into the ecb.aes file.
For this example, we will still reuse the AES key generated above and ECB mode.
file-aes-cipher grades/inputs/grades.txt intro/outputs/aes.key ECB grades/outputs/grades.ecb.aes
Keeping in mind how the mode operations work, and without using the secret key, try to change your grade to 21 in the encrypted files or give everyone in class a 20.
(Why 21 or all 20s? Because you are an ethical hacker using your skills to show that the system is vulnerable, not perform actual cheating.)
- For this exercise, it may be useful to inspect the file with a hex editor, such as
hexedit(GUI),hexdumporxxd.
Did you succeed? Did your changes have side effects?
Now try to attack cbc.aes and ofb.aes.
For this example, we will still reuse the AES key generated above but use the CBC and OFB modes.
file-aes-cipher grades/inputs/grades.txt intro/outputs/aes.key CBC grades/outputs/grades.cbc.aes
file-aes-cipher grades/inputs/grades.txt intro/outputs/aes.key OFB grades/outputs/grades.ofb.aes
How do you compare the results with ECB?
Since the inputs and outputs of cryptographic mechanisms are byte arrays, in many occasions it is necessary to represent encrypted data in text files.
A possibility is to use Base 64 Encoding that, for every binary sequence of 6 bits, assigns a predefined ASCII character.
Execute the following to create a Base 64 representation of files previously generated.
base64-encode grades/outputs/grades.cbc.aes grades/outputs/grades.cbc.aes.b64
Decode them:
base64-decode grades/outputs/grades.cbc.aes.b64 grades/outputs/grades.cbc.aes.b64.decoded
Check if they are similar using the cmp command (or fc /b command on Windows):
cmp grades/outputs/grades.cbc.aes grades/outputs/grades.cbc.aes.b64.decoded
It should not return anything.
Check the difference on the file sizes between grades/outputs/grades.cbc.aes and grades/outputs/grades.cbc.aes.b64.
Can you explain it?
In percentage, how much is it?
Does Base 64 provide any kind of security?
If so, how?
Use Java to generate the message authentication code (MAC) and digital signature of the grades file. By performing these operations, which security requirements can be guaranteed?
This lab guide provided further exploration of cryptographic and data processing tools in Java, including:
one-time pads (symmetric stream cipher), block cipher modes (including ECB, CBC, and OFB), and asymmetric ciphers (focusing on RSA).
Additionally, it included a practical exercise on file tampering, demonstrating how encryption alone may not guarantee data integrity.