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