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-2014 ForgeRock AS. 016 */ 017 018package org.forgerock.openig.filter; 019 020import static java.util.Collections.*; 021import static org.forgerock.openig.util.Json.*; 022 023import java.io.IOException; 024import java.nio.charset.Charset; 025import java.security.GeneralSecurityException; 026import java.security.Key; 027import java.util.ArrayList; 028import java.util.List; 029import java.util.Set; 030 031import javax.crypto.Cipher; 032import javax.crypto.spec.SecretKeySpec; 033 034import org.forgerock.json.fluent.JsonValueException; 035import org.forgerock.openig.handler.Handler; 036import org.forgerock.openig.handler.HandlerException; 037import org.forgerock.openig.heap.GenericHeaplet; 038import org.forgerock.openig.heap.HeapException; 039import org.forgerock.openig.http.Exchange; 040import org.forgerock.openig.http.Message; 041import org.forgerock.openig.http.MessageType; 042import org.forgerock.openig.util.CaseInsensitiveSet; 043import org.forgerock.util.encode.Base64; 044 045/** 046 * Encrypts and decrypts header fields. 047 * All cipher algorithms provided by SunJCE Provider are supported 048 * for encryption but, for now CryptoHeaderFilter does 049 * not implement a way to set/retrieve the initialization vector(IV) (OPENIG-42) 050 * therefore, the CryptoHeader can not decrypt cipher algorithm using IV. 051 */ 052public class CryptoHeaderFilter extends GenericFilter { 053 054 /** 055 * Default cipher algorithm to be used when none is specified. 056 */ 057 public static final String DEFAULT_ALGORITHM = "AES/ECB/PKCS5Padding"; 058 059 /** Should the filter encrypt or decrypt the given headers ? */ 060 public enum Operation { 061 /** 062 * Performs an encryption of the selected headers. 063 */ 064 ENCRYPT, 065 066 /** 067 * Perform a decryption of the selected headers. 068 * Notice that the decrypted value is a trimmed String using the given charset ({@code UTF-8} by default). 069 */ 070 DECRYPT 071 } 072 073 /** Indicated the operation (encryption/decryption) to apply to the headers. */ 074 private Operation operation; 075 076 /** Indicates the type of message in the exchange to process headers for. */ 077 private MessageType messageType; 078 079 /** Cryptographic algorithm. */ 080 private String algorithm; 081 082 /** Encryption key. */ 083 private Key key; 084 085 /** Indicates the {@link Charset} to use for decrypted values. */ 086 private Charset charset; 087 088 /** The names of the headers whose values should be processed for encryption or decryption. */ 089 private final Set<String> headers = new CaseInsensitiveSet(); 090 091 /** 092 * Sets the operation (encryption/decryption) to apply to the headers. 093 * 094 * @param operation 095 * The encryption/decryption) to apply to the headers. 096 */ 097 public void setOperation(final Operation operation) { 098 this.operation = operation; 099 } 100 101 /** 102 * Sets the type of message in the exchange to process headers for. 103 * 104 * @param messageType 105 * The type of message in the exchange to process headers for. 106 */ 107 public void setMessageType(final MessageType messageType) { 108 this.messageType = messageType; 109 } 110 111 /** 112 * Sets the cryptographic algorithm. 113 * 114 * @param algorithm 115 * The cryptographic algorithm. 116 */ 117 public void setAlgorithm(final String algorithm) { 118 this.algorithm = algorithm; 119 } 120 121 /** 122 * Sets the encryption key. 123 * 124 * @param key 125 * The encryption key to set. 126 */ 127 public void setKey(final Key key) { 128 this.key = key; 129 } 130 131 /** 132 * The {@link Charset} to use for decrypted values. 133 * 134 * @param charset 135 * The charset used for decrypted values. 136 */ 137 public void setCharset(final Charset charset) { 138 this.charset = charset; 139 } 140 141 /** 142 * Returns the headers whose values should be processed for encryption or decryption. 143 * 144 * @return The headers whose values should be processed for encryption or decryption. 145 */ 146 public Set<String> getHeaders() { 147 return headers; 148 } 149 150 /** 151 * Finds headers marked for processing and encrypts or decrypts the values. 152 * 153 * @param message the message containing the headers to encrypt/decrypt. 154 */ 155 private void process(Message<?> message) { 156 for (String s : this.headers) { 157 List<String> in = message.getHeaders().get(s); 158 if (in != null) { 159 List<String> out = new ArrayList<String>(); 160 message.getHeaders().remove(s); 161 for (String value : in) { 162 out.add(operation == Operation.ENCRYPT ? encrypt(value) : decrypt(value)); 163 } 164 message.getHeaders().addAll(s, out); 165 } 166 } 167 } 168 169 /** 170 * Decrypts a string value. 171 * 172 * @param in the string to decrypt. 173 * @return the decrypted value. 174 */ 175 private String decrypt(String in) { 176 String result = null; 177 try { 178 byte[] ciphertext = Base64.decode(in); 179 Cipher cipher = Cipher.getInstance(this.algorithm); 180 cipher.init(Cipher.DECRYPT_MODE, key); 181 byte[] plaintext = cipher.doFinal(ciphertext); 182 result = new String(plaintext, charset).trim(); 183 } catch (GeneralSecurityException gse) { 184 logger.error(gse); 185 } 186 return result; 187 } 188 189 /** 190 * Encrypts a string value. 191 * 192 * @param in the string to encrypt. 193 * @return the encrypted value. 194 */ 195 private String encrypt(String in) { 196 String result = null; 197 try { 198 Cipher cipher = Cipher.getInstance(this.algorithm); 199 cipher.init(Cipher.ENCRYPT_MODE, key); 200 byte[] ciphertext = cipher.doFinal(in.getBytes(Charset.defaultCharset())); 201 result = Base64.encode(ciphertext).trim(); 202 } catch (GeneralSecurityException gse) { 203 logger.error(gse); 204 } 205 return result; 206 } 207 208 @Override 209 public void filter(Exchange exchange, Handler next) throws HandlerException, IOException { 210 if (messageType == MessageType.REQUEST) { 211 process(exchange.request); 212 } 213 next.handle(exchange); 214 if (messageType == MessageType.RESPONSE) { 215 process(exchange.response); 216 } 217 } 218 219 /** Creates and initializes a header filter in a heap environment. */ 220 public static class Heaplet extends GenericHeaplet { 221 @Override 222 public Object create() throws HeapException { 223 CryptoHeaderFilter filter = new CryptoHeaderFilter(); 224 filter.messageType = config.get("messageType").required().asEnum(MessageType.class); 225 filter.operation = config.get("operation").required().asEnum(Operation.class); 226 filter.algorithm = config.get("algorithm").defaultTo(DEFAULT_ALGORITHM).asString(); 227 filter.charset = config.get("charset").defaultTo("UTF-8").asCharset(); 228 byte[] key = Base64.decode(evaluate(config.get("key").required())); 229 if ((key == null) || (key.length == 0)) { 230 throw new JsonValueException(config.get("key"), 231 "key evaluation gave an empty result that is not allowed"); 232 } 233 try { 234 filter.key = new SecretKeySpec(key, config.get("keyType").defaultTo("AES").asString()); 235 } catch (IllegalArgumentException iae) { 236 throw new JsonValueException(config, iae); 237 } 238 filter.headers.addAll(config.get("headers").defaultTo(emptyList()).asList(String.class)); 239 return filter; 240 } 241 } 242}