001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2010-2011 ApexIdentity Inc. 015 * Portions Copyright 2011-2015 ForgeRock AS. 016 */ 017 018package org.forgerock.openig.filter; 019 020import static java.util.Collections.emptyList; 021import static org.forgerock.openig.util.JsonValues.evaluate; 022 023import java.nio.charset.Charset; 024import java.security.GeneralSecurityException; 025import java.security.Key; 026import java.util.ArrayList; 027import java.util.List; 028import java.util.Set; 029 030import javax.crypto.Cipher; 031import javax.crypto.spec.SecretKeySpec; 032 033import org.forgerock.http.Filter; 034import org.forgerock.http.Handler; 035import org.forgerock.http.protocol.Header; 036import org.forgerock.http.protocol.Message; 037import org.forgerock.http.protocol.Request; 038import org.forgerock.http.protocol.Response; 039import org.forgerock.http.util.CaseInsensitiveSet; 040import org.forgerock.json.JsonValueException; 041import org.forgerock.openig.heap.GenericHeapObject; 042import org.forgerock.openig.heap.GenericHeaplet; 043import org.forgerock.openig.heap.HeapException; 044import org.forgerock.openig.util.MessageType; 045import org.forgerock.services.context.Context; 046import org.forgerock.util.encode.Base64; 047import org.forgerock.util.promise.NeverThrowsException; 048import org.forgerock.util.promise.Promise; 049import org.forgerock.util.promise.ResultHandler; 050 051/** 052 * Encrypts and decrypts header fields. 053 * All cipher algorithms provided by SunJCE Provider are supported 054 * for encryption but, for now CryptoHeaderFilter does 055 * not implement a way to set/retrieve the initialization vector(IV) (OPENIG-42) 056 * therefore, the CryptoHeader can not decrypt cipher algorithm using IV. 057 */ 058public class CryptoHeaderFilter extends GenericHeapObject implements Filter { 059 060 /** 061 * Default cipher algorithm to be used when none is specified. 062 */ 063 public static final String DEFAULT_ALGORITHM = "AES/ECB/PKCS5Padding"; 064 065 /** Should the filter encrypt or decrypt the given headers ? */ 066 public enum Operation { 067 /** 068 * Performs an encryption of the selected headers. 069 */ 070 ENCRYPT, 071 072 /** 073 * Perform a decryption of the selected headers. 074 * Notice that the decrypted value is a trimmed String using the given charset ({@code UTF-8} by default). 075 */ 076 DECRYPT 077 } 078 079 /** Indicated the operation (encryption/decryption) to apply to the headers. */ 080 private Operation operation; 081 082 /** Indicates the type of message to process headers for. */ 083 private MessageType messageType; 084 085 /** Cryptographic algorithm. */ 086 private String algorithm; 087 088 /** Encryption key. */ 089 private Key key; 090 091 /** Indicates the {@link Charset} to use for decrypted values. */ 092 private Charset charset; 093 094 /** The names of the headers whose values should be processed for encryption or decryption. */ 095 private final Set<String> headers = new CaseInsensitiveSet(); 096 097 /** 098 * Sets the operation (encryption/decryption) to apply to the headers. 099 * 100 * @param operation 101 * The encryption/decryption) to apply to the headers. 102 */ 103 public void setOperation(final Operation operation) { 104 this.operation = operation; 105 } 106 107 /** 108 * Sets the type of message to process headers for. 109 * 110 * @param messageType 111 * The type of message to process headers for. 112 */ 113 public void setMessageType(final MessageType messageType) { 114 this.messageType = messageType; 115 } 116 117 /** 118 * Sets the cryptographic algorithm. 119 * 120 * @param algorithm 121 * The cryptographic algorithm. 122 */ 123 public void setAlgorithm(final String algorithm) { 124 this.algorithm = algorithm; 125 } 126 127 /** 128 * Sets the encryption key. 129 * 130 * @param key 131 * The encryption key to set. 132 */ 133 public void setKey(final Key key) { 134 this.key = key; 135 } 136 137 /** 138 * The {@link Charset} to use for decrypted values. 139 * 140 * @param charset 141 * The charset used for decrypted values. 142 */ 143 public void setCharset(final Charset charset) { 144 this.charset = charset; 145 } 146 147 /** 148 * Returns the headers whose values should be processed for encryption or decryption. 149 * 150 * @return The headers whose values should be processed for encryption or decryption. 151 */ 152 public Set<String> getHeaders() { 153 return headers; 154 } 155 156 /** 157 * Finds headers marked for processing and encrypts or decrypts the values. 158 * 159 * @param message the message containing the headers to encrypt/decrypt. 160 */ 161 private void process(Message message) { 162 for (String s : this.headers) { 163 Header header = message.getHeaders().get(s); 164 if (header != null) { 165 List<String> in = header.getValues(); 166 List<String> out = new ArrayList<>(); 167 message.getHeaders().remove(s); 168 for (String value : in) { 169 out.add(operation == Operation.ENCRYPT ? encrypt(value) : decrypt(value)); 170 } 171 message.getHeaders().put(s, out); 172 } 173 } 174 } 175 176 /** 177 * Decrypts a string value. 178 * 179 * @param in the string to decrypt. 180 * @return the decrypted value. 181 */ 182 private String decrypt(String in) { 183 String result = ""; 184 try { 185 byte[] ciphertext = Base64.decode(in); 186 Cipher cipher = Cipher.getInstance(this.algorithm); 187 cipher.init(Cipher.DECRYPT_MODE, key); 188 byte[] plaintext = cipher.doFinal(ciphertext); 189 result = new String(plaintext, charset).trim(); 190 } catch (GeneralSecurityException gse) { 191 logger.error(gse); 192 } 193 return result; 194 } 195 196 /** 197 * Encrypts a string value. 198 * 199 * @param in the string to encrypt. 200 * @return the encrypted value. 201 */ 202 private String encrypt(String in) { 203 String result = ""; 204 try { 205 Cipher cipher = Cipher.getInstance(this.algorithm); 206 cipher.init(Cipher.ENCRYPT_MODE, key); 207 byte[] ciphertext = cipher.doFinal(in.getBytes(Charset.defaultCharset())); 208 result = Base64.encode(ciphertext).trim(); 209 } catch (GeneralSecurityException gse) { 210 logger.error(gse); 211 } 212 return result; 213 } 214 215 @Override 216 public Promise<Response, NeverThrowsException> filter(final Context context, 217 final Request request, 218 final Handler next) { 219 if (messageType == MessageType.REQUEST) { 220 process(request); 221 } 222 223 Promise<Response, NeverThrowsException> promise = next.handle(context, request); 224 225 // Hook a post-processing function only if needed 226 if (messageType == MessageType.RESPONSE) { 227 return promise.thenOnResult(new ResultHandler<Response>() { 228 @Override 229 public void handleResult(final Response response) { 230 process(response); 231 } 232 }); 233 } 234 return promise; 235 } 236 237 /** Creates and initializes a header filter in a heap environment. */ 238 public static class Heaplet extends GenericHeaplet { 239 @Override 240 public Object create() throws HeapException { 241 CryptoHeaderFilter filter = new CryptoHeaderFilter(); 242 filter.messageType = config.get("messageType").required().asEnum(MessageType.class); 243 filter.operation = config.get("operation").required().asEnum(Operation.class); 244 filter.algorithm = config.get("algorithm").defaultTo(DEFAULT_ALGORITHM).asString(); 245 filter.charset = config.get("charset").defaultTo("UTF-8").asCharset(); 246 byte[] key = Base64.decode(evaluate(config.get("key").required())); 247 if ((key == null) || (key.length == 0)) { 248 throw new JsonValueException(config.get("key"), 249 "key evaluation gave an empty result that is not allowed"); 250 } 251 try { 252 filter.key = new SecretKeySpec(key, config.get("keyType").defaultTo("AES").asString()); 253 } catch (IllegalArgumentException iae) { 254 throw new JsonValueException(config, iae); 255 } 256 filter.headers.addAll(config.get("headers").defaultTo(emptyList()).asList(String.class)); 257 return filter; 258 } 259 } 260}