Skip to content

SDK Example Utilities

Class: ExampleUtil.java Package: co.ankatech.ankasecure.sdk.examples Required for: All 21 integration flows Language: Java 17+

This page documents the ExampleUtil utility class required by all SDK integration flow examples.

Example Code - Required for Flows

This page contains the complete source code of ExampleUtil.java, which is a utility class for SDK integration examples.

Purpose:

  • Developers must copy this class to their projects to run integration flows
  • Provides common functionality (authentication, config loading, metadata printing)
  • NOT internal system code - this is example code for SDK consumers

Usage:

  • Copy the complete Java class below to your project
  • Place in package: co.ankatech.ankasecure.sdk.examples
  • Compile alongside integration flow examples

Overview

ExampleUtil.java provides common utilities for all 21 integration flow examples.

Key Features:

  • Authentication: Credential-based and token-based
  • Configuration Loading: cli.properties from multiple locations
  • Credential Decryption: PBKDF2 + AES-GCM (NIST SP 800-90A compliant)
  • Metadata Printing: Format operation results (encrypt, decrypt, sign, verify, re-encrypt, re-sign)
  • Error Handling: Clean error messages with HTTP status interpretation
  • Stream Utilities: Compare input streams for equality
  • Temporary Directory: Manage temp_files/ for examples

Required by: All integration flows (ExampleScenario1.java through ExampleScenario34.java)


Complete Java Implementation

File: ExampleUtil.java
Package: co.ankatech.ankasecure.sdk.examples

Copy-Paste Ready

The complete source code below is ready to copy directly to your project. It includes all necessary imports, methods, and JavaDoc comments.

/*
 * Copyright 2025 ANKATech Solutions Inc
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package co.ankatech.ankasecure.sdk.examples;

import co.ankatech.ankasecure.sdk.AnkaSecureSdk;
import co.ankatech.ankasecure.sdk.AuthenticatedSdk;
import co.ankatech.ankasecure.sdk.exception.AnkaSecureSdkException;
import co.ankatech.ankasecure.sdk.exception.SdkErrorCode;
import co.ankatech.ankasecure.sdk.internal.util.JsonUtils;
import co.ankatech.ankasecure.sdk.model.*;
import co.ankatech.ankasecure.sdk.model.warning.CryptoWarning;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.Comparator;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

/**
 * Shared utilities for all ANKASecure© SDK example scenarios.
 *
 * <p>This class centralises common functionality used by the
 * {@code ExampleScenario*} programs, including:</p>
 * <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 serialisation with ISO-8601 date/time support</li>
 *   <li>Deriving and decrypting cryptographic keys (PBKDF2, AES-GCM)</li>
 *   <li>Ensuring a temporary working directory for example artefacts</li>
 *   <li>Uniform error handling that classifies SDK exceptions
 *       (timeout, HTTP, I/O, etc.)</li>
 *   <li>Pretty-printing operation metadata (encrypt, decrypt, sign,
 *       verify, re-encrypt, re-sign, combined operations)</li>
 *   <li>Comparing input streams for byte-level equality</li>
 * </ul>
 *
 * <p>All methods are static. This class cannot be instantiated.</p>
 *
 * <p><b>Warning:</b> Several methods in this class call
 * {@link #fatal(String, Throwable)}, which reports the failure to
 * {@code stderr} and then throws {@link ExampleFailedException}. Run
 * standalone, that still terminates the scenario with a non-zero exit code;
 * run inside {@link ExampleComplete}, the suite records the failure and
 * continues. This abort-the-whole-operation style suits standalone example
 * programs but <em>must not</em> be adopted in production SDK integrations,
 * which should surface typed failures the caller can handle.</p>
 *
 * @author ANKATech Solutions Inc.
 * @since 3.0.0
 * @see ExampleScenario1
 */
public final class ExampleUtil {

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

    private static final Logger log = LoggerFactory.getLogger("co.ankatech.cli.dev");

    /** Guards the one-shot reset performed by {@link #prepareWorkingDir()}. */
    private static final AtomicBoolean WORKING_DIR_PREPARED = new AtomicBoolean();

    /** Utility class &mdash; no instantiation. */
    private ExampleUtil() {
        /* static only */
    }

    // =========================================================================
    // CONFIGURATION
    // =========================================================================

    /**
     * Loads CLI properties using the following look-up order:
     *
     * <ol>
     *   <li>A {@code cli.properties} file in the current working directory.</li>
     *   <li>The file pointed to by the {@code cli.config} system property.</li>
     *   <li>A {@code /cli.properties} resource on the classpath.</li>
     * </ol>
     *
     * <p>If none of these sources exist, the method calls
     * {@link #fatal(String, Throwable)}, which aborts the scenario.</p>
     *
     * @return a {@link Properties} object with the loaded key-value pairs;
     *         never {@code null}
     * @throws UncheckedIOException if an I/O error occurs while reading
     *         the 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);
                    throw new IllegalStateException("unreachable"); // makes compiler happy
                }
                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);
        }
    }

    // =========================================================================
    // AUTHENTICATION
    // =========================================================================

    /**
     * Authenticates using encrypted credentials stored in
     * {@code cli.properties} and returns an {@link AuthenticatedSdk}.
     *
     * <p>The method reads four mandatory properties, derives an AES key
     * with PBKDF2, decrypts the client credentials, and performs
     * application authentication via
     * {@link AnkaSecureSdk#authenticateApplication(String, co.ankatech.ankasecure.sdk.security.SecretChars)}.</p>
     *
     * <p><b>Required properties:</b></p>
     * <ul>
     *   <li>{@code client.uuid} &mdash; unique identifier for the client</li>
     *   <li>{@code client.salt} &mdash; hex-encoded salt for key derivation</li>
     *   <li>{@code clientIdEnc} &mdash; AES-GCM-encrypted client ID (Base64)</li>
     *   <li>{@code clientSecretEnc} &mdash; AES-GCM-encrypted client secret (Base64)</li>
     * </ul>
     *
     * <p>If any property is missing the method calls
     * {@link #fatal(String, Throwable)}, which aborts the scenario.</p>
     *
     * @param props CLI properties loaded via {@link #loadProperties()};
     *              must not be {@code null}
     * @return an {@link AuthenticatedSdk} instance bound to a valid JWT
     *         token; never {@code null} under normal operation (the JVM
     *         exits on failure)
     * @see AuthenticatedSdk
     */
    public static AuthenticatedSdk 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);
            throw new IllegalStateException("unreachable");
        }

        AnkaSecureSdk factory = new AnkaSecureSdk(props);
        try {
            byte[] key      = deriveKey(uuid, salt);
            String clientId = decryptValue(idEnc, key);
            String secret   = decryptValue(secEnc, key);

            // SDK-009 (Slice 4): authenticateApplication now requires SecretChars.
            // The local decryptValue still returns String — Slice 5/8 follow-up will
            // migrate the example utilities to native SecretChars decryption. For now,
            // wrap-and-close at the call site so the SecretChars wrapper is destroyed
            // immediately after the SDK call returns, even though the underlying String
            // remains in memory (boundary loss documented in SECURE_MEMORY.md).
            try (co.ankatech.ankasecure.sdk.security.SecretChars secretChars =
                         new co.ankatech.ankasecure.sdk.security.SecretChars(secret.toCharArray())) {
                AuthenticatedSdk sdk = factory.authenticateApplication(clientId, secretChars);
                System.out.println("Authenticated clientId=" + clientId);
                return sdk;
            }
        } catch (AnkaSecureSdkException ex) {
            String cleanMsg = getCleanErrorMessage(ex.getHttpStatus(), ex.getResponseBody());
            fatal(MessageFormat.format("Authentication failed: {0}", cleanMsg), ex);
            throw new IllegalStateException("unreachable");
        } catch (Exception ex) {
            fatal("Error decrypting credentials", ex);
            throw new IllegalStateException("unreachable");
        }
    }

    // =========================================================================
    // JSON HELPERS
    // =========================================================================

    /**
     * Serialises an object to its JSON string representation using the
     * SDK's internal JSON mapper (ISO-8601 dates, pretty-print).
     *
     * @param o the object to serialise; must not be {@code null}
     * @return a JSON string; never {@code null}
     * @throws UncheckedIOException if serialisation fails
     */
    public static String toJson(Object o) {
        return JsonUtils.toJson(o);
    }

    // =========================================================================
    // CRYPTOGRAPHIC HELPERS (private)
    // =========================================================================

    /**
     * Derives a 256-bit AES key from a UUID and hex-encoded salt using
     * PBKDF2-HMAC-SHA256 with 150&nbsp;000 iterations.
     *
     * @param uuid    the passphrase input (client UUID)
     * @param saltHex the salt as a hexadecimal string
     * @return a 32-byte derived key
     * @throws RuntimeException if key derivation fails
     */
    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 an even-length hexadecimal string
     * @return the decoded bytes
     */
    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 blob (12-byte IV prefix + AES-GCM
     * ciphertext) and returns the UTF-8 plaintext.
     *
     * @param base64 Base64-encoded ciphertext; if {@code null} or blank
     *               an empty string is returned
     * @param key    the 16/24/32-byte AES key
     * @return the decrypted UTF-8 string; never {@code null}
     * @throws Exception on any decryption failure
     */
    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);
    }

    // =========================================================================
    // ERROR HANDLING
    // =========================================================================

    /**
     * Builds a clean error message for user display, stripping HTML
     * response bodies that often come from reverse-proxy errors.
     *
     * @param httpStatus   the HTTP status code
     * @param responseBody the raw response body; may be HTML, JSON,
     *                     or {@code null}
     * @return a concise error string suitable for console output
     */
    private static String getCleanErrorMessage(int httpStatus, String responseBody) {
        if (responseBody != null
                && (responseBody.contains("<html>") || responseBody.contains("<HTML>"))) {
            return getHttpStatusMessage(httpStatus);
        }
        if (responseBody != null
                && responseBody.length() < 200
                && !responseBody.contains("<")) {
            return "HTTP " + httpStatus + ": " + responseBody.trim();
        }
        return getHttpStatusMessage(httpStatus);
    }

    /**
     * Returns a human-readable label for common HTTP status codes.
     *
     * @param status the HTTP status code
     * @return a descriptive message such as {@code "HTTP 401 - Unauthorized"}
     */
    private static String getHttpStatusMessage(int status) {
        return switch (status) {
            case 0   -> "Connection failed (check network, SSL certificates, or server availability)";
            case 400 -> "HTTP 400 - Bad Request";
            case 401 -> "HTTP 401 - Unauthorized";
            case 403 -> "HTTP 403 - Forbidden";
            case 404 -> "HTTP 404 - Not Found";
            case 500 -> "HTTP 500 - Internal Server Error";
            case 502 -> "HTTP 502 - Bad Gateway (service may be unavailable)";
            case 503 -> "HTTP 503 - Service Unavailable";
            case 504 -> "HTTP 504 - Gateway Timeout";
            default  -> "HTTP " + status;
        };
    }

    /**
     * Reports a fatal error on {@code stderr} and aborts the scenario by
     * throwing {@link ExampleFailedException}.
     *
     * <p>If the throwable is an {@link AnkaSecureSdkException} the
     * message is enriched with the {@link SdkErrorCode} category
     * (timeout, HTTP status, I/O). The full stack trace is printed only
     * when the system property {@code ankasecure.debugStack} is set to
     * {@code true}.</p>
     *
     * <p>This method <em>never returns normally</em>. Run standalone, the
     * exception propagates out of the scenario's {@code main} and the JVM
     * terminates with a non-zero exit code; run inside {@link ExampleComplete},
     * the suite records the failure and continues with the next scenario.
     * Production code should throw a typed exception the caller can handle
     * rather than reporting to {@code stderr} like this.</p>
     *
     * @param msg the error message to display; must not be {@code null}
     * @param t   the causal exception, or {@code null} if none
     * @throws ExampleFailedException always
     */
    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 {
            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);
        }
        throw new ExampleFailedException(msg);
    }

    // =========================================================================
    // TEMP DIRECTORY
    // =========================================================================

    /**
     * Ensures that the {@link #TEMP_DIR} directory exists, creating it
     * (and any parent directories) if necessary.
     *
     * <p>On failure the method calls
     * {@link #fatal(String, Throwable)}, which aborts the scenario.</p>
     *
     * @see #ensureTempDir(Path)
     */
    public static void ensureTempDir() {
        ensureTempDir(TEMP_DIR);
    }

    /**
     * Ensures that the given directory exists, creating it (and any
     * parent directories) if necessary.
     *
     * <p>On failure the method calls
     * {@link #fatal(String, Throwable)}, which aborts the scenario.</p>
     *
     * @param dir the directory to create; must not be {@code null}
     * @see #ensureTempDir()
     */
    public static void ensureTempDir(Path dir) {
        try {
            Files.createDirectories(dir);
        } catch (IOException e) {
            fatal("Could not create temporary directory: " + dir.toAbsolutePath(), e);
        }
    }

    /**
     * Deletes every artefact left in {@link #TEMP_DIR} by a previous run and
     * recreates the directory empty.
     *
     * <p>The example scenarios write their outputs with the SDK's default
     * {@code OverwritePolicy.FAIL_IF_EXISTS}, so a second run against a
     * populated working directory aborts on the first output file. That policy
     * is a deliberate safety guard and is <em>not</em> relaxed here; instead the
     * precondition it depends on &mdash; an empty working directory &mdash; is
     * established before the scenarios start.</p>
     *
     * <p>The deletion is confined to {@link #TEMP_DIR} and its contents. On
     * failure the method calls {@link #fatal(String, Throwable)}, which aborts
     * the scenario, because continuing would only surface the same collision
     * later with a less obvious message.</p>
     *
     * @see #ensureTempDir()
     */
    /**
     * Prepares the shared working directory for the current JVM.
     *
     * <p>The first call in a JVM clears {@link #TEMP_DIR} via
     * {@link #resetTempDir()}; every later call only ensures the directory
     * exists. That is what makes a scenario re-runnable both on its own and as
     * part of {@link ExampleComplete}: run standalone, the scenario clears the
     * leftovers of its previous run; run inside the suite, the runner consumes
     * the one-shot reset up front and the individual scenarios no longer erase
     * the artefacts produced by the scenarios before them.</p>
     *
     * @see #resetTempDir()
     * @see #ensureTempDir()
     */
    public static void prepareWorkingDir() {
        if (WORKING_DIR_PREPARED.compareAndSet(false, true)) {
            resetTempDir();
        } else {
            ensureTempDir();
        }
    }

    public static void resetTempDir() {
        Path dir = TEMP_DIR;
        try {
            if (Files.isDirectory(dir)) {
                try (Stream<Path> tree = Files.walk(dir)) {
                    List<Path> entries = tree.sorted(Comparator.reverseOrder()).toList();
                    for (Path entry : entries) {
                        Files.deleteIfExists(entry);
                    }
                }
            }
            Files.createDirectories(dir);
        } catch (IOException e) {
            fatal("Could not reset temporary directory: " + dir.toAbsolutePath(), e);
        }
    }

    // =========================================================================
    // STREAM COMPARISON
    // =========================================================================

    /**
     * Compares two input streams byte-by-byte to determine whether they
     * contain identical content.
     *
     * <p>Both streams are read in 8&nbsp;KiB chunks until one (or both)
     * reach EOF. The method returns {@code true} only if every byte
     * matches and both streams reach EOF at the same position.</p>
     *
     * @param a the first stream; must not be {@code null}
     * @param b the second stream; must not be {@code null}
     * @return {@code true} if the streams are byte-identical;
     *         {@code false} otherwise
     * @throws IOException if an I/O error occurs while reading either
     *         stream
     */
    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;
            }
            for (int i = 0; i < lenA; i++) {
                if (bufA[i] != bufB[i]) {
                    return false;
                }
            }
        }
    }

    // =========================================================================
    // NULL-SAFE DISPLAY
    // =========================================================================

    /**
     * Returns the input string if it is non-{@code null} and non-blank;
     * otherwise returns the literal {@code "(none)"}.
     *
     * @param s the input string; may be {@code null}
     * @return {@code s} or {@code "(none)"}
     */
    public static String nullSafe(final String s) {
        return (s == null || s.isBlank()) ? "(none)" : s;
    }

    /**
     * Returns the material version as a string if it is non-{@code null};
     * otherwise returns the literal {@code "n/a"} (e.g. utility mode, or
     * streaming flows where the version rides the JOSE artefact).
     *
     * @param v the material version; may be {@code null}
     * @return {@code v} as a string or {@code "n/a"}
     */
    public static String versionSafe(final Integer v) {
        return (v == null) ? "n/a" : String.valueOf(v);
    }

    // =========================================================================
    // PRETTY-PRINTERS: ENCRYPTION / DECRYPTION
    // =========================================================================

    /**
     * Prints encryption operation metadata to {@code stdout}.
     *
     * <p>Displays the key requested, the key-material version, the
     * algorithm, and any non-fatal warnings returned by the server.</p>
     *
     * @param r the encryption result; must not be {@code null}
     * @see #printDecryptMeta(DecryptResultMetadata)
     * @see #printDecryptMeta(DecryptResult)
     */
    public static void printEncryptMeta(final EncryptResult r) {
        System.out.println("    * Key requested : " + nullSafe(r.getKeyRequested()));
        System.out.println("    * Material ver. : " + versionSafe(r.getMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getAlgorithmUsed()));
        printWarnings(r.getWarnings());
    }

    /**
     * Prints decryption operation metadata to {@code stdout}.
     *
     * <p>Displays the key requested, the key-material version, the
     * algorithm, and any non-fatal warnings.</p>
     *
     * @param m the decryption metadata; must not be {@code null}
     * @see #printDecryptMeta(DecryptResult)
     * @see #printEncryptMeta(EncryptResult)
     */
    public static void printDecryptMeta(final DecryptResultMetadata m) {
        System.out.println("    * Key requested : " + nullSafe(m.getKeyRequested()));
        System.out.println("    * Material ver. : " + versionSafe(m.getMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(m.getAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    /**
     * Prints decryption metadata extracted from a full
     * {@link DecryptResult}, preceded by a section header.
     *
     * @param r the decryption result containing embedded metadata;
     *          must not be {@code null}
     * @see #printDecryptMeta(DecryptResultMetadata)
     */
    public static void printDecryptMeta(final DecryptResult r) {
        System.out.println("----- DECRYPT METADATA -----");
        System.out.println("    * Key requested : " + nullSafe(r.getMeta().getKeyRequested()));
        System.out.println("    * Material ver. : " + versionSafe(r.getMeta().getMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getMeta().getAlgorithmUsed()));
        printWarnings(r.getMeta().getWarnings());
    }

    // =========================================================================
    // PRETTY-PRINTERS: SIGNATURE / VERIFICATION
    // =========================================================================

    /**
     * Prints signature operation metadata to {@code stdout}.
     *
     * @param r the signature result; must not be {@code null}
     * @see #printSignMeta(String, SignResult)
     * @see #printVerifyMeta(VerifySignatureResult)
     */
    public static void printSignMeta(final SignResult r) {
        System.out.println("    * Key requested : " + nullSafe(r.getKeyRequested()));
        System.out.println("    * Material ver. : " + versionSafe(r.getMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getAlgorithmUsed()));
        printWarnings(r.getWarnings());
    }

    /**
     * Prints signature metadata preceded by a contextual heading.
     *
     * @param heading a section label displayed before the metadata
     * @param m       the signature result; must not be {@code null}
     * @see #printSignMeta(SignResult)
     */
    public static void printSignMeta(final String heading, final SignResult m) {
        System.out.println("----- " + heading + " -----");
        printSignMeta(m);
    }

    /**
     * Prints signature verification metadata to {@code stdout}.
     *
     * @param r the verification result; must not be {@code null}
     * @see #printVerifyMeta(String, VerifySignatureResult)
     * @see #printSignMeta(SignResult)
     */
    public static void printVerifyMeta(final VerifySignatureResult r) {
        System.out.println("    * Key requested : " + nullSafe(r.getKeyRequested()));
        System.out.println("    * Material ver. : " + versionSafe(r.getMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getAlgorithmUsed()));
        printWarnings(r.getWarnings());
    }

    /**
     * Prints signature verification metadata preceded by a contextual
     * heading and including the validity flag.
     *
     * @param heading a section label displayed before the metadata
     * @param m       the verification result; must not be {@code null}
     * @see #printVerifyMeta(VerifySignatureResult)
     */
    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("    * Material ver. : " + versionSafe(m.getMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(m.getAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    // =========================================================================
    // PRETTY-PRINTERS: RE-ENCRYPTION / RE-SIGNATURE
    // =========================================================================

    /**
     * Prints re-encryption metadata showing both the old and new key
     * and algorithm details.
     *
     * @param m the re-encryption result; must not be {@code null}
     * @see #printEncryptMeta(EncryptResult)
     * @see #printDecryptMeta(DecryptResultMetadata)
     */
    public static void printReencryptMeta(final ReencryptResult m) {
        System.out.println("    * Old requested : " + nullSafe(m.getOldKeyRequested()));
        System.out.println("      Old mat. ver. : " + versionSafe(m.getSourceMaterialVersion()));
        System.out.println("      Old algorithm : " + nullSafe(m.getOldKeyAlgorithmUsed()));
        System.out.println("    * New requested : " + nullSafe(m.getNewKeyRequested()));
        System.out.println("      New mat. ver. : " + versionSafe(m.getTargetMaterialVersion()));
        System.out.println("      New algorithm : " + nullSafe(m.getNewKeyAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    /**
     * Prints re-signature metadata showing both the old and new key
     * and algorithm details.
     *
     * @param m the re-signature result; must not be {@code null}
     * @see #printSignMeta(SignResult)
     */
    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] mat. ver. : " + versionSafe(m.getSourceMaterialVersion()));
        System.out.println("    * [Old] algorithm : " + nullSafe(m.getOldKeyAlgorithmUsed()));
        System.out.println("    * [New] requested : " + nullSafe(m.getNewKeyRequested()));
        System.out.println("    * [New] mat. ver. : " + versionSafe(m.getTargetMaterialVersion()));
        System.out.println("    * [New] algorithm : " + nullSafe(m.getNewKeyAlgorithmUsed()));
        printWarnings(m.getWarnings());
    }

    // =========================================================================
    // PRETTY-PRINTERS: COMBINED OPERATIONS
    // =========================================================================

    /**
     * Prints metadata for a sign-then-encrypt combined operation,
     * showing both the signature layer and the encryption layer.
     *
     * @param r the combined result; must not be {@code null}
     * @see #printDecryptVerifyMeta(DecryptVerifyResult)
     */
    public static void printSignEncryptMeta(final SignEncryptResult r) {
        System.out.println("----- SIGN-ENCRYPT METADATA -----");
        System.out.println("  [SIGN LAYER]");
        System.out.println("    * Key requested : " + nullSafe(r.getSignKeyRequested()));
        System.out.println("    * Material ver. : " + versionSafe(r.getSignMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getSignAlgorithmUsed()));
        printWarnings(r.getSignWarnings());
        System.out.println("  [ENCRYPT LAYER]");
        System.out.println("    * Key requested : " + nullSafe(r.getEncryptKeyRequested()));
        System.out.println("    * Material ver. : " + versionSafe(r.getEncryptMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getEncryptAlgorithmUsed()));
        printWarnings(r.getEncryptWarnings());
    }

    /**
     * Prints metadata for a decrypt-then-verify combined operation,
     * showing both the decryption layer and the verification layer.
     *
     * @param r the combined result; must not be {@code null}
     * @see #printSignEncryptMeta(SignEncryptResult)
     */
    public static void printDecryptVerifyMeta(final DecryptVerifyResult r) {
        System.out.println("----- DECRYPT-VERIFY METADATA -----");
        System.out.println("  [DECRYPT LAYER]");
        System.out.println("    * Key requested : " + nullSafe(r.getDecryptKeyRequested()));
        System.out.println("    * Material ver. : " + versionSafe(r.getDecryptMaterialVersion()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getDecryptAlgorithmUsed()));
        printWarnings(r.getDecryptWarnings());
        System.out.println("  [VERIFY LAYER]");
        System.out.println("    * Signature valid : " + r.isSignatureValid());
        System.out.println("    * Signing key     : " + nullSafe(r.getSigningKey()));
        System.out.println("    * Material ver.   : " + versionSafe(r.getVerifyMaterialVersion()));
        System.out.println("    * Algorithm       : " + nullSafe(r.getSignAlgorithmUsed()));
        if (r.getSignerInfo() != null) {
            System.out.println("    * Signer info     : " + r.getSignerInfo());
        }
        printWarnings(r.getVerifyWarnings());
    }

    // =========================================================================
    // PRETTY-PRINTERS: WARNINGS
    // =========================================================================

    /**
     * Prints a list of warning messages to {@code stdout}. Each warning
     * is indented and prefixed with a bullet. Does nothing if the list
     * is {@code null} or empty.
     *
     * @param warnings the warning messages; may be {@code null}
     */
    public static void printWarnings(final List<CryptoWarning> warnings) {
        if (warnings != null && !warnings.isEmpty()) {
            System.out.println("    * Warnings:");
            warnings.forEach(w -> System.out.println("      - " + w.message()));
        }
    }
}

Usage in Integration Flows

All 21 integration flows use ExampleUtil for common functionality. Here's how each feature is used:

1. Loading Configuration

Method: loadProperties()

Properties props = ExampleUtil.loadProperties();

Configuration file search order:

  1. Current directory: ./cli.properties
  2. System property: -Dcli.config=/path/to/sdk-config.properties
  3. Classpath resource: /cli.properties

Required properties:

# Application credentials
clientId=f23aa3b4-5c36-45e0-bfd5-730d1dfddad3
clientSecret=Cr1_xY9zAb2CdE4fGh5IjK6lMn7OpQ8rSt0U

# Or encrypted credentials (if using authenticate() method)
client.uuid=...
client.salt=...
clientIdEnc=...
clientSecretEnc=...

# API connection
openapi.scheme=https
openapi.host=demo.ankatech.co
openapi.port=443

2. Authentication

Method: authenticate(props) - Credential-based (standard)

AuthenticatedSdk sdk = ExampleUtil.authenticate(props);

How it works:

  1. Loads encrypted credentials from properties (clientIdEnc, clientSecretEnc)
  2. Derives decryption key using PBKDF2-HMAC-SHA256 (150,000 iterations)
  3. Decrypts credentials using AES-GCM
  4. Creates factory, calls factory.authenticateApplication(clientId, clientSecret)
  5. Returns AuthenticatedSdk instance (immutable, thread-safe)

Method: authenticate(props) — the only authentication entry point

AuthenticatedSdk sdk = ExampleUtil.authenticate(props);

How it works:

  1. Reads client.accessToken from properties
  2. Bypasses authentication flow
  3. Returns SDK instance with pre-issued token

Use for: Debugging or CI environments where token is issued externally


3. Temporary Directory Management

Constant: TEMP_DIR = Path.of("temp_files")

Method: ensureTempDir()

ExampleUtil.ensureTempDir();  // Creates ./temp_files/ if not exists
Path outputFile = ExampleUtil.TEMP_DIR.resolve("encrypted.jwe");

Purpose: All integration flows write output files to temp_files/ directory for easy cleanup


4. Printing Operation Metadata

ExampleUtil provides formatted output for all cryptographic operation results.

Encryption:

EncryptResult result = sdk.encrypt(kid, plaintext);
ExampleUtil.printEncryptMeta(result);

Decryption:

DecryptResult result = sdk.decrypt(ciphertext);
ExampleUtil.printDecryptMeta(result.getMeta());

Signing:

SignResult result = sdk.sign(kid, payload);
ExampleUtil.printSignMeta(result);

Verification:

VerifySignatureResult result = sdk.verifySignature(jws);
ExampleUtil.printVerifyMeta(result);

Re-encryption:

ReencryptResult result = sdk.reencrypt(newKid, jweToken);
ExampleUtil.printReencryptMeta(result);

Re-signing:

ResignResult result = sdk.resign(newKid, jws);
ExampleUtil.printResignMeta(result);

Sign-then-encrypt:

SignEncryptResult result = sdk.signThenEncrypt(signKid, encryptKid, payload);
ExampleUtil.printSignEncryptMeta(result);

Decrypt-then-verify:

DecryptVerifyResult result = sdk.decryptThenVerify(token);
ExampleUtil.printDecryptVerifyMeta(result);

5. Error Handling

try {
    // ... operation
} catch (Exception e) {
    ExampleUtil.fatal("Operation failed", e);
}

Provides:

  • Contextual error messages
  • HTTP status code interpretation
  • Network troubleshooting hints
  • Optional stack traces (via -Dankasecure.debugStack=true)

getHttpStatusMessage has no 422 arm — a semantic refusal falls through to default

The status labeller shown above enumerates 400, 401, 403, 404, 500, 502, 503 and 504. It has no 422 arm, so a semantic refusal renders through default -> "HTTP " + status as the bare string HTTP 422, with none of the guidance the other statuses get.

That matters more than it used to. Several conditions that previously surfaced as a 500 — a refused rotation, an unsupported PKCS#7 structure, a purpose that could not be inferred — now surface as a 422 carrying a specific problem type. Under this labeller they are reported with the least informative label of any status the operation can return.

The example utility is a copy of the SDK's own ExampleUtil source, reproduced here verbatim, so this page deliberately does not patch the switch — a documentation-only edit would make the published listing disagree with the shipped class, which is precisely the defect class this contract work exists to remove. In your own copy of ExampleUtil, add an arm for 422 and, more usefully, read type from the application/problem+json body rather than labelling the status at all. See Dispatch on type.

6. Stream Comparison

try (InputStream original = Files.newInputStream(originalFile);
     InputStream decrypted = Files.newInputStream(decryptedFile)) {
    boolean match = ExampleUtil.streamsAreEqual(original, decrypted);
    System.out.println("Files match: " + match);
}

How to Use in Your Project

Step 1: Add SDK Dependency

Maven:

<dependency>
    <groupId>co.ankatech.secureclient</groupId>
    <artifactId>AnkaSecureSDK</artifactId>
    <version>3.0.0</version>
</dependency>

Gradle:

implementation 'co.ankatech.secureclient:AnkaSecureSDK:3.0.0'

Step 2: Copy ExampleUtil to Your Project

Create package directory:

mkdir -p src/main/java/co/ankatech/ankasecure/sdk/examples/

Copy the complete Java class from the section above to:

src/main/java/co/ankatech/ankasecure/sdk/examples/ExampleUtil.java

Step 3: Copy Integration Flow Examples

Choose any flow from the Integration Flows Catalogue and copy to your project:

# Example: Copy Flow 1 (Asymmetric Encrypt/Decrypt Streaming)
# Copy code from flow1.md to:
# src/main/java/co/ankatech/ankasecure/sdk/examples/Flow1AsymmetricEncryptDecryptStream.java

Step 4: Create Configuration File

Create cli.properties or sdk-config.properties in your project root:

# Application credentials (provided by your administrator)
clientId=f23aa3b4-5c36-45e0-bfd5-730d1dfddad3
clientSecret=Cr1_xY9zAb2CdE4fGh5IjK6lMn7OpQ8rSt0U

# API endpoint
openapi.scheme=https
openapi.host=demo.ankatech.co
openapi.port=443

# TLS (set to true only for dev with self-signed certs)
openapi.insecureSkipTlsVerify=false

Getting credentials:

Your credentials (Client ID and Client Secret) are provided by your organization's administrator or through your AnkaSecure account portal. Contact your administrator or support at [email protected] if you need credentials.


Step 5: Compile

Using Maven:

mvn clean compile

Using javac directly:

javac -cp "AnkaSecureSDK-3.0.0.jar:." \
  src/main/java/co/ankatech/ankasecure/sdk/examples/ExampleUtil.java \
  src/main/java/co/ankatech/ankasecure/sdk/examples/Flow1AsymmetricEncryptDecryptStream.java

Step 6: Run

Using Maven:

mvn exec:java -Dexec.mainClass="co.ankatech.ankasecure.sdk.examples.Flow1AsymmetricEncryptDecryptStream"

Using java directly:

java -cp "AnkaSecureSDK-3.0.0.jar:target/classes" \
  co.ankatech.ankasecure.sdk.examples.Flow1AsymmetricEncryptDecryptStream

Expected output:

Loaded config from /path/to/cli.properties
Authenticated clientId=f23aa3b4-5c36-45e0-bfd5-730d1dfddad3
✓ Generating ML-KEM-768 key...
✓ Encrypting file with streaming...
    * Key requested : ml-kem-key
    * Material ver. : n/a
    * Algorithm     : ML-KEM-768
✓ Decrypting file with streaming...
✓ Files match: true

Method Reference

Authentication Methods

Method Parameters Returns Purpose
loadProperties() None Properties Load configuration from file
authenticate(props) Properties AuthenticatedSdk Authenticate with encrypted credentials

Metadata Printing Methods

Method Parameters Purpose
printEncryptMeta(result) EncryptResult Print encryption operation metadata
printDecryptMeta(result) DecryptResultMetadata Print decryption operation metadata
printSignMeta(result) SignResult Print signing operation metadata
printVerifyMeta(heading, result) String, VerifySignatureResult Print verification metadata with heading
printReencryptMeta(result) ReencryptResult Print re-encryption metadata (old & new keys)
printResignMeta(result) ResignResult Print re-signing metadata (old & new keys)
printSignEncryptMeta(result) SignEncryptResult Print nested sign-then-encrypt metadata
printDecryptVerifyMeta(result) DecryptVerifyResult Print nested decrypt-then-verify metadata

Utility Methods

Method Parameters Returns Purpose
ensureTempDir() None void Create temp_files/ directory
ensureTempDir(path) Path void Create custom temp directory
prepareWorkingDir() None void Create the working directory, resetting it once per JVM so a scenario runs identically standalone and inside the suite
resetTempDir() None void Recursively deletes every artefact under the working directory. Confined to that directory
streamsAreEqual(a, b) InputStream, InputStream boolean Compare two streams byte-by-byte
fatal(msg, throwable) String, Throwable void Print the error and throw ExampleFailedException, aborting the scenario. This abort style is for examples; do not adopt it in production integrations
toJson(object) Object String Serialize object to JSON

SDK Integration:


© 2025 AnkaTech. All rights reserved.