Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-custodial Wallet Hosting & Sync #3442

Open
lyoshenka opened this issue Oct 5, 2021 · 9 comments
Open

Non-custodial Wallet Hosting & Sync #3442

lyoshenka opened this issue Oct 5, 2021 · 9 comments

Comments

@lyoshenka
Copy link
Member

lyoshenka commented Oct 5, 2021

(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

  • channel keys come from seed (lex already working on this Deterministic channel certificates #1433)
  • subscriptions go on-chain?
  • app settings stored in encrypted blob in channel? unsynced, or the sync is done by the app

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?

  • recovery from paper backup
  • changing password if unlocked wallet is/isnt available
  • changing password if active auth is/isnt available

TODO

Multiple user apps (desktop + mobile + paper backup)

TODO

  • how to handle when a password was changed on another app
  • recovery via another app

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

  • makes every settings change a txn. may lead to scaling issues
    • we should assume our blockchain will be upgraded to handle this
  • where do we store the data? its not always tied to channels
  • plus side: if we can do this, we won't need lbry.id to host wallets. every app can do its own thing.

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

  • doesnt meet onepw requirement
  • wallets aren't tied to channels, so we'd need a new place on-chain to store them

Related issues

#2641

References

@lyoshenka lyoshenka changed the title Wallet Hosting Non-custodial Wallet Hosting & Sync Oct 5, 2021
@orblivion
Copy link
Contributor

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:

  • loginPublicKey (user ID)
  • downloadKey
  • domain (prevent reuse elsewhere)
  • timestamp (prevent reuse later)
  • (probably other stuff)
  • Signature of the above using loginPrivateKey

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:

  • No meaningful password breaches; password reuse between services is relatively benign
    • A hacker or a malicious service would gain access to downloadKey, but would probably have access to an encryptedWallet by that point anyway
    • The remaining threat is an attacker who also has walletKey and is playing the long game: use downloadKey to download future versions of the wallet, to quietly capture more secrets. (No idea how important that is).
  • Fewer password sync problems between server and local
    • Suppose the user upgrades their password locally, and the server backup call fails. With the loginKey system, the server would only respond to the previous password, which the user may have forgotten. So we'd have to be extra sure this never happens. With the login key pair system, it's not such a crisis: the login keypair is unchanged, it's encrypted with the new password that the user presumably remembers, and we can just try a new auth request to update downloadKey whenever the network is back up.
  • No password upgrade consistency issue across multiple backup servers
    • Similar to above - Supposing you're backing up to two services and you change your password and only one service gets the message. On the next attempt both will be up to date, no problem.
  • Authorizing signup for new services is unambiguous
    • Supposing you're spinning up a new backup service and a user signs up. How do you know they're who they say they are? I haven't even thought of how to deal with this with the previous design. With this, the id is the public key and the signature from the private key proves it.
  • Additional emergency recovery option
    • Perhaps a long shot, but if the user somehow forgets their latest password but gets access to a local decrypted copy of their wallet, they can regain access to all of their associated accounts.

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:

  • can downloadKey be a key pair, or is the KDF only good enough for a symmetric key? If it can be a key pair, we could do the same signature trick and only send downloadPublicKey for auth requests, removing the small "future wallets" threat above.
  • Can we use the existing LBRY blockchain key pair as loginPublicKey and loginPrivateKey? Or, use the seed feature to make it deterministic? This way, if a user "upgrades" two different LBRY clients to this new system, they won't accidentally end up generating two different login key pairs. That could be a mess.
  • Maybe we could just sign every request instead of using sessionToken, but perhaps there are speed concerns and/or no particular benefit. Plus you wanted to do oauth which uses tokens.

@lyoshenka
Copy link
Member Author

lyoshenka commented Oct 28, 2021

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
Copy link
Member Author

lyoshenka commented Oct 28, 2021

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
Copy link
Member Author

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
Copy link
Contributor

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:

  • User installs LBRY client on a second device.
  • User puts in username and password.
  • Password renders walletKey and downloadKey.
  • Client sends downloadKey and username to server.
  • Server approves and sends encryptedWallet to the client
  • User uses walletKey on encryptedWallet to verify and decrypt wallet

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
Copy link
Contributor

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:

  • No password upgrade consistency issue across multiple backup servers
  • Authorizing signup for new services is unambiguous
  • Additional emergency recovery option

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
Copy link
Member Author

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
Copy link
Member Author

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
Copy link

lbryio/lbry-desktop#6792
lbryio/lbry-android#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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants