176 lines
6.9 KiB
Go
176 lines
6.9 KiB
Go
// 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
|
|
}
|