Encryption, Part 3: Hybrid Encryption
In part one of the series, I covered the topic of symmetric encryption (SE). While in part two, I covered public/ private key encryption, also known as public key encryption (PKE). The limitation of symmetric encryption is that the key used for encryption and decryption is one and the same. Hence, the key needs to be kept a secret. PKE overcomes the problem of key sharing by using two keys — one private and one public. To share a message, it is typically encrypted using the public key. Then, the private key counterpart of the public key can be used to decrypt the message. Thus, PKE achieves security.
But using PKE for large messages is very difficult for two reasons. The first reason is that the longer text that can be encrypted in one go has to be less than the length of the key used for encryption. The second reason is that PKE is more time consuming, compared to symmetric encryption. The third reason why PKE is not used for encrypting long messages is that after encryption, the length of the encrypted text is much larger than the length of the original text.
Hybrid Encryption
Hybrid encryption — a mashup of SE and PKE — provides an elegant solution that preserves the speed of SE, while maintaining the security and exchange flexibility of PKE. We also do not end up increasing the length of the text message to be encrypted.
The idea of hybrid encryption is quite simple. Instead of using PKE to encrypt the text, we use SE to encrypt the message. Then, they maintain the secrecy of the key, and we encrypt the key using PKE. The steps of hybrid encryption are:
- Generate a symmetric key. The symmetric key needs to be kept a secret.
- Encrypt the data using the secret symmetric key.
- The person to whom we wish to send a message will share her public key and keep the private key a secret.
- Encrypt the symmetric key using the public key of the receiver.
- Send the encrypted symmetric key to the receiver.
- Send the encrypted message text.
- The receiver decrypts the encrypted symmetric key using her private key and gets the symmetric key needed for decryption.
- The receiver uses the decrypted symmetric key to decrypt the message, getting the original message.
In the hybrid method, the data to be encrypted is not limited by the length of the encryption key. Additionally, the encrypted symmetric key is secure because it is encrypted using the public key of the receiver. Even if the encrypted data and the encrypted key are intercepted by another person, they will not be able to get the original data as long as the private key is maintained as a secret.
Sample Application in Java
Below is example Java code that illustrates the HE method:
package edh;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class EncDeHybrid
{
// Symmetric encryption algorithms supported - AES, RC4, DES
// encryption algorithm - DES, key size - 56
protected static String DEFAULT_ENCRYPTION_ALGORITHM = "AES";
protected static int DEFAULT_ENCRYPTION_KEY_LENGTH = 256;
// key encryption algorithms supported - RSA, Diffie-Hellman, DSA
// key pair generator - RSA: keyword - RSA, key size: 1024, 2048
// key pair generator - Diffie-Hellman: keyword i DiffieHellman, key size - 1024
// key pair generator - DSA: keyword - DSA, key size: 1024
// NOTE: using asymmetric algorithms other than RSA needs to be worked out
protected static String DEFAULT_KEY_ENCRYPTION_ALGORITHM = "RSA";
protected static int DEFAULT_KEY_ENCRYPTION_KEY_LENGTH = 1024;
protected static String DEFAULT_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
protected SecretKey mSecretKey;
protected String mEncryptionAlgorithm, mKeyEncryptionAlgorithm, mTransformation;
protected int mEncryptionKeyLength, mKeyEncryptionKeyLength;
protected PublicKey mPublicKey;
protected PrivateKey mPrivateKey;
EncDeHybrid()
{
mSecretKey = null;
mEncryptionAlgorithm = EncDeHybrid.DEFAULT_ENCRYPTION_ALGORITHM;
mEncryptionKeyLength = EncDeHybrid.DEFAULT_ENCRYPTION_KEY_LENGTH;
mKeyEncryptionAlgorithm = EncDeHybrid.DEFAULT_KEY_ENCRYPTION_ALGORITHM;
mKeyEncryptionKeyLength = EncDeHybrid.DEFAULT_KEY_ENCRYPTION_KEY_LENGTH;
mPublicKey = null;
mPrivateKey = null;
mTransformation = EncDeHybrid.DEFAULT_TRANSFORMATION;
}
EncDeHybrid(String encAlgo, int encKeyLength, String keyEncAlgo, int keyEncKeyLength, String transformation)
{
mSecretKey = null;
mEncryptionAlgorithm = encAlgo;
mEncryptionKeyLength = encKeyLength;
mKeyEncryptionAlgorithm = keyEncAlgo;
mKeyEncryptionKeyLength = keyEncKeyLength;
mTransformation = transformation;
}
public static BigInteger keyToNumber(byte[] byteArray)
{
return new BigInteger(1, byteArray);
}
public SecretKey getSecretKey()
{
return mSecretKey;
}
public byte[] getSecretKeyAsByteArray()
{
return mSecretKey.getEncoded();
}
// get base64 encoded version of the key
public String getEncodedSecretKey()
{
String encodedKey = Base64.getEncoder().encodeToString(mSecretKey.getEncoded());
return encodedKey;
}
// decode the base64 encoded string
public SecretKey getDecodedSecretKey(String encodedKey, String algo)
{
byte[] decodedKey = Base64.getDecoder().decode(encodedKey);
// rebuild key using SecretKeySpec
SecretKey originalKey = null;
if ( null == algo ) {
originalKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, mEncryptionAlgorithm);
} else {
originalKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, algo);
}
return originalKey;
}
public PublicKey getPublicKey()
{
return mPublicKey;
}
public byte[] getPublicKeyAsByteArray()
{
return mPublicKey.getEncoded();
}
public String getEncodedPublicKey()
{
String encodedKey = Base64.getEncoder().encodeToString(mPublicKey.getEncoded());
return encodedKey;
}
public PrivateKey getPrivateKey()
{
return mPrivateKey;
}
public byte[] getPrivateKeyAsByteArray()
{
return mPrivateKey.getEncoded();
}
public String getEncodedPrivateKey()
{
String encodedKey = Base64.getEncoder().encodeToString(mPrivateKey.getEncoded());
return encodedKey;
}
// step 1 -- generate the symmetric key
public void generateSymmetricKey()
{
KeyGenerator generator;
try {
generator = KeyGenerator.getInstance(mEncryptionAlgorithm);
generator.init(mEncryptionKeyLength);
mSecretKey = generator.generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
// step 2 -- encrypt the plain text
public byte[] encryptText(String textToEncrypt)
{
byte[] byteCipherText = null;
try {
Cipher encCipher = Cipher.getInstance(mEncryptionAlgorithm);
encCipher.init(Cipher.ENCRYPT_MODE, mSecretKey);
byteCipherText = encCipher.doFinal(textToEncrypt.getBytes());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return byteCipherText;
}
// step 3 -- encrypt the secret key using key encryption algorithm
public byte[] encryptSecretKey()
{
byte[] encryptedKey = null;
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(mKeyEncryptionAlgorithm);
kpg.initialize(mKeyEncryptionKeyLength);
KeyPair keyPair = kpg.generateKeyPair();
mPublicKey = keyPair.getPublic();
mPrivateKey = keyPair.getPrivate();
Cipher cipher = Cipher.getInstance(mTransformation);
cipher.init(Cipher.PUBLIC_KEY, mPublicKey);
encryptedKey = cipher.doFinal(mSecretKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return encryptedKey;
}
public byte[] encryptSecretKeyNoTransformation()
{
byte[] encryptedKey = null;
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(mKeyEncryptionAlgorithm);
kpg.initialize(mKeyEncryptionKeyLength);
KeyPair keyPair = kpg.generateKeyPair();
mPublicKey = keyPair.getPublic();
mPrivateKey = keyPair.getPrivate();
Cipher cipher = Cipher.getInstance(mTransformation);
cipher.init(Cipher.PUBLIC_KEY, mPublicKey);
encryptedKey = cipher.doFinal(mSecretKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return encryptedKey;
}
// step 4 -- send across the encrypted text and the encrypted secret key
// setp 5 -- decrypt secret key
public byte[] decryptSecretKey(byte[] encryptedSecretKey)
{
byte[] decryptedKey = null;
try {
Cipher cipher = Cipher.getInstance(mTransformation);
cipher.init(Cipher.PRIVATE_KEY, mPrivateKey);
decryptedKey = cipher.doFinal(encryptedSecretKey);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return decryptedKey;
}
// step 6 -- Decrypt the cipher using decrypted symmetric key
public String decryptText(byte[] decryptedKey, byte[] encryptedText)
{
String decryptedPlainText = null;
try {
SecretKey originalKey = new SecretKeySpec(decryptedKey , 0, decryptedKey.length, mEncryptionAlgorithm);
Cipher aesCipher2 = Cipher.getInstance(mEncryptionAlgorithm);
aesCipher2.init(Cipher.DECRYPT_MODE, originalKey);
byte[] bytePlainText = aesCipher2.doFinal(encryptedText);
decryptedPlainText = new String(bytePlainText);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return decryptedPlainText;
}
}
And the main block to call this is given below:
package edh;
import javax.crypto.SecretKey;
public class Main
{
public static void main(String[] args)
{
String plainText = "Hello World, Hybrid Encryption style";
System.out.println("plainText: '" + plainText + "'");
EncDeHybrid ed = new EncDeHybrid();
ed.generateSymmetricKey();
System.out.println("secret key: '" + EncDeHybrid.keyToNumber(ed.getSecretKey().getEncoded()).toString() + "'" );
String strEncodedSecretKey = ed.getEncodedSecretKey();
System.out.println("encoded secret key: '" + strEncodedSecretKey + "'" );
SecretKey sk = ed.getDecodedSecretKey(strEncodedSecretKey, null);
System.out.println("decoded secret key: '" + EncDeHybrid.keyToNumber(sk.getEncoded()).toString() + "'" );
byte[] encryptedSecretKey = ed.encryptSecretKey();
System.out.println("encrypted secret key: '" + EncDeHybrid.keyToNumber(encryptedSecretKey).toString() + "'" );
byte[] encryptedText = ed.encryptText(plainText);
System.out.println("encrypted text: '" + EncDeHybrid.keyToNumber(encryptedText).toString() + "'" );
byte[] decryptedSecretKey = ed.decryptSecretKey(encryptedSecretKey);
System.out.println("decrypted secret key: '" + EncDeHybrid.keyToNumber(decryptedSecretKey).toString() + "'" );
String decryptedText = ed.decryptText(decryptedSecretKey, encryptedText);
System.out.println("decryptedText: '" + decryptedText + "'");
}
}
That's all on hybrid encryption. Happy encrypting!