DEV Community

Cover image for Animated Cryptographic QR Codes: Killing Screenshot Attacks at the Display Layer
Gernard Cerma
Gernard Cerma

Posted on

Animated Cryptographic QR Codes: Killing Screenshot Attacks at the Display Layer

What if the QR code on your screen was alive?

Not alive in the "it links to a website" sense. Alive in the sense that it changes every 500 milliseconds, each frame is cryptographically signed, and a screenshot of it is worthless before you can even share it.

We are building this at QRAuth, and it solves a problem that no amount of short TTLs or rate limiting can fix.

The Problem: Screenshots Kill QR Authentication

QR-based authentication has a vulnerability that gets hand-waved away: the screenshot attack.

A user opens a login page. A QR code appears. The user screenshots it and sends it to someone else — or worse, an attacker captures it via screen share, a compromised display, or a shoulder-surfing camera. The recipient scans the screenshot and authenticates as the original user.

Short time-to-live values help but do not eliminate the window. A QR code valid for 30 seconds is still valid for 30 seconds. That is plenty of time for an automated relay attack.

Static QR codes displayed on physical surfaces (parking meters, restaurant tables, event posters) have an even worse version of this problem: someone sticks a fraudulent QR code over the legitimate one, and every person who scans it gets redirected to a phishing page. This is called quishing, and it is growing fast — cities like Montreal and Ottawa have already issued public warnings about fake QR codes on parking meters.

The Solution: Animated Cryptographic QR Codes

Instead of displaying a static QR code that refreshes every N seconds, we render a continuous animation where each frame is a new, independently signed QR code.

The protocol:

frame_payload = {
  challenge: base_challenge,
  frame_index: counter,
  timestamp: Date.now(),
  hmac: HMAC-SHA256(server_secret, challenge + timestamp + frame_index)
}
Enter fullscreen mode Exit fullscreen mode

Every 500ms, the display generates a new frame with an incremented counter and fresh timestamp. The HMAC binds the frame to the server secret, the original challenge, and the exact moment it was generated.

When a phone scans the code, the server validates:

  1. Recompute the HMAC with the server secret
  2. Check the timestamp is within the 500ms window
  3. Verify the frame_index is not stale (replay detection)
  4. Confirm the base challenge matches an active session

A screenshot captures exactly one frame. By the time it reaches anyone else, the timestamp window has closed. A video recording captures multiple frames, but without the server secret, the attacker cannot generate the next valid frame in the sequence.

Rendering with Skia

The animation cannot be a flickering mess of black and white squares. It needs to look intentional, designed, and unmistakably alive.

We use Skia for rendering — the same 2D graphics engine behind Chrome, Android, and Flutter. On the web, CanvasKit (Skia compiled to WebAssembly) provides hardware-accelerated canvas rendering. On mobile, @shopify/react-native-skia gives us a declarative rendering pipeline in React Native.

The key insight: QR data modules must stay binary for scanner compatibility, but everything around them is a design surface. Skia lets us:

  • Ripple modules outward from the center like a pulse
  • Make finder patterns glow and breathe with subtle animation
  • Shift a color wash across the quiet zone over time
  • Add a gradient border that cycles in sync with frame rotation

None of this affects decodability. QR scanners read luminance thresholds at each module position, not color. The styling is cosmetic, but the effect is powerful — it is visually obvious that this QR code is alive and cannot be a printout or a sticker.

Performance target: under 8ms per frame generation (ECDSA signing + QR matrix computation + Skia draw call), leaving 492ms of headroom on the 500ms rotation cycle.

Beyond the QR: The Trust Reveal

The animation on the display is half the story. The other half is what happens when you scan.

Most verification flows end with a web page showing a green checkmark. That is forgettable. We are building a Trust Reveal — a designed, 2-3 second verification moment:

  1. Full-screen takeover on the scanning device
  2. A server-generated visual fingerprint unique to that exact scan — a crystalline pattern derived from the SHA-256 of the timestamp, device ID, location, and token
  3. A color sweep from deep red to verified green
  4. The issuer identity materializes on screen

This is the frame that gets screenshotted and shared. It is also cryptographically meaningful — the visual is server-generated and provably live.

For failed verification (scanning a fraudulent QR code), the experience is equally designed: full-screen red, alarm animation, "UNVERIFIED — This QR code is not cryptographically signed. Do not proceed."

Trust-Reactive Animation

Here is where it gets genuinely novel.

The QR code's animation state reflects its security status in real time. The server pushes animation parameters via WebSocket to the display client:

Server State Visual Behavior
Clean Calm, slow pulse
Elevated scan velocity Animation accelerates, subtle hue shift
Fraud flagged Visual distortion, finder patterns degrade
Revoked Code dissolves on screen

The QR code is not just displaying a challenge. It is visually communicating the security state of that challenge to anyone looking at the screen. A calm, slowly pulsing QR code means everything is normal. A distorted, rapidly flickering one means something is wrong — and you do not need to understand cryptography to read that signal.

This sits on top of the existing QRAuth architecture. The event bus already handles scan events. The fraud detection pipeline already tracks scan velocity. The animation layer is a client-side visualization of server-side state, pushed over the same WebSocket connection that handles session authentication.

The Fraud Demo

Two QR codes on a screen, side by side. They look identical to the naked eye — because every QR code looks like a random grid of black and white squares to a human.

Scan QR Code A: the phone shows the Trust Reveal. Green sweep, verified, issuer identity confirmed.

Scan QR Code B: the phone flashes red. "FRAUDULENT CODE DETECTED — DO NOT PROCEED."

The difference was invisible. Until you scanned.

That is the entire value proposition of QR verification in 15 seconds.

What This Means for Quishing

The sticker-over-QR attack works because there is no verification layer between the QR code and the user's browser. You scan, you follow the link, you are on whatever page the attacker set up.

Animated cryptographic QR codes break this at two levels:

  1. A physical sticker cannot animate. If the legitimate QR is pulsing and the sticker is static, the difference is immediately visible.
  2. Even if an attacker manages to display their own animated QR, the cryptographic signature will fail verification because they do not have the signing key.

This is the same trust model as SSL/HTTPS. A padlock icon in your browser means the site's certificate was issued by a trusted authority. A verified QR code means it was signed by a registered issuer. Unsigned codes are flagged — not blocked, but flagged, the same way browsers warn about HTTP sites.

Try It

QRAuth is open source. The animated QR work is on our roadmap for Q3 2026, building on the existing ECDSA-P256 challenge-response protocol.

If you are working on anything related to QR security, authentication, or anti-fraud — we would love to hear from you. The protocol (QRVA) is open and licensed under CC BY 4.0.

Top comments (0)