Hi, Habr! I present to your attention the translation of the article "Java Cryptography" by Jakob Jenkov.
This publication is a translation of the first Java Cryptography article in a series of articles for beginners who want to learn the basics of cryptography in Java.
The Java Cryptography API provides the ability to encrypt and decrypt data in java, as well as manage keys, signatures and authenticate (authenticate) messages, calculate cryptographic hashes, and more.
This article explains the basics of how to use the Java Cryptography API to perform various tasks that require secure encryption.
This article does not explain the basics of cryptographic theory. You will have to look at this information somewhere else.
The Java cryptography API is provided by the so-called Java Cryptography Extension (JCE). JCE has long been part of the Java platform. Initially, JCE was separated from Java due to the fact that the United States had export restrictions on encryption technology. Therefore, the most robust encryption algorithms were not included in the standard Java platform. These more robust encryption algorithms can be used if your company is located in the USA, but in other cases you will have to use weaker algorithms or implement your own encryption algorithms and connect them to the JCE.
Since 2017, the rules for exporting encryption algorithms to the United States have been significantly weakened and international encryption standards through Java JCE can be used in most parts of the world.
Java cryptography architecture
Java Cryptography Architecture (JCA) is the name of the internal cryptography API design in Java. JCA is structured around several main classes and general purpose interfaces. The real functionality of these interfaces is provided by the suppliers. Thus, the Cipher class can be used to encrypt and decrypt some data, but the specific implementation of the cipher (encryption algorithm) depends on the particular provider used.
You can also implement and connect your own providers, but you have to be careful with this. Correctly implement encryption without security holes is difficult! If you don’t know what you’re doing, it’s probably best for you to use the built-in Java provider or use a reliable provider such as Bouncy Castle.
The Java cryptography API consists of the following Java packages:
The main classes and interfaces of these packages are:
The Provider class (java.security.Provider) is the central class in the Java crypto API. In order to use the Java crypto API, you need to install a cryptographic provider. The Java SDK comes with its own cryptographic provider. Unless you explicitly install a cryptographic provider, the default provider will be used. However, this cryptographic provider may not support the encryption algorithms you want to use. Therefore, you may need to install your own cryptographic provider.
One of the most popular cryptographic providers for the Java crypto API is called Bouncy Castle. Here is an example where the BouncyCastleProvider is set up as a cryptographic provider:
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class ProviderExample { public static void main(String[] args) { Security.addProvider(new BouncyCastleProvider()); } }
The Cipher class (javax.crypto.Cipher) represents a cryptographic algorithm. The cipher can be used to both encrypt and decrypt data. The Cipher class is explained in more detail in the following sections, below is a brief description.
Creating an instance of a cipher class that uses the AES encryption algorithm for internal use:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
The Cipher.getInstance (...) method takes a string that determines which encryption algorithm to use, as well as some other algorithm parameters.
In the example above:
Before using a cipher instance, it must be initialized. The cipher instance is initialized by calling the init () method. The init () method takes two parameters:
The first parameter specifies whether the instance works with a cipher: encrypt or decrypt data. The second parameter indicates which key they use to encrypt or decrypt data.
Example:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; String algorithm = "RawBytes"; SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm); cipher.init(Cipher.ENCRYPT_MODE, key);
Note that the key creation method in this example is not secure and should not be used in practice. This article in the following sections will explain how to create keys more securely.
To initialize a cipher instance to decrypt data, you must use Cipher.DECRYPT_MODE, for example:
cipher.init(Cipher.DECRYPT_MODE, key);
After initializing the cipher, you can start encrypting or decrypting the data by calling the update () or doFinal () methods. The update () method is used if you are encrypting or decrypting a piece of data. The doFinal () method is called when you encrypt the last piece of data, or if the data block that you pass to doFinal () is a single data set for encryption.
An example of data encryption using the doFinal () method:
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8"); byte[] cipherText = cipher.doFinal(plainText);
To decrypt data, you must pass the encrypted text (data) to the doFinal () or doUpdate () method.
To encrypt or decrypt data, you need a key. There are two types of keys depending on what type of encryption algorithm is used:
Symmetric keys are used for symmetric encryption algorithms. Symmetric encryption algorithms use the same key for encryption and decryption.
Asymmetric keys are used for asymmetric encryption algorithms. Asymmetric encryption algorithms use one key for encryption, and another for decryption. Public-key and private-key encryption algorithms are examples of asymmetric encryption algorithms.
Somehow, the party that needs to decrypt the data must know the key necessary to decrypt the data. If the decrypting party is not a party to encrypting the data, the two parties must agree on the key or exchange the key. This is called key exchange.
Keys must be hard to guess so that an attacker cannot easily find the encryption key. In the example from the previous section on the Cipher class, a very simple, hard-coded key was used. In practice, this is not worth doing. If the key of the parties is easy to guess, it will be easy for an attacker to decrypt the encrypted data and, possibly, create fake messages on its own. It is important to make a key that is difficult to guess. Thus, the key must consist of random bytes. The more random bytes, the harder it is to guess, because there are more possible combinations.
To generate random encryption keys, you can use the Java KeyGenerator class. KeyGenerator will be described in more detail in the following chapters; here is a small example of its use here:
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); SecureRandom secureRandom = new SecureRandom(); int keyBitSize = 256; keyGenerator.init(keyBitSize, secureRandom); SecretKey secretKey = keyGenerator.generateKey();
The resulting SecretKey instance can be passed to the Cipher.init () method, for example:
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
Asymmetric encryption algorithms use a key pair, consisting of a public key and a private key, to encrypt and decrypt data. To create an asymmetric key pair, you can use KeyPairGenerator (java.security.KeyPairGenerator). KeyPairGenerator will be described in more detail in the following chapters, below is a simple example of using Java KeyPairGenerator:
SecureRandom secureRandom = new SecureRandom(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); KeyPair keyPair = keyPairGenerator.generateKeyPair();
Java KeyStore is a database that can contain keys. Java KeyStore is represented by the KeyStore class (java.security.KeyStore). The keystore can contain keys of the following types:
Private and public keys are used in asymmetric encryption. The public key may have an associated certificate. A certificate is a document proving the identity of a person, organization or device claiming ownership of a public key.
The certificate is usually digitally signed by the verifier as evidence.
Secret keys are used in symmetric encryption. The KeyStore class is quite complex, so it is described in more detail later in a separate chapter on Java KeyStore.
Java Keytool is a command line tool that can work with Java KeyStore files. Keytool can generate key pairs to a KeyStore file, export certificates, and import certificates to KeyStore and some other functions. Keytool comes with a Java installation. Keytool is described in more detail later in a separate chapter on Java Keytool.
When you receive encrypted data from the other side, can you be sure that no one has changed the encrypted data on the way to you?
The usual solution is to compute the digest of the message from the data before encrypting it, and then encrypt both the data and the digest of the message and send it over the network. Message digest is a hash value calculated from the message data. If at least one byte changes in the encrypted data, the message digest calculated by the data will also change.
When you receive the encrypted data, you decrypt it, compute the message digest from it, and compare the calculated message digest with the message digest sent along with the encrypted data. If the two digest messages are the same, there is a high probability (but not 100%) that the data has not been changed.
Java MessageDigest (java.security.MessageDigest) can be used to calculate message digests. To create an instance of MessageDigest, the MessageDigest.getInstance () method is called. There are several different message digest algorithms. You need to specify which algorithm you want to use when creating a MessageDigest instance. Working with MessageDigest will be described in more detail in the Java chapter on MessageDigest.
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
This example creates an instance of MessageDigest that uses the internal cryptographic hash algorithm SHA-256 to compute message digests.
To compute a digest of some data, you call the update () or digest () method. The update () method can be called several times, and the message digest is updated inside the object. When you send all the data you want to include in the message digest, you call digest () and extract the message digest totals.
An example of calling update () several times followed by a call to digest () :
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] data1 = "0123456789".getBytes("UTF-8"); byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); messageDigest.update(data1); messageDigest.update(data2); byte[] digest = messageDigest.digest();
You can also call digest () once, passing all the data to calculate the message digest. Example:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] data1 = "0123456789".getBytes("UTF-8"); byte[] digest = messageDigest.digest(data1);
The Mac Java class is used to create a MAC (Message Authentication Code) from a message. The MAC is similar to message digest, but uses an additional key to encrypt message digest. Only with both the original data and the key, you can check the MAC. Thus, MAC is a more secure way to protect a data block from modification than message digest. The Mac class is described in more detail in the Mac Java chapter, with a brief introduction below.
A Java Java instance is created by calling the Mac.getInstance () method, passing the name of the algorithm to use as a parameter. Here's what it looks like:
Mac mac = Mac.getInstance("HmacSHA256");
Before you create a MAC from data, you must initialize your Mac instance with the key. Here is an example of initializing an instance of a Mac with a key:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8 ,9,10,11,12,13,14,15}; String algorithm = "RawBytes"; SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm); mac.init(key);
After initializing the Mac instance, you can calculate the MAC from the data by calling the update () and doFinal () methods. If you have all the data to calculate the MAC, you can immediately call the doFinal () method. Here's what it looks like:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); byte[] data2 = "0123456789".getBytes("UTF-8"); mac.update(data); mac.update(data2); byte[] macBytes = mac.doFinal();
The Signature class (java.security.Signature) is used to digitally sign data. When the data is signed, a digital signature is created from this data. Thus, the signature is separated from the data.
A digital signature is created by creating a message digest (hash) from the data and encrypting that message digest using the private key of the device, person or organization that is supposed to sign the data. The digest of the encrypted message is called a digital signature.
To create a Signature instance, the Signature.getInstance (...) method is called:
Signature signature = Signature.getInstance("SHA256WithDSA");
To sign data, you must initialize the signature instance in signature mode by calling the initSign (...) method, passing the private key to sign the data. An example of initializing a signature instance in signature mode:
signature.initSign(keyPair.getPrivate(), secureRandom);
After initializing the signature instance, you can use it to sign the data. This is done by calling the update () method, passing the data for the signature as a parameter. You can call the update () method several times to add data to create a signature. After all the data has been transferred to the update () method, the sign () method is called to obtain a digital signature. Here's what it looks like:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature.update(data); byte[] digitalSignature = signature.sign();
To verify a signature, you need to initialize the signature instance in verification mode by calling the initVerify (...) method, passing the public key as a parameter to verify the signature. An example of initialization of the signature instance in the scan mode looks like:
Signature signature = Signature.getInstance("SHA256WithDSA"); signature.initVerify(keyPair.getPublic());
After initialization in verification mode, the data that is signed is transferred to the update () method. Calling the verify () method returns true or false depending on whether the signature can be verified or not. Here is the signature verification:
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature2.update(data2); boolean verified = signature2.verify(digitalSignature);
SecureRandom secureRandom = new SecureRandom(); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); KeyPair keyPair = keyPairGenerator.generateKeyPair(); Signature signature = Signature.getInstance("SHA256WithDSA"); signature.initSign(keyPair.getPrivate(), secureRandom); byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8"); signature.update(data); byte[] digitalSignature = signature.sign(); Signature signature2 = Signature.getInstance("SHA256WithDSA"); signature2.initVerify(keyPair.getPublic()); signature2.update(data); boolean verified = signature2.verify(digitalSignature); System.out.println("verified = " + verified);
Source: https://habr.com/ru/post/444764/
All Articles