1package libkb
2
3import (
4	"encoding/json"
5	"fmt"
6	"strings"
7
8	"github.com/keybase/client/go/kbun"
9	"github.com/keybase/client/go/protocol/gregor1"
10	"github.com/keybase/client/go/protocol/keybase1"
11)
12
13func getWotVouchChainLink(mctx MetaContext, uid keybase1.UID, sigID keybase1.SigID) (cl *WotVouchChainLink, voucher *User, err error) {
14	// requires a full chain load
15	user, err := LoadUser(NewLoadUserArgWithMetaContext(mctx).WithUID(uid).WithStubMode(StubModeUnstubbed))
16	if err != nil {
17		return nil, nil, fmt.Errorf("Error loading user: %v", err)
18	}
19	link := user.LinkFromSigID(sigID)
20	if link == nil {
21		return nil, nil, fmt.Errorf("Could not find link from sigID")
22	}
23	tlink, w := NewTypedChainLink(link)
24	if w != nil {
25		return nil, nil, fmt.Errorf("Could not get typed chain link: %v", w.Warning())
26	}
27	vlink, ok := tlink.(*WotVouchChainLink)
28	if !ok {
29		return nil, nil, fmt.Errorf("Link is not a WotVouchChainLink: %v", tlink)
30	}
31	return vlink, user, nil
32}
33
34func getWotReactChainLink(mctx MetaContext, user *User, sigID keybase1.SigID) (cl *WotReactChainLink, err error) {
35	link := user.LinkFromSigID(sigID)
36	if link == nil {
37		return nil, fmt.Errorf("Could not find link from sigID")
38	}
39	tlink, w := NewTypedChainLink(link)
40	if w != nil {
41		return nil, fmt.Errorf("Could not get typed chain link: %v", w.Warning())
42	}
43	rlink, ok := tlink.(*WotReactChainLink)
44	if !ok {
45		return nil, fmt.Errorf("Link is not a WotReactChainLink: %v", tlink)
46	}
47	return rlink, nil
48}
49
50func assertVouchIsForUser(mctx MetaContext, vouchedUser wotExpansionUser, user *User) (err error) {
51	if user.GetName() != vouchedUser.Username {
52		return fmt.Errorf("wot username isn't expected %s != %s", user.GetName(), vouchedUser.Username)
53	}
54	if user.GetUID() != vouchedUser.UID {
55		return fmt.Errorf("wot uid isn't me %s != %s", user.GetUID(), vouchedUser.UID)
56	}
57	if user.GetEldestKID() != vouchedUser.Eldest.KID {
58		return fmt.Errorf("wot eldest kid isn't me %s != %s", user.GetEldestKID(), vouchedUser.Eldest.KID)
59	}
60	return nil
61}
62
63type wotExpansionUser struct {
64	Eldest struct {
65		KID   keybase1.KID
66		Seqno keybase1.Seqno
67	}
68	SeqTail struct {
69		PayloadHash string
70		Seqno       keybase1.Seqno
71		SigID       string
72	}
73	UID      keybase1.UID
74	Username string
75}
76
77type vouchExpansion struct {
78	User       wotExpansionUser    `json:"user"`
79	Confidence keybase1.Confidence `json:"confidence"`
80	VouchText  string              `json:"vouch_text"`
81}
82
83type reactionExpansion struct {
84	SigID    keybase1.SigID `json:"sig_id"`
85	Reaction string         `json:"reaction"`
86}
87
88type serverWotVouch struct {
89	Voucher               keybase1.UID           `json:"voucher"`
90	VoucherEldestSeqno    keybase1.Seqno         `json:"voucher_eldest_seqno"`
91	Vouchee               keybase1.UID           `json:"vouchee"`
92	VoucheeEldestSeqno    keybase1.Seqno         `json:"vouchee_eldest_seqno"`
93	VouchSigID            keybase1.SigID         `json:"vouch_sig"`
94	VouchExpansionJSON    string                 `json:"vouch_expansion"`
95	ReactionSigID         *keybase1.SigID        `json:"reaction_sig,omitempty"`
96	ReactionExpansionJSON *string                `json:"reaction_expansion,omitempty"`
97	Status                keybase1.WotStatusType `json:"status"`
98}
99
100func transformUserVouch(mctx MetaContext, serverVouch serverWotVouch, voucheeUser *User) (res keybase1.WotVouch, err error) {
101	// load the voucher and fetch the relevant chain link
102	wotVouchLink, voucher, err := getWotVouchChainLink(mctx, serverVouch.Voucher, serverVouch.VouchSigID)
103	if err != nil {
104		return res, fmt.Errorf("error finding the vouch in the voucher's sigchain: %s", err.Error())
105	}
106	// extract the sig expansion
107	expansionObject, err := ExtractExpansionObj(wotVouchLink.ExpansionID, serverVouch.VouchExpansionJSON)
108	if err != nil {
109		return res, fmt.Errorf("error extracting and validating the vouch expansion: %s", err.Error())
110	}
111	// load it into the right type for web-of-trust vouching
112	var wotObj vouchExpansion
113	err = json.Unmarshal(expansionObject, &wotObj)
114	if err != nil {
115		return res, fmt.Errorf("error casting vouch expansion object to expected web-of-trust schema: %s", err.Error())
116	}
117
118	if voucheeUser == nil || voucheeUser.GetUID() != serverVouch.Vouchee {
119		// load vouchee
120		voucheeUser, err = LoadUser(NewLoadUserArgWithMetaContext(mctx).WithUID(serverVouch.Vouchee).WithPublicKeyOptional().WithStubMode(StubModeUnstubbed))
121		if err != nil {
122			return res, fmt.Errorf("error loading vouchee to transform: %s", err.Error())
123		}
124	}
125
126	err = assertVouchIsForUser(mctx, wotObj.User, voucheeUser)
127	if err != nil {
128		mctx.Debug("web-of-trust vouch user-section doesn't look right: %+v", wotObj.User)
129		return res, fmt.Errorf("error verifying user section of web-of-trust expansion: %s", err.Error())
130	}
131
132	hasReaction := serverVouch.ReactionSigID != nil
133	var reactionObj reactionExpansion
134	var reactionStatus keybase1.WotReactionType
135	var wotReactLink *WotReactChainLink
136	if hasReaction {
137		wotReactLink, err = getWotReactChainLink(mctx, voucheeUser, *serverVouch.ReactionSigID)
138		if err != nil {
139			return res, fmt.Errorf("error finding the vouch in the vouchee's sigchain: %s", err.Error())
140		}
141		// extract the sig expansion
142		expansionObject, err = ExtractExpansionObj(wotReactLink.ExpansionID, *serverVouch.ReactionExpansionJSON)
143		if err != nil {
144			return res, fmt.Errorf("error extracting and validating the vouch expansion: %s", err.Error())
145		}
146		// load it into the right type for web-of-trust vouching
147		err = json.Unmarshal(expansionObject, &reactionObj)
148		if err != nil {
149			return res, fmt.Errorf("error casting vouch expansion object to expected web-of-trust schema: %s", err.Error())
150		}
151		if reactionObj.SigID.String()[:30] != wotVouchLink.GetSigID().String()[:30] {
152			return res, fmt.Errorf("reaction sigID doesn't match the original attestation: %s != %s", reactionObj.SigID, wotVouchLink.GetSigID())
153		}
154		reactionStatus = keybase1.WotReactionTypeMap[strings.ToUpper(reactionObj.Reaction)]
155	}
156
157	var status keybase1.WotStatusType
158	switch {
159	case wotVouchLink.revoked:
160		status = keybase1.WotStatusType_REVOKED
161	case wotReactLink != nil && wotReactLink.revoked:
162		status = keybase1.WotStatusType_PROPOSED
163	case !hasReaction:
164		status = keybase1.WotStatusType_PROPOSED
165	case reactionStatus == keybase1.WotReactionType_ACCEPT:
166		status = keybase1.WotStatusType_ACCEPTED
167	case reactionStatus == keybase1.WotReactionType_REJECT:
168		status = keybase1.WotStatusType_REJECTED
169	default:
170		return res, fmt.Errorf("could not determine the status of web-of-trust from %s", voucher.GetName())
171	}
172
173	var proofs []keybase1.WotProofUI
174	for _, proof := range wotObj.Confidence.Proofs {
175		proofForUI, err := NewWotProofUI(mctx, proof)
176		if err != nil {
177			return res, err
178		}
179		proofs = append(proofs, proofForUI)
180	}
181
182	// build a WotVouch
183	return keybase1.WotVouch{
184		Status:          status,
185		Vouchee:         voucheeUser.ToUserVersion(),
186		VoucheeUsername: voucheeUser.GetNormalizedName().String(),
187		Voucher:         voucher.ToUserVersion(),
188		VoucherUsername: voucher.GetNormalizedName().String(),
189		VouchText:       wotObj.VouchText,
190		VouchProof:      serverVouch.VouchSigID,
191		VouchedAt:       keybase1.ToTime(wotVouchLink.GetCTime()),
192		Confidence:      wotObj.Confidence,
193		Proofs:          proofs,
194	}, nil
195}
196
197type apiWot struct {
198	AppStatusEmbed
199	Vouches []serverWotVouch `json:"webOfTrust"`
200}
201
202type FetchWotVouchesArg struct {
203	Vouchee string
204	Voucher string
205}
206
207func fetchWot(mctx MetaContext, vouchee string, voucher string) (res []serverWotVouch, err error) {
208	defer mctx.Trace("fetchWot", &err)()
209	apiArg := APIArg{
210		Endpoint:    "wot/get",
211		SessionType: APISessionTypeREQUIRED,
212	}
213	apiArg.Args = HTTPArgs{}
214	if len(vouchee) > 0 {
215		apiArg.Args["vouchee"] = S{Val: vouchee}
216	}
217	if len(voucher) > 0 {
218		apiArg.Args["voucher"] = S{Val: voucher}
219	}
220	var response apiWot
221	err = mctx.G().API.GetDecode(mctx, apiArg, &response)
222	if err != nil {
223		mctx.Debug("error fetching web-of-trust vouches: %s", err.Error())
224		return nil, err
225	}
226	mctx.Debug("server returned %d web-of-trust vouches", len(response.Vouches))
227	return response.Vouches, nil
228}
229
230// FetchWotVouches gets vouches written for vouchee (if specified) by voucher
231// (if specified).
232func FetchWotVouches(mctx MetaContext, arg FetchWotVouchesArg) (res []keybase1.WotVouch, err error) {
233	vouches, err := fetchWot(mctx, arg.Vouchee, arg.Voucher)
234	if err != nil {
235		mctx.Debug("error fetching web-of-trust vouches for vouchee=%s by voucher=%s: %s", arg.Vouchee, arg.Voucher, err.Error())
236		return nil, err
237	}
238	var voucheeUser *User
239	if len(arg.Vouchee) > 0 {
240		voucheeUser, err = LoadUser(NewLoadUserArgWithMetaContext(mctx).WithName(arg.Vouchee).WithPublicKeyOptional())
241		if err != nil {
242			return nil, fmt.Errorf("error loading vouchee: %s", err.Error())
243		}
244	}
245	for _, serverVouch := range vouches {
246		vouch, err := transformUserVouch(mctx, serverVouch, voucheeUser)
247		if err != nil {
248			mctx.Debug("error validating server-reported web-of-trust vouches for vouchee=%s by voucher=%s: %s", arg.Vouchee, arg.Voucher, err.Error())
249			return nil, err
250		}
251		res = append(res, vouch)
252	}
253	mctx.Debug("found %d web-of-trust vouches for vouchee=%s by voucher=%s", len(res), arg.Vouchee, arg.Voucher)
254	return res, nil
255}
256
257type _wotMsg struct {
258	Voucher *string `json:"voucher,omitempty"`
259	Vouchee *string `json:"vouchee,omitempty"`
260}
261
262func hasWotMsg(testable string) bool {
263	for _, match := range []string{"wot.new_vouch", "wot.accepted", "wot.rejected"} {
264		if match == testable {
265			return true
266		}
267	}
268	return false
269}
270
271func isDismissable(mctx MetaContext, category string, msg _wotMsg, voucher, vouchee kbun.NormalizedUsername) bool {
272	voucherMatches := (msg.Voucher != nil && kbun.NewNormalizedUsername(*msg.Voucher) == voucher)
273	voucheeMatches := (msg.Vouchee != nil && kbun.NewNormalizedUsername(*msg.Vouchee) == vouchee)
274	me := mctx.ActiveDevice().Username(mctx)
275	switch category {
276	case "wot.new_vouch":
277		return voucherMatches && (voucheeMatches || vouchee == me)
278	case "wot.accepted", "wot.rejected":
279		return voucheeMatches && (voucherMatches || voucher == me)
280	default:
281		return false
282	}
283}
284
285func DismissWotNotifications(mctx MetaContext, voucherUsername, voucheeUsername string) (err error) {
286	dismisser := mctx.G().GregorState
287	state, err := mctx.G().GregorState.State(mctx.Ctx())
288	if err != nil {
289		return err
290	}
291	categoryPrefix, err := gregor1.ObjFactory{}.MakeCategory("wot")
292	if err != nil {
293		return err
294	}
295	items, err := state.ItemsWithCategoryPrefix(categoryPrefix)
296	if err != nil {
297		return err
298	}
299	var wotMsg _wotMsg
300	for _, item := range items {
301		category := item.Category().String()
302		if !hasWotMsg(category) {
303			continue
304		}
305		if err := json.Unmarshal(item.Body().Bytes(), &wotMsg); err != nil {
306			return err
307		}
308		if isDismissable(mctx, category, wotMsg, kbun.NewNormalizedUsername(voucherUsername), kbun.NewNormalizedUsername(voucheeUsername)) {
309			itemID := item.Metadata().MsgID()
310			mctx.Debug("dismissing %s for %s,%s", category, voucherUsername, voucheeUsername)
311			if err := dismisser.DismissItem(mctx.Ctx(), nil, itemID); err != nil {
312				return err
313			}
314		}
315	}
316	return nil
317}
318