185 lines
5.1 KiB
Go
185 lines
5.1 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package authorization
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/btcsuite/btcutil/base58"
|
|
"github.com/zeebo/errs"
|
|
"gopkg.in/spacemonkeygo/monkit.v2"
|
|
|
|
"storj.io/storj/pkg/identity"
|
|
"storj.io/storj/pkg/pb"
|
|
"storj.io/storj/pkg/rpc/rpcpeer"
|
|
)
|
|
|
|
const (
|
|
// Bucket is the bucket used with a bolt-backed authorizations DB.
|
|
Bucket = "authorizations"
|
|
// MaxClaimDelaySeconds is the max duration in seconds in the past or
|
|
// future that a claim timestamp is allowed to have and still be valid.
|
|
MaxClaimDelaySeconds = 15
|
|
tokenDataLength = 64 // 2^(64*8) =~ 1.34E+154
|
|
tokenDelimiter = ":"
|
|
tokenVersion = 0
|
|
)
|
|
|
|
var (
|
|
mon = monkit.Package()
|
|
// Error is used when an error occurs involving an authorization.
|
|
Error = errs.Class("authorization error")
|
|
// ErrDB is used when an error occurs involving the authorization database.
|
|
ErrDB = errs.Class("authorization db error")
|
|
// ErrInvalidToken is used when a token is invalid.
|
|
ErrInvalidToken = errs.Class("invalid token error")
|
|
// ErrCount is used when attempting to create an invalid number of authorizations.
|
|
ErrCount = ErrDB.New("cannot add less than one authorizations")
|
|
// ErrEmptyUserID is used when a user ID is required but not provided.
|
|
ErrEmptyUserID = ErrDB.New("userID cannot be empty")
|
|
)
|
|
|
|
// Group is a slice of authorizations for convenient de/serialization.
|
|
// and grouping.
|
|
type Group []*Authorization
|
|
|
|
// Authorization represents a single-use authorization token and its status.
|
|
type Authorization struct {
|
|
Token Token
|
|
Claim *Claim
|
|
}
|
|
|
|
// Token is a userID and a random byte array, when serialized, can be used like
|
|
// a pre-shared key for claiming certificate signatures.
|
|
type Token struct {
|
|
// NB: currently email address for convenience
|
|
UserID string
|
|
Data [tokenDataLength]byte
|
|
}
|
|
|
|
// ClaimOpts hold parameters for claiming an authorization.
|
|
type ClaimOpts struct {
|
|
Req *pb.SigningRequest
|
|
Peer *rpcpeer.Peer
|
|
ChainBytes [][]byte
|
|
MinDifficulty uint16
|
|
}
|
|
|
|
// Claim holds information about the circumstances under which an authorization
|
|
// token was claimed.
|
|
type Claim struct {
|
|
Addr string
|
|
Timestamp int64
|
|
Identity *identity.PeerIdentity
|
|
SignedChainBytes [][]byte
|
|
}
|
|
|
|
func init() {
|
|
gob.Register(&ecdsa.PublicKey{})
|
|
gob.Register(&rsa.PublicKey{})
|
|
gob.Register(elliptic.P256())
|
|
}
|
|
|
|
// NewAuthorization creates a new, unclaimed authorization with a random token value.
|
|
func NewAuthorization(userID string) (*Authorization, error) {
|
|
token := Token{UserID: userID}
|
|
_, err := rand.Read(token.Data[:])
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
return &Authorization{
|
|
Token: token,
|
|
}, nil
|
|
}
|
|
|
|
// ParseToken splits the token string on the delimiter to get a userID and data
|
|
// for a token and base58 decodes the data.
|
|
func ParseToken(tokenString string) (*Token, error) {
|
|
splitAt := strings.LastIndex(tokenString, tokenDelimiter)
|
|
if splitAt == -1 {
|
|
return nil, ErrInvalidToken.New("delimiter missing")
|
|
}
|
|
|
|
userID, b58Data := tokenString[:splitAt], tokenString[splitAt+1:]
|
|
if len(userID) == 0 {
|
|
return nil, ErrInvalidToken.New("user ID missing")
|
|
}
|
|
|
|
data, _, err := base58.CheckDecode(b58Data)
|
|
if err != nil {
|
|
return nil, ErrInvalidToken.Wrap(err)
|
|
}
|
|
|
|
if len(data) != tokenDataLength {
|
|
return nil, ErrInvalidToken.New("data size mismatch")
|
|
}
|
|
t := &Token{
|
|
UserID: userID,
|
|
}
|
|
copy(t.Data[:], data)
|
|
return t, nil
|
|
}
|
|
|
|
// Unmarshal deserializes a set of authorizations.
|
|
func (group *Group) Unmarshal(data []byte) error {
|
|
decoder := gob.NewDecoder(bytes.NewBuffer(data))
|
|
if err := decoder.Decode(group); err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Marshal serializes a set of authorizations.
|
|
func (group Group) Marshal() ([]byte, error) {
|
|
data := new(bytes.Buffer)
|
|
encoder := gob.NewEncoder(data)
|
|
err := encoder.Encode(group)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
return data.Bytes(), nil
|
|
}
|
|
|
|
// GroupByClaimed separates a group of authorizations into a group of claimed
|
|
// and a group of open authorizations.
|
|
func (group Group) GroupByClaimed() (claimed, open Group) {
|
|
for _, auth := range group {
|
|
if auth.Claim != nil {
|
|
// TODO: check if claim is valid? what if not?
|
|
claimed = append(claimed, auth)
|
|
} else {
|
|
open = append(open, auth)
|
|
}
|
|
}
|
|
return claimed, open
|
|
}
|
|
|
|
// String implements the stringer interface and prevents authorization data
|
|
// from completely leaking into logs and errors.
|
|
func (a Authorization) String() string {
|
|
fmtLen := strconv.Itoa(len(a.Token.UserID) + 7)
|
|
return fmt.Sprintf("%."+fmtLen+"s..", a.Token.String())
|
|
}
|
|
|
|
// Equal checks if two tokens have equal user IDs and data
|
|
func (t *Token) Equal(cmpToken *Token) bool {
|
|
return t.UserID == cmpToken.UserID && bytes.Equal(t.Data[:], cmpToken.Data[:])
|
|
}
|
|
|
|
// String implements the stringer interface. Base68 w/ version and checksum bytes
|
|
// are used for easy and reliable human transport.
|
|
func (t *Token) String() string {
|
|
return fmt.Sprintf("%s:%s", t.UserID, base58.CheckEncode(t.Data[:], tokenVersion))
|
|
}
|