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}