// Package pgp implements OpenPGP key generation/import and message encryption for // the webmail client's PGP encryption feature. Deliberately encryption-only: this // codebase uses S/MIME (internal/smime) for signing, PGP only for confidentiality // (an explicit design split) — no signature generation or verification code lives // here. // // Uses github.com/ProtonMail/go-crypto/openpgp, the actively maintained replacement // for the deprecated (and explicitly "unsafe by design", per its own doc comment) // golang.org/x/crypto/openpgp. package pgp import ( "bytes" "errors" "fmt" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" ) // defaultConfig pins AES-256 (the library defaults to AES-128) — same "always pick // the strong option explicitly" posture as internal/smime. RSA-2048 (the library's // own zero-value default when Config.RSABits is unset) matches this codebase's // existing key-size convention (internal/smime, internal/tlsutil both use RSA-2048). func defaultConfig() *packet.Config { return &packet.Config{DefaultCipher: packet.CipherAES256} } // GenerateKeyPair creates a fresh RSA-2048 OpenPGP keypair for email, protects the // private key material with passphrase (the library's own native S2K passphrase // protection, part of the OpenPGP private-key packet format itself — no separate // wrapping layer needed, unlike internal/smime's hand-rolled scrypt+AES-GCM), and // returns both halves ASCII-armored. func GenerateKeyPair(email, passphrase string) (publicArmor, privateArmor []byte, err error) { cfg := defaultConfig() entity, err := openpgp.NewEntity(email, "", email, cfg) if err != nil { return nil, nil, fmt.Errorf("pgp: generate key: %w", err) } if err := entity.EncryptPrivateKeys([]byte(passphrase), cfg); err != nil { return nil, nil, fmt.Errorf("pgp: protect private key: %w", err) } if publicArmor, err = serializePublic(entity); err != nil { return nil, nil, err } if privateArmor, err = serializePrivateWithoutSigning(entity, cfg); err != nil { return nil, nil, err } return publicArmor, privateArmor, nil } // ImportPrivateKey parses an ASCII-armored private key export (e.g. from `gpg // --export-secret-keys --armor`). If it isn't already passphrase-protected, // passphrase is used to protect it before storing (same posture as generate — never // store an unprotected private key). If it's already protected, passphrase must be // the one that already unlocks it — verified here (by actually unlocking it) so a // wrong passphrase is caught at import time rather than silently producing a // permanently unusable stored key. func ImportPrivateKey(armoredData []byte, passphrase string) (publicArmor, privateArmor []byte, err error) { entity, err := readArmoredEntity(armoredData) if err != nil { return nil, nil, err } if entity.PrivateKey == nil { return nil, nil, errors.New("pgp: no private key found in the uploaded file") } cfg := defaultConfig() if entity.PrivateKey.Encrypted { if err := UnlockPrivateKey(entity, passphrase); err != nil { return nil, nil, fmt.Errorf("pgp: wrong passphrase for the imported key: %w", err) } } // Re-encrypt (or encrypt for the first time) with passphrase — verified live // that decrypting and re-encrypting the same in-memory Entity, then serializing // without signing, round-trips correctly. if err := entity.EncryptPrivateKeys([]byte(passphrase), cfg); err != nil { return nil, nil, fmt.Errorf("pgp: protect private key: %w", err) } if publicArmor, err = serializePublic(entity); err != nil { return nil, nil, err } if privateArmor, err = serializePrivateWithoutSigning(entity, cfg); err != nil { return nil, nil, err } return publicArmor, privateArmor, nil } // ParsePublicKey parses an ASCII-armored public key block — used for a contact's // key, added by hand (PGP has no signature here to auto-capture a contact from the // way S/MIME does). func ParsePublicKey(armoredData []byte) (*openpgp.Entity, error) { return readArmoredEntity(armoredData) } // ParsePrivateKey parses a stored (already passphrase-protected) armored private // key back into an Entity, still locked — call UnlockPrivateKey with the passphrase // before using it to decrypt anything. func ParsePrivateKey(armoredData []byte) (*openpgp.Entity, error) { return readArmoredEntity(armoredData) } // UnlockPrivateKey decrypts entity's primary private key AND every subkey's private // key with passphrase — the actual encryption-capable key lives on a subkey in // modern OpenPGP layout (confirmed via a live round-trip test), so both must be // unlocked before Decrypt can use entity as a recipient key. func UnlockPrivateKey(entity *openpgp.Entity, passphrase string) error { if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { if err := entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil { return fmt.Errorf("pgp: %w", err) } } for _, sk := range entity.Subkeys { if sk.PrivateKey != nil && sk.PrivateKey.Encrypted { if err := sk.PrivateKey.Decrypt([]byte(passphrase)); err != nil { return fmt.Errorf("pgp: %w", err) } } } return nil } // Fingerprint returns entity's primary key fingerprint as uppercase hex, for // display — distinguishing keys beyond just their user-supplied label. func Fingerprint(entity *openpgp.Entity) string { return fmt.Sprintf("%X", entity.PrimaryKey.Fingerprint) } func readArmoredEntity(armoredData []byte) (*openpgp.Entity, error) { block, err := armor.Decode(bytes.NewReader(armoredData)) if err != nil { return nil, fmt.Errorf("pgp: invalid armored data: %w", err) } entity, err := openpgp.ReadEntity(packet.NewReader(block.Body)) if err != nil { return nil, fmt.Errorf("pgp: parse key: %w", err) } return entity, nil } func serializePublic(entity *openpgp.Entity) ([]byte, error) { var buf bytes.Buffer w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) if err != nil { return nil, err } if err := entity.Serialize(w); err != nil { return nil, err } if err := w.Close(); err != nil { return nil, err } return buf.Bytes(), nil } // serializePrivateWithoutSigning uses SerializePrivateWithoutSigning, not // SerializePrivate — the latter re-signs identities/subkeys using the private key // as a crypto.Signer, which panics once the key material is encrypted (confirmed // via a live round-trip test; SerializePrivate is only safe to call before // EncryptPrivateKeys, which isn't a option here since every caller wants the // already-protected key serialized). func serializePrivateWithoutSigning(entity *openpgp.Entity, cfg *packet.Config) ([]byte, error) { var buf bytes.Buffer w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil) if err != nil { return nil, err } if err := entity.SerializePrivateWithoutSigning(w, cfg); err != nil { return nil, err } if err := w.Close(); err != nil { return nil, err } return buf.Bytes(), nil }