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 — 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} — unique identifier for the client</li>
* <li>{@code client.salt} — hex-encoded salt for key derivation</li>
* <li>{@code clientIdEnc} — AES-GCM-encrypted client ID (Base64)</li>
* <li>{@code clientSecretEnc} — 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 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 — an empty working directory — 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 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()
Configuration file search order:
- Current directory:
./cli.properties - System property:
-Dcli.config=/path/to/sdk-config.properties - 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)
How it works:
- Loads encrypted credentials from properties (clientIdEnc, clientSecretEnc)
- Derives decryption key using PBKDF2-HMAC-SHA256 (150,000 iterations)
- Decrypts credentials using AES-GCM
- Creates factory, calls
factory.authenticateApplication(clientId, clientSecret) - Returns
AuthenticatedSdkinstance (immutable, thread-safe)
Method: authenticate(props) — the only authentication entry point
How it works:
- Reads
client.accessTokenfrom properties - Bypasses authentication flow
- 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:
Decryption:
Signing:
Verification:
Re-encryption:
Re-signing:
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
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:
Step 2: Copy ExampleUtil to Your Project
Create package directory:
Copy the complete Java class from the section above to:
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:
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 |
Related Documentation
SDK Integration:
- Integration Flows Catalogue - 21 complete examples
- SDK Overview - Architecture and features
- SDK Usage Guide - Setup and basic operations
© 2025 AnkaTech. All rights reserved.