github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/engine/pgp_pull.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package engine
     5  
     6  import (
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/keybase/client/go/libkb"
    11  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    12  )
    13  
    14  type PGPPullEngineArg struct {
    15  	UserAsserts []string
    16  }
    17  
    18  type PGPPullEngine struct {
    19  	listTrackingEngine *ListTrackingEngine
    20  	userAsserts        []string
    21  	gpgClient          *libkb.GpgCLI
    22  	libkb.Contextified
    23  }
    24  
    25  func 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  
    33  func (e *PGPPullEngine) Name() string {
    34  	return "PGPPull"
    35  }
    36  
    37  func (e *PGPPullEngine) Prereqs() Prereqs {
    38  	return Prereqs{}
    39  }
    40  
    41  func (e *PGPPullEngine) RequiredUIs() []libkb.UIKind {
    42  	return []libkb.UIKind{
    43  		libkb.LogUIKind,
    44  	}
    45  }
    46  
    47  func (e *PGPPullEngine) SubConsumers() []libkb.UIConsumer {
    48  	return []libkb.UIConsumer{e.listTrackingEngine}
    49  }
    50  
    51  func 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  
    59  func (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  
   119  func (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  
   133  func (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  
   173  func (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  
   188  func (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  
   273  func (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  
   291  func (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  }