Storing a server secret in a user passkey user id
I want to implement passkey support as a full replacement for passwords but I have some server-side state that still needs to be encrypted to a specific user in a way that can not be decrypted or reproduced on the server side without a user session. As the system works today, when the user logs in we generate a symmetric encryption key with their password, use that to decrypt a private key and use that private key to encrypt/decrypt further data-specific encryption keys.
The goals of this system are:
- Eliminate passwords
- Have a strong binding between server-encrypted data (which is never directly accessible to the user) and their passkey
- Have some non-repudiation for their key use by never persisting the keys on the server side. Basically, it should be impossible to decrypt or reproduce any of the user's action without the user being present.
- Ensure server-encrypted data is useless without the user's passkey being present. (The data-breach case)
Threats are:
- Server-side data breach
- User attempting to claim they didn't perform an action when in-fact they did
- Attacker gaining access to user credentials such that they could decrypt the user's asymmetric key and use it in combination with a data break
- The running server software is not considered a threat
The idea here is to use the binary user id associated with a passkey to store a symmetric key that would be functionally similar to the user's password in the above flow (but not used for authentication, just use the WebAuthn signatures for that). This key is never exposed to the user (a user-friendly display name is shown instead) and is restricted to the relying party that created it so it should never be transmitted to a RP that doesn't have access to it. The process would generate a 256-bit key which would be encrypted with a server-side key, in case some bug in the WebAutn ecosystem results in this key accidentally being exposed to an incorrect relying party (the user themselves are not considered a threat in the security model). This key and a 64-bit server ID would be packed into the passkey user id and stored with the user. The system would use discoverable passkeys to log the user in and access this ID.
On the server side, the authentication flow would look like this:
- Use passkey discovery to perform an authentication session which would result in a challenge signature and the user id.
- Use the context ID from the user id to lookup the crypto context record and decrypt the symmetric key portion of the user id using the server key from the setup step.
- Use a server secret to HMAC the user id into a server-side user ID token to lookup the user's public key. There is a parallel mapping from this token to the user's real ID in the user storage system.
- Finish the authentication following the WebAuthn standard with the user's public key.
- Decrypt the user's private key from the database with the symmetric key from above and discard that key. Place the asymmetric key into the user's encrypted and time limited session cookie for their later use. (Sessions are encrypted using a server-memory-resident key that is automatically rotated daily and never persisted)
Passkey recovery would use a randomly generated code that would ultimately wrap the asymmetric key just like the passkey resident code does. If the user loses this code then they're completely out of luck.
Is this a bad idea? Is there something I'm missing that invalidates the security of this system?