10c552fec4
gob package is not stable across Go version, let's switch to protobuf for encoding these. We still need backwards compatibility for the moment. Change-Id: If1da50658ab39a75d1b2b1f988356b56347cac14
241 lines
6.2 KiB
Go
241 lines
6.2 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package authorization
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
|
"github.com/zeebo/errs"
|
|
|
|
"storj.io/common/base58"
|
|
"storj.io/common/identity"
|
|
"storj.io/common/pb"
|
|
"storj.io/common/rpc/rpcpeer"
|
|
"storj.io/storj/certificate/certificatepb"
|
|
)
|
|
|
|
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")
|
|
// ErrInvalidToken is used when a token is invalid.
|
|
ErrInvalidToken = errs.Class("authorization token")
|
|
)
|
|
|
|
// 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 *certificatepb.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 {
|
|
if bytes.HasPrefix(data, []byte{0x14, 0xff, 0xb3, 0x2, 0x1, 0x1, 0x5, 0x47, 0x72}) {
|
|
decoder := gob.NewDecoder(bytes.NewBuffer(data))
|
|
if err := decoder.Decode(group); err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
msg := &certificatepb.AuthorizationGroup{}
|
|
if err := pb.Unmarshal(data, msg); err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
*group = []*Authorization{}
|
|
for _, auth := range msg.Authorizations {
|
|
res := &Authorization{}
|
|
*group = append(*group, res)
|
|
|
|
if auth.Token != nil {
|
|
var tokendata [tokenDataLength]byte
|
|
copy(tokendata[:], auth.Token.Data)
|
|
res.Token = Token{
|
|
UserID: string(auth.Token.UserId),
|
|
Data: tokendata,
|
|
}
|
|
}
|
|
if auth.Claim != nil {
|
|
pi, err := identity.DecodePeerIdentity(context.Background(), auth.Claim.Identity)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
if len(pi.RestChain) == 0 {
|
|
pi.RestChain = nil
|
|
}
|
|
|
|
res.Claim = &Claim{
|
|
Addr: string(auth.Claim.Addr),
|
|
Timestamp: auth.Claim.Timestamp,
|
|
Identity: pi,
|
|
SignedChainBytes: auth.Claim.SignedChainBytes,
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Marshal serializes a set of authorizations.
|
|
func (group Group) Marshal() ([]byte, error) {
|
|
msg := &certificatepb.AuthorizationGroup{}
|
|
for _, auth := range group {
|
|
token := &certificatepb.Token{
|
|
UserId: []byte(auth.Token.UserID),
|
|
Data: append([]byte{}, auth.Token.Data[:]...),
|
|
}
|
|
var claim *certificatepb.Claim
|
|
if auth.Claim != nil {
|
|
claim = &certificatepb.Claim{
|
|
Addr: []byte(auth.Claim.Addr),
|
|
Timestamp: auth.Claim.Timestamp,
|
|
Identity: identity.EncodePeerIdentity(auth.Claim.Identity),
|
|
SignedChainBytes: auth.Claim.SignedChainBytes,
|
|
}
|
|
}
|
|
|
|
msg.Authorizations = append(msg.Authorizations, &certificatepb.Authorization{
|
|
Token: token,
|
|
Claim: claim,
|
|
})
|
|
}
|
|
|
|
encoded, err := pb.Marshal(msg)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
return encoded, 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))
|
|
}
|