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