github.com/tommi2day/gomodules@v1.13.2-0.20240423190010-b7d55d252a27/pwlib/gpg.go (about)

     1  package pwlib
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"slices"
    12  	"strings"
    13  
    14  	"github.com/tommi2day/gomodules/common"
    15  
    16  	"github.com/ProtonMail/go-crypto/openpgp"
    17  	"github.com/ProtonMail/go-crypto/openpgp/armor"
    18  	log "github.com/sirupsen/logrus"
    19  )
    20  
    21  // GPGConfig holds gpg config
    22  type GPGConfig struct {
    23  	StoreDir      string
    24  	SecretKeyFile string
    25  	SecretKeyPass string
    26  	KeyID         string
    27  }
    28  
    29  // GPGUnlockKey decrypt private key and subkeys
    30  func GPGUnlockKey(gpgEntity *openpgp.Entity, keypass string) (err error) {
    31  	if gpgEntity == nil {
    32  		err = fmt.Errorf("no key loaded")
    33  		return
    34  	}
    35  	err = gpgEntity.DecryptPrivateKeys([]byte(keypass))
    36  	return
    37  }
    38  
    39  // GPGSelectEntity select entity from list by Fingerprint or first one
    40  func GPGSelectEntity(entityList openpgp.EntityList, keyID string) (gpgEntity *openpgp.Entity, err error) {
    41  	if len(entityList) == 0 {
    42  		err = fmt.Errorf("no gpg entity loaded")
    43  		return
    44  	}
    45  	// if len(entityList) == 1 || keyID == "" {
    46  	if keyID == "" {
    47  		gpgEntity = entityList[0]
    48  		log.Debugf("use first key %s", gpgEntity.PrimaryKey.KeyIdString())
    49  	} else {
    50  		keyID = strings.TrimPrefix(keyID, "0x")
    51  		keyID = strings.TrimRight(keyID, "\r\n")
    52  		for e := range entityList {
    53  			if entityList[e].PrimaryKey == nil {
    54  				continue
    55  			}
    56  			primID := entityList[e].PrimaryKey.KeyIdString()
    57  			if primID == keyID {
    58  				gpgEntity = entityList[e]
    59  				log.Debugf("matched primary key Id %s", keyID)
    60  				break
    61  			}
    62  			// match private subkey ID
    63  			if entityList[e].PrivateKey == nil {
    64  				continue
    65  			}
    66  			privID := entityList[e].PrivateKey.KeyIdString()
    67  			if privID == keyID {
    68  				gpgEntity = entityList[e]
    69  				log.Debugf("matched private key Id %s", keyID)
    70  				break
    71  			}
    72  		}
    73  		// if not found, error out
    74  		if gpgEntity == nil {
    75  			err = fmt.Errorf("cannot find key with id %s", keyID)
    76  			return
    77  		}
    78  	}
    79  	return
    80  }
    81  
    82  // GPGReadAmoredKeyRing read keyring from string
    83  func GPGReadAmoredKeyRing(amoredKeyRing string) (entityList openpgp.EntityList, err error) {
    84  	entityList, err = openpgp.ReadArmoredKeyRing(bytes.NewBufferString(amoredKeyRing))
    85  	if err != nil || len(entityList) == 0 {
    86  		if err == nil {
    87  			err = fmt.Errorf("cannot work with entity list empty")
    88  		} else {
    89  			err = fmt.Errorf("cannot decode keyring string: %s", err)
    90  		}
    91  		return
    92  	}
    93  	return
    94  }
    95  
    96  func findGPGFiles(root string) []string {
    97  	var a []string
    98  	err := filepath.WalkDir(root, func(s string, d fs.DirEntry, e error) error {
    99  		if e != nil {
   100  			return e
   101  		}
   102  		sl := filepath.ToSlash(s)
   103  		f := d.Name()
   104  		ext := filepath.Ext(f)
   105  		if ext == ".gpg" {
   106  			a = append(a, sl)
   107  		}
   108  		return nil
   109  	})
   110  	if err != nil {
   111  		log.Warnf("cannot walk from %s: %s", root, err)
   112  		a = []string{}
   113  	}
   114  	return a
   115  }
   116  
   117  // GetGopassSecrets get secrets from gopass store
   118  func GetGopassSecrets(storeRootPath string, secretKeyFile string, keypass string) (secrets string, err error) {
   119  	var gpgid string
   120  	var pass string
   121  
   122  	gpgid, err = checkStoreRoot(storeRootPath)
   123  	if err != nil {
   124  		return
   125  	}
   126  	gpgFiles := findGPGFiles(storeRootPath)
   127  	storeName := filepath.Base(storeRootPath)
   128  	if slices.Contains([]string{".password-store", "root"}, storeName) {
   129  		// strip base dir from name if is storePath store
   130  		storeName = ""
   131  	}
   132  	for _, f := range gpgFiles {
   133  		sn := strings.TrimSuffix(f, ".gpg")
   134  		key := filepath.Base(sn)
   135  		sn = strings.TrimPrefix(sn, storeRootPath+"/")
   136  		secretPath := filepath.Dir(sn)
   137  		secretPath = strings.ReplaceAll(secretPath, ":", "_")
   138  		if secretPath == "." {
   139  			secretPath = ""
   140  		}
   141  		if storeName != "" {
   142  			secretPath = path.Join(storeName, secretPath)
   143  		}
   144  		pass, err = GPGDecryptFile(f, secretKeyFile, keypass, gpgid)
   145  		if err == nil {
   146  			pass = strings.TrimRight(pass, "\r\n")
   147  			secrets += fmt.Sprintf("%s:%s:%s\n", secretPath, key, pass)
   148  		} else {
   149  			err = fmt.Errorf("cannot decrypt %s: %s", f, err)
   150  			secrets = ""
   151  			return
   152  		}
   153  	}
   154  	secrets = strings.TrimRight(secrets, "\n")
   155  	return
   156  }
   157  
   158  func checkStoreRoot(storeRootPath string) (gpgid string, err error) {
   159  	if !common.IsDir(storeRootPath) {
   160  		err = fmt.Errorf("root %s is not a directory", storeRootPath)
   161  		return
   162  	}
   163  	if !common.FileExists(path.Join(storeRootPath, ".gpg-id")) {
   164  		err = fmt.Errorf("root %s is not a gopass store", storeRootPath)
   165  		return
   166  	}
   167  	gpgid, err = common.ReadFileToString(path.Join(storeRootPath, ".gpg-id"))
   168  	return
   169  }
   170  
   171  // GPGDecryptFile decrypt file with GPG Key
   172  func GPGDecryptFile(filename string, secretKeyFile string, keypass string, gpgid string) (decryptedContent string, err error) {
   173  	var entityList openpgp.EntityList
   174  	var entity *openpgp.Entity
   175  	var key string
   176  	key, err = common.ReadFileToString(secretKeyFile)
   177  	if err != nil {
   178  		return
   179  	}
   180  	entityList, err = GPGReadAmoredKeyRing(key)
   181  	if err != nil {
   182  		return
   183  	}
   184  	entity, err = GPGSelectEntity(entityList, gpgid)
   185  	if err != nil {
   186  		return
   187  	}
   188  	err = GPGUnlockKey(entity, keypass)
   189  	if err != nil {
   190  		return
   191  	}
   192  	encrypted := ""
   193  	var md *openpgp.MessageDetails
   194  	encrypted, err = common.ReadFileToString(filename)
   195  	if err != nil {
   196  		return
   197  	}
   198  	r := bytes.NewReader([]byte(encrypted))
   199  	md, err = openpgp.ReadMessage(r, entityList, nil, nil)
   200  	if err != nil {
   201  		return
   202  	}
   203  	decryptedBytes, err := io.ReadAll(md.UnverifiedBody)
   204  	if err != nil {
   205  		return
   206  	}
   207  	decryptedContent = string(decryptedBytes)
   208  	return
   209  }
   210  
   211  // GPGEncryptFile encrypt file with GPG Key
   212  func GPGEncryptFile(plainFile string, targetFile string, publicKeyFile string) (err error) {
   213  	var entityList openpgp.EntityList
   214  	var pubKeys string
   215  	var plain string
   216  	var encryptedBytes []byte
   217  
   218  	// recipients allowed to decrypt
   219  	pubKeys, err = common.ReadFileToString(publicKeyFile)
   220  	if err != nil {
   221  		return
   222  	}
   223  	entityList, err = GPGReadAmoredKeyRing(pubKeys)
   224  	if err != nil {
   225  		return
   226  	}
   227  	plain, err = common.ReadFileToString(plainFile)
   228  	if err != nil {
   229  		return
   230  	}
   231  	encBuffer := new(bytes.Buffer)
   232  	pw, err := openpgp.Encrypt(encBuffer, entityList, nil, &openpgp.FileHints{IsBinary: true}, nil)
   233  	if err != nil {
   234  		return
   235  	}
   236  	// write plaintext to encryptor
   237  	_, err = pw.Write([]byte(plain))
   238  	if err != nil {
   239  		return
   240  	}
   241  	_ = pw.Close()
   242  
   243  	// write encrypted output to file
   244  	encryptedBytes, err = io.ReadAll(encBuffer)
   245  	if err != nil {
   246  		return
   247  	}
   248  	//nolint gosec
   249  	err = os.WriteFile(targetFile, encryptedBytes, 0644)
   250  	return
   251  }
   252  
   253  // CreateGPGEntity create GPG entity with new key pair
   254  func CreateGPGEntity(name string, comment string, email string, passPhrase string) (entity *openpgp.Entity, privKeyID string, err error) {
   255  	var e *openpgp.Entity
   256  
   257  	e, err = openpgp.NewEntity(name, comment, email, nil)
   258  	if err != nil {
   259  		return
   260  	}
   261  
   262  	privKeyID = e.PrivateKey.KeyIdString()
   263  
   264  	// need to resign self-signature with userid and add flags to make it valid
   265  	id := ""
   266  	for _, i := range e.Identities {
   267  		if i.SelfSignature != nil {
   268  			id = i.UserId.Id
   269  			break
   270  		}
   271  	}
   272  	e.Identities[id].SelfSignature.FlagSign = true
   273  	e.Identities[id].SelfSignature.FlagCertify = true
   274  	err = e.Identities[id].SelfSignature.SignUserId(id, e.PrimaryKey, e.PrivateKey, nil)
   275  	if err != nil {
   276  		err = fmt.Errorf("error selfsigning identity: %s", err)
   277  		return
   278  	}
   279  
   280  	// add signing subkey
   281  	err = e.AddSigningSubkey(nil)
   282  	if err != nil {
   283  		err = fmt.Errorf("error adding signing subkey: %s", err)
   284  		return
   285  	}
   286  
   287  	// sign whole identity
   288  	err = e.SignIdentity(id, e, nil)
   289  	if err != nil {
   290  		err = fmt.Errorf("error signing identity: %s", err)
   291  		return
   292  	}
   293  
   294  	// encrypt private key
   295  	err = e.EncryptPrivateKeys([]byte(passPhrase), nil)
   296  	if err != nil {
   297  		err = fmt.Errorf("error while encrypting private key: %s", err)
   298  		return
   299  	}
   300  	return e, privKeyID, nil
   301  }
   302  
   303  // ExportGPGKeyPair export GPG entity to armored public and private key files
   304  func ExportGPGKeyPair(entity *openpgp.Entity, publicFilename string, privFilename string) (err error) {
   305  	var out *os.File
   306  	var w io.WriteCloser
   307  	if entity == nil {
   308  		err = fmt.Errorf("no entity to export")
   309  		return
   310  	}
   311  	//nolint gosec
   312  	out, err = os.Create(publicFilename)
   313  	w, err = armor.Encode(out, openpgp.PublicKeyType, make(map[string]string))
   314  	if err != nil {
   315  		err = fmt.Errorf("error creating public key file %s: %s", publicFilename, err)
   316  		return
   317  	}
   318  
   319  	err = entity.Serialize(w)
   320  	if err != nil {
   321  		err = fmt.Errorf("error serializing public key: %s", err)
   322  		return
   323  	}
   324  	_ = w.Close()
   325  
   326  	//nolint gosec
   327  	out, err = os.Create(privFilename)
   328  	w, err = armor.Encode(out, openpgp.PrivateKeyType, make(map[string]string))
   329  	if err != nil {
   330  		err = fmt.Errorf("error creating private key file %s: %s", privFilename, err)
   331  		return
   332  	}
   333  	// export withoout signg because of missing crypto.signer bug
   334  	err = entity.SerializePrivateWithoutSigning(w, nil)
   335  	if err != nil {
   336  		err = fmt.Errorf("error serializing private key to %s: %s", privFilename, err)
   337  	}
   338  	_ = w.Close()
   339  	return
   340  }
   341  
   342  /*
   343  func createEntityFromRSAKeys(pubKey *packet.PublicKey, privKey *packet.PrivateKey,name string,comment string,email string) (entity *openpgp.Entity,err error) {
   344  	config := packet.Config{
   345  		DefaultHash:            crypto.SHA256,
   346  		DefaultCipher:          packet.CipherAES256,
   347  		DefaultCompressionAlgo: packet.NoCompression,
   348  	}
   349  	currentTime := config.Now()
   350  	uid := packet.NewUserId(name, comment, email)
   351  
   352  	e := openpgp.Entity{
   353  		PrimaryKey: pubKey,
   354  		PrivateKey: privKey,
   355  		Identities: make(map[string]*openpgp.Identity),
   356  	}
   357  	isPrimaryId := false
   358  
   359  	e.Identities[uid.Id] = &openpgp.Identity{
   360  		Name:   uid.Name,
   361  		UserId: uid,
   362  		SelfSignature: &packet.Signature{
   363  			CreationTime: currentTime,
   364  			SigType:      packet.SigTypePositiveCert,
   365  			PubKeyAlgo:   packet.PubKeyAlgoRSA,
   366  			Hash:         config.Hash(),
   367  			IsPrimaryId:  &isPrimaryId,
   368  			FlagsValid:   true,
   369  			FlagSign:     true,
   370  			FlagCertify:  true,
   371  			IssuerKeyId:  &e.PrimaryKey.KeyId,
   372  		},
   373  	}
   374  
   375  	keyLifetimeSecs := uint32(86400 * 365)
   376  
   377  	e.Subkeys = make([]openpgp.Subkey, 1)
   378  	e.Subkeys[0] = openpgp.Subkey{
   379  		PublicKey: pubKey,
   380  		PrivateKey: privKey,
   381  		Sig: &packet.Signature{
   382  			CreationTime:              currentTime,
   383  			SigType:                   packet.SigTypeSubkeyBinding,
   384  			PubKeyAlgo:                packet.PubKeyAlgoRSA,
   385  			Hash:                      config.Hash(),
   386  			PreferredHash:             []uint8{8}, // SHA-256
   387  			FlagsValid:                true,
   388  			FlagEncryptStorage:        true,
   389  			FlagEncryptCommunications: true,
   390  			IssuerKeyId:               &e.PrimaryKey.KeyId,
   391  			KeyLifetimeSecs:           &keyLifetimeSecs,
   392  		},
   393  	}
   394  	return &e
   395  }
   396  */