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	"fmt"
8	"strings"
9	"time"
10
11	"github.com/keybase/client/go/externals"
12	libkb "github.com/keybase/client/go/libkb"
13	keybase1 "github.com/keybase/client/go/protocol/keybase1"
14	"golang.org/x/net/context"
15)
16
17// Prove is an engine used for proving ownership of remote accounts,
18// like Twitter, GitHub, etc.
19type Prove struct {
20	arg               *keybase1.StartProofArg
21	me                *libkb.User
22	serviceType       libkb.ServiceType
23	serviceParameters *keybase1.ProveParameters
24	supersede         bool
25	proof             *libkb.ProofMetadataRes
26	sig               string
27	sigID             keybase1.SigID
28	linkID            libkb.LinkID
29	postRes           *libkb.PostProofRes
30	signingKey        libkb.GenericKey
31	sigInner          []byte
32
33	remoteNameNormalized string
34
35	libkb.Contextified
36}
37
38// NewProve makes a new Prove Engine given an RPC-friendly ProveArg.
39func NewProve(g *libkb.GlobalContext, arg *keybase1.StartProofArg) *Prove {
40	if arg.SigVersion == nil || libkb.SigVersion(*arg.SigVersion) == libkb.KeybaseNullSigVersion {
41		tmp := keybase1.SigVersion(libkb.GetDefaultSigVersion(g))
42		arg.SigVersion = &tmp
43	}
44	return &Prove{
45		arg:          arg,
46		Contextified: libkb.NewContextified(g),
47	}
48}
49
50// Name provides the name of this engine for the engine interface contract
51func (p *Prove) Name() string {
52	return "Prove"
53}
54
55// GetPrereqs returns the engine prereqs.
56func (p *Prove) Prereqs() Prereqs {
57	return Prereqs{Device: true}
58}
59
60// RequiredUIs returns the required UIs.
61func (p *Prove) RequiredUIs() []libkb.UIKind {
62	return []libkb.UIKind{
63		libkb.LogUIKind,
64		libkb.ProveUIKind,
65		libkb.SecretUIKind,
66	}
67}
68
69// SubConsumers returns the other UI consumers for this engine.
70func (p *Prove) SubConsumers() []libkb.UIConsumer {
71	return nil
72}
73
74func (p *Prove) loadMe(m libkb.MetaContext) (err error) {
75	p.me, err = libkb.LoadMe(libkb.NewLoadUserArgWithMetaContext(m).WithForceReload())
76	return err
77}
78
79func (p *Prove) checkExists1(m libkb.MetaContext) (err error) {
80	proofs := p.me.IDTable().GetActiveProofsFor(p.serviceType)
81	if len(proofs) != 0 && !p.arg.Force && p.serviceType.LastWriterWins() {
82		lst := proofs[len(proofs)-1]
83		var redo bool
84		redo, err = m.UIs().ProveUI.PromptOverwrite(m.Ctx(), keybase1.PromptOverwriteArg{
85			Account: lst.ToDisplayString(),
86			Typ:     keybase1.PromptOverwriteType_SOCIAL,
87		})
88		if err != nil {
89			return err
90		}
91		if !redo {
92			return libkb.NotConfirmedError{}
93		}
94		p.supersede = true
95	}
96	return nil
97}
98
99func (p *Prove) promptRemoteName(m libkb.MetaContext) error {
100	// If the name is already supplied, there's no need to prompt.
101	if len(p.arg.Username) > 0 {
102		remoteNameNormalized, err := p.serviceType.NormalizeRemoteName(m, p.arg.Username)
103		if err == nil {
104			p.remoteNameNormalized = remoteNameNormalized
105		}
106		return err
107	}
108
109	// Prompt for the name, retrying if it's invalid.
110	var normalizationError error
111	for {
112		un, err := m.UIs().ProveUI.PromptUsername(m.Ctx(), keybase1.PromptUsernameArg{
113			Prompt:     p.serviceType.GetPrompt(),
114			PrevError:  libkb.ExportErrorAsStatus(m.G(), normalizationError),
115			Parameters: p.serviceParameters,
116		})
117		if err != nil {
118			// Errors here are conditions like EOF. Return them rather than retrying.
119			return err
120		}
121		var remoteNameNormalized string
122		remoteNameNormalized, normalizationError = p.serviceType.NormalizeRemoteName(m, un)
123		if normalizationError == nil {
124			p.remoteNameNormalized = remoteNameNormalized
125			return nil
126		}
127	}
128}
129
130func (p *Prove) checkExists2(m libkb.MetaContext) (err error) {
131	defer m.Trace("Prove#CheckExists2", &err)()
132	if !p.serviceType.LastWriterWins() {
133		var found libkb.RemoteProofChainLink
134		for _, proof := range p.me.IDTable().GetActiveProofsFor(p.serviceType) {
135			_, name := proof.ToKeyValuePair()
136			if libkb.Cicmp(name, p.remoteNameNormalized) {
137				found = proof
138				break
139			}
140		}
141		if found != nil {
142			var redo bool
143			redo, err = m.UIs().ProveUI.PromptOverwrite(m.Ctx(), keybase1.PromptOverwriteArg{
144				Account: found.ToDisplayString(),
145				Typ:     keybase1.PromptOverwriteType_SITE,
146			})
147			if err != nil {
148				return err
149			}
150			if !redo {
151				err = libkb.NotConfirmedError{}
152				return err
153			}
154			p.supersede = true
155		}
156	}
157	return nil
158}
159
160func (p *Prove) doPrechecks(m libkb.MetaContext) (err error) {
161	var w *libkb.Markup
162	w, err = p.serviceType.PreProofCheck(m, p.remoteNameNormalized)
163	if w != nil {
164		if uierr := m.UIs().ProveUI.OutputPrechecks(m.Ctx(), keybase1.OutputPrechecksArg{Text: w.Export()}); uierr != nil {
165			m.Warning("prove ui OutputPrechecks call error: %s", uierr)
166		}
167	}
168	return err
169}
170
171func (p *Prove) doWarnings(m libkb.MetaContext) (err error) {
172	if mu := p.serviceType.PreProofWarning(p.remoteNameNormalized); mu != nil {
173		var ok bool
174		arg := keybase1.PreProofWarningArg{Text: mu.Export()}
175		if ok, err = m.UIs().ProveUI.PreProofWarning(m.Ctx(), arg); err == nil && !ok {
176			err = libkb.NotConfirmedError{}
177		}
178		if err != nil {
179			return err
180		}
181	}
182	return nil
183}
184
185func (p *Prove) generateProof(m libkb.MetaContext) (err error) {
186	ska := libkb.SecretKeyArg{
187		Me:      p.me,
188		KeyType: libkb.DeviceSigningKeyType,
189	}
190
191	p.signingKey, err = m.G().Keyrings.GetSecretKeyWithPrompt(m, m.SecretKeyPromptArg(ska, "tracking signature"))
192	if err != nil {
193		return err
194	}
195
196	sigVersion := libkb.SigVersion(*p.arg.SigVersion)
197
198	if p.proof, err = p.me.ServiceProof(m, p.signingKey, p.serviceType, p.remoteNameNormalized, sigVersion); err != nil {
199		return err
200	}
201
202	if p.sigInner, err = p.proof.J.Marshal(); err != nil {
203		return err
204	}
205
206	p.sig, p.sigID, p.linkID, err = libkb.MakeSig(
207		m,
208		p.signingKey,
209		libkb.LinkTypeWebServiceBinding,
210		p.sigInner,
211		libkb.SigHasRevokes(false),
212		keybase1.SeqType_PUBLIC,
213		libkb.SigIgnoreIfUnsupported(false),
214		p.me,
215		sigVersion,
216	)
217
218	return err
219}
220
221func (p *Prove) postProofToServer(m libkb.MetaContext) (err error) {
222	arg := libkb.PostProofArg{
223		UID:               p.me.GetUID(),
224		Seqno:             p.proof.Seqno,
225		Sig:               p.sig,
226		ProofType:         p.serviceType.GetProofType(),
227		RemoteServiceType: p.serviceType.GetTypeName(),
228		SigID:             p.sigID,
229		LinkID:            p.linkID,
230		Supersede:         p.supersede,
231		RemoteUsername:    p.remoteNameNormalized,
232		RemoteKey:         p.serviceType.GetAPIArgKey(),
233		SigningKey:        p.signingKey,
234	}
235	if libkb.SigVersion(*p.arg.SigVersion) == libkb.KeybaseSignatureV2 {
236		arg.SigInner = p.sigInner
237	}
238	p.postRes, err = libkb.PostProof(m, arg)
239	return err
240}
241
242func (p *Prove) instructAction(m libkb.MetaContext) (err error) {
243	mkp := p.serviceType.PostInstructions(p.remoteNameNormalized)
244	var txt string
245	if txt, err = p.serviceType.FormatProofText(m, p.postRes, p.me.GetNormalizedName().String(), p.remoteNameNormalized, p.sigID); err != nil {
246		return err
247	}
248	err = m.UIs().ProveUI.OutputInstructions(m.Ctx(), keybase1.OutputInstructionsArg{
249		Instructions: mkp.Export(),
250		// If we don't trim newlines here, we'll run into an issue where e.g.
251		// Facebook links get corrupted on iOS. See:
252		// - https://keybase.atlassian.net/browse/DESKTOP-3335
253		// - https://keybase.atlassian.net/browse/CORE-4941
254		// All of our proof verifying code (PVL) should already be flexible
255		// with surrounding whitespace, because users are pasting proofs by
256		// hand anyway.
257		Proof:      strings.TrimSpace(txt),
258		Parameters: p.serviceParameters,
259	})
260	if err != nil {
261		return err
262	}
263
264	return p.checkAutoPost(m, txt)
265}
266
267func (p *Prove) checkAutoPost(m libkb.MetaContext, txt string) error {
268	if !p.arg.Auto {
269		return nil
270	}
271	if libkb.RemoteServiceTypes[p.arg.Service] != keybase1.ProofType_ROOTER {
272		return nil
273	}
274	m.Debug("making automatic post of proof to rooter")
275	apiArg := libkb.APIArg{
276		Endpoint:    "rooter",
277		SessionType: libkb.APISessionTypeREQUIRED,
278		Args: libkb.HTTPArgs{
279			"post":     libkb.S{Val: txt},
280			"username": libkb.S{Val: p.arg.Username},
281		},
282	}
283	if _, err := m.G().API.Post(m, apiArg); err != nil {
284		m.Debug("error posting to rooter: %s", err)
285		return err
286	}
287	return nil
288}
289
290// Keep asking the user whether they posted the proof
291// until it works or they give up.
292func (p *Prove) promptPostedLoop(m libkb.MetaContext) (err error) {
293	found := false
294	for i := 0; ; i++ {
295		var retry bool
296		var status keybase1.ProofStatus
297		var warn *libkb.Markup
298		retry, err = m.UIs().ProveUI.OkToCheck(m.Ctx(), keybase1.OkToCheckArg{
299			Name:    p.serviceType.DisplayName(),
300			Attempt: i,
301		})
302		if !retry || err != nil {
303			break
304		}
305		found, status, _, err = libkb.CheckPosted(m, p.sigID)
306		if found || err != nil {
307			break
308		}
309		warn, err = p.serviceType.RecheckProofPosting(i, status, p.remoteNameNormalized)
310		if warn != nil {
311			uierr := m.UIs().ProveUI.DisplayRecheckWarning(m.Ctx(), keybase1.DisplayRecheckWarningArg{
312				Text: warn.Export(),
313			})
314			if uierr != nil {
315				m.Warning("prove ui DisplayRecheckWarning call error: %s", uierr)
316			}
317		}
318		if err != nil {
319			break
320		}
321	}
322	if !found && err == nil {
323		err = libkb.ProofNotYetAvailableError{}
324	}
325
326	return err
327}
328
329// Poll until the proof succeeds, limited to an hour.
330func (p *Prove) verifyLoop(m libkb.MetaContext) (err error) {
331	timeout := time.Hour
332	m, cancel := m.WithTimeout(timeout)
333	defer cancel()
334	defer func() {
335		if err != nil && m.Ctx().Err() == context.DeadlineExceeded {
336			m.Debug("Prove.verifyLoop rewriting error after timeout: %v", err)
337			err = fmt.Errorf("Timed out after looking for proof for %v", timeout)
338		}
339	}()
340	uierr := m.UIs().ProveUI.Checking(m.Ctx(), keybase1.CheckingArg{
341		Name: p.serviceType.DisplayName(),
342	})
343	if uierr != nil {
344		m.Warning("prove ui Checking call error: %s", uierr)
345	}
346	for i := 0; ; i++ {
347		if shouldContinue, uierr := m.UIs().ProveUI.ContinueChecking(m.Ctx(), 0); !shouldContinue || uierr != nil {
348			if uierr != nil {
349				m.Warning("prove ui ContinueChecking call error: %s", uierr)
350			}
351			return libkb.CanceledError{}
352		}
353		found, status, _, err := libkb.CheckPosted(m, p.sigID)
354		if err != nil {
355			return err
356		}
357		m.Debug("Prove.verifyLoop round:%v found:%v status:%v", i, found, status)
358		if found {
359			return nil
360		}
361		wakeAt := m.G().Clock().Now().Add(2 * time.Second)
362		err = libkb.SleepUntilWithContext(m.Ctx(), m.G().Clock(), wakeAt)
363		if err != nil {
364			return err
365		}
366	}
367}
368
369func (p *Prove) checkProofText(m libkb.MetaContext) error {
370	m.Debug("p.postRes.Text: %q", p.postRes.Text)
371	m.Debug("p.sigID: %q", p.sigID)
372	return p.serviceType.CheckProofText(p.postRes.Text, p.sigID, p.sig)
373}
374
375func (p *Prove) getServiceType(m libkb.MetaContext) (err error) {
376	p.serviceType = m.G().GetProofServices().GetServiceType(m.Ctx(), p.arg.Service)
377	if p.serviceType == nil {
378		return libkb.BadServiceError{Service: p.arg.Service}
379	}
380	if !p.serviceType.CanMakeNewProofs(m) {
381		return libkb.ServiceDoesNotSupportNewProofsError{Service: p.arg.Service}
382	}
383	if serviceType, ok := p.serviceType.(*externals.GenericSocialProofServiceType); ok {
384		tmp := serviceType.ProveParameters(m)
385		p.serviceParameters = &tmp
386	}
387	return nil
388}
389
390// SigID returns the signature id of the proof posted to the
391// server.
392func (p *Prove) SigID() keybase1.SigID {
393	return p.sigID
394}
395
396// Run runs the Prove engine, performing all steps of the proof process.
397func (p *Prove) Run(m libkb.MetaContext) (err error) {
398	defer m.Trace("ProofEngine.Run", &err)()
399
400	stage := func(s string) {
401		m.Debug("| ProofEngine.Run() %s", s)
402	}
403
404	stage("GetServiceType")
405	if err = p.getServiceType(m); err != nil {
406		return err
407	}
408	stage("LoadMe")
409	if err = p.loadMe(m); err != nil {
410		return err
411	}
412	stage("CheckExists1")
413	if err = p.checkExists1(m); err != nil {
414		return err
415	}
416	stage("PromptRemoteName")
417	if err = p.promptRemoteName(m); err != nil {
418		return err
419	}
420	stage("CheckExists2")
421	if err = p.checkExists2(m); err != nil {
422		return err
423	}
424	stage("DoPrechecks")
425	if err = p.doPrechecks(m); err != nil {
426		return err
427	}
428	stage("DoWarnings")
429	if err = p.doWarnings(m); err != nil {
430		return err
431	}
432	m.G().LocalSigchainGuard().Set(m.Ctx(), "Prove")
433	defer m.G().LocalSigchainGuard().Clear(m.Ctx(), "Prove")
434	stage("GenerateProof")
435	if err = p.generateProof(m); err != nil {
436		return err
437	}
438	stage("PostProofToServer")
439	if err = p.postProofToServer(m); err != nil {
440		return err
441	}
442	m.G().LocalSigchainGuard().Clear(m.Ctx(), "Prove")
443	stage("CheckProofText")
444	if err = p.checkProofText(m); err != nil {
445		return err
446	}
447	stage("InstructAction")
448	if err = p.instructAction(m); err != nil {
449		return err
450	}
451
452	if !p.arg.PromptPosted {
453		m.Debug("PromptPosted not set, prove run finished")
454		return nil
455	}
456
457	stage("CheckStart")
458	if p.serviceParameters == nil {
459		stage("PromptPostedLoop")
460		if err = p.promptPostedLoop(m); err != nil {
461			return err
462		}
463	} else {
464		stage("VerifyLoop")
465		if err = p.verifyLoop(m); err != nil {
466			return err
467		}
468	}
469	m.UIs().LogUI.Notice("Success!")
470	return nil
471}
472