package jenkins.security; import hudson.Util; import javax.crypto.KeyGenerator; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; /** * {@link ConfidentialKey} that's used for creating a token by hashing some information with secret * (such as <tt>hash(msg|secret)</tt>). * * <p> * This provides more secure version of it by using HMAC. * See http://rdist.root.org/2009/10/29/stop-using-unsafe-keyed-hashes-use-hmac/ for background. * This implementation also never leaks the secret value to outside, so it makes it impossible * for the careless caller to misuse the key (thus protecting ourselves from our own stupidity!) * * @author Kohsuke Kawaguchi * @since 1.498 */ public class HMACConfidentialKey extends ConfidentialKey { private volatile SecretKey key; private final int length; /** * @param length * Byte length of the HMAC code. * By default we use HMAC-SHA256, which produces 256bit (=32bytes) HMAC, * but if different use cases requires a shorter HMAC, specify the desired length here. * Note that when using {@link #mac(String)}, string encoding causes the length to double. * So if you want to get 16-letter HMAC, you specify 8 here. */ public HMACConfidentialKey(String id, int length) { super(id); this.length = length; } /** * Calls into {@link #HMACConfidentialKey(String, int)} with the longest possible HMAC length. */ public HMACConfidentialKey(String id) { this(id,Integer.MAX_VALUE); } /** * Calls into {@link #HMACConfidentialKey(String, int)} by combining the class name and the shortName * as the ID. */ public HMACConfidentialKey(Class owner, String shortName, int length) { this(owner.getName()+'.'+shortName,length); } public HMACConfidentialKey(Class owner, String shortName) { this(owner,shortName,Integer.MAX_VALUE); } /** * Computes the message authentication code for the specified byte sequence. */ public byte[] mac(byte[] message) { return chop(createMac().doFinal(message)); } /** * Convenience method for verifying the MAC code. */ public boolean checkMac(byte[] message, byte[] mac) { return Arrays.equals(mac(message),mac); } /** * Computes the message authentication code and return it as a string. * While redundant, often convenient. */ public String mac(String message) { try { return Util.toHexString(mac(message.getBytes("UTF-8"))); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); } } /** * Verifies MAC constructed from {@link #mac(String)} */ public boolean checkMac(String message, String mac) { return mac(message).equals(mac); } private byte[] chop(byte[] mac) { if (mac.length<=length) return mac; // already too short byte[] b = new byte[length]; System.arraycopy(mac,0,b,0,b.length); return b; } /** * Creates a new {@link Mac} object. */ public Mac createMac() { try { Mac mac = Mac.getInstance(ALGORITHM); mac.init(getKey()); return mac; } catch (GeneralSecurityException e) { // Javadoc says HmacSHA256 must be supported by every Java implementation. throw new Error(ALGORITHM+" not supported?",e); } } private SecretKey getKey() { if (key==null) { synchronized (this) { if (key==null) { try { byte[] encoded = load(); if (encoded==null) { KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM); SecretKey key = kg.generateKey(); store(encoded=key.getEncoded()); } key = new SecretKeySpec(encoded,ALGORITHM); } catch (IOException e) { throw new Error("Failed to load the key: "+getId(),e); } catch (NoSuchAlgorithmException e) { throw new Error("Failed to load the key: "+getId(),e); } } } } return key; } private static final String ALGORITHM = "HmacSHA256"; }