428 lines
11 KiB
Go
428 lines
11 KiB
Go
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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")
|
||
|
}
|