1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package engine
5
6import (
7	"errors"
8	"fmt"
9
10	"github.com/keybase/client/go/libkb"
11	keybase1 "github.com/keybase/client/go/protocol/keybase1"
12)
13
14// TrackToken is an engine.
15type TrackToken struct {
16	libkb.Contextified
17	arg                 *TrackTokenArg
18	them                *libkb.User
19	trackStatementBytes []byte
20	trackStatement      *libkb.ProofMetadataRes
21}
22
23type TrackTokenArg struct {
24	Token   keybase1.TrackToken
25	Me      *libkb.User
26	Options keybase1.TrackOptions
27	Outcome *libkb.IdentifyOutcome
28}
29
30// NewTrackToken creates a TrackToken engine.
31func NewTrackToken(g *libkb.GlobalContext, arg *TrackTokenArg) *TrackToken {
32	if arg.Options.SigVersion == nil || libkb.SigVersion(*arg.Options.SigVersion) == libkb.KeybaseNullSigVersion {
33		tmp := keybase1.SigVersion(libkb.GetDefaultSigVersion(g))
34		arg.Options.SigVersion = &tmp
35	}
36
37	return &TrackToken{
38		arg:          arg,
39		Contextified: libkb.NewContextified(g),
40	}
41}
42
43// Name is the unique engine name.
44func (e *TrackToken) Name() string {
45	return "TrackToken"
46}
47
48// GetPrereqs returns the engine prereqs.
49func (e *TrackToken) Prereqs() Prereqs {
50	return Prereqs{
51		Device: true,
52	}
53}
54
55// RequiredUIs returns the required UIs.
56func (e *TrackToken) RequiredUIs() []libkb.UIKind {
57	return []libkb.UIKind{}
58}
59
60// SubConsumers returns the other UI consumers for this engine.
61func (e *TrackToken) SubConsumers() []libkb.UIConsumer {
62	return nil
63}
64
65// Run starts the engine.
66func (e *TrackToken) Run(m libkb.MetaContext) (err error) {
67	defer m.Trace("TrackToken#Run", &err)()
68
69	if len(e.arg.Token) == 0 && e.arg.Outcome == nil {
70		err = fmt.Errorf("missing TrackToken argument")
71		return err
72	}
73	if err = e.loadMe(m); err != nil {
74		m.Info("loadme err: %s", err)
75		return err
76	}
77
78	// We can either pass this in directly, or look it up via TrackToken
79	outcome := e.arg.Outcome
80	if outcome == nil {
81		outcome, err = m.G().TrackCache().Get(e.arg.Token)
82		if err != nil {
83			return err
84		}
85	}
86
87	if outcome.TrackStatus() == keybase1.TrackStatus_UPDATE_OK && !e.arg.Options.ForceRetrack {
88		m.Debug("tracking statement up-to-date.")
89		return nil
90	}
91
92	if err = e.loadThem(m, outcome.Username); err != nil {
93		return err
94	}
95
96	if e.arg.Me.Equal(e.them) {
97		err = libkb.SelfTrackError{}
98		return err
99	}
100
101	if err = e.isTrackTokenStale(m, outcome); err != nil {
102		m.Debug("Track statement is stale")
103		return err
104	}
105
106	// need public signing key for track statement
107	signingKeyPub, err := e.arg.Me.SigningKeyPub()
108	if err != nil {
109		return err
110	}
111
112	e.trackStatement, err = e.arg.Me.TrackingProofFor(m, signingKeyPub, libkb.SigVersion(*e.arg.Options.SigVersion), e.them, outcome)
113	if err != nil {
114		m.Debug("tracking proof err: %s", err)
115		return err
116	}
117	if e.trackStatementBytes, err = e.trackStatement.J.Marshal(); err != nil {
118		return err
119	}
120
121	m.Debug("| Tracking statement: %s", string(e.trackStatementBytes))
122
123	if e.arg.Options.LocalOnly || e.arg.Options.ExpiringLocal {
124		m.Debug("| Local")
125		err = e.storeLocalTrack(m)
126	} else {
127		err = e.storeRemoteTrack(m, signingKeyPub.GetKID())
128		if err == nil {
129			// if the remote track succeeded, remove local tracks
130			// (this also removes any snoozes)
131			err := e.removeLocalTracks(m)
132			if err != nil {
133				return err
134			}
135		}
136	}
137	if err != nil {
138		return err
139	}
140	themUPAK, err := e.them.ExportToUPKV2AllIncarnations()
141	if err != nil {
142		return err
143	}
144	err = m.G().Pegboard.TrackUPAK(m, themUPAK.Current)
145	if err != nil {
146		return err
147	}
148
149	// Remove this after desktop notification change complete:
150	m.G().UserChanged(m.Ctx(), e.them.GetUID())
151
152	// Remove these after desktop notification change complete, but
153	// add in: m.G().BustLocalUserCache(e.arg.Me.GetUID())
154	m.G().UserChanged(m.Ctx(), e.arg.Me.GetUID())
155
156	// Keep these:
157	m.G().NotifyRouter.HandleTrackingChanged(e.arg.Me.GetUID(), e.arg.Me.GetNormalizedName(), false)
158	m.G().NotifyRouter.HandleTrackingChanged(e.them.GetUID(), e.them.GetNormalizedName(), true)
159
160	// Dismiss any associated gregor item.
161	if outcome.ResponsibleGregorItem != nil {
162		err = m.G().GregorState.DismissItem(m.Ctx(), nil,
163			outcome.ResponsibleGregorItem.Metadata().MsgID())
164	}
165
166	return err
167}
168
169func (e *TrackToken) isTrackTokenStale(m libkb.MetaContext, o *libkb.IdentifyOutcome) (err error) {
170	if idt := e.arg.Me.IDTable(); idt == nil {
171		return nil
172	} else if tm := idt.GetTrackMap(); tm == nil {
173		return nil
174	} else if v := tm[o.Username]; len(v) == 0 {
175		return nil
176	} else if lastTrack := v[len(v)-1]; lastTrack != nil && !lastTrack.IsRevoked() && o.TrackUsed == nil {
177		// If we had a valid track that we didn't use in the identification, then
178		// someone must have slipped in before us. Distinguish this case from the
179		// other case below for the purposes of testing, to make sure we hit
180		// both error cases in our tests.
181		return libkb.TrackStaleError{FirstTrack: true}
182	} else if o.TrackUsed == nil || lastTrack == nil {
183		return nil
184	} else if o.TrackUsed.GetTrackerSeqno() < lastTrack.GetSeqno() {
185		// Similarly, if there was a last track for this user that wasn't the
186		// one we were expecting, someone also must have intervened.
187		m.Debug("Stale track! We were at seqno %d, but %d is already in chain", o.TrackUsed.GetTrackerSeqno(), lastTrack.GetSeqno())
188		return libkb.TrackStaleError{FirstTrack: false}
189	}
190	return nil
191}
192
193func (e *TrackToken) loadMe(m libkb.MetaContext) error {
194	if e.arg.Me != nil {
195		return nil
196	}
197
198	me, err := libkb.LoadMe(libkb.NewLoadUserArgWithMetaContext(m))
199	if err != nil {
200		return err
201	}
202	e.arg.Me = me
203	return nil
204}
205
206func (e *TrackToken) loadThem(m libkb.MetaContext, username libkb.NormalizedUsername) error {
207
208	arg := libkb.NewLoadUserArgWithMetaContext(m).WithName(username.String()).WithPublicKeyOptional()
209	them, err := libkb.LoadUser(arg)
210	if err != nil {
211		return err
212	}
213	e.them = them
214	return nil
215}
216
217func (e *TrackToken) storeLocalTrack(m libkb.MetaContext) error {
218	return libkb.StoreLocalTrack(m, e.arg.Me.GetUID(), e.them.GetUID(), e.arg.Options.ExpiringLocal, e.trackStatement.J)
219}
220
221func (e *TrackToken) storeRemoteTrack(m libkb.MetaContext, pubKID keybase1.KID) (err error) {
222	defer m.Trace("TrackToken#StoreRemoteTrack", &err)()
223
224	// need unlocked signing key
225	me := e.arg.Me
226	ska := libkb.SecretKeyArg{
227		Me:      me,
228		KeyType: libkb.DeviceSigningKeyType,
229	}
230	arg := m.SecretKeyPromptArg(ska, "tracking signature")
231	signingKey, err := m.G().Keyrings.GetSecretKeyWithPrompt(m, arg)
232	if err != nil {
233		return err
234	}
235	if signingKey == nil {
236		return libkb.NoSecretKeyError{}
237	}
238	// double-check that the KID of the unlocked key matches
239	if signingKey.GetKID().NotEqual(pubKID) {
240		return errors.New("unexpeceted KID mismatch between locked and unlocked signing key")
241	}
242
243	sigVersion := libkb.SigVersion(*e.arg.Options.SigVersion)
244	sig, sigID, linkID, err := libkb.MakeSig(
245		m,
246		signingKey,
247		libkb.LinkTypeTrack,
248		e.trackStatementBytes,
249		libkb.SigHasRevokes(false),
250		keybase1.SeqType_PUBLIC,
251		libkb.SigIgnoreIfUnsupported(false),
252		me,
253		sigVersion,
254	)
255
256	if err != nil {
257		return err
258	}
259
260	httpsArgs := libkb.HTTPArgs{
261		"sig_id_base":  libkb.S{Val: sigID.StripSuffix().String()},
262		"sig_id_short": libkb.S{Val: sigID.ToShortID()},
263		"sig":          libkb.S{Val: sig},
264		"uid":          libkb.UIDArg(e.them.GetUID()),
265		"type":         libkb.S{Val: "track"},
266		"signing_kid":  signingKey.GetKID(),
267	}
268
269	if sigVersion == libkb.KeybaseSignatureV2 {
270		httpsArgs["sig_inner"] = libkb.S{Val: string(e.trackStatementBytes)}
271	}
272	_, err = m.G().API.Post(m, libkb.APIArg{
273		Endpoint:    "follow",
274		SessionType: libkb.APISessionTypeREQUIRED,
275		Args:        httpsArgs,
276	})
277	if err != nil {
278		m.Warning("api error: %s", err)
279		return err
280	}
281	if err = libkb.MerkleCheckPostedUserSig(m, me.GetUID(), e.trackStatement.Seqno, linkID); err != nil {
282		return err
283	}
284
285	me.SigChainBump(linkID, sigID, false)
286	m.G().IdentifyDispatch.NotifyTrackingSuccess(m, e.them.GetUID())
287
288	return err
289}
290
291func (e *TrackToken) removeLocalTracks(m libkb.MetaContext) (err error) {
292	defer m.Trace("removeLocalTracks", &err)()
293	err = libkb.RemoveLocalTracks(m, e.arg.Me.GetUID(), e.them.GetUID())
294	return err
295}
296