ApiKey.java
package net.hostsharing.hsadminng.config;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.val;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HexFormat;
import java.util.Optional;
/**
* Generates and hashes the API-keys of API_KEY subjects.
*
* An API-key has the format {@code hsak_<subject-name>.<64 hex chars>}: the embedded subject
* name makes the key self-describing (like the preferred username in a JWT), the 64 hex chars
* are 256 random bits. Because the SHA-256 hash of the whole key is stored and looked up on
* authentication, tampering with the embedded name invalidates the key - no extra signature
* is needed. The name is informational only and reflects the subject name at creation time.
*
* The clear-text API-key is only returned once, in the response of creating the API_KEY subject;
* just its SHA-256 hash is stored in rbac.api_key.
*/
@UtilityClass
public final class ApiKey {
public static final String PREFIX = "hsak_"; // "HostSharing Api Key"
private static final int KEY_BYTES = 32;
private static final int RANDOM_HEX_CHARS = 2 * KEY_BYTES;
private static final char NAME_DELIMITER = '.';
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static String generate(final String subjectName) {
val randomBytes = new byte[KEY_BYTES];
SECURE_RANDOM.nextBytes(randomBytes);
return PREFIX + subjectName + NAME_DELIMITER + HexFormat.of().formatHex(randomBytes);
}
/** @return the subject name embedded in the given API-key,
* or empty if it has no embedded name, e.g. a legacy key {@code hsak_<64 hex chars>} */
public static Optional<String> subjectNameOf(final String apiKey) {
// the random part has a fixed length, thus parsing from the end is unambiguous
// even though subject names may contain the delimiter character
final var delimiterIndex = apiKey.length() - RANDOM_HEX_CHARS - 1;
if (!apiKey.startsWith(PREFIX)
|| delimiterIndex <= PREFIX.length()
|| apiKey.charAt(delimiterIndex) != NAME_DELIMITER) {
return Optional.empty();
}
return Optional.of(apiKey.substring(PREFIX.length(), delimiterIndex));
}
@SneakyThrows
public static String hash(final String clearTextApiKey) {
val digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(clearTextApiKey.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
}
}