Cryptography in Java
Java cryptographic engines provide mechanisms for digital signatures, message digests, etc. These engines are implemented by security providers (java.security.Provider
), which can be viewed using java.security.Security.getProviders()
. Each engine (java.security.Provider.Service
) has a type and an algorithm.
Keys
We can generate keys of symmetric type (KeyGenerator
) or asymmetric type (KeyPairGenerator
).
KeyGenerator keyGen = KeyGenerator.getInstance(algorithm);
keyGen.init(size);
SecretKey secretKey = keyGen.generateKey();
Typical symmetric algorithms are DES (56-bit) or AES (128, 192, 256 bits).
KeyPairGenerator kpg = KeyPairGenerator.getInstance(algorithm);
kpg.initialize(size);
KeyPair kp = kpg.generateKeyPair();
PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate();
The algorithm must be indicated. The most common is RSA (1024, 2048 bits).
Both SecretKey, PublicKey and PrivateKey are subclasses of java.security.Key
. All of them have a getEncoded() method: the key in binary format.
Encryption
To be able to encrypt, we need an object javax.crypto.Cipher
:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
The format of the parameter (transformation) is algorithm/mode/padding. Mode and padding are optional: if not specified, the default mode and padding are used. Which are?
- Padding: is a technique that consists of adding padding data to the beginning, middle or end of a message before being encrypted. This is done because the algorithms are designed to have input data of a particular size.
- Mode: Defines how the input (plain) and output (encrypted) blocks are mapped. The simplest is the ECB mode: an input block goes to an output block. Other, more secure modes randomize this correspondence.
Then, we need to initialize the object using the mode (Cipher.ENCRYPT_MODE
or Cipher.DECRYPT_MODE
) and the encryption key:
cipher.init(Cipher.ENCRYPT_MODE, key); // encryption
cipher.init(Cipher.DECRYPT_MODE, key); // decryption
Finally, we perform the encryption or decryption:
byte[] bytesOriginal = textOriginal.getBytes("UTF-8"); // I need bytes as input
byte[] bytesEncrypted = cipher.doFinal(bytesOriginal);
The decryption could be:
byte[] bytesDeciphered = cipher.doFinal(bytesDeciphered);
// alternatively, if the content to be decrypted is part of the array:
byte[] bytesDeciphered = cipher.doFinal(bytesDeciphered, start, length);
Some block cipher modes use what is called an initialization vector (IV). This is a random parameter for the encryption algorithm that makes it harder to match blocks based on their inputs. It usually doesn't need to be secret, just not repeated with the same key.
To use these modes (eg CBC), a new parameter must be added when the Cipher is initialized. The size of the IV is usually the same as the block. For AES it is 16 bytes (256 bits).
byte[] iv;
// initialize IV with a random generator like SecureRandom
IvParameterSpec parameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); // encryption
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec); // decryption
Encryption of streams
It makes no sense to encrypt large amounts of data with block methods. For these situations, we can use stream encryption. This cipher is always symmetric.
In Java, we have the classes CipherInputStream and CipherOutputStream.
CipherInputStream and CipherOutputStream support a block symmetric Cipher, such as AES/ECB/PKCS5Padding, or feedback type modes CFB8 or OFB8, (8 = 8-bit blocks), such as AES/CFB8/NoPadding. Feedback modes need initialization vectors (IV).
For example, if we want to open a file and encrypt or decrypt it, we can do it like this:
FileInputStream in = new FileInputStream(inputFilename);
FileOutputStream fileOut = new FileOutputStream(outputFilename);
CipherOutputStream out = new CipherOutputStream(fileOut, cipher);
Then, it would be necessary to copy the stream in to out.
The cipher object must be initialized with the required mode, ENCRYPT_MODE or DECRYPT_MODE.
CipherInputStream can be used analogously. In this case, the output stream could be a FileOutputStream:
FileInputStream fileIn = new FileInputStream(inputFilename);
CipherInputStream in = new CipherInputStream(fileIn, cipher);
FileOutputStream fileOut = new FileOutputStream(outputFilename);
Binary data in text
The keys and encrypted information are in binary format. If it needs to be exchanged using a text channel, they can be converted using Base64. In Java, we have java.util.Base64
.
byte[] binary1 = ...;
String string = Base64.getEncoder().encodeToString(binary1);
byte[] binary2 = Base64.getDecoder().decode(string);
// binary1 and binary2 are equal