File Operation Patterns - Auto-Detection & Explicit Control
Smart Defaults with Expert Control - The SDK provides intelligent file size detection while offering explicit control when needed.
This guide explains the SDK's file operation pattern, helping you choose the right method for your use case.
Pattern Overview
The AnkaSecure SDK implements a three-tier API pattern for file operations:
graph TD
A[SDK File Operation] --> B{Method Pattern}
B -->|No Suffix| C[Auto-Detection]
B -->|Compact Suffix| D[Force Compact]
B -->|Stream Suffix| E[Force Streaming]
C --> F{File Size Check<br/>vs discovered limit}
F -->|< maxPlaintextBytes| G[Compact JWE/JWS<br/>Single-line format]
F -->|>= maxPlaintextBytes| H[Streaming JWE/JWS<br/>Detached format]
D --> G
E --> H
style C fill:#4CAF50
style D fill:#2196F3
style E fill:#FF9800
style G fill:#E8F5E9
style H fill:#FFF3E0 Decision Matrix
When to Use Each Pattern
| Pattern | When to Use | Example Use Case |
|---|---|---|
Auto-DetectionencryptFile() | Default choice - handles any file size | General-purpose encryption without size concerns |
Force CompactencryptFileCompact() | Need single-line JWE/JWS for APIs or JSON | Storing encrypted tokens in databases, REST API transmission |
Force StreamingencryptFileStream() | Large files, low memory, or binary format required | Encrypting 500MB+ files, embedded systems with limited RAM |
Auto-Detection Pattern (Recommended)
How It Works
sequenceDiagram
participant App as Application
participant SDK as SDK
participant Helper as StreamingThresholdHelper
participant API as AnkaSecure API
App->>SDK: encryptFile(kid, input, output)
SDK->>Helper: shouldUseStreaming(input)
Helper->>Helper: Check file size
Helper->>API: GET /api/v3/crypto/limits (cached)
API-->>Helper: { maxPlaintextBytes }
alt File >= maxPlaintextBytes
Helper-->>SDK: true (use streaming)
SDK->>API: POST /api/v3/crypto/stream/encrypt
API-->>SDK: Detached JWE
else File < maxPlaintextBytes
Helper-->>SDK: false (use compact)
SDK->>API: POST /api/v3/crypto/encrypt
API-->>SDK: Compact JWE (or 413 → SDK retries via streaming)
end
SDK-->>App: EncryptResult Threshold Details
The auto-detection threshold is discovered from the server at runtime, not hardcoded. The SDK reads the effective compact-mode limit from GET /api/v3/crypto/limits and caches it (short TTL):
// StreamingThresholdHelper resolves the threshold dynamically:
// GET /api/v3/crypto/limits -> { "maxPlaintextBytes": 4000000 }
// shouldUseStreaming(input) == (inputSize >= maxPlaintextBytes)
// Fail-safe to a conservative code default if discovery is unavailable.
Why discover it (instead of a hardcoded constant)?
- The server limit (
crypto.max-plaintext-bytes, default 4,000,000 bytes) is operator-configurable and hot-reloadable — a hardcoded client constant would drift from the deployment. - It removes the former 5 MiB (
5_242_880) client vs 5 MB (5_000_000) server mismatch. - Compact mode loads the entire file into memory (inefficient for large files); streaming uses chunked transfer with constant memory.
413 → streaming automatic fallback. Discovery is cached, so a mid-flight operator change (or a lowered limit) could leave a stale value. To stay correct, a compact producer call that receives 413 payload-too-large transparently retries the equivalent streaming operation — the 413 is never surfaced to your code.
Example: Auto-Detection in Action
import co.ankatech.ankasecure.sdk.AuthenticatedSdk;
import java.nio.file.Path;
// Authenticate once, reuse for all operations
AuthenticatedSdk sdk = factory.authenticateApplication(clientId, clientSecret);
// Small file (2 MB) - automatically uses compact mode
Path configFile = Path.of("config.json"); // 2 MB
EncryptResult result1 = sdk.encryptFile("my-key", configFile, Path.of("config.jwe"));
// → Compact JWE: single-line Base64 string
// Large file (50 MB) - automatically uses streaming mode
Path databaseDump = Path.of("backup.sql"); // 50 MB
EncryptResult result2 = sdk.encryptFile("my-key", databaseDump, Path.of("backup.jwe"));
// → Streaming JWE: detached format with multipart structure
// You don't need to think about file sizes!
Debug logs show the decision:
DEBUG co.ankatech.cli.dev - Auto-streaming: 2097152 bytes, using compact mode
DEBUG co.ankatech.cli.dev - Auto-streaming: 52428800 bytes, using streaming mode
Explicit Control Patterns
Force Compact Mode
Use when you need a single-line string for APIs or storage.
// Force compact JWE (will fail if file >= the configured maxPlaintextBytes)
Path smallFile = Path.of("document.pdf"); // 2 MB
EncryptResult result = sdk.encryptFileCompact("my-key", smallFile, Path.of("document.jwe"));
// Read as single-line string
String jweToken = Files.readString(Path.of("document.jwe"));
// Send via REST API
JsonObject payload = new JsonObject();
payload.addProperty("encryptedData", jweToken);
httpClient.post("/your-app/store", payload);
Error handling:
try {
sdk.encryptFileCompact("my-key", largeFile, output);
} catch (AnkaSecureSdkException e) {
if (e.getHttpStatus() == 413) {
// Payload exceeded the server's compact limit -- use streaming instead
sdk.encryptFileStream("my-key", largeFile, output);
}
}
The oversize condition is the server's 413, read from getHttpStatus(). SdkErrorCode is a coarse category and carries no size-specific member, so a 413 arrives as HTTP. Read the current member list from SdkErrorCode itself rather than from this page: an enumeration copied into prose is a second copy of a vocabulary, and the first version of this paragraph already listed five of its six members. Forcing the compact form is the only case where you handle this yourself: the auto-detecting encryptFile already retries via streaming on a 413.
Force Streaming Mode
Use for large files or memory-constrained environments.
// Force streaming (works for any file size)
Path largeFile = Path.of("video.mp4"); // 2 GB
EncryptResult result = sdk.encryptFileStream("archive-key", largeFile, Path.of("video.jwe"));
// Memory usage: bounded and independent of file size (see the note below)
// Process time: Linear with file size
When to force streaming:
- Files > 100 MB (even below the discovered threshold, streaming is more efficient at this scale)
- Embedded systems with limited RAM
- Processing user-uploaded files of unknown size
- Need binary output format (not Base64 text)
Operations Supporting Auto-Detection
Encryption Operations
| Method | Auto-Detection | Compact | Streaming |
|---|---|---|---|
| Encrypt | encryptFile() ✓ | encryptFileCompact() | encryptFileStream() |
| Decrypt | decryptFile() ✓ | decryptFileCompact() | decryptFileStream() |
| Re-encrypt | reencryptFile() ✓ | reencryptFileCompact() | reencryptFileStream() |
Note: Decryption auto-detects by input format, not file size:
- If input is compact JWE → uses compact mode
- If input is streaming JWE → uses streaming mode
Signature Operations
| Method | Auto-Detection | Compact | Streaming |
|---|---|---|---|
| Sign | signFile() ✓ | signFileCompact() | signFileStream() |
| Verify | N/A | verifySignature() | verifySignatureStream() |
| Re-sign | N/A | resignFileCompact() | resignFileStream() |
Note: Verification requires knowing the input format explicitly (no auto-detection).
Combined Operations
| Method | Auto-Detection | Compact | Streaming |
|---|---|---|---|
| Sign-then-Encrypt | N/A | signThenEncryptFileCompact() | Not available |
| Decrypt-then-Verify | N/A | decryptThenVerifyFileCompact() | Not available |
Limitation: Combined operations only support compact mode (inputs below the discovered maxPlaintextBytes).
Format Comparison
Compact JWE Format
Structure: Single-line Base64 string (5 parts separated by dots)
Characteristics:
- Size limit: below
maxPlaintextBytes(server-enforced, operator-configurable, default 4 MB; discover viaGET /api/v3/crypto/limits) - Memory: Entire file loaded into RAM
- Output: Text (Base64-encoded)
- Use case: APIs, JSON storage, small files
Example output:
{
"jweToken": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.rT-1...",
"format": "COMPACT",
"algorithmUsed": "RSA-OAEP-256 + AES-256-GCM"
}
Streaming JWE Format
Structure: Detached format with metadata file
Characteristics:
- Size limit: No limit (tested up to 2 GB)
- Memory: bounded and independent of file size
What "bounded" means, with the numbers it comes from
The SDK copies through StreamingMultipartVerdictReader.COPY_BUF (8 KiB) and encrypts through PqcClientCryptoUtil.CIPHER_INPUT_STREAM_BUFFER_SIZE (16 KiB); the buffered verdict and header sections are capped by MAX_TRAILING_BYTES (64 KiB). None of those grows with the file, which is the property that matters. Earlier revisions of this page published "~64 KB constant" as the streaming buffer size: that number is the CAP on the trailing sections, not a buffer, and no constant of that value is allocated per stream.
- Output: Binary + metadata
- Use case: Large files, low memory, archives
Example metadata (output.jwe.meta):
{
"jwe": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.header.iv..tag",
"format": "DETACHED",
"originalSize": 524288000,
"algorithmUsed": "RSA-OAEP-256 + AES-256-GCM"
}
Performance Implications
Memory Usage
graph LR
A[File Size] --> B{Operation Mode}
B -->|Compact| C[RAM = File Size]
B -->|Streaming| D[RAM bounded]
C --> E[100 MB file<br/>= 100 MB RAM]
D --> F[2 GB file<br/>= same bounded RAM]
style E fill:#FFEBEE
style F fill:#E8F5E9 Processing Speed
| File size | Compact mode | Streaming mode | Recommendation |
|---|---|---|---|
| < 1 MB | Supported | Supported | Compact — avoids the multipart round-trip |
| 3 MB | Supported | Supported | Either; compact is simpler |
| 5 MB | Rejected — above the 4,000,000-byte default | Supported | Streaming |
| 50 MB | Rejected | Supported | Streaming |
| 500 MB | Rejected | Supported | Streaming |
The compact/streaming boundary is not a file-size rule of thumb — it is the server's maxPlaintextBytes, default 4,000,000 bytes, operator-configurable and discoverable at GET /api/v3/crypto/limits. A 5 MB input is above that default, so compact mode rejects it, and the SDK's auto-detection selects streaming for exactly that reason. Do not hardcode a threshold: read the limit from the endpoint.
Key insight: compact mode avoids streaming's multipart overhead for small inputs, while streaming scales to arbitrary size with bounded memory. Latency depends on algorithm, payload size, network and deployment hardware, so measure against your own environment rather than budgeting from a published figure.
Best Practices
1. Use Auto-Detection by Default
// ✗ BAD: Manual size checking against a hardcoded constant
if (Files.size(file) < 5_000_000) { // drifts from the server's configurable limit
sdk.encryptFileCompact("my-key", file, output);
} else {
sdk.encryptFileStream("my-key", file, output);
}
// → SDK already does this, using the server-discovered maxPlaintextBytes!
2. Force Compact for API Integration
// ✓ GOOD: Explicit compact for REST API
EncryptResult result = sdk.encryptFileCompact("api-key", configFile, tempFile);
String jweToken = Files.readString(tempFile);
apiClient.sendEncryptedConfig(jweToken);
3. Force Streaming for Large Files
// ✓ GOOD: Streaming for known large files
sdk.encryptFileStream("backup-key", databaseDump, output);
4. Handle Both Formats in Decrypt
// ✓ GOOD: Auto-detection handles both formats
EncryptResult encResult = sdk.encryptFile("my-key", input, encrypted);
// Later: decryption auto-detects format
DecryptResultMetadata decResult = sdk.decryptFile(encrypted, decrypted);
// Works whether encrypted is compact or streaming!
Common Scenarios
Scenario 1: Unknown File Sizes (User Uploads)
// User uploads file via web form (size unknown)
@PostMapping("/upload")
public ResponseEntity<String> handleUpload(@RequestParam("file") MultipartFile upload) {
Path tempFile = Files.createTempFile("upload", ".bin");
upload.transferTo(tempFile.toFile());
// ✓ Auto-detection handles any size
EncryptResult result = sdk.encryptFile("upload-key", tempFile, Path.of("encrypted.jwe"));
return ResponseEntity.ok("Encrypted with: " + result.getAlgorithmUsed());
}
Scenario 2: Database Storage (Compact Required)
// Store encrypted tokens in PostgreSQL JSONB column
Path sensitiveData = Path.of("credentials.json");
// ✓ Force compact for database storage
EncryptResult result = sdk.encryptFileCompact("db-key", sensitiveData, tempOutput);
String jweToken = Files.readString(tempOutput);
// Store in database
jdbcTemplate.update(
"UPDATE secrets SET encrypted_data = ?::jsonb WHERE id = ?",
"{\"jwe\": \"" + jweToken + "\"}",
secretId
);
Scenario 3: Batch Processing (Memory Efficiency)
// Process 1000 files in parallel (limited RAM)
List<Path> files = findAllFiles("/data/archive/");
files.parallelStream().forEach(file -> {
// ✓ Streaming mode keeps memory constant
sdk.encryptFileStream("batch-key", file, file.resolveSibling(file.getFileName() + ".jwe"));
});
// Total memory: 1000 threads * a bounded per-stream footprint (not 1000 * file_size)
Scenario 4: Signature Verification (Format-Specific)
// Received signed document from external partner
Path document = Path.of("contract.pdf");
Path signature = Path.of("contract.jws");
// ✓ Check signature format first
String sigContent = Files.readString(signature);
if (sigContent.startsWith("eyJ")) {
// Compact JWS (single-line)
VerifySignatureResult result = sdk.verifySignature(signature);
} else {
// Streaming JWS (detached)
VerifySignatureResult result = sdk.verifySignatureStream(document, signature);
}
Troubleshooting
Error: File Too Large for Compact Mode
Symptom:
AnkaSecureSdkException: File size (6291456 bytes) exceeds the configured compact mode limit (maxPlaintextBytes)
HTTP status: 413 ErrorCode: HTTP
Solution:
// Option 1: Use auto-detection (will switch to streaming automatically;
// also retries via streaming if the server responds 413)
sdk.encryptFile("my-key", largeFile, output);
// Option 2: Explicitly use streaming
sdk.encryptFileStream("my-key", largeFile, output);
Error: Format Mismatch in Decryption
Symptom:
AnkaSecureSdkException: Cannot decrypt compact JWE with streaming endpoint
ErrorCode: INVALID_FORMAT
Solution:
// Use auto-detection (handles both formats)
sdk.decryptFile(encrypted, decrypted);
// OR: Check format first
if (isCompactFormat(encrypted)) {
sdk.decryptFileCompact(encrypted, decrypted);
} else {
sdk.decryptFileStream(encrypted, decrypted);
}
Memory Issues with Large Files
Symptom:
Cause: Using compact mode for large files
Solution:
// ✗ BAD: Compact mode loads entire file
sdk.encryptFileCompact("key", gigabyteFile, output);
// ✓ GOOD: Streaming mode uses constant memory
sdk.encryptFileStream("key", gigabyteFile, output);
Summary
Quick Reference
| Your Need | Use This Method | Why |
|---|---|---|
| General encryption | encryptFile() | Auto-detection handles everything |
| API/JSON storage | encryptFileCompact() | Single-line string required |
| Large files (>100 MB) | encryptFileStream() | Memory efficiency |
| Unknown file sizes | encryptFile() | Auto-detection adapts |
| Low RAM environment | encryptFileStream() | Bounded, file-size-independent memory |
Decision Flowchart
flowchart TD
Start([Need to encrypt/sign file]) --> Question1{Know the file size?}
Question1 -->|No| UseAuto[Use Auto-Detection<br/>encryptFile]
Question1 -->|Yes| Question2{File < maxPlaintextBytes?}
Question2 -->|Yes| Question3{Need single-line string?}
Question2 -->|No| UseStream[Force Streaming<br/>encryptFileStream]
Question3 -->|Yes| UseCompact[Force Compact<br/>encryptFileCompact]
Question3 -->|No| UseAuto2[Use Auto-Detection<br/>encryptFile]
UseAuto --> Success([Operation Complete])
UseCompact --> Success
UseStream --> Success
UseAuto2 --> Success
style UseAuto fill:#4CAF50,color:#fff
style UseCompact fill:#2196F3,color:#fff
style UseStream fill:#FF9800,color:#fff
style Success fill:#8BC34A,color:#fff Complete Flow Examples by Pattern
The SDK includes 21 production-ready integration flows. Here are the most relevant for each pattern:
🔄 Auto-Detection Pattern
| Flow | Description | File Size | Code |
|---|---|---|---|
| Flow 1 | ML-KEM-512 Encrypt/Decrypt | Any (auto-selects format) | Streaming |
| Flow 2 | Detached-JWS Sign/Verify | Any (streaming signatures) | Streaming |
| Flow 3 | AES-256 Symmetric | Any (fast encryption) | Streaming |
📦 Force Compact Pattern (below the configured limit)
| Flow | Description | Use Case | Code |
|---|---|---|---|
| Flow 5 | ML-KEM-512 Compact | API transmission, unit tests | Compact |
| Flow 6 | ML-DSA-87 Compact | Config files, JSON messages | Compact |
| Flow 7 | AES-256 Compact | Database secrets, tokens | Compact |
| Flow 14 | In-Memory Quick-Start | 100-line reference (no file I/O) | Compact |
🌊 Force Streaming Pattern (Large Files)
| Flow | Description | Memory Usage | Code |
|---|---|---|---|
| Flow 1 | GB-scale PQC encryption | bounded | Streaming |
| Flow 4 | RSA → ML-KEM migration | bounded | Streaming |
| Flow 12 | Large-file signature upgrade | bounded | Streaming |
| Flow 16 | XMSS detached-JWS stream verification | bounded | Streaming |
🔐 Combined & Advanced Patterns
| Flow | Description | Pattern | Code |
|---|---|---|---|
| Flow 17 | Sign-Then-Encrypt | Nested (compact only) | JWE(JWS) |
| Flow 15 | Bulk token rotation | Compact → Compact | Re-encrypt |
| Flow 16 | Continuous scanning | Streaming verification | Sign/Verify |
See Integration Flows Catalogue for all 21 flows organized by category.
Next Steps
- Integration Flows: See 21 complete examples of SDK operations
- Usage Guide: API reference for all SDK methods
- Security Best Practices: Credential management and secure integration
© 2025 AnkaTech. All rights reserved.