CryptoKit
CryptoKit provides end-to-end AES-128-GCM encryption for HTTP request/response bodies. It protects sensitive API calls between the browser and Traveloka's backend services. A companion middleware integrates it into the FetchInterceptor pipeline.
Components
| Component | File | Purpose |
|---|---|---|
CryptoKit | crypto-kit.ts | Static encryption/decryption utilities |
createCryptoKitMiddleware | crypto-kit-middleware.ts | FetchInterceptor middleware factory |
CryptoKit Class
All methods are static — no instantiation required.
Encryption Parameters
| Parameter | Value |
|---|---|
| Algorithm | AES-GCM |
| Key length | 128 bits |
| IV length | 12 bytes |
| Compression | deflate (pako) |
| Key derivation | SHA-256 digest of the secret string |
Secret Construction
CryptoKit.buildSecret(secretArr: Array<string>): string
// Joins array elements with ':::'
// e.g. ['desktop', 'fsp', '/api/foo', 'device123'] → 'desktop:::fsp:::/api/foo:::device123'
The secret components are derived from request metadata:
x-client-interfaceheaderx-domainheader- Request URL
x-did(device ID, if present)
Request Encryption (handleRequest)
plaintext = `${timestamp}###${requestBody}`
→ TextEncoder.encode
→ deflate (pako compression)
→ AES-GCM encrypt (random 12-byte IV)
Wire format: [IV (12 bytes)][ciphertext]
The timestamp prefix provides replay protection.
Modified headers:
x-ck-id: 1.0.0(version marker)Content-Type: application/octet-stream
Response Decryption (handleResponse)
Wire format: [IV (12 bytes)][ciphertext]
→ AES-GCM decrypt
→ inflate (pako decompression)
→ TextDecoder.decode → plaintext string
Modified headers:
→ Remove x-ck-id
→ Content-Type: text/plain
Protection Support Check
CryptoKit.isProtectionSupported() verifies all required browser APIs:
TextEncoder/TextDecodercrypto.subtle.encrypt/crypto.subtle.decryptcrypto.getRandomValues
Returns false (and logs cryptokit.protection_unavailable) if any are missing. This check gates the entire Sentinel initialisation.
Route-Level Encryption
shouldEncrypt({ url, headers }) determines if a specific request should be encrypted based on a static route map:
protectedRoutes = {
fsp: [/.*\/dfp\/.*/], // fingerprint-related endpoints
};
In development environments, encryption is disabled by default but can be toggled via localStorage (lsCmnCryptoKitEnabled = '1').
CryptoKit Middleware
createCryptoKitMiddleware() returns a FetchInterceptorMiddleware with both onRequest and onResponse handlers.
Request Phase
Encrypts the request body when either:
shouldProtectResourceistrue(Sentinel config-driven), ORCryptoKit.shouldEncrypt()istrue(static route map)
The encryption secret is computed from request headers and stored in context.state.requestSecret for response decryption.
Response Phase
Decrypts the response when:
requestSecretexists in context state- Response headers contain
x-ck-id: 1.0.0andContent-Type: application/octet-stream
A new Response object is constructed with the decrypted body and cleaned headers.
Development Bypass
In non-production environments, encryption can be disabled by setting localStorage key lsCmnCryptoKitDisabled to '1'. This aids debugging and local development.
Error Handling
| Log Key | Scenario |
|---|---|
cryptokit.protection_unavailable | Browser doesn't support required crypto APIs |
cryptokit.handle_request_error | Encryption failed (thrown as Error('Invalid request')) |
cryptokit.handle_response_error | Decryption failed (thrown as Error('Invalid response')) |
Security Considerations
- Keys are derived per-request from contextual data (headers + URL + device ID), providing request isolation.
- The IV is generated via
crypto.getRandomValuesensuring non-deterministic ciphertext. - The timestamp embedded in the plaintext enables server-side replay detection.
- AES-GCM provides both confidentiality and authenticity (integrity via the authentication tag).
- Compression before encryption (deflate → encrypt) reduces ciphertext size without leaking plaintext structure in this context.
Last updated: 2026-02-24