com.outerspacecat.util.Utils.java Source code

Java tutorial

Introduction

Here is the source code for com.outerspacecat.util.Utils.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 java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;

/**
 * Defines general utility methods.
 * 
 * @author Caleb Richardson
 */
public final class Utils {
    private final static SecureRandom SECURE_RANDOM = new SecureRandom();

    private final static char[] HEX_ALPHABET = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c',
            'd', 'e', 'f' };

    private Utils() {
    }

    /**
     * Returns the number of bytes required to encode the specified Unicode code
     * point using UTF-8.
     * 
     * @param codePoint the Unicode code point. Must be >= U+0000 and <= U+10FFFF.
     * @return the number of bytes required to encode the specified Unicode code
     *         point using UTF-8
     */
    public static int getUtf8Length(final int codePoint) {
        Preconditions.checkArgument(codePoint >= 0, "code point must be >= 0");

        if (codePoint <= 0x007F) {
            return 1;
        } else if (codePoint <= 0x07FF) {
            return 2;
        } else if (codePoint <= 0xFFFF) {
            return 3;
        } else if (codePoint <= 0x10FFFF) {
            return 4;
        } else {
            throw new IllegalArgumentException("invalid code point: " + codePoint);
        }
    }

    /**
     * Returns the index of {@code c} in {@code buf}, or {@code -1} if {@code c}
     * is not present in {@code buf}.
     * 
     * @param buf the array to search
     * @param c the value to search for
     * @return the index of {@code c} in {@code buf}, or {@code -1} if {@code c}
     *         is not present in {@code buf}
     */
    public static int find(final char[] buf, final char c) {
        Preconditions.checkNotNull(buf, "buf required");

        return find(buf, c, 0, buf.length - 1);
    }

    private static int find(final char[] buf, final char c, final int lo, final int hi) {
        Preconditions.checkNotNull(buf, "buf required");
        Preconditions.checkArgument(lo >= 0 && lo < buf.length, "invalid lo: " + lo);
        Preconditions.checkArgument(hi >= 0 && hi < buf.length, "invalid hi: " + hi);

        if (hi < lo)
            return -1;
        int mid = (lo + hi) >>> 1;
        if (c < buf[mid])
            return find(buf, c, lo, mid - 1);
        if (c > buf[mid])
            return find(buf, c, mid + 1, hi);
        return mid;
    }

    /**
     * Returns whether or not the two supplied character sequences are equal in
     * constant time. {@link String#equals(Object)} will return {@code false} as
     * soon as non matching characters are found, while this function will iterate
     * over the entire sequences, regardless of the implementation types.
     * 
     * @param s1 the first sequence to compare. Must be non {@code null}.
     * @param s2 the second sequence to compare. Must be non {@code null}.
     * @return whether or not the two supplied sequences are equal
     */
    public static boolean constantEquals(final CharSequence s1, final CharSequence s2) {
        Preconditions.checkNotNull(s1, "s1 required");
        Preconditions.checkNotNull(s2, "s2 required");

        if (s1.length() != s2.length())
            return false;

        boolean valid = true;
        for (int i = 0; i < s1.length(); ++i) {
            if (s1.charAt(i) != s2.charAt(i))
                valid = false;
        }

        return valid;
    }

    /**
     * Returns whether or not the two supplied arrays are equal in constant
     * time. {@link java.util.Arrays#equals(byte[], byte[])} will return {@code false} as
     * soon as non matching bytes are found, while this function will iterate over
     * the entire sequences, regardless of the implementation types.
     * 
     * @param a1 the first array to compare. Must be non {@code null}.
     * @param a2 the second array to compare. Must be non {@code null}.
     * @return whether or not the two supplied array are equal
     */
    public static boolean constantEquals(final byte[] a1, final byte[] a2) {
        Preconditions.checkNotNull(a1, "a1 required");
        Preconditions.checkNotNull(a2, "a2 required");

        if (a1.length != a2.length)
            return false;

        boolean valid = true;
        for (int i = 0; i < a1.length; ++i) {
            if (a1[i] != a2[i])
                valid = false;
        }

        return valid;
    }

    /**
     * Converts bytes to hexadecimal characters.
     * 
     * @param input the bytes to convert. Must be non {@code null}.
     * @return a hexadecimal representation of {@code input}. Never {@code null}.
     */
    public static char[] toHex(final byte[] input) {
        char[] ret = new char[input.length * 2];

        for (int i = 0; i < input.length; ++i) {
            int b = 0xFF & input[i];
            ret[i * 2] = HEX_ALPHABET[b >> 4];
            ret[i * 2 + 1] = HEX_ALPHABET[0x0F & b];
        }

        return ret;
    }

    /**
     * Converts hexadecimal characters to bytes.
     * 
     * @param input the hexadecimal characters to convert. Must be non
     *        {@code null}, must have an even length, and all characters must be
     *        valid hexadecimal digits.
     * @return the bytes represented by {@code input}. Never {@code null}.
     */
    public static byte[] fromHex(final char[] input) {
        Preconditions.checkNotNull(input, "input required");
        Preconditions.checkArgument(input.length % 2 == 0, "input length must be even");

        byte[] ret = new byte[input.length / 2];

        for (int i = 0; i < input.length; i += 2) {
            int hi = Character.digit(input[i], 16);
            if (hi < 0)
                throw new IllegalArgumentException("invalid hex character: " + input[i]);

            int lo = Character.digit(input[i + 1], 16);
            if (lo < 0)
                throw new IllegalArgumentException("invalid hex character: " + input[i + 1]);

            ret[i / 2] = (byte) ((hi << 4) + lo);
        }

        return ret;
    }

    /**
     * Returns a MAC of {@code message} using HMAC with SHA1 and {@code key}.
     * 
     * @param message the data to hash. Must be non {@code null}.
     * @param key the key to use for HMAC. Must be non {@code null}, may be any
     *        length.
     * @return an HMAC of {@code message} using SHA1 and {@code key}. Always 20
     *         bytes in length. Never {@code null}.
     */
    public static byte[] hmacSha1(final byte[] message, final byte[] key) {
        Preconditions.checkNotNull(message, "message required");
        Preconditions.checkNotNull(key, "key required");

        try {
            Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(new SecretKeySpec(key, "HmacSHA1"));

            return mac.doFinal(message);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns an MD5 digest of {@code message}.
     * 
     * @param message the message to digest. Must be non {@code null}.
     * @return an MD5 digest of {@code message}. Always 16 bytes in length. Never
     *         {@code null}.
     */
    public static byte[] md5(final byte[] message) {
        Preconditions.checkNotNull(message, "message required");

        try {
            return MessageDigest.getInstance("MD5").digest(message);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns a key derived from {@code message} and {@code salt} using PBKDF2
     * with HMAC with SHA1.
     * 
     * @param message the data to derive the key from. Must be non {@code null}.
     * @param salt a salt to use when deriving the key. Must be non {@code null}
     *        and must be 64 bytes long.
     * @param iterations the number of iterations that PBKDF2 should run. Must be
     *        &gt; 0.
     * @param keyLength the desired key length in bytes. Must be &gt; 0.
     * @return a key derived from {@code message} and {@code salt} using PBKDF2
     *         with HMAC with SHA1
     */
    public static byte[] pbkdf2WithHmacSha1(final char[] message, final byte[] salt, final int iterations,
            final int keyLength) {
        Preconditions.checkNotNull(message, "message required");
        Preconditions.checkNotNull(salt, "salt required");
        Preconditions.checkArgument(salt.length == 64, "salt must be 64 bytes");
        Preconditions.checkArgument(iterations > 0, "iterations must be > 0");
        Preconditions.checkArgument(keyLength > 0, "keyLength must be > 0");

        try {
            return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
                    .generateSecret(new PBEKeySpec(message, salt, iterations, keyLength * 8)).getEncoded();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Encrypts {@code plaintext} using AES, a 128-bit key, the CBC mode of
     * operation, and PKCS5/PKCS7 padding.
     * 
     * @param plaintext the data to encrypt. Must be non {@code null}.
     * @param key the key to use to encrypt {@code plaintext}. Must be non
     *        {@code null} and must be 16 bytes long.
     * @return a tuple containing the 16 byte initialization vector used, and the
     *         resulting ciphertext. Never {@code null}.
     */
    public static Tuple2<byte[], byte[]> encryptAes128Cbc(final byte[] plaintext, final byte[] key) {
        Preconditions.checkNotNull(plaintext, "plaintext required");
        Preconditions.checkNotNull(key, "key required");
        Preconditions.checkArgument(key.length == 16, "key must be 16 bytes");

        byte[] iv = new byte[16];
        SECURE_RANDOM.nextBytes(iv);

        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));

            return Tuple2.of(iv, cipher.doFinal(plaintext));
        } catch (NoSuchPaddingException | BadPaddingException | NoSuchAlgorithmException | InvalidKeyException
                | IllegalBlockSizeException | InvalidAlgorithmParameterException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Decrypts {@code ciphertext} using AES and a 128-bit key. {@code ciphertext}
     * must have been encrypted using the CBC mode of operation and PKCS5/PKCS7
     * padding.
     * 
     * @param ciphertext the data to decrypt. Must be non {@code null}.
     * @param key the key used to encrypt {@code ciphertext}. Must be non
     *        {@code null} and must be 16 bytes long.
     * @param iv the initialization vector used to encrypt {@code ciphertext}.
     *        Must be non {@code null} and must be 16 bytes long.
     * @return plaintext. Never {@code null}.
     */
    public static byte[] decryptAes128Cbc(final byte[] ciphertext, final byte[] key, final byte[] iv) {
        Preconditions.checkNotNull(ciphertext, "ciphertext required");
        Preconditions.checkNotNull(key, "key required");
        Preconditions.checkArgument(key.length == 16);
        Preconditions.checkNotNull(iv, "iv required");
        Preconditions.checkArgument(iv.length == 16);

        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));

            return cipher.doFinal(ciphertext);
        } catch (NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException
                | InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Encrypts {@code plaintext} using AES, a 128-bit key, the ECB mode of
     * operation, and PKCS5/PKCS7 padding. This function artificially limits the
     * length of {@code plaintext} to 16 bytes since ECB is not recommended for
     * encrypting more than one block of data.
     * 
     * @param plaintext the data to encrypt. Must be non {@code null} and must be
     *        &lt;= 16 bytes long.
     * @param key the key to use to encrypt {@code plaintext}. Must be non
     *        {@code null} and must be 16 bytes long.
     * @return ciphertext. Never {@code null}.
     */
    public static byte[] encryptAes128Ecb(final byte[] plaintext, final byte[] key) {
        Preconditions.checkNotNull(plaintext, "plaintext required");
        Preconditions.checkArgument(plaintext.length <= 16, "plaintext must be <= 16 bytes");
        Preconditions.checkNotNull(key, "key required");
        Preconditions.checkArgument(key.length == 16, "key must be 16 bytes");

        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));

            return cipher.doFinal(plaintext);
        } catch (NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException
                | InvalidKeyException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Decrypts {@code ciphertext} using AES and a 128-bit key. {@code ciphertext}
     * must have been encrypted using the ECB mode of operation and PKCS5/PKCS7
     * padding.
     * 
     * @param ciphertext the data to decrypt. Must be non {@code null}.
     * @param key the key used to encrypt {@code ciphertext}. Must be non
     *        {@code null} and must be 16 bytes long.
     * @return plaintext. Never {@code null}.
     */
    public static byte[] decryptAes128Ecb(final byte[] ciphertext, final byte[] key) {
        Preconditions.checkNotNull(ciphertext, "ciphertext required");
        Preconditions.checkNotNull(key, "key required");
        Preconditions.checkArgument(key.length == 16);

        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"));

            return cipher.doFinal(ciphertext);
        } catch (NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException
                | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Escapes {@code input} so that it will be treated literally in an UTF-8 HTML
     * document.
     * <p>
     * The following characters will escaped.
     * <ol>
     * <li>Ampersand ('&amp;', U+0026).</li>
     * <li>Quotation mark ('&quot;', U+0022).</li>
     * <li>Less-than sign ('&lt;', U+003C).</li>
     * <li>Greater-than sign ('&gt;', U+003E).</li>
     * <li>Apostrophe (U+0027).</li>
     * </ol>
     * 
     * @param input the text to escape. Must be non {@code null}.
     * @return {@code input} as escaped text. Never {@code null}.
     */
    public static String escapeUtf8Html(final CharSequence input) {
        Preconditions.checkNotNull(input, "input required");

        StringBuilder buf = new StringBuilder();

        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);
            switch (c) {
            case '&':
                buf.append("&amp;");
                break;
            case '<':
                buf.append("&lt;");
                break;
            case '>':
                buf.append("&gt;");
                break;
            case '"':
                buf.append("&quot;");
                break;
            case '\'':
                buf.append("&#x27;");
                break;
            default:
                buf.append(c);
            }
        }

        return buf.toString();
    }

    /**
     * Performs URI percent encoding defined by <a
     * href="http://tools.ietf.org/html/rfc3986">RFC 3986</a>.
     * 
     * @param input the text to encode. Must be non {@code null}.
     * @return {@code input} as encoded text. Never {@code null}.
     */
    public static String encodeUtf8Uri(final CharSequence input) {
        Preconditions.checkNotNull(input, "input required");

        StringBuilder buf = new StringBuilder();

        byte[] utf8 = input.toString().getBytes(Charsets.UTF_8);

        for (int i = 0; i < utf8.length; i++) {
            int b = utf8[i];
            if ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '-' || b == '.'
                    || b == '_' || b == '~') {
                buf.append((char) b);
            } else {
                buf.append('%').append(HEX_ALPHABET[b >>> 4]).append(HEX_ALPHABET[0x0F & b]);
            }
        }

        return buf.toString();
    }

    /**
     * Parses the provided HTTP query string using the specified encoding.
     * 
     * @param query the query string to parse. Must be non {@code null}.
     * @param enc the encoding to use. Must be non {@code null}.
     * @return parsed parameters. Never {@code null}.
     */
    public static Multimap<String, String> parseHttpQueryString(final String query, final Charset enc) {
        Preconditions.checkNotNull(query, "query required");
        Preconditions.checkNotNull(enc, "enc required");

        Multimap<String, String> ret = LinkedHashMultimap.create();

        try {
            for (String entry : query.split("&")) {
                String[] kv = entry.split("=");
                String key = URLDecoder.decode(kv[0], enc.name());
                String value = "";
                if (kv.length > 1)
                    value = URLDecoder.decode(kv[1], enc.name());
                ret.put(key, value);
            }
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }

        return ret;
    }
}