HTTPS + JWT + AES Encryption
Everything from Level 2, plus an extra encryption envelope wrapped around your request and response payloads.
01What it actually is
Level 3 is Level 2 with a payload-level encryption layer that survives even when TLS doesn't. Even if someone:
- Captures your traffic at a proxy or CDN that decrypts TLS
- Reads your server logs that accidentally logged
req.body - Has access to your monitoring tools (Datadog, Sentry)
...they still see only "data": "yT4kQ8mR..." ciphertext. They need the AES key to read it.
Bank APIs sending account balances, healthcare portals sending patient records, payment gateways sending card details. This is what we recommended for sensitive endpoints in your app.
02Node.js side
Add an encrypt function using AES-256-GCM, then a middleware that wraps every res.json() call so encryption happens automatically. Your route handlers don't change at all — they keep returning plain objects.
1 — The encrypt helper
const crypto = require('crypto');
// Encrypt response payloads using AES-256-GCM
function encrypt(data, key) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const enc = Buffer.concat([
cipher.update(JSON.stringify(data), 'utf8'),
cipher.final()
]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, enc]).toString('base64');
}
2 — The encryption middleware
// Middleware that encrypts EVERY response
function encryptResponse(req, res, next) {
const originalJson = res.json.bind(res);
res.json = (data) => {
const aesKey = Buffer.from(process.env.AES_KEY, 'hex');
return originalJson({ data: encrypt(data, aesKey) });
};
next();
}
app.use(authMiddleware); // Level 2 — check JWT
app.use(encryptResponse); // Level 3 — encrypt payload
3 — Routes stay unchanged
app.get('/api/payment-details', (req, res) => {
// Server sends plain object — middleware encrypts it
res.json({
cardLast4: '4242',
expiryMonth: 12,
expiryYear: 2027
});
// What ACTUALLY goes over the wire:
// { "data": "yT4kQ8mR...iv...tag...ciphertext...base64==" }
});
03Angular side
Mirror image. A CryptoService wraps the browser's Web Crypto API; an HTTP interceptor auto-decrypts every response before your component sees it. Your components stay clean — they receive plain objects, just like in Level 2.
1 — CryptoService (wraps Web Crypto API)
@Injectable({ providedIn: 'root' })
export class CryptoService {
private keyPromise = this.importKey();
private async importKey() {
const hexKey = environment.aesKey; // same as server
const bytes = new Uint8Array(
hexKey.match(/.{2}/g)!.map(b => parseInt(b, 16))
);
return crypto.subtle.importKey(
'raw', bytes, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']
);
}
async decrypt(payload: string) {
const key = await this.keyPromise;
const buf = Uint8Array.from(atob(payload), c => c.charCodeAt(0));
const iv = buf.slice(0, 12);
const tag = buf.slice(12, 28);
const enc = buf.slice(28);
// Web Crypto wants ciphertext+tag together
const combined = new Uint8Array(enc.length + tag.length);
combined.set(enc, 0);
combined.set(tag, enc.length);
const plain = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv }, key, combined
);
return JSON.parse(new TextDecoder().decode(plain));
}
}
2 — Decryption interceptor
// crypto.interceptor.ts — auto-decrypts every response
export const cryptoInterceptor: HttpInterceptorFn = (req, next) => {
const cryptoSvc = inject(CryptoService);
return next(req).pipe(
switchMap(async event => {
if (event instanceof HttpResponse && event.body?.data) {
const decrypted = await cryptoSvc.decrypt(event.body.data);
return event.clone({ body: decrypted });
}
return event;
})
);
};
3 — Components stay invisible to encryption
// Component — code is clean, encryption is invisible
ngOnInit() {
this.http.get('/api/payment-details').subscribe(data => {
console.log(data); // { cardLast4: '4242', expiryMonth: 12, ... }
});
}
04The catch — key management
Level 3 only works if both sides hold the same AES key. That introduces a real operational concern: where does the browser's copy of the key live?
If the AES key ships with the Angular bundle (e.g. baked into environment.ts), anyone who opens DevTools can read it. Level 3 is then only protecting against passive threats — leaky logs, monitoring tools, TLS-decrypting proxies — not against an attacker who has the bundle.
For stronger guarantees, derive a per-session key after login (server sends an ephemeral key wrapped with the user's password-derived key), or graduate to Level 4 entirely.
05What it protects (and what it doesn't)
| Threat | Protected? | Why / Why not |
|---|---|---|
| TLS-decrypting corporate proxy / CDN inspection | Yes | Proxy sees ciphertext only — no AES key. |
Server-side log capture of req.body or res.body |
Yes | Logs record the encrypted blob, not the data. |
| Sentry / Datadog inadvertently capturing payloads | Yes | Same reason — they only see ciphertext. |
| Compromised server (attacker has the AES key) | No | Server holds the key; if root is compromised, so is the key. Level 4 fixes this. |
| Bundled key extracted from Angular DevTools | No | Mitigation: derive per-session keys instead of shipping a static one. |
06Key takeaways
- Code complexity: Medium. Two helpers and two interceptors, then everything else stays clean.
- Server sees data: Yes — server still decrypts to read.
- Use case: Sensitive endpoints — payment, OTP, KYC, medical records, government IDs.
- Performance cost: ~0.16 ms per request (server encrypt + browser decrypt). Imperceptible.
- Recommended approach: Apply L3 selectively — only on endpoints that actually handle sensitive data. Don't blanket-encrypt everything.