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}