com.outerspacecat.util.Cookies.java Source code

Java tutorial

Introduction

Here is the source code for com.outerspacecat.util.Cookies.java

Source

/**
 * Copyright 2011 Caleb Richardson
 * 
 * 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.outerspacecat.util;

import com.google.common.base.Preconditions;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import java.io.IOException;
import java.time.Instant;
import java.util.regex.Pattern;

/**
 * Defines utility methods for working with HTTP cookies.
 * 
 * @author Caleb Richardson
 */
public final class Cookies {
    private final static Pattern HEX_PATTERN = Pattern.compile("[0123456789abcdef]*");

    private Cookies() {
    }

    /**
     * Generates a session cookie value. Uses the algorithm described in <a
     * href="http://www.cse.msu.edu/~alexliu/publications/Cookie/cookie.pdf">A
     * Secure Cookie Protocol</a>.
     * <p>
     * The paper recommends using an SSL session key for the {@code sessionKey}
     * parameter, another possibility is to use a client IP address. Since both of
     * these approaches have flaws (SSL renegotiation and NAT respectively), an
     * empty array may be used if the risk of replay attacks is acceptable. Replay
     * attack risk may also be mitigated by expiring the cookie after a certain
     * amount of time.
     * 
     * @param id a user identifier. Must be non {@code null}.
     * @param data optional data to be stored in the cookie. The data will be
     *        encrypted using {@code key}. Must be non {@code null}, may be empty.
     * @param sessionKey optional information to prevent replay attacks. Must be
     *        non {@code null}, may be empty.
     * @param key a secret server key. Must be non {@code null}, may be any
     *        length.
     * @return a session cookie value. Never {@code null}.
     */
    public static String generateSessionCookie(final byte[] id, final byte[] data, final byte[] sessionKey,
            final byte[] key) {
        Preconditions.checkNotNull(id, "id required");
        Preconditions.checkNotNull(data, "data required");
        Preconditions.checkNotNull(sessionKey, "sessionKey required");
        Preconditions.checkNotNull(key, "key required");

        byte[] created = Longs.toByteArray(System.currentTimeMillis());

        byte[] k = Utils.hmacSha1(Bytes.concat(id, created), key);

        byte[] hmac = Utils.hmacSha1(Bytes.concat(id, created, data, sessionKey), k);

        Tuple2<byte[], byte[]> aesPair = Utils.encryptAes128Cbc(data, Utils.md5(k));

        StringBuilder sb = new StringBuilder(
                6 + id.length * 2 + created.length * 2 + 32 + aesPair.getB().length * 2 + hmac.length * 2);

        sb.append("1$").append(Utils.toHex(id)).append('$').append(Utils.toHex(created)).append('$')
                .append(Utils.toHex(aesPair.getA())).append('$').append(Utils.toHex(aesPair.getB())).append('$')
                .append(Utils.toHex(hmac));

        return sb.toString();
    }

    /**
     * Parses a session cookie previously generated by
     * {@link #generateSessionCookie(byte[], byte[], byte[], byte[])}.
     * 
     * @param key the key that was used to create the cookie. Must be non
     *        {@code null}.
     * @param sessionKey the session key that was used to create the cookie. Must
     *        be non {@code null}.
     * @param cookie the cookie value to parse. Must be non {@code null}.
     * @return a tuple containing the id and data used to create the cookie, along
     *         with a timestamp of when the cookie was created. Never {@code null}
     *         , all tuple values are non {@code null}.
     * @throws IOException if {@code cookie} is not a valid cookie or if it was
     *         not created using {@code key} and {@code sessionKey}.
     */
    public static Tuple3<byte[], byte[], Instant> parseSessionCookie(final byte[] key, final byte[] sessionKey,
            final CharSequence cookie) throws IOException {
        Preconditions.checkNotNull(key, "key required");
        Preconditions.checkNotNull(sessionKey, "sessionKey required");
        Preconditions.checkNotNull(cookie, "cookie required");

        String[] parts = cookie.toString().split("\\$");
        if (parts[0].equals("1")) {
            if (parts.length != 6)
                throw new IllegalArgumentException("invalid cookie: " + cookie);

            if (parts[1].length() % 2 != 0 || !HEX_PATTERN.matcher(parts[1]).matches() || parts[2].length() != 16
                    || !HEX_PATTERN.matcher(parts[2]).matches() || parts[3].length() != 32
                    || !HEX_PATTERN.matcher(parts[3]).matches() || parts[4].length() % 2 != 0
                    || !HEX_PATTERN.matcher(parts[4]).matches() || parts[5].length() % 2 != 0
                    || !HEX_PATTERN.matcher(parts[5]).matches())
                throw new IOException("invalid cookie: " + cookie);

            byte[] id = Utils.fromHex(parts[1].toCharArray());

            byte[] created = Utils.fromHex(parts[2].toCharArray());

            byte[] k = Utils.hmacSha1(Bytes.concat(id, created), key);

            byte[] iv = Utils.fromHex(parts[3].toCharArray());

            byte[] data = Utils.decryptAes128Cbc(Utils.fromHex(parts[4].toCharArray()), Utils.md5(k), iv);

            byte[] hmac = Utils.hmacSha1(Bytes.concat(id, created, data, sessionKey), k);

            if (!Utils.constantEquals(parts[5], new String(Utils.toHex(hmac))))
                throw new IOException("cookie tampering detected: " + cookie);

            return Tuple3.of(id, data, Instant.ofEpochMilli(Longs.fromByteArray(created)));
        } else {
            throw new IOException("invalid cookie: " + cookie);
        }
    }
}