Example Utility Class
All integration flows import co.ankatech.ankasecure.sdk.examples.ExampleUtil
for:
- encrypted-credential authentication
- property loading (
cli.properties
,-Dcli.config
) - JSON pretty-printing with Java Time
- AES-GCM helper functions
- fatal-exit convenience
// FILE: ExampleUtil.java
package co.ankatech.ankasecure.sdk.examples;
import co.ankatech.ankasecure.sdk.AnkaSecureSdk;
import co.ankatech.ankasecure.sdk.exception.AnkaSecureSdkException;
import co.ankatech.ankasecure.sdk.exception.SdkErrorCode;
import co.ankatech.ankasecure.sdk.model.*;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Properties;
/**
* Utility class providing shared support for all ANKASecure SDK example scenarios.
* <p>
* This class centralizes common functionality used by the Example* programs, including:
* <ul>
* <li>Loading CLI configuration from files or classpath resources</li>
* <li>Authenticating via encrypted credentials or a pre-issued JWT token</li>
* <li>JSON serialization and deserialization with ISO-8601 date/time support</li>
* <li>Deriving and decrypting cryptographic keys (PBKDF2, AES-GCM)</li>
* <li>Ensuring and managing a temporary working directory for example artifacts</li>
* <li>Uniform error handling that classifies SDK exceptions (timeout, HTTP, I/O, etc.)</li>
* <li>Rendering operation metadata (encrypt, decrypt, sign, verify, re-encrypt)</li>
* <li>Comparing input streams for equality</li>
* </ul>
* <p>
* All methods are static and intended for one-off use by the ExampleScenario classes.
*/
public final class ExampleUtil {
/** Working directory for the temporary artifacts produced by examples. */
public static final Path TEMP_DIR = Path.of("temp_files");
private ExampleUtil() {
/* static only */
}
/**
* A preconfigured Jackson ObjectMapper instance for JSON serialization and deserialization.
* This mapper is set up with the following configurations:
* - Registers the `JavaTimeModule` for support of ISO-8601 date/time formats.
* - Enables pretty-printing of JSON output using `SerializationFeature.INDENT_OUTPUT`.
* - Disables the writing of dates as epoch timestamps by using `SerializationFeature.WRITE_DATES_AS_TIMESTAMPS`.
* - Configures serialization to exclude null values with `JsonInclude.Include.NON_NULL`.
*/
private static final ObjectMapper JSON = new ObjectMapper()
.registerModule(new JavaTimeModule()) // ISO-8601 dates
.enable(SerializationFeature.INDENT_OUTPUT) // pretty print
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // no epoch millis
.setSerializationInclusion(JsonInclude.Include.NON_NULL); // ⬅ omit nulls
/**
* Loads properties from a configuration file.
* The method checks for a local "cli.properties" file, a file specified by
* the "cli.config" system property, and a default "cli.properties" file on the classpath.
* If none of these are found, an exception is thrown.
*
* @return a Properties object containing the key-value pairs from the loaded configuration file
*/
public static Properties loadProperties() {
Properties props = new Properties();
File local = new File("cli.properties");
try {
if (local.isFile()) {
try (InputStream in = new FileInputStream(local)) {
props.load(in);
System.out.println("Loaded config from " + local.getAbsolutePath());
return props;
}
}
String sys = System.getProperty("cli.config");
if (sys != null) {
File f = new File(sys);
if (f.isFile()) {
try (InputStream in = new FileInputStream(f)) {
props.load(in);
System.out.println("Loaded config from " + f.getAbsolutePath());
return props;
}
}
}
try (InputStream in = ExampleUtil.class.getResourceAsStream("/cli.properties")) {
if (in == null) {
fatal("Could not find cli.properties; run 'init' first.", null);
}
props.load(in);
System.out.println("Loaded config from classpath /cli.properties");
return props;
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to load configuration", e);
}
}
/**
* Creates an AnkaSecureSdk instance using a manually provided JWT token.
*
* <p>
* This method bypasses the authentication flow entirely. It is useful for
* debugging or trusted execution environments where the token is issued externally
* (e.g. from a deployed auth-service).</p>
*
* <p>
* Requires the presence of a property <code>client.accessToken</code> in
* the <code>cli.properties</code> file.</p>
*
* <p><strong>Warning:</strong> It is your responsibility to ensure the token is valid and not expired.</p>
*
* @param props CLI properties loaded via {@link #loadProperties()}
* @return a token-initialized {@link AnkaSecureSdk} instance
*/
public static AnkaSecureSdk authenticateWithToken(Properties props) throws AnkaSecureSdkException {
String token = props.getProperty("client.accessToken");
if (token == null || token.isBlank()) {
fatal("Missing required property: client.accessToken", null);
}
System.out.println("authenticateWithToken(): Using manually provided token from cli.properties");
return new AnkaSecureSdk(token, props);
}
/**
* Authenticates and initializes an instance of {@code AnkaSecureSdk} using the provided properties.
* The method retrieves client-specific credentials (UUID, salt, encrypted client ID, and encrypted client secret)
* from the given {@code Properties} object, derives a cryptographic key, decrypts the credentials,
* and performs application authentication. On successful authentication, it returns an instance of
* {@code AnkaSecureSdk}.
*
* @param props The {@code Properties} containing the necessary authentication information:
* - `client.uuid`: The unique identifier for the client.
* - `client.salt`: The salt value for key derivation.
* - `clientIdEnc`: The encrypted client ID.
* - `clientSecretEnc`: The encrypted client secret.
* All four properties must be non-null and properly initialized.
*
* @return An instance of {@code AnkaSecureSdk} if authentication is successful.
* Returns {@code null} in case of authentication or decryption errors (execution will halt with a fatal error log).
*/
public static AnkaSecureSdk authenticate(Properties props) {
String uuid = props.getProperty("client.uuid");
String salt = props.getProperty("client.salt");
String idEnc = props.getProperty("clientIdEnc");
String secEnc = props.getProperty("clientSecretEnc");
if (uuid == null || salt == null || idEnc == null || secEnc == null) {
fatal("CLI not initialised; run 'init' first.", null);
}
AnkaSecureSdk sdk = new AnkaSecureSdk(props);
try {
byte[] key = deriveKey(uuid, salt);
String clientId = decryptValue(idEnc, key);
String secret = decryptValue(secEnc, key);
sdk.authenticateApplication(clientId, secret);
System.out.println("Authenticated clientId=" + clientId);
return sdk;
} catch (AnkaSecureSdkException ex) {
fatal(MessageFormat.format(
"Authentication failed: HTTP={0}, body={1}",
ex.getHttpStatus(), ex.getResponseBody()), ex);
return null; // unreachable
} catch (Exception ex) {
fatal("Error decrypting credentials", ex);
return null; // unreachable
}
}
/**
* Converts a given object into its JSON string representation.
*
* @param o the object to be converted into JSON format
* @return a JSON string representation of the given object
* @throws UncheckedIOException if the JSON serialization process fails
*/
public static String toJson(Object o) {
try {
return JSON.writeValueAsString(o);
} catch (IOException e) {
throw new UncheckedIOException("JSON serialization failed", e);
}
}
/**
* Derives a cryptographic key using PBKDF2 with HmacSHA256.
*
* @param uuid the input string used as the basis for deriving the key
* @param saltHex the salt in hexadecimal string format used to add randomness to the key derivation
* @return the derived key as a byte array
* @throws RuntimeException when key derivation fails due to algorithm errors or invalid inputs
*/
private static byte[] deriveKey(String uuid, String saltHex) {
try {
byte[] salt = hexToBytes(saltHex);
PBEKeySpec spec = new PBEKeySpec(uuid.toCharArray(), salt, 150_000, 256);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return f.generateSecret(spec).getEncoded();
} catch (Exception e) {
throw new RuntimeException("Key derivation failed: " + e.getMessage(), e);
}
}
/**
* Converts a hexadecimal string to a byte array.
*
* @param hex the hexadecimal string to be converted, should contain an even number of characters
* @return a byte array representing the binary data of the provided hexadecimal string
*/
private static byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
out[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return out;
}
/**
* Decrypts a Base64-encoded string using AES/GCM/NoPadding encryption algorithm.
*
* @param base64 The Base64-encoded string to be decrypted. It must not be null or empty.
* @param key The secret key used for decryption as a byte array. It must be 16, 24, or 32 bytes in length as per AES requirements.
* @return The decrypted value as a string. If the input is null or blank, an empty string will be returned.
* @throws Exception If an error occurs during decryption, such as invalid input format or incorrect key.
*/
private static String decryptValue(String base64, byte[] key) throws Exception {
if (base64 == null || base64.isBlank()) {
return "";
}
byte[] blob = Base64.getDecoder().decode(base64);
byte[] iv = Arrays.copyOfRange(blob, 0, 12);
byte[] ct = Arrays.copyOfRange(blob, 12, blob.length);
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
new GCMParameterSpec(128, iv));
return new String(c.doFinal(ct), StandardCharsets.UTF_8);
}
/**
* Ensures that the specified temporary directory exists. If it does not exist, it attempts to create it.
*
* @param dir The path to the directory that needs to be checked or created.
*/
public static void ensureTempDir(Path dir) {
try {
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
} catch (IOException e) {
fatal("Could not create temporary directory: " + dir, e);
}
}
/**
* Logs a fatal error message to the standard error stream, provides additional context if the error
* is related to the SDK, and terminates the application with exit code 1.
*
* @param msg The error message to be logged.
* @param t The exception causing the fatal error, which may be an instance of
* {@link AnkaSecureSdkException} or another {@link Throwable}. Can be null.
*/
public static void fatal(String msg, Throwable t) {
if (t instanceof AnkaSecureSdkException ase) {
SdkErrorCode code = ase.getErrorCode();
switch (code) {
case TIMEOUT ->
System.err.println(msg + ": network timeout – please retry or increase timeout settings");
case HTTP ->
System.err.println(MessageFormat.format(
"{0}: server returned HTTP {1}", msg, ase.getHttpStatus()));
case IO ->
System.err.println(msg + ": I/O error – " + ase.getMessage());
default ->
System.err.println(msg + ": " + ase.getMessage());
}
} else {
// Non-SDK exceptions
System.err.println(msg + (t != null ? ": " + t.getMessage() : ""));
}
// Show full stack only when debug flag is set
if (t != null && Boolean.getBoolean("ankasecure.debugStack")) {
t.printStackTrace(System.err);
}
System.exit(1);
}
/* ====================================================================== */
/**
* Logs metadata associated with an encryption operation in a human-readable format.
* This includes information about the requested key, actual key used, algorithm utilized,
* and any warnings generated during the process.
*
* @param r the {@link EncryptResult} containing encryption metadata, which includes:
* - the key ID originally requested by the client,
* - the key ID actually used by the server,
* - the algorithm used for encryption,
* - and any warnings or informational messages associated with the operation.
*/
public static void printEncryptionMeta(final EncryptResult r) {
System.out.println(" * Key requested : " + nullSafe(r.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(r.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(r.getAlgorithmUsed()));
printWarnings(r.getWarnings());
}
/**
* Logs decryption metadata in a human-readable format. The metadata includes details
* about the key requested, the actual key used, the algorithm used in the decryption
* process, and any non-fatal warnings encountered.
*
* @param r the {@link DecryptResultMetadata} containing decryption-related information,
* including:
* - the key ID originally requested,
* - the key ID actually used,
* - the algorithm utilized,
* - and any warnings, if present.
*/
public static void printDecryptionMeta(final DecryptResultMetadata r) {
System.out.println(" * Key requested : " + nullSafe(r.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(r.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(r.getAlgorithmUsed()));
printWarnings(r.getWarnings());
}
/**
* Prints a list of warning messages to the console in a formatted manner.
* Each warning is indented and prefixed with a bullet point.
*
* @param warnings a list of strings representing warning messages to be displayed.
* If the list is null or empty, no output will be generated.
*/
public static void printWarnings(final List<String> warnings) {
if (warnings != null && !warnings.isEmpty()) {
System.out.println(" * Warnings:");
warnings.forEach(w -> System.out.println(" – " + w));
}
}
/**
* Returns a non-null, safe string representation of the provided input.
* If the input string is null or blank, the method returns the string "(none)".
*
* @param s the input string that may be null or blank
* @return the input string if it is not null or blank; otherwise, the string "(none)"
*/
public static String nullSafe(final String s) {
return (s == null || s.isBlank()) ? "(none)" : s;
}
/* ====================================================================== */
/**
* Ensures that the temporary directory represented by the constant `TEMP_DIR` exists.
* Creates the directory, including any necessary but nonexistent parent directories.
* If the operation fails, this method terminates the JVM with an error message.
* Any issues encountered during the creation process are handled by invoking the
* {@link #fatal(String, Throwable)} method, which outputs the error message
* and exits the application.
* This method is typically used to guarantee the existence of a directory
* for temporary file storage required by the application.
*
* @throws UncheckedIOException if the directory cannot be created
*/
public static void ensureTempDir() {
try {
Files.createDirectories(TEMP_DIR);
} catch (IOException e) {
fatal("Could not create temp directory: " + TEMP_DIR.toAbsolutePath(), e);
}
}
/**
* Logs metadata associated with the signing operation in a human-readable format.
* Includes details such as the requested key, actual key used, algorithm, and any warnings.
*
* @param r the {@link SignResult} containing metadata about the signing operation,
* which includes:
* - the key ID originally requested by the client,
* - the key ID actually used by the server,
* - the algorithm used during the signing process,
* - and any non-fatal warnings, if present.
*/
public static void printSignMeta(final SignResult r) {
System.out.println(" * Key requested : " + nullSafe(r.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(r.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(r.getAlgorithmUsed()));
printWarnings(r.getWarnings());
}
/**
* Logs metadata associated with the verification operation in a human-readable format.
* Includes details about the requested key, actual key used, algorithm utilized, and any warnings generated.
*
* @param r the {@link VerifySignatureResult} containing verification metadata, which includes:
* - the key ID originally requested by the client,
* - the key ID actually used by the server,
* - the algorithm used during the verification process,
* - and any non-fatal warnings, if present.
*/
public static void printVerifyMeta(final VerifySignatureResult r) {
System.out.println(" * Key requested : " + nullSafe(r.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(r.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(r.getAlgorithmUsed()));
printWarnings(r.getWarnings());
}
/* ====================================================================== */
/**
* Logs metadata associated with an encryption operation in a human-readable format.
* Includes details about the key requested, actual key used, algorithm utilized, and any warnings generated.
*
* @param m the {@link EncryptResult} containing encryption metadata, which includes:
* - the key ID originally requested by the client,
* - the key ID actually used by the server,
* - the algorithm used during the encryption process,
* - and any non-fatal warnings, if present.
*/
public static void printEncryptMeta(final EncryptResult m) {
System.out.println(" * Key requested : " + nullSafe(m.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(m.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(m.getAlgorithmUsed()));
printWarnings(m.getWarnings());
}
/**
* Logs decryption metadata in a human-readable format, including information
* about the requested key, actual key used, algorithm used, and any warnings.
*
* @param m the {@link DecryptResultMetadata} containing the metadata for the decryption process.
* It includes:
* - the key ID originally requested by the client,
* - the key ID actually used by the server,
* - the algorithm negotiated by the crypto engine,
* - and non-fatal warnings, if any.
*/
public static void printDecryptMeta(final DecryptResultMetadata m) {
System.out.println(" * Key requested : " + nullSafe(m.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(m.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(m.getAlgorithmUsed()));
printWarnings(m.getWarnings());
}
/**
* Compares two InputStream objects to determine if they have identical content.
* This method reads both streams concurrently and compares their data byte by byte.
*
* @param a the first InputStream to compare
* @param b the second InputStream to compare
* @return true if both InputStreams have identical content and reach EOF simultaneously; false otherwise
* @throws IOException if an I/O error occurs while reading from either InputStream
*/
public static boolean streamsAreEqual(InputStream a, InputStream b) throws IOException {
byte[] bufA = new byte[8192];
byte[] bufB = new byte[8192];
while (true) {
int lenA = a.read(bufA);
int lenB = b.read(bufB);
if (lenA != lenB) {
return false;
}
if (lenA == -1) {
return true; // both streams reached EOF simultaneously
}
for (int i = 0; i < lenA; i++) {
if (bufA[i] != bufB[i]) {
return false;
}
}
}
}
/**
* Logs metadata associated with the re-encryption operation in a human-readable format,
* including information about keys and algorithms used during the process, as well as any warnings.
*
* @param m the {@link ReencryptResult} containing metadata related to the re-encryption operation,
* including details such as the old key requested, old key used, old algorithm,
* new key requested, new key used, new algorithm, and any warnings associated.
*/
public static void printReencryptMeta(final ReencryptResult m) {
System.out.println(" * Old requested : " + nullSafe(m.getOldKeyRequested()));
System.out.println(" Old used : " + nullSafe(m.getOldKeyUsed()));
System.out.println(" Old algorithm : " + nullSafe(m.getOldKeyAlgorithmUsed()));
System.out.println(" * New requested : " + nullSafe(m.getNewKeyRequested()));
System.out.println(" New used : " + nullSafe(m.getNewKeyUsed()));
System.out.println(" New algorithm : " + nullSafe(m.getNewKeyAlgorithmUsed()));
printWarnings(m.getWarnings());
}
/**
* Logs metadata associated with the signing operation in a human-readable format,
* including a descriptive heading for context.
*
* @param heading a contextual or descriptive heading to prepend to the logged output
* @param m the {@link SignResult} containing metadata about the signing process
*/
/* ---------- pretty printers ------------------------------------------ */
public static void printSignMeta(final String heading, final SignResult m) {
System.out.println("----- " + heading + " -----");
printSignMeta(m);
}
/**
* Logs metadata associated with the re-signing operation in a human-readable format.
*
* @param m the {@link ResignResult} containing metadata about the re-signing process
*/
public static void printResignMeta(final ResignResult m) {
System.out.println("----- RE-SIGN METADATA -----");
System.out.println(" * [Old] requested : " + nullSafe(m.getOldKeyRequested()));
System.out.println(" * [Old] used : " + nullSafe(m.getOldKeyUsed()));
System.out.println(" * [Old] algorithm : " + nullSafe(m.getOldKeyAlgorithmUsed()));
System.out.println(" * [New] requested : " + nullSafe(m.getNewKeyRequested()));
System.out.println(" * [New] used : " + nullSafe(m.getNewKeyUsed()));
System.out.println(" * [New] algorithm : " + nullSafe(m.getNewKeyAlgorithmUsed()));
printWarnings(m.getWarnings());
}
/**
* Logs metadata associated with the verification operation in a human-readable format,
* including information about validation, keys used, algorithm, and warnings if any.
*
* @param heading a descriptive heading or context label for the logged output
* @param m the {@link VerifySignatureResult} containing verification metadata
*/
public static void printVerifyMeta(final String heading, final VerifySignatureResult m) {
System.out.println("----- " + heading + " -----");
System.out.println(MessageFormat.format(" * Valid : {0}", m.isValid()));
System.out.println(" * Key requested : " + nullSafe(m.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(m.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(m.getAlgorithmUsed()));
printWarnings(m.getWarnings());
}
/**
* Logs decryption-side metadata in a human-readable format.
*
* @param r the {@link DecryptResult} containing the decryption metadata
* and decrypted data. Its metadata includes:
* - the key ID originally requested by the client,
* - the key ID actually used by the server (or {@code null} if identical to the requested key),
* - the algorithm negotiated by the crypto-engine,
* - and non-fatal warnings, if any.
*/
public static void printDecryptMeta(final DecryptResult r) {
System.out.println("----- DECRYPT METADATA -----");
System.out.println("Key requested : " + nullSafe(r.getMeta().getKeyRequested()));
System.out.println("Key used : " + nullSafe(r.getMeta().getActualKeyUsed()));
System.out.println("Algorithm : " + nullSafe(r.getMeta().getAlgorithmUsed()));
printWarnings(r.getMeta().getWarnings());
}
}
> **Dependency** — this class imports `ExampleUtil`.
> If you have not copied `ExampleUtil.java` yet, see [example_util.md](example_util.md).
3 · Compile everything together
All other flows run the same way—no code duplication, one shared utility.