-
Notifications
You must be signed in to change notification settings - Fork 32
Beet Maintainers' Handbook 2. WebSocket flow & messages
This post is mostly for my own use but could possibly be useful to others or for optimisation through peer review so I'm leaving it here.
To start off, Beet app ws server runs on port 60555.
The server is started as soon as Beet starts, regardless of existence of wallet or wallet state (locked/unlocked).
A client/dapp connects via websocket protocol to Beet and Beet immediately sets up a ping/pong frame keep-alive scheme.
It also sets a uuid (v4) to that specific client and sets its isAuthenticated
status to false.
It then sets up a handler for all incoming messages.
Incoming messages must conform to the following schema:
{
"id": <random_int_identifying_this_request>,
"type": <type_of_request>, // Can be one of 'version','api','authenticate','link'
"payload": <request_data> // is an object, see below for details
}
The first step for the client after connecting is to authenticate itself. It achieves this by sending a request like so:
{
"id": 12345,
"type": "authenticate"
"payload": {
"origin": "http://sitename.com",
"app_name": "My Super dApp",
"browser": "Chrome",
"apphash": <hex_string_hash>
/*
apphash is OPTIONAL. The app hash is a hash of origin+app_name+browser+account_id and uniquely
identifies an app that has been linked to a specific account on a specific browser on the
computer running the Beet app.
*/
}
}
If there is no apphash, this means that the dApp has not been linked to an account in Beet yet.
Cases:
NO APPHASH
Assuming the origin / app has not been blacklisted (TODO), Beet will set this connection as isAuthenticated=true,isLinked=false
and will be expecting a request with type 'link'
next.
Response:
{
"id": <same_id_as_request>,
"error": false,
"payload": {
"authenticate": true,
"link": false
}
}
WITH APPHASH
If there is an apphash, Beet will look in its storage for an entry for it. The retrieved entry will include a hash of the next request ID. Beet will then set the connection as isAuthenticated=true,isLinked=true
and wait for encrypted API calls.
Response:
{
"id": <same_id_as_request>,
"error": false,
"payload": {
"authenticate": true,
"link": true,
"account_id": <account_id>
}
}
Let's assume the app was not previously linked and did not include an apphash. Beet has "authenticated" the connection in terms of origin/app/browser and is waiting for a 'link'
request in order for the Beet user to link the app to a specific account.
Therefore our next call should be a 'link' request like so:
{
"id": 12345,
"type": "link"
"payload": {
"chain": "BTS",
/*
This parameter is a hint for Beet as to what kind of graphene account this app requires. Currently
supporting only BitShares.
*/
"pubkey": <public_key_of_randomly_generated_private_key>
"next_hash": <hex_string_of_next_id_hash>
}
}
Beet, upon receiving a 'link'
request, will prompt the user to select an account to use with that app. Once an account has been selected, Beet can calculate the apphash which is used as an identifier from then on. It will also generate a keypair and use the public key of the client along with the private key to generate a shared secret key via ECDH that will be used as the HOTP secret. The secret is then stored along with the hash of the next id and the rest of the client details with apphash as the primary key. Beet responds with its own public key so the client can generate and store the shared secret as well.
Response:
{
"id": <same_id_as_request>,
"error": false,
"payload": {
"authenticate": true,
"link": true,
"account_id": <account_id>,
"pubkey": <beet_randomly_generated_public_key>
}
}
In either case, (authentication of previously linked client or authentication & linking for the first time) we now have an agreed upon HOTP key , the client holding the id of the next request it will make to Beet and Beet holding its hash.
From now on, the client can make API requests to Beet that are AES encrypted, each time providing the hash of the next request's id like so:
First the client generates the random request id for the API call AFTER this one and builds the request object:
{
"method": <api_call_name>,
"params": <params_object>,
"next_hash": <hex_string_of_next_id_hash>
}
Taking the previously calculated/stored id, we generate an OTP and use it to AES encrypt this object.
The final request object is thus:
{
"id": <request_id>,
"type": "api",
"payload": <AES_encrypted_object>
}
Beet can then derive the OTP from the request id and decrypt the payload. It performs the action, stores the next id's hash and returns an encrypted response to the client like so
{
"id": <same_id_as_request>,
"error": false,
"payload": <AES_encrypted_object>
}
Above, payload object is API call dependent. Usually includes txID when Beet is asked to sign and broadcast something.
Apart from the above, all error responses are returned unencrypted in the following format:
{
"id": <same_id_as_request>,
"error": true,
"payload": {
"message": <error_msg>
}
}
(NOTE: Rejected operations are not treated as errors)