Chapters
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
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
The method returns Provider objects that hold information about cryptography providers. If you want to add new provider to your platform, invoke
For example:
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
In JDK9 and above, the file is located in the directory where you installed java at
Once you open
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.
However,
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
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
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
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.
Available algrorithms for
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.
Cipher provides the functionality of a cryptographic cipher for encryption and decryption. We use
A
A transformation string can be one of these:
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:
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.
Let's focus on encrypting and decrypting data. To initialize a Cipher object in "encrypt" mode, put
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.
In the example above, the Cipher object uses
From the documentation:
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.
RSA is a block cipher and it requires a padding parameter. Fortunately, java automatically provided a padding parameter for RSA when we invoke
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 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.
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
The limitation length is based on the
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.
The process that I've explained can be simplified in java. We use Signature to hash and encrypt/decrypt it.
"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.
In other words,
In other words,
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 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.
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
If you want more security at the expense of performance, use hash algorithm with higher block size like
Mac class provides the functionality of a "Message Authentication Code" (MAC) algorithm.
Key wrapping is a process of encrypting a key using another key. This example demonstrates wrapping/unwrapping symmetric key using asymmetric keys.
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
Next, to unwrap the key, we invoke
This method unwraps a wrapped key.
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.
This example demonstrates storing key hash in a file and recreating the key in java.
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.
Another way of storing keys is to create a KeyStore.
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.
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.
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
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.
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.
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.
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.
In the example above, I use "PBKDF2WithHmacSHA512" algorithm. This algorithm uses PBKDF2 with HMAC with SHA512.
Once we successfully generated a
We can use
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.
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 bitsMessageDigest 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? trueThe 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"
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 andTake note that we can encrypt files using symmetric encryption. Take a look at this example.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 anInvalidAlgorithmParameterException
if it is being initialized for decryption or key unwrapping. The generated parameters can be retrieved usinggetParameters
orgetIV
(if the parameter is an IV).
... 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
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? trueIn 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? trueIn 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_KEYThese keys are keys for asymmetric encryption.
Cipher.SECRET_KEYThis 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? trueIn 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