This document tracks the design & implementation plan for adding AES-GCM-256 encryption for all outbound payloads when the feature flag is enabled in the Web SDK.
⚠️ Phase 1 covers sending encrypted data to the back-end.
Phase 2 will cover receiving / decrypting encrypted responses and is out-of-scope for this draft.
Today the SDK transmits request bodies in plain-text (optionally compressed). To meet strict security & compliance requirements we must offer encryption in transit so that sensitive customer data is protected between browser and CleverTap’s ingress layer.
We expose a single runtime API:
clevertap.enableEncryptionInTransit(boolean)- Default:
falsefor full backward compatibility. - Behaviour: when set to
truebefore the first network call, every subsequent request uses Fetch API and transmits an encrypted payload envelope.
Internally a static flag will be stored on RequestDispatcher (similar to the new enableFetchApi flag).
- Algorithm: AES-GCM-256
- Key length: 32 bytes (256 bits)
- IV length: 12 bytes (96 bits) as recommended for GCM
- Tag length: 128 bits (built-in to GCM)
- Encoding: All binary artefacts (cipher, key, IV) are base-64 encoded for transport safety.
- Envelope compression: Final JSON envelope is compressed using the existing compress utility from
src/util/encoder.js.
// Minimal reference implementation (subject to extraction into util/security/AES.js)
const utf8 = new TextEncoder();
const toB64 = (u8) => btoa(String.fromCharCode(...u8));
const rnd = (n) => crypto.getRandomValues(new Uint8Array(n));
function encryptForBackend(payload, { id = 'ZWW-WWW-WWRZ' } = {}) {
const key = rnd(32); // 256-bit key
const iv = rnd(12); // 96-bit IV
const alg = { name: 'AES-GCM', iv, tagLength: 128 };
const plainBuf = utf8.encode(typeof payload === 'string' ? payload : JSON.stringify(payload));
return crypto.subtle.importKey('raw', key, alg, false, ['encrypt'])
.then((keyObj) => crypto.subtle.encrypt(alg, keyObj, plainBuf))
.then((cipherBuf) => {
const cipher = new Uint8Array(cipherBuf);
const envelope = {
itp: toB64(cipher), // payload
itk: toB64(key), // key
itv: toB64(iv), // iv
id,
encrypted: true
};
return compress(JSON.stringify(envelope));
});
}- Flag check – Every call to
RequestDispatcher.fireRequestverifiesRequestDispatcher.enableEncryptionInTransit. - Encrypt – When true, the raw request query-string (or body) is passed to
encryptForBackend. - Wrap – The returned Base-64 string replaces the existing
d=parameter (or forms the body for POST). - Send – Use Fetch API (required) to
POSTorGETas per existing logic.
sequenceDiagram
participant Client
participant RequestDispatcher
participant Backend
Client->>RequestDispatcher: Event / profile / ping
RequestDispatcher->>RequestDispatcher: encryptForBackend()
RequestDispatcher->>Backend: Fetch POST (cipher envelope)
Backend-->>RequestDispatcher: 200 OK (payload TBD)
RequestDispatcher-->>Client: resolve()
- Flag plumbing
- Add
enableEncryptionInTransitstatic flag toRequestDispatcher. - Expose public setter
clevertap.enableEncryptionInTransitsimilar to fetch flag.
- Add
- Encryption util
- Create
src/util/security/encryptionInTransit.jswith helpersencryptForBackend,decryptFromBackend(stub).
- Create
- Integrate in RequestDispatcher
- Before firing, build query/body →
encryptForBackend.
- Before firing, build query/body →
- Unit tests
- Validate: identical plaintext produces different ciphertexts (IV).
- Decryption roundtrip equality.
- Documentation & examples
- Update public README and example apps.
End of Phase 1 documentation – receiving/decryption flow to be defined in Phase 2.
With outbound encryption in place, the SDK must also handle encrypted responses originating from CleverTap’s back-end. The implementation must be tolerant to mixed states where either side may or may not have the feature enabled.
| # | Scenario | Expected SDK Behaviour |
|---|---|---|
| 0 | Backend sends encrypted payload while enableEncryptionInTransit === true |
• Decrypt using decryptFromBackend.• Continue normal handling. • Outbound requests remain encrypted as per flag. |
| 1 | Backend sends encrypted payload while enableEncryptionInTransit === false |
• Attempt to decrypt using decryptFromBackend (best-effort).• Continue normal processing on success. • Outbound requests remain unencrypted. |
| 2 | SDK sends encrypted payload but backend encryption disabled / mis-configured | • Backend returns an error code (402 Payment Required or 419).• SDK logs console.error("Encryption in Transit is disabled on server side").• SDK retries the same request using JSONP (plain-text, unencrypted). • SDK sets a session-level fallback flag in local storage. • All subsequent requests in this session use JSONP without encryption. • Flag resets on next clevertap.init() call. |
| 3 | Decryption failures (corrupted data, wrong key/IV, malformed envelope) | • Catch and log via Logger.error("EIT decryption failed", err).• Surface a clevertap.onError callback (TBD) so integrators can react.• Safely ignore the response payload and proceed without applying server changes. |
function decryptFromBackend(envelopeB64) {
const { itp, itk, itv } = JSON.parse(LZS.decompressFromBase64(envelopeB64));
const payload = Uint8Array.from(atob(itp), c => c.charCodeAt(0));
const key = Uint8Array.from(atob(itk), c => c.charCodeAt(0));
const iv = Uint8Array.from(atob(itv), c => c.charCodeAt(0));
const alg = { name: 'AES-GCM', iv, tagLength: 128 };
return crypto.subtle.importKey('raw', key, alg, false, ['decrypt'])
.then((keyObj) => crypto.subtle.decrypt(alg, keyObj, payload))
.then((plainBuf) => new TextDecoder().decode(plainBuf));
}The first operation in handleFetchResponse must be an attempt to decrypt the raw response string – before any parsing or field extraction.
Steps:
- Receive
response.text()(raw). - Try
decryptFromBackend(raw); on success treat the returned plaintext as the canonical response body. - If decryption throws because the data was not encrypted, fall back to the original raw text.
- Parse JSON and continue existing logic (e.g. extracting
tr,meta,wpe).
- Mis-match Flags: Detect server error codes signalling EIT disabled and surface console error.
- Crypto Errors: Wrap decrypt promise in
try/catch; updateLogger.error. - Fallback Path: On failure, ignore encrypted response but do not throw, ensuring SDK remains functional.
When the SDK has encryption enabled but the backend does not support it, the SDK must gracefully degrade to plain-text JSONP requests to ensure data is not lost.
sequenceDiagram
participant Client
participant RequestDispatcher
participant LocalStorage
participant Backend
Client->>RequestDispatcher: Event / profile / ping
RequestDispatcher->>RequestDispatcher: Check session fallback flag
alt Fallback flag is set
RequestDispatcher->>Backend: JSONP (plain-text)
else Encryption enabled, no fallback
RequestDispatcher->>RequestDispatcher: encryptForBackend()
RequestDispatcher->>Backend: Fetch POST (cipher envelope)
alt Backend returns EIT disabled error (402 or 419)
Backend-->>RequestDispatcher: 402/419 EIT_DISABLED
RequestDispatcher->>RequestDispatcher: Log error to console
RequestDispatcher->>LocalStorage: Set session fallback flag
RequestDispatcher->>Backend: Retry via JSONP (plain-text)
Backend-->>RequestDispatcher: 200 OK
else Success
Backend-->>RequestDispatcher: 200 OK
end
end
RequestDispatcher-->>Client: resolve()
| Property | Value |
|---|---|
| Storage Key | CT_EIT_FALLBACK (or similar namespaced key) |
| Storage Location | Local Storage |
| Value | true when backend has rejected encrypted request |
| Scope | Session-level – applies until next clevertap.init() |
| Reset Trigger | Calling clevertap.init() clears the flag |
- User enables encryption via
clevertap.enableEncryptionInTransit(true). - SDK encrypts payload and sends via Fetch API.
- Backend responds with error (
402 Payment Requiredor419). - SDK logs error –
console.error("Encryption in Transit is disabled on server side"). - SDK retries immediately – the same request is resent using JSONP with no encryption.
- SDK sets fallback flag in local storage (
CT_EIT_FALLBACK = true). - Subsequent requests in this session bypass encryption and use JSONP directly.
- On new session – calling
clevertap.init()clears the fallback flag, allowing the SDK to re-attempt encryption.
- The fallback flag check should occur early in
RequestDispatcher.fireRequest, before any encryption logic. - When the fallback flag is set, the SDK should behave as if
enableEncryptionInTransitisfalsefor that session. - The retry after backend rejection must be synchronous in the promise chain to avoid data loss.
- Clear the fallback flag in the
init()method of the main CleverTap module.
- Decrypt utility – implement
decryptFromBackendalongside encrypt util. - Response parsing – modify
RequestDispatcher.handleFetchResponseto detect & decrypt. - Error surfaces – standardise error messages & optional callback.
- JSONP fallback – implement retry logic when backend returns
EIT_DISABLED. - Session fallback flag – add
CT_EIT_FALLBACKto local storage; check infireRequest. - Flag reset on init – clear fallback flag in
clevertap.init(). - Unit tests – round-trip encrypt→decrypt, bad data, key mismatch, fallback scenarios.
- Docs update – example integration, troubleshooting guide.
End of Phase 2 documentation.