All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.javastack.simpleauth.SimpleAuth Maven / Gradle / Ivy

The newest version!
package org.javastack.simpleauth;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

/**
 * Simple Authentication based in HMAC an Time
 */
public class SimpleAuth {
	/**
	 * Authentication HTTP_HEADER: {@value #HTTP_HEADER}
	 */
	public static final String HTTP_HEADER = "Authentication";
	/**
	 * Authentication SCHEME name: {@value #SCHEME}
	 */
	public static final String SCHEME = "torch";
	/**
	 * Default expire: {@value #DEFAULT_EXPIRE} seconds
	 */
	public static final int DEFAULT_EXPIRE = (5 * 60);

	// Excluded visual similar chars 01Ol
	private final static char[] DEF_ALPHABET = ("23456789" + "ABCDEFGHIJKLMNPQRSTUVWXYZ"
			+ "abcdefghijkmnopqrstuvwxyz" + "#$!:.=+-/_").toCharArray();
	private static final Charset UTF8 = Charset.forName("UTF-8");

	private int expire = DEFAULT_EXPIRE;
	private String key = "";

	/**
	 * Create SimpleAuth with empty Key ("") and {@link #DEFAULT_EXPIRE}
	 * 
	 * @see #setPreSharedKey(String)
	 * @see #setExpire(int)
	 */
	public SimpleAuth() {
	}

	private static final String generateRandomKey() {
		final Random r = new SecureRandom();
		final byte[] bX = new byte[32];
		final char[] bY = new char[32];
		r.nextBytes(bX);
		for (int i = 0; i < bX.length; i++) {
			final int j = ((bX[i] & 0xFF) % DEF_ALPHABET.length);
			bY[i] = DEF_ALPHABET[j];
		}
		return new String(bY);
	}

	/**
	 * Get Pre-shared-key
	 * 
	 * @return key
	 */
	public String getPreSharedKey() {
		return key;
	}

	/**
	 * Set Pre-shared-key
	 * 
	 * @param key for signature / verification
	 * @return this
	 */
	public SimpleAuth setPreSharedKey(final String key) {
		this.key = ((key == null) ? "" : key);
		return this;
	}

	/**
	 * Set Pre-shared-key with a Random value
	 * 
	 * @return this
	 */
	public SimpleAuth setPreSharedKeyRandom() {
		this.key = generateRandomKey();
		return this;
	}

	/**
	 * Get Expire time of tokens used in decode / verify 
	 * 
	 * @return expiration expressed in seconds
	 * @see #DEFAULT_EXPIRE
	 */
	public int getExpire() {
		return expire;
	}

	/**
	 * Set Expire time of tokens used in decode / verify
	 * 
	 * @param expire in seconds
	 * @return this
	 */
	public SimpleAuth setExpire(final int expire) {
		if (expire <= 0) {
			throw new IllegalArgumentException("expire must be greater than 0");
		}
		this.expire = expire;
		return this;
	}

	/**
	 * Produce signed token using default algorithm
	 * 
	 * @return signed token
	 * @throws GeneralSecurityException if signature fail
	 */
	public String sign() throws GeneralSecurityException {
		return sign(HashAlg.SHA256, null);
	}

	/**
	 * Produce signed token
	 * 
	 * @param alg hashing algorithm
	 * @return signed token
	 * @throws GeneralSecurityException if signature fail
	 */
	public String sign(final HashAlg alg) throws GeneralSecurityException {
		return sign(alg, null);
	}

	/**
	 * Produce signed token
	 * 
	 * @param alg hashing algorithm
	 * @param data key-value pairs to sign
	 * @return signed token
	 * @throws GeneralSecurityException if signature fail
	 */
	public String sign(final HashAlg alg, final Map data) throws GeneralSecurityException {
		String d;
		try {
			d = encodeMap(data);
		} catch (UnsupportedEncodingException e) {
			throw new GeneralSecurityException(e);
		}
		final long now = System.currentTimeMillis() / 1000L;
		String b = encodeHex(sign(alg, d.getBytes(UTF8), now));
		final StringBuilder sb = new StringBuilder(d.length() + b.length() + 20);
		sb.append(String.valueOf(alg)); // ALG
		sb.append(",");
		sb.append(String.valueOf(now)); // TS
		sb.append(",");
		sb.append(d); // DATA
		sb.append(",");
		sb.append(b); // HASH
		return sb.toString();
	}

	/**
	 * Decode data in signed token
	 * 
	 * @param signed data
	 * @return data of null if verify fail
	 */
	public Map decode(final String signed) {
		final Map map = new LinkedHashMap();
		if (verify(signed, map)) {
			return map;
		}
		return null;
	}

	/**
	 * Verify signed token
	 * 
	 * @param signed data
	 * @return true if signature good
	 */
	public boolean verify(final String signed) {
		return verify(signed, null);
	}

	/**
	 * Verify and Decode in specified mapDecode data in signed token
	 * 
	 * @param signed data
	 * @param mapDecode where put decoded data or null for no decoding
	 * @return true if ok
	 */
	public boolean verify(final String signed, final Map mapDecode) {
		try {
			final long now = System.currentTimeMillis() / 1000L;
			final String[] atdh = signed.split(",");
			// ALG,TS,DATA,HASH
			final String a = atdh[0], t = atdh[1], d = atdh[2], h1 = atdh[3];
			//
			final HashAlg alg = HashAlg.valueOf(a);
			final long ts = Long.parseLong(t);
			if ((ts + expire) < now) {
				// Expired
				return false;
			}
			final byte[] h2 = sign(alg, d.getBytes(UTF8), ts);
			if (Arrays.equals(h2, decodeHex(h1))) {
				if (mapDecode != null) {
					decodeMap(d, mapDecode);
				}
				return true;
			}
		} catch (Exception e) {
		}
		return false;
	}

	/**
	 * Hex decoding
	 * 
	 * @param input string
	 * @return
	 */
	private byte[] decodeHex(final String input) {
		return DatatypeConverter.parseHexBinary(input);
	}

	/**
	 * Hex encoding
	 * 
	 * @param input buffer
	 * @return
	 */
	private static String encodeHex(final byte[] input) {
		return DatatypeConverter.printHexBinary(input);
	}

	/**
	 * Long to byte[]
	 * 
	 * @param value
	 * @return
	 */
	private static final byte[] longToByteArray(final long value) {
		return new byte[] {
				(byte) (value >>> 56), //
				(byte) (value >>> 48), //
				(byte) (value >>> 40),  //
				(byte) (value >>> 32), //
				(byte) (value >>> 24), //
				(byte) (value >>> 16), //
				(byte) (value >>> 8),  //
				(byte) value
		};
	}

	/**
	 * Signature
	 * 
	 * @param alg
	 * @param buf
	 * @param ts
	 * @return
	 * @throws SignatureException
	 */
	private byte[] sign(final HashAlg alg, final byte[] buf, final long ts) throws GeneralSecurityException {
		final Mac m = alg.getMac();
		m.init(new SecretKeySpec(key.getBytes(UTF8), m.getAlgorithm()));
		m.update(longToByteArray(ts));
		m.update(buf);
		return m.doFinal();
	}

	/**
	 * Like application/x-www-form-urlencoded
	 * 
	 * @param map to encode
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	private static String encodeMap(final Map map) throws UnsupportedEncodingException {
		if ((map == null) || map.isEmpty()) {
			return "";
		}
		final StringBuilder sb = new StringBuilder();
		for (final Entry e : map.entrySet()) {
			sb.append(URLEncoder.encode(e.getKey(), "UTF-8"));
			sb.append("=");
			sb.append(URLEncoder.encode(e.getValue(), "UTF-8"));
			sb.append("&");
		}
		if (sb.length() > 0) {
			sb.setLength(sb.length() - 1);
		}
		return sb.toString();
	}

	/**
	 * Like application/x-www-form-urlencoded
	 * 
	 * @param data to decode
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	private static void decodeMap(final String data, final Map map)
			throws UnsupportedEncodingException {
		final char[] buf = data.toCharArray();
		if ((buf == null) || (buf.length <= 0)) {
			return;
		}
		final StringBuilder sb = new StringBuilder();
		final int len = buf.length;
		boolean readValue = false; // finding = (reading key)
		String key = "", value = "";
		for (int i = 0; i < len; i++) {
			final char c = buf[i];
			if (c == '=') {
				key = URLDecoder.decode(sb.toString(), "UTF-8");
				value = "";
				sb.setLength(0);
				readValue = true; // finding & (reading value)
			} else if (c == '&') {
				value = URLDecoder.decode(sb.toString(), "UTF-8");
				sb.setLength(0);
				map.put(key, value);
				key = value = "";
				readValue = false;
			} else {
				sb.append(c);
			}
		}
		if (sb.length() > 0) {
			if (readValue) {
				value = URLDecoder.decode(sb.toString(), "UTF-8");
			} else {
				key = URLDecoder.decode(sb.toString(), "UTF-8");
			}
			sb.setLength(0);
			map.put(key, value);
		}
	}

	public static enum HashAlg {
		/**
		 * HmacSHA256
		 */
		SHA256("HmacSHA256"), //
		/**
		 * HmacSHA512
		 */
		SHA512("HmacSHA512"), //
		;
		private final String mac;

		HashAlg(final String mac) {
			this.mac = mac;
			// Check Early Runtime Exception
			try {
				getMac();
			} catch (SignatureException e) {
				throw new RuntimeException(e);
			}
		}

		public Mac getMac() throws SignatureException {
			try {
				return Mac.getInstance(mac);
			} catch (NoSuchAlgorithmException e) {
				throw new SignatureException(e);
			}
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy