// 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/ext" "decl/internal/transport" "decl/internal/data" "decl/internal/folio" "crypto" "encoding/json" "io" "io/fs" "bytes" "os" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/armor" ) var ( ErrSignatureWriterFailed error = errors.New("Failed creating signature writer") ErrArmoredWriterFailed error = errors.New("Failed to create armored writer") ) const ( OpenPGPSignatureTypeName TypeName = "openpgp-signature" ) func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"openpgp-signature"}, func(u *url.URL) (res data.Resource) { o := NewOpenPGPSignature() if u != nil { if err := folio.CastParsedURI(u).ConstructResource(o); err != nil { panic(err) } } return o }) } type OpenPGPSignature struct { *Common `json:",inline" yaml:",inline"` stater machine.Stater `json:"-" yaml:"-"` Signature string `json:"signature,omitempty" yaml:"signature,omitempty"` KeyRingRef folio.ResourceReference `json:"keyringref,omitempty" yaml:"keyringref,omitempty"` SourceRef folio.ResourceReference `json:"soureref,omitempty" yaml:"sourceref,omitempty"` SignatureRef folio.ResourceReference `json:"signatureref,omitempty" yaml:"signatureref,omitempty"` message *openpgp.MessageDetails entityList openpgp.EntityList } func NewOpenPGPSignature() *OpenPGPSignature { o := &OpenPGPSignature { Common: NewCommon(OpenPGPSignatureTypeName, false), } return o } func (o *OpenPGPSignature) Type() string { return "openpgp-signature" } func (o *OpenPGPSignature) 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 *OpenPGPSignature) NormalizePath() error { return nil } func (o *OpenPGPSignature) StateMachine() machine.Stater { if o.stater == nil { o.stater = StorageMachine(o) } return o.stater } func (o *OpenPGPSignature) URI() string { return string(o.Common.URI()) } func (o *OpenPGPSignature) SigningEntity() (err error) { if o.KeyRingRef.IsEmpty() { var keyringConfig any if keyringConfig, err = o.config.GetValue("keyring"); err == nil { o.entityList = keyringConfig.(openpgp.EntityList) } } else { ringFileStream, _ := o.KeyRingRef.Lookup(o.Resources).ContentReaderStream() defer ringFileStream.Close() o.entityList, err = openpgp.ReadArmoredKeyRing(ringFileStream) } return } func (o *OpenPGPSignature) Sign(message io.Reader, w io.Writer) (err error) { var writer io.WriteCloser entity := o.entityList[0] if writer, err = openpgp.Sign(w, entity, nil, o.Config()); err == nil { defer writer.Close() _, err = io.Copy(writer, message) } else { err = fmt.Errorf("%w: %w", ErrSignatureWriterFailed, err) } return } func (o *OpenPGPSignature) Create(ctx context.Context) (err error) { if err = o.SigningEntity(); err == nil { var sourceReadStream io.ReadCloser sourceReadStream, err = o.SourceRef.Lookup(o.Resources).ContentReaderStream() var signatureStream, armoredWriter io.WriteCloser if o.SignatureRef.IsEmpty() { var signatureContent bytes.Buffer signatureStream = ext.WriteNopCloser(&signatureContent) defer func() { o.Signature = signatureContent.String() }() } else { signatureStream, _ = o.SignatureRef.Lookup(o.Resources).ContentWriterStream() } if armoredWriter, err = armor.Encode(signatureStream, openpgp.SignatureType, nil); err != nil { err = fmt.Errorf("%w: %w", ErrArmoredWriterFailed, err) } defer armoredWriter.Close() err = o.Sign(sourceReadStream, armoredWriter) } return } func (o *OpenPGPSignature) Validate() (err error) { var signatureJson []byte if signatureJson, err = o.JSON(); err == nil { s := NewSchema(o.Type()) err = s.Validate(string(signatureJson)) } return err } func (o *OpenPGPSignature) Config() *packet.Config { config := &packet.Config{ RSABits: 2048, Algorithm: packet.PubKeyAlgoRSA, DefaultHash: crypto.SHA256, DefaultCompressionAlgo: packet.CompressionZLIB, } return config } func (o *OpenPGPSignature) Clone() data.Resource { return &OpenPGPSignature { Common: o.Common.Clone(), KeyRingRef: o.KeyRingRef, SourceRef: o.SourceRef, SignatureRef: o.SignatureRef, } } func (o *OpenPGPSignature) 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("OpenPGPSignature.Notify - EXITSTATE", "dest", m.Dest, "common.state", o.Common.State) } } } func (o *OpenPGPSignature) FilePath() string { return o.Common.Path } func (o *OpenPGPSignature) JSON() ([]byte, error) { return json.Marshal(o) } func (o *OpenPGPSignature) 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 *OpenPGPSignature) Load(docData []byte, format codec.Format) (err error) { err = format.StringDecoder(string(docData)).Decode(o) return } func (o *OpenPGPSignature) LoadReader(r io.ReadCloser, format codec.Format) (err error) { err = format.Decoder(r).Decode(o) return } func (o *OpenPGPSignature) LoadString(docData string, format codec.Format) (err error) { err = format.StringDecoder(docData).Decode(o) return } func (o *OpenPGPSignature) LoadDecl(yamlResourceDeclaration string) (err error) { return o.LoadString(yamlResourceDeclaration, codec.FormatYaml) } func (o *OpenPGPSignature) ResolveId(ctx context.Context) string { if e := o.NormalizePath(); e != nil { panic(e) } return o.Common.Path } func (o *OpenPGPSignature) GetContentSourceRef() string { return string(o.SignatureRef) } func (o *OpenPGPSignature) SetContentSourceRef(uri string) { o.SignatureRef = folio.ResourceReference(uri) } func (o *OpenPGPSignature) SignatureRefStat() (info fs.FileInfo, err error) { err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SignatureRef) if len(o.SignatureRef) > 0 { rs, _ := o.ContentReaderStream() defer rs.Close() return rs.Stat() } return } func (o *OpenPGPSignature) Stat() (info fs.FileInfo, err error) { return o.SignatureRefStat() } func (o *OpenPGPSignature) ReadStat() (err error) { _, err = o.SignatureRefStat() return err } func (o *OpenPGPSignature) Update(ctx context.Context) error { return o.Create(ctx) } func (o *OpenPGPSignature) Delete(ctx context.Context) error { return os.Remove(o.Common.Path) } func (o *OpenPGPSignature) Read(ctx context.Context) ([]byte, error) { return yaml.Marshal(o) } func (o *OpenPGPSignature) ContentReaderStream() (*transport.Reader, error) { return nil, nil }