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}