github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/vault/helper/pgpkeys/keybase.go (about)

     1  package pgpkeys
     2  
     3  import (
     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/terraform-plugin-sdk/internal/vault/sdk/helper/jsonutil"
    12  	"github.com/keybase/go-crypto/openpgp"
    13  )
    14  
    15  const (
    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.
    24  func 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  }