lbryio / lbry-sdk

The LBRY SDK for building decentralized, censorship resistant, monetized, digital content apps.
https://lbry.com
MIT License
7.2k stars 483 forks source link

Non-custodial Wallet Hosting & Sync #3442

Open lyoshenka opened 2 years ago

lyoshenka commented 2 years ago

(draft)

Motivation

Your wallet file stores important data that apps need to access. There's no convenient noncustodial way to back up your wallet or keep it synced across multiple apps. If you use several LBRY apps, you'll end up with many separate wallets.

Goals

name description
noncustodial host has no access to information inside wallet
multiapp multiple apps can use (odysee, lbry desktop, hound.fm, etc)
onepw single password for wallet encryption and app login (prolly via lbry.id oauth)
pwreset password can be changed, and can be reset if forgotten in some cases. some data loss is acceptable here
realtime wallets synced live between apps (at least it should feel like its live). relaxing this constraint since realtime stuff is moving out to federation or local storage
decentralized the whole setup can be run by anyone
userfriendly losing access to your wallet is as hard as we can make it

Plan

1. move as much stuff out of the wallet as possible

2. hosted wallet sync and oauth

set up https://lbry.id service to host wallets and provide oauth (should be open-source and documented so anyone can run their own auth service)

support 2fa?

3. client-side signing via lbry.id or js or browser plugin (a la metamask)

Once this sync protocol is adopted, apps will need a way for users to sign transactions to interact with LBRY. This must be done client side to satisfy the noncustodial requirement. This can be done (in increasing order of security) custom in the app via JS, or using a popup like Deso, or a browser extension like Metamask (which could also do hardware wallet support).

Terms

version (uint)
The protocol version used to encrypt the wallet. This document describes version 1 of the protocol.
id (string)
A string that uniquely identifies a user. If this is an email address, we get the benefit of being able to contact the user if necessary.
password (string)
The password as the user enters it.
rootKey (bytes)
A cryptographic key derived from the password using a key derivation function (KDF)
walletKey (bytes)
The first half of the root key. This key is used to encrypt a user's wallet. It is only used locally on a user's device and is never sent to the server.
loginKey (bytes)
The second half of the root key. This key is used to authenticate a user with the server.
sequence (uint)
A counter that is incremented every time a wallet is updated. This prevents race conditions where multiple simultaneous writes overwrite one another.
authToken (bytes)
A token representing a user session.
mfaCode (uint)
The TOTP code from a multi-factor device (Authenticator app, etc)
wallet (bytes)
Plaintext private user data. May include seed phrases, private keys, app info, settings, etc.
encryptedWallet (bytes)
`wallet` data that is encrypted and may be shared securely.

Server Operations

register

Register a new account

params: id, password, version response: authToken errors: version not supported, id already exists

oauth

Support the standard Oauth flows. We'll probably integrate with existing IDP project

getWallet

Get the current wallet data

params: authToken response: version, encryptedWallet, sequence errors: invalid authToken

putWallet

Store new wallet data

params: authToken, version, encryptedWallet, sequence response: ok errors: invalid authToken, version not supported, sequence mismatch

changePassword

This is the same as a putWallet, but also changes the loginKey.

params: authToken, version, loginKey, encryptedWallet, sequence response: ok errors: invalid authToken, version not supported, sequence mismatch

info

Get info about a user

params: authToken response: id, version, sequence

websocket

Connect to websocket to be notified when new wallet data is stored

params: authToken

Common flows

New account

TODO

Connect an app

TODO

Conflicting writes

TODO

I forgot my password, what can I do?

TODO

Multiple user apps (desktop + mobile + paper backup)

TODO

UX Considerations

Different wallets have different needs. If you just started an account, its no big deal to lose your wallet. If you have a lot of channels/claims/lbc in there, you want more backups and security.

Security

how to do this so its secure? what's the threat model?

prolly want an audit for this, but shouldn't block deployment

Rabbit holes

password resets

oauth and multiple apps

secure client-side signing via lbry.id

migrating odysee to this system

phishing

do we have to have the same password across multiple apps?

Extras

When unlocking, run sync first to make sure latest version is stored in cloud

Unlock via QR code or copying a string?

optional passcode to wrap root key locally

encrypt data with intermediate key, then encrypt that key with the password. this lets you back up your wallet encrpytion key on paper

does this work with multiple apps? can you have separate passwords across multiple apps? prolly not… so then a malicious app could get your odysee password

can you login with zksnarks? proof you know some encrypted string without revealing that string?

tell ppl when their passwords are poor using haveibeenpwned.com

outline what changes sdk has to make

should we store an updatedAt timestamp for wallet changes?

Alternative ideas

no wallets. everything is on-chain and can be restored from seed

store wallet in public cloud (s3, google drive, etc). define storage location in channel claim.

Related issues

https://github.com/lbryio/lbry-sdk/issues/2641

References

orblivion commented 2 years ago

Inspiration: You mentioned zksnarks. That got me thinking here.

I had an idea for a small but significant change to your approach that I think could ease a few of our problems. But I wanted to run it by you before I go too deeply into using it for use cases and diagrams.

KDF(password) = (walletKey, downloadKey)

walletKey does the same thing as before. downloadKey authorizes downloading encryptedWallet and nothing else.

wallet contains a new key pair: loginPublicKey and loginPrivateKey which will never change.

An auth request (login or password change) contains:

The response is sessionToken. It also updates the server with the latest supplied downloadKey.

The sessionToken can authenticate all requests other than the auth request above. downloadKey, again, can only authenticate download requests.

From this point, loginKey in the previous design is replaced by sessionToken or downloadKey, and it works roughly the same. downloadKey is the only medium-to-long term secret sent across the wire. loginPrivateKey is a new permanent secret, but it's stored with the rest of the keys to the kingdom anyway.


I can think of several ways that it makes this system at least a little bit smoother:

It just seems like a tighter system. More naturally decentralized. Are there downsides I haven't thought of? Did LBRY already consider and reject this idea at some point?

Is this somehow a bad idea for Odysee? Our plan involved decrypting the wallet in-browser for Odysee anyway, right?

I can think of one hypothetical downside: Without taking the time to think too deeply about it, my intuition tells me that this design may make it easier to clobber conflicting changes between two devices if there's a password change involved, that the previous design would stop us from doing. We just have to think extra carefully about it. Perhaps related: loginKey is always kept in sync with encryptedWallet in your design, and maybe I need to change things around to preserve that.


Some quick thoughts about tightening things up a bit more:

lyoshenka commented 2 years ago

just to make sure i understand the process:

to make a new account, you send the server a loginPublicKey and a downloadKey

to get the wallet, you use the loginPublicKey and the downloadKey

to set the wallet, you use the loginPublicKey and downloadKey and sign the request with the loginPrivateKey (you didn't say this, im just making it up)

to make a session which lets you interact with a particular service, you send it a loginPublicKey, a downloadKey, a few others fields, and sign it all with the loginPrivateKey

is that right? so the idea is that you can't really do anything on a service until you get your wallet and get the private key out?

lyoshenka commented 2 years ago

this seems like it could work. next step to verify that is probably to draw out all the interactions. you could get a similar effect by doing KDF(password) = (walletKey, downloadKey, loginPrivateKey) but idk if you can get enough bytes from the KDF for that to be secure

lyoshenka commented 2 years ago

can downloadKey be a key pair?

sure. a private key is just some random bytes, and you derive a public key from the private key.

orblivion commented 2 years ago

just to make sure i understand the process:

I should have given some example interactions.

to get the wallet, you use the loginPublicKey and the downloadKey

Okay, this was a bit of an oversight on my part. I'll assume the hard case, that you're talking about the user installing onto a second device (or recovering from backup). I was originally thinking that you don't need the loginPublicKey for this part, only the downloadKey. However, the server still needs to know which user is requesting their encryptedWallet in the first place. Expecting the user to somehow know their loginPublicKey before they have their wallet on a new device is possible but obviously very unfriendly.

So, I suppose we'd still need a username field (email address, etc) separate from the id (loginPublicKey). We could allow changing the username, but their id wouldn't change. The username would be relatively low stakes just like downloadKey: it would only be used for downloading the encryptedWallet.

With that in mind:

So now, the user only needs a username and password to set up a new device. The server does not get the loginPublicKey for this part.

is that right? so the idea is that you can't really do anything on a service until you get your wallet and get the private key out?

Everything other than downloading encryptedWallet, yes.

to make a session which lets you interact with a particular service, you send it a loginPublicKey, a downloadKey, a few others fields, and sign it all with the loginPrivateKey

To perhaps clairfy, the only reason I mentioned sending downloadKey during this request was to set it on the server side, so that another client could download the encryptedWallet after. It could and perhaps should be sent in a different request.

Othrewise, yes.

to set the wallet, you use the loginPublicKey and downloadKey and sign the request with the loginPrivateKey (you didn't say this, im just making it up)

Again, don't need downloadKey here, unless we choose to set it in this request along with the wallet.

orblivion commented 2 years ago

KDF(password) = (walletKey, downloadKey, loginPrivateKey)

Note that the only reason I made downloadKey separate from the login keypair was that it was available without the wallet. With what you have here, loginPrivateKey is available without the wallet already. So we can get rid of downloadKey (giving us more bytes for security) and just require the auth token to download.

KDF(password) = (walletKey, loginPrivateKey)

However, a change of password would mean change of loginPrivateKey and thus loginPublicKey, and thus a change of identity according to the design I've laid out. So, it's a different system but maybe that's fine. It would be like your loginKey system plus the benefit of no secret crossing the wire, so it still fixes the password breach problem.

But given that login keypair can change, you'd lose these benefits from above:

The first and last may be minor. Not sure about the middle. In general I like the elegance of an identity that never changes, but again maybe there's a downside I'm missing.

lyoshenka commented 2 years ago

a change of identity according to the design I've laid out

i didnt think about that. so its a question of whether your identity is tied to something like an email address, or to a private key. we'll have to think about that

lyoshenka commented 2 years ago

Putting this here so I don't forget: it would be nice if this also worked with social login. Maybe there's a way to get a string from the social login params and use that as a password? Or store a password with the social login service.

trymeouteh commented 2 years ago

https://github.com/lbryio/lbry-desktop/issues/6792 https://github.com/lbryio/lbry-android/issues/1212

I made a suggestion to the LBRY desktop and mobile clients on better wallet management, I am for these features since it will allow your LBRY account/wallet to only have one seed phrase and not a whole bunch of seed phrases over time as you login to new devices.