// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "errors" "fmt" "log/slog" "gopkg.in/yaml.v3" "net/url" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" "decl/internal/transport" "decl/internal/data" "decl/internal/folio" "crypto" "encoding/json" "io" "strings" "io/fs" "os" "bytes" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/armor" ) var ( ErrOpenPGPEncryptionFailure error = errors.New("OpenPGP encryption failure") ) const ( OpenPGPKeyRingTypeName TypeName = "openpgp-keyring" ) func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"openpgp-keyring"}, func(u *url.URL) (res data.Resource) { o := NewOpenPGPKeyRing() if u != nil { if err := folio.CastParsedURI(u).ConstructResource(o); err != nil { panic(err) } } return o }) } type OpenPGPKeyRing struct { *Common `json:",inline" yaml:",inline"` stater machine.Stater `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` Email string `json:"email,omitempty" yaml:"email,omitempty"` KeyRing string `json:"keyring,omitempty" yaml:"keyring,omitempty"` Bits int `json:"bits" yaml:"bits"` KeyRingRef folio.ResourceReference `json:"keyringref,omitempty" yaml:"keyringref,omitempty"` entityList openpgp.EntityList } func NewOpenPGPKeyRing() (o *OpenPGPKeyRing) { o = &OpenPGPKeyRing { Common: NewCommon(OpenPGPKeyRingTypeName, false), Bits: 2048, } return } func (o *OpenPGPKeyRing) Init(uri data.URIParser) error { if uri == nil { uri = folio.URI(o.URI()).Parse() } else { // o.Name = uri.URL().Hostname() } return o.SetParsedURI(uri) } func (o *OpenPGPKeyRing) NormalizePath() error { return nil } func (o *OpenPGPKeyRing) Validate() (err error) { var keyringJson []byte if keyringJson, err = o.JSON(); err == nil { s := NewSchema(o.Type()) err = s.Validate(string(keyringJson)) } return err } func (o *OpenPGPKeyRing) Config() *packet.Config { config := &packet.Config{ RSABits: 2048, Algorithm: packet.PubKeyAlgoRSA, DefaultHash: crypto.SHA256, DefaultCompressionAlgo: packet.CompressionZLIB, } return config } func (o *OpenPGPKeyRing) IsEncrypted(index int) (result bool) { if len(o.entityList) >= index { result = o.entityList[index].PrivateKey.Encrypted } return } func (o *OpenPGPKeyRing) EncryptPrivateKey(entity *openpgp.Entity) error { passphraseConfig, _ := o.config.GetValue("passphrase") passphrase := []byte(passphraseConfig.(string)) if len(passphrase) > 0 { if encryptErr := entity.PrivateKey.Encrypt(passphrase); encryptErr != nil { return fmt.Errorf("%w private key: %w", ErrOpenPGPEncryptionFailure, encryptErr) } for _, subkey := range entity.Subkeys { if encryptErr := subkey.PrivateKey.Encrypt(passphrase); encryptErr != nil { return fmt.Errorf("%w subkey (private key): %w", ErrOpenPGPEncryptionFailure, encryptErr) } } } return nil } func (o *OpenPGPKeyRing) Create(ctx context.Context) (err error) { var entity *openpgp.Entity cfg := o.Config() entity, err = openpgp.NewEntity(o.Name, o.Comment, o.Email, cfg) o.entityList = append(o.entityList, entity) if entity.PrivateKey == nil { return fmt.Errorf("Failed creating new private key") } if entity.PrimaryKey == nil { return fmt.Errorf("Failed creating new public key") } if err = o.EncryptPrivateKey(entity); err != nil { return } if len(o.KeyRing) == 0 { var keyringBuffer bytes.Buffer if publicKeyWriter, err := armor.Encode(&keyringBuffer, openpgp.PublicKeyType, nil); err == nil { if err = entity.Serialize(publicKeyWriter); err == nil { } publicKeyWriter.Close() } keyringBuffer.WriteString("\n") if privateKeyWriter, err := armor.Encode(&keyringBuffer, openpgp.PrivateKeyType, nil); err == nil { if err = entity.SerializePrivateWithoutSigning(privateKeyWriter, nil); err == nil { } privateKeyWriter.Close() } keyringBuffer.WriteString("\n") o.KeyRing = keyringBuffer.String() } return } func (o *OpenPGPKeyRing) Clone() data.Resource { return &OpenPGPKeyRing { Common: o.Common.Clone(), Name: o.Name, Comment: o.Comment, Email: o.Email, Bits: o.Bits, } } func (o *OpenPGPKeyRing) StateMachine() machine.Stater { if o.stater == nil { o.stater = StorageMachine(o) } return o.stater } func (o *OpenPGPKeyRing) Notify(m *machine.EventMessage) { ctx := context.Background() switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_stat": if statErr := o.ReadStat(); statErr == nil { if triggerErr := o.StateMachine().Trigger("exists"); triggerErr == nil { return } } else { if triggerErr := o.StateMachine().Trigger("notexists"); triggerErr == nil { return } } case "start_read": if _,readErr := o.Read(ctx); readErr == nil { if triggerErr := o.StateMachine().Trigger("state_read"); triggerErr == nil { return } else { _ = o.AddError(triggerErr) } } else { _ = o.AddError(readErr) if o.IsResourceInconsistent() { if triggerErr := o.StateMachine().Trigger("read-failed"); triggerErr == nil { panic(readErr) } else { _ = o.AddError(triggerErr) panic(fmt.Errorf("%w - %w", readErr, triggerErr)) } } _ = o.AddError(o.StateMachine().Trigger("notexists")) } case "start_create": if createErr := o.Create(ctx); createErr == nil { if triggerErr := o.StateMachine().Trigger("created"); triggerErr == nil { return } else { _ = o.AddError(triggerErr) } } else { _ = o.AddError(createErr) if o.IsResourceInconsistent() { if triggerErr := o.StateMachine().Trigger("create-failed"); triggerErr == nil { panic(createErr) } else { _ = o.AddError(triggerErr) panic(fmt.Errorf("%w - %w", createErr, triggerErr)) } } _ = o.StateMachine().Trigger("notexists") panic(createErr) } case "start_update": if updateErr := o.Update(ctx); updateErr == nil { if triggerErr := o.stater.Trigger("updated"); triggerErr == nil { return } else { _ = o.AddError(triggerErr) } } else { _ = o.AddError(updateErr) if o.IsResourceInconsistent() { if triggerErr := o.StateMachine().Trigger("update-failed"); triggerErr == nil { panic(updateErr) } else { panic(fmt.Errorf("%w - %w", updateErr, triggerErr)) } } _ = o.StateMachine().Trigger("notexists") panic(updateErr) } case "start_delete": if deleteErr := o.Delete(ctx); deleteErr == nil { if triggerErr := o.StateMachine().Trigger("deleted"); triggerErr == nil { return } else { o.Common.State = "present" panic(triggerErr) } } else { _ = o.StateMachine().Trigger("exists") panic(deleteErr) } case "inconsistent": o.Common.State = "inconsistent" case "absent": o.Common.State = "absent" case "present", "created", "read": o.Common.State = "present" } case machine.EXITSTATEEVENT: switch m.Dest { case "start_create": slog.Info("OpenPGP_Entity.Notify - EXITSTATE", "dest", m.Dest, "common.state", o.Common.State) } } } func (o *OpenPGPKeyRing) KeyRingRefStat() (info fs.FileInfo, err error) { if len(o.KeyRingRef) > 0 { rs, _ := o.ContentReaderStream() defer rs.Close() info, err = rs.Stat() } return } func (o *OpenPGPKeyRing) FilePath() string { return o.Common.Path } func (o *OpenPGPKeyRing) JSON() ([]byte, error) { return json.Marshal(o) } func (o *OpenPGPKeyRing) Apply() error { ctx := context.Background() switch o.Common.State { case "absent": return o.Delete(ctx) case "present": return o.Create(ctx) } return nil } func (o *OpenPGPKeyRing) Load(docData []byte, format codec.Format) (err error) { err = format.StringDecoder(string(docData)).Decode(o) return } func (o *OpenPGPKeyRing) LoadReader(r io.ReadCloser, format codec.Format) (err error) { err = format.Decoder(r).Decode(o) return } func (o *OpenPGPKeyRing) LoadString(docData string, format codec.Format) (err error) { err = format.StringDecoder(docData).Decode(o) return } func (o *OpenPGPKeyRing) LoadDecl(yamlResourceDeclaration string) (err error) { return o.LoadString(yamlResourceDeclaration, codec.FormatYaml) } func (o *OpenPGPKeyRing) ResolveId(ctx context.Context) string { if e := o.NormalizePath(); e != nil { panic(e) } return o.Common.Path } func (o *OpenPGPKeyRing) GetContentSourceRef() string { return string(o.KeyRingRef) } func (o *OpenPGPKeyRing) SetContentSourceRef(uri string) { o.KeyRingRef = folio.ResourceReference(uri) } func (o *OpenPGPKeyRing) Stat() (info fs.FileInfo, err error) { return o.KeyRingRefStat() } func (o *OpenPGPKeyRing) Update(ctx context.Context) error { return o.Create(ctx) } func (o *OpenPGPKeyRing) Delete(ctx context.Context) error { return os.Remove(o.Common.Path) } func (o *OpenPGPKeyRing) ReadStat() (err error) { if _, err = o.Stat(); err != nil { o.Common.State = "absent" } return } func (o *OpenPGPKeyRing) Read(ctx context.Context) (yamlData []byte, err error) { var keyringReader io.ReadCloser statErr := o.ReadStat() if statErr != nil { return nil, fmt.Errorf("%w - %w: %s", ErrResourceStateAbsent, statErr, o.Path) } if keyringReader, err = o.GetContent(nil); err == nil { if krData, krErr := io.ReadAll(keyringReader); krErr == nil { o.KeyRing = string(krData) o.entityList, err = openpgp.ReadArmoredKeyRing(strings.NewReader(o.KeyRing)) } else { err = krErr } } o.Common.State = "present" return yaml.Marshal(o) } func (o *OpenPGPKeyRing) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) { contentReader, err = o.readThru() if w != nil { copyBuffer := make([]byte, 32 * 1024) _, writeErr := io.CopyBuffer(w, contentReader, copyBuffer) if writeErr != nil { return nil, fmt.Errorf("OpenPGPKeyRing.GetContent(): CopyBuffer failed %v %v: %w", w, contentReader, writeErr) } return nil, nil } return } func (o *OpenPGPKeyRing) readThru() (contentReader io.ReadCloser, err error) { if o.KeyRingRef.IsEmpty() { if len(o.KeyRing) != 0 { contentReader = io.NopCloser(strings.NewReader(o.KeyRing)) } } else { contentReader, err = o.KeyRingRef.Lookup(nil).ContentReaderStream() contentReader.(*transport.Reader).SetGzip(false) } return } func (o *OpenPGPKeyRing) URI() string { return string(o.Common.URI()) } func (o *OpenPGPKeyRing) Type() string { return "openpgp-keyring" } func (o *OpenPGPKeyRing) ContentReaderStream() (*transport.Reader, error) { if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() { return o.KeyRingRef.Lookup(nil).ContentReaderStream() } return nil, fmt.Errorf("Cannot provide transport reader for string content") } func (o *OpenPGPKeyRing) ContentWriterStream() (*transport.Writer, error) { if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() { return o.KeyRingRef.Lookup(nil).ContentWriterStream() } return nil, fmt.Errorf("Cannot provide transport writer for string content") }