1package libkb
2
3import (
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;
30const nistExpirationMargin = 26 * time.Hour // I.e., half of the lifetime
31const nistLifetime = 52 * time.Hour         // A little longer than 2 days.
32const nistSessionIDLength = 16
33const nistShortHashLen = 19
34const nistWebAuthTokenLifetime = 24 * time.Hour // website tokens expire in a day
35
36type nistType int
37type nistMode int
38type sessionVersion int
39
40const (
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
49func (t nistType) sessionVersion() sessionVersion {
50	if t == nistClient {
51		return nistVersion
52	}
53	return nistVersionWebAuthToken
54}
55
56func (t nistType) lifetime() time.Duration {
57	if t == nistClient {
58		return nistLifetime
59	}
60	return nistWebAuthTokenLifetime
61}
62
63func (t nistType) signaturePrefix() kbcrypto.SignaturePrefix {
64	if t == nistClient {
65		return kbcrypto.SignaturePrefixNIST
66	}
67	return kbcrypto.SignaturePrefixNISTWebAuthToken
68}
69
70func (t nistType) encoding() *base64.Encoding {
71	if t == nistClient {
72		return base64.StdEncoding
73	}
74	return base64.RawURLEncoding
75}
76
77type 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
87type NISTToken struct {
88	b        []byte
89	nistType nistType
90}
91
92func (n NISTToken) Bytes() []byte  { return n.b }
93func (n NISTToken) String() string { return n.nistType.encoding().EncodeToString(n.Bytes()) }
94func (n NISTToken) Hash() []byte {
95	tmp := sha256.Sum256(n.Bytes())
96	return tmp[:]
97}
98func (n NISTToken) ShortHash() []byte {
99	return n.Hash()[0:nistShortHashLen]
100}
101
102type 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
113func 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
122func (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
132func (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
177func (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
183func durationToSeconds(d time.Duration) int64 {
184	return int64(d / time.Second)
185}
186
187func (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
195func (n *NIST) IsExpired() bool {
196	isValid, _ := n.IsStillValid()
197	return !isValid
198}
199
200func (n *NIST) DidSucceed() bool {
201	n.RLock()
202	defer n.RUnlock()
203	return n.succeeded
204}
205
206func (n *NIST) DidFail() bool {
207	n.RLock()
208	defer n.RUnlock()
209	return n.failed
210}
211
212func (n *NIST) MarkFailure() {
213	n.Lock()
214	defer n.Unlock()
215	n.failed = true
216}
217
218func (n *NIST) MarkSuccess() {
219	n.Lock()
220	defer n.Unlock()
221	n.succeeded = true
222}
223
224func newNIST(g *GlobalContext) *NIST {
225	return &NIST{Contextified: NewContextified(g)}
226}
227
228type 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
241type nistSig struct {
242	_struct bool `codec:",toarray"` //nolint
243	Version sessionVersion
244	Mode    nistMode
245	Sig     []byte
246	Payload nistPayloadShort
247}
248
249type 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
259type nistHash struct {
260	_struct bool `codec:",toarray"` //nolint
261	Version sessionVersion
262	Mode    nistMode
263	Hash    []byte
264}
265
266func (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
274func (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
282func (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
294func (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
380func (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}
388