SaslUtils.java

/*
 * SPDX-FileCopyrightText: 2025 Lucimber UG
 * SPDX-License-Identifier: Apache-2.0
 */
package com.lucimber.dbus.util;

import java.io.FileNotFoundException;
import java.math.BigInteger;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Objects;

/** Utility class for common methods used for SASL authentication. */
public final class SaslUtils {

    private SaslUtils() {
        // Utility class
    }

    /**
     * Translates a byte-array to its corresponding hexadecimal representation.
     *
     * @param bytes The byte-array
     * @return The hexadecimal representation as a {@link String}.
     */
    public static String toHexadecimalString(final byte[] bytes) {
        Objects.requireNonNull(bytes);
        final BigInteger integer = new BigInteger(1, bytes);
        final int hexRadix = 16;
        return integer.toString(hexRadix);
    }

    /**
     * Translates a hexadecimal string to its corresponding binary representation.
     *
     * @param hexString The hexadecimal string
     * @return The binary representation as a byte-array.
     */
    public static byte[] fromHexadecimalString(final String hexString) {
        Objects.requireNonNull(hexString);
        final int hexRadix = 16;
        final BigInteger integer = new BigInteger(hexString, hexRadix);
        return integer.toByteArray();
    }

    /**
     * Generates an ASCII-safe challenge string.
     *
     * @param challengeLength The desired length of the challenge.
     * @return The challenge as a {@link String}.
     */
    public static String generateChallengeString(final int challengeLength) {
        if (challengeLength <= 0) {
            throw new IllegalArgumentException("length must be greater than zero");
        }
        final String symbols =
                "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789";
        final int bound = symbols.length();
        final SecureRandom random = new SecureRandom();
        final StringBuilder builder = new StringBuilder();
        for (int i = 0; i < challengeLength; i++) {
            final int idx = random.nextInt(bound);
            builder.append(symbols.charAt(idx));
        }
        return builder.toString();
    }

    /**
     * Locates the cookie file for the DBUS_COOKIE_SHA1 authentication mechanism.
     *
     * @param directoryPath The path of the directory in which the file resides.
     * @param cookeFileName The name of the cookie file.
     * @return The resolved path of the cookie file.
     * @throws FileNotFoundException If the resolved path points to no (regular) file.
     * @throws AccessDeniedException If the cookie file cannot be accessed by the VM.
     */
    public static Path locateCookieFile(final Path directoryPath, final String cookeFileName)
            throws FileNotFoundException, AccessDeniedException {
        Objects.requireNonNull(directoryPath);
        Objects.requireNonNull(cookeFileName);
        if (cookeFileName.isEmpty()) {
            throw new IllegalArgumentException("file name must not be empty");
        }
        final Path resolvedPath = Paths.get(directoryPath.toString(), cookeFileName);
        if (Files.isRegularFile(resolvedPath)) {
            if (Files.isReadable(resolvedPath)) {
                return resolvedPath;
            } else {
                throw new AccessDeniedException(resolvedPath.toString());
            }
        } else {
            throw new FileNotFoundException(resolvedPath.toString());
        }
    }

    /**
     * Computes a hash value via the SHA.
     *
     * @param bytes The bytes that should be hashed.
     * @return The hash as a byte-array.
     * @throws NoSuchAlgorithmException If the JVM is not capable of computing the hash via SHA.
     */
    public static byte[] computeHashValue(final byte[] bytes) throws NoSuchAlgorithmException {
        Objects.requireNonNull(bytes);
        final MessageDigest digest = MessageDigest.getInstance("SHA");
        return digest.digest(bytes);
    }
}