github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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 }