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 */