1package pgpkeys
2
3import (
4	"bytes"
5	"encoding/base64"
6	"fmt"
7	"strings"
8
9	"github.com/hashicorp/errwrap"
10	cleanhttp "github.com/hashicorp/go-cleanhttp"
11	"github.com/hashicorp/vault/sdk/helper/jsonutil"
12	"github.com/keybase/go-crypto/openpgp"
13)
14
15const (
16	kbPrefix = "keybase:"
17)
18
19// FetchKeybasePubkeys fetches public keys from Keybase given a set of
20// usernames, which are derived from correctly formatted input entries. It
21// doesn't use their client code due to both the API and the fact that it is
22// considered alpha and probably best not to rely on it.  The keys are returned
23// as base64-encoded strings.
24func FetchKeybasePubkeys(input []string) (map[string]string, error) {
25	client := cleanhttp.DefaultClient()
26	if client == nil {
27		return nil, fmt.Errorf("unable to create an http client")
28	}
29
30	if len(input) == 0 {
31		return nil, nil
32	}
33
34	usernames := make([]string, 0, len(input))
35	for _, v := range input {
36		if strings.HasPrefix(v, kbPrefix) {
37			usernames = append(usernames, strings.TrimPrefix(v, kbPrefix))
38		}
39	}
40
41	if len(usernames) == 0 {
42		return nil, nil
43	}
44
45	ret := make(map[string]string, len(usernames))
46	url := fmt.Sprintf("https://keybase.io/_/api/1.0/user/lookup.json?usernames=%s&fields=public_keys", strings.Join(usernames, ","))
47	resp, err := client.Get(url)
48	if err != nil {
49		return nil, err
50	}
51	defer resp.Body.Close()
52
53	type PublicKeys struct {
54		Primary struct {
55			Bundle string
56		}
57	}
58
59	type LThem struct {
60		PublicKeys `json:"public_keys"`
61	}
62
63	type KbResp struct {
64		Status struct {
65			Name string
66		}
67		Them []LThem
68	}
69
70	out := &KbResp{
71		Them: []LThem{},
72	}
73
74	if err := jsonutil.DecodeJSONFromReader(resp.Body, out); err != nil {
75		return nil, err
76	}
77
78	if out.Status.Name != "OK" {
79		return nil, fmt.Errorf("got non-OK response: %q", out.Status.Name)
80	}
81
82	missingNames := make([]string, 0, len(usernames))
83	var keyReader *bytes.Reader
84	serializedEntity := bytes.NewBuffer(nil)
85	for i, themVal := range out.Them {
86		if themVal.Primary.Bundle == "" {
87			missingNames = append(missingNames, usernames[i])
88			continue
89		}
90		keyReader = bytes.NewReader([]byte(themVal.Primary.Bundle))
91		entityList, err := openpgp.ReadArmoredKeyRing(keyReader)
92		if err != nil {
93			return nil, err
94		}
95		if len(entityList) != 1 {
96			return nil, fmt.Errorf("primary key could not be parsed for user %q", usernames[i])
97		}
98		if entityList[0] == nil {
99			return nil, fmt.Errorf("primary key was nil for user %q", usernames[i])
100		}
101
102		serializedEntity.Reset()
103		err = entityList[0].Serialize(serializedEntity)
104		if err != nil {
105			return nil, errwrap.Wrapf(fmt.Sprintf("error serializing entity for user %q: {{err}}", usernames[i]), err)
106		}
107
108		// The API returns values in the same ordering requested, so this should properly match
109		ret[kbPrefix+usernames[i]] = base64.StdEncoding.EncodeToString(serializedEntity.Bytes())
110	}
111
112	if len(missingNames) > 0 {
113		return nil, fmt.Errorf("unable to fetch keys for user(s) %q from keybase", strings.Join(missingNames, ","))
114	}
115
116	return ret, nil
117}
118