// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package pgpkeys import ( "bytes" "encoding/base64" "errors" "fmt" "os" "strings" "github.com/ProtonMail/go-crypto/openpgp" ) // PubKeyFileFlag implements flag.Value and command.Example to receive exactly // one PGP or keybase key via a flag. type PubKeyFileFlag string func (p *PubKeyFileFlag) String() string { return string(*p) } func (p *PubKeyFileFlag) Set(val string) error { if p != nil && *p != "" { return errors.New("can only be specified once") } keys, err := ParsePGPKeys(strings.Split(val, ",")) if err != nil { return err } if len(keys) > 1 { return errors.New("can only specify one pgp key") } *p = PubKeyFileFlag(keys[0]) return nil } func (p *PubKeyFileFlag) Example() string { return "keybase:user" } // PGPPubKeyFiles implements the flag.Value interface and allows parsing and // reading a list of PGP public key files. type PubKeyFilesFlag []string func (p *PubKeyFilesFlag) String() string { return fmt.Sprint(*p) } func (p *PubKeyFilesFlag) Set(val string) error { if len(*p) > 0 { return errors.New("can only be specified once") } keys, err := ParsePGPKeys(strings.Split(val, ",")) if err != nil { return err } *p = PubKeyFilesFlag(keys) return nil } func (p *PubKeyFilesFlag) Example() string { return "keybase:user1, keybase:user2, ..." } // ParsePGPKeys takes a list of PGP keys and parses them either using keybase // or reading them from disk and returns the "expanded" list of pgp keys in // the same order. func ParsePGPKeys(keyfiles []string) ([]string, error) { keys := make([]string, len(keyfiles)) keybaseMap, err := FetchKeybasePubkeys(keyfiles) if err != nil { return nil, err } for i, keyfile := range keyfiles { keyfile = strings.TrimSpace(keyfile) if strings.HasPrefix(keyfile, kbPrefix) { key, ok := keybaseMap[keyfile] if !ok || key == "" { return nil, fmt.Errorf("keybase user %q not found", strings.TrimPrefix(keyfile, kbPrefix)) } keys[i] = key continue } pgpStr, err := ReadPGPFile(keyfile) if err != nil { return nil, err } keys[i] = pgpStr } return keys, nil } // ReadPGPFile reads the given PGP file from disk. func ReadPGPFile(path string) (string, error) { if len(path) <= 0 { return "", errors.New("empty path") } if path[0] == '@' { path = path[1:] } f, err := os.Open(path) if err != nil { return "", err } defer f.Close() buf := bytes.NewBuffer(nil) _, err = buf.ReadFrom(f) if err != nil { return "", err } // First parse as an armored keyring file, if that doesn't work, treat it as a straight binary/b64 string keyReader := bytes.NewReader(buf.Bytes()) entityList, err := openpgp.ReadArmoredKeyRing(keyReader) if err == nil { if len(entityList) != 1 { return "", fmt.Errorf("more than one key found in file %q", path) } if entityList[0] == nil { return "", fmt.Errorf("primary key was nil for file %q", path) } serializedEntity := bytes.NewBuffer(nil) err = entityList[0].Serialize(serializedEntity) if err != nil { return "", fmt.Errorf("error serializing entity for file %q: %w", path, err) } return base64.StdEncoding.EncodeToString(serializedEntity.Bytes()), nil } _, err = base64.StdEncoding.DecodeString(buf.String()) if err == nil { return buf.String(), nil } return base64.StdEncoding.EncodeToString(buf.Bytes()), nil }