Java tutorial
/** * 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); } } }