github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/libkb/nist.go (about)

     1  package libkb
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/base64"
     6  	"encoding/hex"
     7  	"errors"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/keybase/client/go/kbcrypto"
    12  	"github.com/keybase/client/go/msgpack"
    13  	"github.com/keybase/client/go/protocol/keybase1"
    14  	context "golang.org/x/net/context"
    15  )
    16  
    17  //
    18  // NIST = "Non-Interactive Session Token"
    19  //
    20  // If a client has an unlocked device key, it's able to sign a statement
    21  // of its own creation, and present it to the server as a session key.
    22  // Or, to save bandwidth, can just present the hash of a previously-accepted
    23  // NIST token.
    24  //
    25  // Use NIST tokens rather than the prior generation of session tokens so that
    26  // we're more responsive when we come back from backgrounding, etc.
    27  //
    28  
    29  // If we're within 26 hours of expiration, generate a new NIST;
    30  const nistExpirationMargin = 26 * time.Hour // I.e., half of the lifetime
    31  const nistLifetime = 52 * time.Hour         // A little longer than 2 days.
    32  const nistSessionIDLength = 16
    33  const nistShortHashLen = 19
    34  const nistWebAuthTokenLifetime = 24 * time.Hour // website tokens expire in a day
    35  
    36  type nistType int
    37  type nistMode int
    38  type sessionVersion int
    39  
    40  const (
    41  	nistVersion             sessionVersion = 34
    42  	nistVersionWebAuthToken sessionVersion = 35
    43  	nistModeSignature       nistMode       = 1
    44  	nistModeHash            nistMode       = 2
    45  	nistClient              nistType       = 0
    46  	nistWebAuthToken        nistType       = 1
    47  )
    48  
    49  func (t nistType) sessionVersion() sessionVersion {
    50  	if t == nistClient {
    51  		return nistVersion
    52  	}
    53  	return nistVersionWebAuthToken
    54  }
    55  
    56  func (t nistType) lifetime() time.Duration {
    57  	if t == nistClient {
    58  		return nistLifetime
    59  	}
    60  	return nistWebAuthTokenLifetime
    61  }
    62  
    63  func (t nistType) signaturePrefix() kbcrypto.SignaturePrefix {
    64  	if t == nistClient {
    65  		return kbcrypto.SignaturePrefixNIST
    66  	}
    67  	return kbcrypto.SignaturePrefixNISTWebAuthToken
    68  }
    69  
    70  func (t nistType) encoding() *base64.Encoding {
    71  	if t == nistClient {
    72  		return base64.StdEncoding
    73  	}
    74  	return base64.RawURLEncoding
    75  }
    76  
    77  type NISTFactory struct {
    78  	Contextified
    79  	sync.Mutex
    80  	uid                keybase1.UID
    81  	deviceID           keybase1.DeviceID
    82  	key                GenericKey // cached secret signing key
    83  	nist               *NIST
    84  	lastSuccessfulNIST *NIST
    85  }
    86  
    87  type NISTToken struct {
    88  	b        []byte
    89  	nistType nistType
    90  }
    91  
    92  func (n NISTToken) Bytes() []byte  { return n.b }
    93  func (n NISTToken) String() string { return n.nistType.encoding().EncodeToString(n.Bytes()) }
    94  func (n NISTToken) Hash() []byte {
    95  	tmp := sha256.Sum256(n.Bytes())
    96  	return tmp[:]
    97  }
    98  func (n NISTToken) ShortHash() []byte {
    99  	return n.Hash()[0:nistShortHashLen]
   100  }
   101  
   102  type NIST struct {
   103  	Contextified
   104  	sync.RWMutex
   105  	expiresAt time.Time
   106  	failed    bool
   107  	succeeded bool
   108  	long      *NISTToken
   109  	short     *NISTToken
   110  	nistType  nistType
   111  }
   112  
   113  func NewNISTFactory(g *GlobalContext, uid keybase1.UID, deviceID keybase1.DeviceID, key GenericKey) *NISTFactory {
   114  	return &NISTFactory{
   115  		Contextified: NewContextified(g),
   116  		uid:          uid,
   117  		deviceID:     deviceID,
   118  		key:          key,
   119  	}
   120  }
   121  
   122  func (f *NISTFactory) UID() keybase1.UID {
   123  	if f == nil {
   124  		return keybase1.UID("")
   125  	}
   126  
   127  	f.Lock()
   128  	defer f.Unlock()
   129  	return f.uid
   130  }
   131  
   132  func (f *NISTFactory) NIST(ctx context.Context) (ret *NIST, err error) {
   133  	if f == nil {
   134  		return nil, nil
   135  	}
   136  
   137  	f.Lock()
   138  	defer f.Unlock()
   139  
   140  	makeNew := true
   141  
   142  	if f.nist == nil {
   143  		f.G().Log.CDebugf(ctx, "| NISTFactory#NIST: nil NIST, making new one")
   144  	} else if f.nist.DidFail() {
   145  		f.G().Log.CDebugf(ctx, "| NISTFactory#NIST: NIST previously failed, so we'll make a new one")
   146  	} else {
   147  		if f.nist.DidSucceed() {
   148  			f.lastSuccessfulNIST = f.nist
   149  		}
   150  
   151  		valid, until := f.nist.IsStillValid()
   152  		if valid {
   153  			f.G().Log.CDebugf(ctx, "| NISTFactory#NIST: returning existing NIST (expires conservatively in %s, expiresAt: %s)", until, f.nist.expiresAt)
   154  			makeNew = false
   155  		} else {
   156  			f.G().Log.CDebugf(ctx, "| NISTFactory#NIST: NIST expired (conservatively) %s ago, making a new one (expiresAt: %s)", -until, f.nist.expiresAt)
   157  		}
   158  	}
   159  
   160  	if makeNew {
   161  		ret = newNIST(f.G())
   162  		var lastSuccessfulNISTShortHash []byte
   163  		if f.lastSuccessfulNIST != nil {
   164  			lastSuccessfulNISTShortHash = f.lastSuccessfulNIST.long.ShortHash()
   165  		}
   166  		err = ret.generate(ctx, f.uid, f.deviceID, f.key, nistClient, lastSuccessfulNISTShortHash)
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  		f.nist = ret
   171  		f.G().Log.CDebugf(ctx, "| NISTFactory#NIST: Installing new NIST; expiresAt: %s", f.nist.expiresAt)
   172  	}
   173  
   174  	return f.nist, nil
   175  }
   176  
   177  func (f *NISTFactory) GenerateWebAuthToken(ctx context.Context) (ret *NIST, err error) {
   178  	ret = newNIST(f.G())
   179  	err = ret.generate(ctx, f.uid, f.deviceID, f.key, nistWebAuthToken, nil)
   180  	return ret, err
   181  }
   182  
   183  func durationToSeconds(d time.Duration) int64 {
   184  	return int64(d / time.Second)
   185  }
   186  
   187  func (n *NIST) IsStillValid() (bool, time.Duration) {
   188  	n.RLock()
   189  	defer n.RUnlock()
   190  	now := ForceWallClock(n.G().Clock().Now())
   191  	diff := n.expiresAt.Sub(now) - nistExpirationMargin
   192  	return (diff > 0), diff
   193  }
   194  
   195  func (n *NIST) IsExpired() bool {
   196  	isValid, _ := n.IsStillValid()
   197  	return !isValid
   198  }
   199  
   200  func (n *NIST) DidSucceed() bool {
   201  	n.RLock()
   202  	defer n.RUnlock()
   203  	return n.succeeded
   204  }
   205  
   206  func (n *NIST) DidFail() bool {
   207  	n.RLock()
   208  	defer n.RUnlock()
   209  	return n.failed
   210  }
   211  
   212  func (n *NIST) MarkFailure() {
   213  	n.Lock()
   214  	defer n.Unlock()
   215  	n.failed = true
   216  }
   217  
   218  func (n *NIST) MarkSuccess() {
   219  	n.Lock()
   220  	defer n.Unlock()
   221  	n.succeeded = true
   222  }
   223  
   224  func newNIST(g *GlobalContext) *NIST {
   225  	return &NIST{Contextified: NewContextified(g)}
   226  }
   227  
   228  type nistPayload struct {
   229  	_struct   bool `codec:",toarray"` //nolint
   230  	Version   sessionVersion
   231  	Mode      nistMode
   232  	Hostname  string
   233  	UID       []byte
   234  	DeviceID  []byte
   235  	KID       []byte
   236  	Generated int64
   237  	Lifetime  int64
   238  	SessionID []byte
   239  }
   240  
   241  type nistSig struct {
   242  	_struct bool `codec:",toarray"` //nolint
   243  	Version sessionVersion
   244  	Mode    nistMode
   245  	Sig     []byte
   246  	Payload nistPayloadShort
   247  }
   248  
   249  type nistPayloadShort struct {
   250  	_struct     bool `codec:",toarray"` //nolint
   251  	UID         []byte
   252  	DeviceID    []byte
   253  	Generated   int64
   254  	Lifetime    int64
   255  	SessionID   []byte
   256  	OldNISTHash []byte
   257  }
   258  
   259  type nistHash struct {
   260  	_struct bool `codec:",toarray"` //nolint
   261  	Version sessionVersion
   262  	Mode    nistMode
   263  	Hash    []byte
   264  }
   265  
   266  func (h nistSig) pack(t nistType) (*NISTToken, error) {
   267  	b, err := msgpack.Encode(h)
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  	return &NISTToken{b: b, nistType: t}, nil
   272  }
   273  
   274  func (h nistHash) pack(t nistType) (*NISTToken, error) {
   275  	b, err := msgpack.Encode(h)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	return &NISTToken{b: b, nistType: t}, nil
   280  }
   281  
   282  func (n nistPayload) abbreviate(lastSuccessfulNISTShortHash []byte) nistPayloadShort {
   283  	short := nistPayloadShort{
   284  		UID:         n.UID,
   285  		DeviceID:    n.DeviceID,
   286  		Generated:   n.Generated,
   287  		Lifetime:    n.Lifetime,
   288  		SessionID:   n.SessionID,
   289  		OldNISTHash: lastSuccessfulNISTShortHash,
   290  	}
   291  	return short
   292  }
   293  
   294  func (n *NIST) generate(ctx context.Context, uid keybase1.UID, deviceID keybase1.DeviceID, key GenericKey, typ nistType, lastSuccessfulShortHash []byte) (err error) {
   295  	defer n.G().CTrace(ctx, "NIST#generate", &err)()
   296  
   297  	n.Lock()
   298  	defer n.Unlock()
   299  
   300  	naclKey, ok := (key).(NaclSigningKeyPair)
   301  	if !ok {
   302  		return errors.New("cannot generate a NIST without a NaCl key")
   303  	}
   304  
   305  	var generated time.Time
   306  
   307  	// For some tests we ignore the clock in n.G().Clock() and just use the standard
   308  	// time.Now() clock, because otherwise, the server would start to reject our
   309  	// NISTs.
   310  	if n.G().Env.UseTimeClockForNISTs() {
   311  		generated = time.Now()
   312  	} else {
   313  		generated = n.G().Clock().Now()
   314  	}
   315  
   316  	lifetime := typ.lifetime()
   317  	expires := generated.Add(lifetime)
   318  	version := typ.sessionVersion()
   319  
   320  	payload := nistPayload{
   321  		Version:   version,
   322  		Mode:      nistModeSignature,
   323  		Hostname:  CanonicalHost,
   324  		UID:       uid.ToBytes(),
   325  		Generated: generated.Unix(),
   326  		Lifetime:  durationToSeconds(lifetime),
   327  		KID:       key.GetBinaryKID(),
   328  	}
   329  	n.G().Log.CDebugf(ctx, "NIST: uid=%s; kid=%s; deviceID=%s", uid, key.GetKID().String(), deviceID)
   330  	payload.DeviceID, err = hex.DecodeString(string(deviceID))
   331  	if err != nil {
   332  		return err
   333  	}
   334  	payload.SessionID, err = RandBytes(nistSessionIDLength)
   335  	if err != nil {
   336  		return err
   337  	}
   338  	var sigInfo kbcrypto.NaclSigInfo
   339  	var payloadPacked []byte
   340  	payloadPacked, err = msgpack.Encode(payload)
   341  	if err != nil {
   342  		return err
   343  	}
   344  	sigInfo, err = naclKey.SignV2(payloadPacked, typ.signaturePrefix())
   345  	if err != nil {
   346  		return err
   347  	}
   348  
   349  	var longTmp, shortTmp *NISTToken
   350  
   351  	long := nistSig{
   352  		Version: version,
   353  		Mode:    nistModeSignature,
   354  		Sig:     sigInfo.Sig[:],
   355  		Payload: payload.abbreviate(lastSuccessfulShortHash),
   356  	}
   357  
   358  	longTmp, err = (long).pack(typ)
   359  	if err != nil {
   360  		return err
   361  	}
   362  
   363  	shortTmp, err = (nistHash{
   364  		Version: version,
   365  		Mode:    nistModeHash,
   366  		Hash:    longTmp.ShortHash(),
   367  	}).pack(typ)
   368  	if err != nil {
   369  		return err
   370  	}
   371  
   372  	n.long = longTmp
   373  	n.short = shortTmp
   374  	n.expiresAt = ForceWallClock(expires)
   375  	n.nistType = typ
   376  
   377  	return nil
   378  }
   379  
   380  func (n *NIST) Token() *NISTToken {
   381  	n.RLock()
   382  	defer n.RUnlock()
   383  	if n.succeeded {
   384  		return n.short
   385  	}
   386  	return n.long
   387  }