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 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 * > 0. * @param keyLength the desired key length in bytes. Must be > 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 * <= 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 ('&', U+0026).</li> * <li>Quotation mark ('"', U+0022).</li> * <li>Less-than sign ('<', U+003C).</li> * <li>Greater-than sign ('>', 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("&"); break; case '<': buf.append("<"); break; case '>': buf.append(">"); break; case '"': buf.append("""); break; case '\'': buf.append("'"); 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; } }