Java tutorial
/** * Copyright (C) 2014 meltmedia (christian.trimble@meltmedia.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.meltmedia.jackson.crypto; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.security.AlgorithmParameters; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidParameterSpecException; import java.security.spec.KeySpec; import java.util.Set; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import javax.validation.ConstraintViolation; import javax.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * A base class for encryption service implementations. * * ## Keys * * The keys in this implementation are created using PBKDF2WithHmacSHA1 key stretching. Options for the stretch iterations and key length * can be specified. * * ## Cipher * * The ciphers used by this implementation are created using AES/CBC/PKCS5Padding. * * ## General Settings * * - AES 256 * - CBC mode with 128 bit blocks * - PBKDF2 w/ a configurable stretch iterations * - 32 bit salt * * * @author Christian Trimble * */ public class EncryptionService<E extends EncryptedJson> { private static final Logger logger = LoggerFactory.getLogger(EncryptionService.class); /** * Remove cryptographic restrictions in the JVM. */ static { try { Field field = Class.forName("javax.crypto.JceSecurity").getDeclaredField("isRestricted"); field.setAccessible(true); field.set(null, java.lang.Boolean.FALSE); } catch (ClassNotFoundException | NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) { logger.info("cannot remove JCE Security restrictions", ex); } } public static class Builder<E extends EncryptedJson> { ObjectMapper mapper; Validator validator; Supplier<E> encryptedSupplier; Function<String, char[]> passphraseLookup; Supplier<byte[]> saltSupplier; int iterations = Defaults.KEY_STRETCH_ITERATIONS; int keyLength = Defaults.KEY_LENGTH; String name = Defaults.DEFAULT_NAME; public Builder<E> withObjectMapper(ObjectMapper mapper) { this.mapper = mapper; return this; } public Builder<E> withValidator(Validator validator) { this.validator = validator; return this; } public Builder<E> withEncryptedJsonSupplier(Supplier<E> encryptedSupplier) { this.encryptedSupplier = encryptedSupplier; return this; } public Builder<E> withPassphraseLookup(Function<String, char[]> passphraseLookup) { this.passphraseLookup = passphraseLookup; return this; } public Builder<E> withSaltSupplier(Supplier<byte[]> saltSupplier) { this.saltSupplier = saltSupplier; return this; } public Builder<E> withIterations(int iterations) { this.iterations = iterations; return this; } public Builder<E> withKeyLength(int keyLength) { this.keyLength = keyLength; return this; } public Builder<E> withName(String name) { this.name = name; return this; } public EncryptionService<E> build() { Supplier<byte[]> buildSaltSupplier = saltSupplier != null ? saltSupplier : Salts.saltSupplier(); if (encryptedSupplier == null) { throw new IllegalArgumentException("the encrypted supplier is required."); } if (passphraseLookup == null) { throw new IllegalArgumentException("the key lookup function is required."); } return new EncryptionService<E>(name, Defaults.defaultObjectMapper(mapper), Defaults.defaultValidator(validator), buildSaltSupplier, encryptedSupplier, passphraseLookup, iterations, keyLength); } } public static interface Supplier<T> { public T get(); } public static interface Function<D, R> { public R apply(D domain); } public static <E extends EncryptedJson> Builder<E> builder() { return new Builder<E>(); } Supplier<E> encryptedSupplier; Supplier<byte[]> saltSupplier; Function<String, char[]> passphraseLookup; ObjectMapper mapper; Validator validator; int iterations; int keyLength; String name; Class<E> encryptedType = (Class<E>) EncryptedJson.class; public EncryptionService(String name, ObjectMapper mapper, Validator validator, Supplier<byte[]> saltSupplier, Supplier<E> encryptedSupplier, Function<String, char[]> passphraseLookup, int iterations, int keyLength) { this.name = name; this.mapper = mapper; this.validator = validator; this.encryptedSupplier = encryptedSupplier; this.passphraseLookup = passphraseLookup; this.saltSupplier = saltSupplier; this.iterations = iterations; this.keyLength = keyLength; } private void validate(E encrypted) throws EncryptionException { if (encrypted == null) { throw new EncryptionException("null encrypted value encountered"); } Set<ConstraintViolation<E>> violations = validator.validate(encrypted); if (!violations.isEmpty()) { String message = String.format("invalid encrypted value%n%s", validationErrorMessage(encrypted, violations)); logger.warn(message); throw new EncryptionException(message); } } private String validationErrorMessage(E encrypted, Set<ConstraintViolation<E>> violations) { StringBuilder sb = new StringBuilder(); try { sb.append("value:").append(mapper.writeValueAsString(encrypted)).append("\n"); } catch (JsonProcessingException e) { sb.append(e.getMessage()).append("\n"); } sb.append("violations:\n"); for (ConstraintViolation<E> violation : violations) { sb.append("- ").append(violation.getPropertyPath().toString() + " " + violation.getMessage()) .append("\n"); } return sb.toString(); } /** * Creates secret key for the encrypted value. * * @param encrypted the encrypted value to create the key for. The keyName and salt must already be defined. * @return the secret key appropriate for the specified value * @throws EncryptionException */ SecretKey createSecretKey(E encrypted) throws EncryptionException { if (KeyDerivations.PBKDF2.equals(encrypted.getKeyDerivation())) { char[] passphrase = passphraseLookup.apply(encrypted.getKeyName()); try { return stretchKey(passphrase, encrypted.getSalt(), encrypted.getIterations(), encrypted.getKeyLength()); } catch (Exception e) { throw new EncryptionException("could not generate secret key", e); } } else { throw new EncryptionException(String.format("could not create secret key. unknown key derivation %s", encrypted.getKeyDerivation())); } } /** * Performs PBKDF2WithHmacSHA1 key stretching on password and returns a key of the specified length. * * @param password the clear text password to base the key on. * @param salt the salt to add to the password * @param iterationCount the number of iterations used when stretching * @param keyLength the length of the resulting key in bits * @return the stretched key * @throws NoSuchAlgorithmException if PBKDF2WithHmacSHA1 is not available * @throws InvalidKeySpecException if the specification of the key is invalid. */ static SecretKey stretchKey(char[] password, byte[] salt, int iterationCount, int keyLength) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); KeySpec spec = new PBEKeySpec(password, salt, iterationCount, keyLength); return factory.generateSecret(spec); } /** * Creates a cipher for doing encryption. The generated iv is placed in the value as a side effect. * * @param secret the pre stretched secret key * @param value the value that the encrypted data will be stored in. * @return the cipher to use. * @throws EncryptionException */ Cipher createEncryptionCipher(SecretKey secret, E value) throws EncryptionException { if (Ciphers.AES_256_CBC.equals(value.getCipher()) && KeyDerivations.PBKDF2.equals(value.getKeyDerivation())) { try { SecretKeySpec spec = new SecretKeySpec(secret.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, spec); AlgorithmParameters params = cipher.getParameters(); value.setIv(params.getParameterSpec(IvParameterSpec.class).getIV()); return cipher; } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidParameterSpecException e) { throw new EncryptionException("could not create encryption cypher", e); } } else { throw new EncryptionException(String.format("unsupported cipher %s and key derivation %s", value.getCipher(), value.getKeyDerivation())); } } /** * Creates a decryption cipher for the encrypted value value using `AES/CBC/PKCS5Padding`. The base64 encoded * iv must already be present in the encrypted value. * * @param secret the key to use for decryption. * @param value the value that will decrypted with this cipher. The base64 iv must be present on this value. * @return a cipher that will decrypt the specified value with the specified key. * @throws EncryptionException if the cipher could not be created for any reason. */ Cipher createDecryptionCipher(SecretKey secret, E value) throws EncryptionException { if (Ciphers.AES_256_CBC.equals(value.getCipher()) && KeyDerivations.PBKDF2.equals(value.getKeyDerivation())) { try { SecretKeySpec spec = new SecretKeySpec(secret.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(value.getIv())); return cipher; } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) { throw new EncryptionException("could not create decryption cypher", e); } } else { throw new EncryptionException(String.format("unsupported cipher %s and key derivation %s", value.getCipher(), value.getKeyDerivation())); } } /** * Encrypts the given data and returns an encrypted value for it. * * @param data the data to encrypt * @return the encrypted value, along with the salt, iv, and name of the settings used. * @throws EncryptionException if the value could not be encrypted for any reason. */ public E encrypt(byte[] data) throws EncryptionException { E result = encryptedSupplier.get(); result.setSalt(saltSupplier.get()); result.setCipher(Defaults.DEFAULT_CIPHER); result.setKeyDerivation(Defaults.DEFAULT_KEY_DERIVATION); result.setKeyLength(keyLength); result.setIterations(iterations); SecretKey secret = createSecretKey(result); Cipher cipher = createEncryptionCipher(secret, result); try { byte[] encrypted = cipher.doFinal(data); result.setValue(encrypted); return result; } catch (IllegalBlockSizeException | BadPaddingException e) { throw new EncryptionException("could not encrypt text", e); } } /** * Encrypts the given text using the specified encoding. * * @param text the text to encrypt. * @param encoding the encoding to use. * @return the encrypted value, along with the salt, iv, and other properties required for decryption. * * @throws UnsupportedEncodingException if the encoding is unsupported. * @throws EncryptionException if the value could not be encrypted for any reason. */ public E encrypt(String text, String encoding) throws UnsupportedEncodingException, EncryptionException { return encrypt(text.getBytes(encoding)); } public <T> E encryptValue(T node, String encoding) throws UnsupportedEncodingException, EncryptionException { try { return encrypt(mapper.writeValueAsString(node), encoding); } catch (JsonProcessingException e) { throw new EncryptionException("could not serialize node", e); } } /** * Decrypts the encrypted value. * * @param value the value to decrypt. * @return the decrypted value. * @throws EncryptionException if the value could not be decypted for any reason. */ public byte[] decrypt(E value) throws EncryptionException { // make sure the value is valid. validate(value); SecretKey secret = createSecretKey(value); Cipher cipher = createDecryptionCipher(secret, value); try { return cipher.doFinal(value.getValue()); } catch (IllegalBlockSizeException | BadPaddingException e) { throw new EncryptionException("could not decrypt text", e); } } /** * Decrypts the encrypted value into a string, using the specified encoding. * * @param value the value to decrypt. * @param encoding the encoding used to convert the value into a string. * @return the decrypted string. * @throws UnsupportedEncodingException if the encoding is not supported. * @throws EncryptionException if the value could not be decrypted for any reason. */ public String decrypt(E value, String encoding) throws UnsupportedEncodingException, EncryptionException { return new String(decrypt(value), encoding); } public JsonParser decrypt(JsonParser parser, String encoding) throws JsonParseException, JsonMappingException, UnsupportedEncodingException, EncryptionException, IOException { try { return mapper.getFactory() .createParser(decrypt((E) mapper.readValue(parser, EncryptedJson.class), encoding)); } catch (EncryptionException ee) { throw ee; } catch (Exception e) { throw new EncryptionException("could not decrypt from parser", e); } } public <T> T decryptAs(E secret, String encoding, Class<T> type) throws EncryptionException { try { return mapper.readValue(decrypt(secret, encoding), type); } catch (IOException e) { throw new EncryptionException("could not decrypt value", e); } } public String getName() { return name; } public Object decrypt(JsonParser parser, JsonDeserializer<?> deser, DeserializationContext context, JavaType type) { try { if (deser == null) { // TODO: This service allows for extension of EncryptedJson, but does // not include // a class defining the subtype being used. return mapper.readValue(decrypt((E) mapper.readValue(parser, EncryptedJson.class)), type); } else { return deser.deserialize(mapper.getFactory() .createParser(decrypt((E) mapper.readValue(parser, EncryptedJson.class))), context); } } catch (EncryptionException ee) { throw ee; } catch (Exception e) { throw new EncryptionException("could not decyrpt value", e); } } }