Skip to content

Example Utility Class

All integration flows import co.ankatech.ankasecure.sdk.examples.ExampleUtil for:

  • encrypted-credential authentication
  • property loading (cli.properties, -Dcli.config)
  • JSON pretty-printing with Java Time
  • AES-GCM helper functions
  • fatal-exit convenience

// FILE: ExampleUtil.java
package co.ankatech.ankasecure.sdk.examples;

import co.ankatech.ankasecure.sdk.AnkaSecureSdk;
import co.ankatech.ankasecure.sdk.exception.AnkaSecureSdkException;
import co.ankatech.ankasecure.sdk.exception.SdkErrorCode;
import co.ankatech.ankasecure.sdk.model.*;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Properties;

/**
 * Utility class providing shared support for all ANKASecure SDK example scenarios.
 * <p>
 * This class centralizes common functionality used by the Example* programs, including:
 * <ul>
 *   <li>Loading CLI configuration from files or classpath resources</li>
 *   <li>Authenticating via encrypted credentials or a pre-issued JWT token</li>
 *   <li>JSON serialization and deserialization with ISO-8601 date/time support</li>
 *   <li>Deriving and decrypting cryptographic keys (PBKDF2, AES-GCM)</li>
 *   <li>Ensuring and managing a temporary working directory for example artifacts</li>
 *   <li>Uniform error handling that classifies SDK exceptions (timeout, HTTP, I/O, etc.)</li>
 *   <li>Rendering operation metadata (encrypt, decrypt, sign, verify, re-encrypt)</li>
 *   <li>Comparing input streams for equality</li>
 * </ul>
 * <p>
 * All methods are static and intended for one-off use by the ExampleScenario classes.
 */
public final class ExampleUtil {

    /** Working directory for the temporary artifacts produced by examples. */
    public static final Path TEMP_DIR = Path.of("temp_files");

    private ExampleUtil() {
        /* static only */
    }

    /**
     * A preconfigured Jackson ObjectMapper instance for JSON serialization and deserialization.
     * This mapper is set up with the following configurations:
     * - Registers the `JavaTimeModule` for support of ISO-8601 date/time formats.
     * - Enables pretty-printing of JSON output using `SerializationFeature.INDENT_OUTPUT`.
     * - Disables the writing of dates as epoch timestamps by using `SerializationFeature.WRITE_DATES_AS_TIMESTAMPS`.
     * - Configures serialization to exclude null values with `JsonInclude.Include.NON_NULL`.
     */
    private static final ObjectMapper JSON = new ObjectMapper()
            .registerModule(new JavaTimeModule())                         // ISO-8601 dates
            .enable(SerializationFeature.INDENT_OUTPUT)                   // pretty print
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)      // no epoch millis
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);     // ⬅ omit nulls

    /**
     * Loads properties from a configuration file.
     * The method checks for a local "cli.properties" file, a file specified by
     * the "cli.config" system property, and a default "cli.properties" file on the classpath.
     * If none of these are found, an exception is thrown.
     *
     * @return a Properties object containing the key-value pairs from the loaded configuration file
     */
    public static Properties loadProperties() {
        Properties props = new Properties();
        File local = new File("cli.properties");
        try {
            if (local.isFile()) {
                try (InputStream in = new FileInputStream(local)) {
                    props.load(in);
                    System.out.println("Loaded config from " + local.getAbsolutePath());
                    return props;
                }
            }
            String sys = System.getProperty("cli.config");
            if (sys != null) {
                File f = new File(sys);
                if (f.isFile()) {
                    try (InputStream in = new FileInputStream(f)) {
                        props.load(in);
                        System.out.println("Loaded config from " + f.getAbsolutePath());
                        return props;
                    }
                }
            }
            try (InputStream in = ExampleUtil.class.getResourceAsStream("/cli.properties")) {
                if (in == null) {
                    fatal("Could not find cli.properties; run 'init' first.", null);
                }
                props.load(in);
                System.out.println("Loaded config from classpath /cli.properties");
                return props;
            }
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to load configuration", e);
        }
    }

    /**
     * Creates an AnkaSecureSdk instance using a manually provided JWT token.
     *
     * <p>
     * This method bypasses the authentication flow entirely. It is useful for
     * debugging or trusted execution environments where the token is issued externally
     * (e.g. from a deployed auth-service).</p>
     *
     * <p>
     * Requires the presence of a property <code>client.accessToken</code> in
     * the <code>cli.properties</code> file.</p>
     *
     * <p><strong>Warning:</strong> It is your responsibility to ensure the token is valid and not expired.</p>
     *
     * @param props CLI properties loaded via {@link #loadProperties()}
     * @return a token-initialized {@link AnkaSecureSdk} instance
     */
    public static AnkaSecureSdk authenticateWithToken(Properties props) throws AnkaSecureSdkException {
        String token = props.getProperty("client.accessToken");

        if (token == null || token.isBlank()) {
            fatal("Missing required property: client.accessToken", null);
        }

        System.out.println("authenticateWithToken(): Using manually provided token from cli.properties");
        return new AnkaSecureSdk(token, props);
    }


    /**
     * Authenticates and initializes an instance of {@code AnkaSecureSdk} using the provided properties.

     * The method retrieves client-specific credentials (UUID, salt, encrypted client ID, and encrypted client secret)
     * from the given {@code Properties} object, derives a cryptographic key, decrypts the credentials,
     * and performs application authentication. On successful authentication, it returns an instance of
     * {@code AnkaSecureSdk}.
     *
     * @param props The {@code Properties} containing the necessary authentication information:
     *              - `client.uuid`: The unique identifier for the client.
     *              - `client.salt`: The salt value for key derivation.
     *              - `clientIdEnc`: The encrypted client ID.
     *              - `clientSecretEnc`: The encrypted client secret.
     *              All four properties must be non-null and properly initialized.
     *
     * @return An instance of {@code AnkaSecureSdk} if authentication is successful.
     *         Returns {@code null} in case of authentication or decryption errors (execution will halt with a fatal error log).
     */
    public static AnkaSecureSdk authenticate(Properties props) {
        String uuid = props.getProperty("client.uuid");
        String salt = props.getProperty("client.salt");
        String idEnc = props.getProperty("clientIdEnc");
        String secEnc = props.getProperty("clientSecretEnc");

        if (uuid == null || salt == null || idEnc == null || secEnc == null) {
            fatal("CLI not initialised; run 'init' first.", null);
        }

        AnkaSecureSdk sdk = new AnkaSecureSdk(props);
        try {
            byte[] key = deriveKey(uuid, salt);
            String clientId = decryptValue(idEnc, key);
            String secret = decryptValue(secEnc, key);
            sdk.authenticateApplication(clientId, secret);
            System.out.println("Authenticated clientId=" + clientId);
            return sdk;
        } catch (AnkaSecureSdkException ex) {
            fatal(MessageFormat.format(
                    "Authentication failed: HTTP={0}, body={1}",
                    ex.getHttpStatus(), ex.getResponseBody()), ex);
            return null; // unreachable
        } catch (Exception ex) {
            fatal("Error decrypting credentials", ex);
            return null; // unreachable
        }
    }

    /**
     * Converts a given object into its JSON string representation.
     *
     * @param o the object to be converted into JSON format
     * @return a JSON string representation of the given object
     * @throws UncheckedIOException if the JSON serialization process fails
     */
    public static String toJson(Object o) {
        try {
            return JSON.writeValueAsString(o);
        } catch (IOException e) {
            throw new UncheckedIOException("JSON serialization failed", e);
        }
    }

    /**
     * Derives a cryptographic key using PBKDF2 with HmacSHA256.
     *
     * @param uuid the input string used as the basis for deriving the key
     * @param saltHex the salt in hexadecimal string format used to add randomness to the key derivation
     * @return the derived key as a byte array
     * @throws RuntimeException when key derivation fails due to algorithm errors or invalid inputs
     */
    private static byte[] deriveKey(String uuid, String saltHex) {
        try {
            byte[] salt = hexToBytes(saltHex);
            PBEKeySpec spec = new PBEKeySpec(uuid.toCharArray(), salt, 150_000, 256);
            SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            return f.generateSecret(spec).getEncoded();
        } catch (Exception e) {
            throw new RuntimeException("Key derivation failed: " + e.getMessage(), e);
        }
    }

    /**
     * Converts a hexadecimal string to a byte array.
     *
     * @param hex the hexadecimal string to be converted, should contain an even number of characters
     * @return a byte array representing the binary data of the provided hexadecimal string
     */
    private static byte[] hexToBytes(String hex) {
        int len = hex.length();
        byte[] out = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            out[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return out;
    }

    /**
     * Decrypts a Base64-encoded string using AES/GCM/NoPadding encryption algorithm.
     *
     * @param base64 The Base64-encoded string to be decrypted. It must not be null or empty.
     * @param key The secret key used for decryption as a byte array. It must be 16, 24, or 32 bytes in length as per AES requirements.
     * @return The decrypted value as a string. If the input is null or blank, an empty string will be returned.
     * @throws Exception If an error occurs during decryption, such as invalid input format or incorrect key.
     */
    private static String decryptValue(String base64, byte[] key) throws Exception {
        if (base64 == null || base64.isBlank()) {
            return "";
        }
        byte[] blob = Base64.getDecoder().decode(base64);
        byte[] iv = Arrays.copyOfRange(blob, 0, 12);
        byte[] ct = Arrays.copyOfRange(blob, 12, blob.length);

        Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
        c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
                new GCMParameterSpec(128, iv));
        return new String(c.doFinal(ct), StandardCharsets.UTF_8);
    }

    /**
     * Ensures that the specified temporary directory exists. If it does not exist, it attempts to create it.
     *
     * @param dir The path to the directory that needs to be checked or created.
     */
    public static void ensureTempDir(Path dir) {
        try {
            if (!Files.exists(dir)) {
                Files.createDirectories(dir);
            }
        } catch (IOException e) {
            fatal("Could not create temporary directory: " + dir, e);
        }
    }

    /**
     * Logs a fatal error message to the standard error stream, provides additional context if the error
     * is related to the SDK, and terminates the application with exit code 1.
     *
     * @param msg The error message to be logged.
     * @param t   The exception causing the fatal error, which may be an instance of
     *            {@link AnkaSecureSdkException} or another {@link Throwable}. Can be null.
     */
    public static void fatal(String msg, Throwable t) {
        if (t instanceof AnkaSecureSdkException ase) {
            SdkErrorCode code = ase.getErrorCode();
            switch (code) {
                case TIMEOUT ->
                        System.err.println(msg + ": network timeout – please retry or increase timeout settings");
                case HTTP ->
                        System.err.println(MessageFormat.format(
                                "{0}: server returned HTTP {1}", msg, ase.getHttpStatus()));
                case IO ->
                        System.err.println(msg + ": I/O error – " + ase.getMessage());
                default ->
                        System.err.println(msg + ": " + ase.getMessage());
            }
        } else {
            // Non-SDK exceptions
            System.err.println(msg + (t != null ? ": " + t.getMessage() : ""));
        }

        // Show full stack only when debug flag is set
        if (t != null && Boolean.getBoolean("ankasecure.debugStack")) {
            t.printStackTrace(System.err);
        }
        System.exit(1);
    }

    /* ====================================================================== */
    /**
     * Logs metadata associated with an encryption operation in a human-readable format.
     * This includes information about the requested key, actual key used, algorithm utilized,
     * and any warnings generated during the process.
     *
     * @param r the {@link EncryptResult} containing encryption metadata, which includes:
     *          - the key ID originally requested by the client,
     *          - the key ID actually used by the server,
     *          - the algorithm used for encryption,
     *          - and any warnings or informational messages associated with the operation.
     */
    public static void printEncryptionMeta(final EncryptResult r) {
        System.out.println("    * Key requested : " + nullSafe(r.getKeyRequested()));
        System.out.println("    * Key used      : " + nullSafe(r.getActualKeyUsed()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getAlgorithmUsed()));
        printWarnings(r.getWarnings());
    }

    /**
     * Logs decryption metadata in a human-readable format. The metadata includes details
     * about the key requested, the actual key used, the algorithm used in the decryption
     * process, and any non-fatal warnings encountered.
     *
     * @param r the {@link DecryptResultMetadata} containing decryption-related information,
     *          including:
     *          - the key ID originally requested,
     *          - the key ID actually used,
     *          - the algorithm utilized,
     *          - and any warnings, if present.
     */
    public static void printDecryptionMeta(final DecryptResultMetadata r) {
        System.out.println("    * Key requested : " + nullSafe(r.getKeyRequested()));
        System.out.println("    * Key used      : " + nullSafe(r.getActualKeyUsed()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getAlgorithmUsed()));
        printWarnings(r.getWarnings());
    }

    /**
     * Prints a list of warning messages to the console in a formatted manner.
     * Each warning is indented and prefixed with a bullet point.
     *
     * @param warnings a list of strings representing warning messages to be displayed.
     *                 If the list is null or empty, no output will be generated.
     */
    public static void printWarnings(final List<String> warnings) {
        if (warnings != null && !warnings.isEmpty()) {
            System.out.println("    * Warnings:");
            warnings.forEach(w -> System.out.println("      – " + w));
        }
    }

    /**
     * Returns a non-null, safe string representation of the provided input.
     * If the input string is null or blank, the method returns the string "(none)".
     *
     * @param s the input string that may be null or blank
     * @return the input string if it is not null or blank; otherwise, the string "(none)"
     */
    public static String nullSafe(final String s) {
        return (s == null || s.isBlank()) ? "(none)" : s;
    }

    /* ====================================================================== */
    /**
     * Ensures that the temporary directory represented by the constant `TEMP_DIR` exists.
     * Creates the directory, including any necessary but nonexistent parent directories.
     * If the operation fails, this method terminates the JVM with an error message.

     * Any issues encountered during the creation process are handled by invoking the
     * {@link #fatal(String, Throwable)} method, which outputs the error message
     * and exits the application.

     * This method is typically used to guarantee the existence of a directory
     * for temporary file storage required by the application.
     *
     * @throws UncheckedIOException if the directory cannot be created
     */
    public static void ensureTempDir() {
        try {
            Files.createDirectories(TEMP_DIR);
        } catch (IOException e) {
            fatal("Could not create temp directory: " + TEMP_DIR.toAbsolutePath(), e);
        }
    }


    /**
     * Logs metadata associated with the signing operation in a human-readable format.
     * Includes details such as the requested key, actual key used, algorithm, and any warnings.
     *
     * @param r the {@link SignResult} containing metadata about the signing operation,
     *          which includes:
     *          - the key ID originally requested by the client,
     *          - the key ID actually used by the server,
     *          - the algorithm used during the signing process,
     *          - and any non-fatal warnings, if present.
     */
    public static void printSignMeta(final SignResult r) {
        System.out.println("    * Key requested : " + nullSafe(r.getKeyRequested()));
        System.out.println("    * Key used      : " + nullSafe(r.getActualKeyUsed()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getAlgorithmUsed()));
        printWarnings(r.getWarnings());
    }

    /**
     * Logs metadata associated with the verification operation in a human-readable format.
     * Includes details about the requested key, actual key used, algorithm utilized, and any warnings generated.
     *
     * @param r the {@link VerifySignatureResult} containing verification metadata, which includes:
     *          - the key ID originally requested by the client,
     *          - the key ID actually used by the server,
     *          - the algorithm used during the verification process,
     *          - and any non-fatal warnings, if present.
     */
    public static void printVerifyMeta(final VerifySignatureResult r) {
        System.out.println("    * Key requested : " + nullSafe(r.getKeyRequested()));
        System.out.println("    * Key used      : " + nullSafe(r.getActualKeyUsed()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getAlgorithmUsed()));
        printWarnings(r.getWarnings());
    }

    /* ====================================================================== */
    /**
     * Logs metadata associated with an encryption operation in a human-readable format.
     * Includes details about the key requested, actual key used, algorithm utilized, and any warnings generated.
     *
     * @param m the {@link EncryptResult} containing encryption metadata, which includes:
     *          - the key ID originally requested by the client,
     *          - the key ID actually used by the server,
     *          - the algorithm used during the encryption process,
     *          - and any non-fatal warnings, if present.
     */
    public static void printEncryptMeta(final EncryptResult m) {
        System.out.println("    * Key requested : " + nullSafe(m.getKeyRequested()));
        System.out.println("    * Key used      : " + nullSafe(m.getActualKeyUsed()));
        System.out.println("    * Algorithm     : " + nullSafe(m.getAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    /**
     * Logs decryption metadata in a human-readable format, including information
     * about the requested key, actual key used, algorithm used, and any warnings.
     *
     * @param m the {@link DecryptResultMetadata} containing the metadata for the decryption process.
     *          It includes:
     *          - the key ID originally requested by the client,
     *          - the key ID actually used by the server,
     *          - the algorithm negotiated by the crypto engine,
     *          - and non-fatal warnings, if any.
     */
    public static void printDecryptMeta(final DecryptResultMetadata m) {
        System.out.println("    * Key requested : " + nullSafe(m.getKeyRequested()));
        System.out.println("    * Key used      : " + nullSafe(m.getActualKeyUsed()));
        System.out.println("    * Algorithm     : " + nullSafe(m.getAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    /**
     * Compares two InputStream objects to determine if they have identical content.
     * This method reads both streams concurrently and compares their data byte by byte.
     *
     * @param a the first InputStream to compare
     * @param b the second InputStream to compare
     * @return true if both InputStreams have identical content and reach EOF simultaneously; false otherwise
     * @throws IOException if an I/O error occurs while reading from either InputStream
     */
    public static boolean streamsAreEqual(InputStream a, InputStream b) throws IOException {
        byte[] bufA = new byte[8192];
        byte[] bufB = new byte[8192];
        while (true) {
            int lenA = a.read(bufA);
            int lenB = b.read(bufB);
            if (lenA != lenB) {
                return false;
            }
            if (lenA == -1) {
                return true; // both streams reached EOF simultaneously
            }
            for (int i = 0; i < lenA; i++) {
                if (bufA[i] != bufB[i]) {
                    return false;
                }
            }
        }
    }

    /**
     * Logs metadata associated with the re-encryption operation in a human-readable format,
     * including information about keys and algorithms used during the process, as well as any warnings.
     *
     * @param m the {@link ReencryptResult} containing metadata related to the re-encryption operation,
     *          including details such as the old key requested, old key used, old algorithm,
     *          new key requested, new key used, new algorithm, and any warnings associated.
     */
    public static void printReencryptMeta(final ReencryptResult m) {
        System.out.println("    * Old requested : " + nullSafe(m.getOldKeyRequested()));
        System.out.println("      Old used      : " + nullSafe(m.getOldKeyUsed()));
        System.out.println("      Old algorithm : " + nullSafe(m.getOldKeyAlgorithmUsed()));
        System.out.println("    * New requested : " + nullSafe(m.getNewKeyRequested()));
        System.out.println("      New used      : " + nullSafe(m.getNewKeyUsed()));
        System.out.println("      New algorithm : " + nullSafe(m.getNewKeyAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    /**
     * Logs metadata associated with the signing operation in a human-readable format,
     * including a descriptive heading for context.
     *
     * @param heading a contextual or descriptive heading to prepend to the logged output
     * @param m       the {@link SignResult} containing metadata about the signing process
     */
    /* ---------- pretty printers ------------------------------------------ */
    public static void printSignMeta(final String heading, final SignResult m) {
        System.out.println("----- " + heading + " -----");
        printSignMeta(m);
    }

    /**
     * Logs metadata associated with the re-signing operation in a human-readable format.
     *
     * @param m the {@link ResignResult} containing metadata about the re-signing process
     */
    public static void printResignMeta(final ResignResult m) {
        System.out.println("----- RE-SIGN METADATA -----");
        System.out.println("    * [Old] requested : " + nullSafe(m.getOldKeyRequested()));
        System.out.println("    * [Old] used      : " + nullSafe(m.getOldKeyUsed()));
        System.out.println("    * [Old] algorithm : " + nullSafe(m.getOldKeyAlgorithmUsed()));
        System.out.println("    * [New] requested : " + nullSafe(m.getNewKeyRequested()));
        System.out.println("    * [New] used      : " + nullSafe(m.getNewKeyUsed()));
        System.out.println("    * [New] algorithm : " + nullSafe(m.getNewKeyAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    /**
     * Logs metadata associated with the verification operation in a human-readable format,
     * including information about validation, keys used, algorithm, and warnings if any.
     *
     * @param heading a descriptive heading or context label for the logged output
     * @param m       the {@link VerifySignatureResult} containing verification metadata
     */
    public static void printVerifyMeta(final String heading, final VerifySignatureResult m) {
        System.out.println("----- " + heading + " -----");
        System.out.println(MessageFormat.format("    * Valid         : {0}", m.isValid()));
        System.out.println("    * Key requested : " + nullSafe(m.getKeyRequested()));
        System.out.println("    * Key used      : " + nullSafe(m.getActualKeyUsed()));
        System.out.println("    * Algorithm     : " + nullSafe(m.getAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    /**
     * Logs decryption-side metadata in a human-readable format.
     *
     * @param r the {@link DecryptResult} containing the decryption metadata
     *          and decrypted data. Its metadata includes:
     *          - the key ID originally requested by the client,
     *          - the key ID actually used by the server (or {@code null} if identical to the requested key),
     *          - the algorithm negotiated by the crypto-engine,
     *          - and non-fatal warnings, if any.
     */
    public static void printDecryptMeta(final DecryptResult r) {
        System.out.println("----- DECRYPT METADATA -----");
        System.out.println("Key requested : " + nullSafe(r.getMeta().getKeyRequested()));
        System.out.println("Key used      : " + nullSafe(r.getMeta().getActualKeyUsed()));
        System.out.println("Algorithm     : " + nullSafe(r.getMeta().getAlgorithmUsed()));
        printWarnings(r.getMeta().getWarnings());
    }


}
2 · Reference the utility once per flow At the top of every flow page add a short note:

> **Dependency** — this class imports `ExampleUtil`.  
> If you have not copied `ExampleUtil.java` yet, see [example_util.md](example_util.md).
No more duplicated helpers—each scenario remains a single, self-contained Java file that compiles as long as ExampleUtil.java sits in the same Maven/Gradle source tree.

3 · Compile everything together

src/
└─ main/
└─ java/
└─ co/ankatech/ankasecure/sdk/examples/
├─ ExampleUtil.java
├─ ExampleScenario1.java
├─ ExampleScenario2.java
└─ 
mvn -q compile exec:java
-Dexec.mainClass="co.ankatech.ankasecure.sdk.examples.ExampleScenario1"
All other flows run the same way—no code duplication, one shared utility.