Change to normal password auth, and various things
This commit is contained in:
parent
15c68d7153
commit
0bf11b059c
17 changed files with 776 additions and 821 deletions
66
auth/auth.go
66
auth/auth.go
|
@ -10,12 +10,15 @@ import (
|
|||
|
||||
// TODO - Learn how to use https://github.com/golang/oauth2 instead
|
||||
// TODO - Look into jwt, etc.
|
||||
// For now I just want a process that's shaped like what I'm looking for (pubkey signatures, downloadKey, etc)
|
||||
// For now I just want a process that's shaped like what I'm looking for.
|
||||
// (email/password, encrypted wallets, hmac, lastSynced, etc)
|
||||
|
||||
type UserId int32
|
||||
type Email string
|
||||
type DeviceId string
|
||||
type Password string
|
||||
type AuthTokenString string
|
||||
type PublicKey string
|
||||
type DownloadKey string
|
||||
type Signature string
|
||||
|
||||
type AuthScope string
|
||||
|
||||
|
@ -24,70 +27,37 @@ const ScopeGetWalletState = AuthScope("get-wallet-state")
|
|||
|
||||
// For test stubs
|
||||
type AuthInterface interface {
|
||||
NewToken(pubKey PublicKey, DeviceID string, Scope AuthScope) (*AuthToken, error)
|
||||
IsValidSignature(pubKey PublicKey, payload string, signature Signature) bool
|
||||
ValidateTokenRequest(tokenRequest *TokenRequest) bool
|
||||
// TODO maybe have a "refresh token" thing if the client won't have email available all the time?
|
||||
NewToken(UserId, DeviceId, AuthScope) (*AuthToken, error)
|
||||
}
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a *Auth) IsValidSignature(pubKey PublicKey, payload string, signature Signature) bool {
|
||||
// TODO - a real check
|
||||
return signature == "Good Signature"
|
||||
}
|
||||
|
||||
// Note that everything here is given to anybody who presents a valid
|
||||
// downloadKey and associated email. Currently these fields are safe to give
|
||||
// at that low security level, but keep this in mind as we change this struct.
|
||||
type AuthToken struct {
|
||||
Token AuthTokenString `json:"token"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
DeviceId DeviceId `json:"deviceId"`
|
||||
Scope AuthScope `json:"scope"`
|
||||
PubKey PublicKey `json:"publicKey"`
|
||||
UserId UserId `json:"userId"`
|
||||
Expiration *time.Time `json:"expiration"`
|
||||
}
|
||||
|
||||
type TokenRequest struct {
|
||||
DeviceID string `json:"deviceId"`
|
||||
RequestTime int64 `json:"requestTime"`
|
||||
// TODO - add target domain as well. anything to limit the scope of the
|
||||
// request to mitigate replays.
|
||||
}
|
||||
|
||||
func (a *Auth) ValidateTokenRequest(tokenRequest *TokenRequest) bool {
|
||||
if tokenRequest.DeviceID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Since we're going by signatures with a key that we don't want to change,
|
||||
// let's avoid replays.
|
||||
timeDiff := time.Now().Unix() - tokenRequest.RequestTime
|
||||
if timeDiff < -2 {
|
||||
// Maybe time drift will cause the request time to be in the future. This
|
||||
// would also include request time. Only allow a few seconds of this.
|
||||
return false
|
||||
}
|
||||
if timeDiff > 60 {
|
||||
// Maybe the request is slow. Allow for a minute of lag time.
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const AuthTokenLength = 32
|
||||
|
||||
func (a *Auth) NewToken(pubKey PublicKey, DeviceID string, Scope AuthScope) (*AuthToken, error) {
|
||||
func (a *Auth) NewToken(userId UserId, deviceId DeviceId, scope AuthScope) (*AuthToken, error) {
|
||||
b := make([]byte, AuthTokenLength)
|
||||
// TODO - Is this is a secure random function? (Maybe audit)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return nil, fmt.Errorf("Error generating token: %+v", err)
|
||||
}
|
||||
|
||||
return &AuthToken{
|
||||
Token: AuthTokenString(hex.EncodeToString(b)),
|
||||
DeviceID: DeviceID,
|
||||
Scope: Scope,
|
||||
PubKey: pubKey,
|
||||
DeviceId: deviceId,
|
||||
Scope: scope,
|
||||
UserId: userId,
|
||||
// TODO add Expiration here instead of putting it in store.go. and thus redo store.go. d'oh.
|
||||
}, nil
|
||||
}
|
||||
|
@ -109,3 +79,9 @@ func (d DownloadKey) Obfuscate() string {
|
|||
hash := sha256.Sum256([]byte(d))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func (p Password) Obfuscate() string {
|
||||
// TODO KDF instead
|
||||
hash := sha256.Sum256([]byte(p))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
|
|
@ -6,11 +6,6 @@ import (
|
|||
|
||||
// Test stubs for now
|
||||
|
||||
func TestAuthValidateTokenRequest(t *testing.T) {
|
||||
// also add a basic test case for this in TestServerAuthHandlerErrors to make sure it's called at all
|
||||
t.Fatalf("Test me: Implement and test ValidateTokenRequest")
|
||||
}
|
||||
|
||||
func TestAuthSignaturePass(t *testing.T) {
|
||||
t.Fatalf("Test me: Valid siganture passes")
|
||||
}
|
||||
|
|
140
server/auth.go
140
server/auth.go
|
@ -3,165 +3,69 @@ package server
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"orblivion/lbry-id/auth"
|
||||
"orblivion/lbry-id/store"
|
||||
)
|
||||
|
||||
/*
|
||||
TODO - Consider reworking the naming convention in (currently named)
|
||||
`AuthFullRequest` so we can reuse code with `WalletStateRequest`. Both structs
|
||||
have a pubkey, a signature, and a signed payload (which is in turn an encoded
|
||||
json string). We verify the signature for both in a similar pattern.
|
||||
*/
|
||||
|
||||
// DeviceId is decided by the device. UserId is decided by the server, and is
|
||||
// gatekept by Email/Password
|
||||
type AuthFullRequest struct {
|
||||
// TokenRequestJSON: json string within json, so that the string representation is
|
||||
// unambiguous for the purposes of signing. This means we need to deserialize the
|
||||
// request body twice.
|
||||
TokenRequestJSON string `json:"tokenRequestJSON"`
|
||||
PubKey auth.PublicKey `json:"publicKey"`
|
||||
Signature auth.Signature `json:"signature"`
|
||||
DeviceId auth.DeviceId `json:"deviceId"`
|
||||
Email auth.Email `json:"email"`
|
||||
Password auth.Password `json:"password"`
|
||||
}
|
||||
|
||||
func (r *AuthFullRequest) validate() bool {
|
||||
return (r.TokenRequestJSON != "" &&
|
||||
r.PubKey != auth.PublicKey("") &&
|
||||
r.Signature != auth.Signature(""))
|
||||
return (r.DeviceId != "" &&
|
||||
r.Email != auth.Email("") && // TODO email validation. Here or store. Stdlib does it: https://stackoverflow.com/a/66624104
|
||||
r.Password != auth.Password(""))
|
||||
}
|
||||
|
||||
type AuthForGetWalletStateRequest struct {
|
||||
Email string `json:"email"`
|
||||
Email auth.Email `json:"email"`
|
||||
DownloadKey auth.DownloadKey `json:"downloadKey"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
DeviceId auth.DeviceId `json:"deviceId"`
|
||||
}
|
||||
|
||||
func (r *AuthForGetWalletStateRequest) validate() bool {
|
||||
return (r.Email != "" &&
|
||||
r.DownloadKey != auth.DownloadKey("") &&
|
||||
r.DeviceID != "")
|
||||
}
|
||||
|
||||
// NOTE - (Perhaps for docs)
|
||||
//
|
||||
// This is not very well authenticated. Requiring the downloadKey and email
|
||||
// isn't very high security. It adds an entry into the same auth_tokens db
|
||||
// table as full auth tokens. There won't be a danger of a malicious actor
|
||||
// overriding existing auth tokens so long as the legitimate devices choose
|
||||
// unique DeviceIDs. (DeviceID being part of the primary key in the auth token
|
||||
// table.)
|
||||
//
|
||||
// A malicious actor could try to flood the auth token table to take down the
|
||||
// server, but then again they could do this with a legitimate account as well.
|
||||
// We could perhaps require registration (valid email) for full auth tokens and
|
||||
// limit to 10 get-wallet-state auth tokens per account.
|
||||
|
||||
func (s *Server) getAuthTokenForGetWalletState(w http.ResponseWriter, req *http.Request) {
|
||||
var authRequest AuthForGetWalletStateRequest
|
||||
if !getPostData(w, req, &authRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
pubKey, err := s.store.GetPublicKey(authRequest.Email, authRequest.DownloadKey)
|
||||
if err == store.ErrNoPubKey {
|
||||
errorJSON(w, http.StatusUnauthorized, "No match for email and password")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error getting public key")
|
||||
return
|
||||
}
|
||||
|
||||
authToken, err := s.auth.NewToken(pubKey, authRequest.DeviceID, auth.ScopeGetWalletState)
|
||||
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error generating auth token")
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE - see comment on auth.AuthToken definition regarding what we may
|
||||
// want to present to the client that has only presented a valid
|
||||
// downloadKey and email
|
||||
response, err := json.Marshal(&authToken)
|
||||
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error generating auth token")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.store.SaveToken(authToken); err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error saving auth token")
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, string(response))
|
||||
r.DeviceId != "")
|
||||
}
|
||||
|
||||
func (s *Server) getAuthTokenFull(w http.ResponseWriter, req *http.Request) {
|
||||
/*
|
||||
(This comment may only be needed for WIP)
|
||||
|
||||
Server should be in charge of such things as:
|
||||
* Request body size check (in particular to not tie up signature check)
|
||||
* JSON validation/deserialization
|
||||
|
||||
auth.Auth should be in charge of such things as:
|
||||
* Checking signatures
|
||||
* Generating tokens
|
||||
|
||||
The order of events:
|
||||
* Server checks the request body size
|
||||
* Server deserializes and then validates the AuthFullRequest
|
||||
* auth.Auth checks the signature of authRequest.TokenRequestJSON
|
||||
* This the awkward bit, since auth.Auth is being passed a (serialized) JSON string.
|
||||
However, it's not deserializing it. It's ONLY checking the signature of it
|
||||
as a string per se. (The same function will be used for signed walletState)
|
||||
* Server deserializes and then validates the auth.TokenRequest
|
||||
* auth.Auth takes auth.TokenRequest and PubKey and generates a token
|
||||
* DataStore stores the token. The pair (PubKey, TokenRequest.DeviceID) is the primary key.
|
||||
We should have one token for each device.
|
||||
*/
|
||||
|
||||
var authRequest AuthFullRequest
|
||||
if !getPostData(w, req, &authRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.auth.IsValidSignature(authRequest.PubKey, authRequest.TokenRequestJSON, authRequest.Signature) {
|
||||
errorJSON(w, http.StatusForbidden, "Bad signature")
|
||||
userId, err := s.store.GetUserId(authRequest.Email, authRequest.Password)
|
||||
if err == store.ErrNoUId {
|
||||
errorJson(w, http.StatusUnauthorized, "No match for email and password")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
internalServiceErrorJson(w, err, "Error getting User Id")
|
||||
return
|
||||
}
|
||||
|
||||
var tokenRequest auth.TokenRequest
|
||||
if err := json.Unmarshal([]byte(authRequest.TokenRequestJSON), &tokenRequest); err != nil {
|
||||
errorJSON(w, http.StatusBadRequest, "Malformed tokenRequest JSON")
|
||||
return
|
||||
}
|
||||
|
||||
if !s.auth.ValidateTokenRequest(&tokenRequest) {
|
||||
errorJSON(w, http.StatusBadRequest, "tokenRequest failed validation")
|
||||
return
|
||||
}
|
||||
|
||||
authToken, err := s.auth.NewToken(authRequest.PubKey, tokenRequest.DeviceID, auth.ScopeFull)
|
||||
authToken, err := s.auth.NewToken(userId, authRequest.DeviceId, auth.ScopeFull)
|
||||
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error generating auth token")
|
||||
internalServiceErrorJson(w, err, "Error generating auth token")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := json.Marshal(&authToken)
|
||||
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error generating auth token")
|
||||
internalServiceErrorJson(w, err, "Error generating auth token")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.store.SaveToken(authToken); err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error saving auth token")
|
||||
internalServiceErrorJson(w, err, "Error saving auth token")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ func TestServerAuthHandlerSuccess(t *testing.T) {
|
|||
testStore := TestStore{}
|
||||
s := Server{&testAuth, &testStore, &wallet.WalletUtil{}}
|
||||
|
||||
requestBody := []byte(`{"tokenRequestJSON": "{}", "publicKey": "abc", "signature": "123"}`)
|
||||
requestBody := []byte(`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, PathAuthTokenFull, bytes.NewBuffer(requestBody))
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -51,7 +51,7 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
|||
expectedStatusCode int
|
||||
expectedErrorString string
|
||||
|
||||
authFailSigCheck bool
|
||||
authFailLogin bool
|
||||
authFailGenToken bool
|
||||
storeFailSave bool
|
||||
}{
|
||||
|
@ -65,7 +65,7 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
|||
{
|
||||
name: "request body too large",
|
||||
method: http.MethodPost,
|
||||
requestBody: fmt.Sprintf(`{"tokenRequestJSON": "%s"}`, strings.Repeat("a", 10000)),
|
||||
requestBody: fmt.Sprintf(`{"password": "%s"}`, strings.Repeat("a", 10000)),
|
||||
expectedStatusCode: http.StatusRequestEntityTooLarge,
|
||||
expectedErrorString: http.StatusText(http.StatusRequestEntityTooLarge),
|
||||
},
|
||||
|
@ -84,26 +84,19 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
|||
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation",
|
||||
},
|
||||
{
|
||||
name: "signature check fail",
|
||||
name: "login fail",
|
||||
method: http.MethodPost,
|
||||
// so long as the JSON is well-formed, the content doesn't matter here since the signature check will be stubbed out
|
||||
requestBody: `{"tokenRequestJSON": "{}", "publicKey": "abc", "signature": "123"}`,
|
||||
expectedStatusCode: http.StatusForbidden,
|
||||
expectedErrorString: http.StatusText(http.StatusForbidden) + ": Bad signature",
|
||||
// so long as the JSON is well-formed, the content doesn't matter here since the password check will be stubbed out
|
||||
requestBody: `{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
||||
expectedStatusCode: http.StatusUnauthorized,
|
||||
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": No match for email and password",
|
||||
|
||||
authFailSigCheck: true,
|
||||
},
|
||||
{
|
||||
name: "malformed tokenRequest JSON",
|
||||
method: http.MethodPost,
|
||||
requestBody: `{"tokenRequestJSON": "{", "publicKey": "abc", "signature": "123"}`,
|
||||
expectedStatusCode: http.StatusBadRequest,
|
||||
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Malformed tokenRequest JSON",
|
||||
authFailLogin: true,
|
||||
},
|
||||
{
|
||||
name: "generate token fail",
|
||||
method: http.MethodPost,
|
||||
requestBody: `{"tokenRequestJSON": "{}", "publicKey": "abc", "signature": "123"}`,
|
||||
requestBody: `{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
||||
expectedStatusCode: http.StatusInternalServerError,
|
||||
expectedErrorString: http.StatusText(http.StatusInternalServerError),
|
||||
|
||||
|
@ -112,7 +105,7 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
|||
{
|
||||
name: "save token fail",
|
||||
method: http.MethodPost,
|
||||
requestBody: `{"tokenRequestJSON": "{}", "publicKey": "abc", "signature": "123"}`,
|
||||
requestBody: `{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
||||
expectedStatusCode: http.StatusInternalServerError,
|
||||
expectedErrorString: http.StatusText(http.StatusInternalServerError),
|
||||
|
||||
|
@ -125,8 +118,8 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
|||
// Set this up to fail according to specification
|
||||
testAuth := TestAuth{TestToken: auth.AuthTokenString("seekrit")}
|
||||
testStore := TestStore{}
|
||||
if tc.authFailSigCheck {
|
||||
testAuth.FailSigCheck = true
|
||||
if tc.authFailLogin {
|
||||
testStore.FailLogin = true
|
||||
} else if tc.authFailGenToken {
|
||||
testAuth.FailGenToken = true
|
||||
} else if tc.storeFailSave {
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"orblivion/lbry-id/store"
|
||||
"orblivion/lbry-id/wallet"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Whereas sever_test.go stubs out auth store and wallet, these will use the real thing, but test fewer paths.
|
||||
|
@ -68,24 +67,32 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
|||
&wallet.WalletUtil{},
|
||||
)
|
||||
|
||||
////////////////////
|
||||
// Register email address - any device
|
||||
////////////////////
|
||||
|
||||
var registerResponse struct{}
|
||||
responseBody, statusCode := request(
|
||||
t,
|
||||
http.MethodPost,
|
||||
s.register,
|
||||
PathRegister,
|
||||
®isterResponse,
|
||||
`{"email": "abc@example.com", "password": "123"}`,
|
||||
)
|
||||
|
||||
////////////////////
|
||||
// Get auth token - device 1
|
||||
////////////////////
|
||||
|
||||
var authToken1 auth.AuthToken
|
||||
responseBody, statusCode := request(
|
||||
responseBody, statusCode = request(
|
||||
t,
|
||||
http.MethodPost,
|
||||
s.getAuthTokenFull,
|
||||
PathAuthTokenFull,
|
||||
&authToken1,
|
||||
fmt.Sprintf(`{
|
||||
"tokenRequestJSON": "{\"deviceID\": \"dev-1\", \"requestTime\": %d}",
|
||||
"publickey": "testPubKey",
|
||||
"signature": "Good Signature"
|
||||
}`,
|
||||
time.Now().Unix(),
|
||||
),
|
||||
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
@ -95,8 +102,8 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
|||
if len(authToken1.Token) != expectedTokenLength {
|
||||
t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody))
|
||||
}
|
||||
if authToken1.DeviceID != "dev-1" {
|
||||
t.Fatalf("Unexpected response DeviceID. want: %+v got: %+v", "dev-1", authToken1.DeviceID)
|
||||
if authToken1.DeviceId != "dev-1" {
|
||||
t.Fatalf("Unexpected response DeviceId. want: %+v got: %+v", "dev-1", authToken1.DeviceId)
|
||||
}
|
||||
if authToken1.Scope != auth.ScopeFull {
|
||||
t.Fatalf("Unexpected response Scope. want: %+v got: %+v", auth.ScopeFull, authToken1.Scope)
|
||||
|
@ -113,19 +120,13 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
|||
s.getAuthTokenFull,
|
||||
PathAuthTokenFull,
|
||||
&authToken2,
|
||||
fmt.Sprintf(`{
|
||||
"tokenRequestJSON": "{\"deviceID\": \"dev-2\", \"requestTime\": %d}",
|
||||
"publickey": "testPubKey",
|
||||
"signature": "Good Signature"
|
||||
}`,
|
||||
time.Now().Unix(),
|
||||
),
|
||||
`{"deviceId": "dev-2", "email": "abc@example.com", "password": "123"}`,
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
if authToken2.DeviceID != "dev-2" {
|
||||
t.Fatalf("Unexpected response DeviceID. want: %+v got: %+v", "dev-2", authToken2.DeviceID)
|
||||
if authToken2.DeviceId != "dev-2" {
|
||||
t.Fatalf("Unexpected response DeviceId. want: %+v got: %+v", "dev-2", authToken2.DeviceId)
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
@ -141,17 +142,15 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
|||
&walletStateResponse,
|
||||
fmt.Sprintf(`{
|
||||
"token": "%s",
|
||||
"bodyJSON": "{\"encryptedWallet\": \"blah\", \"lastSynced\":{\"dev-1\": 1}, \"deviceId\": \"dev-1\" }",
|
||||
"publickey": "testPubKey",
|
||||
"downloadKey": "myDownloadKey",
|
||||
"signature": "Good Signature"
|
||||
"walletStateJson": "{\"encryptedWallet\": \"blah\", \"lastSynced\":{\"dev-1\": 1}, \"deviceId\": \"dev-1\" }",
|
||||
"hmac": "my-hmac-1"
|
||||
}`, authToken1.Token),
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
var walletState wallet.WalletState
|
||||
err := json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
||||
err := json.Unmarshal([]byte(walletStateResponse.WalletStateJson), &walletState)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %+v", err)
|
||||
|
@ -170,16 +169,14 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
|||
t,
|
||||
http.MethodGet,
|
||||
s.getWalletState,
|
||||
fmt.Sprintf(
|
||||
"%s?token=%s&publicKey=%s&deviceId=%s",
|
||||
PathWalletState, authToken2.Token, authToken2.PubKey, "dev-2"),
|
||||
fmt.Sprintf("%s?token=%s", PathWalletState, authToken2.Token),
|
||||
&walletStateResponse,
|
||||
"",
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
err = json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
||||
err = json.Unmarshal([]byte(walletStateResponse.WalletStateJson), &walletState)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %+v", err)
|
||||
|
@ -202,16 +199,14 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
|||
&walletStateResponse,
|
||||
fmt.Sprintf(`{
|
||||
"token": "%s",
|
||||
"bodyJSON": "{\"encryptedWallet\": \"blah2\", \"lastSynced\":{\"dev-1\": 1, \"dev-2\": 2}, \"deviceId\": \"dev-2\" }",
|
||||
"publickey": "testPubKey",
|
||||
"downloadKey": "myDownloadKey",
|
||||
"signature": "Good Signature"
|
||||
"walletStateJson": "{\"encryptedWallet\": \"blah2\", \"lastSynced\":{\"dev-1\": 1, \"dev-2\": 2}, \"deviceId\": \"dev-2\" }",
|
||||
"hmac": "my-hmac-2"
|
||||
}`, authToken2.Token),
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
err = json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
||||
err = json.Unmarshal([]byte(walletStateResponse.WalletStateJson), &walletState)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %+v", err)
|
||||
|
@ -230,16 +225,14 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
|||
t,
|
||||
http.MethodGet,
|
||||
s.getWalletState,
|
||||
fmt.Sprintf(
|
||||
"%s?token=%s&publicKey=%s&deviceId=%s",
|
||||
PathWalletState, authToken1.Token, authToken1.PubKey, "dev-1"),
|
||||
fmt.Sprintf("%s?token=%s", PathWalletState, authToken1.Token),
|
||||
&walletStateResponse,
|
||||
"",
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
err = json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
||||
err = json.Unmarshal([]byte(walletStateResponse.WalletStateJson), &walletState)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %+v", err)
|
||||
|
@ -250,159 +243,3 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
|||
t.Fatalf("Unexpected response Sequence(). want: %+v got: %+v", 2, sequence)
|
||||
}
|
||||
}
|
||||
|
||||
// Test a flow with a new device that needs to use the download key
|
||||
func TestIntegrationNewDevice(t *testing.T) {
|
||||
st, tmpFile := store.StoreTestInit(t)
|
||||
defer store.StoreTestCleanup(tmpFile)
|
||||
|
||||
s := Server{
|
||||
&auth.Auth{},
|
||||
&st,
|
||||
&wallet.WalletUtil{},
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// Get full auth token - device 1
|
||||
////////////////////
|
||||
|
||||
var authToken1 auth.AuthToken
|
||||
responseBody, statusCode := request(
|
||||
t,
|
||||
http.MethodPost,
|
||||
s.getAuthTokenFull,
|
||||
PathAuthTokenFull,
|
||||
&authToken1,
|
||||
fmt.Sprintf(`{
|
||||
"tokenRequestJSON": "{\"deviceID\": \"dev-1\", \"requestTime\": %d}",
|
||||
"publickey": "testPubKey",
|
||||
"signature": "Good Signature"
|
||||
}`,
|
||||
time.Now().Unix(),
|
||||
),
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
if authToken1.DeviceID != "dev-1" {
|
||||
t.Fatalf("Unexpected response DeviceID. want: %+v got: %+v", "dev-1", authToken1.DeviceID)
|
||||
}
|
||||
if authToken1.Scope != auth.ScopeFull {
|
||||
t.Fatalf("Unexpected response Scope. want: %+v got: %+v", auth.ScopeFull, authToken1.Scope)
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// Register email address - device 1
|
||||
////////////////////
|
||||
|
||||
var registerResponse struct{}
|
||||
responseBody, statusCode = request(
|
||||
t,
|
||||
http.MethodPost,
|
||||
s.register,
|
||||
PathRegister,
|
||||
®isterResponse,
|
||||
fmt.Sprintf(`{
|
||||
"token": "%s",
|
||||
"publicKey": "testPubKey",
|
||||
"deviceId": "dev-1",
|
||||
"email": "address@example.com"
|
||||
}`, authToken1.Token),
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody, http.StatusCreated)
|
||||
|
||||
////////////////////
|
||||
// Put wallet state - device 1
|
||||
////////////////////
|
||||
|
||||
var walletStateResponse WalletStateResponse
|
||||
responseBody, statusCode = request(
|
||||
t,
|
||||
http.MethodPost,
|
||||
s.postWalletState,
|
||||
PathWalletState,
|
||||
&walletStateResponse,
|
||||
fmt.Sprintf(`{
|
||||
"token": "%s",
|
||||
"bodyJSON": "{\"encryptedWallet\": \"blah\", \"lastSynced\":{\"dev-1\": 1}, \"deviceId\": \"dev-1\" }",
|
||||
"publickey": "testPubKey",
|
||||
"downloadKey": "myDownloadKey",
|
||||
"signature": "Good Signature"
|
||||
}`, authToken1.Token),
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
var walletState wallet.WalletState
|
||||
err := json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %+v", err)
|
||||
}
|
||||
|
||||
sequence := walletState.Sequence()
|
||||
if sequence != 1 {
|
||||
t.Fatalf("Unexpected response Sequence(). want: %+v got: %+v", 1, sequence)
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// Get get-wallet-state auth token - device 2
|
||||
////////////////////
|
||||
|
||||
var authToken2 auth.AuthToken
|
||||
responseBody, statusCode = request(
|
||||
t,
|
||||
http.MethodPost,
|
||||
s.getAuthTokenForGetWalletState,
|
||||
PathAuthTokenGetWalletState,
|
||||
&authToken2,
|
||||
`{
|
||||
"email": "address@example.com",
|
||||
"downloadKey": "myDownloadKey",
|
||||
"deviceID": "dev-2"
|
||||
}`,
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
// result.Token is in hex, auth.AuthTokenLength is bytes in the original
|
||||
expectedTokenLength := auth.AuthTokenLength * 2
|
||||
if len(authToken2.Token) != expectedTokenLength {
|
||||
t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody))
|
||||
}
|
||||
if authToken2.DeviceID != "dev-2" {
|
||||
t.Fatalf("Unexpected response DeviceID. want: %+v got: %+v", "dev-2", authToken2.DeviceID)
|
||||
}
|
||||
if authToken2.Scope != auth.ScopeGetWalletState {
|
||||
t.Fatalf("Unexpected response Scope. want: %+v got: %+v", auth.ScopeGetWalletState, authToken1.Scope)
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// Get wallet state - device 2
|
||||
////////////////////
|
||||
|
||||
responseBody, statusCode = request(
|
||||
t,
|
||||
http.MethodGet,
|
||||
s.getWalletState,
|
||||
fmt.Sprintf(
|
||||
"%s?token=%s&publicKey=%s&deviceId=%s",
|
||||
PathWalletState, authToken2.Token, authToken2.PubKey, "dev-2"),
|
||||
&walletStateResponse,
|
||||
"",
|
||||
)
|
||||
|
||||
checkStatusCode(t, statusCode, responseBody)
|
||||
|
||||
err = json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %+v", err)
|
||||
}
|
||||
|
||||
sequence = walletState.Sequence()
|
||||
if sequence != 1 {
|
||||
t.Fatalf("Unexpected response Sequence(). want: %+v got: %+v", 1, sequence)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,18 +9,15 @@ import (
|
|||
"orblivion/lbry-id/store"
|
||||
)
|
||||
|
||||
// TODO email verification cycle
|
||||
|
||||
type RegisterRequest struct {
|
||||
Token auth.AuthTokenString `json:"token"`
|
||||
PubKey auth.PublicKey `json:"publicKey"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
Email string `json:"email"`
|
||||
Email auth.Email `json:"email"`
|
||||
Password auth.Password `json:"password"`
|
||||
}
|
||||
|
||||
func (r *RegisterRequest) validate() bool {
|
||||
return (r.Token != auth.AuthTokenString("") &&
|
||||
r.PubKey != auth.PublicKey("") &&
|
||||
r.DeviceID != "" &&
|
||||
r.Email != "")
|
||||
return r.Email != "" && r.Password != ""
|
||||
}
|
||||
|
||||
func (s *Server) register(w http.ResponseWriter, req *http.Request) {
|
||||
|
@ -29,23 +26,13 @@ func (s *Server) register(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !s.checkAuth(
|
||||
w,
|
||||
registerRequest.PubKey,
|
||||
registerRequest.DeviceID,
|
||||
registerRequest.Token,
|
||||
auth.ScopeFull,
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
err := s.store.InsertEmail(registerRequest.PubKey, registerRequest.Email)
|
||||
err := s.store.CreateAccount(registerRequest.Email, registerRequest.Password)
|
||||
|
||||
if err != nil {
|
||||
if err == store.ErrDuplicateEmail || err == store.ErrDuplicateAccount {
|
||||
errorJSON(w, http.StatusConflict, "Error registering")
|
||||
errorJson(w, http.StatusConflict, "Error registering")
|
||||
} else {
|
||||
internalServiceErrorJSON(w, err, "Error registering")
|
||||
internalServiceErrorJson(w, err, "Error registering")
|
||||
}
|
||||
log.Print(err)
|
||||
return
|
||||
|
@ -56,7 +43,7 @@ func (s *Server) register(w http.ResponseWriter, req *http.Request) {
|
|||
response, err = json.Marshal(registerResponse)
|
||||
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error generating register response")
|
||||
internalServiceErrorJson(w, err, "Error generating register response")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
// TODO proper doc comments!
|
||||
|
||||
const PathAuthTokenFull = "/auth/full"
|
||||
const PathAuthTokenGetWalletState = "/auth/get-wallet-state"
|
||||
const PathRegister = "/signup"
|
||||
const PathWalletState = "/wallet-state"
|
||||
|
||||
|
@ -39,30 +38,32 @@ type ErrorResponse struct {
|
|||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func errorJSON(w http.ResponseWriter, code int, extra string) {
|
||||
func errorJson(w http.ResponseWriter, code int, extra string) {
|
||||
errorStr := http.StatusText(code)
|
||||
if extra != "" {
|
||||
errorStr = errorStr + ": " + extra
|
||||
}
|
||||
authErrorJSON, err := json.Marshal(ErrorResponse{Error: errorStr})
|
||||
authErrorJson, err := json.Marshal(ErrorResponse{Error: errorStr})
|
||||
if err != nil {
|
||||
// In case something really stupid happens
|
||||
http.Error(w, `{"error": "error when JSON-encoding error message"}`, code)
|
||||
}
|
||||
http.Error(w, string(authErrorJSON), code)
|
||||
http.Error(w, string(authErrorJson), code)
|
||||
return
|
||||
}
|
||||
|
||||
// Don't report any details to the user. Log it instead.
|
||||
func internalServiceErrorJSON(w http.ResponseWriter, err error, errContext string) {
|
||||
func internalServiceErrorJson(w http.ResponseWriter, serverErr error, errContext string) {
|
||||
errorStr := http.StatusText(http.StatusInternalServerError)
|
||||
authErrorJSON, err := json.Marshal(ErrorResponse{Error: errorStr})
|
||||
authErrorJson, err := json.Marshal(ErrorResponse{Error: errorStr})
|
||||
if err != nil {
|
||||
// In case something really stupid happens
|
||||
http.Error(w, `{"error": "error when JSON-encoding error message"}`, http.StatusInternalServerError)
|
||||
log.Printf("error when JSON-encoding error message")
|
||||
return
|
||||
}
|
||||
http.Error(w, string(authErrorJSON), http.StatusInternalServerError)
|
||||
log.Printf("%s: %+v\n", errContext, err)
|
||||
http.Error(w, string(authErrorJson), http.StatusInternalServerError)
|
||||
log.Printf("%s: %+v\n", errContext, serverErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -77,7 +78,7 @@ func internalServiceErrorJSON(w http.ResponseWriter, err error, errContext strin
|
|||
|
||||
func requestOverhead(w http.ResponseWriter, req *http.Request, method string) bool {
|
||||
if req.Method != method {
|
||||
errorJSON(w, http.StatusMethodNotAllowed, "")
|
||||
errorJson(w, http.StatusMethodNotAllowed, "")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -106,13 +107,13 @@ func getPostData(w http.ResponseWriter, req *http.Request, reqStruct PostRequest
|
|||
}
|
||||
|
||||
if err := json.NewDecoder(req.Body).Decode(&reqStruct); err != nil {
|
||||
errorJSON(w, http.StatusBadRequest, "Malformed request body JSON")
|
||||
errorJson(w, http.StatusBadRequest, "Malformed request body JSON")
|
||||
return false
|
||||
}
|
||||
|
||||
if !reqStruct.validate() {
|
||||
// TODO validate() should return useful error messages instead of a bool.
|
||||
errorJSON(w, http.StatusBadRequest, "Request failed validation")
|
||||
errorJson(w, http.StatusBadRequest, "Request failed validation")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -124,41 +125,35 @@ func getGetData(w http.ResponseWriter, req *http.Request) bool {
|
|||
return requestOverhead(w, req, http.MethodGet)
|
||||
}
|
||||
|
||||
// TODO - probably don't return all of authToken since we only need userId and
|
||||
// deviceId. Also this is apparently not idiomatic go error handling.
|
||||
func (s *Server) checkAuth(
|
||||
w http.ResponseWriter,
|
||||
pubKey auth.PublicKey,
|
||||
deviceId string,
|
||||
token auth.AuthTokenString,
|
||||
scope auth.AuthScope,
|
||||
) bool {
|
||||
authToken, err := s.store.GetToken(pubKey, deviceId)
|
||||
) *auth.AuthToken {
|
||||
authToken, err := s.store.GetToken(token)
|
||||
if err == store.ErrNoToken {
|
||||
errorJSON(w, http.StatusUnauthorized, "Token Not Found")
|
||||
return false
|
||||
errorJson(w, http.StatusUnauthorized, "Token Not Found")
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error getting Token")
|
||||
return false
|
||||
}
|
||||
|
||||
if authToken.Token != token {
|
||||
errorJSON(w, http.StatusUnauthorized, "Token Invalid")
|
||||
return false
|
||||
internalServiceErrorJson(w, err, "Error getting Token")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !authToken.ScopeValid(scope) {
|
||||
errorJSON(w, http.StatusForbidden, "Scope")
|
||||
return false
|
||||
errorJson(w, http.StatusForbidden, "Scope")
|
||||
return nil
|
||||
}
|
||||
|
||||
return true
|
||||
return authToken
|
||||
}
|
||||
|
||||
// TODO - both wallet and token requests should be PUT, not POST.
|
||||
// PUT = "...creates a new resource or replaces a representation of the target resource with the request payload."
|
||||
|
||||
func (s *Server) Serve() {
|
||||
http.HandleFunc(PathAuthTokenGetWalletState, s.getAuthTokenForGetWalletState)
|
||||
http.HandleFunc(PathAuthTokenFull, s.getAuthTokenFull)
|
||||
http.HandleFunc(PathWalletState, s.handleWalletState)
|
||||
http.HandleFunc(PathRegister, s.register)
|
||||
|
|
|
@ -3,6 +3,8 @@ package server
|
|||
import (
|
||||
"fmt"
|
||||
"orblivion/lbry-id/auth"
|
||||
"orblivion/lbry-id/store"
|
||||
"orblivion/lbry-id/wallet"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -10,28 +12,19 @@ import (
|
|||
|
||||
type TestAuth struct {
|
||||
TestToken auth.AuthTokenString
|
||||
FailSigCheck bool
|
||||
FailGenToken bool
|
||||
}
|
||||
|
||||
func (a *TestAuth) NewToken(pubKey auth.PublicKey, DeviceID string, Scope auth.AuthScope) (*auth.AuthToken, error) {
|
||||
func (a *TestAuth) NewToken(userId auth.UserId, deviceId auth.DeviceId, scope auth.AuthScope) (*auth.AuthToken, error) {
|
||||
if a.FailGenToken {
|
||||
return nil, fmt.Errorf("Test error: fail to generate token")
|
||||
}
|
||||
return &auth.AuthToken{Token: a.TestToken, Scope: Scope}, nil
|
||||
}
|
||||
|
||||
func (a *TestAuth) IsValidSignature(pubKey auth.PublicKey, payload string, signature auth.Signature) bool {
|
||||
return !a.FailSigCheck
|
||||
}
|
||||
|
||||
func (a *TestAuth) ValidateTokenRequest(tokenRequest *auth.TokenRequest) bool {
|
||||
// TODO
|
||||
return true
|
||||
return &auth.AuthToken{Token: a.TestToken, UserId: userId, DeviceId: deviceId, Scope: scope}, nil
|
||||
}
|
||||
|
||||
type TestStore struct {
|
||||
FailSave bool
|
||||
FailSave bool
|
||||
FailLogin bool
|
||||
|
||||
SaveTokenCalled bool
|
||||
}
|
||||
|
@ -44,29 +37,31 @@ func (s *TestStore) SaveToken(token *auth.AuthToken) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *TestStore) GetToken(auth.PublicKey, string) (*auth.AuthToken, error) {
|
||||
func (s *TestStore) GetToken(auth.AuthTokenString) (*auth.AuthToken, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *TestStore) GetPublicKey(string, auth.DownloadKey) (auth.PublicKey, error) {
|
||||
return "", nil
|
||||
func (s *TestStore) GetUserId(auth.Email, auth.Password) (auth.UserId, error) {
|
||||
if s.FailLogin {
|
||||
return 0, store.ErrNoUId
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *TestStore) InsertEmail(auth.PublicKey, string) error {
|
||||
func (s *TestStore) CreateAccount(auth.Email, auth.Password) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TestStore) SetWalletState(
|
||||
pubKey auth.PublicKey,
|
||||
UserId auth.UserId,
|
||||
walletStateJson string,
|
||||
sequence int,
|
||||
signature auth.Signature,
|
||||
downloadKey auth.DownloadKey,
|
||||
) (latestWalletStateJson string, latestSignature auth.Signature, updated bool, err error) {
|
||||
hmac wallet.WalletStateHmac,
|
||||
) (latestWalletStateJson string, latestHmac wallet.WalletStateHmac, updated bool, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *TestStore) GetWalletState(pubKey auth.PublicKey) (walletStateJSON string, signature auth.Signature, err error) {
|
||||
func (s *TestStore) GetWalletState(UserId auth.UserId) (walletStateJson string, hmac wallet.WalletStateHmac, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -10,27 +10,21 @@ import (
|
|||
)
|
||||
|
||||
type WalletStateRequest struct {
|
||||
Token auth.AuthTokenString `json:"token"`
|
||||
BodyJSON string `json:"bodyJSON"`
|
||||
PubKey auth.PublicKey `json:"publicKey"`
|
||||
Signature auth.Signature `json:"signature"`
|
||||
|
||||
// downloadKey is derived from the same password used to encrypt the wallet.
|
||||
// We want to keep it all in sync so we update it at the same time.
|
||||
DownloadKey auth.DownloadKey `json:"downloadKey"`
|
||||
Token auth.AuthTokenString `json:"token"`
|
||||
WalletStateJson string `json:"walletStateJson"`
|
||||
Hmac wallet.WalletStateHmac `json:"hmac"`
|
||||
}
|
||||
|
||||
func (r *WalletStateRequest) validate() bool {
|
||||
return (r.Token != auth.AuthTokenString("") &&
|
||||
r.BodyJSON != "" &&
|
||||
r.PubKey != auth.PublicKey("") &&
|
||||
r.Signature != auth.Signature(""))
|
||||
r.WalletStateJson != "" &&
|
||||
r.Hmac != wallet.WalletStateHmac(""))
|
||||
}
|
||||
|
||||
type WalletStateResponse struct {
|
||||
BodyJSON string `json:"bodyJSON"`
|
||||
Signature auth.Signature `json:"signature"`
|
||||
Error string `json:"error"` // in case of 409 Conflict responses. TODO - make field not show up if it's empty, to avoid confusion
|
||||
WalletStateJson string `json:"walletStateJson"`
|
||||
Hmac wallet.WalletStateHmac `json:"hmac"`
|
||||
Error string `json:"error"` // in case of 409 Conflict responses. TODO - make field not show up if it's empty, to avoid confusion
|
||||
}
|
||||
|
||||
func (s *Server) handleWalletState(w http.ResponseWriter, req *http.Request) {
|
||||
|
@ -39,31 +33,21 @@ func (s *Server) handleWalletState(w http.ResponseWriter, req *http.Request) {
|
|||
} else if req.Method == http.MethodPost {
|
||||
s.postWalletState(w, req)
|
||||
} else {
|
||||
errorJSON(w, http.StatusMethodNotAllowed, "")
|
||||
errorJson(w, http.StatusMethodNotAllowed, "")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - There's probably a struct-based solution here like with POST/PUT.
|
||||
// We could put that struct up top as well.
|
||||
func getWalletStateParams(req *http.Request) (pubKey auth.PublicKey, deviceId string, token auth.AuthTokenString, err error) {
|
||||
func getWalletStateParams(req *http.Request) (token auth.AuthTokenString, err error) {
|
||||
tokenSlice, hasTokenSlice := req.URL.Query()["token"]
|
||||
deviceIDSlice, hasDeviceId := req.URL.Query()["deviceId"]
|
||||
pubKeySlice, hasPubKey := req.URL.Query()["publicKey"]
|
||||
|
||||
if !hasDeviceId {
|
||||
err = fmt.Errorf("Missing deviceId parameter")
|
||||
}
|
||||
if !hasTokenSlice {
|
||||
err = fmt.Errorf("Missing token parameter")
|
||||
}
|
||||
if !hasPubKey {
|
||||
err = fmt.Errorf("Missing publicKey parameter")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
deviceId = deviceIDSlice[0]
|
||||
token = auth.AuthTokenString(tokenSlice[0])
|
||||
pubKey = auth.PublicKey(pubKeySlice[0])
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -74,39 +58,41 @@ func (s *Server) getWalletState(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
pubKey, deviceId, token, err := getWalletStateParams(req)
|
||||
token, paramsErr := getWalletStateParams(req)
|
||||
|
||||
if err != nil {
|
||||
// In this specific case, err is limited to values that are safe to give to
|
||||
// the user
|
||||
errorJSON(w, http.StatusBadRequest, err.Error())
|
||||
if paramsErr != nil {
|
||||
// In this specific case, the error is limited to values that are safe to
|
||||
// give to the user.
|
||||
errorJson(w, http.StatusBadRequest, paramsErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkAuth(w, pubKey, deviceId, token, auth.ScopeGetWalletState) {
|
||||
authToken := s.checkAuth(w, token, auth.ScopeGetWalletState)
|
||||
|
||||
if authToken == nil {
|
||||
return
|
||||
}
|
||||
|
||||
latestWalletStateJSON, latestSignature, err := s.store.GetWalletState(pubKey)
|
||||
latestWalletStateJson, latestHmac, err := s.store.GetWalletState(authToken.UserId)
|
||||
|
||||
var response []byte
|
||||
|
||||
if err == store.ErrNoWalletState {
|
||||
errorJSON(w, http.StatusNotFound, "No wallet state")
|
||||
errorJson(w, http.StatusNotFound, "No wallet state")
|
||||
return
|
||||
} else if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error retrieving walletState")
|
||||
internalServiceErrorJson(w, err, "Error retrieving walletState")
|
||||
return
|
||||
}
|
||||
|
||||
walletStateResponse := WalletStateResponse{
|
||||
BodyJSON: latestWalletStateJSON,
|
||||
Signature: latestSignature,
|
||||
WalletStateJson: latestWalletStateJson,
|
||||
Hmac: latestHmac,
|
||||
}
|
||||
response, err = json.Marshal(walletStateResponse)
|
||||
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error generating latestWalletState response")
|
||||
internalServiceErrorJson(w, err, "Error generating latestWalletState response")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -119,44 +105,33 @@ func (s *Server) postWalletState(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !s.auth.IsValidSignature(walletStateRequest.PubKey, walletStateRequest.BodyJSON, walletStateRequest.Signature) {
|
||||
errorJSON(w, http.StatusBadRequest, "Bad signature")
|
||||
var walletStateMetadata wallet.WalletStateMetadata
|
||||
if err := json.Unmarshal([]byte(walletStateRequest.WalletStateJson), &walletStateMetadata); err != nil {
|
||||
errorJson(w, http.StatusBadRequest, "Malformed walletStateJson")
|
||||
return
|
||||
}
|
||||
|
||||
var walletState wallet.WalletState
|
||||
if err := json.Unmarshal([]byte(walletStateRequest.BodyJSON), &walletState); err != nil {
|
||||
errorJSON(w, http.StatusBadRequest, "Malformed walletState JSON")
|
||||
return
|
||||
}
|
||||
|
||||
if s.walletUtil.ValidateWalletState(&walletState) {
|
||||
if s.walletUtil.ValidateWalletStateMetadata(&walletStateMetadata) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if !s.checkAuth(
|
||||
w,
|
||||
walletStateRequest.PubKey,
|
||||
walletState.DeviceID,
|
||||
walletStateRequest.Token,
|
||||
auth.ScopeFull,
|
||||
) {
|
||||
authToken := s.checkAuth(w, walletStateRequest.Token, auth.ScopeFull)
|
||||
if authToken == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - We could do an extra check - pull from db, make sure the new
|
||||
// walletState doesn't regress lastSynced for any given device.
|
||||
// walletStateMetadata doesn't regress lastSynced for any given device.
|
||||
// This is primarily the responsibility of the clients, but we may want to
|
||||
// trade a db call here for a double-check against bugs in the client.
|
||||
// We do already do some validation checks here, but those doesn't require
|
||||
// new database calls.
|
||||
|
||||
latestWalletStateJSON, latestSignature, updated, err := s.store.SetWalletState(
|
||||
walletStateRequest.PubKey,
|
||||
walletStateRequest.BodyJSON,
|
||||
walletState.Sequence(),
|
||||
walletStateRequest.Signature,
|
||||
walletStateRequest.DownloadKey,
|
||||
latestWalletStateJson, latestHmac, updated, err := s.store.SetWalletState(
|
||||
authToken.UserId,
|
||||
walletStateRequest.WalletStateJson,
|
||||
walletStateMetadata.Sequence(),
|
||||
walletStateRequest.Hmac,
|
||||
)
|
||||
|
||||
var response []byte
|
||||
|
@ -166,17 +141,17 @@ func (s *Server) postWalletState(w http.ResponseWriter, req *http.Request) {
|
|||
// there was nothing there. This should only happen if the client sets
|
||||
// sequence != 1 for the first walletState, which would be a bug.
|
||||
// TODO - figure out better error messages and/or document this
|
||||
errorJSON(w, http.StatusConflict, "Bad sequence number (No existing wallet state)")
|
||||
errorJson(w, http.StatusConflict, "Bad sequence number (No existing wallet state)")
|
||||
return
|
||||
} else if err != nil {
|
||||
// Something other than sequence error
|
||||
internalServiceErrorJSON(w, err, "Error saving walletState")
|
||||
internalServiceErrorJson(w, err, "Error saving walletState")
|
||||
return
|
||||
}
|
||||
|
||||
walletStateResponse := WalletStateResponse{
|
||||
BodyJSON: latestWalletStateJSON,
|
||||
Signature: latestSignature,
|
||||
WalletStateJson: latestWalletStateJson,
|
||||
Hmac: latestHmac,
|
||||
}
|
||||
if !updated {
|
||||
// TODO - should we even call this an error?
|
||||
|
@ -185,7 +160,7 @@ func (s *Server) postWalletState(w http.ResponseWriter, req *http.Request) {
|
|||
response, err = json.Marshal(walletStateResponse)
|
||||
|
||||
if err != nil {
|
||||
internalServiceErrorJSON(w, err, "Error generating walletState response")
|
||||
internalServiceErrorJson(w, err, "Error generating walletStateResponse")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ func TestServerPostWalletTooLate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestServerPostWalletErrors(t *testing.T) {
|
||||
// (malformed json, db fail, auth token not found, walletstate signature fail, walletstate invalid (via stub, make sure the validation function is even called), sequence too high, device id doesn't match token device id)
|
||||
// (malformed json, db fail, auth token not found, walletstate invalid (via stub, make sure the validation function is even called), sequence too high, device id doesn't match token device id)
|
||||
// Client sends sequence != 1 for first entry
|
||||
// Client sends sequence == x + 10 for xth entry or whatever
|
||||
t.Fatalf("Test me: PostWallet fails for various reasons")
|
||||
|
@ -33,7 +33,6 @@ func TestServerPostWalletErrors(t *testing.T) {
|
|||
|
||||
func TestServerValidateWalletStateRequest(t *testing.T) {
|
||||
// also add a basic test case for this in TestServerAuthHandlerSuccess to make sure it's called at all
|
||||
// Maybe 401 specifically for missing signature?
|
||||
t.Fatalf("Test me: Implement and test WalletStateRequest.validate()")
|
||||
}
|
||||
|
||||
|
|
157
store/store.go
157
store/store.go
|
@ -1,6 +1,6 @@
|
|||
package store
|
||||
|
||||
// TODO - DeviceID - What about clients that lie about deviceID? Maybe require a certain format to make sure it gives a real value? Something it wouldn't come up with by accident.
|
||||
// TODO - DeviceId - What about clients that lie about deviceId? Maybe require a certain format to make sure it gives a real value? Something it wouldn't come up with by accident.
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/mattn/go-sqlite3"
|
||||
"log"
|
||||
"orblivion/lbry-id/auth"
|
||||
"orblivion/lbry-id/wallet"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -23,17 +24,17 @@ var (
|
|||
ErrDuplicateEmail = fmt.Errorf("Email already exists for this user")
|
||||
ErrDuplicateAccount = fmt.Errorf("User already has an account")
|
||||
|
||||
ErrNoPubKey = fmt.Errorf("Public Key not found with these credentials")
|
||||
ErrNoUId = fmt.Errorf("User Id not found with these credentials")
|
||||
)
|
||||
|
||||
// For test stubs
|
||||
type StoreInterface interface {
|
||||
SaveToken(*auth.AuthToken) error
|
||||
GetToken(auth.PublicKey, string) (*auth.AuthToken, error)
|
||||
SetWalletState(auth.PublicKey, string, int, auth.Signature, auth.DownloadKey) (string, auth.Signature, bool, error)
|
||||
GetWalletState(auth.PublicKey) (string, auth.Signature, error)
|
||||
GetPublicKey(string, auth.DownloadKey) (auth.PublicKey, error)
|
||||
InsertEmail(auth.PublicKey, string) (err error)
|
||||
GetToken(auth.AuthTokenString) (*auth.AuthToken, error)
|
||||
SetWalletState(auth.UserId, string, int, wallet.WalletStateHmac) (string, wallet.WalletStateHmac, bool, error)
|
||||
GetWalletState(auth.UserId) (string, wallet.WalletStateHmac, error)
|
||||
GetUserId(auth.Email, auth.Password) (auth.UserId, error)
|
||||
CreateAccount(auth.Email, auth.Password) (err error)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
|
@ -56,29 +57,41 @@ func (s *Store) Migrate() error {
|
|||
// specify "WHERE sequence=5". Only one of these commands will succeed, and
|
||||
// the other will get back an error.
|
||||
|
||||
// We use AUTOINCREMENT against the protestations of people on the Internet
|
||||
// who claim that INTEGER PRIMARY KEY automatically has autoincrment, and
|
||||
// that using it when it's not "strictly needed" uses extra resources. But
|
||||
// without AUTOINCREMENT, it might reuse primary keys if a row is deleted and
|
||||
// re-added. Who wants that risk? Besides, we'll switch to Postgres when it's
|
||||
// time to scale anyway.
|
||||
|
||||
// We use UNIQUE on auth_tokens.token so that we can retrieve it easily and
|
||||
// identify the user (and I suppose the uniqueness provides a little extra
|
||||
// security in case we screw up the random generator). However the primary
|
||||
// key should still be (user_id, device_id) so that a device's row can be
|
||||
// updated with a new token.
|
||||
|
||||
// TODO does it actually fail with empty "NOT NULL" fields?
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS auth_tokens(
|
||||
token TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
expiration DATETIME NOT NULL,
|
||||
PRIMARY KEY (device_id)
|
||||
PRIMARY KEY (user_id, device_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS wallet_states(
|
||||
public_key TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
wallet_state_blob TEXT NOT NULL,
|
||||
sequence INTEGER NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
download_key TEXT NOT NULL,
|
||||
PRIMARY KEY (public_key)
|
||||
hmac TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id)
|
||||
FOREIGN KEY (user_id) REFERENCES accounts(user_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS accounts(
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
public_key TEXT NOT NULL,
|
||||
PRIMARY KEY (public_key),
|
||||
FOREIGN KEY (public_key) REFERENCES wallet_states(public_key)
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
`
|
||||
|
||||
|
@ -90,12 +103,16 @@ func (s *Store) Migrate() error {
|
|||
// Auth Token //
|
||||
////////////////
|
||||
|
||||
func (s *Store) GetToken(pubKey auth.PublicKey, deviceID string) (*auth.AuthToken, error) {
|
||||
// TODO - Is it safe to assume that the owner of the token is legit, and is
|
||||
// coming from the legit device id? No need to query by userId and deviceId
|
||||
// (which I did previously)?
|
||||
//
|
||||
// TODO Put the timestamp in the token to avoid duplicates over time. And/or just use a library! Someone solved this already.
|
||||
func (s *Store) GetToken(token auth.AuthTokenString) (*auth.AuthToken, error) {
|
||||
expirationCutoff := time.Now().UTC()
|
||||
|
||||
rows, err := s.db.Query(
|
||||
"SELECT * FROM auth_tokens WHERE public_key=? AND device_id=? AND expiration>?",
|
||||
pubKey, deviceID, expirationCutoff,
|
||||
"SELECT * FROM auth_tokens WHERE token=? AND expiration>?", token, expirationCutoff,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -107,8 +124,8 @@ func (s *Store) GetToken(pubKey auth.PublicKey, deviceID string) (*auth.AuthToke
|
|||
|
||||
err := rows.Scan(
|
||||
&authToken.Token,
|
||||
&authToken.PubKey,
|
||||
&authToken.DeviceID,
|
||||
&authToken.UserId,
|
||||
&authToken.DeviceId,
|
||||
&authToken.Scope,
|
||||
&authToken.Expiration,
|
||||
)
|
||||
|
@ -123,8 +140,8 @@ func (s *Store) GetToken(pubKey auth.PublicKey, deviceID string) (*auth.AuthToke
|
|||
|
||||
func (s *Store) insertToken(authToken *auth.AuthToken, expiration time.Time) (err error) {
|
||||
_, err = s.db.Exec(
|
||||
"INSERT INTO auth_tokens (token, public_key, device_id, scope, expiration) values(?,?,?,?,?)",
|
||||
authToken.Token, authToken.PubKey, authToken.DeviceID, authToken.Scope, expiration,
|
||||
"INSERT INTO auth_tokens (token, user_id, device_id, scope, expiration) values(?,?,?,?,?)",
|
||||
authToken.Token, authToken.UserId, authToken.DeviceId, authToken.Scope, expiration,
|
||||
)
|
||||
|
||||
var sqliteErr sqlite3.Error
|
||||
|
@ -141,8 +158,8 @@ func (s *Store) insertToken(authToken *auth.AuthToken, expiration time.Time) (er
|
|||
|
||||
func (s *Store) updateToken(authToken *auth.AuthToken, experation time.Time) (err error) {
|
||||
res, err := s.db.Exec(
|
||||
"UPDATE auth_tokens SET token=?, expiration=?, scope=? WHERE public_key=? AND device_id=?",
|
||||
authToken.Token, experation, authToken.Scope, authToken.PubKey, authToken.DeviceID,
|
||||
"UPDATE auth_tokens SET token=?, expiration=?, scope=? WHERE user_id=? AND device_id=?",
|
||||
authToken.Token, experation, authToken.Scope, authToken.UserId, authToken.DeviceId,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -191,10 +208,10 @@ func (s *Store) SaveToken(token *auth.AuthToken) (err error) {
|
|||
// Wallet State / Download Key //
|
||||
/////////////////////////////////
|
||||
|
||||
func (s *Store) GetWalletState(pubKey auth.PublicKey) (walletStateJSON string, signature auth.Signature, err error) {
|
||||
func (s *Store) GetWalletState(userId auth.UserId) (walletStateJson string, hmac wallet.WalletStateHmac, err error) {
|
||||
rows, err := s.db.Query(
|
||||
"SELECT wallet_state_blob, signature FROM wallet_states WHERE public_key=?",
|
||||
pubKey,
|
||||
"SELECT wallet_state_blob, hmac FROM wallet_states WHERE user_id=?",
|
||||
userId,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -203,8 +220,8 @@ func (s *Store) GetWalletState(pubKey auth.PublicKey) (walletStateJSON string, s
|
|||
|
||||
for rows.Next() {
|
||||
err = rows.Scan(
|
||||
&walletStateJSON,
|
||||
&signature,
|
||||
&walletStateJson,
|
||||
&hmac,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -213,17 +230,16 @@ func (s *Store) GetWalletState(pubKey auth.PublicKey) (walletStateJSON string, s
|
|||
}
|
||||
|
||||
func (s *Store) insertFirstWalletState(
|
||||
pubKey auth.PublicKey,
|
||||
walletStateJSON string,
|
||||
signature auth.Signature,
|
||||
downloadKey auth.DownloadKey,
|
||||
userId auth.UserId,
|
||||
walletStateJson string,
|
||||
hmac wallet.WalletStateHmac,
|
||||
) (err error) {
|
||||
// This will only be used to attempt to insert the first wallet state
|
||||
// (sequence=1). The database will enforce that this will not be set
|
||||
// if this user already has a walletState.
|
||||
_, err = s.db.Exec(
|
||||
"INSERT INTO wallet_states (public_key, wallet_state_blob, sequence, signature, download_key) values(?,?,?,?,?)",
|
||||
pubKey, walletStateJSON, 1, signature, downloadKey.Obfuscate(),
|
||||
"INSERT INTO wallet_states (user_id, wallet_state_blob, sequence, hmac) values(?,?,?,?)",
|
||||
userId, walletStateJson, 1, hmac,
|
||||
)
|
||||
|
||||
var sqliteErr sqlite3.Error
|
||||
|
@ -239,19 +255,18 @@ func (s *Store) insertFirstWalletState(
|
|||
}
|
||||
|
||||
func (s *Store) updateWalletStateToSequence(
|
||||
pubKey auth.PublicKey,
|
||||
walletStateJSON string,
|
||||
userId auth.UserId,
|
||||
walletStateJson string,
|
||||
sequence int,
|
||||
signature auth.Signature,
|
||||
downloadKey auth.DownloadKey,
|
||||
hmac wallet.WalletStateHmac,
|
||||
) (err error) {
|
||||
// This will be used for wallet states with sequence > 1.
|
||||
// Use the database to enforce that we only update if we are incrementing the sequence.
|
||||
// This way, if two clients attempt to update at the same time, it will return
|
||||
// ErrNoWalletState for the second one.
|
||||
res, err := s.db.Exec(
|
||||
"UPDATE wallet_states SET wallet_state_blob=?, sequence=?, signature=?, download_key=? WHERE public_key=? AND sequence=?",
|
||||
walletStateJSON, sequence, signature, downloadKey.Obfuscate(), pubKey, sequence-1,
|
||||
"UPDATE wallet_states SET wallet_state_blob=?, sequence=?, hmac=? WHERE user_id=? AND sequence=?",
|
||||
walletStateJson, sequence, hmac, userId, sequence-1,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -269,25 +284,24 @@ func (s *Store) updateWalletStateToSequence(
|
|||
|
||||
// Assumption: walletState has been validated (sequence >=1, etc)
|
||||
// Assumption: Sequence matches walletState.Sequence()
|
||||
// Sequence is only passed in here to avoid deserializing walletStateJSON again
|
||||
// WalletState *struct* is not passed in because we need the exact signed string
|
||||
// Sequence is only passed in here to avoid deserializing walletStateJson again
|
||||
// WalletState *struct* is not passed in because the clients need the exact string to match the hmac
|
||||
func (s *Store) SetWalletState(
|
||||
pubKey auth.PublicKey,
|
||||
walletStateJSON string,
|
||||
userId auth.UserId,
|
||||
walletStateJson string,
|
||||
sequence int,
|
||||
signature auth.Signature,
|
||||
downloadKey auth.DownloadKey,
|
||||
) (latestWalletStateJSON string, latestSignature auth.Signature, updated bool, err error) {
|
||||
hmac wallet.WalletStateHmac,
|
||||
) (latestWalletStateJson string, latestHmac wallet.WalletStateHmac, updated bool, err error) {
|
||||
if sequence == 1 {
|
||||
// If sequence == 1, the client assumed that this is our first
|
||||
// walletState. Try to insert. If we get a conflict, the client
|
||||
// assumed incorrectly and we proceed below to return the latest
|
||||
// walletState from the db.
|
||||
err = s.insertFirstWalletState(pubKey, walletStateJSON, signature, downloadKey)
|
||||
err = s.insertFirstWalletState(userId, walletStateJson, hmac)
|
||||
if err == nil {
|
||||
// Successful update
|
||||
latestWalletStateJSON = walletStateJSON
|
||||
latestSignature = signature
|
||||
latestWalletStateJson = walletStateJson
|
||||
latestHmac = hmac
|
||||
updated = true
|
||||
return
|
||||
} else if err != ErrDuplicateWalletState {
|
||||
|
@ -299,10 +313,10 @@ func (s *Store) SetWalletState(
|
|||
// with sequence - 1. Explicitly try to update the walletState with
|
||||
// sequence - 1. If we updated no rows, the client assumed incorrectly
|
||||
// and we proceed below to return the latest walletState from the db.
|
||||
err = s.updateWalletStateToSequence(pubKey, walletStateJSON, sequence, signature, downloadKey)
|
||||
err = s.updateWalletStateToSequence(userId, walletStateJson, sequence, hmac)
|
||||
if err == nil {
|
||||
latestWalletStateJSON = walletStateJSON
|
||||
latestSignature = signature
|
||||
latestWalletStateJson = walletStateJson
|
||||
latestHmac = hmac
|
||||
updated = true
|
||||
return
|
||||
} else if err != ErrNoWalletState {
|
||||
|
@ -313,20 +327,18 @@ func (s *Store) SetWalletState(
|
|||
// We failed to update above due to a sequence conflict. Perhaps the client
|
||||
// was unaware of an update done by another client. Let's send back the latest
|
||||
// version right away so the requesting client can take care of it.
|
||||
//
|
||||
//
|
||||
// Note that this means that `err` will not be `nil` at this point, but we
|
||||
// already accounted for it with `updated=false`. Instead, we'll pass on any
|
||||
// errors from calling `GetWalletState`.
|
||||
latestWalletStateJSON, latestSignature, err = s.GetWalletState(pubKey)
|
||||
latestWalletStateJson, latestHmac, err = s.GetWalletState(userId)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Store) GetPublicKey(email string, downloadKey auth.DownloadKey) (pubKey auth.PublicKey, err error) {
|
||||
func (s *Store) GetUserId(email auth.Email, password auth.Password) (userId auth.UserId, err error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT ws.public_key from wallet_states ws INNER JOIN accounts a
|
||||
ON a.public_key=ws.public_key
|
||||
WHERE email=? AND download_key=?`,
|
||||
email, downloadKey.Obfuscate(),
|
||||
`SELECT user_id from accounts WHERE email=? AND password=?`,
|
||||
email, password.Obfuscate(),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -334,27 +346,30 @@ func (s *Store) GetPublicKey(email string, downloadKey auth.DownloadKey) (pubKey
|
|||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&pubKey)
|
||||
err = rows.Scan(&userId)
|
||||
return
|
||||
}
|
||||
err = ErrNoPubKey
|
||||
err = ErrNoUId
|
||||
return
|
||||
}
|
||||
|
||||
///////////
|
||||
// Email //
|
||||
///////////
|
||||
/////////////
|
||||
// Account //
|
||||
/////////////
|
||||
|
||||
func (s *Store) InsertEmail(pubKey auth.PublicKey, email string) (err error) {
|
||||
func (s *Store) CreateAccount(email auth.Email, password auth.Password) (err error) {
|
||||
// userId auto-increments
|
||||
_, err = s.db.Exec(
|
||||
"INSERT INTO accounts (public_key, email) values(?,?)",
|
||||
pubKey, email,
|
||||
"INSERT INTO accounts (email, password) values(?,?)",
|
||||
email, password.Obfuscate(),
|
||||
)
|
||||
|
||||
var sqliteErr sqlite3.Error
|
||||
if errors.As(err, &sqliteErr) {
|
||||
// I initially expected to need to check for ErrConstraintUnique.
|
||||
// Maybe for psql it will be?
|
||||
// TODO - is this right? Does the above comment explain that it's backwards
|
||||
// from what I would have expected? Or did I do this backwards?
|
||||
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintPrimaryKey) {
|
||||
err = ErrDuplicateEmail
|
||||
}
|
||||
|
|
|
@ -19,52 +19,54 @@ func TestStoreInsertToken(t *testing.T) {
|
|||
s, sqliteTmpFile := StoreTestInit(t)
|
||||
defer StoreTestCleanup(sqliteTmpFile)
|
||||
|
||||
// created for addition to the DB (no expiration attached)
|
||||
authToken1 := auth.AuthToken{
|
||||
Token: "seekrit-1",
|
||||
DeviceID: "dID",
|
||||
DeviceId: "dId",
|
||||
Scope: "*",
|
||||
PubKey: "pubKey",
|
||||
UserId: 123,
|
||||
}
|
||||
|
||||
// The value expected when we pull it from the database.
|
||||
authToken1DB := authToken1
|
||||
authToken1DB.Expiration = timePtr(time.Now().Add(time.Hour * 24 * 14).UTC())
|
||||
|
||||
authToken2 := authToken1
|
||||
authToken2.Token = "seekrit-2"
|
||||
expiration := time.Now().Add(time.Hour * 24 * 14).UTC()
|
||||
|
||||
// Get a token, come back empty
|
||||
gotToken, err := s.GetToken(authToken1.PubKey, authToken1.DeviceID)
|
||||
gotToken, err := s.GetToken(authToken1.Token)
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||
}
|
||||
|
||||
// Put in a token
|
||||
if err := s.insertToken(&authToken1, *authToken1DB.Expiration); err != nil {
|
||||
if err := s.insertToken(&authToken1, expiration); err != nil {
|
||||
t.Fatalf("Unexpected error in insertToken: %+v", err)
|
||||
}
|
||||
|
||||
// The value expected when we pull it from the database.
|
||||
authToken1Expected := authToken1
|
||||
authToken1Expected.Expiration = timePtr(expiration)
|
||||
|
||||
// Get and confirm the token we just put in
|
||||
gotToken, err = s.GetToken(authToken1.PubKey, authToken1.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken1.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||
}
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken1DB) {
|
||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken1DB, *gotToken)
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken1Expected) {
|
||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken1Expected, *gotToken)
|
||||
}
|
||||
|
||||
// Try to put a different token, fail becaues we already have one
|
||||
if err := s.insertToken(&authToken2, *authToken1DB.Expiration); err != ErrDuplicateToken {
|
||||
// Try to put a different token, fail because we already have one
|
||||
authToken2 := authToken1
|
||||
authToken2.Token = "seekrit-2"
|
||||
|
||||
if err := s.insertToken(&authToken2, expiration); err != ErrDuplicateToken {
|
||||
t.Fatalf(`insertToken err: wanted "%+v", got "%+v"`, ErrDuplicateToken, err)
|
||||
}
|
||||
|
||||
// Get the same *first* token we successfully put in
|
||||
gotToken, err = s.GetToken(authToken1.PubKey, authToken1.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken1.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||
}
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken1DB) {
|
||||
t.Fatalf("token: expected %+v, got: %+v", authToken1DB, gotToken)
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken1Expected) {
|
||||
t.Fatalf("token: expected %+v, got: %+v", authToken1Expected, gotToken)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,57 +78,67 @@ func TestStoreUpdateToken(t *testing.T) {
|
|||
s, sqliteTmpFile := StoreTestInit(t)
|
||||
defer StoreTestCleanup(sqliteTmpFile)
|
||||
|
||||
authToken1 := auth.AuthToken{
|
||||
Token: "seekrit-1",
|
||||
DeviceID: "dID",
|
||||
// created for addition to the DB (no expiration attached)
|
||||
authTokenUpdate := auth.AuthToken{
|
||||
Token: "seekrit-update",
|
||||
DeviceId: "dId",
|
||||
Scope: "*",
|
||||
PubKey: "pubKey",
|
||||
UserId: 123,
|
||||
}
|
||||
authToken2 := authToken1
|
||||
authToken2.Token = "seekrit-2"
|
||||
|
||||
// The value expected when we pull it from the database.
|
||||
authToken2DB := authToken2
|
||||
authToken2DB.Expiration = timePtr(time.Now().Add(time.Hour * 24 * 14).UTC())
|
||||
expiration := time.Now().Add(time.Hour * 24 * 14).UTC()
|
||||
|
||||
// Try to get a token, come back empty because we're just starting out
|
||||
gotToken, err := s.GetToken(authToken1.PubKey, authToken1.DeviceID)
|
||||
gotToken, err := s.GetToken(authTokenUpdate.Token)
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||
}
|
||||
|
||||
// Try to update the token - fail because we don't have an entry there in the first place
|
||||
if err := s.updateToken(&authToken1, *authToken2DB.Expiration); err != ErrNoToken {
|
||||
if err := s.updateToken(&authTokenUpdate, expiration); err != ErrNoToken {
|
||||
t.Fatalf(`updateToken err: wanted "%+v", got "%+v"`, ErrNoToken, err)
|
||||
}
|
||||
|
||||
// Try to get a token, come back empty because the update attempt failed to do anything
|
||||
gotToken, err = s.GetToken(authToken1.PubKey, authToken1.DeviceID)
|
||||
gotToken, err = s.GetToken(authTokenUpdate.Token)
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||
}
|
||||
|
||||
// Put in a token - just so we have something to test updateToken with
|
||||
if err := s.insertToken(&authToken1, *authToken2DB.Expiration); err != nil {
|
||||
// Put in a different token, just so we have something to test that
|
||||
// updateToken overwrites it
|
||||
authTokenInsert := authTokenUpdate
|
||||
authTokenInsert.Token = "seekrit-insert"
|
||||
|
||||
if err := s.insertToken(&authTokenInsert, expiration); err != nil {
|
||||
t.Fatalf("Unexpected error in insertToken: %+v", err)
|
||||
}
|
||||
|
||||
// Now successfully update token
|
||||
if err := s.updateToken(&authToken2, *authToken2DB.Expiration); err != nil {
|
||||
if err := s.updateToken(&authTokenUpdate, expiration); err != nil {
|
||||
t.Fatalf("Unexpected error in updateToken: %+v", err)
|
||||
}
|
||||
|
||||
// The value expected when we pull it from the database.
|
||||
authTokenUpdateExpected := authTokenUpdate
|
||||
authTokenUpdateExpected.Expiration = timePtr(expiration)
|
||||
|
||||
// Get and confirm the token we just put in
|
||||
gotToken, err = s.GetToken(authToken2.PubKey, authToken2.DeviceID)
|
||||
gotToken, err = s.GetToken(authTokenUpdate.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||
}
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken2DB) {
|
||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken2DB, *gotToken)
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authTokenUpdateExpected) {
|
||||
t.Fatalf("token: \n expected %+v\n got: %+v", authTokenUpdateExpected, *gotToken)
|
||||
}
|
||||
|
||||
// Fail to get the token we previously inserted, because it's now been overwritten
|
||||
gotToken, err = s.GetToken(authTokenInsert.Token)
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Two different devices.
|
||||
// Test that a user can have two different devices.
|
||||
// Test first and second Save (one for insert, one for update)
|
||||
// Get fails initially
|
||||
// Put token1-d1 token1-d2
|
||||
|
@ -138,30 +150,24 @@ func TestStoreSaveToken(t *testing.T) {
|
|||
defer StoreTestCleanup(sqliteTmpFile)
|
||||
|
||||
// Version 1 of the token for both devices
|
||||
// created for addition to the DB (no expiration attached)
|
||||
authToken_d1_1 := auth.AuthToken{
|
||||
Token: "seekrit-d1-1",
|
||||
DeviceID: "dID-1",
|
||||
DeviceId: "dId-1",
|
||||
Scope: "*",
|
||||
PubKey: "pubKey",
|
||||
UserId: 123,
|
||||
}
|
||||
|
||||
authToken_d2_1 := authToken_d1_1
|
||||
authToken_d2_1.DeviceID = "dID-2"
|
||||
authToken_d2_1.DeviceId = "dId-2"
|
||||
authToken_d2_1.Token = "seekrit-d2-1"
|
||||
|
||||
// Version 2 of the token for both devices
|
||||
authToken_d1_2 := authToken_d1_1
|
||||
authToken_d1_2.Token = "seekrit-d1-2"
|
||||
|
||||
authToken_d2_2 := authToken_d2_1
|
||||
authToken_d2_2.Token = "seekrit-d2-2"
|
||||
|
||||
// Try to get the tokens, come back empty because we're just starting out
|
||||
gotToken, err := s.GetToken(authToken_d1_1.PubKey, authToken_d1_1.DeviceID)
|
||||
gotToken, err := s.GetToken(authToken_d1_1.Token)
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||
}
|
||||
gotToken, err = s.GetToken(authToken_d2_1.PubKey, authToken_d2_1.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken_d2_1.Token)
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||
}
|
||||
|
@ -184,14 +190,14 @@ func TestStoreSaveToken(t *testing.T) {
|
|||
}
|
||||
|
||||
// Get and confirm the tokens we just put in
|
||||
gotToken, err = s.GetToken(authToken_d1_1.PubKey, authToken_d1_1.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken_d1_1.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||
}
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken_d1_1) {
|
||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken_d1_1, gotToken)
|
||||
}
|
||||
gotToken, err = s.GetToken(authToken_d2_1.PubKey, authToken_d2_1.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken_d2_1.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||
}
|
||||
|
@ -199,6 +205,13 @@ func TestStoreSaveToken(t *testing.T) {
|
|||
t.Fatalf("token: expected %+v, got: %+v", authToken_d2_1, gotToken)
|
||||
}
|
||||
|
||||
// Version 2 of the token for both devices
|
||||
authToken_d1_2 := authToken_d1_1
|
||||
authToken_d1_2.Token = "seekrit-d1-2"
|
||||
|
||||
authToken_d2_2 := authToken_d2_1
|
||||
authToken_d2_2.Token = "seekrit-d2-2"
|
||||
|
||||
// Save Version 2 tokens for both devices
|
||||
if err = s.SaveToken(&authToken_d1_2); err != nil {
|
||||
t.Fatalf("Unexpected error in SaveToken: %+v", err)
|
||||
|
@ -217,14 +230,14 @@ func TestStoreSaveToken(t *testing.T) {
|
|||
}
|
||||
|
||||
// Get and confirm the tokens we just put in
|
||||
gotToken, err = s.GetToken(authToken_d1_2.PubKey, authToken_d1_2.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken_d1_2.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||
}
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken_d1_2) {
|
||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken_d1_2, gotToken)
|
||||
}
|
||||
gotToken, err = s.GetToken(authToken_d2_2.PubKey, authToken_d2_2.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken_d2_2.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||
}
|
||||
|
@ -235,9 +248,8 @@ func TestStoreSaveToken(t *testing.T) {
|
|||
|
||||
// test GetToken using insertToken and updateToken as helpers (so we can set expiration timestamps)
|
||||
// normal
|
||||
// not found for pubkey
|
||||
// not found for device (one for another device does exist)
|
||||
// expired token not returned
|
||||
// token not found
|
||||
// expired not returned
|
||||
func TestStoreGetToken(t *testing.T) {
|
||||
s, sqliteTmpFile := StoreTestInit(t)
|
||||
defer StoreTestCleanup(sqliteTmpFile)
|
||||
|
@ -245,39 +257,34 @@ func TestStoreGetToken(t *testing.T) {
|
|||
// created for addition to the DB (no expiration attached)
|
||||
authToken := auth.AuthToken{
|
||||
Token: "seekrit-d1",
|
||||
DeviceID: "dID",
|
||||
DeviceId: "dId",
|
||||
Scope: "*",
|
||||
PubKey: "pubKey",
|
||||
UserId: 123,
|
||||
}
|
||||
|
||||
// The value expected when we pull it from the database.
|
||||
authTokenDB := authToken
|
||||
authTokenDB.Expiration = timePtr(time.Time(time.Now().UTC().Add(time.Hour * 24 * 14)))
|
||||
expiration := time.Time(time.Now().UTC().Add(time.Hour * 24 * 14))
|
||||
|
||||
// Not found (nothing saved for this pubkey)
|
||||
gotToken, err := s.GetToken(authToken.PubKey, authToken.DeviceID)
|
||||
gotToken, err := s.GetToken(authToken.Token)
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||
}
|
||||
|
||||
// Put in a token
|
||||
if err := s.insertToken(&authToken, *authTokenDB.Expiration); err != nil {
|
||||
if err := s.insertToken(&authToken, expiration); err != nil {
|
||||
t.Fatalf("Unexpected error in insertToken: %+v", err)
|
||||
}
|
||||
|
||||
// The value expected when we pull it from the database.
|
||||
authTokenExpected := authToken
|
||||
authTokenExpected.Expiration = timePtr(expiration)
|
||||
|
||||
// Confirm it saved
|
||||
gotToken, err = s.GetToken(authToken.PubKey, authToken.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||
}
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authTokenDB) {
|
||||
t.Fatalf("token: \n expected %+v\n got: %+v", authTokenDB, gotToken)
|
||||
}
|
||||
|
||||
// Fail to get for another device
|
||||
gotToken, err = s.GetToken(authToken.PubKey, "other-device")
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken for nonexistent device. token: %+v err: %+v", gotToken, err)
|
||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authTokenExpected) {
|
||||
t.Fatalf("token: \n expected %+v\n got: %+v", authTokenExpected, gotToken)
|
||||
}
|
||||
|
||||
// Update the token to be expired
|
||||
|
@ -287,7 +294,7 @@ func TestStoreGetToken(t *testing.T) {
|
|||
}
|
||||
|
||||
// Fail to get the expired token
|
||||
gotToken, err = s.GetToken(authToken.PubKey, authToken.DeviceID)
|
||||
gotToken, err = s.GetToken(authToken.Token)
|
||||
if gotToken != nil || err != ErrNoToken {
|
||||
t.Fatalf("Expected ErrNoToken, for expired token. token: %+v err: %+v", gotToken, err)
|
||||
}
|
||||
|
|
|
@ -4,144 +4,211 @@ A couple example flows so it's clear how it works.
|
|||
|
||||
## Initial setup and account recovery
|
||||
|
||||
```
|
||||
>>> import test_client
|
||||
>>> c1 = test_client.Client()
|
||||
```
|
||||
|
||||
Create a new wallet locally and authenticate based on the newly created public key (the email and password are not used just yet)
|
||||
Set up two clients with the same account (which won't exist on the server yet).
|
||||
|
||||
```
|
||||
>>> c1.new_wallet('email@example.com', '123')
|
||||
>>> c1.get_full_auth_token()
|
||||
Got auth token: 787cefea147f3a7b38e1b9fda49490371b52a3b7077507364854b72c3538f94e
|
||||
>>> from test_client import Client
|
||||
>>> c1 = Client()
|
||||
>>> c2 = Client()
|
||||
>>> c1.set_account("joe2@example.com", "123abc2")
|
||||
>>> c2.set_account("joe2@example.com", "123abc2")
|
||||
```
|
||||
|
||||
Post the wallet along with the downloadKey. The downloadKey is based on the password. It's the same password that will be used (in the full implementation) to encrypt the wallet. This is why we are sending it with the wallet state. We want to keep everything related to the user's password consistent.
|
||||
Each device will have a device_id which will be used in the wallet state metadata to mark which device created a given version. This is used in the `lastSynced` field (see below).
|
||||
|
||||
```
|
||||
>>> c1.post_wallet_state()
|
||||
Successfully updated wallet state
|
||||
Got new walletState: {'deviceId': 'e0349bc4-7e7a-48a2-a562-6c530b28a350', 'lastSynced': {'e0349bc4-7e7a-48a2-a562-6c530b28a350': 1}, 'encryptedWallet': ''}
|
||||
>>> c1.device_id
|
||||
'974690df-85a6-481d-9015-6293226db8c9'
|
||||
>>> c2.device_id
|
||||
'545643c9-ee47-443d-b260-cb9178b8646c'
|
||||
```
|
||||
|
||||
Note that every time a client posts, the server sends back the latest wallet state, whether or not the posted wallet state was rejected for being out of sequence. More on this below.
|
||||
|
||||
Send the email address
|
||||
Register the account on the server with one of the clients.
|
||||
|
||||
```
|
||||
>>> c1.register()
|
||||
Registered
|
||||
```
|
||||
|
||||
Now let's set up a second device
|
||||
|
||||
```
|
||||
>>> c2 = test_client.Client()
|
||||
```
|
||||
|
||||
Gets limited-scope auth token (which includes pubkey) based on email address and downloadKey (which comes from password). This token only allows downloading a wallet state (thus the "downloadKey").
|
||||
|
||||
```
|
||||
>>> c2.get_download_auth_token('email@example.com', '123')
|
||||
Got auth token: fd3f4074e6f1b2401b33e21ce5f69d93255680b37c334b6a4e8ea6385b454b0b
|
||||
Got public key: eeA0FfE5E57E3647524759CA9D7c7Cb1
|
||||
>>>
|
||||
```
|
||||
|
||||
Full auth token requires signature, which requires the wallet, which we don't have yet. (For demo we have a fake signature check, so this restriction is faked by the client)
|
||||
Now that the account exists, grab an auth token with both clients.
|
||||
|
||||
```
|
||||
>>> c1.get_full_auth_token()
|
||||
Got auth token: 941e5159a2caff15f0bdc1c0e6da92691d3073543dbfae810cfe57d51c35f0e0
|
||||
>>> c2.get_full_auth_token()
|
||||
No wallet state, thus no access to private key (or so we pretend for this demo), thus we cannot create a signature
|
||||
Got auth token: b323a18e51263ac052777ca68de716c1f3b4983bf4c918477e355f637c8ea2d4
|
||||
```
|
||||
|
||||
Get the wallet state.
|
||||
## Syncing
|
||||
|
||||
Create a new wallet state (wallet + metadata) and post it to the server. Note that after posting, it says it "got" a new wallet state. This is because the post endpoint also returns the latest version. The purpose of this will be explained in "Conflicts" below.
|
||||
|
||||
The fields in the walletstate are:
|
||||
|
||||
* `encryptedWallet` - the actual encrypted wallet data
|
||||
* `lastSynced` - a mapping between deviceId and the latest sequence number that it _created_. This is bookkeeping to prevent certain syncing errors.
|
||||
* `deviceId` - the device that made _this_ wallet state version (NOTE this admittedly seems redundant with `lastSynced` and may be removed)
|
||||
|
||||
```
|
||||
>>> c1.new_wallet_state()
|
||||
>>> c1.post_wallet_state()
|
||||
Successfully updated wallet state on server
|
||||
Got new walletState:
|
||||
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||
'encryptedWallet': '',
|
||||
'lastSynced': {'974690df-85a6-481d-9015-6293226db8c9': 1}}
|
||||
```
|
||||
|
||||
With the other client, get it from the server. Note that both clients have the same data now.
|
||||
|
||||
```
|
||||
>>> c2.get_wallet_state()
|
||||
Got latest walletState: {'deviceId': 'e0349bc4-7e7a-48a2-a562-6c530b28a350', 'lastSynced': {'e0349bc4-7e7a-48a2-a562-6c530b28a350': 1}, 'encryptedWallet': ''}
|
||||
Got latest walletState:
|
||||
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||
'encryptedWallet': '',
|
||||
'lastSynced': {'974690df-85a6-481d-9015-6293226db8c9': 1}}
|
||||
```
|
||||
|
||||
The download-only auth token doesn't allow posting a wallet.
|
||||
## Updating
|
||||
|
||||
Push a new version, get it with the other client. Even though we haven't edited the encrypted wallet yet, each version of a wallet _state_ has an incremented sequence number, and the deviceId that created it.
|
||||
|
||||
```
|
||||
>>> c2.post_wallet_state()
|
||||
Error 403
|
||||
b'{"error":"Forbidden: Scope"}\n'
|
||||
Successfully updated wallet state on server
|
||||
Got new walletState:
|
||||
{'deviceId': '545643c9-ee47-443d-b260-cb9178b8646c',
|
||||
'encryptedWallet': '',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 1}}
|
||||
>>> c1.get_wallet_state()
|
||||
Got latest walletState:
|
||||
{'deviceId': '545643c9-ee47-443d-b260-cb9178b8646c',
|
||||
'encryptedWallet': '',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 1}}
|
||||
```
|
||||
|
||||
But, we can get the full auth token now that we downloaded the wallet. In the full implementation, the wallet would be encrypted with the password. This means that somebody who merely intercepts the public key and download key wouldn't be able to do this step.
|
||||
|
||||
```
|
||||
>>> c2.get_full_auth_token()
|
||||
Got auth token: 4b19739a66f55aff5b7e0f1375c42f41d944b5175f5c5d32b35698a360bb0e5b
|
||||
>>> c2.post_wallet_state()
|
||||
Successfully updated wallet state
|
||||
Got new walletState: {'deviceId': '2ede3f32-4e65-4312-8b89-3b6bde0c5d8e', 'lastSynced': {'e0349bc4-7e7a-48a2-a562-6c530b28a350': 1, '2ede3f32-4e65-4312-8b89-3b6bde0c5d8e': 2}, 'encryptedWallet': ''}
|
||||
```
|
||||
|
||||
# Handling conflicts
|
||||
|
||||
Changes here are represented by 4 random characters separated by colons. The sequence of the changes is relevant to the final state of the wallet. Our goal is to make sure that all clients have all of the changes in the same order. This will thus demonstrate how clients can implement a "rebase" behavior when there is a conflict. In a full implementation, there would also be a system to resolve merge conflicts, but that is out of scope here.
|
||||
|
||||
First, create a local change and post it
|
||||
## Wallet Changes
|
||||
|
||||
For demo purposes, this test client represents each change to the wallet by appending segments separated by `:` so that we can more easily follow the history. (The real app will not actually edit the wallet in the form of an append log.)
|
||||
|
||||
```
|
||||
>>> c1.cur_encrypted_wallet()
|
||||
''
|
||||
>>> c1.change_encrypted_wallet()
|
||||
>>> c1.cur_encrypted_wallet()
|
||||
':f801'
|
||||
':2fbE'
|
||||
```
|
||||
|
||||
The wallet is synced between the clients.
|
||||
|
||||
```
|
||||
>>> c1.post_wallet_state()
|
||||
Successfully updated wallet state
|
||||
Got new walletState: {'deviceId': 'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 2}, 'encryptedWallet': ':f801'}
|
||||
>>> c1.cur_encrypted_wallet()
|
||||
':f801'
|
||||
```
|
||||
|
||||
The other client gets the update and sees the same thing locally:
|
||||
|
||||
```
|
||||
Successfully updated wallet state on server
|
||||
Got new walletState:
|
||||
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||
'encryptedWallet': ':2fbE',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 3}}
|
||||
>>> c2.get_wallet_state()
|
||||
Got latest walletState: {'deviceId': 'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 2}, 'encryptedWallet': ':f801'}
|
||||
Got latest walletState:
|
||||
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||
'encryptedWallet': ':2fbE',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 3}}
|
||||
>>> c2.cur_encrypted_wallet()
|
||||
':f801'
|
||||
':2fbE'
|
||||
```
|
||||
|
||||
Now, both clients make different local changes and both try to post them
|
||||
## Merging Changes
|
||||
|
||||
Both clients create changes. They now have diverging wallets.
|
||||
|
||||
```
|
||||
>>> c1.change_encrypted_wallet()
|
||||
>>> c2.change_encrypted_wallet()
|
||||
>>> c1.cur_encrypted_wallet()
|
||||
':f801:576b'
|
||||
':2fbE:BD62'
|
||||
>>> c2.cur_encrypted_wallet()
|
||||
':f801:dDE7'
|
||||
':2fbE:e7ac'
|
||||
```
|
||||
|
||||
One client posts its change first. The other client pulls that change, and _merges_ those changes on top of the changes it had saved locally.
|
||||
|
||||
The _merge base_ that a given client uses is the last version that it successfully got from or posted to the server. You can see the merge base here: the first part of the wallet which does not change from this merge.
|
||||
|
||||
```
|
||||
>>> c1.post_wallet_state()
|
||||
Successfully updated wallet state
|
||||
Got new walletState: {'deviceId': 'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 3}, 'encryptedWallet': ':f801:576b'}
|
||||
|
||||
>>> c2.post_wallet_state()
|
||||
Wallet state out of date. Getting updated wallet state. Try again.
|
||||
Got new walletState: {'deviceId': 'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 3}, 'encryptedWallet': ':f801:576b'}
|
||||
```
|
||||
|
||||
Client 2 gets a conflict, and the server sends it the updated wallet state that was just created by Client 1 (to save an extra request to `getWalletState`).
|
||||
|
||||
Its local change still exists, but now it's on top of client 1's latest change. (In a full implementation, this is where conflict resolution might take place.)
|
||||
|
||||
```
|
||||
Successfully updated wallet state on server
|
||||
Got new walletState:
|
||||
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||
'encryptedWallet': ':2fbE:BD62',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 4}}
|
||||
>>> c2.get_wallet_state()
|
||||
Got latest walletState:
|
||||
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||
'encryptedWallet': ':2fbE:BD62',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 4}}
|
||||
>>> c2.cur_encrypted_wallet()
|
||||
':f801:576b:dDE7'
|
||||
':2fbE:BD62:e7ac'
|
||||
```
|
||||
|
||||
Client 2 tries again to post, and it succeeds. Client 1 receives it.
|
||||
Finally, the client with the merged wallet pushes it to the server, and the other client gets the update.
|
||||
|
||||
```
|
||||
>>> c2.post_wallet_state()
|
||||
Successfully updated wallet state
|
||||
Got new walletState: {'deviceId': '127e0045-425c-4dd8-a742-90cd52b9377b', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 3, '127e0045-425c-4dd8-a742-90cd52b9377b': 4}, 'encryptedWallet': ':f801:576b:dDE7'}
|
||||
Successfully updated wallet state on server
|
||||
Got new walletState:
|
||||
{'deviceId': '545643c9-ee47-443d-b260-cb9178b8646c',
|
||||
'encryptedWallet': ':2fbE:BD62:e7ac',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 5,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 4}}
|
||||
>>> c1.get_wallet_state()
|
||||
Got latest walletState: {'deviceId': '127e0045-425c-4dd8-a742-90cd52b9377b', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 3, '127e0045-425c-4dd8-a742-90cd52b9377b': 4}, 'encryptedWallet': ':f801:576b:dDE7'}
|
||||
Got latest walletState:
|
||||
{'deviceId': '545643c9-ee47-443d-b260-cb9178b8646c',
|
||||
'encryptedWallet': ':2fbE:BD62:e7ac',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 5,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 4}}
|
||||
>>> c1.cur_encrypted_wallet()
|
||||
':f801:576b:dDE7'
|
||||
':2fbE:BD62:e7ac'
|
||||
```
|
||||
|
||||
## Conflicts
|
||||
|
||||
A client cannot post if it is not up to date. It needs to merge in any new changes on the server before posting its own changes. For convenience, if a conflicting post request is made, the server responds with the latest version of the wallet state (just like a GET request). This way the client doesn't need to make a second request to perform the merge.
|
||||
|
||||
(If a non-conflicting post request is made, it responds with the same wallet state that the client just posted, as it is now the server's current wallet state)
|
||||
|
||||
```
|
||||
>>> c2.change_encrypted_wallet()
|
||||
>>> c2.post_wallet_state()
|
||||
Successfully updated wallet state on server
|
||||
Got new walletState:
|
||||
{'deviceId': '545643c9-ee47-443d-b260-cb9178b8646c',
|
||||
'encryptedWallet': ':2fbE:BD62:e7ac:4EEf',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 6,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 4}}
|
||||
>>> c1.change_encrypted_wallet()
|
||||
>>> c1.post_wallet_state()
|
||||
Wallet state out of date. Getting updated wallet state. Try again.
|
||||
Got new walletState:
|
||||
{'deviceId': '545643c9-ee47-443d-b260-cb9178b8646c',
|
||||
'encryptedWallet': ':2fbE:BD62:e7ac:4EEf',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 6,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 4}}
|
||||
```
|
||||
|
||||
Now the merge is complete, and the client can make a second post request containing the merged wallet.
|
||||
|
||||
```
|
||||
>>> c1.post_wallet_state()
|
||||
Successfully updated wallet state on server
|
||||
Got new walletState:
|
||||
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||
'encryptedWallet': ':2fbE:BD62:e7ac:4EEf:DC86',
|
||||
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 6,
|
||||
'974690df-85a6-481d-9015-6293226db8c9': 7}}
|
||||
```
|
||||
|
|
177
test_client/gen-readme.py
Normal file
177
test_client/gen-readme.py
Normal file
|
@ -0,0 +1,177 @@
|
|||
# Generate the README since I want real behavior interspersed with comments
|
||||
# Come to think of it, this is accidentally a pretty okay integration test for client and server
|
||||
# NOTE - delete the database before running this, or else you'll get an error for registering. also we want the wallet to start empty
|
||||
|
||||
def code_block(code):
|
||||
print ("```")
|
||||
for line in code.strip().split('\n'):
|
||||
print(">>> " + line)
|
||||
if ' = ' in line or "import" in line:
|
||||
exec('global c1, c2\n' + line)
|
||||
else:
|
||||
result = eval(line)
|
||||
if result is not None:
|
||||
print(repr(result))
|
||||
print ("```")
|
||||
|
||||
print("""# Test Client
|
||||
|
||||
A couple example flows so it's clear how it works.
|
||||
""")
|
||||
|
||||
print("""## Initial setup and account recovery
|
||||
|
||||
Set up two clients with the same account (which won't exist on the server yet).
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
from test_client import Client
|
||||
c1 = Client()
|
||||
c2 = Client()
|
||||
c1.set_account("joe2@example.com", "123abc2")
|
||||
c2.set_account("joe2@example.com", "123abc2")
|
||||
""")
|
||||
|
||||
print("""
|
||||
Each device will have a device_id which will be used in the wallet state metadata to mark which device created a given version. This is used in the `lastSynced` field (see below).
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.device_id
|
||||
c2.device_id
|
||||
""")
|
||||
|
||||
print("""
|
||||
Register the account on the server with one of the clients.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.register()
|
||||
""")
|
||||
|
||||
print("""
|
||||
Now that the account exists, grab an auth token with both clients.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.get_full_auth_token()
|
||||
c2.get_full_auth_token()
|
||||
""")
|
||||
|
||||
# TODO - wait isn't it redundant to have the `deviceId` field, for the same reason it's redundant to have the `sequence` field?
|
||||
|
||||
print("""
|
||||
## Syncing
|
||||
|
||||
Create a new wallet state (wallet + metadata) and post it to the server. Note that after posting, it says it "got" a new wallet state. This is because the post endpoint also returns the latest version. The purpose of this will be explained in "Conflicts" below.
|
||||
|
||||
The fields in the walletstate are:
|
||||
|
||||
* `encryptedWallet` - the actual encrypted wallet data
|
||||
* `lastSynced` - a mapping between deviceId and the latest sequence number that it _created_. This is bookkeeping to prevent certain syncing errors.
|
||||
* `deviceId` - the device that made _this_ wallet state version (NOTE this admittedly seems redundant with `lastSynced` and may be removed)
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.new_wallet_state()
|
||||
c1.post_wallet_state()
|
||||
""")
|
||||
|
||||
print("""
|
||||
With the other client, get it from the server. Note that both clients have the same data now.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c2.get_wallet_state()
|
||||
""")
|
||||
|
||||
print("""
|
||||
## Updating
|
||||
|
||||
Push a new version, get it with the other client. Even though we haven't edited the encrypted wallet yet, each version of a wallet _state_ has an incremented sequence number, and the deviceId that created it.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c2.post_wallet_state()
|
||||
c1.get_wallet_state()
|
||||
""")
|
||||
|
||||
print("""
|
||||
## Wallet Changes
|
||||
|
||||
For demo purposes, this test client represents each change to the wallet by appending segments separated by `:` so that we can more easily follow the history. (The real app will not actually edit the wallet in the form of an append log.)
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.cur_encrypted_wallet()
|
||||
c1.change_encrypted_wallet()
|
||||
c1.cur_encrypted_wallet()
|
||||
""")
|
||||
|
||||
print("""
|
||||
The wallet is synced between the clients.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.post_wallet_state()
|
||||
c2.get_wallet_state()
|
||||
c2.cur_encrypted_wallet()
|
||||
""")
|
||||
|
||||
print("""
|
||||
## Merging Changes
|
||||
|
||||
Both clients create changes. They now have diverging wallets.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.change_encrypted_wallet()
|
||||
c2.change_encrypted_wallet()
|
||||
c1.cur_encrypted_wallet()
|
||||
c2.cur_encrypted_wallet()
|
||||
""")
|
||||
|
||||
print("""
|
||||
One client posts its change first. The other client pulls that change, and _merges_ those changes on top of the changes it had saved locally.
|
||||
|
||||
The _merge base_ that a given client uses is the last version that it successfully got from or posted to the server. You can see the merge base here: the first part of the wallet which does not change from this merge.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.post_wallet_state()
|
||||
c2.get_wallet_state()
|
||||
c2.cur_encrypted_wallet()
|
||||
""")
|
||||
|
||||
print("""
|
||||
Finally, the client with the merged wallet pushes it to the server, and the other client gets the update.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c2.post_wallet_state()
|
||||
c1.get_wallet_state()
|
||||
c1.cur_encrypted_wallet()
|
||||
""")
|
||||
|
||||
print("""
|
||||
## Conflicts
|
||||
|
||||
A client cannot post if it is not up to date. It needs to merge in any new changes on the server before posting its own changes. For convenience, if a conflicting post request is made, the server responds with the latest version of the wallet state (just like a GET request). This way the client doesn't need to make a second request to perform the merge.
|
||||
|
||||
(If a non-conflicting post request is made, it responds with the same wallet state that the client just posted, as it is now the server's current wallet state)
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c2.change_encrypted_wallet()
|
||||
c2.post_wallet_state()
|
||||
c1.change_encrypted_wallet()
|
||||
c1.post_wallet_state()
|
||||
""")
|
||||
|
||||
print("""
|
||||
Now the merge is complete, and the client can make a second post request containing the merged wallet.
|
||||
""")
|
||||
|
||||
code_block("""
|
||||
c1.post_wallet_state()
|
||||
""")
|
|
@ -1,5 +1,6 @@
|
|||
#!/bin/python3
|
||||
import random, string, json, uuid, requests, hashlib, time
|
||||
import random, string, json, uuid, requests, hashlib
|
||||
from pprint import pprint
|
||||
|
||||
BASE_URL = 'http://localhost:8090'
|
||||
AUTH_FULL_URL = BASE_URL + '/auth/full'
|
||||
|
@ -12,8 +13,21 @@ def wallet_state_sequence(wallet_state):
|
|||
return 0
|
||||
return wallet_state['lastSynced'][wallet_state['deviceId']]
|
||||
|
||||
def download_key(password):
|
||||
return hashlib.sha256(password.encode('utf-8')).hexdigest()
|
||||
# TODO - do this correctly
|
||||
def create_login_password(root_password):
|
||||
return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[:32]
|
||||
|
||||
# TODO - do this correctly
|
||||
def create_encryption_key(root_password):
|
||||
return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[32:]
|
||||
|
||||
# TODO - do this correctly
|
||||
def check_hmac(wallet_state, encryption_key, hmac):
|
||||
return hmac == 'Good HMAC'
|
||||
|
||||
# TODO - do this correctly
|
||||
def create_hmac(wallet_state, encryption_key):
|
||||
return 'Good HMAC'
|
||||
|
||||
class Client():
|
||||
def _validate_new_wallet_state(self, new_wallet_state):
|
||||
|
@ -48,25 +62,34 @@ class Client():
|
|||
|
||||
self.wallet_state = None
|
||||
|
||||
def new_wallet(self, email, password):
|
||||
# Obviously not real behavior
|
||||
self.public_key = ''.join(random.choice(string.hexdigits) for x in range(32))
|
||||
# TODO - save change to disk in between, associated with account and/or
|
||||
# wallet
|
||||
self._encrypted_wallet_local_changes = ''
|
||||
|
||||
# TODO - make this act more sdk-like. in fact maybe even install the sdk?
|
||||
|
||||
# TODO - This does not deal with the question of tying accounts to wallets.
|
||||
# Does a new wallet state mean a we're creating a new account? What happens
|
||||
# if we create a new wallet state tied to an existing account? Do we merge it
|
||||
# with what's on the server anyway? Do we refuse to merge, or warn the user?
|
||||
# Etc. This sort of depends on how the LBRY Desktop/SDK usually behave. For
|
||||
# now, it'll end up just merging any un-saved local changes with whatever is
|
||||
# on the server.
|
||||
def new_wallet_state(self):
|
||||
# camel-cased to ease json interop
|
||||
self.wallet_state = {'lastSynced': {}, 'encryptedWallet': ''}
|
||||
|
||||
# TODO - actual encryption with password
|
||||
# TODO - actual encryption with encryption_key
|
||||
self._encrypted_wallet_local_changes = ''
|
||||
|
||||
def set_account(self, email, root_password):
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.root_password = root_password
|
||||
|
||||
def register(self):
|
||||
body = json.dumps({
|
||||
'token': self.auth_token,
|
||||
'publicKey': self.public_key,
|
||||
'deviceId': self.device_id,
|
||||
'email': self.email,
|
||||
'password': create_login_password(self.root_password),
|
||||
})
|
||||
response = requests.post(REGISTER_URL, body)
|
||||
if response.status_code != 201:
|
||||
|
@ -78,7 +101,7 @@ class Client():
|
|||
def get_download_auth_token(self, email, password):
|
||||
body = json.dumps({
|
||||
'email': email,
|
||||
'downloadKey': download_key(password),
|
||||
'password': create_login_password(password),
|
||||
'deviceId': self.device_id,
|
||||
})
|
||||
response = requests.post(AUTH_GET_WALLET_STATE_URL, body)
|
||||
|
@ -87,22 +110,18 @@ class Client():
|
|||
print (response.content)
|
||||
return
|
||||
self.auth_token = json.loads(response.content)['token']
|
||||
self.public_key = json.loads(response.content)['publicKey']
|
||||
print ("Got auth token: ", self.auth_token)
|
||||
print ("Got public key: ", self.public_key)
|
||||
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.root_password = root_password
|
||||
|
||||
# TODO - Rename to get_auth_token. same in go. Remember to grep, gotta change
|
||||
# it in README as well.
|
||||
def get_full_auth_token(self):
|
||||
if not self.wallet_state:
|
||||
print ("No wallet state, thus no access to private key (or so we pretend for this demo), thus we cannot create a signature")
|
||||
return
|
||||
|
||||
body = json.dumps({
|
||||
'tokenRequestJSON': json.dumps({'deviceId': self.device_id, 'requestTime': int(time.time())}),
|
||||
'publicKey': self.public_key,
|
||||
'signature': 'Good Signature',
|
||||
'email': self.email,
|
||||
'password': create_login_password(self.root_password),
|
||||
'deviceId': self.device_id,
|
||||
})
|
||||
response = requests.post(AUTH_FULL_URL, body)
|
||||
if response.status_code != 200:
|
||||
|
@ -112,11 +131,14 @@ class Client():
|
|||
self.auth_token = json.loads(response.content)['token']
|
||||
print ("Got auth token: ", self.auth_token)
|
||||
|
||||
# TODO - What about cases where we are managing multiple different wallets?
|
||||
# Some will have lower sequences. If you accidentally mix it up client-side,
|
||||
# you might end up overwriting one with a lower sequence entirely. Maybe we
|
||||
# want to annotate them with which account we're talking about. Again, we
|
||||
# should see how LBRY Desktop/SDK deal with it.
|
||||
def get_wallet_state(self):
|
||||
params = {
|
||||
'token': self.auth_token,
|
||||
'publicKey': self.public_key,
|
||||
'deviceId': self.device_id,
|
||||
}
|
||||
response = requests.get(WALLET_STATE_URL, params=params)
|
||||
if response.status_code != 200:
|
||||
|
@ -124,17 +146,16 @@ class Client():
|
|||
print (response.content)
|
||||
return
|
||||
|
||||
if json.loads(response.content)['signature'] != "Good Signature":
|
||||
print ('Error - bad signature on new wallet')
|
||||
print (response.content)
|
||||
return
|
||||
if response.status_code != 200:
|
||||
print ('Error', response.status_code)
|
||||
new_wallet_state_str = json.loads(response.content)['walletStateJson']
|
||||
new_wallet_state = json.loads(new_wallet_state_str)
|
||||
encryption_key = create_encryption_key(self.root_password)
|
||||
hmac = json.loads(response.content)['hmac']
|
||||
if not check_hmac(new_wallet_state_str, encryption_key, hmac):
|
||||
print ('Error - bad hmac on new wallet')
|
||||
print (response.content)
|
||||
return
|
||||
|
||||
# In reality, we'd examine, merge, verify, validate etc this new wallet state.
|
||||
new_wallet_state = json.loads(json.loads(response.content)['bodyJSON'])
|
||||
if self.wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state):
|
||||
print ('Error - new wallet does not validate')
|
||||
print (response.content)
|
||||
|
@ -144,58 +165,63 @@ class Client():
|
|||
# This is if we're getting a wallet_state for the first time. Initialize
|
||||
# the local changes.
|
||||
self._encrypted_wallet_local_changes = ''
|
||||
|
||||
self.wallet_state = new_wallet_state
|
||||
|
||||
print ("Got latest walletState: ", self.wallet_state)
|
||||
print ("Got latest walletState:")
|
||||
pprint(self.wallet_state)
|
||||
|
||||
def post_wallet_state(self):
|
||||
# Create a *new* wallet state, indicating that it was last updated by this
|
||||
# device, with the updated sequence, and include our local encrypted wallet changes.
|
||||
# Don't set self.wallet_state to this until we know that it's accepted by
|
||||
# the server.
|
||||
if self.wallet_state:
|
||||
submitted_wallet_state = {
|
||||
"deviceId": self.device_id,
|
||||
"lastSynced": dict(self.wallet_state['lastSynced']),
|
||||
"encryptedWallet": self.cur_encrypted_wallet(),
|
||||
}
|
||||
submitted_wallet_state['lastSynced'][self.device_id] = wallet_state_sequence(self.wallet_state) + 1
|
||||
else:
|
||||
# If we have no self.wallet_state, we shouldn't be able to have a full
|
||||
# auth token, so this code path is just to demonstrate an auth failure
|
||||
submitted_wallet_state = {
|
||||
"deviceId": self.device_id,
|
||||
"lastSynced": {self.device_id: 1},
|
||||
"encryptedWallet": self.cur_encrypted_wallet(),
|
||||
}
|
||||
if not self.wallet_state:
|
||||
print ("No wallet state to post.")
|
||||
return
|
||||
|
||||
submitted_wallet_state = {
|
||||
"deviceId": self.device_id,
|
||||
"lastSynced": dict(self.wallet_state['lastSynced']),
|
||||
"encryptedWallet": self.cur_encrypted_wallet(),
|
||||
}
|
||||
submitted_wallet_state['lastSynced'][self.device_id] = wallet_state_sequence(self.wallet_state) + 1
|
||||
|
||||
encryption_key = create_encryption_key(self.root_password)
|
||||
|
||||
submitted_wallet_state_str = json.dumps(submitted_wallet_state)
|
||||
submitted_wallet_state_hmac = create_hmac(submitted_wallet_state_str, encryption_key)
|
||||
body = json.dumps({
|
||||
'token': self.auth_token,
|
||||
'bodyJSON': json.dumps(submitted_wallet_state),
|
||||
'publicKey': self.public_key,
|
||||
'downloadKey': download_key(self.password),
|
||||
'signature': 'Good Signature',
|
||||
'walletStateJson': submitted_wallet_state_str,
|
||||
'hmac': submitted_wallet_state_hmac
|
||||
})
|
||||
response = requests.post(WALLET_STATE_URL, body)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Our local changes are no longer local, so we reset them
|
||||
self._encrypted_wallet_local_changes = ''
|
||||
print ('Successfully updated wallet state')
|
||||
print ('Successfully updated wallet state on server')
|
||||
elif response.status_code == 409:
|
||||
print ('Wallet state out of date. Getting updated wallet state. Try again.')
|
||||
# Don't return yet! We got the updated state here, so we still process it below.
|
||||
else:
|
||||
print ('Error', response.status_code)
|
||||
print (response.content)
|
||||
return
|
||||
|
||||
if json.loads(response.content)['signature'] != "Good Signature":
|
||||
print ('Error - bad signature on new wallet')
|
||||
# Now we get a new wallet state back as a response
|
||||
# TODO - factor this into the same thing as the get_wallet_state function
|
||||
|
||||
new_wallet_state_str = json.loads(response.content)['walletStateJson']
|
||||
new_wallet_state_hmac = json.loads(response.content)['hmac']
|
||||
new_wallet_state = json.loads(new_wallet_state_str)
|
||||
if not check_hmac(new_wallet_state_str, encryption_key, new_wallet_state_hmac):
|
||||
print ('Error - bad hmac on new wallet')
|
||||
print (response.content)
|
||||
return
|
||||
|
||||
# In reality, we'd examine, merge, verify, validate etc this new wallet state.
|
||||
new_wallet_state = json.loads(json.loads(response.content)['bodyJSON'])
|
||||
if submitted_wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state):
|
||||
print ('Error - new wallet does not validate')
|
||||
print (response.content)
|
||||
|
@ -203,7 +229,8 @@ class Client():
|
|||
|
||||
self.wallet_state = new_wallet_state
|
||||
|
||||
print ("Got new walletState: ", self.wallet_state)
|
||||
print ("Got new walletState:")
|
||||
pprint(self.wallet_state)
|
||||
|
||||
def change_encrypted_wallet(self):
|
||||
if not self.wallet_state:
|
||||
|
|
|
@ -1,26 +1,32 @@
|
|||
package wallet
|
||||
|
||||
import "orblivion/lbry-id/auth"
|
||||
|
||||
// Currently a small package but given other packages it makes imports easier.
|
||||
// Also this might grow substantially over time
|
||||
|
||||
// For test stubs
|
||||
type WalletUtilInterface interface {
|
||||
ValidateWalletState(walletState *WalletState) bool
|
||||
ValidateWalletStateMetadata(walletState *WalletStateMetadata) bool
|
||||
}
|
||||
|
||||
type WalletUtil struct{}
|
||||
|
||||
type WalletState struct {
|
||||
DeviceID string `json:"deviceId"`
|
||||
LastSynced map[string]int `json:"lastSynced"`
|
||||
// This is a subset of the WalletState structure, only the metadata fields. We
|
||||
// don't need access to the encrypted wallet.
|
||||
type WalletStateMetadata struct {
|
||||
DeviceId auth.DeviceId `json:"deviceId"`
|
||||
LastSynced map[auth.DeviceId]int `json:"lastSynced"`
|
||||
}
|
||||
|
||||
type WalletStateHmac string
|
||||
|
||||
// TODO - These "validate" functions could/should be methods. Though I think
|
||||
// we'd lose mockability for testing, since the method isn't the
|
||||
// WalletUtilInterface.
|
||||
// Mainly the job of the clients but we may as well short-circuit problems
|
||||
// here before saving them.
|
||||
func (wu *WalletUtil) ValidateWalletState(walletState *WalletState) bool {
|
||||
func (wu *WalletUtil) ValidateWalletStateMetadata(walletState *WalletStateMetadata) bool {
|
||||
|
||||
// TODO - nonempty fields, up to date, etc
|
||||
return true
|
||||
|
@ -28,6 +34,6 @@ func (wu *WalletUtil) ValidateWalletState(walletState *WalletState) bool {
|
|||
|
||||
// Assumptions: `ws` has been validated
|
||||
// Avoid having to check for error
|
||||
func (ws *WalletState) Sequence() int {
|
||||
return ws.LastSynced[ws.DeviceID]
|
||||
func (ws *WalletStateMetadata) Sequence() int {
|
||||
return ws.LastSynced[ws.DeviceId]
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@ import (
|
|||
// Test stubs for now
|
||||
|
||||
func TestWalletSequence(t *testing.T) {
|
||||
t.Fatalf("Test me: test that walletState.Sequence() == walletState.lastSynced[wallet.DeviceID]")
|
||||
t.Fatalf("Test me: test that walletState.Sequence() == walletState.lastSynced[wallet.DeviceId]")
|
||||
}
|
||||
|
||||
func TestWalletValidateWalletState(t *testing.T) {
|
||||
// walletState.DeviceID in walletState.lastSynced
|
||||
// walletState.DeviceId in walletState.lastSynced
|
||||
// Sequence for lastSynced all > 1
|
||||
t.Fatalf("Test me: Implement and test validateWalletState.")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue