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	"time"
9
10	"github.com/keybase/client/go/libkb"
11	keybase1 "github.com/keybase/client/go/protocol/keybase1"
12)
13
14type PGPPullEngineArg struct {
15	UserAsserts []string
16}
17
18type PGPPullEngine struct {
19	listTrackingEngine *ListTrackingEngine
20	userAsserts        []string
21	gpgClient          *libkb.GpgCLI
22	libkb.Contextified
23}
24
25func NewPGPPullEngine(g *libkb.GlobalContext, arg *PGPPullEngineArg) *PGPPullEngine {
26	return &PGPPullEngine{
27		listTrackingEngine: NewListTrackingEngine(g, &ListTrackingEngineArg{}),
28		userAsserts:        arg.UserAsserts,
29		Contextified:       libkb.NewContextified(g),
30	}
31}
32
33func (e *PGPPullEngine) Name() string {
34	return "PGPPull"
35}
36
37func (e *PGPPullEngine) Prereqs() Prereqs {
38	return Prereqs{}
39}
40
41func (e *PGPPullEngine) RequiredUIs() []libkb.UIKind {
42	return []libkb.UIKind{
43		libkb.LogUIKind,
44	}
45}
46
47func (e *PGPPullEngine) SubConsumers() []libkb.UIConsumer {
48	return []libkb.UIConsumer{e.listTrackingEngine}
49}
50
51func proofSetFromUserSummary(summary keybase1.UserSummary) *libkb.ProofSet {
52	proofs := []libkb.Proof{
53		{Key: "keybase", Value: summary.Username},
54		{Key: "uid", Value: summary.Uid.String()},
55	}
56	return libkb.NewProofSet(proofs)
57}
58
59func (e *PGPPullEngine) getTrackedUserSummaries(m libkb.MetaContext) ([]keybase1.UserSummary, []string, error) {
60	err := RunEngine2(m, e.listTrackingEngine)
61	if err != nil {
62		return nil, nil, err
63	}
64	allTrackedSummaries := e.listTrackingEngine.TableResult().Users
65
66	// Without any userAsserts specified, just all summaries and no leftovers.
67	if e.userAsserts == nil || len(e.userAsserts) == 0 {
68		return allTrackedSummaries, nil, nil
69	}
70
71	// With userAsserts specified, return only those summaries. If an assert
72	// doesn't match any tracked users, that's an error. If an assert matches
73	// more than one tracked user, that is also an error. If multiple
74	// assertions match the same user, that's fine.
75
76	// First parse all the assertion expressions.
77	parsedAsserts := make(map[string]libkb.AssertionExpression)
78	for _, assertString := range e.userAsserts {
79		assertExpr, err := libkb.AssertionParseAndOnly(e.G().MakeAssertionContext(m), assertString)
80		if err != nil {
81			return nil, nil, err
82		}
83		parsedAsserts[assertString] = assertExpr
84	}
85
86	// Then loop over all the tracked users, keeping track of which expressions
87	// have matched before.
88	matchedSummaries := make(map[string]keybase1.UserSummary)
89	assertionsUsed := make(map[string]bool)
90	for _, summary := range allTrackedSummaries {
91		proofSet := proofSetFromUserSummary(summary)
92		for assertStr, parsedAssert := range parsedAsserts {
93			if parsedAssert.MatchSet(*proofSet) {
94				if assertionsUsed[assertStr] {
95					return nil, nil, fmt.Errorf("Assertion \"%s\" matched more than one tracked user.", assertStr)
96				}
97				assertionsUsed[assertStr] = true
98				matchedSummaries[summary.Username] = summary
99			}
100		}
101	}
102
103	var leftovers []string
104	// Make sure every assertion found a match.
105	for _, assertString := range e.userAsserts {
106		if !assertionsUsed[assertString] {
107			m.Info("Assertion \"%s\" did not match any tracked users.", assertString)
108			leftovers = append(leftovers, assertString)
109		}
110	}
111
112	matchedList := []keybase1.UserSummary{}
113	for _, summary := range matchedSummaries {
114		matchedList = append(matchedList, summary)
115	}
116	return matchedList, leftovers, nil
117}
118
119func (e *PGPPullEngine) runLoggedOut(m libkb.MetaContext) error {
120	if len(e.userAsserts) == 0 {
121		return libkb.PGPPullLoggedOutError{}
122	}
123	t := time.Now()
124	for i, assertString := range e.userAsserts {
125		t = e.rateLimit(t, i)
126		if err := e.processUserWithIdentify(m, assertString); err != nil {
127			return err
128		}
129	}
130	return nil
131}
132
133func (e *PGPPullEngine) processUserWithIdentify(m libkb.MetaContext, u string) error {
134	m.Debug("Processing with identify: %s", u)
135
136	iarg := keybase1.Identify2Arg{
137		UserAssertion:    u,
138		ForceRemoteCheck: true,
139		AlwaysBlock:      true,
140		NeedProofSet:     true, // forces prompt even if we declined before
141	}
142	topts := keybase1.TrackOptions{
143		LocalOnly:  true,
144		ForPGPPull: true,
145	}
146	ieng := NewResolveThenIdentify2WithTrack(m.G(), &iarg, topts)
147	if err := RunEngine2(m, ieng); err != nil {
148		m.Info("identify run err: %s", err)
149		return err
150	}
151
152	// prompt if the identify is correct
153	result := ieng.ConfirmResult()
154	if !result.IdentityConfirmed {
155		m.Warning("Not confirmed; skipping key import")
156		return nil
157	}
158
159	idRes, err := ieng.Result(m)
160	if err != nil {
161		return err
162	}
163	// with more plumbing, there is likely a more efficient way to get this identified user out
164	// of the identify2 engine, but `pgp pull` is not likely to be called often.
165	arg := libkb.NewLoadUserArgWithMetaContext(m).WithUID(idRes.Upk.GetUID())
166	user, err := libkb.LoadUser(arg)
167	if err != nil {
168		return err
169	}
170	return e.exportKeysToGPG(m, user, nil)
171}
172
173func (e *PGPPullEngine) Run(m libkb.MetaContext) error {
174
175	e.gpgClient = libkb.NewGpgCLI(m.G(), m.UIs().LogUI)
176	err := e.gpgClient.Configure(m)
177	if err != nil {
178		return err
179	}
180
181	if ok, _ := isLoggedIn(m); !ok {
182		return e.runLoggedOut(m)
183	}
184
185	return e.runLoggedIn(m)
186}
187
188func (e *PGPPullEngine) runLoggedIn(m libkb.MetaContext) error {
189	summaries, leftovers, err := e.getTrackedUserSummaries(m)
190	// leftovers contains unmatched assertions, likely users
191	// we want to pull but we do not track.
192	if err != nil {
193		return err
194	}
195
196	pkLookup := make(map[keybase1.UID][]string)
197
198	err = m.G().GetFullSelfer().WithSelf(m.Ctx(), func(user *libkb.User) error {
199		if user == nil {
200			return libkb.UserNotFoundError{}
201		}
202
203		var trackList []*libkb.TrackChainLink
204		if idTable := user.IDTable(); idTable != nil {
205			trackList = idTable.GetTrackList()
206		}
207
208		for _, track := range trackList {
209			trackedUID, err := track.GetTrackedUID()
210			if err != nil {
211				return err
212			}
213			keys, err := track.GetTrackedKeys()
214			if err != nil {
215				return err
216			}
217			for _, key := range keys {
218				pkLookup[trackedUID] = append(pkLookup[trackedUID], key.Fingerprint.String())
219			}
220		}
221		return nil
222	})
223	if err != nil {
224		return err
225	}
226
227	// Loop over the list of all users we track.
228	t := time.Now()
229	for i, userSummary := range summaries {
230		t = e.rateLimit(t, i)
231		// Compute the set of tracked pgp fingerprints. LoadUser will fetch key
232		// data from the server, and we will compare it against this.
233		trackedFingerprints := make(map[string]bool)
234		fprs, ok := pkLookup[userSummary.Uid]
235		if !ok {
236			fprs = []string{}
237		}
238		for _, pubKey := range fprs {
239			if pubKey != "" {
240				trackedFingerprints[pubKey] = true
241			}
242		}
243
244		// Get user data from the server.
245		user, err := libkb.LoadUser(
246			libkb.NewLoadUserByNameArg(e.G(), userSummary.Username).
247				WithPublicKeyOptional())
248		if err != nil {
249			m.Error("Failed to load user %s: %s", userSummary.Username, err)
250			continue
251		}
252		if user.GetStatus() == keybase1.StatusCode_SCDeleted {
253			m.Debug("User %q is deleted, skipping", userSummary.Username)
254			continue
255		}
256
257		if err = e.exportKeysToGPG(m, user, trackedFingerprints); err != nil {
258			return err
259		}
260	}
261
262	// Loop over unmatched list and process with identify prompts.
263	for i, assertString := range leftovers {
264		t = e.rateLimit(t, i)
265		if err := e.processUserWithIdentify(m, assertString); err != nil {
266			return err
267		}
268	}
269
270	return nil
271}
272
273func (e *PGPPullEngine) exportKeysToGPG(m libkb.MetaContext, user *libkb.User, tfp map[string]bool) error {
274	for _, bundle := range user.GetActivePGPKeys(false) {
275		// Check each key against the tracked set.
276		if tfp != nil && !tfp[bundle.GetFingerprint().String()] {
277			m.Warning("Keybase says that %s owns key %s, but you have not tracked this fingerprint before.", user.GetName(), bundle.GetFingerprint())
278			continue
279		}
280
281		if err := e.gpgClient.ExportKey(m, *bundle, false /* export public key only */, false /* no batch */); err != nil {
282			m.Warning("Failed to import %'s public key %s: %s", user.GetName(), bundle.GetFingerprint(), err.Error())
283			continue
284		}
285
286		m.Info("Imported key for %s.", user.GetName())
287	}
288	return nil
289}
290
291func (e *PGPPullEngine) rateLimit(start time.Time, index int) time.Time {
292	// server currently limiting to 32 req/s, but there can be 4 requests for each loaduser call.
293	const loadUserPerSec = 4
294	if index == 0 {
295		return start
296	}
297	if index%loadUserPerSec != 0 {
298		return start
299	}
300	d := time.Second - time.Since(start)
301	if d > 0 {
302		e.G().Log.Debug("sleeping for %s to slow down api requests", d)
303		time.Sleep(d)
304	}
305	return time.Now()
306}
307