In this post, I cover the basics, what I have learned about encryption while building a module to protect Personal Identifiable Information (PII) data using the Java Cryptography API (JCA) and Bouncy Castle API.
You may find this post helpful if:
You use the JCE library with Bouncy Castle as the security provider to encrypt a plaintext into a byte array. You then take the byte array and encode into cipher text for storing into the database. Below is the high level of the process.
Initialization
Encryption:
Decryption:
If you use Java 8 or below, and you want to use a strong key which is longer than 128 bits, you may get this exception.
Caused by: java.security.InvalidKeyException: Illegal key size or default parameters
Because of import restrictions in various countries, the JDK limits the key length by default. To get around this, do one of the following:
/** * Use reflection to disable the cryptography strength restrictions */ JCEUnlimitedPolicyUtil.removeCryptographyRestrictions(); public static class JCEUnlimitedPolicyUtil { private static void removeCryptographyRestrictions() { if (!isRestrictedCryptography()) { logger.fine("Cryptography restrictions removal not needed"); return; } try { /* * Do the following, but with reflection to bypass access checks: * * JceSecurity.isRestricted = false; * JceSecurity.defaultPolicy.perms.clear(); * JceSecurity.defaultPolicy.add(CryptoAllPermission.INSTANCE); */ final Class << ? > jceSecurity = Class.forName("javax.crypto.JceSecurity"); final Class << ? > cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions"); final Class << ? > cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission"); final Field isRestrictedField = jceSecurity.getDeclaredField("isRestricted"); isRestrictedField.setAccessible(true); final Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(isRestrictedField, isRestrictedField.getModifiers() & ~Modifier.FINAL); isRestrictedField.set(null, false); final Field defaultPolicyField = jceSecurity.getDeclaredField("defaultPolicy"); defaultPolicyField.setAccessible(true); final PermissionCollection defaultPolicy = (PermissionCollection) defaultPolicyField.get(null); final Field perms = cryptoPermissions.getDeclaredField("perms"); perms.setAccessible(true); ((Map << ? , ? > ) perms.get(defaultPolicy)).clear(); final Field instance = cryptoAllPermission.getDeclaredField("INSTANCE"); instance.setAccessible(true); defaultPolicy.add((Permission) instance.get(null)); logger.fine("Successfully removed cryptography restrictions"); } catch (final Exception e) { logger.log(Level.WARNING, "Failed to remove cryptography restrictions", e); } } private static boolean isRestrictedCryptography() { // This matches Oracle Java 7 and 8, but not Java 9 or OpenJDK.
final String name = System.getProperty("java.runtime.name"); final String ver = System.getProperty("java.version"); return name != null && name.equals("Java(TM) SE Runtime Environment") && ver != null && (ver.startsWith("1.7") || ver.startsWith("1.8")); } }
To use Bouncy Castle as a security provider, you need to have the Jar on your class path. Then, you can either update the java.security file following this post or add the code below to register the provider at runtime.
/** * Security Providers initialization. The first call of the init method * will have the class loader do the job. This technique ensures proper * initialization without the need of maintaining the * <i>${java_home}/lib/security/java.security</i> file, that would otherwise * need the addition of the following line: * <code>security.provider.<i>n</i>=org.bouncycastle.jce.provider.BouncyCastleProvider</code>. */ Security.insertProviderAt( new BouncyCastleProvider(), 1 );
For encrypting data at rest, we use symmetric encryption.
In case you are not familiar with symmetric encryption, it essentially means using a same key for both encryption and decryption. The other type of encryption is asymmetric encryption, which operates on two keys, one public and one private.
The simplest approach is to use a shared key for all the data you want to protect. However, the downside is if an attacker gains access to the key, the attacker may be able to decrypt all the data with only that one key.
private static final SecureRandom SECURE_RANDOM = new SecureRandom( ); public static byte[] generateKey( int keyLength ) throws IOException, NoSuchProviderException, NoSuchAlgorithmException { CipherKeyGenerator cipherKeyGenerator = new CipherKeyGenerator(); cipherKeyGenerator.init( new KeyGenerationParameters( SECURE_RANDOM, keyLength ) ); return cipherKeyGenerator.generateKey(); }
If your data pertain to individual users, a more secure approach is deriving individual keys for each user based on the user’s given passphrase/password (password-based encryption (PBE)). That way, at worse, an attacker who knows the key for one user can only decrypt the data for that user.
A key is vulnerable to several types of attack. For instance, in a brute-force attack, the attacker utilizes computing resources to try all the possible passphrase to crack the key. In a dictionary attack, the attacker tries to crack the key by attempting to use common words from a list as passphrase.
A key is probably most of what is necessary to decrypt the data. So it’s important to use a strong key that make it impractical to crack. Some of the characteristics of a strong key include:
The sample code below shows how to derive a PBE key using the JCA
public SecretKey generateKey(string passphrase) { SecureRandom secureRandom = SecureRandom.getInstance(); // use a large number of iterations to prevent brute force attacks. int iterations = 2000; int keyLength = 256; string algorithm = "PBEWITHSHA256AND256BITAES-CBC-BC"; try { byte[] salt = secureRandom.nextBytes(new byte[20]); PBEUTFKeySpec keySpec = new PBEKeySpec(passphrase, salt, iterations, 256); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm); SecretKey key = keyFactory.generateSecret(keySpec); } catch (Exception e) { logThrowException("Key factory initialization failed! ", e); } return key; }
In the above snippet, we specify the algorithm to generate the key as “PBEWITHSHA256AND256BITAES-CBC-BC”. Here is the explanation from this post on StackExchange:
PBE
= password-based encryption. Isn’t this what you need?
WITHSHA256
= SHA-256 is a modern hash algorithm, and is used to prove that the data can’t have been modified by anyone with having the password at hand. 256-bit hashes are the modern standard, with 512-bit hashes for those of us who want just a little bit more reassurance. The SHA-2 family of hash functions is the most recent NIST standard digests.
AND256BITAES
= AES-256 is a modern encryption algorithm. The 256-bit key size is for those of us who want just a little bit more reassurance, even though a 128-bit key size can be sufficient for many uses. The AES family of functions are the most recent NIST standard block ciphers.
CBC
= The CBC mode for encryption is the default choice for turning a block cipher into a stream cipher. Stream ciphers are needed when one has a stream of data of arbitrarily length to be enciphered. AES in CBC mode is a more modern choice than RC4.
BC
= The choice of crypto provider library. Choose a reliable, performance crypto library. BouncyCastle is implemented in Java, so the JIT may be able to optimize it. OpenSSL is implemented in C and the crypto provider might have bindings to the C functions, but the JIT is unable to optimize the C functions.
You use a Cipher object of the javax.crypto package to perform encryption or decryption depending on the mode.
private Cipher encryptCipher; private Cipher decryptCipher; try { SecretKeySpec spec = new SecretKeySpec(key.getEncoded(), "AES"); encryptCipher = Cipher.getInstance("AES/CTR/NOPADDING"); encryptCipher.init(Cipher.ENCRYPT_MODE, spec, ivParameterSpec, secureRandom); decryptCipher = Cipher.getInstance("AES/CTR/NOPADDING"); decryptCipher.init(Cipher.DECRYPT_MODE, spec, ivParameterSpec, secureRandom); } catch (Exception e) { logThrowException("Cipher initialization failed! ", e); }
AES – Advanced Encryption Standards is a block cipher. From Wikipedia,
In cryptography, a block cipher is a deterministic algorithm operating on fixed-length groups of bits, called a block, with an unvarying transformation that is specified by a symmetric key.
CTR – Counter is one of the five modes of block ciphers. The other four modes are: Electronic Code Book (ECB), Cipher Block Chaining (CBC), Cipher FeedBack (CFB), and Output FeedBack (OFB). From the article on The Internet Engineering Task Force,
AES-CTR uses the AES block cipher to create a stream cipher. Data is encrypted and decrypted by XORing with the key stream produced by AES encrypting sequential counter block values.
CTR requires an initialization vector (IV). In a block cipher, the result of encrypting a block is the input for encrypting the next block. Since the first block has no other block before it, a randomized data gets used as input. This randomized data is called the initialization vector. Using an initialization vector make the patterns in a cipher text unique such that an attacker is not able to detect patterns which may help to decrypt all or part of the original text. For more details, see this post.
The code snippet below demonstrates how to use the Cipher object to encrypt a plain text.
/** * Encrypt a string input. * * @param valueToEncrypt the plain text value to encrypt. * @return the encrypted value. */ public synchronized String encrypt( String valueToEncrypt ) { // avoid double encryption. if ( !isEncrypted( valueToEncrypt ) ) { try { byte[] encryptedBytes = encryptCipher.doFinal( valueToEncrypt.getBytes() ); String cipherText = new String( Hex.encode( encryptedBytes ), "UTF-8" ); return ENCRYPTED_TOKEN_START + cipherText; } catch ( Exception e ) { logThrowException( "Failed to encrypt '" + valueToEncrypt + "'. ", e ); } } return valueToEncrypt; }
To decrypt a cipher text, we just reverse the steps we apply on encryption.
/** * reverse the steps done by encryption. * * @param valueToDecrypt the value to decrypt * @return the original value before encryption. * @see #encrypt(String) */ public synchronized String decrypt( String valueToDecrypt ) { LOGGER.info( "Using key to decrypt: " + Hex.toHexString( key.getEncoded() ) ); if ( isEncrypted( valueToDecrypt ) ) { try { // remove the encryption header String cipher = removeEncryptionMetadata( valueToDecrypt ); // get back the cipher bytes that were Hex encoded on encryption and decrypt byte[] cipherBytes = Hex.decode( cipher ); return new String ( decryptCipher.doFinal( cipherBytes ), "UTF-8" ); } catch ( Exception e ) { LOGGER.info( "Decryption failed value: " + valueToDecrypt + " cipher: " + removeEncryptionMetadata( valueToDecrypt )); logThrowException( "Decryption failed! ", e ); } } return valueToDecrypt; }
When encrypting a plaintext using the JCA, you get back a byte array. As such, you need to encode the byte array into characters for storing into the database. I know of two encoding schemes for storing binary data are Base64 and Hexadecimal encodings.
In terms of space efficiency, Base64 uses less space than Hex to represent data. Base64 typically contains a-z, A-Z, 0-9, “/”, “+”, and “=”. It uses four characters for every three bytes. Compared to Base64, Hex (Base16) contains 0-9 and a-f and uses two characters for every byte.
base64-encoded data is only 33% larger than the raw data, whereas the hexadecimal representation is 100% larger.
In terms of simplicity and compatibility, Hex is better as it only uses 16 characters to represent data; all of those characters probably don’t need escaping. With Base64, you may need to worry about escaping since Base64 use special characters (“/”, “+”, “=”). I personally run into issues when storing Base64 encoded cipher texts as DN values in an Ldap database.
I hope you find the post helpful, especially if you are new to encryption or how to do it in Java.
Some of the links below I have also referenced in the above sections. These are the ones I find helpful while learning more about the topic and writing this post.
Easily test sending and receiving email locally using MailHog
Migrating from Oracle to Azure SQL caveat – java.sql.Date does not represent time.
Notes on the three programming paradigms
The file is damaged and could not be repaired.
Building backend API with Spring data, Hibernate, JPA.
Neo4j slow query caused SSLException: SSL peer shut down incorrectly