Skip to content

SDK Example Utilities

Class: ExampleUtil.java Package: co.ankatech.ankasecure.sdk.examples Required for: All 34 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 34 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 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.List;
import java.util.Properties;

/**
 * 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 terminates the JVM via
 * {@link System#exit(int)}. This is acceptable for standalone example
 * programs but <em>must not</em> be adopted in production SDK
 * integrations. Prefer throwing exceptions and letting the caller
 * decide how to handle failures.</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");

    /** 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)} and the JVM terminates.</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
    // =========================================================================

    /**
     * Creates an {@link AnkaSecureSdk} instance using a manually provided
     * JWT token, bypassing the normal authentication flow.
     *
     * <p>This is useful for debugging or trusted execution environments
     * where the token is issued externally (e.g.&nbsp;from a deployed
     * auth-service).</p>
     *
     * <p>Requires the property {@code client.accessToken} in
     * {@code cli.properties}. If the property is missing or blank the
     * method calls {@link #fatal(String, Throwable)} and the JVM
     * terminates.</p>
     *
     * <p><b>Warning:</b> it is your responsibility to ensure the token
     * is valid and not expired.</p>
     *
     * @param props CLI properties loaded via {@link #loadProperties()};
     *              must not be {@code null}
     * @return a token-initialised {@link AnkaSecureSdk} factory instance;
     *         never {@code null}
     * @throws AnkaSecureSdkException if the OpenAPI client cannot be
     *         initialised with the provided token
     * @see #authenticate(Properties)
     */
    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);
            throw new IllegalStateException("unreachable");
        }

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

    /**
     * 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, String)}.</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)} and the JVM terminates.</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 #authenticateWithToken(Properties)
     * @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);

            AuthenticatedSdk sdk = factory.authenticateApplication(clientId, secret);
            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;
        };
    }

    /**
     * Logs a fatal error to {@code stderr} and terminates the JVM with
     * exit code&nbsp;1.
     *
     * <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><b>Warning &mdash; JVM termination:</b> this method calls
     * {@link System#exit(int)} and <em>never returns</em>. It is
     * designed exclusively for standalone example programs. Production
     * code should throw an appropriate exception instead.</p>
     *
     * @param msg the error message to display; must not be {@code null}
     * @param t   the causal exception, or {@code null} if none
     */
    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);
        }
        System.exit(1);
    }

    // =========================================================================
    // 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)} and the JVM terminates.</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)} and the JVM terminates.</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);
        }
    }

    // =========================================================================
    // 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;
    }

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

    /**
     * Prints encryption operation metadata to {@code stdout}.
     *
     * <p>Displays the key requested, the key actually used, 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("    * Key used      : " + nullSafe(r.getActualKeyUsed()));
        System.out.println("    * Algorithm     : " + nullSafe(r.getAlgorithmUsed()));
        printWarnings(r.getWarnings());
    }

    /**
     * Prints decryption operation metadata to {@code stdout}.
     *
     * <p>Displays the key requested, the key actually used, 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("    * Key used      : " + nullSafe(m.getActualKeyUsed()));
        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("    * Key used      : " + nullSafe(r.getMeta().getActualKeyUsed()));
        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("    * Key used      : " + nullSafe(r.getActualKeyUsed()));
        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("    * Key used      : " + nullSafe(r.getActualKeyUsed()));
        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("    * Key used      : " + nullSafe(m.getActualKeyUsed()));
        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 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());
    }

    /**
     * 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] 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());
    }

    // =========================================================================
    // 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("    * Key used      : " + nullSafe(r.getActualSignKeyUsed()));
        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("    * Key used      : " + nullSafe(r.getActualEncryptKeyUsed()));
        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("    * Key used      : " + nullSafe(r.getActualDecryptKeyUsed()));
        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("    * 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<String> warnings) {
        if (warnings != null && !warnings.isEmpty()) {
            System.out.println("    * Warnings:");
            warnings.forEach(w -> System.out.println("      - " + w));
        }
    }
}

Usage in Integration Flows

All 34 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: authenticateWithToken(props) - Token-based (debugging/CI)

AnkaSecureSdk factory = ExampleUtil.authenticateWithToken(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.verify(jws);
ExampleUtil.printVerifyMeta(result);

Re-encryption:

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

Re-signing:

ResignResult result = sdk.resign(oldKid, 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)

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
    * Key used      : ml-kem-key
    * 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
authenticateWithToken(props) Properties AnkaSecureSdk (factory) Create factory with pre-issued JWT token

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
streamsAreEqual(a, b) InputStream, InputStream boolean Compare two streams byte-by-byte
fatal(msg, throwable) String, Throwable void Print error and exit with code 1
toJson(object) Object String Serialize object to JSON

SDK Integration:


© 2025 AnkaTech. All rights reserved.