Friday, May 6, 2022

Java Tutorial: Cryptography

Chapters

Introduction

In computer programming, Cryptography is a way of hiding data(passwords, messages, etc.) from third parties(adversaries) by securing the communication process between two entities. Cryptographic functions can be found in two java librarties: Java Cryptography Architecture(JCA) and Java Cryptography Extension(JCE).

JCA is tightly integrated with Java core API and provides basic cryptographic functions whereas JCE provides advanced cryptographic functions. Classes belonging to JCA and JCE are called engines. JCA engines are located at java.security package and JCE engines are located at javax.crypto packages. In the past, JCE is separated from JCA due to US export policies. Over time, This export control has been relaxed and in the present JCE becomes part of Java SE.

JCA and JCE provide functionalities. The actual implementations of those functions are provided by providers. Java has default providers we can use right away. Invoke getProviders() method from Security class in order to see all available providers in your java platform. The order of the providers in the array is their preference order.

The method returns Provider objects that hold information about cryptography providers. If you want to add new provider to your platform, invoke addProvider method from Security class.

For example:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;

public class ProviderExample{

  public static void main(String[] args){
    Security.addProvider(new BouncyCastleProvider());
  }
}
BouncyCastle is one of the popular cryptography providers that can be used in java. It was developed by an Australian charitable organization thus, US law restrictions do not apply to this provider. In the course of this tutorial, we're going to use the default providers provided by java.

Another way of using a third-party provider is to register it in the java.security file. In JDK8 and lower, the file is located in the directory where you installed java at jre\lib\security\java.security. Place the provider JAR in lib\ext

In JDK9 and above, the file is located in the directory where you installed java at conf\security\java.security. Place the provider JAR in the classpath.

Once you open java.security file, look at this section:
...
#
# List of providers and their preference orders (see above):
#
security.provider.1=SUN
security.provider.2=SunRsaSign
security.provider.3=SunEC
security.provider.4=SunJSSE
...
Then, add your third-party provider in the list:
...
#
# List of providers and their preference orders (see above):
#
security.provider.1=SUN
security.provider.2=SunRsaSign
security.provider.3=SunEC
security.provider.4=SunJSSE
security.provider.5=third-party-provider-name
...
In this tutorial, I'll be discussing Hashing, Symmetric Encryption, Asymmetric Encryption and Digital Signature. Java can also create digital certificate but I won't cover cerfiticates in this tutorial.

Hashing

Hashing or cryptographic hash function is a process of mapping data("message") with arbitrary size to an array of bits with fixed size (also called "hash value", "hash", "message digest").

Hashing is a one-way function. It means that the output is unfeasible to invert or reverse. Also, hashing must be deterministic. It means that messages that are the same have equivalent hash. On the other hand, a slight change to a message creates a different hash for the message.

This example demonstrates hashing in java.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.*;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     UnsupportedEncodingException{
    MessageDigest digestInstance = 
    MessageDigest.getInstance("SHA-256");
    String message1 = "Secret Message!";
    String message2 = 
    "The big brown fox jumps over the lazy doge";
    
    byte[] input = message1.getBytes("UTF-8");
    byte[] hash = digestInstance.digest(input);
    
    System.out.println("Message1: " + message1);
    System.out.println("Hash Value: " + 
    new BigInteger(1, hash).toString(16));
    System.out.println("Hash Length");
    System.out.println(hash.length + " bytes");
    System.out.println((hash.length*8) + " bits");
    
    input = message2.getBytes("UTF-8");
    hash = digestInstance.digest(input);
    
    System.out.println();
    System.out.println("Message2: " + message2);
    System.out.println("Hash Value: " + 
    new BigInteger(1, hash).toString(16));
    System.out.println("Hash Length");
    System.out.println(hash.length + " bytes");
    System.out.println((hash.length*8) + " bits");
  }
}

Result
Message1: Secret Message!
Hash Value: 1bf7aa187ccdf...
Hash Length
32 bytes
256 bits

Message2: The big brown fox jumps over the lazy doge
Hash Value: ec74b616322364f...
Hash Length
32 bytes
256 bits
MessageDigest provides cryptographic hash function with cryptographic hash algorithm such as SHA-256. Take a look at this article to know the supported cryptographic hash algorithms of MessageDigest.

SHA(Secure Hash Algorithm)-256 is a cryptographic hash algorithm that produces 256-bit hash value. This algorithm produces more bits than the old SHA algorithms such as SHA-0 and SHA-1 which produce 160-bit hash value. Thus SHA-256 is more secure than its old counterparts. If you want more secure algorithms, use SHA3 algorithms.

However, SHA-2 algorithms such as SHA-256, SHA-384, SHA-512 and SHA-224 are better than SHA3 algorithms in terms of performance. MD2 and MD5 are old and less secure than SHA-2 and SHA3 algorithms. Thus, it's not recommended to be used to secure data.

Although, MD5 is still used for checking the integrity of a file. Take note that files consist of bytes. Thus, we can get those bytes and put them in MessageDigest instance. MessageDigest.getInstance method returns a MessageDigest object with the specified hash algorithm. getBytes(String charsetName) method returns the byte structure of a java object or file in array form. The bytes are formatted based on charsetName.

digest method completes the hash computation by performing final operations such as padding. The digest is reset after this call is made. Padding is an additional bit added to the hash value if the input bits are less than the bits that a hash algorithm generates. That's why, In the example above, even Message1 is less than 256 bits, the hash value is still 256 bits.

We can use hash values to ensure that the expected message that the received message of a receiver is equivalent to the sent message of the sender. Take a look at this example
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.*;
import java.util.Arrays;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     UnsupportedEncodingException{
    /*sender*/
    MessageDigest senderDigest = 
    MessageDigest.getInstance("SHA-256");
    String message = "My Message!";
    
    byte[] senderInput = message.getBytes("UTF-8");
    byte[] senderHash = 
    senderDigest.digest(senderInput);
    System.out.println("Sender Hash Value: " +
    new BigInteger(1, senderHash).toString(16));
    
    /*receiver*/
    MessageDigest receiverDigest = 
    MessageDigest.getInstance("SHA-256");
    String expectedMsg = "My Message!";
    
    byte[] receiverInput = 
    expectedMsg.getBytes("UTF-8");
    byte[] receiverHash = 
    receiverDigest.digest(receiverInput);
    
    System.out.println("Receiver Hash Value: " +
    new BigInteger(1, receiverHash).toString(16));
    System.out.println("Sender and Receiver" +
    " hash values are equal?");
    System.out.println(
    Arrays.equals(senderHash, receiverHash));
  }
}

Result
Sender Hash Value: d2f02a685b80a087de...
Receiver Hash Value: d2f02a685b80a087de...
Sender and Receiver hash values are equal?
true
The example above is analogous to password hashing. When we create an application, we take the hash value of the password instead of the password itself and store the hash value in the database. This implementation hides passwords from employees and prying eyes.

Hashing has vulnerabilities such as brute-force search and rainbow-table. Although, those vulnerabilities may take a very long time breaking hash algorithms. We're talking about years or decades.

To defend against these vulnerabilities, we need to add a salt. To put it simply, a salt is just a random data added to the input. The salt is stored along with the password hash. During verification, the stored salt is included in the hash process along with the input. Then, the generated hash will be compared with the stored hash.

If you want the hash of multiple data, we use the update method. Take a look at this example.
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.math.BigInteger;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     UnsupportedEncodingException{
    MessageDigest digest = 
    MessageDigest.getInstance("SHA-512");
    String message1 = "Message1";
    String message2 = "Message2";
    
    digest.update(message1.getBytes("UTF-8"));
    digest.update(message2.getBytes("UTF-8"));
    byte[] hash = digest.digest();
    
    System.out.println("Hash value: " + 
    new BigInteger(1, hash).toString(16));
    System.out.println("Hash Length");
    System.out.println(hash.length + " bytes");
    System.out.println((hash.length*8) + " bits");
  }
}

Result
Hash value: dae036f171ffd578ff...
Hash length
64 bytes
512 bits
Symmetric Encryption

Symmetric encryption uses a single key for encrypting and decrypting data. An unencrypted data is called "cleartext" or "plaintext". The encrypted data is called "ciphertext". This example demonstrates symmetric encryption in java.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.BadPaddingException;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     InvalidKeyException,
                     IllegalBlockSizeException,
                     NoSuchPaddingException,
                     BadPaddingException,
                     UnsupportedEncodingException{
    KeyGenerator keygen = 
    KeyGenerator.getInstance("AES");
    
    keygen.init(192);
    SecretKey key = keygen.generateKey();
    System.out.println("Generated Key");
    System.out.println(
    new BigInteger(1, key.getEncoded()).toString(16));
    
    byte[] input = 
    "The big brown fox jumps over a lazy dog."
    .getBytes("UTF-8");
    System.out.println("Plain text");
    System.out.println(new String(input));
    
    Cipher cipher = 
    Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    byte[] encryptedData = cipher.doFinal(input);
    System.out.println("Ciphertext");
    System.out.println(
    new BigInteger(1, encryptedData).toString(16));
    
    cipher.init(Cipher.DECRYPT_MODE, key);
    byte[] decryptedData = 
    cipher.doFinal(encryptedData);
    System.out.println("Decrypted Data");
    System.out.println(new String(decryptedData));
  }
}

Result(may vary)
Generated Key
57197a794884a...
Plain Text
The big brown fox jumps over a lazy dog.
Ciphertext
2968b36c3699c5a12843...
Decrypted Data
The big brown fox jumps over a lazy dog.
KeyGenerator provides the functionality of a secret (symmetric) key generator. KeyGenerator objects are reusable, i.e., after a key has been generated, the same KeyGenerator object can be re-used to generate further keys. KeyGenerator.getInstance(String algorithm) method returns a KeyGenerator object with the specified algorithm for generating a key.

Available algrorithms for KeyGenerator can be found in this article. Take note that DES(Data Encryption Standard) is discouraged to be used because it's old and not secure anymore.

init(int keysize) method initializes this key generator for a certain keysize using the provider's SecureRandom. AES(Advanced Encryption Standard) supports 128/192/256 bits keysize. AES uses Substitution–permutation network.

To put it simply, AES scrambles bits in multiple rounds. Key size of AES determines the number of round that is going to be used. 128-bit key size implements 10 rounds. 192-bit implements 12 rounds. 256-bit implements 14 rounds. A key with higher key size is more secure. However, key with higher key size takes too long to encrypt/decrypt data. Thus, degrading our application's performance. For standard applications, at the time I write this tutorial, 128-bit key size should suffice.

generateKey method returns SecretKey compatible with the specified algorithm in KeyGenerator object. A key is a string of characters used within an encryption algorithm for altering data so that it appears random.

Cipher provides the functionality of a cryptographic cipher for encryption and decryption. We use Cipher class to encrypt and decrypt data. getInstance(String transformation) returns a Cipher object that implements the specified transformation.

A transformation is a string that describes the operation (or set of operations) to be performed on the given input, to produce some output. A transformation always includes the name of a cryptographic algorithm (e.g., AES), and may be followed by a feedback mode and padding scheme.

A transformation string can be one of these:
  • "algorithm/mode/padding"
  • "algorithm"
In the latter case, provider-specific default values for the mode and padding scheme are used. Before we discuss the mode in a transformation, we need to know two types of Ciphers: Stream Cipher and Block Cipher.

Stream Cipher encrypts the digits (typically bytes), or letters (in substitution ciphers) of a message one at a time. An example is ChaCha20. Substitution ciphers are well-known ciphers, but can be easily decrypted using a frequency table.

Block Cipher takes a number of bits and encrypt them as a single unit, padding the plaintext so that it is a multiple of the block size. The Advanced Encryption Standard (AES) algorithm, approved by NIST in December 2001, uses 128-bit blocks. To put it simply, Block cipher divides the number of bits of plaintext into blocks based on block size. Each block size of blocks is equal.

Now, mode or block cipher mode determines how blocks are encrypted. Some modes(such as CFB and OFB) can encypt and decrypt blocks that are smaller than the actual cipher's block size. There are different types of modes and I'm not gonna explain all of them in this tutorial. You can read this article if you're interested to learn more about them.

ECB(Electronic Code Book) is the simplest cipher block mode because unlike other modes, this mode directly encrypt and decrypt blocks without additional procedures. However, this mode is less secure than others. This mode is bad at hiding data patterns and susceptible to replay attacks since this mode encrypts and decrypts blocks the same way.

padding is any of a number of distinct practices which all include adding data to the beginning, middle, or end of a message prior to encryption. In classical cryptography, padding may include adding nonsense phrases to a message to obscure the fact that many messages end in predictable ways.

PKCS5Padding is commonly used padding type that is provided by default providers. In PKCS5Padding, Padding is in whole bytes. The value of each added byte is the number of bytes that are added, i.e. N bytes, each of value N are added. The number of bytes added will depend on the block boundary to which the message needs to be extended.

The padding will be one of:
01
02 02
03 03 03
04 04 04 04
05 05 05 05 05
06 06 06 06 06 06
etc.
Example: In the following example the block size is 8 bytes and padding is required for 4 bytes.
... | DD DD DD DD DD DD DD DD | DD DD DD DD 04 04 04 04 |
In java, we can use "NoPadding" in the string transformation if we're going to provide our own padding. Otherwise, we need to use one of the padding types available to our platform.

More information about padding types can be found in this article. More information about PKCS(Public Key Cryptography Standards) can be found in this article. Take note that ISO10126Padding is not encouraged to be used anymore because it's widthrawn in 2007.

init(int opmode, Key key) initializes a Cipher object with specified operation and key. The cipher is initialized for one of the following four operations: encryption, decryption, key wrapping or key unwrapping, depending on the value of opmode. This method form uses the provider's SecureRandom if the Cipher object needs random bytes.

Let's focus on encrypting and decrypting data. To initialize a Cipher object in "encrypt" mode, put Cipher.ENCRYPT_MODE in the argument list with the selected key. To initialize a Cipher object in "decrypt" mode, put Cipher.DECRYPT_MODE in the argument list with selected key.

doFinal(byte[] input) executes the operation of the Cipher object that invokes this method. If you want to encrypt multi-part data, use update(byte[] input) method per data and then invoke doFinal() method to finalize the input and execute the selected operation.

Next, I'm gonna demonstrate another cipher block mode which is the CBC(Cipher Block Chaining). This mode is more secure than ECB and is commonly used. In CBC mode, each block of plaintext is XORed with the previous ciphertext block before being encrypted. This way, each ciphertext block depends on all plaintext blocks processed up to that point. To make each message unique, an initialization vector(IV) must be used in the first block.

This example demonstrates CBC mode.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.BadPaddingException;
import javax.crypto.spec.IvParameterSpec;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     InvalidAlgorithmParameterException,
                     InvalidKeyException,
                     IllegalBlockSizeException,
                     NoSuchPaddingException,
                     BadPaddingException,
                     UnsupportedEncodingException{
                     
    KeyGenerator keygen = 
    KeyGenerator.getInstance("AES");
    
    keygen.init(192);
    SecretKey key = keygen.generateKey();
    System.out.println("Generated Key");
    System.out.println(
    new BigInteger(1, key.getEncoded()).toString(16));
    
    SecureRandom sr = 
    SecureRandom.getInstance("SHA1PRNG");
    byte[] randBytes = new byte[16];
    sr.nextBytes(randBytes);
    IvParameterSpec iv = 
    new IvParameterSpec(randBytes);
    System.out.println("IV");
    System.out.println(
    new BigInteger(1, iv.getIV()).toString(16));
    
    byte[] input = 
    "The big brown fox jumps over a lazy dog."
    .getBytes("UTF-8");
    System.out.println("Plain text");
    System.out.println(new String(input));
    
    Cipher cipher = 
    Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    byte[] encryptedData = cipher.doFinal(input);
    System.out.println("Ciphertext");
    System.out.println(
    new BigInteger(1, encryptedData).toString(16));
    
    cipher.init(Cipher.DECRYPT_MODE, key, iv);
    byte[] decryptedData = 
    cipher.doFinal(encryptedData);
    System.out.println("Decrypted Data");
    System.out.println(new String(decryptedData));
  }
}

Result(may vary)
Generated Key
50bfcd0d400bc...
IV
afd7ece30...
Plain Text
The big brown fox jumps over a lazy dog.
Ciphertext
dcae499df38120f91...
Decrypted Data
he big brown fox jumps over a lazy dog.
SecureRandom.getInstance(String algorithm) returns a SecureRandom object that implements the specified Random Number Generator (RNG) algorithm. nextBytes(byte[] bytes) method generates random bytes and put them in the bytes array.

IvParameterSpec specifies an initialization vector (IV). Examples which use IVs are ciphers in feedback mode, e.g., DES in CBC mode and RSA ciphers with OAEP encoding operation. getIV method returns the initialization vector (IV). Take note that The IV size depends on the cryptographic primitive used; for block ciphers it is generally the cipher's block-size. In the example above, the IV size is 16 bytes or 128 bits, which is equal to AES block size.

init(int opmode, Key key, AlgorithmParameterSpec params) Initializes this cipher with a key and a set of algorithm parameters. We use this constructor if the algorithm in our Cipher object requires a parameter. Algorithm-specific parameters in java are represented by AlgorithmParameterSpec and its implementing classes and known subinterfaces.

In the example above, the Cipher object uses CBC(Cipher Block Chaining) and CBC requires an IV(initialization vector) parameter. To fulfill that requirement, we need to instantiate a IvParameterSpec object.

From the documentation:
If the cipher requires any algorithm parameters and params is null, the underlying cipher implementation is supposed to generate the required parameters itself (using provider-specific default or random values) if it is being initialized for encryption or key wrapping, and raise an InvalidAlgorithmParameterException if it is being initialized for decryption or key unwrapping. The generated parameters can be retrieved using getParameters or getIV (if the parameter is an IV).
Take note that we can encrypt files using symmetric encryption. Take a look at this example.
...
byte[] input = java.nio.file.Files.
readAllBytes(
new java.io.File("fileTest.zip").
toPath());

Cipher cipher =
Cipher.getInstance("AES/CBC/PKCS5Padding");

cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] encryptedData = cipher.doFinal(input);
System.out.println("File Encrypted!");

cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] decryptedData =
cipher.doFinal(encryptedData);

try(FileWriter fw =
    new FileWriter("decryptedFile.zip")){
    for(byte b : decryptedData)
      fw.write(b);
}
System.out.println("File Decrypted!");
...
Asymmetric Encryption

Asymmetric Encryption or Public-key cryptography uses a pair of keys to encrypt/decrypt data. In Asymmetric Encryption, there are two types of keys: Public Key and Private Key. Public key is used for encrypting data and private key is used for decrypting data. For example, In a sender-receiver scenario, Assuming we're the receiver, we share our public key to the sender and the sender uses our public key to encrypt the message(data) that the sender wanna send to us.

Then, once we receive the encrypted data, we decrypt it using our private key. Remember that we only share your public keys and we keep our private keys "private". To put it simply, don't share your private key with anyone except you. As you can see, Asymmetric encryption is more secure than symmetric encryption.

Even the attackers intercepted your public key, they couldn't use the public key to decrypt encrypted messages. However, asymmetric encryption is still vulnerable to some kind of attacks. The good news is that those attacks are hard to implement and don't break asymmetric encryption easily. Read this article to know more about the weaknesses of asymmetric encryption.

Another disadvantage of this encryption is that this encryption can encrypt data with limited length. Typically, asymmetric encryption is slower than symmetric encryption. There are different types of algorithms that implement asymmetric encryption. In this tutorial, I'm gonna use the RSA(Rivest–Shamir–Adleman). This article lists algorithms that supports asymmetric encryption.

This example demonstrates asymmetric cryptography using RSA algorithm.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.PrivateKey;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;


public class SampleClass{

  public static void main(String[] args) throws
                     BadPaddingException,
                     IllegalBlockSizeException,
                     InvalidKeyException,
                     NoSuchAlgorithmException,
                     NoSuchPaddingException,
                     UnsupportedEncodingException
                     {
    KeyPairGenerator keygen = 
    KeyPairGenerator.getInstance("RSA");
    keygen.initialize(2048);
    
    KeyPair pair = keygen.generateKeyPair();
    PrivateKey privKey = pair.getPrivate();
    PublicKey pubKey = pair.getPublic();
    
    byte[] privKeyBytes = privKey.getEncoded();
    System.out.println("Private Key");
    System.out.println(
    new BigInteger(1, privKeyBytes).toString(16));
    System.out.println("Key Size");
    System.out.println(privKeyBytes.length + " bytes");
    System.out.println(
    (privKeyBytes.length*8) + " bits");
    System.out.println();
    
    byte[] pubKeyBytes = pubKey.getEncoded();
    System.out.println("Public Key");
    System.out.println(
    new BigInteger(1, pubKeyBytes).toString(16));
    System.out.println("Key Size");
    System.out.println(pubKeyBytes.length + " bytes");
    System.out.println(
    (pubKeyBytes.length*8) + " bits");
    System.out.println();
    
    String input = "I love programming!";
    System.out.println("Input");
    System.out.println(input);
    System.out.println();
    
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.ENCRYPT_MODE, pubKey);
    
    byte[] inputBytes = input.getBytes("UTF-8");
    byte[] encryptedData = cipher.doFinal(inputBytes);
    System.out.println("Encrypted Data");
    System.out.println(
    new BigInteger(1, encryptedData).toString(16));
    System.out.println();
    
    cipher.init(Cipher.DECRYPT_MODE, privKey);
    byte[] decryptedData = cipher.doFinal(encryptedData);
    System.out.println("Decrypted Data");
    System.out.println(new String(decryptedData, "UTF-8"));
  }
}

Result(may vary)
Private Key
308204bc020100300d06902a...
Key Size
1216 bytes
9728 bits

Public Key
30820122300d0692a...
Key Size
294 bytes
2352 bits

Input
I love programming!

Encrypted Data
3d6c5729ec927abae5bd...

Decrypted Data
I love programming!
KeyPairGenerator is used to generate pairs of public and private keys. A Key pair generator for a particular algorithm creates a public/private key pair that can be used with this algorithm. It also associates algorithm-specific parameters with each of the generated keys.

RSA is a block cipher and it requires a padding parameter. Fortunately, java automatically provided a padding parameter for RSA when we invoke KeyPairGenerator.getInstance(String algorithm) with "RSA" as our asymmetric encryption algorithm. This article lists asymmetric algorithms that are available to java by default.

initialize(int keysize) Initializes the key pair generator for a certain keysize using a default algorithm-specific parameter set and the SecureRandom implementation of the highest-priority installed provider as the source of randomness. (If none of the installed providers supply an implementation of SecureRandom, a system-provided source of randomness is used.)

RSA supports 512, 1024, 2048 and 4096 bits key size. Key size in this context is not the actual key size of both public and private keys. Key size in this context is used to determine the length(size) of public and private keys. The higher the key size, the longer the length of public and private keys. Keys with higher length are more secure.

However, keys with higher size take time to encrypt/decrypt data. For standard applications, at the time I write this tutorial, 2048 key size is widely used because it has a good balance between security and performance. KeyPair is a simple holder for a key pair (a public key and a private key). It does not enforce any security, and should be treated like a PrivateKey when initialized because the instance of this class holds a private key.

PrivateKey is used to group (and provide type safety for) all private key interfaces. PublicKey is an interface that contains no methods or constants. It merely serves to group (and provide type safety for) all public key interfaces.

generateKeyPair() generates a pair of key(public and private) based on the selected algorithm and key size of KeyPairGenerator instance. getPrivate() retuns a PrivateKey instance with the generated private key stored in KeyPairGenerator instance. getPublic retuns a PublicKey instance with the generated public key stored in KeyPairGenerator instance.

I didn't explain some classes and methods that I've used in the example above because I already explained some of them in previous topics. I assumed you've read previous topics before reading this topic. If you're already familiar with java and its cryptography API, you don't need to read the previous topics.

In the example above, Notice that private key is longer than public key. In RSA, private key stores more information than public key and some of those information are confidential.

Take note that RSA data encryption has length limit. For example, using my java platform's default RSA provider, If I increase the byte length in input variable in the example above:
...
//repeat() means, repeat this string by the
//specified amount in the argument. In this example,
//the String is repeated 50 times.
String input = "I love programming!".repeat(50);
...
I'll get this exception:
...
Exception in thread "main" javax.crypto.IllegalBlockSizeException: Data must not be longer than 245 bytes.
...


The limitation length is based on the key size we put in initialize(int keysize). The limitation length increases/decreasees as key size gets larger/smaller. If we want to encrypt large data, symmetric encrypt is more preferrable to be used. Some of the usage of asymmetric encryption are:
  • Encrypting/Decrypting symmetric keys
  • Encrypting/Decrypting hashes
You may ask yourself on how to verify if a public key that you receive comes from the real sender you're expecting. This is where digital certificate comes in. Digital certificate, to put it simply, is used verify the identity of a public key. Digital certificate is not part of the scope of this tutorial.
Digital Signature

Digital signatures are used for verifying the integrity of data. Digital signature ensures that the data we receive from a sender is not tampered. Digital signature is simply just a hash of the data that is going to be sent and the hash is encrypted. This example demonstrates asymmetric digital signature.
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import javax.crypto.Cipher;

public class SampleClass{

  public static void main(String[] args) throws
                     InvalidKeyException,
                     NoSuchAlgorithmException,
                     SignatureException,
                     UnsupportedEncodingException{
  
    /*Sender*/
    
    KeyPairGenerator keygen = 
    KeyPairGenerator.getInstance("RSA");
    keygen.initialize(2048);
    
    KeyPair pair = keygen.generateKeyPair();
    PrivateKey privKey = pair.getPrivate();
    PublicKey pubKey = pair.getPublic();
    
    String input = "I love programming!";
    
    Signature signatureAlg = 
    Signature.getInstance("SHA256WithRSA");
    signatureAlg.initSign(privKey);
    signatureAlg.update(input.getBytes("UTF-8"));
    byte[] signBytes = signatureAlg.sign();
    
    /*Receiver*/
    
    Signature verificationAlg =
    Signature.getInstance("SHA256WithRSA");
    verificationAlg.initVerify(pubKey);
    verificationAlg.update(input.getBytes("UTF-8"));
    System.out.println("Is data not tampered? " +
    verificationAlg.verify(signBytes));
  }
}

Result
Is data not tampered? true
In the example above, the sender gets the hash of the data and encrypts the hash using his/her private key. Then, the sender sends his/her public key, signature(encrypted hash) and the data to the receiver. Now, the receiver uses the received public key to decrypt the received hash. Then, we get the hash of the received data and compare it to the decrypted hash.

The process that I've explained can be simplified in java. We use Signature to hash and encrypt/decrypt it. Signature.getInstance(String algorithm) returns a Signature object that implements the specified signature algorithm.

"SHA256WithRSA" means SHA-256 is the hash algorithm and RSA is the encryption/decryption algorithm. More available algorithms that are supported by java can be found in this article.

initSign(PrivateKey privateKey) initializes a Signature instance for signing with PrivateKey argument. update(byte[] data) updates the data to be signed or verified, using the specified array of bytes. sign() creates a digital signature with the specified algorithm and data.

In other words, sign() gets the hash of the data, encrypts the hash and return the encrypted hash. initVerify(PublicKey publicKey) initializes a Signature instance for verification with PublicKey argument. verify(byte[] signature) verifies the specified signature.

In other words, verify(byte[] signature) decrypts the encrypted hash (signature), gets the hash of the received data and compare the decrypted hash with the hash of the received data. If both hashes match, the received data is not tampered. In the example above, I only encrypt the hash of the data that is sent to the receiver and not the data itself.

If you only want to check the integrity of your data and now worried about it to be intercepted, You don't need to encrypt the data itself. Otherwise, encrypt the data. I skipped encrypting the data itself in order to focus on digital signature.

Message Authentication Code(MAC)

Message Authentication Code is a symmetric digital signature. MAC uses a single key to create and verify a digital signature. This is also called as keyed hashing because it secures the hash of data by generating the hash with a key. One of the advantages of MAC over asymmetric digital signature is that MAC is much faster than most algorithms used for asymmetric digital signature.

However, asymmetric digital signature is commonly used than MAC because it offers more security. MAC can be cipher-based(CMAC & GMAC) or hash-based(HMAC). In this tutorial, we're going to focus on HMAC. Cipher-based MACs are tricky to implement. It's better to start with HMAC first.

This example demonstrate HMAC.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;

public class SampleClass{

  public static void main(String[] args) throws
                       NoSuchAlgorithmException,
                       InvalidKeyException,
                       UnsupportedEncodingException{
    
    /*Sender*/
    
    KeyGenerator keygen = KeyGenerator.getInstance("HmacSHA256");
    keygen.init(256);
    SecretKey key = keygen.generateKey();
    
    Mac senderMac = Mac.getInstance("HmacSHA256");
    senderMac.init(key);
    
    String input = "A secret between you and me.";
    senderMac.update(input.getBytes("UTF-8"));
    byte[] senderComputedMac = senderMac.doFinal();
    
    /*Receiver*/
    
    Mac receiverMac = Mac.getInstance("HmacSHA256");
    receiverMac.init(key);
    receiverMac.update(input.getBytes("UTF-8"));
    byte[] receiverComputedMac = receiverMac.doFinal();
    
    String senderMacStr = 
    new BigInteger(1, senderComputedMac).toString(16);
    String receiverMacStr = 
    new BigInteger(1, receiverComputedMac).toString(16);
    
    System.out.println("Sender MAC");
    System.out.println(senderMacStr);
    System.out.println();
    
    System.out.println("Receiver MAC");
    System.out.println(receiverMacStr);
    System.out.println();
    
    System.out.println("Is data not tampered?");
    System.out.println(
    senderMacStr.equals(receiverMacStr));
  }
}

Result

Sender MAC
5df66b8ef0a885bd2...

Receiver MAC
5df66b8ef0a885bd2...

Is data not tampered?
true
In the example above, the sender generates a key, generates MAC by generating a hash from the data with the key and send the key and MAC over secure channel. The data can be sent over unsecure channel if the sender is not worried about the data getting intercepted. Otherwise, encrypt the data first before sending it to the unsecure channel. Next, once the key, MAC and data has been sent to the receiver, the receiver generates a new MAC by generating a hash from the received data with the received key.

If the received MAC and the generated MAC of the receiver are the same, it means that the data came from the sender who owns the received key and it's not tampered. Otherwise, the data may be tampered, the sender gave a wrong key or the received key is not the key that generated the received MAC.

Key size is 256 bits because at the time I write this, 256 bits keysize should suffice for standard applications. If you want more security at the expense of performance, you can increase the key size. The algorithm used is "HmacSHA256" or generated HMAC using SHA-256 algorithm. More supported Mac algorithm by java can be found in this article.

The algorithm used in the KeyGenerator and Mac instances must be the same. Take note that HmacMD5 and HmacSHA1 are discouraged to be used because they're not secure anymore. Use other algorithms like HmacSHA2 such as HmacSHA256 and HmacSHA512; HmacSHA3 algorithms.

If you want more security at the expense of performance, use hash algorithm with higher block size like HmacSHA512 or use the new SHA family which is the SHA3 family. SHA2 and SHA3 have different algorithms. SHA2 is vulnerable to length extension attack whereas SHA3 is not.

Mac class provides the functionality of a "Message Authentication Code" (MAC) algorithm.

Key Wrapping

Key wrapping is a process of encrypting a key using another key. This example demonstrates wrapping/unwrapping symmetric key using asymmetric keys.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.*;
import javax.crypto.*;

public class SampleClass{

  public static void main(String[] args) throws
                     BadPaddingException,
                     IllegalBlockSizeException,
                     InvalidKeyException,
                     NoSuchAlgorithmException,
                     NoSuchPaddingException,
                     UnsupportedEncodingException{
    KeyPairGenerator keyPairGen = 
    KeyPairGenerator.getInstance("RSA");
    keyPairGen.initialize(2048);
    
    KeyPair pair = keyPairGen.generateKeyPair();
    PrivateKey privKey = pair.getPrivate();
    PublicKey pubKey = pair.getPublic();
    
    KeyGenerator keygen =
    KeyGenerator.getInstance("AES");
    keygen.init(192);
    
    SecretKey secretKey = keygen.generateKey();
    
    String input = "Send this message!";
    System.out.println("input: " + input);
    
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    
    byte[] encryptedData = 
    cipher.doFinal(input.getBytes("UTF-8"));
    System.out.println("Encrypted Data");
    System.out.println(
    new BigInteger(1, encryptedData).toString(16));
    
    cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.WRAP_MODE, pubKey);
    byte[] encryptedKey = 
    cipher.wrap(secretKey);
    
    cipher.init(Cipher.UNWRAP_MODE, privKey);
    SecretKey decryptedKey = 
    (SecretKey)cipher.unwrap(encryptedKey, "AES", 
    Cipher.SECRET_KEY);
    
    cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, decryptedKey);
    byte[] decryptedData = cipher.doFinal(encryptedData);
    
    System.out.println("Decrypted Data: " + 
    new String(decryptedData, "UTF-8"));
  }
}

Result
input: Send this message!
Encrypted Data
118729c524039308c...
Decrypted Data: Send this message!
In the example above, The sender wraps the data that he is going to send using AES which is a symmetric encryption algorithm. Then, the generated key for the encrypted data is encrypted using RSA algorithm which is an asymmetric encryption algorithm. He uses the public key of the receiver to encrypt the key of the encrypted data.

The sender sends the encrypted key and data to the receiver. Then, the receiver unwraps(decrypts) the wrapped(encrypted) key using his private key. Then, he uses the unwrapped key to decrypt the data. We can use the example above if we want to securely transfer a large encrypted data to a receiver. The asymmetric keys encrypt/decrypt the symmetric key of the large data and the symmetric key encrypts the large data.

To wrap a key, invoke init(int opmode, Key key) method of Cipher class. opmode parameter is the operation mode. We put Cipher.WRAP_MODE if we're going to wrap a key. key parameter is the key that we're going to use to wrap a key. Once init is invoked. Invoke wrap(Key key) of Cipher class to wrap the key. key parameter is the key that we're going to wrap.

Next, to unwrap the key, we invoke init again but this time the operation mode is Cipher.UNWRAP_MODE and the key is the private key of the receiver. Once init is invoked, invoke unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) method.

This method unwraps a wrapped key. wrappedKey parameter is the bytes of the encrypted key. wrappedKeyAlgorithm is the associated algorithm of the encrypted key. wrappedKeyType is the key type of the encrypted key. There are three types of keys in Cipher class:
Cipher.PRIVATE_KEY
Cipher.PUBLIC_KEY
These keys are keys for asymmetric encryption.
Cipher.SECRET_KEY
This key is a key for symmetric encryption.

Storing Keys

There are two ways of storing keys. The simplest way of storing keys is to write keys to a persistent file. Make sure that you store your keys in a safe place and don't share your key with untrusted people.

Write the key hash of your key to a persistent file if you're planning to use your keys to other programming platforms. If you're planning to use your keys only in java, you can serialize the keys. SecretKey, PublicKey and PrivateKey are serializable.

This example demonstrates storing key hash in a file and recreating the key in java.
import java.io.*;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class SampleClass{

  public static void main(String[] args) throws
                     IOException,
                     NoSuchAlgorithmException{
    File secretKeyFile = 
    new File("key/my-secret.key");
    
    KeyGenerator keygen =
    KeyGenerator.getInstance("AES");
    keygen.init(192);
    
    SecretKey secretKey = keygen.generateKey();
    
    //write the keys to a persistent file
    if(!secretKeyFile.exists()){
         try(BufferedWriter bw = 
             new BufferedWriter(
             new FileWriter(secretKeyFile))){
             String key =  
             new BigInteger(1, secretKey.getEncoded())
             .toString(16);
             bw.write(key, 0, key.length());
             System.out.println("Key written to file");
             System.out.println(key);
             
             //let gc remove the key as soon as possible
             //After use, don't let the key persist with
             //memory for too long.
             key = null;
         }
     }
     else{
       System.out.println("File/s already exist!");
       return;
     }
    
    //read the keys from the persistent file
    try(FileReader fr = new FileReader(secretKeyFile)){
      StringBuilder sb = new StringBuilder();
      
      int i = 0;
      while((i = fr.read()) != -1)
        sb.append((char)i);
      
      //convert the key hash to array of bytes
      //in order to recreate the secret key using
      //SecretKeySpec class
      SecretKey keyFromFile = 
      new SecretKeySpec(
      HexFormat.of().parseHex(sb.toString()),
      "AES");
      sb = null;
      
      System.out.println("key read from file");
      System.out.println(
      new BigInteger(1, 
      keyFromFile.getEncoded()).toString(16));
    }
    
  }
}

Result
Key written to file
73f6040f39f1...
Key read from file
73f6040f39f1...
HexFormat is a utility class that we can use to parse hex strings. In the example above, we're writing a plain key hash in a persistent file. If you want to obscure key hash, you can use Base64 in java.

HexFormat is introduced in java 17. If you're using java with version lower than 17, HexFormat is not available. This dicussion has a solution for parsing key hashes that works in java with version lower than 17.

If you're planning to use your keys on other platforms and you want to obscure them using Base64, make sure that the platform can decode Base64 characters. SecretKeySpec can be used to construct a SecretKey from a byte array, without having to go through a (provider-based) SecretKeyFactory.

SecretKeySpec is only useful for raw secret keys that can be represented as a byte array and have no key parameters associated with them, e.g., DES or Triple DES keys.

Another way of storing keys is to create a KeyStore. KeyStore can store keys and certificates. In this tutorial, I'm gonna demonstrates on how to store a key for symmetric encryption.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.File;
import java.math.BigInteger;
import java.security.cert.CertificateException;
import java.security.NoSuchAlgorithmException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.KeyStore.PasswordProtection;
import java.security.UnrecoverableKeyException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class SampleClass{

  public static void main(String[] args) throws
                          IOException,
                          NoSuchAlgorithmException,
                          CertificateException,
                          KeyStoreException,
                          UnrecoverableKeyException{
    
    KeyGenerator keygen =
    KeyGenerator.getInstance("AES");
    keygen.init(192);
    
    SecretKey secretKey = keygen.generateKey();
    
    KeyStore keystore = KeyStore.getInstance("JCEKS");
    //This is unsecure. Don't hardcode passwords.
    //It's better to put your KeyStore password
    //in a persistent file. Before that, obscure
    //or encrypt your password first.
    char[] pass = "password".toCharArray();
    
    //initialize Keystore
    keystore.load(null, pass);
    
    File source = new File("keystore/my-keystore.jks");
    if(source.exists()){
       System.out.println("KeyStore already exists!");
       return;
    }
    
    //write KeyStore object to a file
    try(FileOutputStream fos = new 
        FileOutputStream(source)){
        keystore.store(fos, pass);
    }
    
    //Initialize KeyStore from the given
    //input stream
    try(FileInputStream fis = new 
        FileInputStream(source)){
        keystore.load(fis, pass);
    }
    
    //add entry to KeyStore
    KeyStore.SecretKeyEntry keyEntry =
    new KeyStore.SecretKeyEntry(secretKey);
    KeyStore.ProtectionParameter entryPass =
    new KeyStore.PasswordProtection(pass);
    keystore.setEntry("secret-key", keyEntry, entryPass);
    
    SecretKey ksSecretKey = 
    (SecretKey)keystore.getKey("secret-key", pass);
    
    System.out.println("Generated Key");
    System.out.println(
    new BigInteger(1, secretKey.getEncoded()).toString(16));
    System.out.println();
    
    System.out.println("Key in KeyStore");
    System.out.println(
    new BigInteger(1, ksSecretKey.getEncoded()).toString(16));
  }
}

Result
Generated Key
e71094e7294563d771...

Key in KeyStore
e71094e7294563d771...
getInstance(String type) returns KeyStore object with the specified type. JCEKS is similar to JKS(Java KeyStore). Although, JCEKS supports symmetric keys whereas JKS only supports asymmetric keys. In terms of security, JCEKS has stronger key protection.

load(InputStream stream, char[] password) Loads the KeyStore contents from the given input stream to the KeyStore intance. If stream argument is null, this method initializes KeyStore with no contents and associates the given password to it. store(OutputStream stream, char[] password) writes the KeyStore object to a persistent file via output stream and protect the file's integrity with the given password.

KeyStore.SecretKeyEntry creates an entry for a secret key.KeyStore.ProtectionParameter is used to protect the contents of a key store. KeyStore.PasswordProtection is used to wrap password that can be implemented in key store.

setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam) saves a KeyStore entry to a KeyStore. alias parameter is the name associated with the entry. We can use alias if we want to select the entry associated with the alias. entry parameter is the entry that we wanna store in the KeyStore. protParam parameter is a protection of KeyStore wrapped into KeyStore.ProtectionParameter.

getKey(String alias, char[] password) retrives a key with the associated alias and the password that we put in the entry of the key. Storing symmetric key in KeyStore is simple. However, Some keys or certificates are kinda tricky to be stored in KeyStore. Another way of create a KeyStore is using the keytool command in java. keytool is not in the scope of this tutorial.

Salted Password

Salted password is a password that is mixed with cryptographic salt. Cryptographic salt or salt for short, is random data that is used as an additional input to a one-way function that hashes data. In other words, we mix salt and a data, password in our case, and then generate a key from the password with mixed salt.

In this tutorial we're gonna be using PBKDF2. bcrypt and scrypt are some of the popular alternatives to PBKDF2 but java doesn't support those algorithm by default. If you wanna use those algorithms, you need a third-party vendor that implements those algorithms.

This example demonstrates PBKDF2.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class SampleClass{
  public static void main(String[] args)
                throws UnsupportedEncodingException,
                       NoSuchAlgorithmException,
                       InvalidKeySpecException{
    /*Encrypting*/
    String pass = "password";
    byte[] salt = new byte[16];
    SecureRandom sr =
    SecureRandom.getInstance("SHA1PRNG");
    sr.nextBytes(salt);
    int iterations = 10000;
    int keyLength = 512;
    
    SecretKeyFactory keyFactory =
    SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
    
    PBEKeySpec spec =
    new PBEKeySpec(pass.toCharArray(), salt,
                   iterations, keyLength);
    SecretKey key = keyFactory.generateSecret(spec);
    byte[] generatedBytes = key.getEncoded();
    String storedHash = 
    new BigInteger(1, generatedBytes).toString(16);
    System.out.println("Stored Hash");
    System.out.println(storedHash);
    System.out.println();
    
    /*Verification*/
    keyFactory = 
    SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
    spec = new PBEKeySpec(pass.toCharArray(), salt,
                          iterations, keyLength);
    key = keyFactory.generateSecret(spec);
    generatedBytes = key.getEncoded();
    String inputHash =
    new BigInteger(1, generatedBytes).toString(16);
    System.out.println("Input Hash");
    System.out.println(inputHash);
    System.out.println();
    
    System.out.println("Stored and input hashes equal?");
    System.out.println(storedHash.equals(inputHash));
  }
}

Result
Stored Hash
b2f36d5f92ac1f...

Input Hash
b2f36d5f92ac1f...

Stored and input hashes equal?
true
In the example above, SecureRandom is instantiated with "SHA1PRNG" algorithm. There are other algorithms that are available and they can be found here. "SHA1PRNG" is commonly used PRNG(Pseudo-Random Number Generator) algorithm due to simplicity of SHA-1 algorithm.

When generating hash or cryptographic key, SHA-1 is discouraged to be used because it's unsecure. Since we just want to generate random numbers that can be mixed with our password, "SHA1PRNG" is alright to be used. Next, we set iterations variable to 10,000 and keyLength variable to 512.

PBEKeySpec class can create a key specification with the specification of password-based encryption(PBE). PBKDF2 is a password-based encryption. Thus, The key that is created by PBEKeySpec is compatible with PBKDF2.

PBEKeySpec(char[] password, byte[] salt, int iterationCount, int keyLength) generates a key spec with the specified password mixed with salt, iteration and key length. password parameter is the password that is gonna be mixed with salt. salt parameter is the random bytes that is gonna be mixed with password. Standard salt size for PBKDF2 is at least 64 bits or 8 bytes.

In the example above, I use 16 bytes or 128 bits recommended by The US National Institute of Standards and Technology. Salt is a way to reduce the ability of attackers to break a password using pre-computed hashes such as rainbow tables.

IterationCount parameter is the number of repetitive use of pseudorandom for password along with the salt. This process makes password cracking much more difficult. This process is also known as key stretching.

Organizations have different standards when it comes to the number of iteration count. For example, Apple reportedly used 2000 for iOS 3, and 10000 for iOS 4. OWASP recommended to use 310000 iterations for PBKDF2-HMAC-SHA256 and 120000 for PBKDF2-HMAC-SHA512.

If you want more security from password cracking, you can increase the itearation count anytime. Just remember that the higher the iteration count, the slower the key generation. If computers get faster in the future, attackers can crack passwords fast. To counter that problem, we can just increase the iteration count.

keyLength is the length of the key. Long keys are harder to break. Thus, adding more security to our passwords. For stanrdard applications, length of 512 bits should suffice.

Once we generate a key spec for PBKDF2. We need to convert the key spec into a cryptographic key. To do that, we need to get an instance of SecretKeyFactory. Key factories are used to convert keys (opaque cryptographic keys of type Key) into key specifications (transparent representations of the underlying key material), and vice versa.

SecretKeyFactory.getInstance(String algorithm) returns a SecretKeyFactory object that converts secret keys of the specified algorithm. Available algorithms for SecretKeyFactory provided by java can be found here.

In the example above, I use "PBKDF2WithHmacSHA512" algorithm. This algorithm uses PBKDF2 with HMAC with SHA512. generateSecret(KeySpec keySpec) generates a SecretKey object from the provided key specification. We use SecretKey as a key type of PBKDF2 because PBKDF2 with HMAC is a symmetric encryption.

Once we successfully generated a SecretKey for our password with salt, we need to get its hash in hexadecimal form. To do that, we need to get its byte structure by invoking getEncoded() method and convert the bytes to hexadecimal using BigInteger.

We can use formatHex(byte[] bytes) in HexFormat class to convert array of bytes to hex. However, HexFormat is introduced in java 17. Java versions older than 17 can't use HexFormat.

Now, We need to store the hash of the key in hexadecimal form, salt, iteration count and length of the key because during verification, we need those values. Now, assume that the user is gonna login to our program.

To verify that the password he/she sends to our program is the same as the password he/she sent to our program, we need to create a key with the stored salt, iteration count and key length. Then, get the hash of the new key and compare the new hash to the stored hash. If they're both the same then the user can access his/her account.

No comments:

Post a Comment