Something with IT security
A year ago I started building an Open-Source Snapchat alternative called twonly, as I liked the basic features of Snapchat, but wanted my images and text messages to not be scanned, viewed by their employees, or potentially by attackers or a government.
Directly from the start, users frequently lost access to their account after installing the app on a new device or losing their old phone. So a recovery mechanism was needed. Signal and WhatsApp use, beside their (unencrypted (WhatsApp)) backup, the user's phone number to identify the user and allow them to reset their private key.
But I do not want to use phone numbers, as this would undermine the users' privacy. Also, allowing users to recover their accounts using solely a phone number moves the Root of Trust from the private key to the phone number and with that to the server, which claims to have verified it. The server can then notify contacts about the user's new public key, which the clients accept. Yes, the clients show "Your safety numbers have changed", but this is ignored by almost all users (from my own experience).
As a starting point I decided to use a similar approach to Threema's Safe, as the protocol was already designed and audited by experts. But this system requires the user to remember their password to protect the uploaded private key. And believe it or not, users frequently forget their password, again requiring them to recreate their account, losing all their friends, rejoining existing groups, and, in the case of an E2EE encrypted image backup which I am planning to implement, losing access to their precious images. To fix this I started designing a passwordless recovery protocol involving Trusted Friends and the server as a second factor.
When involving friends to store a share of the user's secret key, it must be ensured that these are actually their friends' accounts and not an attacker impersonating their actual friends or a potentially compromised server performing an active MITM attack. For this, I already rolled out as part of my master's thesis an improved Authentication Ceremony which users can more easily understand and which is partially automated via a Web of Trust approach. Users then can only select such verified friends.
But as I want this protocol to be secure, it should ideally be verified by external security professionals. However, as a student, I cannot afford a professional audit. So before releasing it to the public, I am starting a 50€ Bug Bounty challenge with this blog post. The deal: You find a flaw in my protocol, you receive 50€. And yes, this may not sound like much, but hey, I am a student and have to pay this out of my own pocket. At the end of this post, you will find the rules and the security goals for the bug bounty.
To enable the passwordless recovery, a user first has to select at least T + 2 verified trusted friends (N), where T is the threshold for Shamir's Secret Sharing. The + 2 is there so that even if a couple of friends lose their phone or become unreachable, there are still enough shares left to recover. Then they can decide to enable a second factor, either a PIN or an email. From there, the user has finished their task. In the background the user's secret key is then split into multiple parts and then distributed to the selected friends. In case the second factor was enabled, these shares are further encrypted using a server key. Here is the full setup flow:
sequenceDiagram
participant U as User
participant S as Server
participant F as Friends
U->>U: RecoveryData = userId + secret key
alt Second Factor: Email
U->>U: Derive encKey = SHA-256(email)
U->>U: generates random
serverKey (32 bytes)
encNonce (24 bytes)
U->>U: protectedServerKey, MAC = XChaCha20-Poly1305.encrypt(serverKey, encKey, encNonce)
U->>S: protectedServerKey + MAC
U->>U: RecoveryData = XChaCha20-Poly1305.encrypt(RecoveryData, serverKey)
else Second Factor: PIN
U->>U: Generate pinSeed (32 bytes)
U->>U: Derive encKey = HMAC-SHA-256(pin, pinSeed)
U->>U: generates random
serverKey (32 bytes)
encNonce (24 bytes)
pinUnlockToken (32 bytes)
U->>U: protectedServerKey, MAC = XChaCha20-Poly1305.encrypt(serverKey, encKey, encNonce)
U->>S: protectedServerKey + MAC + pinUnlockToken
U->>U: RecoveryData = XChaCha20-Poly1305.encrypt(RecoveryData, serverKey)
else No Second Factor
U->>U: increase threshold T from >=2 to >=4
end
U->>U: Bundle SharedSecretData:
RecoveryData +
(optional: encNonce + pinSeed + pinUnlockToken)
U->>U: Shamir split SharedSecretData
into N shares (threshold T)
U->>F: Send each share via
E2E encrypted message
F->>F: Store share locally
Because the server does not receive the encNonce (192-bit nonce), the server is unable to brute-force the user's email or PIN. But because it has the MAC, it can then during recovery ensure that it received the original user's input.
When a user now forgets their backup password and has enabled the passwordless recovery, they can ask their friends to send them their shares. For this, the following flow is used to make it as easy as possible for all participants:
sequenceDiagram
participant R as Recovering User
participant S as Server
participant F as Trusted Friend
participant APP as Friend's App
R->>R: generates random
notificationID (UUID)
downloadAuthToken (32 bytes)
notificationKey (32 bytes)
R->>R: Read out pushToken
R->>S: notificationID, downloadAuthToken,
pushToken
R->>R: Creates link (behind fragment):
notificationID, notificationKey
R->>F: Shows QR code or shares link
F->>APP: Opens recovery link / scans QR
APP->>APP: Parse notificationId +
notificationKey from URL
APP->>F: "Select which contact is recovering"
F->>APP: Selects contact from list
APP->>F: "Are you sure?" phishing-prevention dialog
with a 10s timer
F->>APP: Confirms
APP->>APP: Encrypt stored share + userId
with notificationKey
(XChaCha20-Poly1305)
APP->>S: Submit encrypted envelope
(notificationId)
S->>R: Push notification:
"A friend submitted a share"
A symmetric encryption key was deliberately chosen over an asymmetric keypair: the goal is to protect submitted shares from the server, not from the user. Only the user holding the downloadAuthToken can download the submissions, and only the one holding the notificationKey can decrypt them. This makes the protocol post-quantum safe from the start, without requiring large public keys which cannot be shared via a QR code or a link.
To reduce potential phishing attacks, the user must manually select which friend they want to help, and then confirm their selection. This confirmation has a 10s timer and a red warning, asking them if they are sure that they have been asked by the selected user via a secure channel (like in-person or via Signal).
In the last step, the recovering user now can use the collected shares to recover their secret key:
sequenceDiagram
participant R as Recovering User
participant S as Server
participant I as User's inbox
R->>S: Download encrypted shares
(authenticated via downloadAuthToken)
R->>R: Decrypt userId + shares
with notificationKey
R->>R: Feed T shares into
Shamir reconstruction
R->>R: Obtain SharedSecretData
alt Email second factor
R->>R: Inputs email
R->>S: Send userId, SharedSecretData.encNonce, email
S->>S: serverKey = decrypt(protectedServerKey, key=SHA-256(email), encNonce, MAC)
S->>I: Send serverKey
I->>R: Copy serverKey
R->>R: Decrypt RecoveryData
with serverKey
else PIN second factor
R->>R: Derive key = HMAC-SHA-256(pin, SharedSecretData.pinSeed)
R->>S: Send userId, key + SharedSecretData.encNonce
+ SharedSecretData.pinUnlockToken
S->>S: Check pinUnlockToken
S->>S: serverKey = decrypt(protectedServerKey, key, encNonce, MAC)
(~10 tries max)
S->>R: Return serverKey
R->>R: Decrypt RecoveryData
with serverKey
else No second factor
R->>R: RecoveryData is directly
in SharedSecretData
end
R->>R: Restore userId + secret key
The server will automatically delete the protectedServerKey when more than 10 tries for the PIN were performed. The reason for the pinUnlockToken is to prevent denial-of-service: without it, anyone could just spam the recovery endpoint with wrong keys and trigger the 10-attempt lockout, permanently destroying the user's protectedServerKey. Since only the trusted friends hold the Shamir shares containing the pinUnlockToken, only they (in case they collude) or the legitimate recovering user can actually initiate a recovery attempt.
As this protocol is designed to only recover small amounts of data (the secret key), this is combined with an encrypted backup in the cloud, which is protected by the user's secret key that can then be downloaded by the user, restoring their contacts, messages and images.
If you have found something, please send either an email or find me on Signal: tobi.02.