Skip to main content

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

ComponentFilePurpose
CryptoKitcrypto-kit.tsStatic encryption/decryption utilities
createCryptoKitMiddlewarecrypto-kit-middleware.tsFetchInterceptor middleware factory

CryptoKit Class

All methods are static — no instantiation required.

Encryption Parameters

ParameterValue
AlgorithmAES-GCM
Key length128 bits
IV length12 bytes
Compressiondeflate (pako)
Key derivationSHA-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-interface header
  • x-domain header
  • 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 / TextDecoder
  • crypto.subtle.encrypt / crypto.subtle.decrypt
  • crypto.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:

  • shouldProtectResource is true (Sentinel config-driven), OR
  • CryptoKit.shouldEncrypt() is true (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:

  • requestSecret exists in context state
  • Response headers contain x-ck-id: 1.0.0 and Content-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 KeyScenario
cryptokit.protection_unavailableBrowser doesn't support required crypto APIs
cryptokit.handle_request_errorEncryption failed (thrown as Error('Invalid request'))
cryptokit.handle_response_errorDecryption 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.getRandomValues ensuring 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