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 }