SDK Security Best Practices
Audience: Developers integrating the AnkaSecure SDK into their applications Scope: Credential management, token lifecycle, secure integration patterns
This guide provides security recommendations for applications consuming the AnkaSecure SDK. Following these practices ensures your integration maintains the same security posture as the cryptographic operations you're protecting.
1. Credential Management
1.1 Never Hardcode Credentials
Critical Rule: Never embed clientId, clientSecret, or user credentials directly in source code.
// ❌ NEVER DO THIS
String clientId = "my-app-id";
String clientSecret = "super-secret-value"; // Exposed in version control!
// ✅ CORRECT: Load from secure source
String clientId = System.getenv("ANKASECURE_CLIENT_ID");
String clientSecret = System.getenv("ANKASECURE_CLIENT_SECRET");
Why it matters: Credentials in source code can be:
- Exposed in version control history (even after deletion)
- Discovered by automated scanning tools
- Leaked through code sharing or repository access
1.2 Credential Storage Hierarchy
Use this priority order for credential storage (most secure first):
| Priority | Method | Use Case |
|---|---|---|
| 1 | Hardware Security Module (HSM) | Enterprise, high-security environments |
| 2 | Cloud Secret Manager | AWS Secrets Manager, Azure Key Vault, GCP Secret Manager |
| 3 | Environment Variables | Container deployments, CI/CD pipelines |
| 4 | Encrypted Configuration Files | Desktop applications, local development |
| 5 | Plain Configuration Files | Never in production |
1.3 Encrypted Credential Storage (When Persistence is Required)
If your application must persist credentials locally (e.g., CLI tools, desktop applications), implement encryption at rest:
Recommended approach:
- Generate a unique encryption key per installation using PBKDF2 with high iteration count
- Encrypt credentials using AES-256-GCM (authenticated encryption)
- Store encrypted values with unique IV per encryption
- Protect the key derivation material (e.g., machine-specific identifier + user secret)
Key derivation parameters:
// Recommended PBKDF2 parameters (NIST SP 800-132 compliant)
Algorithm: PBKDF2WithHmacSHA256
Iterations: 150,000 minimum (adjust based on target hardware)
Salt: 128 bits, cryptographically random, unique per installation
Key length: 256 bits
Encryption parameters:
// Recommended AES-GCM parameters
Algorithm: AES/GCM/NoPadding
IV: 96 bits (12 bytes), cryptographically random, unique per encryption
Tag length: 128 bits
Storage format example:
# Encrypted credential storage format
client.salt=<random-hex-salt>
client.uuid=<installation-unique-identifier>
clientIdEnc=<base64(IV || ciphertext || tag)>
clientSecretEnc=<base64(IV || ciphertext || tag)>
Key Material Protection
The key derivation inputs (salt, UUID) do not need encryption, but the installation identifier should be machine-specific and not easily guessable. Consider using:
- Machine hardware identifiers
- User-specific entropy
- Installation-time generated UUIDs
1.4 File Permissions
When storing credentials locally, restrict file access:
Unix/Linux/macOS:
chmod 600 ~/.myapp/credentials.properties # Owner read/write only
chmod 700 ~/.myapp/ # Owner access only
Windows:
# PowerShell: Restrict to current user only
$path = "$env:USERPROFILE\.myapp\credentials.properties"
$acl = Get-Acl $path
$acl.SetAccessRuleProtection($true, $false) # Disable inheritance
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
$env:USERNAME, "FullControl", "Allow")
$acl.SetAccessRule($rule)
Set-Acl $path $acl
2. Token Lifecycle Management
2.1 Understanding SDK Token Behavior
The AnkaSecure SDK uses immutable authenticated instances:
// Factory creates unauthenticated SDK
AnkaSecureSdk factory = new AnkaSecureSdk(props);
// Authentication returns immutable, thread-safe instance
AuthenticatedSdk sdk = factory.authenticateApplication(clientId, clientSecret);
// Token is embedded in the AuthenticatedSdk instance
// Instance is valid until token expires
Key characteristics:
AuthenticatedSdkis immutable — token cannot be refreshed in-place- Instance is thread-safe — share across 10,000+ concurrent threads
- Token expiration must be checked by the consumer
- Re-authentication creates a new
AuthenticatedSdkinstance
2.2 Token Expiration Handling
Always check token validity before critical operations:
public class SecureSdkManager {
private final AnkaSecureSdk factory;
private volatile AuthenticatedSdk sdk;
private final String clientId;
private final String clientSecret;
public SecureSdkManager(Properties props, String clientId, String clientSecret)
throws AnkaSecureSdkException {
this.factory = new AnkaSecureSdk(props);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.sdk = factory.authenticateApplication(clientId, clientSecret);
}
public synchronized AuthenticatedSdk getSdk() throws AnkaSecureSdkException {
// Check if current token is expired or about to expire (5 min buffer)
if (sdk.isTokenExpired() || sdk.getTokenExpiresIn() < 300) {
// Re-authenticate to get fresh token
sdk = factory.authenticateApplication(clientId, clientSecret);
}
return sdk;
}
}
Alternative: Lazy re-authentication on failure:
public EncryptResult encryptWithRetry(String kid, byte[] data)
throws AnkaSecureSdkException {
try {
return sdk.encrypt(kid, data);
} catch (AnkaSecureSdkException e) {
if (isTokenExpiredError(e)) {
// Re-authenticate and retry once
sdk = factory.authenticateApplication(clientId, clientSecret);
return sdk.encrypt(kid, data);
}
throw e;
}
}
private boolean isTokenExpiredError(AnkaSecureSdkException e) {
return e.getMessage().contains("401") ||
e.getMessage().contains("Unauthorized") ||
e.getMessage().contains("token expired");
}
2.3 Token Storage Recommendations
| Scenario | Recommendation |
|---|---|
| Short-lived operations | Don't persist tokens; re-authenticate per session |
| Long-running services | Keep AuthenticatedSdk in memory; refresh proactively |
| Serverless functions | Authenticate per invocation (tokens are short-lived) |
| Desktop applications | Persist encrypted credentials, not tokens |
Why Not Persist Tokens?
JWT tokens have built-in expiration. Persisting tokens adds complexity (encryption, expiry tracking) without benefit. Persist credentials (encrypted), not tokens.
3. Memory Security
3.1 Credential Handling in Memory
Use char[] instead of String for sensitive data:
// ❌ String is immutable and stays in memory until GC
String password = "my-secret";
// ✅ char[] can be explicitly cleared
char[] password = new char[]{'m','y','-','s','e','c','r','e','t'};
try {
// Use password...
} finally {
Arrays.fill(password, '\0'); // Clear from memory
}
Why it matters: Java Strings are immutable and remain in memory until garbage collected. Sensitive data in Strings can be:
- Extracted from heap dumps
- Found in memory analysis
- Persisted in swap files
3.2 Secure Credential Input
Console password input (CLI applications):
Console console = System.console();
if (console != null) {
char[] password = console.readPassword("Enter client secret: ");
try {
String secret = new String(password);
AuthenticatedSdk sdk = factory.authenticateApplication(clientId, secret);
// Use sdk...
} finally {
Arrays.fill(password, '\0');
}
}
GUI password input:
JPasswordField passwordField = new JPasswordField();
// ... user enters password ...
char[] password = passwordField.getPassword();
try {
// Use password...
} finally {
Arrays.fill(password, '\0');
passwordField.setText(""); // Clear UI field
}
3.3 Avoid Logging Sensitive Data
Configure logging to exclude sensitive fields:
// ❌ NEVER log credentials or tokens
logger.info("Authenticating with clientId={}, secret={}", clientId, secret);
logger.debug("Token: {}", sdk.getAccessToken());
// ✅ Log operation metadata only
logger.info("Authenticating application: clientId={}", clientId);
logger.debug("Authentication successful, token expires in {} seconds",
sdk.getTokenExpiresIn());
Logback configuration to redact sensitive patterns:
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</layout>
<!-- Add custom converter to redact patterns like "secret=..." -->
</encoder>
4. Secure Integration Patterns
4.1 Singleton vs Per-Request SDK
Singleton pattern (recommended for most applications):
public class SdkSingleton {
private static volatile AuthenticatedSdk instance;
private static final Object lock = new Object();
public static AuthenticatedSdk getInstance(Properties props,
String clientId,
String clientSecret)
throws AnkaSecureSdkException {
if (instance == null || instance.isTokenExpired()) {
synchronized (lock) {
if (instance == null || instance.isTokenExpired()) {
AnkaSecureSdk factory = new AnkaSecureSdk(props);
instance = factory.authenticateApplication(clientId, clientSecret);
}
}
}
return instance;
}
}
Benefits:
- Single authentication per token lifetime
- Thread-safe sharing across all operations
- Automatic re-authentication on expiration
Per-request pattern (serverless, high-isolation requirements):
// AWS Lambda handler example
public class CryptoHandler implements RequestHandler<APIGatewayProxyRequestEvent,
APIGatewayProxyResponseEvent> {
@Override
public APIGatewayProxyResponseEvent handleRequest(
APIGatewayProxyRequestEvent event, Context context) {
// Authenticate per invocation (short-lived context)
Properties props = loadPropertiesFromSecretManager();
String clientId = props.getProperty("clientId");
String clientSecret = props.getProperty("clientSecret");
AnkaSecureSdk factory = new AnkaSecureSdk(props);
AuthenticatedSdk sdk = factory.authenticateApplication(clientId, clientSecret);
// Perform operation
EncryptResult result = sdk.encrypt(event.getBody().getBytes());
// SDK instance is garbage collected after invocation
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(result.getJweToken());
}
}
4.2 Connection Security
Always use TLS in production:
Development/testing only:
Never disable TLS verification in production
Setting insecureSkipTlsVerify=true in production exposes your application to man-in-the-middle attacks, completely bypassing transport security.
4.3 Timeout Configuration
Configure timeouts to prevent resource exhaustion:
# Recommended timeout settings (milliseconds)
openapi.connectTimeoutMs=10000 # 10 seconds to establish connection
openapi.readTimeoutMs=30000 # 30 seconds for response (adjust for large files)
openapi.writeTimeoutMs=30000 # 30 seconds for request upload
For streaming operations with large files:
# Extended timeouts for large file operations
openapi.readTimeoutMs=300000 # 5 minutes
openapi.writeTimeoutMs=300000 # 5 minutes
5. Error Handling Security
5.1 Secure Error Messages
Don't expose internal details in error responses:
// ❌ Exposes sensitive information
catch (AnkaSecureSdkException e) {
return "Authentication failed: " + e.getMessage();
// Could reveal: "Invalid client secret for clientId=prod-app"
}
// ✅ Generic user-facing message with internal logging
catch (AnkaSecureSdkException e) {
String correlationId = UUID.randomUUID().toString();
logger.error("Authentication failed [correlationId={}]: {}",
correlationId, e.getMessage());
return "Authentication failed. Reference: " + correlationId;
}
5.2 Rate Limiting Awareness
The AnkaSecure API implements rate limiting. Handle 429 responses gracefully:
public EncryptResult encryptWithBackoff(String kid, byte[] data)
throws AnkaSecureSdkException {
int maxRetries = 3;
int baseDelayMs = 1000;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
return sdk.encrypt(kid, data);
} catch (AnkaSecureSdkException e) {
if (isRateLimitError(e) && attempt < maxRetries - 1) {
int delay = baseDelayMs * (int) Math.pow(2, attempt);
logger.warn("Rate limited, retrying in {} ms", delay);
Thread.sleep(delay);
} else {
throw e;
}
}
}
throw new AnkaSecureSdkException("Max retries exceeded");
}
private boolean isRateLimitError(AnkaSecureSdkException e) {
return e.getMessage().contains("429") ||
e.getMessage().contains("rate limit");
}
6. Compliance Considerations
6.1 Audit Trail
For regulated environments, maintain cryptographic operation logs:
public class AuditingSdkWrapper {
private final AuthenticatedSdk sdk;
private final AuditLogger auditLogger;
public EncryptResult encrypt(String kid, byte[] data)
throws AnkaSecureSdkException {
String operationId = UUID.randomUUID().toString();
auditLogger.logOperationStart("ENCRYPT", operationId, kid, data.length);
try {
EncryptResult result = sdk.encrypt(kid, data);
auditLogger.logOperationSuccess("ENCRYPT", operationId);
return result;
} catch (AnkaSecureSdkException e) {
auditLogger.logOperationFailure("ENCRYPT", operationId, e.getMessage());
throw e;
}
}
}
Audit log fields:
- Timestamp (ISO 8601)
- Operation type (ENCRYPT, DECRYPT, SIGN, VERIFY, etc.)
- Key ID (kid)
- Data size (bytes)
- Outcome (SUCCESS/FAILURE)
- Correlation ID
- User/application identity
6.2 Key Usage Tracking
The SDK and Core API track key usage automatically. Query usage for compliance reporting:
// Get key metadata including usage statistics
KeyMetadata metadata = sdk.getKeyMetadata("compliance-key");
System.out.println("Total operations: " + metadata.getCurrentUsageCount());
System.out.println("Usage limit: " + metadata.getMaxUsageLimit());
System.out.println("Expires: " + metadata.getExpiresAt());
7. Security Checklist
Before deploying your SDK integration, verify:
Credential Management
- [ ] No credentials hardcoded in source code
- [ ] Credentials loaded from secure source (env vars, secret manager, encrypted file)
- [ ] If persisting credentials locally, using AES-256-GCM encryption
- [ ] Configuration files have restricted permissions (600 on Unix)
- [ ] Credentials excluded from version control (
.gitignore)
Token Handling
- [ ] Token expiration checked before operations
- [ ] Re-authentication logic implemented
- [ ] Tokens NOT persisted to disk
- [ ] Token errors handled gracefully
Memory Security
- [ ] Sensitive data cleared from memory when no longer needed
- [ ] Using
char[]instead ofStringfor passwords where possible - [ ] No credentials or tokens in log output
- [ ] Log levels appropriate for production
Network Security
- [ ] TLS enabled (
https://) - [ ]
insecureSkipTlsVerify=falsein production - [ ] Appropriate timeouts configured
- [ ] Rate limit handling implemented
Error Handling
- [ ] Generic error messages to users
- [ ] Detailed errors logged server-side only
- [ ] Correlation IDs for error tracking
- [ ] No sensitive data in error responses
8. Example: Complete Secure Integration
package com.example.secure;
import co.ankatech.ankasecure.sdk.AnkaSecureSdk;
import co.ankatech.ankasecure.sdk.AuthenticatedSdk;
import co.ankatech.ankasecure.sdk.exception.AnkaSecureSdkException;
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.security.SecureRandom;
import java.util.Base64;
import java.util.Properties;
import java.util.UUID;
/**
* Secure SDK integration example demonstrating:
* - Encrypted credential storage
* - Token lifecycle management
* - Thread-safe singleton pattern
*/
public class SecureSdkIntegration {
private static final int PBKDF2_ITERATIONS = 150_000;
private static final int KEY_LENGTH_BITS = 256;
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
private final AnkaSecureSdk factory;
private volatile AuthenticatedSdk sdk;
private final Object authLock = new Object();
private final String clientId;
private final String clientSecret;
/**
* Initialize with encrypted credentials from secure storage.
*/
public SecureSdkIntegration(Properties config) throws Exception {
// Load connection properties
Properties sdkProps = new Properties();
sdkProps.setProperty("openapi.scheme", config.getProperty("openapi.scheme", "https"));
sdkProps.setProperty("openapi.host", config.getProperty("openapi.host"));
sdkProps.setProperty("openapi.port", config.getProperty("openapi.port", "443"));
// Decrypt credentials
String uuid = config.getProperty("client.uuid");
String salt = config.getProperty("client.salt");
String clientIdEnc = config.getProperty("clientIdEnc");
String clientSecretEnc = config.getProperty("clientSecretEnc");
byte[] key = deriveKey(uuid, salt);
this.clientId = decrypt(clientIdEnc, key);
this.clientSecret = decrypt(clientSecretEnc, key);
// Clear key from memory
java.util.Arrays.fill(key, (byte) 0);
// Initialize factory and authenticate
this.factory = new AnkaSecureSdk(sdkProps);
this.sdk = factory.authenticateApplication(clientId, clientSecret);
}
/**
* Get authenticated SDK, refreshing token if needed.
*/
public AuthenticatedSdk getSdk() throws AnkaSecureSdkException {
if (sdk.isTokenExpired()) {
synchronized (authLock) {
if (sdk.isTokenExpired()) {
sdk = factory.authenticateApplication(clientId, clientSecret);
}
}
}
return sdk;
}
// --- Credential encryption utilities ---
public static void encryptAndStoreCredentials(
String clientId,
String clientSecret,
File outputFile) throws Exception {
// Generate unique installation identifier and salt
String uuid = UUID.randomUUID().toString().replace("-", "");
byte[] saltBytes = new byte[16];
new SecureRandom().nextBytes(saltBytes);
String salt = bytesToHex(saltBytes);
// Derive encryption key
byte[] key = deriveKey(uuid, salt);
// Encrypt credentials
String clientIdEnc = encrypt(clientId, key);
String clientSecretEnc = encrypt(clientSecret, key);
// Clear key
java.util.Arrays.fill(key, (byte) 0);
// Store properties
Properties props = new Properties();
props.setProperty("client.uuid", uuid);
props.setProperty("client.salt", salt);
props.setProperty("clientIdEnc", clientIdEnc);
props.setProperty("clientSecretEnc", clientSecretEnc);
// Write with restricted permissions
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
props.store(fos, "Encrypted AnkaSecure credentials");
}
// Set file permissions (Unix)
outputFile.setReadable(false, false);
outputFile.setWritable(false, false);
outputFile.setReadable(true, true);
outputFile.setWritable(true, true);
}
private static byte[] deriveKey(String uuid, String saltHex) throws Exception {
byte[] salt = hexToBytes(saltHex);
PBEKeySpec spec = new PBEKeySpec(
uuid.toCharArray(),
salt,
PBKDF2_ITERATIONS,
KEY_LENGTH_BITS
);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return factory.generateSecret(spec).getEncoded();
}
private static String encrypt(String plaintext, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, "AES"),
new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine IV + ciphertext
byte[] combined = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(combined);
}
private static String decrypt(String base64Ciphertext, byte[] key) throws Exception {
byte[] combined = Base64.getDecoder().decode(base64Ciphertext);
byte[] iv = new byte[GCM_IV_LENGTH];
byte[] ciphertext = new byte[combined.length - GCM_IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH);
System.arraycopy(combined, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(key, "AES"),
new GCMParameterSpec(GCM_TAG_LENGTH, iv));
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private static byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
}
Next Steps
- SDK Usage Guide: Basic SDK operations
- Integration Flows: 34 detailed integration examples
© 2025 AnkaTech. All rights reserved.