github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/auth/token.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  // Code used to support authentication tokens for arbitrary purposes.
     5  package auth
     6  
     7  import (
     8  	"bytes"
     9  	"crypto/rand"
    10  	"encoding/hex"
    11  	"encoding/json"
    12  	"math"
    13  	"time"
    14  
    15  	"github.com/keybase/client/go/kbcrypto"
    16  	libkb "github.com/keybase/client/go/libkb"
    17  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    18  )
    19  
    20  const (
    21  	TokenType             = "auth"
    22  	CurrentTokenVersion   = 2
    23  	ChallengeLengthBytes  = 32
    24  	ChallengeLengthString = ChallengeLengthBytes * 2 // we use hex encoding
    25  )
    26  
    27  type TokenAuth struct {
    28  	Server    string `json:"server"`
    29  	Challenge string `json:"session"`
    30  }
    31  
    32  type TokenKey struct {
    33  	UID      keybase1.UID             `json:"uid"`
    34  	Username libkb.NormalizedUsername `json:"username"`
    35  	KID      keybase1.KID             `json:"kid"`
    36  }
    37  
    38  type TokenBody struct {
    39  	Auth    TokenAuth `json:"auth"`
    40  	Key     TokenKey  `json:"key"`
    41  	Type    string    `json:"type"`
    42  	Version int       `json:"version"`
    43  }
    44  
    45  type TokenClient struct {
    46  	Name    string `json:"name"`
    47  	Version string `json:"version"`
    48  }
    49  
    50  type Token struct {
    51  	Body         TokenBody   `json:"body"`
    52  	Client       TokenClient `json:"client"`
    53  	CreationTime int64       `json:"ctime"`
    54  	ExpireIn     int         `json:"expire_in"`
    55  	Tag          string      `json:"tag"`
    56  }
    57  
    58  func NewToken(uid keybase1.UID, username libkb.NormalizedUsername, kid keybase1.KID,
    59  	server, challenge string, now int64, expireIn int,
    60  	clientName, clientVersion string) *Token {
    61  	return &Token{
    62  		Body: TokenBody{
    63  			Auth: TokenAuth{
    64  				Server:    server,
    65  				Challenge: challenge,
    66  			},
    67  			Key: TokenKey{
    68  				UID:      uid,
    69  				Username: username,
    70  				KID:      kid,
    71  			},
    72  			Type:    TokenType,
    73  			Version: CurrentTokenVersion,
    74  		},
    75  		Client: TokenClient{
    76  			Name:    clientName,
    77  			Version: clientVersion,
    78  		},
    79  		CreationTime: now,
    80  		ExpireIn:     expireIn,
    81  		Tag:          "signature",
    82  	}
    83  }
    84  
    85  func (t Token) Bytes() []byte {
    86  	bytes, err := json.Marshal(&t)
    87  	if err != nil {
    88  		return []byte{}
    89  	}
    90  	return bytes
    91  }
    92  
    93  func (t Token) String() string {
    94  	return string(t.Bytes())
    95  }
    96  
    97  func VerifyToken(signature, server, challenge string, maxExpireIn int) (*Token, error) {
    98  	var t *Token
    99  	key, token, _, err := kbcrypto.NaclVerifyAndExtract(signature)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	if t, err = parseToken(token); err != nil {
   104  		return nil, err
   105  	}
   106  	if key.GetKID() != t.KID() {
   107  		return nil, InvalidTokenKeyError{
   108  			expected: key.GetKID().String(),
   109  			received: t.KID().String(),
   110  		}
   111  	}
   112  	if TokenType != t.Type() {
   113  		return nil, InvalidTokenTypeError{
   114  			expected: TokenType,
   115  			received: t.Type(),
   116  		}
   117  	}
   118  	if server != t.Server() {
   119  		return nil, InvalidTokenServerError{
   120  			expected: server,
   121  			received: t.Server(),
   122  		}
   123  	}
   124  	if challenge != t.Challenge() {
   125  		return nil, InvalidTokenChallengeError{
   126  			expected: challenge,
   127  			received: t.Challenge(),
   128  		}
   129  	}
   130  	remaining := t.TimeRemaining()
   131  	if remaining > maxExpireIn {
   132  		return nil, MaxTokenExpiresError{
   133  			creationTime: t.CreationTime,
   134  			expireIn:     t.ExpireIn,
   135  			now:          time.Now().Unix(),
   136  			maxExpireIn:  maxExpireIn,
   137  			remaining:    remaining,
   138  		}
   139  	}
   140  	if remaining <= 0 {
   141  		return nil, TokenExpiredError{
   142  			creationTime: t.CreationTime,
   143  			expireIn:     t.ExpireIn,
   144  			now:          time.Now().Unix(),
   145  		}
   146  	}
   147  	return t, nil
   148  }
   149  
   150  func (t Token) TimeRemaining() int {
   151  	ctime := time.Unix(t.CreationTime, 0)
   152  	expires := ctime.Add(time.Duration(t.ExpireIn) * time.Second)
   153  	return int(math.Ceil(time.Until(expires).Seconds()))
   154  }
   155  
   156  func (t Token) Server() string {
   157  	return t.Body.Auth.Server
   158  }
   159  
   160  func (t Token) Challenge() string {
   161  	return t.Body.Auth.Challenge
   162  }
   163  
   164  func (t Token) UID() keybase1.UID {
   165  	return t.Body.Key.UID
   166  }
   167  
   168  func (t Token) KID() keybase1.KID {
   169  	return t.Body.Key.KID
   170  }
   171  
   172  func (t Token) Username() libkb.NormalizedUsername {
   173  	return t.Body.Key.Username
   174  }
   175  
   176  func (t Token) Type() string {
   177  	return t.Body.Type
   178  }
   179  
   180  func (t Token) Version() int {
   181  	return t.Body.Version
   182  }
   183  
   184  func (t Token) ClientName() string {
   185  	return t.Client.Name
   186  }
   187  
   188  func (t Token) ClientVersion() string {
   189  	return t.Client.Version
   190  }
   191  
   192  func parseToken(token []byte) (*Token, error) {
   193  	decoder := json.NewDecoder(bytes.NewReader(token))
   194  	decoder.UseNumber()
   195  	var t Token
   196  	if err := decoder.Decode(&t); err != nil {
   197  		return nil, err
   198  	}
   199  	return &t, nil
   200  }
   201  
   202  // GenerateChallenge returns a cryptographically secure random challenge string.
   203  func GenerateChallenge() (string, error) {
   204  	buf := make([]byte, ChallengeLengthBytes)
   205  	if _, err := rand.Read(buf); err != nil {
   206  		return "", err
   207  	}
   208  	return hex.EncodeToString(buf), nil
   209  }
   210  
   211  // IsValidChallenge returns true if the passed challenge is validly formed.
   212  func IsValidChallenge(challenge string) bool {
   213  	if len(challenge) != ChallengeLengthString {
   214  		return false
   215  	}
   216  	if _, err := hex.DecodeString(challenge); err != nil {
   217  		return false
   218  	}
   219  	return true
   220  }