Security of data exchange is one of the most important properties of modern applications. Since ancient times, people have come up with cunning methods, which with the development of mankind became the whole science of Cryptography. Naturally, Java did not stand aside and offered developers the Java Cryptography Architecture (JCA). This review should give a first idea of how it works.
What could this mean:
As the documentation says, " The Java platform includes a number of built-in providers ". That is, the Java platform provides a set of built-in providers that can be expanded if necessary. You can see this for yourself:
It might look like this in code:
To avoid replay in this case, you should use another mode - Cipher Block Chaining (CBC). This mode introduces the concept of Initialization Vector (represented by the IvParameterSpec class). And also thanks to this mode, the result of generating the last block will be used to generate the next one:
Let's now write this in code:
Above we saw how the parties exchange data. Isn't there some standard interface for this interaction provided in JCA? It turns out that there is. Let's look at it.
Preface
I propose to travel back in time. Before us is Ancient Rome. And before us is Gaius Julius Caesar, who sends a message to his commanders. Let's see what's in this message:"ЕСКЕУГЬГМХИФЯ Е УЛП"
? Let's open Java Online Compiler, for example: repl.it
class Main {
public static void main(String[] args) {
String code = "ЕСКЕУГЬГМХИФЯ Е УЛП";
for (char symbol : code.toCharArray()) {
if (symbol != ' ') {
symbol = (char) (symbol - 3);
}
System.out.print(symbol);
}
}
}
Before us is the simplest implementation of the Caesar Cipher. According to the work of the ancient Roman historian Suetonius entitled “The Lives of the Twelve Caesars,” this is exactly how Caesar encrypted messages to his generals. And this is one of the most ancient references to the use of such a thing as Cryptography . The word "cryptography" comes from the ancient Greek words "hidden" and "write", i.e. it is the science of privacy techniques. Java has its own support for cryptography and it is called Java Cryptography Architecture (JCA). The description can be found in the official documentation from Oracle - " Java Cryptography Architecture (JCA) ". I suggest you look at what opportunities we get thanks to JCA.
J.C.A.
As we previously learned, Java offers the Java Cryptography Architecture (JCA) for working with cryptography. This architecture contains an API (i.e. a certain set of interfaces) and providers (that implement them):import java.security.Provider;
import java.security.Security;
class Main {
public static void main(String[] args) {
Provider[] providers = Security.getProviders();
for (Provider p : providers) {
System.out.println(p.getName());
}
}
}
Registering a third party provider is very easy. For example: Security.addProvider(new BouncyCastleProvider());
This example connects one of the most famous providers - BouncyCastle . But in this review we will use only basic tools, without third-party libraries. Our main document: " Java Cryptography Architecture (JCA) ". Understanding how JCA works will help you more easily understand the technologies in which this same JCA is actively used. For example: HTTPS (see " From HTTP to HTTPS ").
MessageDigest
The first thing mentioned in the JCA documentation is MessageDigest. In general, Digest in Russian will be the same - a digest corresponds in meaning to “a summary”. But in cryptography, a digest is a hash sum. You can also easily remember that in English Digest can also be translated as digest. More details can be found in the JCA documentation in the " MessageDigest " section. As the documentation says, MessageDigest generates a fixed-size result called digest or hash. Hashing is a one-way function, i.e. if we hashed something, then from the result (i.e. from the hash) we cannot get the original source. But if identical objects are hashed (for example, strings of identical characters), then their hash must match. As stated in the documentation, such a hash is sometimes also called a “checksum” or “digital fingerprint” of data. Hashing can be performed using different algorithms. Available algorithms can be viewed in the document " Java Cryptography Architecture Standard Algorithm Name Documentation for JDK 8 ". Let's do the hashing and print the hash to the console:import javax.xml.bind.DatatypeConverter;
import java.security.*;
public class Main {
public static void main(String[] args) {
try {
MessageDigest digester = MessageDigest.getInstance("SHA-512");
byte[] input = "Secret string".getBytes();
byte[] digest = digester.digest(input);
System.out.println(DatatypeConverter.printHexBinary(digest));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
Hashing can be useful, for example, when storing passwords. Since the hash of the entered password can be checked against a previously saved hash. If the hashes match, then the password also matches. For even more secure hashing, a concept called “salt” is used. Salt can be implemented using the SecureRandom class . Before executing the digest method, let's describe adding "salt":
byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt);
digester.update(salt);
But hash is a one-way function. But what if you want to be able to encrypt and decrypt?
Symmetric key cryptography
Symmetric encryption is encryption that uses the same key for encryption and decryption. In order to use symmetric encryption we need a key. To get it we use KeyGenerator . In addition, we will need a class that represents a cipher ( Cipher ). As stated in the JCA documentation in the section “ Creating a Cipher Object ”, to create a Cipher you need to specify not just an algorithm, but a “transformation” in the line. The transformation description looks like this: "algorithm/mode/padding":- Algorithm : here we look at the standard names for “ Cipher (Encryption) Algorithms ”. It is recommended to use AES.
- Mode : encryption mode. For example: ECB or CBC (we'll talk about this a little later)
- Indentation/Split : Each block of data is encrypted separately. This parameter determines how much data is counted as 1 block.
"AES/ECB/PKCS5Padding"
. That is, the encryption algorithm is AES, the encryption mode is ECB (short for Electronic Codebook), the block size is PKCS5Padding. PKCS5Padding says that the size of one block is 2 bytes (16 bits). The Electronic Codebook encryption mode involves sequential encryption of each block:
import javax.xml.bind.DatatypeConverter;
import javax.crypto.*;
import java.security.Key;
public class Main {
public static void main(String[] args) throws Exception {
String text = "secret!!secret!!secret!!secret!!";
// Generate new key
KeyGenerator keygen = KeyGenerator.getInstance("AES");
keygen.init(256);
Key key = keygen.generateKey();
// Encrypt with key
String transformation = "AES/ECB/PKCS5Padding";
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(text.getBytes());
System.out.println(DatatypeConverter.printHexBinary(encrypted));
// Decrypt with key
cipher.init(Cipher.DECRYPT_MODE, key);
String result = new String(cipher.doFinal(encrypted));
System.out.println(result);
}
}
If we execute, we will expect to see a repeat, because we specified 32 characters. These characters make up 2 blocks of 16 bits:
import javax.xml.bind.DatatypeConverter;
import javax.crypto.*;
import java.security.*;
import javax.crypto.spec.IvParameterSpec;
public class Main {
public static void main(String[] args) throws Exception {
// Initialization Vector
SecureRandom random = SecureRandom.getInstanceStrong();
byte[] rnd = new byte[16];
random.nextBytes(rnd);
IvParameterSpec ivSpec = new IvParameterSpec(rnd);
// Prepare key
KeyGenerator keygen = KeyGenerator.getInstance("AES");
keygen.init(256);
Key key = keygen.generateKey();
// CBC
String text = "secret!!secret!!secret!!secret!!";
String transformation = "AES/CBC/PKCS5Padding";
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] enc = cipher.doFinal(text.getBytes());
System.out.println(DatatypeConverter.printHexBinary(enc));
// Decrypt
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
String result = new String(cipher.doFinal(enc));
System.out.println(result);
}
}
As we see, as a result we do not see repeated cipher blocks. For this reason, ECB mode is not recommended, because makes it possible to see repetitions and use this knowledge for decryption. For more information about ECB and CBC, I advise you to read the material: “ Electronic codebook mode ”. But symmetric encryption has an obvious problem - you need to somehow transfer the key from the one who encrypts to the one who encrypts. And along this path, this key can be intercepted and then it will be possible to intercept data. And asymmetric encryption is designed to solve this problem.
Asymmetric encryption
Asymmetric encryption or Public-key cryptography is an encryption method that uses a pair of keys: a private key (kept secret from everyone) and a public key (available to the public). This separation is necessary in order to securely exchange the public key between the parties to the exchange of information, while keeping the secret key safe. When creating a key pair, KeyGenerator is no longer enough for us; we need KeyPairGenerator . Let's look at an example:import javax.crypto.*;
import java.security.*;
public class Main {
public static void main(String[] args) throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(1024);
KeyPair keyPair = generator.generateKeyPair();
// Encrypt with PRIVATE KEY
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] data = cipher.doFinal("Hello!".getBytes());
// Decrypt with PUBLIC KEY
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte[] result = cipher.doFinal(data);
System.out.println(new String(result));
}
}
It is important to understand here that when using asymmetric encryption, we always use KeyPair to use one key for encryption and another for decryption. But because The point of encryption is that only the recipient can decrypt it; it is encrypted with a public key, and decrypted only with a private one.
Digital signature
As we saw above, knowing the public key, you can send data so that only the owner of the private key can decrypt it. That is, the essence of asymmetric encryption is that anyone encrypts, but only we read. There is also a reverse procedure - a digital signature, represented by the Signature class . A digital signature can use the following algorithms: " Signature Algorithms ". The JCA documentation suggests taking a closer look at these two: DSAwithMD5 and RSAwithMD5 What is better than DSA or RSA and what is their difference you can read here: " Which Works Best for Encrypted File Transfers - RSA or DSA? ". Or read the discussions here: " RSA vs. DSA for SSH authentication keys ". So, digital signature. We will need, as before, a KeyPair and a new Signature class. If you have so far tested in online compilers, then the following example may be somewhat difficult for them. My example only ran here: rextester.com . We import the classes we need:import javax.crypto.*;
import java.security.*;
We’ll also rewrite the main method:
public static void main(String[] args) throws Exception {
// Generate keys
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
SecureRandom random = SecureRandom.getInstanceStrong();
generator.initialize(2048, random);
KeyPair keyPair = generator.generateKeyPair();
// Digital Signature
Signature dsa = Signature.getInstance("SHA256withRSA");
dsa.initSign(keyPair.getPrivate());
// Update and sign the data
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] data = cipher.doFinal("Hello!".getBytes());
dsa.update(data);
byte[] signature = dsa.sign();
// Verify signature
dsa.initVerify(keyPair.getPublic());
dsa.update(data);
boolean verifies = dsa.verify(signature);
System.out.println("Signature is ok: " + verifies);
// Decrypt if signature is correct
if (verifies) {
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte[] result = cipher.doFinal(data);
System.out.println(new String(result));
}
}
This is how a digital signature works. Digital signature is an interesting topic. I advise you to look at the report on this topic:
KeyAgreement
The Java Cryptography Architecture introduces an important tool - Key agreement is a protocol. It is represented by the KeyAgreement class . As stated in the JCA documentation, this protocol allows you to set the same cryptographic key for multiple parties, while no secret information is transferred between the parties. Sounds weird? Then let's look at an example:// 1. Одна из сторон (Алиса) генерирует пару ключей. Encoded публичный ключ отдаёт.
KeyPairGenerator generator = KeyPairGenerator.getInstance("DH");
KeyPair aliceKeyPair = generator.generateKeyPair();
byte[] alicePubKeyEncoded = aliceKeyPair.getPublic().getEncoded();
// 2. Другая сторона (например, Боб) получает открытый ключ Алисы
KeyFactory bobKeyFactory = KeyFactory.getInstance("DH");
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(alicePubKeyEncoded);
PublicKey alicePubKey = bobKeyFactory.generatePublic(x509KeySpec);
// Параметры, которые использовала Алиса при генерации ключей
DHParameterSpec dhParamFromAlicePubKey = ((DHPublicKey)alicePubKey).getParams();
// Создаёт свою пару ключей. Отдаёт свой Encoded открытый ключ
KeyPairGenerator bobKpairGen = KeyPairGenerator.getInstance("DH");
bobKpairGen.initialize(dhParamFromAlicePubKey);
KeyPair bobKeyPair = bobKpairGen.generateKeyPair();
byte[] bobPubKeyEncoded = bobKeyPair.getPublic().getEncoded();
Теперь, у Алисы есть открытый ключ Боба, а у Боба есть открытый ключ Алисы. What дальше?
Как сказано в documentации JCA, у нас есть инструмент KeyAgreement, https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html#KeyAgreement который позволяет установить одинаковые ключи шифрования без необходимости обмениваться секретной информацией (т.е. без обмена private key). Соглашение выглядит следующим образом:
// 3. Соглашение по протоколу Диффи-Хеллмана (Diffie–Hellman, DH)
KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("DH");
aliceKeyAgree.init(aliceKeyPair.getPrivate());
// Алиса на основе ключа боба и своего private key создаёт общий shared ключ
KeyFactory aliceKeyFactory = KeyFactory.getInstance("DH");
x509KeySpec = new X509EncodedKeySpec(bobPubKeyEncoded);
PublicKey bobPubKey = aliceKeyFactory.generatePublic(x509KeySpec);
aliceKeyAgree.doPhase(bobPubKey, true);
byte[] aliceSharedSecret = aliceKeyAgree.generateSecret();
SecretKeySpec aliceAesKey = new SecretKeySpec(aliceSharedSecret, 0, 16, "AES");
// Боб на основе ключа Алисы и своего private key создаёт общий shared ключ
KeyAgreement bobKeyAgree = KeyAgreement.getInstance("DH");
bobKeyAgree.init(bobKeyPair.getPrivate());
bobKeyAgree.doPhase(alicePubKey, true);
byte[] bobSharedSecret = bobKeyAgree.generateSecret();
SecretKeySpec bobAesKey = new SecretKeySpec(bobSharedSecret, 0, 16, "AES");
// Общий ключ у Алисы и Боба одинаков
System.out.println("Shared keys are equals: " + Arrays.equals(aliceSharedSecret, bobSharedSecret));
Далее Боб и Алиса, используя общий ключ, про который больше никто не знает, обмениваются зашифрованными данными:
// 4. Боб шифрует сообщение для Алисы
Cipher bobCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
bobCipher.init(Cipher.ENCRYPT_MODE, bobAesKey);
byte[] ciphertext = bobCipher.doFinal("Hello, Alice!".getBytes());
// Передаёт Алисе параметры, с которыми выполнялась шифровка
byte[] encodedParamsFromBob = bobCipher.getParameters().getEncoded();
// 5. Алиса принимает сообщение и расшифровывает его
AlgorithmParameters aesParams = AlgorithmParameters.getInstance("AES");
aesParams.init(encodedParamsFromBob);
Cipher aliceCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
aliceCipher.init(Cipher.DECRYPT_MODE, aliceAesKey, aesParams);
byte[] recovered = aliceCipher.doFinal(ciphertext);
System.out.println(new String(recovered));
This example was taken from the JCA documentation example: " Diffie-Hellman Key Exchange between 2 Parties ". This is roughly what asymmetric encryption looks like in the Java Cryptography Architecture using the Key agreement protocol. For more information about asymmetric encryption, recommended videos:
Certificates
Well, for dessert we still have something no less important - certificates. Typically, certificates are generated using the keytool utility included with the jdk. You can read more details, for example, here: " Generating a self-signed SSL certificate using the Java keytool command ". You can also read the manuals from Oracle. For example, here: " To Use keytool to Create a Server Certificate ". For example, let's use Tutorialspoint Java Online Compiler :import sun.security.tools.keytool.CertAndKeyGen;
import sun.security.x509.*;
import java.security.cert.*;
import java.security.*;
// Compiler args: -XDignore.symbol.file
public class Main {
public static void main(String[] args) throws Exception {
CertAndKeyGen certGen = new CertAndKeyGen("RSA", "SHA256WithRSA", null);
// generate it with 2048 bits
certGen.generate(2048);
PrivateKey privateKey = certGen.getPrivateKey();
X509Key publicKey = certGen.getPublicKey();
// prepare the validity of the certificate
long validSecs = (long) 365 * 24 * 60 * 60; // valid for one year
// enter your details according to your application
X500Name principal = new X500Name("CN=My Application,O=My Organisation,L=My City,C=DE");
// add the certificate information, currently only valid for one year.
X509Certificate cert = certGen.getSelfCertificate(principal, validSecs);
// Public Key from Cert equals Public Key from generator
PublicKey publicKeyFromCert = cert.getPublicKey();
System.out.println(publicKeyFromCert.equals(publicKey));
}
}
As we can see, a certificate provides the ability to provide a public key. This method has a drawback - we use sun.security
, which is considered risky, because... this package is not part of the public Java API. That is why during compilation it is necessary to specify the parameter - XDignore.symbol.file
. There is another way - to create a certificate manually. The downside is that it uses an internal API that is not documented. However, it is useful to know about it. At a minimum, because it is clearly visible how the RFC-2459 specification is used: “ Internet X.509 Public Key Infrastructure ”. Here's an example:
// 1. Генерируем пару ключей
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(4096);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 2. Определяем данные сертификата
// Определяем срок действия сертификата
Date from = new Date();
Date to = new Date(from.getTime() + 365 * 1000L * 24L * 60L * 60L);
CertificateValidity interval = new CertificateValidity(from, to);
// Определяем subject name, т.е. Name того, с чем ассоциирован публичный ключ
// CN = Common Name. Через точку с запятой могут быть указаны также другие атрибуты
// См. https://docs.oracle.com/cd/E24191_01/common/tutorials/authz_cert_attributes.html
X500Name owner = new X500Name("cn=Unknown");
// Уникальный в пределах CA, т.е. Certificate Authority (тот, кто выдаёт сертификат) номер
BigInteger number = new BigInteger(64, new SecureRandom());
CertificateSerialNumber serialNumber = new CertificateSerialNumber(number);
// Определяем алгоритм подписи сертификата
AlgorithmId algorithmId = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
CertificateAlgorithmId certificateAlgorithmId = new CertificateAlgorithmId(algorithmId);
// 3. По подготовленной информации создаём сертификат
X509CertInfo info = new X509CertInfo();
info.set(X509CertInfo.VALIDITY, interval);
info.set(X509CertInfo.SERIAL_NUMBER, serialNumber);
info.set(X509CertInfo.SUBJECT, owner);
info.set(X509CertInfo.ISSUER, owner);
info.set(X509CertInfo.KEY, new CertificateX509Key(keyPair.getPublic()));
info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
info.set(X509CertInfo.ALGORITHM_ID, certificateAlgorithmId);
// 4. Подписываем сертификат
X509CertImpl certificate = new X509CertImpl(info);
certificate.sign(keyPair.getPrivate(), "SHA256withRSA");
// 5. Проверка сертификата
try {
// В случае ошибки здесь будет брошено исключение. Например: java.security.SignatureException
certificate.verify(keyPair.getPublic());
} catch (Exception e) {
throw new IllegalStateException(e);
}
Keystore (KeyStore)
The last thing I would like to talk about is the key and certificate store, which is called KeyStore. It is clear that constantly generating certificates and keys is expensive and pointless. Therefore, they need to be stored somehow safely. There is a tool for this - KeyStore. The key store is described in the JCA documentation in the " KeyManagement " chapter. The API for working with it is very clear. Here's a small example:KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
String alias = "EntityAlias";
java.security.cert.Certificate[] chain = {certificate};
keyStore.setKeyEntry(alias, keyPair.getPrivate(), "keyPassword".toCharArray(), chain);
// Загрузка содержимого (Private Key + Certificate)
Key key = keyStore.getKey(alias, "keyPassword".toCharArray());
Certificate[] certificateChain = keyStore.getCertificateChain(alias);
// Сохранение KeyStore на диск
File file = File.createTempFile("security_", ".ks");
System.out.println(file.getAbsolutePath());
try (FileOutputStream fos = new FileOutputStream(file)) {
keyStore.store(fos, "keyStorePassword".toCharArray());
}
As you can see from the example, it is executed first load
for the KeyStore. But in our case, we specified the first attribute as null, i.e. there is no source for KeyStore. This means the KeyStore is created empty in order to save it further. The second parameter is also null, because we are creating a new KeyStore. If we were loading KeyStore from a file, then we would need to specify a password here (similar to the KeyStore method called store).