August 4, 2026
QR codes are a convenient way to pair a mobile device with a desktop application or a web application running on the desktop: the desktop displays a code, and the phone scans it. But scanning the code does not prove which device scanned it.
Imagine an attacker standing nearby, watching through a hidden camera, or watching a screen share in an online meeting, who scans the pairing code with his own phone before the legitimate user does. Without another authentication step, the desktop could end up connected to the attacker’s phone instead of the user’s.
That can open the door to a social-engineering attack. An attacker who controls what appears in the paired session may impersonate contacts or otherwise manipulate the user into revealing sensitive information.
So how do you protect against this when the attacker can see almost everything? Assume he can observe the desktop screen, the phone screen, and the keyboard, and can scan any QR code shown on the monitor. With his own phone, he can do anything the user can do.
The only things he cannot do are physically operate the user’s phone or type on the user’s keyboard. Those are the only two meaningful advantages the legitimate user has. The pairing process has to build on them.
Establishing a shared secret
The QR code cannot contain a symmetric encryption key. A QR code can safely carry public information, but it cannot contain a secret: anyone who sees it can copy it.
Instead, the desktop generates a Diffie-Hellman key pair and displays its public component as a QR code. The phone scans it, generates its own key pair, and sends its public key to the desktop through the server. Both devices can now derive the same shared secret. Someone watching the exchange sees both public values but cannot calculate the shared secret.
sequenceDiagram
participant Desktop
participant Mobile
Desktop->>Mobile: DH public key via QR
Mobile->>Desktop: Mobile DH public key
Note over Desktop,Mobile: Shared secret
But there is still a problem. The desktop has no way to know whether the public key came from the user’s phone or the attacker’s—both can scan the QR code. The answer lies in the one thing the attacker cannot do: type on the user’s keyboard.
Using the keyboard as an authenticator
After the shared secret is established, the desktop generates a random one-time password, encrypts it with a key derived from that secret, and sends it to the phone through the server. The server cannot read it. Only the phone that took part in that Diffie-Hellman exchange can decrypt it.
The phone displays a short numeric code:
482917
The user types it into the desktop:
Enter the code shown on your phone:
[ _ _ _ _ _ _ ]
The improved protocol looks like this:
sequenceDiagram
participant Desktop
participant Mobile
Note over Desktop: Generate DH key pair
Desktop->>Mobile: DH public key via QR
Note over Mobile: Scan QR<br/>Generate DH key pair
Mobile->>Desktop: Mobile DH public key
Note over Desktop,Mobile: Derive shared secret
Note over Desktop: Generate random OTP<br/>Encrypt OTP
Desktop->>Mobile: Encrypted OTP
Note over Mobile: Decrypt OTP<br/>Display OTP
Mobile->>Desktop: User enters OTP on keyboard
The attacker may see the code, but seeing it is not enough. He still cannot enter it on the user’s keyboard.
When the wrong phone wins
There is one more problem. The attacker’s phone may complete the Diffie-Hellman exchange before the legitimate one. The desktop may then establish the shared secret with the attacker’s phone and send the encrypted OTP to him instead. Meanwhile, the legitimate user scans the QR code and waits for a code that never arrives.
From the user’s perspective, the application simply appears to have stopped working. That confusion creates an opportunity for social engineering. An attacker nearby may offer to help, suggest that the user leave the workstation to seek assistance, or otherwise try to gain access to the physical keyboard.
Cryptographically, everything may be working exactly as intended. The problem is the user experience. The legitimate user cannot tell whether pairing failed because of an ordinary technical problem or because another phone completed the exchange first.
When pairing takes too long
An unexpected delay should be treated as a failed pairing attempt.
After sending its public key, the phone should expect to receive the encrypted OTP within a short period. If it does not arrive, the app should tell the user that pairing failed and start over. The same applies on the desktop: if the correct code is not entered in time, it abandons the exchange and starts again.
The cause of a timeout does not matter. It may be an ordinary network interruption or interference from an attacker. There is no need to distinguish between the two. Either way, the safest response is to throw the exchange away and start over with new keys.
Conclusion
A QR code alone does not authenticate the device that scans it. Even if an attacker can see both screens, watch the keyboard, and scan the QR code himself, pairing can still be secure if it ultimately depends on something he cannot do: control the user’s keyboard.
There is a broader lesson here. A system can be cryptographically sound and still be vulnerable if its failure mode leaves the user confused. That confusion becomes part of the attack surface. In this case, a clear timeout and a complete restart close that gap.
Keep reading
-
Hiding a String in a Numeric ID
September 4, 2026
When designing IDs that a person may need to read, copy, type, or pass to someone else, usability matters as much as uniqueness. A lookup ID may have to be dictated over the phone, written down, or entered by hand. For arbitrary machine-generated lookup IDs, numbers are a particularly practical format for human communication. People already handle long numbers: credit card numbers are routinely read, copied, and dictated despite containing sixteen digits.
Numbers are also easier to speak than arbitrary letters and symbols. There is no need to distinguish “B” from “D,” explain capitalization, or say whether a character is the letter “O” or the digit “0.” A number can be read aloud.
For this reason we use a twenty-digit numeric lookup ID, written in four groups of five digits. The grouping makes the number easier to scan and check, following the same general principle as a credit card:
12505-89847-63568-88524A lookup ID such as this looks like nothing more than a random string of digits. But the number itself can carry a small amount of hidden information. If designed correctly, this eliminates the need for the server to maintain a separate table mapping every ID to metadata.