Migrating from RSA-2048 to ML-KEM-1024 (streaming JWET)
This walkthrough shows how to upgrade existing ciphertext from RSA-2048 to ML-KEM-1024 with zero plaintext exposure.
All cryptographic steps run in streaming mode using the high-throughput Secure Streaming endpoints.
1 · Process overview
| # | Action | Endpoint | Payload format |
|---|---|---|---|
| 1 | Generate RSA-2048 key | POST /api/key-management/keys |
JSON |
| 2 | Encrypt file (RSA) | POST /api/crypto/stream/encrypt |
multipart/form-data → JWET |
| 3 | Generate ML-KEM-1024 key | POST /api/key-management/keys |
JSON |
| 4 | Re-encrypt JWET (RSA → ML-KEM) | POST /api/crypto/stream/reencrypt |
multipart/form-data (JWET header + ciphertext) |
| 5 | Decrypt JWET (ML-KEM) | POST /api/crypto/stream/decrypt |
multipart/form-data |
2 - Detailed steps
2.1 Generate RSA-2048 key
-
Endpoint
POST /api/key-management/keys -
Body
application/json
{
"kid": "rsa_2048_key_001",
"kty": "RSA",
"alg": "RSA-2048",
"keyOps": ["encrypt", "decrypt"],
"exportable": true
}
Response 201 Created
2.2 Encrypt file (streaming, RSA)
-
Endpoint
POST /api/crypto/stream/encrypt -
Body
multipart/form-data
| Part | Name | Content-Type | Notes |
|---|---|---|---|
| 1 | metadata |
application/json |
{ "kid": "rsa_2048_key_001" } |
| 2 | file |
application/octet-stream |
Raw plaintext stream |
Result
-
multipart/mixedstream containing:-
JWET General JSON header (RSA recipient)
-
Ciphertext block(s) + tag
-
-
Headers:
2.3 Generate ML-KEM-1024 key
-
Endpoint
POST /api/key-management/keys -
Body
application/json
{
"kid": "mlkem_1024_key_001",
"kty": "ML-KEM",
"alg": "ML-KEM-1024",
"keyOps": ["encrypt", "decrypt"],
"exportable": true
}
Response 201 Created
2.4 Re-encrypt JWET (RSA → ML-KEM)
-
Endpoint
POST /api/crypto/stream/reencrypt?newKid=mlkem_1024_key_001 -
Body
multipart/form-data
| Part | Name | Content-Type | Notes |
|---|---|---|---|
| 1 | metadata |
application/json |
JWET header (JSON) -- may include or omit kid |
| 2 | file |
application/octet-stream |
Ciphertext (JWET body) |
Result
-
multipart/mixedstream containing a new JWET whose recipient uses ML-KEM-1024. -
Lifecycle headers:
X-OldKey-Requested: rsa_2048_key_001
X-OldKey-Used: rsa_2048_key_001
X-NewKey-Requested: mlkem_1024_key_001
X-NewKey-Used: mlkem_1024_key_001
X-OldKey-Algorithm-Used: RSA-2048+A256GCM
X-NewKey-Algorithm-Used: ML-KEM-1024+A256GCM
If kid was absent in the incoming JWET header, the server adds:
2.4.1 · metadata part anatomy
The first form-data part is a JWET header in General-JSON serialization that conforms to the component
#/components/schemas/JwetGeneralJsonHeaderReencrypt.
| Field | Type | Required | Description |
|---|---|---|---|
protected |
string |
Yes | Base64url-encoded protected header (alg, enc, typ, etc.). |
iv |
string |
Yes | Base64url-encoded IV for the AEAD content-encryption algorithm. |
ciphertext |
string \| null |
No (always null) |
Detached ciphertext placeholder. |
tag |
string \| null |
No (always null) |
Detached authentication tag placeholder. |
recipients[0].header.alg |
string |
Yes | Key-encapsulation / wrap algorithm (e.g. RSA-OAEP-256). |
recipients[0].header.kid |
string \| null |
Conditional | Managed flow: present → used as oldKid. Migration flow: absent → caller must supply sourceKidOverride. |
recipients[0].encrypted_key |
string |
Yes | Base64url-encoded encrypted CEK for this recipient. |
Exactly one recipient is permitted; the server rejects additional entries with 400 /errors/invalid-jwet.
Managed flow (kid present)
{
"protected": "eyJhbGciOiJSQS1PQUVQLTI1NiIsImVuYyI6IkEyNTZHQ00ifQ",
"iv": "QE-6gFcGy4B9yaQw",
"ciphertext": null,
"tag": null,
"recipients": [
{
"header": { "alg": "RSA-OAEP-256", "kid": "rsa_2048_key_001" },
"encrypted_key": "uJrM_6C5x..."
}
]
}
Migration flow (kid absent + sourceKidOverride)
{
"protected": "eyJhbGciOiJSQS1PQUVQLTI1NiIsImVuYyI6IkEyNTZHQ00ifQ",
"iv": "QE-6gFcGy4B9yaQw",
"ciphertext": null,
"tag": null,
"recipients": [
{
"header": { "alg": "RSA-OAEP-256" },
"encrypted_key": "uJrM_6C5x..."
}
]
}
2.5 Decrypt JWET (ML-KEM)
-
Endpoint
POST /api/crypto/stream/decrypt -
Body
multipart/form-data
| Part | Name | Content-Type | Notes |
|---|---|---|---|
| 1 | metadata |
application/json |
{ "kid": "mlkem_1024_key_001" } |
| 2 | file |
application/octet-stream |
JWET header + ciphertext |
Result
-
Plaintext stream (
application/octet-stream) -
Headers:
X-Key-Requested: mlkem_1024_key_001
X-Key-Used: mlkem_1024_key_001
X-Algorithm-Used: ML-KEM-1024+A256GCM
2.5.1 · metadata part anatomy
The first form-data part is a JWET header in General-JSON format that satisfies the component
#/components/schemas/JwetGeneralJsonHeaderDecrypt.
| Field | Type | Required | Description |
|---|---|---|---|
protected |
string |
Yes | Base64url-encoded protected header containing alg, enc, typ, etc. |
iv |
string |
Yes | Base64url-encoded IV for the AEAD content-encryption algorithm. |
ciphertext |
string \| null |
No (always null) |
Detached ciphertext placeholder. |
tag |
string \| null |
No (always null) |
Detached authentication tag placeholder. |
recipients[0].header.alg |
string |
Yes | Key-encapsulation / wrap algorithm (e.g. ML-KEM-1024). |
recipients[0].header.kid |
string |
Yes | Required – identifies the decryption key (mlkem_1024_key_001). |
recipients[0].encrypted_key |
string |
Yes | Base64url-encoded encrypted CEK for this recipient. |
Exactly one recipient is permitted; more than one triggers 400 /errors/invalid-jwet.
Example (managed decryption flow)
{
"protected": "eyJhbGciOiJNTC1LRU0tMTAyNCIsImVuYyI6IkEyNTZHQ00ifQ",
"iv": "Fq_odhuz_50HZBW-",
"ciphertext": null,
"tag": null,
"recipients": [
{
"header": { "alg": "ML-KEM-1024", "kid": "mlkem_1024_key_001" },
"encrypted_key": "sbM_9XtE..."
}
]
}
3 - Key considerations
-
No plaintext disclosure -- data is decrypted and re-encrypted in-memory; nothing hits disk.
-
Constant-memory streaming -- supports multi-gigabyte blobs without RAM spikes.
-
Quantum-safe target -- ML-KEM-1024 provides security against future quantum adversaries.
-
Lifecycle telemetry -- header set gives full visibility into rotation status and algorithm lineage.
-
Automate rotations -- schedule periodic JWET re-encrypt jobs to keep ahead of key expiry and cryptanalytic advances.
Before You Begin
Prerequisites: - Active AnkaSecure tenant with API access - Existing RSA-encrypted data (or ability to create test data) - API credentials (JWT token or API key)
Estimated Time: 30 minutes (including test data generation)
Complexity: Intermediate (requires understanding of streaming APIs)
Troubleshooting
Re-encryption Fails with 422 Error
Symptom: {"error":"CRYPTO_001","message":"Ciphertext integrity check failed"}
Causes: - JWET header corrupted during transfer - Ciphertext modified or truncated - Wrong key ID specified
Solutions:
- Verify JWET header is valid JSON
- Check kid in header matches source key
- Ensure complete ciphertext transmitted (no truncation)
Performance Slower Than Expected
Symptom: Re-encryption takes >10 seconds for 5 MB file
Causes: - Network latency (check ping to api.ankasecure.com) - Large file size (expected for >100 MB files)
Solutions: - Use SaaS region closest to your location - Consider on-premise for ultra-low latency requirements
Next Steps
Migration Successful? Continue with:
- RSA → ML-DSA Migration - Migrate digital signatures
- Migration Planning Guide - Plan organization-wide migration (includes hybrid approaches)
Need Help? - Error Reference - Complete error code catalog - API Documentation - Streaming API details - Support - Contact support team
Documentation Version: 3.0.0 Last Updated: 2025-12-26