// Package accesstoken provides storage and validation of Chain Core // credentials. package accesstoken import ( "crypto/rand" "encoding/json" "fmt" "regexp" "strings" "time" "github.com/vapor/crypto/sha3pool" dbm "github.com/vapor/database/leveldb" "github.com/vapor/errors" ) const tokenSize = 32 var ( // ErrBadID is returned when Create is called on an invalid id string. ErrBadID = errors.New("invalid id") // ErrDuplicateID is returned when Create is called on an existing ID. ErrDuplicateID = errors.New("duplicate access token ID") // ErrBadType is returned when Create is called with a bad type. ErrBadType = errors.New("type must be client or network") // ErrNoMatchID is returned when Delete is called on nonexisting ID. ErrNoMatchID = errors.New("nonexisting access token ID") // ErrInvalidToken is returned when Check is called on invalid token ErrInvalidToken = errors.New("invalid token") // validIDRegexp checks that all characters are alphumeric, _ or -. // It also must have a length of at least 1. validIDRegexp = regexp.MustCompile(`^[\w-]+$`) ) // Token describe the access token. type Token struct { ID string `json:"id"` Token string `json:"token,omitempty"` Type string `json:"type,omitempty"` Created time.Time `json:"created_at"` } // CredentialStore store user access credential. type CredentialStore struct { DB dbm.DB } // NewStore creates and returns a new Store object. func NewStore(db dbm.DB) *CredentialStore { return &CredentialStore{ DB: db, } } // Create generates a new access token with the given ID. func (cs *CredentialStore) Create(id, typ string) (*Token, error) { if !validIDRegexp.MatchString(id) { return nil, errors.WithDetailf(ErrBadID, "invalid id %q", id) } key := []byte(id) if cs.DB.Get(key) != nil { return nil, errors.WithDetailf(ErrDuplicateID, "id %q already in use", id) } secret := make([]byte, tokenSize) if _, err := rand.Read(secret); err != nil { return nil, err } hashedSecret := make([]byte, tokenSize) sha3pool.Sum256(hashedSecret, secret) token := &Token{ ID: id, Token: fmt.Sprintf("%s:%x", id, hashedSecret), Type: typ, Created: time.Now(), } value, err := json.Marshal(token) if err != nil { return nil, err } cs.DB.Set(key, value) return token, nil } // Check returns whether or not an id-secret pair is a valid access token. func (cs *CredentialStore) Check(id string, secret string) error { if !validIDRegexp.MatchString(id) { return errors.WithDetailf(ErrBadID, "invalid id %q", id) } value := cs.DB.Get([]byte(id)) if value == nil { return errors.WithDetailf(ErrNoMatchID, "check id %q nonexisting", id) } token := &Token{} if err := json.Unmarshal(value, token); err != nil { return err } if strings.Split(token.Token, ":")[1] == secret { return nil } return ErrInvalidToken } // List lists all access tokens. func (cs *CredentialStore) List() ([]*Token, error) { tokens := make([]*Token, 0) iter := cs.DB.Iterator() defer iter.Release() for iter.Next() { token := &Token{} if err := json.Unmarshal(iter.Value(), token); err != nil { return nil, err } tokens = append(tokens, token) } return tokens, nil } // Delete deletes an access token by id. func (cs *CredentialStore) Delete(id string) error { if !validIDRegexp.MatchString(id) { return errors.WithDetailf(ErrBadID, "invalid id %q", id) } if value := cs.DB.Get([]byte(id)); value == nil { return errors.WithDetailf(ErrNoMatchID, "check id %q", id) } cs.DB.Delete([]byte(id)) return nil }