Note: As was pointed out in numerous comments on Hacker News and here, this approach is problematic for a number of reasons. Transferring a secret key between devices in this manner can leave you vulnerable to serverside exploits and increases the likelyhood that your key will be exposed. This method also doesn't allow key revokation or forward secrecy. The goal of my project, Mentat, is to strike a good balance between privacy and convenience/features; it's an early-stage project and I'm still trying to find that balance. If your goal is ultimate privacy, there are a bunch of projects already out there that are better-suited for your needs: check out Signal, Matrix, etc. That being said, what follows is the original article...
When it came time to implement 2FA in my open-source project Mentat, I wanted to try something a little different. As an end-to-end encrypted chat app, asymmetric encryption was already an important aspect of the platform, and was easy enough to implement using OpenPGP.js. When a user signs up for the platform, a keypair is generated and the public key is saved in the database as part of that user's identity. But an issue arises when the user wants to sign into a different device: how can the user's private key be transmitted in a way that doesn't reveal their credentials to the server? As it turns out, I was able to solve this issue and add a second authentication factor in the same step.
Signing in on Mentat starts with the user inputting their email and password on a new device. When this occurs, the device will generate a brand new keypair and send the public key to the server. The server will check if this public key matches the one stored for the user, and this check will fail because a keypair has already been added as part of the signup process. The user is then shown a wall explaining that the device needs to be authenticated:
Meanwhile, a request will be sent to all of the user's previously-authenticated devices. The request will contain the public key of the new device and will ask if this request should be accepted:
If the request is accepted, the authenticated device will encrypt the user's private key using the new device's public key and transmit this packet to the new device. The new device will decrypt the user private key and replace its keypair with the valid keys, thus authenticating this device and receiving the user keypair at the same time. With the valid keys, the new device is able to decrypt group chat messages received from the server and send new messages under a single identity between devices.
Some work still needs to be done to increase the security of this process. For example, the server (or another device) should verify that the new device truly owns the private key before lifting the 2FA gate on the new device. This can be achieved by simply signing a message and having this signature verified. Additionally, the request could list some details, including model or OS, of the new device requesting access, in case a fraudulent request was sent.
Feedback or security concerns? Let me know in the comments. Wanna try the platform? Sign up here!