Move module identity to our own Forgejo instance. All import paths updated across 434 Go files, sub-module go.mod files, and go.work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
191 lines
5.3 KiB
Go
191 lines
5.3 KiB
Go
package openpgp
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
goio "io"
|
|
"strings"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
|
core "forge.lthn.ai/core/cli/pkg/framework/core"
|
|
)
|
|
|
|
// Service implements the core.Crypt interface using OpenPGP.
|
|
type Service struct {
|
|
core *core.Core
|
|
}
|
|
|
|
// New creates a new OpenPGP service instance.
|
|
func New(c *core.Core) (any, error) {
|
|
return &Service{core: c}, nil
|
|
}
|
|
|
|
// CreateKeyPair generates a new RSA-4096 PGP keypair.
|
|
// Returns the armored private key string.
|
|
func (s *Service) CreateKeyPair(name, passphrase string) (string, error) {
|
|
config := &packet.Config{
|
|
Algorithm: packet.PubKeyAlgoRSA,
|
|
RSABits: 4096,
|
|
DefaultHash: crypto.SHA256,
|
|
DefaultCipher: packet.CipherAES256,
|
|
}
|
|
|
|
entity, err := openpgp.NewEntity(name, "Workspace Key", "", config)
|
|
if err != nil {
|
|
return "", core.E("openpgp.CreateKeyPair", "failed to create entity", err)
|
|
}
|
|
|
|
// Encrypt private key if passphrase is provided
|
|
if passphrase != "" {
|
|
err = entity.PrivateKey.Encrypt([]byte(passphrase))
|
|
if err != nil {
|
|
return "", core.E("openpgp.CreateKeyPair", "failed to encrypt private key", err)
|
|
}
|
|
for _, subkey := range entity.Subkeys {
|
|
err = subkey.PrivateKey.Encrypt([]byte(passphrase))
|
|
if err != nil {
|
|
return "", core.E("openpgp.CreateKeyPair", "failed to encrypt subkey", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil)
|
|
if err != nil {
|
|
return "", core.E("openpgp.CreateKeyPair", "failed to create armor encoder", err)
|
|
}
|
|
|
|
// Manual serialization to avoid panic from re-signing encrypted keys
|
|
err = s.serializeEntity(w, entity)
|
|
if err != nil {
|
|
w.Close()
|
|
return "", core.E("openpgp.CreateKeyPair", "failed to serialize private key", err)
|
|
}
|
|
w.Close()
|
|
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// serializeEntity manually serializes an OpenPGP entity to avoid re-signing.
|
|
func (s *Service) serializeEntity(w goio.Writer, e *openpgp.Entity) error {
|
|
err := e.PrivateKey.Serialize(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, ident := range e.Identities {
|
|
err = ident.UserId.Serialize(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ident.SelfSignature.Serialize(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, subkey := range e.Subkeys {
|
|
err = subkey.PrivateKey.Serialize(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = subkey.Sig.Serialize(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncryptPGP encrypts data for a recipient identified by their public key (armored string in recipientPath).
|
|
// The encrypted data is written to the provided writer and also returned as an armored string.
|
|
func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) {
|
|
entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(recipientPath))
|
|
if err != nil {
|
|
return "", core.E("openpgp.EncryptPGP", "failed to read recipient key", err)
|
|
}
|
|
|
|
var armoredBuf bytes.Buffer
|
|
armoredWriter, err := armor.Encode(&armoredBuf, "PGP MESSAGE", nil)
|
|
if err != nil {
|
|
return "", core.E("openpgp.EncryptPGP", "failed to create armor encoder", err)
|
|
}
|
|
|
|
// MultiWriter to write to both the provided writer and our armored buffer
|
|
mw := goio.MultiWriter(writer, armoredWriter)
|
|
|
|
w, err := openpgp.Encrypt(mw, entityList, nil, nil, nil)
|
|
if err != nil {
|
|
armoredWriter.Close()
|
|
return "", core.E("openpgp.EncryptPGP", "failed to start encryption", err)
|
|
}
|
|
|
|
_, err = goio.WriteString(w, data)
|
|
if err != nil {
|
|
w.Close()
|
|
armoredWriter.Close()
|
|
return "", core.E("openpgp.EncryptPGP", "failed to write data", err)
|
|
}
|
|
|
|
w.Close()
|
|
armoredWriter.Close()
|
|
|
|
return armoredBuf.String(), nil
|
|
}
|
|
|
|
// DecryptPGP decrypts a PGP message using the provided armored private key and passphrase.
|
|
func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) {
|
|
entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(privateKey))
|
|
if err != nil {
|
|
return "", core.E("openpgp.DecryptPGP", "failed to read private key", err)
|
|
}
|
|
|
|
entity := entityList[0]
|
|
if entity.PrivateKey.Encrypted {
|
|
err = entity.PrivateKey.Decrypt([]byte(passphrase))
|
|
if err != nil {
|
|
return "", core.E("openpgp.DecryptPGP", "failed to decrypt private key", err)
|
|
}
|
|
for _, subkey := range entity.Subkeys {
|
|
_ = subkey.PrivateKey.Decrypt([]byte(passphrase))
|
|
}
|
|
}
|
|
|
|
// Decrypt armored message
|
|
block, err := armor.Decode(strings.NewReader(message))
|
|
if err != nil {
|
|
return "", core.E("openpgp.DecryptPGP", "failed to decode armored message", err)
|
|
}
|
|
|
|
md, err := openpgp.ReadMessage(block.Body, entityList, nil, nil)
|
|
if err != nil {
|
|
return "", core.E("openpgp.DecryptPGP", "failed to read message", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
_, err = goio.Copy(&buf, md.UnverifiedBody)
|
|
if err != nil {
|
|
return "", core.E("openpgp.DecryptPGP", "failed to read decrypted body", err)
|
|
}
|
|
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// HandleIPCEvents handles PGP-related IPC messages.
|
|
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
|
switch m := msg.(type) {
|
|
case map[string]any:
|
|
action, _ := m["action"].(string)
|
|
switch action {
|
|
case "openpgp.create_key_pair":
|
|
name, _ := m["name"].(string)
|
|
passphrase, _ := m["passphrase"].(string)
|
|
_, err := s.CreateKeyPair(name, passphrase)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Ensure Service implements core.Crypt.
|
|
var _ core.Crypt = (*Service)(nil)
|