// Package gpgx 提供基于 OpenPGP(RFC 4880 / RFC 9580)的密钥环读取、 // 加解密、签名验签与密钥生成能力。 // // 实现基于 github.com/ProtonMail/go-crypto/openpgp —— 官方推荐的 // golang.org/x/crypto/openpgp 替代品(后者已被 govulncheck 判定为 // GO-2026-5932:unmaintained, unsafe by design, and has known security // issues,且没有修复版本)。 // // 设计约定: // - 所有导出函数都不 panic,全部通过 error 返回失败(用 %w 包装以便 // errors.Is / errors.As 判断)。 // - 所有 io.WriteCloser 都会在写出后 Close,并检查 Close 返回的错误 // (加密流的结尾数据在 Close 时才真正写出,漏检会导致密文不完整)。 // - 需要私钥的操作(解密、签名)在传入 passphrase 时操作密钥环的副本, // 不会修改调用方传入的 openpgp.EntityList。 // - 本包不向 stdout/stderr 输出任何内容。 package gpgx import ( "bytes" "crypto" "errors" "fmt" "io" "os" "time" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" ) // 密钥长度约束。OpenPGP 允许更短的 RSA 密钥,但本包拒绝生成弱密钥。 const ( // MinRSABits 是 GenerateKeyPair 接受的 RSA 最小位数。 MinRSABits = 2048 // DefaultRSABits 是 GenerateKeyPair 的 RSA 默认位数。 DefaultRSABits = 2048 ) // armorHeader 用于快速判断输入是否为 ASCII armor 文本。 const armorHeader = "-----BEGIN PGP" // ---------------------------------------------------------------- 密钥环读取 // ReadKeyRing 从 r 读取 OpenPGP 密钥环,自动识别 armored 与二进制格式。 // // 返回的 EntityList 同时包含公钥与(可能已加密的)私钥材料。 func ReadKeyRing(r io.Reader) (openpgp.EntityList, error) { raw, err := readAll(r) if err != nil { return nil, fmt.Errorf("gpgx: 读取密钥环数据失败: %w", err) } if len(raw) == 0 { return nil, errors.New("gpgx: 密钥环数据为空") } return parseKeyRing(raw) } // ReadArmoredKeyRing 从 r 读取 ASCII armor 编码的 OpenPGP 密钥环。 // // 若输入不是合法的 armor 文本(例如二进制密钥环或垃圾数据),返回 error。 func ReadArmoredKeyRing(r io.Reader) (openpgp.EntityList, error) { raw, err := readAll(r) if err != nil { return nil, fmt.Errorf("gpgx: 读取 armored 密钥环数据失败: %w", err) } if len(raw) == 0 { return nil, errors.New("gpgx: armored 密钥环数据为空") } if !isArmored(raw) { return nil, errors.New("gpgx: 输入不是 ASCII armor 文本(缺少 \"-----BEGIN PGP\" 头)") } entities, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(raw)) if err != nil { return nil, fmt.Errorf("gpgx: 解析 armored 密钥环失败: %w", err) } if len(entities) == 0 { return nil, errors.New("gpgx: armored 密钥环中不包含任何实体") } return entities, nil } // ReadKeyRingFile 从文件读取 OpenPGP 密钥环,自动识别 armored 与二进制格式。 // // 内部使用 os.ReadFile,不会泄漏文件句柄。 func ReadKeyRingFile(path string) (openpgp.EntityList, error) { raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("gpgx: 读取密钥环文件 %q 失败: %w", path, err) } if len(raw) == 0 { return nil, fmt.Errorf("gpgx: 密钥环文件 %q 为空", path) } entities, err := parseKeyRing(raw) if err != nil { return nil, fmt.Errorf("gpgx: 解析密钥环文件 %q 失败: %w", path, err) } return entities, nil } // parseKeyRing 按内容自动选择 armored 或二进制解析。 func parseKeyRing(raw []byte) (openpgp.EntityList, error) { var ( entities openpgp.EntityList err error ) if isArmored(raw) { entities, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(raw)) } else { entities, err = openpgp.ReadKeyRing(bytes.NewReader(raw)) } if err != nil { return nil, fmt.Errorf("解析密钥环失败: %w", err) } if len(entities) == 0 { return nil, errors.New("密钥环中不包含任何实体") } return entities, nil } // isArmored 判断数据是否为 PGP ASCII armor(允许前置空白)。 func isArmored(data []byte) bool { return bytes.HasPrefix(bytes.TrimLeft(data, " \t\r\n"), []byte(armorHeader)) } // maxUnwrappedArmorSize 限制单个 armor 块解出的原始报文大小,避免畸形输入 // 造成无界内存占用。 const maxUnwrappedArmorSize = 64 << 20 // 64 MiB // unwrapArmor 在输入为 ASCII armor 时返回其内部的原始报文字节;否则原样返回。 // // 该函数不校验 armor 块的 Type 字段:调用方(ReadMessage / // CheckDetachedSignature)会对解出的报文本身做严格校验,而 armor 的 CRC // 校验由 crypto/openpgp 的 packet 层与完整性保护(MDC/AEAD)负责。 func unwrapArmor(data []byte) ([]byte, error) { if !isArmored(data) { return data, nil } block, err := armor.Decode(bytes.NewReader(data)) if err != nil { return nil, err } if block == nil { return nil, errors.New("armor 块为空") } raw, err := io.ReadAll(io.LimitReader(block.Body, maxUnwrappedArmorSize+1)) if err != nil { return nil, err } if len(raw) > maxUnwrappedArmorSize { return nil, fmt.Errorf("armor 块解出的报文超过 %d 字节上限", maxUnwrappedArmorSize) } if len(raw) == 0 { return nil, errors.New("armor 块不包含任何数据") } return raw, nil } // ------------------------------------------------------------------ 加密解密 // Encrypt 使用 recipients 中的公钥加密 plaintext,返回二进制 OpenPGP 报文。 func Encrypt(recipients openpgp.EntityList, plaintext []byte) ([]byte, error) { if err := validateRecipients(recipients); err != nil { return nil, err } var buf bytes.Buffer plaintextWriter, err := openpgp.Encrypt(&buf, recipients, nil, nil, nil) if err != nil { return nil, fmt.Errorf("gpgx: 初始化加密流失败: %w", err) } if err := writeAndClose(plaintextWriter, plaintext); err != nil { return nil, fmt.Errorf("gpgx: 加密失败: %w", err) } return buf.Bytes(), nil } // EncryptArmored 使用 recipients 中的公钥加密 plaintext,返回 ASCII armor 报文。 func EncryptArmored(recipients openpgp.EntityList, plaintext []byte) ([]byte, error) { binaryMessage, err := Encrypt(recipients, plaintext) if err != nil { return nil, err } var buf bytes.Buffer armorWriter, err := armor.Encode(&buf, openpgp.MessageType, nil) if err != nil { return nil, fmt.Errorf("gpgx: 初始化 armor 编码器失败: %w", err) } if _, err := armorWriter.Write(binaryMessage); err != nil { _ = armorWriter.Close() return nil, fmt.Errorf("gpgx: 写出 armor 数据失败: %w", err) } if err := armorWriter.Close(); err != nil { return nil, fmt.Errorf("gpgx: 关闭 armor 编码器失败: %w", err) } return buf.Bytes(), nil } // EncryptWithPassphrase 使用口令对称加密 plaintext(等价于 gpg -c), // 返回二进制 OpenPGP 报文;解密端使用 DecryptWithPassphrase。 // // 对称加密同样受完整性保护(MDC/AEAD),不需要密钥环。 func EncryptWithPassphrase(passphrase, plaintext []byte) ([]byte, error) { if len(passphrase) == 0 { return nil, errors.New("gpgx: passphrase 为空,无法进行对称加密") } var buf bytes.Buffer plaintextWriter, err := openpgp.SymmetricallyEncrypt(&buf, passphrase, nil, nil) if err != nil { return nil, fmt.Errorf("gpgx: 初始化对称加密流失败: %w", err) } if err := writeAndClose(plaintextWriter, plaintext); err != nil { return nil, fmt.Errorf("gpgx: 对称加密失败: %w", err) } return buf.Bytes(), nil } // Decrypt 使用 keyring 解密 ciphertext,自动识别 armored 与二进制格式。 // // 若会话密钥被公钥加密,keyring 中必须包含对应私钥;私钥本身可以是 // 未加密的。已加密的私钥请使用 DecryptWithPassphrase。 func Decrypt(keyring openpgp.EntityList, ciphertext []byte) ([]byte, error) { return decryptWith(keyring, ciphertext, nil, nil) } // DecryptWithPassphrase 使用 keyring 与 passphrase 解密 ciphertext。 // // passphrase 用于解开已加密的私钥;也可以直接解密「口令加密」(对称加密, // gpg -c)的报文。若私钥未加密,传入非空 passphrase 同样可以正常工作。 func DecryptWithPassphrase(keyring openpgp.EntityList, ciphertext, passphrase []byte) ([]byte, error) { if len(keyring) == 0 { return nil, errors.New("gpgx: 密钥环为空,无法解密") } // 在副本上解密私钥,避免修改调用方传入的密钥环。 copied := cloneKeyRing(keyring) if err := decryptedPrivateKeys(copied, passphrase); err != nil { return nil, err } var prompt openpgp.PromptFunction if len(passphrase) > 0 { // copied 中的私钥已在上一步解开;此回调用于覆盖「口令加密」报文 // 以及上一步未能解开的私钥,同时避免库无限重试同一口令。 prompt = oneShotPassphrasePrompt(passphrase) } return decryptWith(copied, ciphertext, prompt, nil) } // decryptWith 是解密路径的公共实现。keyring 由调用方保证不会被共享。 func decryptWith(keyring openpgp.EntityList, ciphertext []byte, prompt openpgp.PromptFunction, config *packet.Config) ([]byte, error) { if len(ciphertext) == 0 { return nil, errors.New("gpgx: 密文为空") } if len(keyring) == 0 { return nil, errors.New("gpgx: 密钥环为空,无法解密") } // openpgp.ReadMessage 只接受二进制报文(packet.Reader 不会解析 armor), // 因此 armored 输入需要先取出 armor 内部的原始字节。 binaryMessage, err := unwrapArmor(ciphertext) if err != nil { return nil, fmt.Errorf("gpgx: 解析加密报文失败: %w", err) } md, err := openpgp.ReadMessage(bytes.NewReader(binaryMessage), keyring, prompt, config) if err != nil { return nil, fmt.Errorf("gpgx: 解析加密报文失败: %w", err) } if md.UnverifiedBody == nil { return nil, errors.New("gpgx: 加密报文不包含可读数据(可能不是加密报文)") } // 必须读到 EOF:完整性保护(MDC/AEAD)在 EOF 时才校验,提前返回会 // 跳过校验。 plaintext, err := io.ReadAll(md.UnverifiedBody) if err != nil { return nil, fmt.Errorf("gpgx: 读取或校验明文失败: %w", err) } if md.IsSigned { if md.SignatureError != nil { return nil, fmt.Errorf("gpgx: 报文的签名校验失败: %w", md.SignatureError) } if md.Signature == nil { return nil, errors.New("gpgx: 报文声称已签名,但未找到可验证的签名") } } return plaintext, nil } // ---------------------------------------------------------------- 签名与验签 // Sign 使用 keyring 中的私钥对 plaintext 生成内联签名报文 // (明文内嵌于 PGP 报文内,可直接被 Verify 还原)。 // // 若私钥已加密,必须提供正确的 passphrase;私钥未加密时可传 nil。 func Sign(keyring openpgp.EntityList, plaintext, passphrase []byte) ([]byte, error) { if len(keyring) == 0 { return nil, errors.New("gpgx: 密钥环为空,无法签名") } copied := cloneKeyRing(keyring) if err := decryptedPrivateKeys(copied, passphrase); err != nil { return nil, err } var buf bytes.Buffer plaintextWriter, err := openpgp.Sign(&buf, copied[0], nil, nil) if err != nil { return nil, fmt.Errorf("gpgx: 初始化签名流失败: %w", err) } if err := writeAndClose(plaintextWriter, plaintext); err != nil { return nil, fmt.Errorf("gpgx: 签名失败: %w", err) } return buf.Bytes(), nil } // Verify 验证内联签名报文并返回其中的明文。 // // 返回 error 的情况包括:签名不合法、签名者不在 keyring 中、报文被篡改, // 或报文并非合法的 OpenPGP 签名报文。 func Verify(keyring openpgp.EntityList, signedMessage []byte) ([]byte, error) { if len(signedMessage) == 0 { return nil, errors.New("gpgx: 签名报文为空") } if len(keyring) == 0 { return nil, errors.New("gpgx: 密钥环为空,无法验签") } // openpgp.ReadMessage 只接受二进制报文,armored 输入需要先解出原始字节。 binaryMessage, err := unwrapArmor(signedMessage) if err != nil { return nil, fmt.Errorf("gpgx: 解析签名报文失败: %w", err) } md, err := openpgp.ReadMessage(bytes.NewReader(binaryMessage), cloneKeyRing(keyring), nil, nil) if err != nil { return nil, fmt.Errorf("gpgx: 解析签名报文失败: %w", err) } if md.UnverifiedBody == nil { return nil, errors.New("gpgx: 签名报文不包含可读数据(可能不是签名报文)") } // 签名在读取到 EOF 时才完成校验。 plaintext, err := io.ReadAll(md.UnverifiedBody) if err != nil { return nil, fmt.Errorf("gpgx: 读取签名报文明文失败: %w", err) } if !md.IsSigned { return nil, errors.New("gpgx: 报文未包含签名") } if md.SignatureError != nil { return nil, fmt.Errorf("gpgx: 签名校验失败: %w", md.SignatureError) } if md.Signature == nil { return nil, errors.New("gpgx: 未找到可验证的签名") } return plaintext, nil } // SignDetachedArmored 为 plaintext 生成 ASCII armor 编码的分离签名。 // // 若私钥已加密,必须提供正确的 passphrase;私钥未加密时可传 nil。 func SignDetachedArmored(keyring openpgp.EntityList, plaintext, passphrase []byte) ([]byte, error) { if len(keyring) == 0 { return nil, errors.New("gpgx: 密钥环为空,无法签名") } copied := cloneKeyRing(keyring) if err := decryptedPrivateKeys(copied, passphrase); err != nil { return nil, err } var buf bytes.Buffer if err := openpgp.ArmoredDetachSign(&buf, copied[0], bytes.NewReader(plaintext), nil); err != nil { return nil, fmt.Errorf("gpgx: 生成分离签名失败: %w", err) } return buf.Bytes(), nil } // VerifyDetached 校验分离签名(支持 armored 与二进制签名), // 成功时返回签名者实体。 // // data 或 signature 任一被篡改、或签名者公钥不在 keyring 中,都会返回 error。 func VerifyDetached(keyring openpgp.EntityList, data, signature []byte) (*openpgp.Entity, error) { if len(signature) == 0 { return nil, errors.New("gpgx: 分离签名为空") } if len(keyring) == 0 { return nil, errors.New("gpgx: 密钥环为空,无法验签") } signatureReader := bytes.NewReader(signature) var ( signer *openpgp.Entity err error ) if isArmored(signature) { signer, err = openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(data), signatureReader, nil) } else { signer, err = openpgp.CheckDetachedSignature(keyring, bytes.NewReader(data), signatureReader, nil) } if err != nil { return nil, fmt.Errorf("gpgx: 分离签名校验失败: %w", err) } if signer == nil { return nil, errors.New("gpgx: 分离签名校验未返回签名者") } return signer, nil } // ---------------------------------------------------------------- 密钥生成 // GenerateKeyPair 生成一对 OpenPGP 密钥,返回 ASCII armor 编码的私钥与公钥。 // // bits 的含义: // - bits <= 0:生成 Ed25519 + X25519 密钥(速度快,推荐用于测试与快速上手)。 // - bits > 0:生成 RSA 密钥,位数不得小于 MinRSABits(否则返回 error, // 而不是生成弱密钥)。 // // passphrase 非空时,私钥会被加密保护;为空则私钥为未加密形式。 // name/email/comment 用于构造 User ID,不得包含 "()<>\x00" 等字符。 func GenerateKeyPair(name, email, comment string, passphrase []byte, bits int) (privateArmored, publicArmored []byte, err error) { config := &packet.Config{DefaultHash: defaultHashFor(bits)} switch { case bits <= 0: config.Algorithm = packet.PubKeyAlgoEd25519 case bits < MinRSABits: return nil, nil, fmt.Errorf("gpgx: RSA 位数 %d 过小,至少需要 %d 位", bits, MinRSABits) default: config.Algorithm = packet.PubKeyAlgoRSA config.RSABits = bits config.MinRSABits = uint16(MinRSABits) } entity, err := openpgp.NewEntity(name, comment, email, config) if err != nil { return nil, nil, fmt.Errorf("gpgx: 生成密钥失败: %w", err) } // 注意顺序:Entity.SerializePrivate 会重新签名 User ID 与子密钥绑定, // 而签名需要未加密的私钥(否则签名包内部的私钥类型断言会 panic)。 // 因此必须先把私钥写成明文形式,再加密私钥并重新序列化。 privateArmored, err = armorEntity(openpgp.PrivateKeyType, func(w io.Writer) error { return entity.SerializePrivate(w, config) }) if err != nil { return nil, nil, fmt.Errorf("gpgx: 序列化私钥失败: %w", err) } if len(passphrase) > 0 { if err := entity.EncryptPrivateKeys(passphrase, config); err != nil { return nil, nil, fmt.Errorf("gpgx: 加密私钥失败: %w", err) } // 私钥已加密,不能再触发重签名,使用不重签名的序列化路径。 privateArmored, err = armorEntity(openpgp.PrivateKeyType, func(w io.Writer) error { return entity.SerializePrivateWithoutSigning(w, config) }) if err != nil { return nil, nil, fmt.Errorf("gpgx: 序列化加密私钥失败: %w", err) } } publicArmored, err = armorEntity(openpgp.PublicKeyType, func(w io.Writer) error { return entity.Serialize(w) }) if err != nil { return nil, nil, fmt.Errorf("gpgx: 序列化公钥失败: %w", err) } return privateArmored, publicArmored, nil } // defaultHashFor 依据密钥类型选择哈希:Ed25519 通常搭配 SHA-256,RSA 使用 // SHA-512 以避免与部分实现的自签名哈希偏好冲突。 func defaultHashFor(bits int) crypto.Hash { if bits <= 0 { return crypto.SHA256 } return crypto.SHA512 } // armorEntity 把 serialize 写出的实体内容包装成 ASCII armor 块。 func armorEntity(blockType string, serialize func(io.Writer) error) ([]byte, error) { var buf bytes.Buffer armorWriter, err := armor.Encode(&buf, blockType, nil) if err != nil { return nil, err } if err := serialize(armorWriter); err != nil { _ = armorWriter.Close() return nil, err } if err := armorWriter.Close(); err != nil { return nil, err } return buf.Bytes(), nil } // -------------------------------------------------------------------- 工具 // validateRecipients 校验接收方密钥环至少包含一个可用于加密的公钥。 func validateRecipients(recipients openpgp.EntityList) error { if len(recipients) == 0 { return errors.New("gpgx: 接收方密钥环为空,无法加密") } for _, entity := range recipients { if entity == nil || entity.PrimaryKey == nil { return errors.New("gpgx: 接收方密钥环包含空实体") } if _, ok := entity.EncryptionKey(time.Now()); ok { return nil } } return errors.New("gpgx: 接收方密钥环中没有可用的加密密钥(可能已过期或被吊销)") } // writeAndClose 写入数据并关闭 WriteCloser,两处错误都会返回。 // // 加密/签名流的末尾数据(MDC、签名包)只有 Close 时才写出,因此 Close // 的错误必须检查,否则会静默产生不完整的报文。 func writeAndClose(w io.WriteCloser, data []byte) error { if _, err := w.Write(data); err != nil { // 尽力释放内部资源,但以写入错误为准。 _ = w.Close() return err } if err := w.Close(); err != nil { return err } return nil } // decryptedPrivateKeys 解开 entities 中所有已加密的私钥副本。 // // 若 passphrase 为空而私钥已加密,返回明确错误而不是让上层库反复重试。 func decryptedPrivateKeys(entities openpgp.EntityList, passphrase []byte) error { var encrypted int for _, entity := range entities { if entity == nil { continue } encrypted += countEncryptedPrivateKeys(entity) } if encrypted == 0 { // 私钥未加密:忽略传入的口令。 return nil } if len(passphrase) == 0 { return errors.New("gpgx: 私钥已加密,但未提供 passphrase") } for _, entity := range entities { if entity == nil { continue } if err := entity.DecryptPrivateKeys(passphrase); err != nil { // 不把口令写进错误信息。 return fmt.Errorf("gpgx: 使用给定 passphrase 解开私钥失败: %w", err) } } return nil } // countEncryptedPrivateKeys 统计实体中已加密的私钥数量。 func countEncryptedPrivateKeys(entity *openpgp.Entity) int { var n int if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { n++ } for _, subkey := range entity.Subkeys { if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { n++ } } return n } // oneShotPassphrasePrompt 返回一个最多成功响应一次的口令回调。 // // openpgp.ReadMessage 在口令不正确时会无限重试 prompt,因此这里在第二次 // 调用时直接返回错误,避免死循环。 func oneShotPassphrasePrompt(passphrase []byte) openpgp.PromptFunction { used := false return func(_ []openpgp.Key, _ bool) ([]byte, error) { if used { return nil, errors.New("gpgx: passphrase 不正确(已重试)") } used = true // 返回副本,避免上层库持有调用方的切片。 return append([]byte(nil), passphrase...), nil } } // cloneKeyRing 返回 keyring 的独立副本,用于需要解开私钥的操作。 // // 只有私钥结构会被复制:解密会就地修改 *packet.PrivateKey,复制后调用方 // 传入的密钥环(以及其它并发使用者)不受影响。公钥与签名是只读的,直接共享。 func cloneKeyRing(keyring openpgp.EntityList) openpgp.EntityList { cloned := make(openpgp.EntityList, 0, len(keyring)) for _, entity := range keyring { if entity == nil { continue } cloned = append(cloned, cloneEntity(entity)) } return cloned } // cloneEntity 复制实体中可变的私钥材料。 func cloneEntity(entity *openpgp.Entity) *openpgp.Entity { cloned := *entity cloned.PrivateKey = clonePrivateKey(entity.PrivateKey) if len(entity.Subkeys) > 0 { subkeys := make([]openpgp.Subkey, len(entity.Subkeys)) copy(subkeys, entity.Subkeys) for i := range subkeys { subkeys[i].PrivateKey = clonePrivateKey(subkeys[i].PrivateKey) } cloned.Subkeys = subkeys } return &cloned } // clonePrivateKey 浅拷贝私钥结构。 // // 已加密的私钥清空 PrivateKey 字段,使 *packet.PrivateKey.Decrypt 在副本上 // 重新解析出独立的私钥对象;未加密的私钥保留原有私钥对象指针(只读使用)。 func clonePrivateKey(key *packet.PrivateKey) *packet.PrivateKey { if key == nil { return nil } cloned := *key if key.Encrypted { cloned.PrivateKey = nil } return &cloned } // readAll 读取全部数据,并保证不会因为上游返回 (0, nil) 而死循环。 func readAll(r io.Reader) ([]byte, error) { if r == nil { return nil, errors.New("reader 为 nil") } var buf bytes.Buffer if _, err := io.Copy(&buf, r); err != nil { return nil, err } return buf.Bytes(), nil }