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 |
flowchart TD
Start(("Start")) --> K1[Generate RSA-2048 key]
K1 --> ENC["Encrypt file → JWET (RSA)"]
ENC --> K2[Generate ML-KEM-1024 key]
K2 --> REENC["Re-encrypt JWET (RSA → ML-KEM)"]
REENC --> DEC["Decrypt JWET (ML-KEM)"]
DEC --> End(("End"))
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/mixed
stream 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/mixed
stream 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.
Document version 2.2.0 -- generated from OpenAPI build 2025-05-31