cli/pkg/crypt/openpgp/service.go
Snider c5c4bebd19
Implement Authentication and Authorization Features (#314)
* Implement authentication and authorization features

- Define Workspace and Crypt interfaces in pkg/framework/core/interfaces.go
- Add Workspace() and Crypt() methods to Core in pkg/framework/core/core.go
- Implement PGP service in pkg/crypt/openpgp/service.go using ProtonMail go-crypto
- Implement Workspace service in pkg/workspace/service.go with encrypted directory structure
- Register new services in pkg/cli/app.go
- Add IPC handlers to both services for frontend/CLI communication
- Add unit tests for PGP service in pkg/crypt/openpgp/service_test.go

This implementation aligns the codebase with the features described in the README, providing a foundation for secure, encrypted workspaces and PGP key management.

* Implement authentication and authorization features with fixes

- Define Workspace and Crypt interfaces in pkg/framework/core/interfaces.go
- Add Workspace() and Crypt() methods to Core in pkg/framework/core/core.go
- Implement PGP service in pkg/crypt/openpgp/service.go using ProtonMail go-crypto
- Implement Workspace service in pkg/workspace/service.go with encrypted directory structure
- Register new services in pkg/cli/app.go with proper service names ('crypt', 'workspace')
- Add IPC handlers to both services for frontend/CLI communication
- Add unit tests for PGP and Workspace services
- Fix panic in PGP key serialization by using manual packet serialization
- Fix PGP decryption by adding armor decoding support

This implementation provides the secure, encrypted workspace manager features described in the README.

* Implement authentication and authorization features (Final)

- Define Workspace and Crypt interfaces in pkg/framework/core/interfaces.go
- Add Workspace() and Crypt() methods to Core in pkg/framework/core/core.go
- Implement PGP service in pkg/crypt/openpgp/service.go using ProtonMail go-crypto
- Implement Workspace service in pkg/workspace/service.go with encrypted directory structure
- Register new services in pkg/cli/app.go with proper service names ('crypt', 'workspace')
- Add IPC handlers to both services for frontend/CLI communication
- Add unit tests for PGP and Workspace services
- Fix panic in PGP key serialization by using manual packet serialization
- Fix PGP decryption by adding armor decoding support
- Fix formatting and unused imports

This implementation provides the secure, encrypted workspace manager features described in the README.

* Fix CI failure and implement auth features

- Fix auto-merge workflow by implementing it locally with proper repository context
- Implement Workspace and Crypt interfaces and services
- Add unit tests and IPC handlers for new services
- Fix formatting and unused imports in modified files
- Fix PGP key serialization and decryption issues

---------

Co-authored-by: Claude <developers@lethean.io>
2026-02-05 06:55:50 +00:00

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 "github.com/host-uk/core/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)