001package com.nimbusds.common.id; 002 003 004import java.security.InvalidKeyException; 005import java.security.MessageDigest; 006import java.security.NoSuchAlgorithmException; 007import javax.crypto.Mac; 008import javax.crypto.SecretKey; 009import javax.crypto.spec.SecretKeySpec; 010 011import org.apache.commons.lang3.ArrayUtils; 012 013import com.nimbusds.jose.util.Base64URL; 014 015 016/** 017 * Identifier with Hash-based (SHA-256) Message Authentication Code (HMAC). 018 */ 019public class IdentifierWithHMAC extends BaseIdentifier { 020 021 022 /** 023 * The default byte length of generated identifiers. 024 */ 025 public static final int DEFAULT_BYTE_LENGTH = 16; 026 027 028 /** 029 * Computes a SHA-256 based HMAC for the specified message, truncated 030 * to the 128 left-most bits. 031 * 032 * @param message The message. Must not be {@code null}. 033 * @param hmacKey The HMAC key. Must be at least 128 bits long. 034 * 035 * @return The HMAC, truncated to the 128 left-most bits. 036 * 037 * @throws RuntimeException If HMAC computation failed. 038 */ 039 private static byte[] computeHMAC(final byte[] message, final SecretKey hmacKey) { 040 041 if (hmacKey.getEncoded().length < 32) { 042 throw new IllegalArgumentException("The HMAC key must be at least 256 bits long"); 043 } 044 045 Mac hmacComputer; 046 047 try { 048 hmacComputer = Mac.getInstance("HmacSHA256"); 049 } catch (NoSuchAlgorithmException e) { 050 throw new RuntimeException(e.getMessage(), e); 051 } 052 053 SecretKeySpec secret_key = new SecretKeySpec(hmacKey.getEncoded(), "HmacSHA256"); 054 055 try { 056 hmacComputer.init(secret_key); 057 } catch (InvalidKeyException e) { 058 throw new RuntimeException(e.getMessage(), e); 059 } 060 061 byte[] hmac256bit = hmacComputer.doFinal(message); 062 063 // Truncate to 128 bits (keep left most) 064 // https://tools.ietf.org/html/rfc2104#section-5 065 return ArrayUtils.subarray(hmac256bit, 0, 16); 066 } 067 068 069 /** 070 * Generates a new 128-bite secure random identifier and protects it 071 * with a SHA-256 based HMAC. 072 * 073 * @param hmacKey The HMAC key. Must be at least 128 bits long. 074 * 075 * @return The generated identifier, BASE64-URL encoded, where a dot 076 * delimits the identifier from the HMAC. 077 * 078 * @throws RuntimeException If HMAC computation failed. 079 */ 080 private static String generate(final SecretKey hmacKey) { 081 082 byte[] n = new byte[DEFAULT_BYTE_LENGTH]; 083 SECURE_RANDOM.nextBytes(n); 084 byte[] hmac = computeHMAC(n, hmacKey); 085 return Base64URL.encode(n) + "." + Base64URL.encode(hmac); 086 } 087 088 089 /** 090 * Creates a new identifier protected with a SHA-256 based HMAC. 091 * 092 * @param value The identifier value. Must not empty or {@code null}. 093 * @param hmacKey The HMAC key. Must be at least 128 bits long. 094 */ 095 public IdentifierWithHMAC(final byte[] value, final SecretKey hmacKey) { 096 super(Base64URL.encode(value) + "." + Base64URL.encode(computeHMAC(value, hmacKey))); 097 } 098 099 100 /** 101 * Generates a new 128-bite secure random identifier and protects it 102 * with a SHA-256 based HMAC. 103 * 104 * @param hmacKey The HMAC key. Must be at least 128 bits long. 105 * 106 * @throws RuntimeException If HMAC computation failed. 107 */ 108 public IdentifierWithHMAC(final SecretKey hmacKey) { 109 super(generate(hmacKey)); 110 } 111 112 113 /** 114 * Creates a new 128-bite identifier protected with a SHA-256 based 115 * HMAC. 116 * 117 * @param value The identifier value. Must have been validated by the 118 * caller. 119 */ 120 private IdentifierWithHMAC(final String value) { 121 super(value); 122 } 123 124 125 @Override 126 public boolean equals(final Object object) { 127 128 return object instanceof IdentifierWithHMAC && this.toString().equals(object.toString()); 129 } 130 131 132 /** 133 * Parses and validates the specified identifier with HMAC protection. 134 * 135 * @param value The identifier value to parse and validate. Must not 136 * be {@code null}. 137 * @param hmacKey The HMAC key. Must be at least 128 bits long. 138 * 139 * @return The validated identifier. 140 * 141 * @throws InvalidIdentifierException If the identifier format is 142 * illegal or the HMAC is invalid. 143 * @throws RuntimeException If HMAC computation failed. 144 */ 145 public static IdentifierWithHMAC parseAndValidate(final String value, final SecretKey hmacKey) 146 throws InvalidIdentifierException { 147 148 String[] parts = value.split("\\."); 149 150 if (parts.length != 2) { 151 throw new InvalidIdentifierException("Illegal identifier with HMAC format"); 152 } 153 154 if (parts[0].trim().isEmpty()) { 155 throw new InvalidIdentifierException("Missing identifier value"); 156 } 157 158 if (parts[1].trim().isEmpty()) { 159 throw new InvalidIdentifierException("Missing HMAC for the identifier value"); 160 } 161 162 byte[] n = new Base64URL(parts[0]).decode(); 163 164 if (n.length == 0) { 165 throw new InvalidIdentifierException("Illegal identifier value"); 166 } 167 168 byte[] hmac = new Base64URL(parts[1]).decode(); 169 170 if (hmac.length == 0) { 171 throw new InvalidIdentifierException("Illegal HMAC value"); 172 } 173 174 byte[] expectedHMAC = computeHMAC(n, hmacKey); 175 176 if (! MessageDigest.isEqual(expectedHMAC, hmac)) { 177 throw new InvalidIdentifierException("Invalid HMAC"); 178 } 179 180 return new IdentifierWithHMAC(value); 181 } 182}