storj/certificate/authorization/authorizations.go

180 lines
4.7 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"
"time"
"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"
// MaxClockSkew is the max duration in the past or future that a claim
// timestamp is allowed to have and still be valid.
MaxClockSkew = 5 * time.Minute
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")
// ErrInvalidToken is used when a token is invalid.
ErrInvalidToken = errs.Class("authorization token error")
)
// 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))
}