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 - Learn how to use https://github.com/golang/oauth2 instead
|
||||||
// TODO - Look into jwt, etc.
|
// 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 AuthTokenString string
|
||||||
type PublicKey string
|
|
||||||
type DownloadKey string
|
type DownloadKey string
|
||||||
type Signature string
|
|
||||||
|
|
||||||
type AuthScope string
|
type AuthScope string
|
||||||
|
|
||||||
|
@ -24,70 +27,37 @@ const ScopeGetWalletState = AuthScope("get-wallet-state")
|
||||||
|
|
||||||
// For test stubs
|
// For test stubs
|
||||||
type AuthInterface interface {
|
type AuthInterface interface {
|
||||||
NewToken(pubKey PublicKey, DeviceID string, Scope AuthScope) (*AuthToken, error)
|
// TODO maybe have a "refresh token" thing if the client won't have email available all the time?
|
||||||
IsValidSignature(pubKey PublicKey, payload string, signature Signature) bool
|
NewToken(UserId, DeviceId, AuthScope) (*AuthToken, error)
|
||||||
ValidateTokenRequest(tokenRequest *TokenRequest) bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct{}
|
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
|
// Note that everything here is given to anybody who presents a valid
|
||||||
// downloadKey and associated email. Currently these fields are safe to give
|
// 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.
|
// at that low security level, but keep this in mind as we change this struct.
|
||||||
type AuthToken struct {
|
type AuthToken struct {
|
||||||
Token AuthTokenString `json:"token"`
|
Token AuthTokenString `json:"token"`
|
||||||
DeviceID string `json:"deviceId"`
|
DeviceId DeviceId `json:"deviceId"`
|
||||||
Scope AuthScope `json:"scope"`
|
Scope AuthScope `json:"scope"`
|
||||||
PubKey PublicKey `json:"publicKey"`
|
UserId UserId `json:"userId"`
|
||||||
Expiration *time.Time `json:"expiration"`
|
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
|
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)
|
b := make([]byte, AuthTokenLength)
|
||||||
|
// TODO - Is this is a secure random function? (Maybe audit)
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
return nil, fmt.Errorf("Error generating token: %+v", err)
|
return nil, fmt.Errorf("Error generating token: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &AuthToken{
|
return &AuthToken{
|
||||||
Token: AuthTokenString(hex.EncodeToString(b)),
|
Token: AuthTokenString(hex.EncodeToString(b)),
|
||||||
DeviceID: DeviceID,
|
DeviceId: deviceId,
|
||||||
Scope: Scope,
|
Scope: scope,
|
||||||
PubKey: pubKey,
|
UserId: userId,
|
||||||
// TODO add Expiration here instead of putting it in store.go. and thus redo store.go. d'oh.
|
// TODO add Expiration here instead of putting it in store.go. and thus redo store.go. d'oh.
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -109,3 +79,9 @@ func (d DownloadKey) Obfuscate() string {
|
||||||
hash := sha256.Sum256([]byte(d))
|
hash := sha256.Sum256([]byte(d))
|
||||||
return hex.EncodeToString(hash[:])
|
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
|
// 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) {
|
func TestAuthSignaturePass(t *testing.T) {
|
||||||
t.Fatalf("Test me: Valid siganture passes")
|
t.Fatalf("Test me: Valid siganture passes")
|
||||||
}
|
}
|
||||||
|
|
140
server/auth.go
140
server/auth.go
|
@ -3,165 +3,69 @@ package server
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"orblivion/lbry-id/auth"
|
"orblivion/lbry-id/auth"
|
||||||
"orblivion/lbry-id/store"
|
"orblivion/lbry-id/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
// DeviceId is decided by the device. UserId is decided by the server, and is
|
||||||
TODO - Consider reworking the naming convention in (currently named)
|
// gatekept by Email/Password
|
||||||
`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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
type AuthFullRequest struct {
|
type AuthFullRequest struct {
|
||||||
// TokenRequestJSON: json string within json, so that the string representation is
|
DeviceId auth.DeviceId `json:"deviceId"`
|
||||||
// unambiguous for the purposes of signing. This means we need to deserialize the
|
Email auth.Email `json:"email"`
|
||||||
// request body twice.
|
Password auth.Password `json:"password"`
|
||||||
TokenRequestJSON string `json:"tokenRequestJSON"`
|
|
||||||
PubKey auth.PublicKey `json:"publicKey"`
|
|
||||||
Signature auth.Signature `json:"signature"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AuthFullRequest) validate() bool {
|
func (r *AuthFullRequest) validate() bool {
|
||||||
return (r.TokenRequestJSON != "" &&
|
return (r.DeviceId != "" &&
|
||||||
r.PubKey != auth.PublicKey("") &&
|
r.Email != auth.Email("") && // TODO email validation. Here or store. Stdlib does it: https://stackoverflow.com/a/66624104
|
||||||
r.Signature != auth.Signature(""))
|
r.Password != auth.Password(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthForGetWalletStateRequest struct {
|
type AuthForGetWalletStateRequest struct {
|
||||||
Email string `json:"email"`
|
Email auth.Email `json:"email"`
|
||||||
DownloadKey auth.DownloadKey `json:"downloadKey"`
|
DownloadKey auth.DownloadKey `json:"downloadKey"`
|
||||||
DeviceID string `json:"deviceId"`
|
DeviceId auth.DeviceId `json:"deviceId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AuthForGetWalletStateRequest) validate() bool {
|
func (r *AuthForGetWalletStateRequest) validate() bool {
|
||||||
return (r.Email != "" &&
|
return (r.Email != "" &&
|
||||||
r.DownloadKey != auth.DownloadKey("") &&
|
r.DownloadKey != auth.DownloadKey("") &&
|
||||||
r.DeviceID != "")
|
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getAuthTokenFull(w http.ResponseWriter, req *http.Request) {
|
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
|
var authRequest AuthFullRequest
|
||||||
if !getPostData(w, req, &authRequest) {
|
if !getPostData(w, req, &authRequest) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.auth.IsValidSignature(authRequest.PubKey, authRequest.TokenRequestJSON, authRequest.Signature) {
|
userId, err := s.store.GetUserId(authRequest.Email, authRequest.Password)
|
||||||
errorJSON(w, http.StatusForbidden, "Bad signature")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenRequest auth.TokenRequest
|
authToken, err := s.auth.NewToken(userId, authRequest.DeviceId, auth.ScopeFull)
|
||||||
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)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServiceErrorJSON(w, err, "Error generating auth token")
|
internalServiceErrorJson(w, err, "Error generating auth token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := json.Marshal(&authToken)
|
response, err := json.Marshal(&authToken)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServiceErrorJSON(w, err, "Error generating auth token")
|
internalServiceErrorJson(w, err, "Error generating auth token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.store.SaveToken(authToken); err != nil {
|
if err := s.store.SaveToken(authToken); err != nil {
|
||||||
internalServiceErrorJSON(w, err, "Error saving auth token")
|
internalServiceErrorJson(w, err, "Error saving auth token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ func TestServerAuthHandlerSuccess(t *testing.T) {
|
||||||
testStore := TestStore{}
|
testStore := TestStore{}
|
||||||
s := Server{&testAuth, &testStore, &wallet.WalletUtil{}}
|
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))
|
req := httptest.NewRequest(http.MethodPost, PathAuthTokenFull, bytes.NewBuffer(requestBody))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
@ -51,7 +51,7 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
||||||
expectedStatusCode int
|
expectedStatusCode int
|
||||||
expectedErrorString string
|
expectedErrorString string
|
||||||
|
|
||||||
authFailSigCheck bool
|
authFailLogin bool
|
||||||
authFailGenToken bool
|
authFailGenToken bool
|
||||||
storeFailSave bool
|
storeFailSave bool
|
||||||
}{
|
}{
|
||||||
|
@ -65,7 +65,7 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "request body too large",
|
name: "request body too large",
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
requestBody: fmt.Sprintf(`{"tokenRequestJSON": "%s"}`, strings.Repeat("a", 10000)),
|
requestBody: fmt.Sprintf(`{"password": "%s"}`, strings.Repeat("a", 10000)),
|
||||||
expectedStatusCode: http.StatusRequestEntityTooLarge,
|
expectedStatusCode: http.StatusRequestEntityTooLarge,
|
||||||
expectedErrorString: http.StatusText(http.StatusRequestEntityTooLarge),
|
expectedErrorString: http.StatusText(http.StatusRequestEntityTooLarge),
|
||||||
},
|
},
|
||||||
|
@ -84,26 +84,19 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
||||||
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation",
|
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "signature check fail",
|
name: "login fail",
|
||||||
method: http.MethodPost,
|
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
|
// so long as the JSON is well-formed, the content doesn't matter here since the password check will be stubbed out
|
||||||
requestBody: `{"tokenRequestJSON": "{}", "publicKey": "abc", "signature": "123"}`,
|
requestBody: `{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
||||||
expectedStatusCode: http.StatusForbidden,
|
expectedStatusCode: http.StatusUnauthorized,
|
||||||
expectedErrorString: http.StatusText(http.StatusForbidden) + ": Bad signature",
|
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": No match for email and password",
|
||||||
|
|
||||||
authFailSigCheck: true,
|
authFailLogin: 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",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "generate token fail",
|
name: "generate token fail",
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
requestBody: `{"tokenRequestJSON": "{}", "publicKey": "abc", "signature": "123"}`,
|
requestBody: `{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
||||||
expectedStatusCode: http.StatusInternalServerError,
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
expectedErrorString: http.StatusText(http.StatusInternalServerError),
|
expectedErrorString: http.StatusText(http.StatusInternalServerError),
|
||||||
|
|
||||||
|
@ -112,7 +105,7 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "save token fail",
|
name: "save token fail",
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
requestBody: `{"tokenRequestJSON": "{}", "publicKey": "abc", "signature": "123"}`,
|
requestBody: `{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
||||||
expectedStatusCode: http.StatusInternalServerError,
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
expectedErrorString: http.StatusText(http.StatusInternalServerError),
|
expectedErrorString: http.StatusText(http.StatusInternalServerError),
|
||||||
|
|
||||||
|
@ -125,8 +118,8 @@ func TestServerAuthHandlerErrors(t *testing.T) {
|
||||||
// Set this up to fail according to specification
|
// Set this up to fail according to specification
|
||||||
testAuth := TestAuth{TestToken: auth.AuthTokenString("seekrit")}
|
testAuth := TestAuth{TestToken: auth.AuthTokenString("seekrit")}
|
||||||
testStore := TestStore{}
|
testStore := TestStore{}
|
||||||
if tc.authFailSigCheck {
|
if tc.authFailLogin {
|
||||||
testAuth.FailSigCheck = true
|
testStore.FailLogin = true
|
||||||
} else if tc.authFailGenToken {
|
} else if tc.authFailGenToken {
|
||||||
testAuth.FailGenToken = true
|
testAuth.FailGenToken = true
|
||||||
} else if tc.storeFailSave {
|
} else if tc.storeFailSave {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"orblivion/lbry-id/store"
|
"orblivion/lbry-id/store"
|
||||||
"orblivion/lbry-id/wallet"
|
"orblivion/lbry-id/wallet"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Whereas sever_test.go stubs out auth store and wallet, these will use the real thing, but test fewer paths.
|
// 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{},
|
&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
|
// Get auth token - device 1
|
||||||
////////////////////
|
////////////////////
|
||||||
|
|
||||||
var authToken1 auth.AuthToken
|
var authToken1 auth.AuthToken
|
||||||
responseBody, statusCode := request(
|
responseBody, statusCode = request(
|
||||||
t,
|
t,
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
s.getAuthTokenFull,
|
s.getAuthTokenFull,
|
||||||
PathAuthTokenFull,
|
PathAuthTokenFull,
|
||||||
&authToken1,
|
&authToken1,
|
||||||
fmt.Sprintf(`{
|
`{"deviceId": "dev-1", "email": "abc@example.com", "password": "123"}`,
|
||||||
"tokenRequestJSON": "{\"deviceID\": \"dev-1\", \"requestTime\": %d}",
|
|
||||||
"publickey": "testPubKey",
|
|
||||||
"signature": "Good Signature"
|
|
||||||
}`,
|
|
||||||
time.Now().Unix(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
@ -95,8 +102,8 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
if len(authToken1.Token) != expectedTokenLength {
|
if len(authToken1.Token) != expectedTokenLength {
|
||||||
t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody))
|
t.Fatalf("Expected auth response to contain token length 32: result: %+v", string(responseBody))
|
||||||
}
|
}
|
||||||
if authToken1.DeviceID != "dev-1" {
|
if authToken1.DeviceId != "dev-1" {
|
||||||
t.Fatalf("Unexpected response DeviceID. want: %+v got: %+v", "dev-1", authToken1.DeviceID)
|
t.Fatalf("Unexpected response DeviceId. want: %+v got: %+v", "dev-1", authToken1.DeviceId)
|
||||||
}
|
}
|
||||||
if authToken1.Scope != auth.ScopeFull {
|
if authToken1.Scope != auth.ScopeFull {
|
||||||
t.Fatalf("Unexpected response Scope. want: %+v got: %+v", auth.ScopeFull, authToken1.Scope)
|
t.Fatalf("Unexpected response Scope. want: %+v got: %+v", auth.ScopeFull, authToken1.Scope)
|
||||||
|
@ -113,19 +120,13 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
s.getAuthTokenFull,
|
s.getAuthTokenFull,
|
||||||
PathAuthTokenFull,
|
PathAuthTokenFull,
|
||||||
&authToken2,
|
&authToken2,
|
||||||
fmt.Sprintf(`{
|
`{"deviceId": "dev-2", "email": "abc@example.com", "password": "123"}`,
|
||||||
"tokenRequestJSON": "{\"deviceID\": \"dev-2\", \"requestTime\": %d}",
|
|
||||||
"publickey": "testPubKey",
|
|
||||||
"signature": "Good Signature"
|
|
||||||
}`,
|
|
||||||
time.Now().Unix(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
|
||||||
if authToken2.DeviceID != "dev-2" {
|
if authToken2.DeviceId != "dev-2" {
|
||||||
t.Fatalf("Unexpected response DeviceID. want: %+v got: %+v", "dev-2", authToken2.DeviceID)
|
t.Fatalf("Unexpected response DeviceId. want: %+v got: %+v", "dev-2", authToken2.DeviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
|
@ -141,17 +142,15 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
&walletStateResponse,
|
&walletStateResponse,
|
||||||
fmt.Sprintf(`{
|
fmt.Sprintf(`{
|
||||||
"token": "%s",
|
"token": "%s",
|
||||||
"bodyJSON": "{\"encryptedWallet\": \"blah\", \"lastSynced\":{\"dev-1\": 1}, \"deviceId\": \"dev-1\" }",
|
"walletStateJson": "{\"encryptedWallet\": \"blah\", \"lastSynced\":{\"dev-1\": 1}, \"deviceId\": \"dev-1\" }",
|
||||||
"publickey": "testPubKey",
|
"hmac": "my-hmac-1"
|
||||||
"downloadKey": "myDownloadKey",
|
|
||||||
"signature": "Good Signature"
|
|
||||||
}`, authToken1.Token),
|
}`, authToken1.Token),
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
|
||||||
var walletState wallet.WalletState
|
var walletState wallet.WalletState
|
||||||
err := json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
err := json.Unmarshal([]byte(walletStateResponse.WalletStateJson), &walletState)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %+v", err)
|
t.Fatalf("Unexpected error: %+v", err)
|
||||||
|
@ -170,16 +169,14 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
t,
|
t,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
s.getWalletState,
|
s.getWalletState,
|
||||||
fmt.Sprintf(
|
fmt.Sprintf("%s?token=%s", PathWalletState, authToken2.Token),
|
||||||
"%s?token=%s&publicKey=%s&deviceId=%s",
|
|
||||||
PathWalletState, authToken2.Token, authToken2.PubKey, "dev-2"),
|
|
||||||
&walletStateResponse,
|
&walletStateResponse,
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
err = json.Unmarshal([]byte(walletStateResponse.WalletStateJson), &walletState)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %+v", err)
|
t.Fatalf("Unexpected error: %+v", err)
|
||||||
|
@ -202,16 +199,14 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
&walletStateResponse,
|
&walletStateResponse,
|
||||||
fmt.Sprintf(`{
|
fmt.Sprintf(`{
|
||||||
"token": "%s",
|
"token": "%s",
|
||||||
"bodyJSON": "{\"encryptedWallet\": \"blah2\", \"lastSynced\":{\"dev-1\": 1, \"dev-2\": 2}, \"deviceId\": \"dev-2\" }",
|
"walletStateJson": "{\"encryptedWallet\": \"blah2\", \"lastSynced\":{\"dev-1\": 1, \"dev-2\": 2}, \"deviceId\": \"dev-2\" }",
|
||||||
"publickey": "testPubKey",
|
"hmac": "my-hmac-2"
|
||||||
"downloadKey": "myDownloadKey",
|
|
||||||
"signature": "Good Signature"
|
|
||||||
}`, authToken2.Token),
|
}`, authToken2.Token),
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
err = json.Unmarshal([]byte(walletStateResponse.WalletStateJson), &walletState)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %+v", err)
|
t.Fatalf("Unexpected error: %+v", err)
|
||||||
|
@ -230,16 +225,14 @@ func TestIntegrationWalletUpdates(t *testing.T) {
|
||||||
t,
|
t,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
s.getWalletState,
|
s.getWalletState,
|
||||||
fmt.Sprintf(
|
fmt.Sprintf("%s?token=%s", PathWalletState, authToken1.Token),
|
||||||
"%s?token=%s&publicKey=%s&deviceId=%s",
|
|
||||||
PathWalletState, authToken1.Token, authToken1.PubKey, "dev-1"),
|
|
||||||
&walletStateResponse,
|
&walletStateResponse,
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
checkStatusCode(t, statusCode, responseBody)
|
checkStatusCode(t, statusCode, responseBody)
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(walletStateResponse.BodyJSON), &walletState)
|
err = json.Unmarshal([]byte(walletStateResponse.WalletStateJson), &walletState)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %+v", err)
|
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)
|
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"
|
"orblivion/lbry-id/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO email verification cycle
|
||||||
|
|
||||||
type RegisterRequest struct {
|
type RegisterRequest struct {
|
||||||
Token auth.AuthTokenString `json:"token"`
|
Email auth.Email `json:"email"`
|
||||||
PubKey auth.PublicKey `json:"publicKey"`
|
Password auth.Password `json:"password"`
|
||||||
DeviceID string `json:"deviceId"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RegisterRequest) validate() bool {
|
func (r *RegisterRequest) validate() bool {
|
||||||
return (r.Token != auth.AuthTokenString("") &&
|
return r.Email != "" && r.Password != ""
|
||||||
r.PubKey != auth.PublicKey("") &&
|
|
||||||
r.DeviceID != "" &&
|
|
||||||
r.Email != "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) register(w http.ResponseWriter, req *http.Request) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.checkAuth(
|
err := s.store.CreateAccount(registerRequest.Email, registerRequest.Password)
|
||||||
w,
|
|
||||||
registerRequest.PubKey,
|
|
||||||
registerRequest.DeviceID,
|
|
||||||
registerRequest.Token,
|
|
||||||
auth.ScopeFull,
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.store.InsertEmail(registerRequest.PubKey, registerRequest.Email)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == store.ErrDuplicateEmail || err == store.ErrDuplicateAccount {
|
if err == store.ErrDuplicateEmail || err == store.ErrDuplicateAccount {
|
||||||
errorJSON(w, http.StatusConflict, "Error registering")
|
errorJson(w, http.StatusConflict, "Error registering")
|
||||||
} else {
|
} else {
|
||||||
internalServiceErrorJSON(w, err, "Error registering")
|
internalServiceErrorJson(w, err, "Error registering")
|
||||||
}
|
}
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
return
|
return
|
||||||
|
@ -56,7 +43,7 @@ func (s *Server) register(w http.ResponseWriter, req *http.Request) {
|
||||||
response, err = json.Marshal(registerResponse)
|
response, err = json.Marshal(registerResponse)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServiceErrorJSON(w, err, "Error generating register response")
|
internalServiceErrorJson(w, err, "Error generating register response")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
// TODO proper doc comments!
|
// TODO proper doc comments!
|
||||||
|
|
||||||
const PathAuthTokenFull = "/auth/full"
|
const PathAuthTokenFull = "/auth/full"
|
||||||
const PathAuthTokenGetWalletState = "/auth/get-wallet-state"
|
|
||||||
const PathRegister = "/signup"
|
const PathRegister = "/signup"
|
||||||
const PathWalletState = "/wallet-state"
|
const PathWalletState = "/wallet-state"
|
||||||
|
|
||||||
|
@ -39,30 +38,32 @@ type ErrorResponse struct {
|
||||||
Error string `json:"error"`
|
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)
|
errorStr := http.StatusText(code)
|
||||||
if extra != "" {
|
if extra != "" {
|
||||||
errorStr = errorStr + ": " + extra
|
errorStr = errorStr + ": " + extra
|
||||||
}
|
}
|
||||||
authErrorJSON, err := json.Marshal(ErrorResponse{Error: errorStr})
|
authErrorJson, err := json.Marshal(ErrorResponse{Error: errorStr})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// In case something really stupid happens
|
// In case something really stupid happens
|
||||||
http.Error(w, `{"error": "error when JSON-encoding error message"}`, code)
|
http.Error(w, `{"error": "error when JSON-encoding error message"}`, code)
|
||||||
}
|
}
|
||||||
http.Error(w, string(authErrorJSON), code)
|
http.Error(w, string(authErrorJson), code)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't report any details to the user. Log it instead.
|
// 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)
|
errorStr := http.StatusText(http.StatusInternalServerError)
|
||||||
authErrorJSON, err := json.Marshal(ErrorResponse{Error: errorStr})
|
authErrorJson, err := json.Marshal(ErrorResponse{Error: errorStr})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// In case something really stupid happens
|
// In case something really stupid happens
|
||||||
http.Error(w, `{"error": "error when JSON-encoding error message"}`, http.StatusInternalServerError)
|
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)
|
http.Error(w, string(authErrorJson), http.StatusInternalServerError)
|
||||||
log.Printf("%s: %+v\n", errContext, err)
|
log.Printf("%s: %+v\n", errContext, serverErr)
|
||||||
|
|
||||||
return
|
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 {
|
func requestOverhead(w http.ResponseWriter, req *http.Request, method string) bool {
|
||||||
if req.Method != method {
|
if req.Method != method {
|
||||||
errorJSON(w, http.StatusMethodNotAllowed, "")
|
errorJson(w, http.StatusMethodNotAllowed, "")
|
||||||
return false
|
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 {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reqStruct.validate() {
|
if !reqStruct.validate() {
|
||||||
// TODO validate() should return useful error messages instead of a bool.
|
// 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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,41 +125,35 @@ func getGetData(w http.ResponseWriter, req *http.Request) bool {
|
||||||
return requestOverhead(w, req, http.MethodGet)
|
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(
|
func (s *Server) checkAuth(
|
||||||
w http.ResponseWriter,
|
w http.ResponseWriter,
|
||||||
pubKey auth.PublicKey,
|
|
||||||
deviceId string,
|
|
||||||
token auth.AuthTokenString,
|
token auth.AuthTokenString,
|
||||||
scope auth.AuthScope,
|
scope auth.AuthScope,
|
||||||
) bool {
|
) *auth.AuthToken {
|
||||||
authToken, err := s.store.GetToken(pubKey, deviceId)
|
authToken, err := s.store.GetToken(token)
|
||||||
if err == store.ErrNoToken {
|
if err == store.ErrNoToken {
|
||||||
errorJSON(w, http.StatusUnauthorized, "Token Not Found")
|
errorJson(w, http.StatusUnauthorized, "Token Not Found")
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServiceErrorJSON(w, err, "Error getting Token")
|
internalServiceErrorJson(w, err, "Error getting Token")
|
||||||
return false
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
if authToken.Token != token {
|
|
||||||
errorJSON(w, http.StatusUnauthorized, "Token Invalid")
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authToken.ScopeValid(scope) {
|
if !authToken.ScopeValid(scope) {
|
||||||
errorJSON(w, http.StatusForbidden, "Scope")
|
errorJson(w, http.StatusForbidden, "Scope")
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return authToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - both wallet and token requests should be PUT, not POST.
|
// 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."
|
// PUT = "...creates a new resource or replaces a representation of the target resource with the request payload."
|
||||||
|
|
||||||
func (s *Server) Serve() {
|
func (s *Server) Serve() {
|
||||||
http.HandleFunc(PathAuthTokenGetWalletState, s.getAuthTokenForGetWalletState)
|
|
||||||
http.HandleFunc(PathAuthTokenFull, s.getAuthTokenFull)
|
http.HandleFunc(PathAuthTokenFull, s.getAuthTokenFull)
|
||||||
http.HandleFunc(PathWalletState, s.handleWalletState)
|
http.HandleFunc(PathWalletState, s.handleWalletState)
|
||||||
http.HandleFunc(PathRegister, s.register)
|
http.HandleFunc(PathRegister, s.register)
|
||||||
|
|
|
@ -3,6 +3,8 @@ package server
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"orblivion/lbry-id/auth"
|
"orblivion/lbry-id/auth"
|
||||||
|
"orblivion/lbry-id/store"
|
||||||
|
"orblivion/lbry-id/wallet"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,28 +12,19 @@ import (
|
||||||
|
|
||||||
type TestAuth struct {
|
type TestAuth struct {
|
||||||
TestToken auth.AuthTokenString
|
TestToken auth.AuthTokenString
|
||||||
FailSigCheck bool
|
|
||||||
FailGenToken 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 {
|
if a.FailGenToken {
|
||||||
return nil, fmt.Errorf("Test error: fail to generate token")
|
return nil, fmt.Errorf("Test error: fail to generate token")
|
||||||
}
|
}
|
||||||
return &auth.AuthToken{Token: a.TestToken, Scope: Scope}, nil
|
return &auth.AuthToken{Token: a.TestToken, UserId: userId, DeviceId: deviceId, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestStore struct {
|
type TestStore struct {
|
||||||
FailSave bool
|
FailSave bool
|
||||||
|
FailLogin bool
|
||||||
|
|
||||||
SaveTokenCalled bool
|
SaveTokenCalled bool
|
||||||
}
|
}
|
||||||
|
@ -44,29 +37,31 @@ func (s *TestStore) SaveToken(token *auth.AuthToken) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestStore) GetToken(auth.PublicKey, string) (*auth.AuthToken, error) {
|
func (s *TestStore) GetToken(auth.AuthTokenString) (*auth.AuthToken, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestStore) GetPublicKey(string, auth.DownloadKey) (auth.PublicKey, error) {
|
func (s *TestStore) GetUserId(auth.Email, auth.Password) (auth.UserId, error) {
|
||||||
return "", nil
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestStore) SetWalletState(
|
func (s *TestStore) SetWalletState(
|
||||||
pubKey auth.PublicKey,
|
UserId auth.UserId,
|
||||||
walletStateJson string,
|
walletStateJson string,
|
||||||
sequence int,
|
sequence int,
|
||||||
signature auth.Signature,
|
hmac wallet.WalletStateHmac,
|
||||||
downloadKey auth.DownloadKey,
|
) (latestWalletStateJson string, latestHmac wallet.WalletStateHmac, updated bool, err error) {
|
||||||
) (latestWalletStateJson string, latestSignature auth.Signature, updated bool, err error) {
|
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,25 +11,19 @@ import (
|
||||||
|
|
||||||
type WalletStateRequest struct {
|
type WalletStateRequest struct {
|
||||||
Token auth.AuthTokenString `json:"token"`
|
Token auth.AuthTokenString `json:"token"`
|
||||||
BodyJSON string `json:"bodyJSON"`
|
WalletStateJson string `json:"walletStateJson"`
|
||||||
PubKey auth.PublicKey `json:"publicKey"`
|
Hmac wallet.WalletStateHmac `json:"hmac"`
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *WalletStateRequest) validate() bool {
|
func (r *WalletStateRequest) validate() bool {
|
||||||
return (r.Token != auth.AuthTokenString("") &&
|
return (r.Token != auth.AuthTokenString("") &&
|
||||||
r.BodyJSON != "" &&
|
r.WalletStateJson != "" &&
|
||||||
r.PubKey != auth.PublicKey("") &&
|
r.Hmac != wallet.WalletStateHmac(""))
|
||||||
r.Signature != auth.Signature(""))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WalletStateResponse struct {
|
type WalletStateResponse struct {
|
||||||
BodyJSON string `json:"bodyJSON"`
|
WalletStateJson string `json:"walletStateJson"`
|
||||||
Signature auth.Signature `json:"signature"`
|
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
|
Error string `json:"error"` // in case of 409 Conflict responses. TODO - make field not show up if it's empty, to avoid confusion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,31 +33,21 @@ func (s *Server) handleWalletState(w http.ResponseWriter, req *http.Request) {
|
||||||
} else if req.Method == http.MethodPost {
|
} else if req.Method == http.MethodPost {
|
||||||
s.postWalletState(w, req)
|
s.postWalletState(w, req)
|
||||||
} else {
|
} else {
|
||||||
errorJSON(w, http.StatusMethodNotAllowed, "")
|
errorJson(w, http.StatusMethodNotAllowed, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - There's probably a struct-based solution here like with POST/PUT.
|
// TODO - There's probably a struct-based solution here like with POST/PUT.
|
||||||
// We could put that struct up top as well.
|
// 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"]
|
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 {
|
if !hasTokenSlice {
|
||||||
err = fmt.Errorf("Missing token parameter")
|
err = fmt.Errorf("Missing token parameter")
|
||||||
}
|
}
|
||||||
if !hasPubKey {
|
|
||||||
err = fmt.Errorf("Missing publicKey parameter")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
deviceId = deviceIDSlice[0]
|
|
||||||
token = auth.AuthTokenString(tokenSlice[0])
|
token = auth.AuthTokenString(tokenSlice[0])
|
||||||
pubKey = auth.PublicKey(pubKeySlice[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -74,39 +58,41 @@ func (s *Server) getWalletState(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pubKey, deviceId, token, err := getWalletStateParams(req)
|
token, paramsErr := getWalletStateParams(req)
|
||||||
|
|
||||||
if err != nil {
|
if paramsErr != nil {
|
||||||
// In this specific case, err is limited to values that are safe to give to
|
// In this specific case, the error is limited to values that are safe to
|
||||||
// the user
|
// give to the user.
|
||||||
errorJSON(w, http.StatusBadRequest, err.Error())
|
errorJson(w, http.StatusBadRequest, paramsErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.checkAuth(w, pubKey, deviceId, token, auth.ScopeGetWalletState) {
|
authToken := s.checkAuth(w, token, auth.ScopeGetWalletState)
|
||||||
|
|
||||||
|
if authToken == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
latestWalletStateJSON, latestSignature, err := s.store.GetWalletState(pubKey)
|
latestWalletStateJson, latestHmac, err := s.store.GetWalletState(authToken.UserId)
|
||||||
|
|
||||||
var response []byte
|
var response []byte
|
||||||
|
|
||||||
if err == store.ErrNoWalletState {
|
if err == store.ErrNoWalletState {
|
||||||
errorJSON(w, http.StatusNotFound, "No wallet state")
|
errorJson(w, http.StatusNotFound, "No wallet state")
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
internalServiceErrorJSON(w, err, "Error retrieving walletState")
|
internalServiceErrorJson(w, err, "Error retrieving walletState")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
walletStateResponse := WalletStateResponse{
|
walletStateResponse := WalletStateResponse{
|
||||||
BodyJSON: latestWalletStateJSON,
|
WalletStateJson: latestWalletStateJson,
|
||||||
Signature: latestSignature,
|
Hmac: latestHmac,
|
||||||
}
|
}
|
||||||
response, err = json.Marshal(walletStateResponse)
|
response, err = json.Marshal(walletStateResponse)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServiceErrorJSON(w, err, "Error generating latestWalletState response")
|
internalServiceErrorJson(w, err, "Error generating latestWalletState response")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,44 +105,33 @@ func (s *Server) postWalletState(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.auth.IsValidSignature(walletStateRequest.PubKey, walletStateRequest.BodyJSON, walletStateRequest.Signature) {
|
var walletStateMetadata wallet.WalletStateMetadata
|
||||||
errorJSON(w, http.StatusBadRequest, "Bad signature")
|
if err := json.Unmarshal([]byte(walletStateRequest.WalletStateJson), &walletStateMetadata); err != nil {
|
||||||
|
errorJson(w, http.StatusBadRequest, "Malformed walletStateJson")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var walletState wallet.WalletState
|
if s.walletUtil.ValidateWalletStateMetadata(&walletStateMetadata) {
|
||||||
if err := json.Unmarshal([]byte(walletStateRequest.BodyJSON), &walletState); err != nil {
|
|
||||||
errorJSON(w, http.StatusBadRequest, "Malformed walletState JSON")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.walletUtil.ValidateWalletState(&walletState) {
|
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.checkAuth(
|
authToken := s.checkAuth(w, walletStateRequest.Token, auth.ScopeFull)
|
||||||
w,
|
if authToken == nil {
|
||||||
walletStateRequest.PubKey,
|
|
||||||
walletState.DeviceID,
|
|
||||||
walletStateRequest.Token,
|
|
||||||
auth.ScopeFull,
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - We could do an extra check - pull from db, make sure the new
|
// 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
|
// 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.
|
// 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
|
// We do already do some validation checks here, but those doesn't require
|
||||||
// new database calls.
|
// new database calls.
|
||||||
|
|
||||||
latestWalletStateJSON, latestSignature, updated, err := s.store.SetWalletState(
|
latestWalletStateJson, latestHmac, updated, err := s.store.SetWalletState(
|
||||||
walletStateRequest.PubKey,
|
authToken.UserId,
|
||||||
walletStateRequest.BodyJSON,
|
walletStateRequest.WalletStateJson,
|
||||||
walletState.Sequence(),
|
walletStateMetadata.Sequence(),
|
||||||
walletStateRequest.Signature,
|
walletStateRequest.Hmac,
|
||||||
walletStateRequest.DownloadKey,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var response []byte
|
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
|
// there was nothing there. This should only happen if the client sets
|
||||||
// sequence != 1 for the first walletState, which would be a bug.
|
// sequence != 1 for the first walletState, which would be a bug.
|
||||||
// TODO - figure out better error messages and/or document this
|
// 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
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
// Something other than sequence error
|
// Something other than sequence error
|
||||||
internalServiceErrorJSON(w, err, "Error saving walletState")
|
internalServiceErrorJson(w, err, "Error saving walletState")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
walletStateResponse := WalletStateResponse{
|
walletStateResponse := WalletStateResponse{
|
||||||
BodyJSON: latestWalletStateJSON,
|
WalletStateJson: latestWalletStateJson,
|
||||||
Signature: latestSignature,
|
Hmac: latestHmac,
|
||||||
}
|
}
|
||||||
if !updated {
|
if !updated {
|
||||||
// TODO - should we even call this an error?
|
// 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)
|
response, err = json.Marshal(walletStateResponse)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalServiceErrorJSON(w, err, "Error generating walletState response")
|
internalServiceErrorJson(w, err, "Error generating walletStateResponse")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ func TestServerPostWalletTooLate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerPostWalletErrors(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 != 1 for first entry
|
||||||
// Client sends sequence == x + 10 for xth entry or whatever
|
// Client sends sequence == x + 10 for xth entry or whatever
|
||||||
t.Fatalf("Test me: PostWallet fails for various reasons")
|
t.Fatalf("Test me: PostWallet fails for various reasons")
|
||||||
|
@ -33,7 +33,6 @@ func TestServerPostWalletErrors(t *testing.T) {
|
||||||
|
|
||||||
func TestServerValidateWalletStateRequest(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
|
// 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()")
|
t.Fatalf("Test me: Implement and test WalletStateRequest.validate()")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
155
store/store.go
155
store/store.go
|
@ -1,6 +1,6 @@
|
||||||
package store
|
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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
"log"
|
"log"
|
||||||
"orblivion/lbry-id/auth"
|
"orblivion/lbry-id/auth"
|
||||||
|
"orblivion/lbry-id/wallet"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,17 +24,17 @@ var (
|
||||||
ErrDuplicateEmail = fmt.Errorf("Email already exists for this user")
|
ErrDuplicateEmail = fmt.Errorf("Email already exists for this user")
|
||||||
ErrDuplicateAccount = fmt.Errorf("User already has an account")
|
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
|
// For test stubs
|
||||||
type StoreInterface interface {
|
type StoreInterface interface {
|
||||||
SaveToken(*auth.AuthToken) error
|
SaveToken(*auth.AuthToken) error
|
||||||
GetToken(auth.PublicKey, string) (*auth.AuthToken, error)
|
GetToken(auth.AuthTokenString) (*auth.AuthToken, error)
|
||||||
SetWalletState(auth.PublicKey, string, int, auth.Signature, auth.DownloadKey) (string, auth.Signature, bool, error)
|
SetWalletState(auth.UserId, string, int, wallet.WalletStateHmac) (string, wallet.WalletStateHmac, bool, error)
|
||||||
GetWalletState(auth.PublicKey) (string, auth.Signature, error)
|
GetWalletState(auth.UserId) (string, wallet.WalletStateHmac, error)
|
||||||
GetPublicKey(string, auth.DownloadKey) (auth.PublicKey, error)
|
GetUserId(auth.Email, auth.Password) (auth.UserId, error)
|
||||||
InsertEmail(auth.PublicKey, string) (err error)
|
CreateAccount(auth.Email, auth.Password) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
|
@ -56,29 +57,41 @@ func (s *Store) Migrate() error {
|
||||||
// specify "WHERE sequence=5". Only one of these commands will succeed, and
|
// specify "WHERE sequence=5". Only one of these commands will succeed, and
|
||||||
// the other will get back an error.
|
// 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?
|
// TODO does it actually fail with empty "NOT NULL" fields?
|
||||||
query := `
|
query := `
|
||||||
CREATE TABLE IF NOT EXISTS auth_tokens(
|
CREATE TABLE IF NOT EXISTS auth_tokens(
|
||||||
token TEXT NOT NULL,
|
token TEXT NOT NULL UNIQUE,
|
||||||
public_key TEXT NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
device_id TEXT NOT NULL,
|
device_id TEXT NOT NULL,
|
||||||
scope TEXT NOT NULL,
|
scope TEXT NOT NULL,
|
||||||
expiration DATETIME NOT NULL,
|
expiration DATETIME NOT NULL,
|
||||||
PRIMARY KEY (device_id)
|
PRIMARY KEY (user_id, device_id)
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS wallet_states(
|
CREATE TABLE IF NOT EXISTS wallet_states(
|
||||||
public_key TEXT NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
wallet_state_blob TEXT NOT NULL,
|
wallet_state_blob TEXT NOT NULL,
|
||||||
sequence INTEGER NOT NULL,
|
sequence INTEGER NOT NULL,
|
||||||
signature TEXT NOT NULL,
|
hmac TEXT NOT NULL,
|
||||||
download_key TEXT NOT NULL,
|
PRIMARY KEY (user_id)
|
||||||
PRIMARY KEY (public_key)
|
FOREIGN KEY (user_id) REFERENCES accounts(user_id)
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS accounts(
|
CREATE TABLE IF NOT EXISTS accounts(
|
||||||
email TEXT NOT NULL UNIQUE,
|
email TEXT NOT NULL UNIQUE,
|
||||||
public_key TEXT NOT NULL,
|
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
PRIMARY KEY (public_key),
|
password TEXT NOT NULL
|
||||||
FOREIGN KEY (public_key) REFERENCES wallet_states(public_key)
|
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -90,12 +103,16 @@ func (s *Store) Migrate() error {
|
||||||
// Auth Token //
|
// 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()
|
expirationCutoff := time.Now().UTC()
|
||||||
|
|
||||||
rows, err := s.db.Query(
|
rows, err := s.db.Query(
|
||||||
"SELECT * FROM auth_tokens WHERE public_key=? AND device_id=? AND expiration>?",
|
"SELECT * FROM auth_tokens WHERE token=? AND expiration>?", token, expirationCutoff,
|
||||||
pubKey, deviceID, expirationCutoff,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -107,8 +124,8 @@ func (s *Store) GetToken(pubKey auth.PublicKey, deviceID string) (*auth.AuthToke
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&authToken.Token,
|
&authToken.Token,
|
||||||
&authToken.PubKey,
|
&authToken.UserId,
|
||||||
&authToken.DeviceID,
|
&authToken.DeviceId,
|
||||||
&authToken.Scope,
|
&authToken.Scope,
|
||||||
&authToken.Expiration,
|
&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) {
|
func (s *Store) insertToken(authToken *auth.AuthToken, expiration time.Time) (err error) {
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
"INSERT INTO auth_tokens (token, public_key, device_id, scope, expiration) values(?,?,?,?,?)",
|
"INSERT INTO auth_tokens (token, user_id, device_id, scope, expiration) values(?,?,?,?,?)",
|
||||||
authToken.Token, authToken.PubKey, authToken.DeviceID, authToken.Scope, expiration,
|
authToken.Token, authToken.UserId, authToken.DeviceId, authToken.Scope, expiration,
|
||||||
)
|
)
|
||||||
|
|
||||||
var sqliteErr sqlite3.Error
|
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) {
|
func (s *Store) updateToken(authToken *auth.AuthToken, experation time.Time) (err error) {
|
||||||
res, err := s.db.Exec(
|
res, err := s.db.Exec(
|
||||||
"UPDATE auth_tokens SET token=?, expiration=?, scope=? WHERE public_key=? AND device_id=?",
|
"UPDATE auth_tokens SET token=?, expiration=?, scope=? WHERE user_id=? AND device_id=?",
|
||||||
authToken.Token, experation, authToken.Scope, authToken.PubKey, authToken.DeviceID,
|
authToken.Token, experation, authToken.Scope, authToken.UserId, authToken.DeviceId,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -191,10 +208,10 @@ func (s *Store) SaveToken(token *auth.AuthToken) (err error) {
|
||||||
// Wallet State / Download Key //
|
// 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(
|
rows, err := s.db.Query(
|
||||||
"SELECT wallet_state_blob, signature FROM wallet_states WHERE public_key=?",
|
"SELECT wallet_state_blob, hmac FROM wallet_states WHERE user_id=?",
|
||||||
pubKey,
|
userId,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -203,8 +220,8 @@ func (s *Store) GetWalletState(pubKey auth.PublicKey) (walletStateJSON string, s
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.Scan(
|
err = rows.Scan(
|
||||||
&walletStateJSON,
|
&walletStateJson,
|
||||||
&signature,
|
&hmac,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -213,17 +230,16 @@ func (s *Store) GetWalletState(pubKey auth.PublicKey) (walletStateJSON string, s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) insertFirstWalletState(
|
func (s *Store) insertFirstWalletState(
|
||||||
pubKey auth.PublicKey,
|
userId auth.UserId,
|
||||||
walletStateJSON string,
|
walletStateJson string,
|
||||||
signature auth.Signature,
|
hmac wallet.WalletStateHmac,
|
||||||
downloadKey auth.DownloadKey,
|
|
||||||
) (err error) {
|
) (err error) {
|
||||||
// This will only be used to attempt to insert the first wallet state
|
// 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
|
// (sequence=1). The database will enforce that this will not be set
|
||||||
// if this user already has a walletState.
|
// if this user already has a walletState.
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
"INSERT INTO wallet_states (public_key, wallet_state_blob, sequence, signature, download_key) values(?,?,?,?,?)",
|
"INSERT INTO wallet_states (user_id, wallet_state_blob, sequence, hmac) values(?,?,?,?)",
|
||||||
pubKey, walletStateJSON, 1, signature, downloadKey.Obfuscate(),
|
userId, walletStateJson, 1, hmac,
|
||||||
)
|
)
|
||||||
|
|
||||||
var sqliteErr sqlite3.Error
|
var sqliteErr sqlite3.Error
|
||||||
|
@ -239,19 +255,18 @@ func (s *Store) insertFirstWalletState(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) updateWalletStateToSequence(
|
func (s *Store) updateWalletStateToSequence(
|
||||||
pubKey auth.PublicKey,
|
userId auth.UserId,
|
||||||
walletStateJSON string,
|
walletStateJson string,
|
||||||
sequence int,
|
sequence int,
|
||||||
signature auth.Signature,
|
hmac wallet.WalletStateHmac,
|
||||||
downloadKey auth.DownloadKey,
|
|
||||||
) (err error) {
|
) (err error) {
|
||||||
// This will be used for wallet states with sequence > 1.
|
// 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.
|
// 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
|
// This way, if two clients attempt to update at the same time, it will return
|
||||||
// ErrNoWalletState for the second one.
|
// ErrNoWalletState for the second one.
|
||||||
res, err := s.db.Exec(
|
res, err := s.db.Exec(
|
||||||
"UPDATE wallet_states SET wallet_state_blob=?, sequence=?, signature=?, download_key=? WHERE public_key=? AND sequence=?",
|
"UPDATE wallet_states SET wallet_state_blob=?, sequence=?, hmac=? WHERE user_id=? AND sequence=?",
|
||||||
walletStateJSON, sequence, signature, downloadKey.Obfuscate(), pubKey, sequence-1,
|
walletStateJson, sequence, hmac, userId, sequence-1,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -269,25 +284,24 @@ func (s *Store) updateWalletStateToSequence(
|
||||||
|
|
||||||
// Assumption: walletState has been validated (sequence >=1, etc)
|
// Assumption: walletState has been validated (sequence >=1, etc)
|
||||||
// Assumption: Sequence matches walletState.Sequence()
|
// Assumption: Sequence matches walletState.Sequence()
|
||||||
// Sequence is only passed in here to avoid deserializing walletStateJSON again
|
// Sequence is only passed in here to avoid deserializing walletStateJson again
|
||||||
// WalletState *struct* is not passed in because we need the exact signed string
|
// WalletState *struct* is not passed in because the clients need the exact string to match the hmac
|
||||||
func (s *Store) SetWalletState(
|
func (s *Store) SetWalletState(
|
||||||
pubKey auth.PublicKey,
|
userId auth.UserId,
|
||||||
walletStateJSON string,
|
walletStateJson string,
|
||||||
sequence int,
|
sequence int,
|
||||||
signature auth.Signature,
|
hmac wallet.WalletStateHmac,
|
||||||
downloadKey auth.DownloadKey,
|
) (latestWalletStateJson string, latestHmac wallet.WalletStateHmac, updated bool, err error) {
|
||||||
) (latestWalletStateJSON string, latestSignature auth.Signature, updated bool, err error) {
|
|
||||||
if sequence == 1 {
|
if sequence == 1 {
|
||||||
// If sequence == 1, the client assumed that this is our first
|
// If sequence == 1, the client assumed that this is our first
|
||||||
// walletState. Try to insert. If we get a conflict, the client
|
// walletState. Try to insert. If we get a conflict, the client
|
||||||
// assumed incorrectly and we proceed below to return the latest
|
// assumed incorrectly and we proceed below to return the latest
|
||||||
// walletState from the db.
|
// walletState from the db.
|
||||||
err = s.insertFirstWalletState(pubKey, walletStateJSON, signature, downloadKey)
|
err = s.insertFirstWalletState(userId, walletStateJson, hmac)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Successful update
|
// Successful update
|
||||||
latestWalletStateJSON = walletStateJSON
|
latestWalletStateJson = walletStateJson
|
||||||
latestSignature = signature
|
latestHmac = hmac
|
||||||
updated = true
|
updated = true
|
||||||
return
|
return
|
||||||
} else if err != ErrDuplicateWalletState {
|
} else if err != ErrDuplicateWalletState {
|
||||||
|
@ -299,10 +313,10 @@ func (s *Store) SetWalletState(
|
||||||
// with sequence - 1. Explicitly try to update the walletState with
|
// with sequence - 1. Explicitly try to update the walletState with
|
||||||
// sequence - 1. If we updated no rows, the client assumed incorrectly
|
// sequence - 1. If we updated no rows, the client assumed incorrectly
|
||||||
// and we proceed below to return the latest walletState from the db.
|
// 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 {
|
if err == nil {
|
||||||
latestWalletStateJSON = walletStateJSON
|
latestWalletStateJson = walletStateJson
|
||||||
latestSignature = signature
|
latestHmac = hmac
|
||||||
updated = true
|
updated = true
|
||||||
return
|
return
|
||||||
} else if err != ErrNoWalletState {
|
} else if err != ErrNoWalletState {
|
||||||
|
@ -317,16 +331,14 @@ func (s *Store) SetWalletState(
|
||||||
// Note that this means that `err` will not be `nil` at this point, but we
|
// 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
|
// already accounted for it with `updated=false`. Instead, we'll pass on any
|
||||||
// errors from calling `GetWalletState`.
|
// errors from calling `GetWalletState`.
|
||||||
latestWalletStateJSON, latestSignature, err = s.GetWalletState(pubKey)
|
latestWalletStateJson, latestHmac, err = s.GetWalletState(userId)
|
||||||
return
|
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(
|
rows, err := s.db.Query(
|
||||||
`SELECT ws.public_key from wallet_states ws INNER JOIN accounts a
|
`SELECT user_id from accounts WHERE email=? AND password=?`,
|
||||||
ON a.public_key=ws.public_key
|
email, password.Obfuscate(),
|
||||||
WHERE email=? AND download_key=?`,
|
|
||||||
email, downloadKey.Obfuscate(),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -334,27 +346,30 @@ func (s *Store) GetPublicKey(email string, downloadKey auth.DownloadKey) (pubKey
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.Scan(&pubKey)
|
err = rows.Scan(&userId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = ErrNoPubKey
|
err = ErrNoUId
|
||||||
return
|
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(
|
_, err = s.db.Exec(
|
||||||
"INSERT INTO accounts (public_key, email) values(?,?)",
|
"INSERT INTO accounts (email, password) values(?,?)",
|
||||||
pubKey, email,
|
email, password.Obfuscate(),
|
||||||
)
|
)
|
||||||
|
|
||||||
var sqliteErr sqlite3.Error
|
var sqliteErr sqlite3.Error
|
||||||
if errors.As(err, &sqliteErr) {
|
if errors.As(err, &sqliteErr) {
|
||||||
// I initially expected to need to check for ErrConstraintUnique.
|
// I initially expected to need to check for ErrConstraintUnique.
|
||||||
// Maybe for psql it will be?
|
// 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) {
|
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintPrimaryKey) {
|
||||||
err = ErrDuplicateEmail
|
err = ErrDuplicateEmail
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,52 +19,54 @@ func TestStoreInsertToken(t *testing.T) {
|
||||||
s, sqliteTmpFile := StoreTestInit(t)
|
s, sqliteTmpFile := StoreTestInit(t)
|
||||||
defer StoreTestCleanup(sqliteTmpFile)
|
defer StoreTestCleanup(sqliteTmpFile)
|
||||||
|
|
||||||
|
// created for addition to the DB (no expiration attached)
|
||||||
authToken1 := auth.AuthToken{
|
authToken1 := auth.AuthToken{
|
||||||
Token: "seekrit-1",
|
Token: "seekrit-1",
|
||||||
DeviceID: "dID",
|
DeviceId: "dId",
|
||||||
Scope: "*",
|
Scope: "*",
|
||||||
PubKey: "pubKey",
|
UserId: 123,
|
||||||
}
|
}
|
||||||
|
expiration := time.Now().Add(time.Hour * 24 * 14).UTC()
|
||||||
// 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"
|
|
||||||
|
|
||||||
// Get a token, come back empty
|
// 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 {
|
if gotToken != nil || err != ErrNoToken {
|
||||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put in a token
|
// 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)
|
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
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||||
}
|
}
|
||||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken1DB) {
|
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken1Expected) {
|
||||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken1DB, *gotToken)
|
t.Fatalf("token: \n expected %+v\n got: %+v", authToken1Expected, *gotToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to put a different token, fail becaues we already have one
|
// Try to put a different token, fail because we already have one
|
||||||
if err := s.insertToken(&authToken2, *authToken1DB.Expiration); err != ErrDuplicateToken {
|
authToken2 := authToken1
|
||||||
|
authToken2.Token = "seekrit-2"
|
||||||
|
|
||||||
|
if err := s.insertToken(&authToken2, expiration); err != ErrDuplicateToken {
|
||||||
t.Fatalf(`insertToken err: wanted "%+v", got "%+v"`, ErrDuplicateToken, err)
|
t.Fatalf(`insertToken err: wanted "%+v", got "%+v"`, ErrDuplicateToken, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the same *first* token we successfully put in
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||||
}
|
}
|
||||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken1DB) {
|
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken1Expected) {
|
||||||
t.Fatalf("token: expected %+v, got: %+v", authToken1DB, gotToken)
|
t.Fatalf("token: expected %+v, got: %+v", authToken1Expected, gotToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,57 +78,67 @@ func TestStoreUpdateToken(t *testing.T) {
|
||||||
s, sqliteTmpFile := StoreTestInit(t)
|
s, sqliteTmpFile := StoreTestInit(t)
|
||||||
defer StoreTestCleanup(sqliteTmpFile)
|
defer StoreTestCleanup(sqliteTmpFile)
|
||||||
|
|
||||||
authToken1 := auth.AuthToken{
|
// created for addition to the DB (no expiration attached)
|
||||||
Token: "seekrit-1",
|
authTokenUpdate := auth.AuthToken{
|
||||||
DeviceID: "dID",
|
Token: "seekrit-update",
|
||||||
|
DeviceId: "dId",
|
||||||
Scope: "*",
|
Scope: "*",
|
||||||
PubKey: "pubKey",
|
UserId: 123,
|
||||||
}
|
}
|
||||||
authToken2 := authToken1
|
expiration := time.Now().Add(time.Hour * 24 * 14).UTC()
|
||||||
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())
|
|
||||||
|
|
||||||
// Try to get a token, come back empty because we're just starting out
|
// 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 {
|
if gotToken != nil || err != ErrNoToken {
|
||||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
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
|
// 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)
|
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
|
// 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 {
|
if gotToken != nil || err != ErrNoToken {
|
||||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put in a token - just so we have something to test updateToken with
|
// Put in a different token, just so we have something to test that
|
||||||
if err := s.insertToken(&authToken1, *authToken2DB.Expiration); err != nil {
|
// 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)
|
t.Fatalf("Unexpected error in insertToken: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now successfully update token
|
// 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)
|
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
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||||
}
|
}
|
||||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken2DB) {
|
if gotToken == nil || !reflect.DeepEqual(*gotToken, authTokenUpdateExpected) {
|
||||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken2DB, *gotToken)
|
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)
|
// Test first and second Save (one for insert, one for update)
|
||||||
// Get fails initially
|
// Get fails initially
|
||||||
// Put token1-d1 token1-d2
|
// Put token1-d1 token1-d2
|
||||||
|
@ -138,30 +150,24 @@ func TestStoreSaveToken(t *testing.T) {
|
||||||
defer StoreTestCleanup(sqliteTmpFile)
|
defer StoreTestCleanup(sqliteTmpFile)
|
||||||
|
|
||||||
// Version 1 of the token for both devices
|
// Version 1 of the token for both devices
|
||||||
|
// created for addition to the DB (no expiration attached)
|
||||||
authToken_d1_1 := auth.AuthToken{
|
authToken_d1_1 := auth.AuthToken{
|
||||||
Token: "seekrit-d1-1",
|
Token: "seekrit-d1-1",
|
||||||
DeviceID: "dID-1",
|
DeviceId: "dId-1",
|
||||||
Scope: "*",
|
Scope: "*",
|
||||||
PubKey: "pubKey",
|
UserId: 123,
|
||||||
}
|
}
|
||||||
|
|
||||||
authToken_d2_1 := authToken_d1_1
|
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"
|
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
|
// 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 {
|
if gotToken != nil || err != ErrNoToken {
|
||||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
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 {
|
if gotToken != nil || err != ErrNoToken {
|
||||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
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
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||||
}
|
}
|
||||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken_d1_1) {
|
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken_d1_1) {
|
||||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken_d1_1, gotToken)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
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)
|
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
|
// Save Version 2 tokens for both devices
|
||||||
if err = s.SaveToken(&authToken_d1_2); err != nil {
|
if err = s.SaveToken(&authToken_d1_2); err != nil {
|
||||||
t.Fatalf("Unexpected error in SaveToken: %+v", err)
|
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
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||||
}
|
}
|
||||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken_d1_2) {
|
if gotToken == nil || !reflect.DeepEqual(*gotToken, authToken_d1_2) {
|
||||||
t.Fatalf("token: \n expected %+v\n got: %+v", authToken_d1_2, gotToken)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
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)
|
// test GetToken using insertToken and updateToken as helpers (so we can set expiration timestamps)
|
||||||
// normal
|
// normal
|
||||||
// not found for pubkey
|
// token not found
|
||||||
// not found for device (one for another device does exist)
|
// expired not returned
|
||||||
// expired token not returned
|
|
||||||
func TestStoreGetToken(t *testing.T) {
|
func TestStoreGetToken(t *testing.T) {
|
||||||
s, sqliteTmpFile := StoreTestInit(t)
|
s, sqliteTmpFile := StoreTestInit(t)
|
||||||
defer StoreTestCleanup(sqliteTmpFile)
|
defer StoreTestCleanup(sqliteTmpFile)
|
||||||
|
@ -245,39 +257,34 @@ func TestStoreGetToken(t *testing.T) {
|
||||||
// created for addition to the DB (no expiration attached)
|
// created for addition to the DB (no expiration attached)
|
||||||
authToken := auth.AuthToken{
|
authToken := auth.AuthToken{
|
||||||
Token: "seekrit-d1",
|
Token: "seekrit-d1",
|
||||||
DeviceID: "dID",
|
DeviceId: "dId",
|
||||||
Scope: "*",
|
Scope: "*",
|
||||||
PubKey: "pubKey",
|
UserId: 123,
|
||||||
}
|
}
|
||||||
|
expiration := time.Time(time.Now().UTC().Add(time.Hour * 24 * 14))
|
||||||
// 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)))
|
|
||||||
|
|
||||||
// Not found (nothing saved for this pubkey)
|
// 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 {
|
if gotToken != nil || err != ErrNoToken {
|
||||||
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
t.Fatalf("Expected ErrNoToken. token: %+v err: %+v", gotToken, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put in a token
|
// 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)
|
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
|
// Confirm it saved
|
||||||
gotToken, err = s.GetToken(authToken.PubKey, authToken.DeviceID)
|
gotToken, err = s.GetToken(authToken.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
t.Fatalf("Unexpected error in GetToken: %+v", err)
|
||||||
}
|
}
|
||||||
if gotToken == nil || !reflect.DeepEqual(*gotToken, authTokenDB) {
|
if gotToken == nil || !reflect.DeepEqual(*gotToken, authTokenExpected) {
|
||||||
t.Fatalf("token: \n expected %+v\n got: %+v", authTokenDB, gotToken)
|
t.Fatalf("token: \n expected %+v\n got: %+v", authTokenExpected, 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the token to be expired
|
// Update the token to be expired
|
||||||
|
@ -287,7 +294,7 @@ func TestStoreGetToken(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fail to get the expired token
|
// 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 {
|
if gotToken != nil || err != ErrNoToken {
|
||||||
t.Fatalf("Expected ErrNoToken, for expired token. token: %+v err: %+v", gotToken, err)
|
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
|
## Initial setup and account recovery
|
||||||
|
|
||||||
```
|
Set up two clients with the same account (which won't exist on the server yet).
|
||||||
>>> 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)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> c1.new_wallet('email@example.com', '123')
|
>>> from test_client import Client
|
||||||
>>> c1.get_full_auth_token()
|
>>> c1 = Client()
|
||||||
Got auth token: 787cefea147f3a7b38e1b9fda49490371b52a3b7077507364854b72c3538f94e
|
>>> 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()
|
>>> c1.device_id
|
||||||
Successfully updated wallet state
|
'974690df-85a6-481d-9015-6293226db8c9'
|
||||||
Got new walletState: {'deviceId': 'e0349bc4-7e7a-48a2-a562-6c530b28a350', 'lastSynced': {'e0349bc4-7e7a-48a2-a562-6c530b28a350': 1}, 'encryptedWallet': ''}
|
>>> 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.
|
Register the account on the server with one of the clients.
|
||||||
|
|
||||||
Send the email address
|
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> c1.register()
|
>>> c1.register()
|
||||||
Registered
|
Registered
|
||||||
```
|
```
|
||||||
|
|
||||||
Now let's set up a second device
|
Now that the account exists, grab an auth token with both clients.
|
||||||
|
|
||||||
```
|
|
||||||
>>> 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)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
>>> c1.get_full_auth_token()
|
||||||
|
Got auth token: 941e5159a2caff15f0bdc1c0e6da92691d3073543dbfae810cfe57d51c35f0e0
|
||||||
>>> c2.get_full_auth_token()
|
>>> 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()
|
>>> 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()
|
>>> c2.post_wallet_state()
|
||||||
Error 403
|
Successfully updated wallet state on server
|
||||||
b'{"error":"Forbidden: Scope"}\n'
|
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.
|
## 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.)
|
||||||
>>> 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
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
>>> c1.cur_encrypted_wallet()
|
||||||
|
''
|
||||||
>>> c1.change_encrypted_wallet()
|
>>> c1.change_encrypted_wallet()
|
||||||
>>> c1.cur_encrypted_wallet()
|
>>> c1.cur_encrypted_wallet()
|
||||||
':f801'
|
':2fbE'
|
||||||
|
```
|
||||||
|
|
||||||
|
The wallet is synced between the clients.
|
||||||
|
|
||||||
|
```
|
||||||
>>> c1.post_wallet_state()
|
>>> c1.post_wallet_state()
|
||||||
Successfully updated wallet state
|
Successfully updated wallet state on server
|
||||||
Got new walletState: {'deviceId': 'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 2}, 'encryptedWallet': ':f801'}
|
Got new walletState:
|
||||||
>>> c1.cur_encrypted_wallet()
|
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||||
':f801'
|
'encryptedWallet': ':2fbE',
|
||||||
```
|
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||||
|
'974690df-85a6-481d-9015-6293226db8c9': 3}}
|
||||||
The other client gets the update and sees the same thing locally:
|
|
||||||
|
|
||||||
```
|
|
||||||
>>> c2.get_wallet_state()
|
>>> 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()
|
>>> 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()
|
>>> c1.change_encrypted_wallet()
|
||||||
>>> c2.change_encrypted_wallet()
|
>>> c2.change_encrypted_wallet()
|
||||||
>>> c1.cur_encrypted_wallet()
|
>>> c1.cur_encrypted_wallet()
|
||||||
':f801:576b'
|
':2fbE:BD62'
|
||||||
>>> c2.cur_encrypted_wallet()
|
>>> 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()
|
>>> c1.post_wallet_state()
|
||||||
Successfully updated wallet state
|
Successfully updated wallet state on server
|
||||||
Got new walletState: {'deviceId': 'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 3}, 'encryptedWallet': ':f801:576b'}
|
Got new walletState:
|
||||||
|
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||||
>>> c2.post_wallet_state()
|
'encryptedWallet': ':2fbE:BD62',
|
||||||
Wallet state out of date. Getting updated wallet state. Try again.
|
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||||
Got new walletState: {'deviceId': 'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938', 'lastSynced': {'f9acb3bb-ec3b-43f9-9c93-b279b9fdc938': 3}, 'encryptedWallet': ':f801:576b'}
|
'974690df-85a6-481d-9015-6293226db8c9': 4}}
|
||||||
```
|
>>> c2.get_wallet_state()
|
||||||
|
Got latest walletState:
|
||||||
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`).
|
{'deviceId': '974690df-85a6-481d-9015-6293226db8c9',
|
||||||
|
'encryptedWallet': ':2fbE:BD62',
|
||||||
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.)
|
'lastSynced': {'545643c9-ee47-443d-b260-cb9178b8646c': 2,
|
||||||
|
'974690df-85a6-481d-9015-6293226db8c9': 4}}
|
||||||
```
|
|
||||||
>>> c2.cur_encrypted_wallet()
|
>>> 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()
|
>>> c2.post_wallet_state()
|
||||||
Successfully updated wallet state
|
Successfully updated wallet state on server
|
||||||
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'}
|
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()
|
>>> 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()
|
>>> 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
|
#!/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'
|
BASE_URL = 'http://localhost:8090'
|
||||||
AUTH_FULL_URL = BASE_URL + '/auth/full'
|
AUTH_FULL_URL = BASE_URL + '/auth/full'
|
||||||
|
@ -12,8 +13,21 @@ def wallet_state_sequence(wallet_state):
|
||||||
return 0
|
return 0
|
||||||
return wallet_state['lastSynced'][wallet_state['deviceId']]
|
return wallet_state['lastSynced'][wallet_state['deviceId']]
|
||||||
|
|
||||||
def download_key(password):
|
# TODO - do this correctly
|
||||||
return hashlib.sha256(password.encode('utf-8')).hexdigest()
|
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():
|
class Client():
|
||||||
def _validate_new_wallet_state(self, new_wallet_state):
|
def _validate_new_wallet_state(self, new_wallet_state):
|
||||||
|
@ -48,25 +62,34 @@ class Client():
|
||||||
|
|
||||||
self.wallet_state = None
|
self.wallet_state = None
|
||||||
|
|
||||||
def new_wallet(self, email, password):
|
# TODO - save change to disk in between, associated with account and/or
|
||||||
# Obviously not real behavior
|
# wallet
|
||||||
self.public_key = ''.join(random.choice(string.hexdigits) for x in range(32))
|
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
|
# camel-cased to ease json interop
|
||||||
self.wallet_state = {'lastSynced': {}, 'encryptedWallet': ''}
|
self.wallet_state = {'lastSynced': {}, 'encryptedWallet': ''}
|
||||||
|
|
||||||
# TODO - actual encryption with password
|
# TODO - actual encryption with encryption_key
|
||||||
self._encrypted_wallet_local_changes = ''
|
self._encrypted_wallet_local_changes = ''
|
||||||
|
|
||||||
|
def set_account(self, email, root_password):
|
||||||
self.email = email
|
self.email = email
|
||||||
self.password = password
|
self.root_password = root_password
|
||||||
|
|
||||||
def register(self):
|
def register(self):
|
||||||
body = json.dumps({
|
body = json.dumps({
|
||||||
'token': self.auth_token,
|
|
||||||
'publicKey': self.public_key,
|
|
||||||
'deviceId': self.device_id,
|
|
||||||
'email': self.email,
|
'email': self.email,
|
||||||
|
'password': create_login_password(self.root_password),
|
||||||
})
|
})
|
||||||
response = requests.post(REGISTER_URL, body)
|
response = requests.post(REGISTER_URL, body)
|
||||||
if response.status_code != 201:
|
if response.status_code != 201:
|
||||||
|
@ -78,7 +101,7 @@ class Client():
|
||||||
def get_download_auth_token(self, email, password):
|
def get_download_auth_token(self, email, password):
|
||||||
body = json.dumps({
|
body = json.dumps({
|
||||||
'email': email,
|
'email': email,
|
||||||
'downloadKey': download_key(password),
|
'password': create_login_password(password),
|
||||||
'deviceId': self.device_id,
|
'deviceId': self.device_id,
|
||||||
})
|
})
|
||||||
response = requests.post(AUTH_GET_WALLET_STATE_URL, body)
|
response = requests.post(AUTH_GET_WALLET_STATE_URL, body)
|
||||||
|
@ -87,22 +110,18 @@ class Client():
|
||||||
print (response.content)
|
print (response.content)
|
||||||
return
|
return
|
||||||
self.auth_token = json.loads(response.content)['token']
|
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 auth token: ", self.auth_token)
|
||||||
print ("Got public key: ", self.public_key)
|
|
||||||
|
|
||||||
self.email = email
|
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):
|
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({
|
body = json.dumps({
|
||||||
'tokenRequestJSON': json.dumps({'deviceId': self.device_id, 'requestTime': int(time.time())}),
|
'email': self.email,
|
||||||
'publicKey': self.public_key,
|
'password': create_login_password(self.root_password),
|
||||||
'signature': 'Good Signature',
|
'deviceId': self.device_id,
|
||||||
})
|
})
|
||||||
response = requests.post(AUTH_FULL_URL, body)
|
response = requests.post(AUTH_FULL_URL, body)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
|
@ -112,11 +131,14 @@ class Client():
|
||||||
self.auth_token = json.loads(response.content)['token']
|
self.auth_token = json.loads(response.content)['token']
|
||||||
print ("Got auth token: ", self.auth_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):
|
def get_wallet_state(self):
|
||||||
params = {
|
params = {
|
||||||
'token': self.auth_token,
|
'token': self.auth_token,
|
||||||
'publicKey': self.public_key,
|
|
||||||
'deviceId': self.device_id,
|
|
||||||
}
|
}
|
||||||
response = requests.get(WALLET_STATE_URL, params=params)
|
response = requests.get(WALLET_STATE_URL, params=params)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
|
@ -124,17 +146,16 @@ class Client():
|
||||||
print (response.content)
|
print (response.content)
|
||||||
return
|
return
|
||||||
|
|
||||||
if json.loads(response.content)['signature'] != "Good Signature":
|
new_wallet_state_str = json.loads(response.content)['walletStateJson']
|
||||||
print ('Error - bad signature on new wallet')
|
new_wallet_state = json.loads(new_wallet_state_str)
|
||||||
print (response.content)
|
encryption_key = create_encryption_key(self.root_password)
|
||||||
return
|
hmac = json.loads(response.content)['hmac']
|
||||||
if response.status_code != 200:
|
if not check_hmac(new_wallet_state_str, encryption_key, hmac):
|
||||||
print ('Error', response.status_code)
|
print ('Error - bad hmac on new wallet')
|
||||||
print (response.content)
|
print (response.content)
|
||||||
return
|
return
|
||||||
|
|
||||||
# In reality, we'd examine, merge, verify, validate etc this new wallet state.
|
# 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):
|
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 ('Error - new wallet does not validate')
|
||||||
print (response.content)
|
print (response.content)
|
||||||
|
@ -144,58 +165,63 @@ class Client():
|
||||||
# This is if we're getting a wallet_state for the first time. Initialize
|
# This is if we're getting a wallet_state for the first time. Initialize
|
||||||
# the local changes.
|
# the local changes.
|
||||||
self._encrypted_wallet_local_changes = ''
|
self._encrypted_wallet_local_changes = ''
|
||||||
|
|
||||||
self.wallet_state = new_wallet_state
|
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):
|
def post_wallet_state(self):
|
||||||
# Create a *new* wallet state, indicating that it was last updated by this
|
# 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.
|
# 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
|
# Don't set self.wallet_state to this until we know that it's accepted by
|
||||||
# the server.
|
# the server.
|
||||||
if self.wallet_state:
|
if not self.wallet_state:
|
||||||
|
print ("No wallet state to post.")
|
||||||
|
return
|
||||||
|
|
||||||
submitted_wallet_state = {
|
submitted_wallet_state = {
|
||||||
"deviceId": self.device_id,
|
"deviceId": self.device_id,
|
||||||
"lastSynced": dict(self.wallet_state['lastSynced']),
|
"lastSynced": dict(self.wallet_state['lastSynced']),
|
||||||
"encryptedWallet": self.cur_encrypted_wallet(),
|
"encryptedWallet": self.cur_encrypted_wallet(),
|
||||||
}
|
}
|
||||||
submitted_wallet_state['lastSynced'][self.device_id] = wallet_state_sequence(self.wallet_state) + 1
|
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(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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({
|
body = json.dumps({
|
||||||
'token': self.auth_token,
|
'token': self.auth_token,
|
||||||
'bodyJSON': json.dumps(submitted_wallet_state),
|
'walletStateJson': submitted_wallet_state_str,
|
||||||
'publicKey': self.public_key,
|
'hmac': submitted_wallet_state_hmac
|
||||||
'downloadKey': download_key(self.password),
|
|
||||||
'signature': 'Good Signature',
|
|
||||||
})
|
})
|
||||||
response = requests.post(WALLET_STATE_URL, body)
|
response = requests.post(WALLET_STATE_URL, body)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
# Our local changes are no longer local, so we reset them
|
# Our local changes are no longer local, so we reset them
|
||||||
self._encrypted_wallet_local_changes = ''
|
self._encrypted_wallet_local_changes = ''
|
||||||
print ('Successfully updated wallet state')
|
print ('Successfully updated wallet state on server')
|
||||||
elif response.status_code == 409:
|
elif response.status_code == 409:
|
||||||
print ('Wallet state out of date. Getting updated wallet state. Try again.')
|
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:
|
else:
|
||||||
print ('Error', response.status_code)
|
print ('Error', response.status_code)
|
||||||
print (response.content)
|
print (response.content)
|
||||||
return
|
return
|
||||||
|
|
||||||
if json.loads(response.content)['signature'] != "Good Signature":
|
# Now we get a new wallet state back as a response
|
||||||
print ('Error - bad signature on new wallet')
|
# 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)
|
print (response.content)
|
||||||
return
|
return
|
||||||
|
|
||||||
# In reality, we'd examine, merge, verify, validate etc this new wallet state.
|
# 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):
|
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 ('Error - new wallet does not validate')
|
||||||
print (response.content)
|
print (response.content)
|
||||||
|
@ -203,7 +229,8 @@ class Client():
|
||||||
|
|
||||||
self.wallet_state = new_wallet_state
|
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):
|
def change_encrypted_wallet(self):
|
||||||
if not self.wallet_state:
|
if not self.wallet_state:
|
||||||
|
|
|
@ -1,26 +1,32 @@
|
||||||
package wallet
|
package wallet
|
||||||
|
|
||||||
|
import "orblivion/lbry-id/auth"
|
||||||
|
|
||||||
// Currently a small package but given other packages it makes imports easier.
|
// Currently a small package but given other packages it makes imports easier.
|
||||||
// Also this might grow substantially over time
|
// Also this might grow substantially over time
|
||||||
|
|
||||||
// For test stubs
|
// For test stubs
|
||||||
type WalletUtilInterface interface {
|
type WalletUtilInterface interface {
|
||||||
ValidateWalletState(walletState *WalletState) bool
|
ValidateWalletStateMetadata(walletState *WalletStateMetadata) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type WalletUtil struct{}
|
type WalletUtil struct{}
|
||||||
|
|
||||||
type WalletState struct {
|
// This is a subset of the WalletState structure, only the metadata fields. We
|
||||||
DeviceID string `json:"deviceId"`
|
// don't need access to the encrypted wallet.
|
||||||
LastSynced map[string]int `json:"lastSynced"`
|
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
|
// TODO - These "validate" functions could/should be methods. Though I think
|
||||||
// we'd lose mockability for testing, since the method isn't the
|
// we'd lose mockability for testing, since the method isn't the
|
||||||
// WalletUtilInterface.
|
// WalletUtilInterface.
|
||||||
// Mainly the job of the clients but we may as well short-circuit problems
|
// Mainly the job of the clients but we may as well short-circuit problems
|
||||||
// here before saving them.
|
// 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
|
// TODO - nonempty fields, up to date, etc
|
||||||
return true
|
return true
|
||||||
|
@ -28,6 +34,6 @@ func (wu *WalletUtil) ValidateWalletState(walletState *WalletState) bool {
|
||||||
|
|
||||||
// Assumptions: `ws` has been validated
|
// Assumptions: `ws` has been validated
|
||||||
// Avoid having to check for error
|
// Avoid having to check for error
|
||||||
func (ws *WalletState) Sequence() int {
|
func (ws *WalletStateMetadata) Sequence() int {
|
||||||
return ws.LastSynced[ws.DeviceID]
|
return ws.LastSynced[ws.DeviceId]
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,11 @@ import (
|
||||||
// Test stubs for now
|
// Test stubs for now
|
||||||
|
|
||||||
func TestWalletSequence(t *testing.T) {
|
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) {
|
func TestWalletValidateWalletState(t *testing.T) {
|
||||||
// walletState.DeviceID in walletState.lastSynced
|
// walletState.DeviceId in walletState.lastSynced
|
||||||
// Sequence for lastSynced all > 1
|
// Sequence for lastSynced all > 1
|
||||||
t.Fatalf("Test me: Implement and test validateWalletState.")
|
t.Fatalf("Test me: Implement and test validateWalletState.")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue