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 — 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. 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} — 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)} 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 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 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 — 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 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()
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: authenticateWithToken(props) - Token-based (debugging/CI)
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:
ReencryptResult result = sdk.reencrypt(oldKid, newKid, ciphertext);
ExampleUtil.printReencryptMeta(result);
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)
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
* 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 |
Related Documentation
SDK Integration:
- Integration Flows Catalogue - 34 complete examples
- SDK Overview - Architecture and features
- SDK Usage Guide - Setup and basic operations
© 2025 AnkaTech. All rights reserved.