Level 03 — Advanced

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:

...they still see only "data": "yT4kQ8mR..." ciphertext. They need the AES key to read it.

→ Real-world example

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

crypto.js
JavaScript
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/encrypt.js
JavaScript
// 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

routes/payment.js
JavaScript
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)

crypto.service.ts
TypeScript
@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
TypeScript
// 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

payment.component.ts
TypeScript
// 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?

→ Important

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