feat: sign JSON Web Token with HmacSHA algorithms
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 OnixByte.
|
||||||
|
*
|
||||||
|
* 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.onixbyte.jwt;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface TokenCreator {
|
||||||
|
|
||||||
|
String sign(TokenPayload payload);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 OnixByte.
|
||||||
|
*
|
||||||
|
* 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.onixbyte.jwt;
|
||||||
|
|
||||||
|
import com.onixbyte.jwt.data.RawTokenComponent;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface TokenResolver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
void verify(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Map<String, String> getHeader(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param payload
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Map<String, Object> getPayload(String payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
RawTokenComponent splitToken(String token);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 OnixByte.
|
||||||
|
*
|
||||||
|
* 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.onixbyte.jwt.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public enum Algorithm {
|
||||||
|
HS256(1, 256, "HmacSHA256"),
|
||||||
|
HS384(1, 384, "HmacSHA384"),
|
||||||
|
HS512(1, 512, "HmacSHA512"),
|
||||||
|
RS256(2, 256, "SHA256withRSA"),
|
||||||
|
RS384(2, 384, "SHA384withRSA"),
|
||||||
|
RS512(2, 512, "SHA512withRSA"),
|
||||||
|
ES256(3, 256, "SHA256withECDSA"),
|
||||||
|
ES384(3, 384, "SHA384withECDSA"),
|
||||||
|
ES512(3, 512, "SHA512withECDSA");
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final int HS_FLAG = 1; // 001
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final int RS_FLAG = 2; // 010
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final int ES_FLAG = 3; // 011
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private final int typeFlag;
|
||||||
|
private final int shaLength;
|
||||||
|
private final String algorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param typeFlag
|
||||||
|
* @param shaLength
|
||||||
|
* @param algorithm
|
||||||
|
*/
|
||||||
|
Algorithm(int typeFlag, int shaLength, String algorithm) {
|
||||||
|
this.typeFlag = typeFlag;
|
||||||
|
this.shaLength = shaLength;
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isHmac() {
|
||||||
|
return (this.typeFlag & HS_FLAG) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isRsa() {
|
||||||
|
return (this.typeFlag & RS_FLAG) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isEcdsa() {
|
||||||
|
return (this.typeFlag & ES_FLAG) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public int getShaLength() {
|
||||||
|
return shaLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public int getTypeFlag() {
|
||||||
|
return typeFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 OnixByte.
|
||||||
|
*
|
||||||
|
* 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.onixbyte.jwt.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.onixbyte.jwt.TokenCreator;
|
||||||
|
import com.onixbyte.jwt.TokenPayload;
|
||||||
|
import com.onixbyte.jwt.constant.Algorithm;
|
||||||
|
import com.onixbyte.jwt.constant.HeaderClaims;
|
||||||
|
import com.onixbyte.jwt.holder.ObjectMapperHolder;
|
||||||
|
import com.onixbyte.jwt.util.CryptoUtil;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link TokenCreator} that generates HMAC-signed JSON Web Tokens (JWTs).
|
||||||
|
* <p>
|
||||||
|
* This class uses a specified HMAC algorithm to create signed tokens, incorporating a header,
|
||||||
|
* payload, and signature. It ensures the secret key meets the minimum length requirement for
|
||||||
|
* the chosen algorithm and handles JSON serialisation of the token components.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public class HmacTokenCreator implements TokenCreator {
|
||||||
|
|
||||||
|
private final Algorithm algorithm;
|
||||||
|
private final String issuer;
|
||||||
|
private final byte[] secret;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an HMAC token creator with the specified algorithm, issuer, and secret key.
|
||||||
|
* <p>
|
||||||
|
* Validates that the secret key length meets the minimum requirement for the chosen algorithm.
|
||||||
|
*
|
||||||
|
* @param algorithm the HMAC algorithm to use for signing (e.g., HS256, HS384, HS512)
|
||||||
|
* @param issuer the issuer identifier to include in the token payload if not already present
|
||||||
|
* @param secret the secret key as a string, used to generate the HMAC signature
|
||||||
|
* @throws IllegalArgumentException if the secret key is shorter than the minimum required
|
||||||
|
* length for the specified algorithm
|
||||||
|
*/
|
||||||
|
public HmacTokenCreator(Algorithm algorithm, String issuer, String secret) {
|
||||||
|
var _minSecretLength = algorithm.getShaLength() >> 3;
|
||||||
|
var secretBytesLength = secret.getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
if (secretBytesLength < _minSecretLength) {
|
||||||
|
throw new IllegalArgumentException("Secret key too short for HS%d: minimum %d bytes required, got %d."
|
||||||
|
.formatted(algorithm.getShaLength(), _minSecretLength, secretBytesLength)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.issuer = issuer;
|
||||||
|
this.secret = secret.getBytes(StandardCharsets.UTF_8);
|
||||||
|
this.objectMapper = ObjectMapperHolder.getInstance().getObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and signs a JWT using the HMAC algorithm.
|
||||||
|
* <p>
|
||||||
|
* Generates a token by encoding the header and payload as Base64 URL-safe strings,
|
||||||
|
* creating an HMAC signature, and concatenating them with dots. If the payload does not
|
||||||
|
* include an issuer, the configured issuer is added.
|
||||||
|
*
|
||||||
|
* @param payload the {@link TokenPayload} containing claims to include in the token
|
||||||
|
* @return the signed JWT as a string in the format "header.payload.signature"
|
||||||
|
* @throws IllegalArgumentException if the payload cannot be serialised to JSON due to
|
||||||
|
* invalid data or structure
|
||||||
|
* @throws RuntimeException if an unexpected error occurs during JSON processing
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String sign(TokenPayload payload) {
|
||||||
|
var header = new HashMap<String, String>();
|
||||||
|
|
||||||
|
header.put(HeaderClaims.ALGORITHM, algorithm.name());
|
||||||
|
if (!header.containsKey(HeaderClaims.TYPE)) {
|
||||||
|
header.put(HeaderClaims.TYPE, "JWT");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.hasIssuer()) {
|
||||||
|
payload.withIssuer(issuer);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var encodedHeader = Base64.getUrlEncoder().withoutPadding()
|
||||||
|
.encodeToString(objectMapper.writeValueAsBytes(header));
|
||||||
|
var encodedPayload = Base64.getUrlEncoder().withoutPadding()
|
||||||
|
.encodeToString(objectMapper.writeValueAsBytes(payload.getPayload()));
|
||||||
|
|
||||||
|
var signatureBytes = CryptoUtil.createSignatureFor(algorithm,
|
||||||
|
secret,
|
||||||
|
encodedHeader.getBytes(StandardCharsets.UTF_8),
|
||||||
|
encodedPayload.getBytes(StandardCharsets.UTF_8));
|
||||||
|
var signature = Base64.getUrlEncoder()
|
||||||
|
.withoutPadding()
|
||||||
|
.encodeToString((signatureBytes));
|
||||||
|
|
||||||
|
return "%s.%s.%s".formatted(encodedHeader, encodedPayload, signature);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to serialise token header or payload to JSON.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 OnixByte.
|
||||||
|
*
|
||||||
|
* 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.onixbyte.jwt.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.onixbyte.jwt.TokenResolver;
|
||||||
|
import com.onixbyte.jwt.constant.Algorithm;
|
||||||
|
import com.onixbyte.jwt.constant.RegisteredClaims;
|
||||||
|
import com.onixbyte.jwt.data.RawTokenComponent;
|
||||||
|
import com.onixbyte.jwt.holder.ObjectMapperHolder;
|
||||||
|
import com.onixbyte.jwt.util.CryptoUtil;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link TokenResolver} that resolves and verifies HMAC-signed JSON Web
|
||||||
|
* Tokens (JWTs).
|
||||||
|
* <p>
|
||||||
|
* This class splits a JWT into its components, verifies its signature using an HMAC algorithm, and
|
||||||
|
* deserialises the header and payload into usable data structures. It ensures the secret key meets
|
||||||
|
* the minimum length requirement for the specified algorithm.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public class HmacTokenResolver implements TokenResolver {
|
||||||
|
|
||||||
|
private final Algorithm algorithm;
|
||||||
|
private final byte[] secret;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an HMAC token resolver with the specified algorithm and secret key.
|
||||||
|
* <p>
|
||||||
|
* Validates that the secret key length meets the minimum requirement for the chosen algorithm.
|
||||||
|
*
|
||||||
|
* @param algorithm the HMAC algorithm used for signature verification (e.g., HS256,
|
||||||
|
* HS384, HS512)
|
||||||
|
* @param secret the secret key as a string, used to verify the HMAC signature
|
||||||
|
* @throws IllegalArgumentException if the secret key is shorter than the minimum required
|
||||||
|
* length for the specified algorithm
|
||||||
|
*/
|
||||||
|
public HmacTokenResolver(Algorithm algorithm, String secret) {
|
||||||
|
var _minSecretLength = algorithm.getShaLength() >> 3;
|
||||||
|
var secretBytesLength = secret.getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
if (secretBytesLength < _minSecretLength) {
|
||||||
|
throw new IllegalArgumentException("Secret key too short for HS%d: minimum %d bytes required, got %d"
|
||||||
|
.formatted(algorithm.getShaLength(), _minSecretLength, secretBytesLength)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.secret = secret.getBytes(StandardCharsets.UTF_8);
|
||||||
|
this.objectMapper = ObjectMapperHolder.getInstance().getObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a JWT into its raw components: header, payload, and signature.
|
||||||
|
*
|
||||||
|
* @param token the JWT string to split
|
||||||
|
* @return a {@link RawTokenComponent} containing the header, payload, and signature as strings
|
||||||
|
* @throws IllegalArgumentException if the token does not consist of exactly three parts
|
||||||
|
* separated by dots
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public RawTokenComponent splitToken(String token) {
|
||||||
|
var tokenTuple = token.split("\\.");
|
||||||
|
|
||||||
|
if (tokenTuple.length != 3) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The provided JWT is invalid: it must consist of exactly three parts separated by dots.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RawTokenComponent(tokenTuple[0], tokenTuple[1], tokenTuple[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the HMAC signature of the provided JWT.
|
||||||
|
* <p>
|
||||||
|
* Splits the token into its components and uses the configured algorithm and secret to check
|
||||||
|
* the signature's validity. If the signature does not match, an exception is thrown by the
|
||||||
|
* underlying cryptographic utility.
|
||||||
|
*
|
||||||
|
* @param token the JWT string to verify
|
||||||
|
* @throws IllegalArgumentException if the token is malformed or the signature verification
|
||||||
|
* fails due to an invalid algorithm, key, or
|
||||||
|
* mismatched signature
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void verify(String token) {
|
||||||
|
var _token = splitToken(token);
|
||||||
|
|
||||||
|
var isValid = CryptoUtil.verifySignatureFor(algorithm,
|
||||||
|
secret,
|
||||||
|
_token.header(),
|
||||||
|
_token.payload(),
|
||||||
|
_token.signature().getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
if (!isValid) throw new IllegalArgumentException(
|
||||||
|
"JWT signature verification failed: the token may be tampered with or invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the header claims from the provided JWT.
|
||||||
|
* <p>
|
||||||
|
* Decodes the Base64-encoded header and deserialises it into a map of strings.
|
||||||
|
*
|
||||||
|
* @param token the JWT string from which to extract the header
|
||||||
|
* @return a map containing the header claims as key-value pairs
|
||||||
|
* @throws IllegalArgumentException if the token is malformed or the header cannot be
|
||||||
|
* deserialised due to invalid JSON format
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getHeader(String token) {
|
||||||
|
var _token = splitToken(token);
|
||||||
|
|
||||||
|
var headerBytes = Base64.getDecoder().decode(_token.header());
|
||||||
|
var headerJson = new String(headerBytes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(headerJson, new TypeReference<>() {
|
||||||
|
});
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to deserialise JWT header: the header JSON is invalid or malformed.", e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the payload claims from the provided JWT, excluding registered claims.
|
||||||
|
* <p>
|
||||||
|
* Decodes the Base64-encoded payload, deserialises it into a map, and removes any registered
|
||||||
|
* claims as defined in {@link RegisteredClaims}.
|
||||||
|
*
|
||||||
|
* @param token the JWT string from which to extract the payload
|
||||||
|
* @return a map containing the custom payload claims as key-value pairs
|
||||||
|
* @throws IllegalArgumentException if the token is malformed or the payload cannot be
|
||||||
|
* deserialised due to invalid JSON format
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getPayload(String token) {
|
||||||
|
var _token = splitToken(token);
|
||||||
|
|
||||||
|
var payloadBytes = Base64.getDecoder().decode(_token.payload());
|
||||||
|
var payloadJson = new String(payloadBytes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var payloadMap = objectMapper.readValue(payloadJson, new TypeReference<Map<String, Object>>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
payloadMap.keySet().removeIf(RegisteredClaims.VALUES::contains);
|
||||||
|
return payloadMap;
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to deserialise JWT payload: the payload JSON is invalid or malformed.", e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 OnixByte.
|
||||||
|
*
|
||||||
|
* 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.onixbyte.jwt.util;
|
||||||
|
|
||||||
|
import com.onixbyte.jwt.constant.Algorithm;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for cryptographic operations related to JWT processing.
|
||||||
|
* <p>
|
||||||
|
* Provides methods for creating and verifying signatures using specified algorithms, primarily for
|
||||||
|
* JSON Web Token (JWT) authentication purposes.
|
||||||
|
*
|
||||||
|
* @author zihluwang
|
||||||
|
*/
|
||||||
|
public final class CryptoUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to prevent instantiation of this utility class.
|
||||||
|
*/
|
||||||
|
private CryptoUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final byte JWT_PART_SEPARATOR = (byte) 46;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a signature for the given header and payload using the specified algorithm
|
||||||
|
* and secret.
|
||||||
|
*
|
||||||
|
* @param algorithm the cryptographic algorithm to use (e.g., HMAC-SHA256)
|
||||||
|
* @param secret the secret key bytes used for signing
|
||||||
|
* @param header the header bytes to include in the signature
|
||||||
|
* @param payload the payload bytes to include in the signature
|
||||||
|
* @return the generated signature bytes
|
||||||
|
* @throws IllegalArgumentException if the algorithm is not supported or the key is invalid
|
||||||
|
*/
|
||||||
|
public static byte[] createSignatureFor(
|
||||||
|
Algorithm algorithm,
|
||||||
|
byte[] secret,
|
||||||
|
byte[] header,
|
||||||
|
byte[] payload) {
|
||||||
|
try {
|
||||||
|
final var mac = Mac.getInstance(algorithm.getAlgorithm());
|
||||||
|
mac.init(new SecretKeySpec(secret, algorithm.getAlgorithm()));
|
||||||
|
mac.update(header);
|
||||||
|
mac.update(JWT_PART_SEPARATOR);
|
||||||
|
return mac.doFinal(payload);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IllegalArgumentException("The provided secret key is invalid for the algorithm '%s'."
|
||||||
|
.formatted(algorithm.getAlgorithm()), e);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalArgumentException("The specified algorithm '%s' is not supported."
|
||||||
|
.formatted(algorithm.getAlgorithm()), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the signature for the given header and payload using the specified algorithm
|
||||||
|
* and secret.
|
||||||
|
* <p>
|
||||||
|
* This method converts the header and payload strings to UTF-8 bytes before verification.
|
||||||
|
*
|
||||||
|
* @param algorithm the cryptographic algorithm used for signing
|
||||||
|
* @param secretBytes the secret key bytes used for signing
|
||||||
|
* @param header the header string to verify
|
||||||
|
* @param payload the payload string to verify
|
||||||
|
* @param signatureBytes the signature bytes to check against
|
||||||
|
* @return {@code true} if the signature is valid, {@code false} otherwise
|
||||||
|
* @throws IllegalArgumentException if the algorithm is not supported or the key is invalid
|
||||||
|
*/
|
||||||
|
public static boolean verifySignatureFor(
|
||||||
|
Algorithm algorithm,
|
||||||
|
byte[] secretBytes,
|
||||||
|
String header,
|
||||||
|
String payload,
|
||||||
|
byte[] signatureBytes) {
|
||||||
|
return verifySignatureFor(
|
||||||
|
algorithm,
|
||||||
|
secretBytes,
|
||||||
|
header.getBytes(StandardCharsets.UTF_8),
|
||||||
|
payload.getBytes(StandardCharsets.UTF_8),
|
||||||
|
signatureBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the signature for the given header and payload bytes using the specified algorithm
|
||||||
|
* and secret.
|
||||||
|
*
|
||||||
|
* @param algorithm the cryptographic algorithm used for signing
|
||||||
|
* @param secretBytes the secret key bytes used for signing
|
||||||
|
* @param headerBytes the header bytes to verify
|
||||||
|
* @param payloadBytes the payload bytes to verify
|
||||||
|
* @param signatureBytes the signature bytes to check against
|
||||||
|
* @return {@code true} if the signature matches, {@code false} otherwise
|
||||||
|
* @throws IllegalArgumentException if the algorithm is not supported or the key is invalid
|
||||||
|
*/
|
||||||
|
public static boolean verifySignatureFor(
|
||||||
|
Algorithm algorithm,
|
||||||
|
byte[] secretBytes,
|
||||||
|
byte[] headerBytes,
|
||||||
|
byte[] payloadBytes,
|
||||||
|
byte[] signatureBytes) {
|
||||||
|
return MessageDigest.isEqual(
|
||||||
|
createSignatureFor(
|
||||||
|
algorithm,
|
||||||
|
secretBytes,
|
||||||
|
headerBytes,
|
||||||
|
payloadBytes),
|
||||||
|
signatureBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user