diff --git a/cmd/core-gui/main.go b/cmd/core-gui/main.go
index 54c1150..2a1d7b1 100644
--- a/cmd/core-gui/main.go
+++ b/cmd/core-gui/main.go
@@ -4,7 +4,7 @@ import (
"embed"
"log"
- "github.com/Snider/Core/pkg/runtime"
+ "github.com/Snider/Core/runtime"
"github.com/wailsapp/wails/v3/pkg/application"
)
diff --git a/cmd/core/cmd/sync.go b/cmd/core/cmd/sync.go
index 2c9fe24..77e0e27 100644
--- a/cmd/core/cmd/sync.go
+++ b/cmd/core/cmd/sync.go
@@ -101,10 +101,10 @@ package {{.ServiceName}}
import (
// Import the internal implementation with an alias.
- impl "github.com/Snider/Core/pkg/{{.ServiceName}}"
+ impl "github.com/Snider/Core/{{.ServiceName}}"
// Import the core contracts to re-export the interface.
- "github.com/Snider/Core/pkg/core"
+ "github.com/Snider/Core/core"
)
{{range .Symbols}}
diff --git a/cmd/examples/core-static-di/main.go b/cmd/examples/core-static-di/main.go
index 27759f1..137fd18 100644
--- a/cmd/examples/core-static-di/main.go
+++ b/cmd/examples/core-static-di/main.go
@@ -4,7 +4,7 @@ import (
"embed"
"log"
- "github.com/Snider/Core/pkg/runtime"
+ "github.com/Snider/Core/runtime"
"github.com/wailsapp/wails/v3/pkg/application"
)
diff --git a/cmd/examples/core-task-change/main.go b/cmd/examples/core-task-change/main.go
index f15a36d..e08eaf9 100644
--- a/cmd/examples/core-task-change/main.go
+++ b/cmd/examples/core-task-change/main.go
@@ -4,7 +4,7 @@ import (
"embed"
"log"
- "github.com/Snider/Core/pkg/runtime"
+ "github.com/Snider/Core/runtime"
"github.com/wailsapp/wails/v3/pkg/application"
)
diff --git a/pkg/core/actions.go b/core/actions.go
similarity index 100%
rename from pkg/core/actions.go
rename to core/actions.go
diff --git a/pkg/core/core.go b/core/core.go
similarity index 100%
rename from pkg/core/core.go
rename to core/core.go
diff --git a/pkg/core/core_test.go b/core/core_test.go
similarity index 100%
rename from pkg/core/core_test.go
rename to core/core_test.go
diff --git a/pkg/core/interfaces.go b/core/interfaces.go
similarity index 100%
rename from pkg/core/interfaces.go
rename to core/interfaces.go
diff --git a/pkg/core/runtime.go b/core/runtime.go
similarity index 100%
rename from pkg/core/runtime.go
rename to core/runtime.go
diff --git a/pkg/core/testdata/test.txt b/core/testdata/test.txt
similarity index 100%
rename from pkg/core/testdata/test.txt
rename to core/testdata/test.txt
diff --git a/pkg/core/testutil/testutil.go b/core/testutil/testutil.go
similarity index 100%
rename from pkg/core/testutil/testutil.go
rename to core/testutil/testutil.go
diff --git a/docs/index.md b/docs/index.md
index 02dfb7a..096f2d4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -21,7 +21,7 @@ import (
"embed"
"log"
- "github.com/Snider/Core/pkg/runtime"
+ "github.com/Snider/Core/runtime"
"github.com/wailsapp/wails/v3/pkg/application"
)
diff --git a/pkg/e/e.go b/e/e.go
similarity index 100%
rename from pkg/e/e.go
rename to e/e.go
diff --git a/pkg/e/e_test.go b/e/e_test.go
similarity index 100%
rename from pkg/e/e_test.go
rename to e/e_test.go
diff --git a/go.mod b/go.mod
index 99715f1..12b86f5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,17 +3,14 @@ module github.com/Snider/Core
go 1.25
require (
- github.com/ProtonMail/go-crypto v1.3.0
- github.com/pkg/sftp v1.13.10
- github.com/skeema/knownhosts v1.3.2
github.com/stretchr/testify v1.11.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.37
- golang.org/x/crypto v0.43.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
@@ -32,7 +29,6 @@ require (
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
- github.com/kr/fs v0.1.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.1.2 // indirect
@@ -45,9 +41,11 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
+ github.com/skeema/knownhosts v1.3.2 // indirect
github.com/wailsapp/go-webview2 v1.0.22 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
+ golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
diff --git a/go.sum b/go.sum
index 1790463..2ccabe3 100644
--- a/go.sum
+++ b/go.sum
@@ -54,8 +54,6 @@ github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PW
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
-github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -84,8 +82,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
-github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
diff --git a/pkg/crypt/crypt.go b/pkg/crypt/crypt.go
deleted file mode 100644
index 848be61..0000000
--- a/pkg/crypt/crypt.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package crypt
-
-import (
- "github.com/Snider/Core/pkg/core"
- "github.com/Snider/Core/pkg/crypt/internal"
-)
-
-// Options holds configuration for the crypt service.
-type Options = internal.Options
-
-// Service provides cryptographic functions to the application.
-type Service = internal.Service
-
-// HashType defines the supported hashing algorithms.
-type HashType = internal.HashType
-
-const (
- LTHN = internal.LTHN
- SHA512 = internal.SHA512
- SHA256 = internal.SHA256
- SHA1 = internal.SHA1
- MD5 = internal.MD5
-)
-
-// New is the constructor for static dependency injection.
-func New() (*Service, error) {
- return internal.New()
-}
-
-// Register is the constructor for dynamic dependency injection.
-func Register(c *core.Core) (any, error) {
- return internal.Register(c)
-}
diff --git a/pkg/crypt/crypt_test.go b/pkg/crypt/crypt_test.go
deleted file mode 100644
index c02904d..0000000
--- a/pkg/crypt/crypt_test.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package crypt
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestHash(t *testing.T) {
- s, err := New()
- assert.NoError(t, err)
- payload := "hello"
- hash := s.Hash(LTHN, payload)
- assert.NotEmpty(t, hash)
-}
-
-func TestLuhn(t *testing.T) {
- s, err := New()
- assert.NoError(t, err)
- assert.True(t, s.Luhn("79927398713"))
- assert.False(t, s.Luhn("79927398714"))
-}
diff --git a/pkg/crypt/internal/service.go b/pkg/crypt/internal/service.go
deleted file mode 100644
index 66ed1b7..0000000
--- a/pkg/crypt/internal/service.go
+++ /dev/null
@@ -1,181 +0,0 @@
-package internal
-
-import (
- "bytes"
- "crypto/md5"
- "crypto/sha1"
- "crypto/sha256"
- "crypto/sha512"
- "encoding/binary"
- "encoding/hex"
- "io"
- "strconv"
- "strings"
-
- "github.com/Snider/Core/pkg/core"
- "github.com/Snider/Core/pkg/crypt/lthn"
- "github.com/Snider/Core/pkg/crypt/openpgp"
- "github.com/Snider/Core/pkg/e"
-)
-
-// Options holds configuration for the crypt service.
-type Options struct{}
-
-// Service provides cryptographic functions to the application.
-type Service struct {
- *core.Runtime[Options]
-}
-
-// HashType defines the supported hashing algorithms.
-type HashType string
-
-const (
- LTHN HashType = "lthn"
- SHA512 HashType = "sha512"
- SHA256 HashType = "sha256"
- SHA1 HashType = "sha1"
- MD5 HashType = "md5"
-)
-
-// newCryptService contains the common logic for initializing a Service struct.
-func newCryptService() (*Service, error) {
- return &Service{}, nil
-}
-
-// New is the constructor for static dependency injection.
-// It creates a Service instance without initializing the core.Runtime field.
-func New() (*Service, error) {
- return newCryptService()
-}
-
-// Register is the constructor for dynamic dependency injection (used with core.WithService).
-// It creates a Service instance and initializes its core.Runtime field.
-func Register(c *core.Core) (any, error) {
- s, err := newCryptService()
- if err != nil {
- return nil, e.E("crypt.Register", "failed to create new crypt service", err)
- }
- s.Runtime = core.NewRuntime(c, Options{})
- return s, nil
-}
-
-// --- Hashing ---
-
-// Hash computes a hash of the payload using the specified algorithm.
-func (s *Service) Hash(lib HashType, payload string) string {
- switch lib {
- case LTHN:
- return lthn.Hash(payload)
- case SHA512:
- hash := sha512.Sum512([]byte(payload))
- return hex.EncodeToString(hash[:])
- case SHA1:
- hash := sha1.Sum([]byte(payload))
- return hex.EncodeToString(hash[:])
- case MD5:
- hash := md5.Sum([]byte(payload))
- return hex.EncodeToString(hash[:])
- case SHA256:
- fallthrough
- default:
- hash := sha256.Sum256([]byte(payload))
- return hex.EncodeToString(hash[:])
- }
-}
-
-// --- Checksums ---
-
-// Luhn validates a number using the Luhn algorithm.
-func (s *Service) Luhn(payload string) bool {
- payload = strings.ReplaceAll(payload, " ", "")
- sum := 0
- isSecond := false
- for i := len(payload) - 1; i >= 0; i-- {
- digit, err := strconv.Atoi(string(payload[i]))
- if err != nil {
- return false // Contains non-digit
- }
-
- if isSecond {
- digit = digit * 2
- if digit > 9 {
- digit = digit - 9
- }
- }
-
- sum += digit
- isSecond = !isSecond
- }
- return sum%10 == 0
-}
-
-// Fletcher16 computes the Fletcher-16 checksum.
-func (s *Service) Fletcher16(payload string) uint16 {
- data := []byte(payload)
- var sum1, sum2 uint16
- for _, b := range data {
- sum1 = (sum1 + uint16(b)) % 255
- sum2 = (sum2 + sum1) % 255
- }
- return (sum2 << 8) | sum1
-}
-
-// Fletcher32 computes the Fletcher-32 checksum.
-func (s *Service) Fletcher32(payload string) uint32 {
- data := []byte(payload)
- if len(data)%2 != 0 {
- data = append(data, 0)
- }
-
- var sum1, sum2 uint32
- for i := 0; i < len(data); i += 2 {
- val := binary.LittleEndian.Uint16(data[i : i+2])
- sum1 = (sum1 + uint32(val)) % 65535
- sum2 = (sum2 + sum1) % 65535
- }
- return (sum2 << 16) | sum1
-}
-
-// Fletcher64 computes the Fletcher-64 checksum.
-func (s *Service) Fletcher64(payload string) uint64 {
- data := []byte(payload)
- if len(data)%4 != 0 {
- padding := 4 - (len(data) % 4)
- data = append(data, make([]byte, padding)...)
- }
-
- var sum1, sum2 uint64
- for i := 0; i < len(data); i += 4 {
- val := binary.LittleEndian.Uint32(data[i : i+4])
- sum1 = (sum1 + uint64(val)) % 4294967295
- sum2 = (sum2 + sum1) % 4294967295
- }
- return (sum2 << 32) | sum1
-}
-
-// --- PGP ---
-
-// EncryptPGP encrypts data for a recipient, optionally signing it.
-func (s *Service) EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) (string, error) {
- var buf bytes.Buffer
- err := openpgp.EncryptPGP(&buf, recipientPath, data, signerPath, signerPassphrase)
- if err != nil {
- return "", e.E("crypt.EncryptPGP", "failed to encrypt PGP message", err)
- }
-
- // Copy the encrypted data to the original writer.
- if _, err := writer.Write(buf.Bytes()); err != nil {
- return "", e.E("crypt.EncryptPGP", "failed to write encrypted PGP message to writer", err)
- }
-
- return buf.String(), nil
-}
-
-// DecryptPGP decrypts a PGP message, optionally verifying the signature.
-func (s *Service) DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) {
- decrypted, err := openpgp.DecryptPGP(recipientPath, message, passphrase, signerPath)
- if err != nil {
- return "", e.E("crypt.DecryptPGP", "failed to decrypt PGP message", err)
- }
- return decrypted, nil
-}
diff --git a/pkg/crypt/lthn/hash_test.go b/pkg/crypt/lthn/hash_test.go
deleted file mode 100644
index 463ea5d..0000000
--- a/pkg/crypt/lthn/hash_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package lthn
-
-import (
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestHash(t *testing.T) {
- input := "test_string"
- expectedHash := "45d4027179b17265c38732fb1e7089a0b1adfe1d3ba4105fce66f7d46ba42f7d"
-
- hashed := Hash(input)
- fmt.Printf("Hash for \"%s\": %s\n", input, hashed)
-
- assert.Equal(t, expectedHash, hashed, "The hash should match the expected value")
-}
-
-func TestCreateSalt(t *testing.T) {
- // Test with default keyMap
- SetKeyMap(map[rune]rune{})
- assert.Equal(t, "gnirts_tset", createSalt("test_string"))
- assert.Equal(t, "", createSalt(""))
- assert.Equal(t, "A", createSalt("A"))
-
- // Test with a custom keyMap
- customKeyMap := map[rune]rune{
- 'a': 'x',
- 'b': 'y',
- 'c': 'z',
- }
- SetKeyMap(customKeyMap)
- assert.Equal(t, "zyx", createSalt("abc"))
- assert.Equal(t, "gnirts_tset", createSalt("test_string")) // 'test_string' doesn't have 'a', 'b', 'c'
-
- // Reset keyMap to default for other tests
- SetKeyMap(map[rune]rune{})
-}
-
-func TestVerify(t *testing.T) {
- input := "another_test_string"
- hashed := Hash(input)
-
- assert.True(t, Verifyf(input, hashed), "Verifyf should return true for a matching hash")
- assert.False(t, Verifyf(input, "wrong_hash"), "Verifyf should return false for a non-matching hash")
- assert.False(t, Verifyf("different_input", hashed), "Verifyf should return false for different input")
-}
diff --git a/pkg/crypt/lthn/lthn.go b/pkg/crypt/lthn/lthn.go
deleted file mode 100644
index 1b6c97d..0000000
--- a/pkg/crypt/lthn/lthn.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package lthn
-
-import (
- "crypto/sha256"
- "encoding/hex"
-)
-
-// keyMap is the default character-swapping map used for the quasi-salting process.
-var keyMap = map[rune]rune{
- 'o': '0',
- 'l': '1',
- 'e': '3',
- 'a': '4',
- 's': 'z',
- 't': '7',
- '0': 'o',
- '1': 'l',
- '3': 'e',
- '4': 'a',
- '7': 't',
-}
-
-// SetKeyMap sets the key map for the notarisation process.
-func SetKeyMap(newKeyMap map[rune]rune) {
- keyMap = newKeyMap
-}
-
-// GetKeyMap gets the current key map.
-func GetKeyMap() map[rune]rune {
- return keyMap
-}
-
-// Hash creates a reproducible hash from a string.
-func Hash(input string) string {
- salt := createSalt(input)
- hash := sha256.Sum256([]byte(input + salt))
- return hex.EncodeToString(hash[:])
-}
-
-// createSalt creates a quasi-salt from a string by reversing it and swapping characters.
-func createSalt(input string) string {
- if input == "" {
- return ""
- }
- runes := []rune(input)
- salt := make([]rune, len(runes))
- for i := 0; i < len(runes); i++ {
- char := runes[len(runes)-1-i]
- if replacement, ok := keyMap[char]; ok {
- salt[i] = replacement
- } else {
- salt[i] = char
- }
- }
- return string(salt)
-}
-
-// Verify checks if an input string matches a given hash.
-func Verifyf(input string, hash string) bool {
- return Hash(input) == hash
-}
diff --git a/pkg/crypt/openpgp/encrypt.go b/pkg/crypt/openpgp/encrypt.go
deleted file mode 100644
index 04853ed..0000000
--- a/pkg/crypt/openpgp/encrypt.go
+++ /dev/null
@@ -1,233 +0,0 @@
-package openpgp
-
-import (
- "bytes"
- "fmt"
- "io"
- "os"
- "strings"
-
- "github.com/ProtonMail/go-crypto/openpgp"
- "github.com/ProtonMail/go-crypto/openpgp/armor"
- "github.com/ProtonMail/go-crypto/openpgp/packet"
-)
-
-// readRecipientEntity reads an armored PGP public key from the given path.
-func readRecipientEntity(path string) (entity *openpgp.Entity, err error) {
- recipientFile, err := os.Open(path)
- if err != nil {
- return nil, fmt.Errorf("openpgp: failed to open recipient public key file at %s: %w", path, err)
- }
- defer func() {
- if closeErr := recipientFile.Close(); closeErr != nil && err == nil {
- err = fmt.Errorf("openpgp: failed to close recipient key file: %w", closeErr)
- }
- }()
-
- block, err := armor.Decode(recipientFile)
- if err != nil {
- return nil, fmt.Errorf("openpgp: failed to decode armored key from %s: %w", path, err)
- }
-
- if block.Type != openpgp.PublicKeyType {
- return nil, fmt.Errorf("openpgp: invalid key type in %s: expected public key, got %s", path, block.Type)
- }
-
- entity, err = openpgp.ReadEntity(packet.NewReader(block.Body))
- if err != nil {
- return nil, fmt.Errorf("openpgp: failed to read entity from public key: %w", err)
- }
- return entity, nil
-}
-
-// readSignerEntity reads and decrypts an armored PGP private key.
-func readSignerEntity(path, passphrase string) (entity *openpgp.Entity, err error) {
- signerFile, err := os.Open(path)
- if err != nil {
- return nil, fmt.Errorf("openpgp: failed to open signer private key file at %s: %w", path, err)
- }
- defer func() {
- if closeErr := signerFile.Close(); closeErr != nil && err == nil {
- err = fmt.Errorf("openpgp: failed to close signer key file: %w", closeErr)
- }
- }()
-
- block, err := armor.Decode(signerFile)
- if err != nil {
- return nil, fmt.Errorf("openpgp: failed to decode armored key from %s: %w", path, err)
- }
-
- if block.Type != openpgp.PrivateKeyType {
- return nil, fmt.Errorf("openpgp: invalid key type in %s: expected private key, got %s", path, block.Type)
- }
-
- entity, err = openpgp.ReadEntity(packet.NewReader(block.Body))
- if err != nil {
- return nil, fmt.Errorf("openpgp: failed to read entity from private key: %w", err)
- }
-
- // Decrypt the primary private key.
- if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
- if err := entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
- return nil, fmt.Errorf("openpgp: failed to decrypt private key: %w", err)
- }
- }
-
- // Decrypt all subkeys.
- for _, subkey := range entity.Subkeys {
- if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted {
- if err := subkey.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
- return nil, fmt.Errorf("openpgp: failed to decrypt subkey: %w", err)
- }
- }
- }
-
- return entity, nil
-}
-
-// readRecipientKeyRing reads an armored PGP key ring from the given path.
-func readRecipientKeyRing(path string) (entityList openpgp.EntityList, err error) {
- recipientFile, err := os.Open(path)
- if err != nil {
- return nil, fmt.Errorf("openpgp: failed to open recipient key file at %s: %w", path, err)
- }
- defer func() {
- if closeErr := recipientFile.Close(); closeErr != nil && err == nil {
- err = fmt.Errorf("openpgp: failed to close recipient key file: %w", closeErr)
- }
- }()
-
- entityList, err = openpgp.ReadArmoredKeyRing(recipientFile)
- if err != nil {
- return nil, fmt.Errorf("openpgp: failed to read armored key ring from %s: %w", path, err)
- }
- if len(entityList) == 0 {
- return nil, fmt.Errorf("openpgp: no keys found in recipient key file %s", path)
- }
-
- return entityList, nil
-}
-
-// EncryptPGP encrypts a string using PGP, writing the armored, encrypted
-// result to the provided io.Writer.
-func EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) error {
- // 1. Read the recipient's public key
- recipientEntity, err := readRecipientEntity(recipientPath)
- if err != nil {
- return err
- }
-
- // 2. Set up the list of recipients
- to := openpgp.EntityList{recipientEntity}
-
- // 3. Handle optional signing
- var signer *openpgp.Entity
- if signerPath != nil {
- var passphrase string
- if signerPassphrase != nil {
- passphrase = *signerPassphrase
- }
- signer, err = readSignerEntity(*signerPath, passphrase)
- if err != nil {
- return fmt.Errorf("openpgp: failed to prepare signer: %w", err)
- }
- }
-
- // 4. Create an armored writer and encrypt the message
- armoredWriter, err := armor.Encode(writer, "PGP MESSAGE", nil)
- if err != nil {
- return fmt.Errorf("openpgp: failed to create armored writer: %w", err)
- }
-
- plaintext, err := openpgp.Encrypt(armoredWriter, to, signer, nil, nil)
- if err != nil {
- _ = armoredWriter.Close() // Attempt to close, but prioritize the encryption error.
- return fmt.Errorf("openpgp: failed to begin encryption: %w", err)
- }
-
- _, err = plaintext.Write([]byte(data))
- if err != nil {
- _ = plaintext.Close()
- _ = armoredWriter.Close()
- return fmt.Errorf("openpgp: failed to write data to encryption stream: %w", err)
- }
-
- // 5. Explicitly close the writers to finalize the message.
- if err := plaintext.Close(); err != nil {
- return fmt.Errorf("openpgp: failed to finalize plaintext writer: %w", err)
- }
- if err := armoredWriter.Close(); err != nil {
- return fmt.Errorf("openpgp: failed to finalize armored writer: %w", err)
- }
-
- return nil
-}
-
-// DecryptPGP decrypts an armored PGP message.
-func DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) {
- // 1. Read the recipient's private key
- entityList, err := readRecipientKeyRing(recipientPath)
- if err != nil {
- return "", err
- }
-
- // 2. Decode the armored message
- block, err := armor.Decode(strings.NewReader(message))
- if err != nil {
- return "", fmt.Errorf("openpgp: failed to decode armored message: %w", err)
- }
- if block.Type != "PGP MESSAGE" {
- return "", fmt.Errorf("openpgp: invalid message type: got %s, want PGP MESSAGE", block.Type)
- }
-
- // 3. If signature verification is required, add signer's public key to keyring
- var signerEntity *openpgp.Entity
- keyring := entityList
- if signerPath != nil {
- signerEntity, err = readRecipientEntity(*signerPath)
- if err != nil {
- return "", fmt.Errorf("openpgp: failed to read signer public key: %w", err)
- }
- keyring = append(keyring, signerEntity)
- }
-
- // 4. Decrypt the message body
- md, err := openpgp.ReadMessage(block.Body, keyring, func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
- return []byte(passphrase), nil
- }, nil)
- if err != nil {
- return "", fmt.Errorf("openpgp: failed to read PGP message: %w", err)
- }
-
- // Buffer the unverified body. Do not return or act on it until signature checks pass.
- plaintextBuffer := new(bytes.Buffer)
- if _, err := io.Copy(plaintextBuffer, md.UnverifiedBody); err != nil {
- return "", fmt.Errorf("openpgp: failed to buffer plaintext message body: %w", err)
- }
-
- // 5. Handle optional signature verification
- if signerPath != nil {
- // First, ensure a signature actually exists when one is expected.
- if md.SignedByKeyId == 0 {
- return "", fmt.Errorf("openpgp: signature verification failed: message is not signed")
- }
-
- if md.SignatureError != nil {
- return "", fmt.Errorf("openpgp: signature verification failed: %w", md.SignatureError)
- }
- if signerEntity != nil && md.SignedByKeyId != signerEntity.PrimaryKey.KeyId {
- match := false
- for _, subkey := range signerEntity.Subkeys {
- if subkey.PublicKey != nil && subkey.PublicKey.KeyId == md.SignedByKeyId {
- match = true
- break
- }
- }
- if !match {
- return "", fmt.Errorf("openpgp: signature from unexpected key id: got %d, want one of signer key IDs", md.SignedByKeyId)
- }
- }
- }
-
- return plaintextBuffer.String(), nil
-}
diff --git a/pkg/crypt/openpgp/encrypt_extra_test.go b/pkg/crypt/openpgp/encrypt_extra_test.go
deleted file mode 100644
index c0b46bc..0000000
--- a/pkg/crypt/openpgp/encrypt_extra_test.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package openpgp
-
-import (
- "bytes"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-// TestDecryptWithWrongPassphrase checks that DecryptPGP returns an error when the wrong passphrase is used.
-func TestDecryptWithWrongPassphrase(t *testing.T) {
- recipientPub, _, cleanup := generateTestKeys(t, "recipient", "") // Unencrypted key for encryption
- defer cleanup()
-
- // Use the pre-generated encrypted key for decryption test
- encryptedPrivKeyPath, cleanup2 := createEncryptedKeyFile(t)
- defer cleanup2()
-
- originalMessage := "This message should fail to decrypt."
-
- var encryptedBuf bytes.Buffer
- err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, nil, nil)
- assert.NoError(t, err, "Encryption failed unexpectedly")
- encryptedMessage := encryptedBuf.String()
-
- _, err = DecryptPGP(encryptedPrivKeyPath, encryptedMessage, "wrong-passphrase", nil)
- assert.Error(t, err, "Decryption was expected to fail with wrong passphrase, but it succeeded.")
- assert.Contains(t, err.Error(), "failed to read PGP message", "Expected error message about failing to read PGP message")
-}
-
-// TestDecryptMalformedMessage checks that DecryptPGP handles non-PGP or malformed input gracefully.
-func TestDecryptMalformedMessage(t *testing.T) {
- // Generate an unencrypted key for this test, as we expect failure before key usage.
- _, recipientPriv, cleanup := generateTestKeys(t, "recipient", "")
- defer cleanup()
-
- malformedMessage := "This is not a PGP message."
-
- // The passphrase here is irrelevant as the key is not encrypted, but we pass one
- // to satisfy the function signature.
- _, err := DecryptPGP(recipientPriv, malformedMessage, "any-pass", nil)
- assert.Error(t, err, "Decryption should fail for a malformed message, but it did not.")
- assert.Contains(t, err.Error(), "failed to decode armored message", "Expected error about decoding armored message")
-}
-
-// TestEncryptWithNonexistentRecipient checks that EncryptPGP fails when the recipient's public key file does not exist.
-func TestEncryptWithNonexistentRecipient(t *testing.T) {
- var encryptedBuf bytes.Buffer
- err := EncryptPGP(&encryptedBuf, "/path/to/nonexistent/key.pub", "message", nil, nil)
- assert.Error(t, err, "Encryption should fail if recipient key does not exist, but it succeeded.")
- assert.Contains(t, err.Error(), "failed to open recipient public key file", "Expected file open error for recipient key")
-}
-
-// TestEncryptAndSignWithWrongPassphrase checks that signing during encryption fails with an incorrect passphrase.
-func TestEncryptAndSignWithWrongPassphrase(t *testing.T) {
- recipientPub, _, rCleanup := generateTestKeys(t, "recipient", "")
- defer rCleanup()
-
- // Use the pre-generated encrypted key for the signer
- signerPriv, sCleanup := createEncryptedKeyFile(t)
- defer sCleanup()
-
- originalMessage := "This message should fail to sign."
- wrongPassphrase := "wrong-signer-pass"
-
- var encryptedBuf bytes.Buffer
- err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, &signerPriv, &wrongPassphrase)
-
- assert.Error(t, err, "Encryption with signing was expected to fail with a wrong passphrase, but it succeeded.")
- assert.Contains(t, err.Error(), "failed to decrypt private key", "Expected error about private key decryption failure")
-}
diff --git a/pkg/crypt/openpgp/encrypt_test.go b/pkg/crypt/openpgp/encrypt_test.go
deleted file mode 100644
index 5fa7ec8..0000000
--- a/pkg/crypt/openpgp/encrypt_test.go
+++ /dev/null
@@ -1,168 +0,0 @@
-package openpgp
-
-import (
- "bytes"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/ProtonMail/go-crypto/openpgp"
- "github.com/ProtonMail/go-crypto/openpgp/armor"
- "github.com/ProtonMail/go-crypto/openpgp/packet"
-)
-
-// generateTestKeys creates a new PGP entity and saves the public and private keys to temporary files.
-func generateTestKeys(t *testing.T, name, passphrase string) (string, string, func()) {
- t.Helper()
-
- tempDir, err := os.MkdirTemp("", "pgp-keys-*")
- if err != nil {
- t.Fatalf("test setup: failed to create temp dir for keys: %v", err)
- }
-
- config := &packet.Config{
- RSABits: 2048, // Use a reasonable key size for tests
- }
-
- entity, err := openpgp.NewEntity(name, "", name, config)
- if err != nil {
- t.Fatalf("test setup: failed to create new PGP entity: %v", err)
- }
-
- // --- Save Public Key ---
- pubKeyPath := filepath.Join(tempDir, name+".pub")
- pubKeyFile, err := os.Create(pubKeyPath)
- if err != nil {
- t.Fatalf("test setup: failed to create public key file: %v", err)
- }
- pubKeyWriter, err := armor.Encode(pubKeyFile, openpgp.PublicKeyType, nil)
- if err != nil {
- t.Fatalf("test setup: failed to create armored writer for public key: %v", err)
- }
- if err := entity.Serialize(pubKeyWriter); err != nil {
- t.Fatalf("test setup: failed to serialize public key: %v", err)
- }
- if err := pubKeyWriter.Close(); err != nil {
- t.Fatalf("test setup: failed to close public key writer: %v", err)
- }
- if err := pubKeyFile.Close(); err != nil {
- t.Fatalf("test setup: failed to close public key file: %v", err)
- }
-
- // --- Save Private Key (unencrypted for test setup) ---
- privKeyPath := filepath.Join(tempDir, name+".asc")
- privKeyFile, err := os.Create(privKeyPath)
- if err != nil {
- t.Fatalf("test setup: failed to create private key file: %v", err)
- }
- privKeyWriter, err := armor.Encode(privKeyFile, openpgp.PrivateKeyType, nil)
- if err != nil {
- t.Fatalf("test setup: failed to create armored writer for private key: %v", err)
- }
-
- // Serialize the whole entity with an unencrypted private key.
- if err := entity.SerializePrivate(privKeyWriter, nil); err != nil {
- t.Fatalf("test setup: failed to serialize private key: %v", err)
- }
- if err := privKeyWriter.Close(); err != nil {
- t.Fatalf("test setup: failed to close private key writer: %v", err)
- }
- if err := privKeyFile.Close(); err != nil {
- t.Fatalf("test setup: failed to close private key file: %v", err)
- }
-
- cleanup := func() { os.RemoveAll(tempDir) }
- return pubKeyPath, privKeyPath, cleanup
-}
-
-func TestEncryptDecryptPGP(t *testing.T) {
- recipientPub, recipientPriv, cleanup := generateTestKeys(t, "recipient", "recipient-pass")
- defer cleanup()
-
- originalMessage := "This is a secret message."
-
- // --- Test Encryption ---
- var encryptedBuf bytes.Buffer
- err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, nil, nil)
- if err != nil {
- t.Fatalf("EncryptPGP() failed unexpectedly: %v", err)
- }
- encryptedMessage := encryptedBuf.String()
-
- if !strings.Contains(encryptedMessage, "-----BEGIN PGP MESSAGE-----") {
- t.Errorf("Encrypted message does not appear to be PGP armored")
- }
-
- // --- Test Decryption ---
- decryptedMessage, err := DecryptPGP(recipientPriv, encryptedMessage, "recipient-pass", nil)
- if err != nil {
- t.Fatalf("DecryptPGP() failed unexpectedly: %v", err)
- }
-
- if decryptedMessage != originalMessage {
- t.Errorf("Decrypted message mismatch: got=%q, want=%q", decryptedMessage, originalMessage)
- }
-}
-
-func TestSignAndVerifyPGP(t *testing.T) {
- recipientPub, recipientPriv, rCleanup := generateTestKeys(t, "recipient", "recipient-pass")
- defer rCleanup()
-
- signerPub, signerPriv, sCleanup := generateTestKeys(t, "signer", "signer-pass")
- defer sCleanup()
-
- originalMessage := "This is a signed and verified message."
-
- // --- Encrypt and Sign ---
- var encryptedBuf bytes.Buffer
- signerPass := "signer-pass"
- err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, &signerPriv, &signerPass)
- if err != nil {
- t.Fatalf("EncryptPGP() with signing failed unexpectedly: %v", err)
- }
- encryptedMessage := encryptedBuf.String()
-
- // --- Decrypt and Verify ---
- decryptedMessage, err := DecryptPGP(recipientPriv, encryptedMessage, "recipient-pass", &signerPub)
- if err != nil {
- t.Fatalf("DecryptPGP() with verification failed unexpectedly: %v", err)
- }
-
- if decryptedMessage != originalMessage {
- t.Errorf("Decrypted message mismatch after signing: got=%q, want=%q", decryptedMessage, originalMessage)
- }
-}
-
-func TestVerificationFailure(t *testing.T) {
- recipientPub, recipientPriv, rCleanup := generateTestKeys(t, "recipient", "recipient-pass")
- defer rCleanup()
-
- _, signerPriv, sCleanup := generateTestKeys(t, "signer", "signer-pass")
- defer sCleanup()
-
- // Generate a third, unexpected key to test verification failure
- unexpectedSignerPub, _, uCleanup := generateTestKeys(t, "unexpected", "unexpected-pass")
- defer uCleanup()
-
- originalMessage := "This message should fail verification."
-
- // --- Encrypt and Sign with the actual signer key ---
- var encryptedBuf bytes.Buffer
- signerPass := "signer-pass"
- err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, &signerPriv, &signerPass)
- if err != nil {
- t.Fatalf("EncryptPGP() with signing failed unexpectedly: %v", err)
- }
- encryptedMessage := encryptedBuf.String()
-
- // --- Attempt to Decrypt and Verify with the WRONG public key ---
- _, err = DecryptPGP(recipientPriv, encryptedMessage, "recipient-pass", &unexpectedSignerPub)
- if err == nil {
- t.Fatal("DecryptPGP() did not fail, but verification with an incorrect key was expected to fail.")
- }
-
- if !strings.Contains(err.Error(), "signature from unexpected key") {
- t.Errorf("Expected error to contain 'signature from unexpected key', but got: %v", err)
- }
-}
diff --git a/pkg/crypt/openpgp/key.go b/pkg/crypt/openpgp/key.go
deleted file mode 100644
index 2a15ad8..0000000
--- a/pkg/crypt/openpgp/key.go
+++ /dev/null
@@ -1,225 +0,0 @@
-package openpgp
-
-import (
- "bytes"
- "crypto"
- "fmt"
- "path/filepath"
- "time"
-
- "github.com/ProtonMail/go-crypto/openpgp"
- "github.com/ProtonMail/go-crypto/openpgp/armor"
- "github.com/ProtonMail/go-crypto/openpgp/packet"
- "github.com/Snider/Core/pkg/crypt/lthn"
-)
-
-// CreateKeyPair generates a new OpenPGP key pair.
-// The password parameter is optional. If not provided, the private key will not be encrypted.
-func CreateKeyPair(username string, passwords ...string) (*KeyPair, error) {
- var password string
- if len(passwords) > 0 {
- password = passwords[0]
- }
-
- entity, err := openpgp.NewEntity(username, "Lethean Desktop", "", &packet.Config{
- RSABits: 4096,
- DefaultHash: crypto.SHA256,
- })
- if err != nil {
- return nil, fmt.Errorf("failed to create new entity: %w", err)
- }
-
- // The private key is initially unencrypted after NewEntity.
- // Generate revocation certificate while the private key is unencrypted.
- revocationCert, err := createRevocationCertificate(entity)
- if err != nil {
- revocationCert = "" // Non-critical, proceed without it if it fails
- }
-
- // Encrypt the private key only if a password is provided, after revocation cert generation.
- if password != "" {
- if err := entity.PrivateKey.Encrypt([]byte(password)); err != nil {
- return nil, fmt.Errorf("failed to encrypt private key: %w", err)
- }
- }
-
- publicKey, err := serializeEntity(entity, openpgp.PublicKeyType, "") // Public key doesn't need password
- if err != nil {
- return nil, err
- }
-
- // Private key serialization. The key is already in its final encrypted/unencrypted state.
- privateKey, err := serializeEntity(entity, openpgp.PrivateKeyType, "") // No password needed here for serialization
- if err != nil {
- return nil, err
- }
-
- return &KeyPair{
- PublicKey: publicKey,
- PrivateKey: privateKey,
- RevocationCertificate: revocationCert,
- }, nil
-}
-
-// CreateServerKeyPair creates and stores a key pair for the server in a specific directory.
-func CreateServerKeyPair(keysDir string) error {
- serverKeyPath := filepath.Join(keysDir, "server.lthn.pub")
- // Passphrase is derived from the path itself, consistent with original logic.
- passphrase := lthn.Hash(serverKeyPath)
- return createAndStoreKeyPair("server", passphrase, keysDir)
-}
-
-// GetPublicKey retrieves an armored public key for a given ID.
-func GetPublicKey(path string) (*openpgp.Entity, error) {
- return readEntity(path)
-}
-
-// GetPrivateKey retrieves and decrypts an armored private key.
-func GetPrivateKey(path, passphrase string) (*openpgp.Entity, error) {
- entity, err := readEntity(path)
- if err != nil {
- return nil, err
- }
-
- if entity.PrivateKey == nil {
- return nil, fmt.Errorf("no private key found for path %s", path)
- }
-
- if entity.PrivateKey.Encrypted {
- if err := entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
- return nil, fmt.Errorf("failed to decrypt private key for path %s: %w", path, err)
- }
- }
-
- var primaryIdentity *openpgp.Identity
- for _, identity := range entity.Identities {
- if identity.SelfSignature.IsPrimaryId != nil && *identity.SelfSignature.IsPrimaryId {
- primaryIdentity = identity
- break
- }
- }
- if primaryIdentity == nil {
- for _, identity := range entity.Identities {
- primaryIdentity = identity
- break
- }
- }
-
- if primaryIdentity == nil {
- return nil, fmt.Errorf("key for %s has no identity", path)
- }
-
- if primaryIdentity.SelfSignature.KeyLifetimeSecs != nil {
- if primaryIdentity.SelfSignature.CreationTime.Add(time.Duration(*primaryIdentity.SelfSignature.KeyLifetimeSecs) * time.Second).Before(time.Now()) {
- return nil, fmt.Errorf("key for %s has expired", path)
- }
- }
-
- return entity, nil
-}
-
-// --- Helper Functions ---
-
-func createAndStoreKeyPair(id, password, dir string) error {
- //var keyPair *KeyPair
- var err error
-
- //if password != "" {
- // keyPair, err = CreateKeyPair(id, password)
- //} else {
- // keyPair, err = CreateKeyPair(id)
- //}
-
- if err != nil {
- return fmt.Errorf("failed to create key pair for id %s: %w", id, err)
- }
-
- //if err := io.Local.EnsureDir(dir); err != nil {
- // return fmt.Errorf("failed to ensure key directory exists: %w", err)
- //}
- //
- //files := map[string]string{
- // filepath.Join(dir, fmt.Sprintf("%s.lthn.pub", id)): keyPair.PublicKey,
- // filepath.Join(dir, fmt.Sprintf("%s.lthn.key", id)): keyPair.PrivateKey,
- // filepath.Join(dir, fmt.Sprintf("%s.lthn.rev", id)): keyPair.RevocationCertificate, // Re-enabled
- //}
- //
- //for path, content := range files {
- // if content == "" {
- // continue
- // }
- // if err := io.Local.Write(path, content); err != nil {
- // return fmt.Errorf("failed to write key file %s: %w", path, err)
- // }
- //}
- return nil
-}
-
-func readEntity(path string) (*openpgp.Entity, error) {
- //keyArmored, err := m.Read(path)
- //if err != nil {
- // return nil, fmt.Errorf("failed to read key file %s: %w", path, err)
- //}
-
- //entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(keyArmored))
- //if err != nil {
- // return nil, fmt.Errorf("failed to parse key file %s: %w", path, err)
- //}
- //if len(entityList) == 0 {
- // return nil, fmt.Errorf("no entity found in key file %s", path)
- //}
- //return entityList[0], nil
- return nil, nil
-}
-
-func serializeEntity(entity *openpgp.Entity, keyType string, password string) (string, error) {
- buf := new(bytes.Buffer)
- writer, err := armor.Encode(buf, keyType, nil)
- if err != nil {
- return "", fmt.Errorf("failed to create armor encoder: %w", err)
- }
-
- if keyType == openpgp.PrivateKeyType {
- // Serialize the private key in its current in-memory state.
- // Encryption is handled by CreateKeyPair before this function is called.
- err = entity.SerializePrivateWithoutSigning(writer, nil)
- } else {
- err = entity.Serialize(writer)
- }
-
- if err != nil {
- return "", fmt.Errorf("failed to serialize entity: %w", err)
- }
- if err := writer.Close(); err != nil {
- return "", fmt.Errorf("failed to close armor writer: %w", err)
- }
- return buf.String(), nil
-}
-
-func createRevocationCertificate(entity *openpgp.Entity) (string, error) {
- buf := new(bytes.Buffer)
- writer, err := armor.Encode(buf, openpgp.SignatureType, nil)
- if err != nil {
- return "", fmt.Errorf("failed to create armor encoder for revocation: %w", err)
- }
-
- sig := &packet.Signature{
- SigType: packet.SigTypeKeyRevocation,
- PubKeyAlgo: entity.PrimaryKey.PubKeyAlgo,
- Hash: crypto.SHA256,
- CreationTime: time.Now(),
- IssuerKeyId: &entity.PrimaryKey.KeyId,
- }
-
- // SignKey requires an unencrypted private key.
- if err := sig.SignKey(entity.PrimaryKey, entity.PrivateKey, nil); err != nil {
- return "", fmt.Errorf("failed to sign revocation: %w", err)
- }
- if err := sig.Serialize(writer); err != nil {
- return "", fmt.Errorf("failed to serialize revocation signature: %w", err)
- }
- if err := writer.Close(); err != nil {
- return "", fmt.Errorf("failed to close revocation writer: %w", err)
- }
- return buf.String(), nil
-}
diff --git a/pkg/crypt/openpgp/openpgp.go b/pkg/crypt/openpgp/openpgp.go
deleted file mode 100644
index 1e604a5..0000000
--- a/pkg/crypt/openpgp/openpgp.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package openpgp
-
-// pgpMessageHeader is the standard armor header for PGP messages.
-const pgpMessageHeader = "PGP MESSAGE"
-
-// KeyPair holds the generated armored keys and revocation certificate.
-// This is the primary data structure representing a user's PGP identity within the system.
-type KeyPair struct {
- PublicKey string
- PrivateKey string
- RevocationCertificate string
-}
diff --git a/pkg/crypt/openpgp/sign.go b/pkg/crypt/openpgp/sign.go
deleted file mode 100644
index a853350..0000000
--- a/pkg/crypt/openpgp/sign.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package openpgp
-
-import (
- "bytes"
- "fmt"
- "strings"
-
- "github.com/ProtonMail/go-crypto/openpgp"
-)
-
-// Sign creates a detached signature for the data.
-func Sign(data, privateKeyPath, passphrase string) (string, error) {
- signer, err := GetPrivateKey(privateKeyPath, passphrase)
- if err != nil {
- return "", fmt.Errorf("failed to get private key for signing: %w", err)
- }
-
- buf := new(bytes.Buffer)
- if err := openpgp.ArmoredDetachSign(buf, signer, strings.NewReader(data), nil); err != nil {
- return "", fmt.Errorf("failed to create detached signature: %w", err)
- }
-
- return buf.String(), nil
-}
-
-// Verify checks a detached signature.
-func Verify(data, signature, publicKeyPath string) (bool, error) {
- keyring, err := GetPublicKey(publicKeyPath)
- if err != nil {
- return false, fmt.Errorf("failed to get public key for verification: %w", err)
- }
-
- _, err = openpgp.CheckArmoredDetachedSignature(openpgp.EntityList{keyring}, strings.NewReader(data), strings.NewReader(signature), nil)
- if err != nil {
- return false, fmt.Errorf("signature verification failed: %w", err)
- }
- return true, nil
-}
diff --git a/pkg/crypt/openpgp/test_util.go b/pkg/crypt/openpgp/test_util.go
deleted file mode 100644
index fd239c8..0000000
--- a/pkg/crypt/openpgp/test_util.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package openpgp
-
-import (
- "os"
- "path/filepath"
- "testing"
-)
-
-// encryptedPrivateKey is a pre-generated, armored PGP private key, encrypted with the passphrase "test-passphrase".
-// This key is used in tests where programmatic key generation and encryption is not feasible due to library limitations.
-const encryptedPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
-
-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
-hw==
-=u7WH
------END PGP PRIVATE KEY BLOCK-----`
-
-// createEncryptedKeyFile creates a temporary file containing a pre-generated, encrypted private key.
-// It returns the path to the temporary file and a cleanup function to remove the temporary directory.
-func createEncryptedKeyFile(t *testing.T) (string, func()) {
- t.Helper()
-
- tempDir, err := os.MkdirTemp("", "pgp-test-key-*")
- if err != nil {
- t.Fatalf("test setup: failed to create temp dir for encrypted key: %v", err)
- }
-
- privKeyPath := filepath.Join(tempDir, "encrypted-key.asc")
- err = os.WriteFile(privKeyPath, []byte(encryptedPrivateKey), 0600)
- if err != nil {
- t.Fatalf("test setup: failed to write encrypted key to file: %v", err)
- }
-
- cleanup := func() { os.RemoveAll(tempDir) }
- return privKeyPath, cleanup
-}
diff --git a/pkg/io/client.go b/pkg/io/client.go
deleted file mode 100644
index 85d48fa..0000000
--- a/pkg/io/client.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package io
-
-import (
- "github.com/Snider/Core/pkg/io/sftp"
- "github.com/Snider/Core/pkg/io/webdav"
-)
-
-// NewSFTPMedium creates and returns a new SFTP medium.
-func NewSFTPMedium(cfg sftp.ConnectionConfig) (Medium, error) {
- return sftp.New(cfg)
-}
-
-// NewWebDAVMedium creates and returns a new WebDAV medium.
-func NewWebDAVMedium(cfg webdav.ConnectionConfig) (Medium, error) {
- return webdav.New(cfg)
-}
-
-// Read retrieves the content of a file from the given medium.
-func Read(m Medium, path string) (string, error) {
- return m.Read(path)
-}
-
-// Write saves content to a file on the given medium.
-func Write(m Medium, path, content string) error {
- return m.Write(path, content)
-}
-
-// EnsureDir ensures a directory exists on the given medium.
-func EnsureDir(m Medium, path string) error {
- return m.EnsureDir(path)
-}
-
-// IsFile checks if a path is a file on the given medium.
-func IsFile(m Medium, path string) bool {
- return m.IsFile(path)
-}
-
-// Copy copies a file from a source medium to a destination medium.
-func Copy(sourceMedium Medium, sourcePath string, destMedium Medium, destPath string) error {
- content, err := sourceMedium.Read(sourcePath)
- if err != nil {
- return err
- }
- return destMedium.Write(destPath, content)
-}
diff --git a/pkg/io/client_test.go b/pkg/io/client_test.go
deleted file mode 100644
index ad1f4d9..0000000
--- a/pkg/io/client_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package io
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestRead(t *testing.T) {
- m := NewMockMedium()
- m.Files["test.txt"] = "hello"
- content, err := Read(m, "test.txt")
- assert.NoError(t, err)
- assert.Equal(t, "hello", content)
-}
-
-func TestWrite(t *testing.T) {
- m := NewMockMedium()
- err := Write(m, "test.txt", "hello")
- assert.NoError(t, err)
- assert.Equal(t, "hello", m.Files["test.txt"])
-}
-
-func TestCopy(t *testing.T) {
- source := NewMockMedium()
- dest := NewMockMedium()
- source.Files["test.txt"] = "hello"
- err := Copy(source, "test.txt", dest, "test.txt")
- assert.NoError(t, err)
- assert.Equal(t, "hello", dest.Files["test.txt"])
-}
diff --git a/pkg/io/io.go b/pkg/io/io.go
deleted file mode 100644
index df02342..0000000
--- a/pkg/io/io.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package io
-
-// Medium defines the standard interface for a storage backend.
-// This allows for different implementations (e.g., local disk, S3, SFTP)
-// to be used interchangeably.
-type Medium interface {
- // Read retrieves the content of a file as a string.
- Read(path string) (string, error)
-
- // Write saves the given content to a file, overwriting it if it exists.
- Write(path, content string) error
-
- // EnsureDir makes sure a directory exists, creating it if necessary.
- EnsureDir(path string) error
-
- // IsFile checks if a path exists and is a regular file.
- IsFile(path string) bool
-
- // FileGet is a convenience function that reads a file from the medium.
- FileGet(path string) (string, error)
-
- // FileSet is a convenience function that writes a file to the medium.
- FileSet(path, content string) error
-}
-
-// Pre-initialized, sandboxed medium for the local filesystem.
-var Local Medium
diff --git a/pkg/io/io_test.go b/pkg/io/io_test.go
deleted file mode 100644
index aad8db1..0000000
--- a/pkg/io/io_test.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package io
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestIO_Read_Good(t *testing.T) {
- medium := NewMockMedium()
- medium.Files["test.txt"] = "hello"
-
- content, err := Read(medium, "test.txt")
- assert.NoError(t, err)
- assert.Equal(t, "hello", content)
-}
-
-func TestIO_Read_Bad(t *testing.T) {
- medium := NewMockMedium()
-
- _, err := Read(medium, "nonexistent.txt")
- assert.Error(t, err)
-}
-
-func TestIO_Write_Good(t *testing.T) {
- medium := NewMockMedium()
-
- err := Write(medium, "test.txt", "hello")
- assert.NoError(t, err)
-
- writtenContent, ok := medium.Files["test.txt"]
- assert.True(t, ok)
- assert.Equal(t, "hello", writtenContent)
-}
-
-// TODO: The current MockMedium cannot simulate a write error.
-// func TestIO_Write_Bad(t *testing.T) {
-// medium := NewMockMedium()
-// // How to make Write fail?
-// err := Write(medium, "test.txt", "hello")
-// assert.Error(t, err)
-// }
-
-func TestIO_EnsureDir_Good(t *testing.T) {
- medium := NewMockMedium()
- err := EnsureDir(medium, "testdir")
- assert.NoError(t, err)
- exists := medium.Dirs["testdir"]
- assert.True(t, exists)
-}
-
-// TODO: The current MockMedium cannot simulate an EnsureDir error.
-// func TestIO_EnsureDir_Bad(t *testing.T) {
-// medium := NewMockMedium()
-// // How to make EnsureDir fail?
-// err := EnsureDir(medium, "testdir")
-// assert.Error(t, err)
-// }
-
-func TestIO_IsFile_Good(t *testing.T) {
- medium := NewMockMedium()
- medium.Files["test.txt"] = "content"
- assert.True(t, IsFile(medium, "test.txt"))
- assert.False(t, IsFile(medium, "nonexistent.txt"))
-}
-
-func TestIO_Copy_Good(t *testing.T) {
- source := NewMockMedium()
- source.Files["source.txt"] = "hello"
-
- dest := NewMockMedium()
-
- err := Copy(source, "source.txt", dest, "dest.txt")
- assert.NoError(t, err)
-
- copiedContent, ok := dest.Files["dest.txt"]
- assert.True(t, ok)
- assert.Equal(t, "hello", copiedContent)
-}
-
-func TestIO_Copy_Bad(t *testing.T) {
- source := NewMockMedium() // No source file
- dest := NewMockMedium()
-
- err := Copy(source, "source.txt", dest, "dest.txt")
- assert.Error(t, err)
-}
diff --git a/pkg/io/local/client.go b/pkg/io/local/client.go
deleted file mode 100644
index 0efe171..0000000
--- a/pkg/io/local/client.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package local
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
-)
-
-// New creates a new instance of the local storage medium.
-// It requires a root path to sandbox all file operations.
-func New(rootPath string) (*Medium, error) {
- if err := os.MkdirAll(rootPath, os.ModePerm); err != nil {
- return nil, fmt.Errorf("could not create root directory at %s: %w", rootPath, err)
- }
- return &Medium{root: rootPath}, nil
-}
-
-// path returns a full, safe path within the medium's root.
-func (m *Medium) path(subpath string) (string, error) {
- if strings.Contains(subpath, "..") {
- return "", fmt.Errorf("path traversal attempt detected")
- }
- return filepath.Join(m.root, subpath), nil
-}
-
-// Read retrieves the content of a file from the local disk.
-func (m *Medium) Read(path string) (string, error) {
- safePath, err := m.path(path)
- if err != nil {
- return "", err
- }
- data, err := os.ReadFile(safePath)
- if err != nil {
- return "", err
- }
- return string(data), nil
-}
-
-// Write saves the given content to a file on the local disk.
-func (m *Medium) Write(path, content string) error {
- safePath, err := m.path(path)
- if err != nil {
- return err
- }
- dir := filepath.Dir(safePath)
- if err := os.MkdirAll(dir, os.ModePerm); err != nil {
- return err
- }
- return os.WriteFile(safePath, []byte(content), 0644)
-}
-
-// EnsureDir makes sure a directory exists on the local disk.
-func (m *Medium) EnsureDir(path string) error {
- safePath, err := m.path(path)
- if err != nil {
- return err
- }
- return os.MkdirAll(safePath, os.ModePerm)
-}
-
-// IsFile checks if a path exists and is a regular file on the local disk.
-func (m *Medium) IsFile(path string) bool {
- safePath, err := m.path(path)
- if err != nil {
- return false
- }
- info, err := os.Stat(safePath)
- if os.IsNotExist(err) {
- return false
- }
- return !info.IsDir()
-}
-
-// FileGet is a convenience function that reads a file from the medium.
-func (m *Medium) FileGet(path string) (string, error) {
- return m.Read(path)
-}
-
-// FileSet is a convenience function that writes a file to the medium.
-func (m *Medium) FileSet(path, content string) error {
- return m.Write(path, content)
-}
diff --git a/pkg/io/local/client_test.go b/pkg/io/local/client_test.go
deleted file mode 100644
index ff3dce7..0000000
--- a/pkg/io/local/client_test.go
+++ /dev/null
@@ -1,154 +0,0 @@
-package local
-
-import (
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestNew(t *testing.T) {
- // Create a temporary directory for testing
- testRoot, err := os.MkdirTemp("", "local_test_root")
- assert.NoError(t, err)
- defer os.RemoveAll(testRoot) // Clean up after the test
-
- // Test successful creation
- medium, err := New(testRoot)
- assert.NoError(t, err)
- assert.NotNil(t, medium)
- assert.Equal(t, testRoot, medium.root)
-
- // Verify the root directory exists
- info, err := os.Stat(testRoot)
- assert.NoError(t, err)
- assert.True(t, info.IsDir())
-
- // Test creating a new instance with an existing directory (should not error)
- medium2, err := New(testRoot)
- assert.NoError(t, err)
- assert.NotNil(t, medium2)
-}
-
-func TestPath(t *testing.T) {
- testRoot := "/tmp/test_root"
- medium := &Medium{root: testRoot}
-
- // Valid path
- validPath, err := medium.path("file.txt")
- assert.NoError(t, err)
- assert.Equal(t, filepath.Join(testRoot, "file.txt"), validPath)
-
- // Subdirectory path
- subDirPath, err := medium.path("dir/sub/file.txt")
- assert.NoError(t, err)
- assert.Equal(t, filepath.Join(testRoot, "dir", "sub", "file.txt"), subDirPath)
-
- // Path traversal attempt
- _, err = medium.path("../secret.txt")
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "path traversal attempt detected")
-
- _, err = medium.path("dir/../../secret.txt")
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "path traversal attempt detected")
-}
-
-func TestReadWrite(t *testing.T) {
- testRoot, err := os.MkdirTemp("", "local_read_write_test")
- assert.NoError(t, err)
- defer os.RemoveAll(testRoot)
-
- medium, err := New(testRoot)
- assert.NoError(t, err)
-
- fileName := "testfile.txt"
- filePath := filepath.Join("subdir", fileName)
- content := "Hello, Gopher!\nThis is a test file."
-
- // Test Write
- err = medium.Write(filePath, content)
- assert.NoError(t, err)
-
- // Verify file content by reading directly from OS
- readContent, err := os.ReadFile(filepath.Join(testRoot, filePath))
- assert.NoError(t, err)
- assert.Equal(t, content, string(readContent))
-
- // Test Read
- readByMedium, err := medium.Read(filePath)
- assert.NoError(t, err)
- assert.Equal(t, content, readByMedium)
-
- // Test Read non-existent file
- _, err = medium.Read("nonexistent.txt")
- assert.Error(t, err)
- assert.True(t, os.IsNotExist(err))
-
- // Test Write to a path with traversal attempt
- writeErr := medium.Write("../badfile.txt", "malicious content")
- assert.Error(t, writeErr)
- assert.Contains(t, writeErr.Error(), "path traversal attempt detected")
-}
-
-func TestEnsureDir(t *testing.T) {
- testRoot, err := os.MkdirTemp("", "local_ensure_dir_test")
- assert.NoError(t, err)
- defer os.RemoveAll(testRoot)
-
- medium, err := New(testRoot)
- assert.NoError(t, err)
-
- dirName := "newdir/subdir"
- dirPath := filepath.Join(testRoot, dirName)
-
- // Test creating a new directory
- err = medium.EnsureDir(dirName)
- assert.NoError(t, err)
- info, err := os.Stat(dirPath)
- assert.NoError(t, err)
- assert.True(t, info.IsDir())
-
- // Test ensuring an existing directory (should not error)
- err = medium.EnsureDir(dirName)
- assert.NoError(t, err)
-
- // Test ensuring a directory with path traversal attempt
- err = medium.EnsureDir("../bad_dir")
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "path traversal attempt detected")
-}
-
-func TestIsFile(t *testing.T) {
- testRoot, err := os.MkdirTemp("", "local_is_file_test")
- assert.NoError(t, err)
- defer os.RemoveAll(testRoot)
-
- medium, err := New(testRoot)
- assert.NoError(t, err)
-
- // Create a test file
- fileName := "existing_file.txt"
- filePath := filepath.Join(testRoot, fileName)
- err = os.WriteFile(filePath, []byte("content"), 0644)
- assert.NoError(t, err)
-
- // Create a test directory
- dirName := "existing_dir"
- dirPath := filepath.Join(testRoot, dirName)
- err = os.Mkdir(dirPath, 0755)
- assert.NoError(t, err)
-
- // Test with an existing file
- assert.True(t, medium.IsFile(fileName))
-
- // Test with a non-existent file
- assert.False(t, medium.IsFile("nonexistent_file.txt"))
-
- // Test with a directory
- assert.False(t, medium.IsFile(dirName))
-
- // Test with path traversal attempt
- assert.False(t, medium.IsFile("../bad_file.txt"))
-}
diff --git a/pkg/io/local/local.go b/pkg/io/local/local.go
deleted file mode 100644
index f833975..0000000
--- a/pkg/io/local/local.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package local
-
-// Medium implements the io.Medium interface for the local disk.
-type Medium struct {
- root string
-}
diff --git a/pkg/io/mock.go b/pkg/io/mock.go
deleted file mode 100644
index bb0da8e..0000000
--- a/pkg/io/mock.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package io
-
-import "github.com/stretchr/testify/assert"
-
-// MockMedium implements the Medium interface for testing purposes.
-type MockMedium struct {
- Files map[string]string
- Dirs map[string]bool
-}
-
-func NewMockMedium() *MockMedium {
- return &MockMedium{
- Files: make(map[string]string),
- Dirs: make(map[string]bool),
- }
-}
-
-func (m *MockMedium) Read(path string) (string, error) {
- content, ok := m.Files[path]
- if !ok {
- return "", assert.AnError // Simulate file not found error
- }
- return content, nil
-}
-
-func (m *MockMedium) Write(path, content string) error {
- m.Files[path] = content
- return nil
-}
-
-func (m *MockMedium) EnsureDir(path string) error {
- m.Dirs[path] = true
- return nil
-}
-
-func (m *MockMedium) IsFile(path string) bool {
- _, ok := m.Files[path]
- return ok
-}
-
-func (m *MockMedium) FileGet(path string) (string, error) {
- return m.Read(path)
-}
-
-func (m *MockMedium) FileSet(path, content string) error {
- return m.Write(path, content)
-}
diff --git a/pkg/io/sftp/client.go b/pkg/io/sftp/client.go
deleted file mode 100644
index 271e6a2..0000000
--- a/pkg/io/sftp/client.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package sftp
-
-import (
- "fmt"
- "io"
- "net"
- "os"
- "path/filepath"
- "strconv"
- "time"
-
- "github.com/pkg/sftp"
- "github.com/skeema/knownhosts"
- "golang.org/x/crypto/ssh"
-)
-
-// New creates a new, connected instance of the SFTP storage medium.
-func New(cfg ConnectionConfig) (*Medium, error) {
- // Validate port
- port, err := strconv.Atoi(cfg.Port)
- if err != nil || port < 1 || port > 65535 {
- return nil, fmt.Errorf("invalid port: %s", cfg.Port)
- }
-
- var authMethods []ssh.AuthMethod
-
- if cfg.KeyFile != "" {
- key, err := os.ReadFile(cfg.KeyFile)
- if err != nil {
- return nil, fmt.Errorf("unable to read private key: %w", err)
- }
- signer, err := ssh.ParsePrivateKey(key)
- if err != nil {
- return nil, fmt.Errorf("unable to parse private key: %w", err)
- }
- authMethods = append(authMethods, ssh.PublicKeys(signer))
- } else if cfg.Password != "" {
- authMethods = append(authMethods, ssh.Password(cfg.Password))
- } else {
- return nil, fmt.Errorf("no authentication method provided (password or keyfile)")
- }
-
- kh, err := knownhosts.New(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
- if err != nil {
- return nil, fmt.Errorf("failed to read known_hosts: %w", err)
- }
-
- // Set a default timeout if one is not provided.
- if cfg.Timeout == 0 {
- cfg.Timeout = 30 * time.Second
- }
-
- sshConfig := &ssh.ClientConfig{
- User: cfg.User,
- Auth: authMethods,
- HostKeyCallback: kh.HostKeyCallback(),
- Timeout: cfg.Timeout,
- }
-
- addr := net.JoinHostPort(cfg.Host, cfg.Port)
- conn, err := ssh.Dial("tcp", addr, sshConfig)
- if err != nil {
- return nil, fmt.Errorf("failed to dial ssh: %w", err)
- }
-
- sftpClient, err := sftp.NewClient(conn)
- if err != nil {
- // Ensure the underlying ssh connection is closed on failure
- conn.Close()
- return nil, fmt.Errorf("failed to create sftp client: %w", err)
- }
-
- return &Medium{client: sftpClient}, nil
-}
-
-// Read retrieves the content of a file from the SFTP server.
-func (m *Medium) Read(path string) (string, error) {
- file, err := m.client.Open(path)
- if err != nil {
- return "", fmt.Errorf("sftp: failed to open file %s: %w", path, err)
- }
- defer file.Close()
-
- data, err := io.ReadAll(file)
- if err != nil {
- return "", fmt.Errorf("sftp: failed to read file %s: %w", path, err)
- }
-
- return string(data), nil
-}
-
-// Write saves the given content to a file on the SFTP server.
-func (m *Medium) Write(path, content string) error {
- // Ensure the remote directory exists first.
- dir := filepath.Dir(path)
- if err := m.EnsureDir(dir); err != nil {
- return err
- }
-
- file, err := m.client.Create(path)
- if err != nil {
- return fmt.Errorf("sftp: failed to create file %s: %w", path, err)
- }
- defer file.Close()
-
- if _, err := file.Write([]byte(content)); err != nil {
- return fmt.Errorf("sftp: failed to write to file %s: %w", path, err)
- }
-
- return nil
-}
-
-// EnsureDir makes sure a directory exists on the SFTP server.
-func (m *Medium) EnsureDir(path string) error {
- // MkdirAll is idempotent, so it won't error if the path already exists.
- return m.client.MkdirAll(path)
-}
-
-// IsFile checks if a path exists and is a regular file on the SFTP server.
-func (m *Medium) IsFile(path string) bool {
- info, err := m.client.Stat(path)
- if err != nil {
- // If the error is "not found", it's definitely not a file.
- // For any other error, we also conservatively say it's not a file.
- return false
- }
- // Return true only if it's not a directory.
- return !info.IsDir()
-}
-
-// FileGet is a convenience function that reads a file from the medium.
-func (m *Medium) FileGet(path string) (string, error) {
- return m.Read(path)
-}
-
-// FileSet is a convenience function that writes a file to the medium.
-func (m *Medium) FileSet(path, content string) error {
- return m.Write(path, content)
-}
diff --git a/pkg/io/sftp/sftp.go b/pkg/io/sftp/sftp.go
deleted file mode 100644
index 2c4a629..0000000
--- a/pkg/io/sftp/sftp.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package sftp
-
-import (
- "time"
-
- "github.com/pkg/sftp"
-)
-
-// Medium implements the io.Medium interface for the SFTP protocol.
-type Medium struct {
- client *sftp.Client
-}
-
-// ConnectionConfig holds the necessary details to connect to an SFTP server.
-type ConnectionConfig struct {
- Host string
- Port string
- User string
- Password string // For password-based auth
- KeyFile string // Path to a private key for key-based auth
-
- // Timeout specifies the duration for the network connection. If set to 0,
- // a default timeout of 30 seconds will be used.
- Timeout time.Duration
-}
diff --git a/pkg/io/sftp/sftp_test.go b/pkg/io/sftp/sftp_test.go
deleted file mode 100644
index 3c7873d..0000000
--- a/pkg/io/sftp/sftp_test.go
+++ /dev/null
@@ -1,165 +0,0 @@
-package sftp
-
-import (
- "os"
- "path/filepath"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// setupTest creates a temporary home directory and a dummy known_hosts file
-// to prevent tests from failing in CI environments where the file doesn't exist.
-func setupTest(t *testing.T) {
- t.Helper()
- homeDir := t.TempDir()
- t.Setenv("HOME", homeDir)
- sshDir := filepath.Join(homeDir, ".ssh")
- err := os.Mkdir(sshDir, 0700)
- require.NoError(t, err)
- knownHostsFile := filepath.Join(sshDir, "known_hosts")
- err = os.WriteFile(knownHostsFile, []byte{}, 0600)
- require.NoError(t, err)
-}
-
-func TestNew(t *testing.T) {
- setupTest(t)
- // Provide a dummy ConnectionConfig for testing.
- // Since we are not setting up a real SFTP server, we expect an error during connection.
- cfg := ConnectionConfig{
- Host: "localhost",
- Port: "22",
- User: "testuser",
- // No password or keyfile provided, so connection should fail.
- }
-
- service, err := New(cfg)
- assert.Error(t, err)
- assert.Nil(t, service, "New() should return a nil service instance on connection error")
- assert.Contains(t, err.Error(), "no authentication method provided", "Expected authentication error")
-}
-
-func TestNew_InvalidHost(t *testing.T) {
- setupTest(t)
- cfg := ConnectionConfig{
- Host: "non-resolvable-host.domain.invalid",
- Port: "22",
- User: "testuser",
- Password: "password",
- }
-
- service, err := New(cfg)
- assert.Error(t, err)
- assert.Nil(t, service)
- assert.Contains(t, err.Error(), "lookup non-resolvable-host.domain.invalid")
-}
-
-func TestNew_InvalidPort(t *testing.T) {
- setupTest(t)
- cfg := ConnectionConfig{
- Host: "localhost",
- Port: "99999", // Invalid port number
- User: "testuser",
- Password: "password",
- }
-
- service, err := New(cfg)
- assert.Error(t, err)
- assert.Nil(t, service)
- assert.Contains(t, err.Error(), "invalid port")
-}
-
-func TestNew_ConnectionTimeout(t *testing.T) {
- setupTest(t)
- cfg := ConnectionConfig{
- Host: "192.0.2.0", // Non-routable IP to simulate timeout
- Port: "22",
- User: "testuser",
- Password: "password",
- Timeout: 100 * time.Millisecond,
- }
-
- service, err := New(cfg)
- assert.Error(t, err)
- assert.Nil(t, service)
- assert.Contains(t, err.Error(), "i/o timeout")
-}
-
-func TestNew_AuthFailure_NonexistentKeyfile(t *testing.T) {
- setupTest(t)
- cfg := ConnectionConfig{
- Host: "localhost",
- Port: "22",
- User: "testuser",
- KeyFile: "/path/to/nonexistent/keyfile",
- }
-
- service, err := New(cfg)
- assert.Error(t, err)
- assert.Nil(t, service)
- assert.ErrorIs(t, err, os.ErrNotExist)
-}
-
-func TestNew_AuthFailure_InvalidKeyFormat(t *testing.T) {
- setupTest(t)
- // Create a temporary file with invalid key content
- tmpFile, err := os.CreateTemp("", "invalid_key")
- require.NoError(t, err)
- defer func(name string) {
- err := os.Remove(name)
- if err != nil {
- t.Logf("Failed to remove temporary file: %v", err)
- }
- }(tmpFile.Name())
-
- _, err = tmpFile.WriteString("not a valid ssh key")
- require.NoError(t, err)
- err = tmpFile.Close()
- require.NoError(t, err)
-
- cfg := ConnectionConfig{
- Host: "localhost",
- Port: "22",
- User: "testuser",
- KeyFile: tmpFile.Name(),
- }
-
- service, err := New(cfg)
- assert.Error(t, err)
- assert.Nil(t, service)
- assert.Contains(t, err.Error(), "unable to parse private key")
-}
-
-func TestNew_MultipleAuthMethods(t *testing.T) {
- setupTest(t)
- // Create a temporary file with invalid key content to ensure key-based auth is attempted
- tmpFile, err := os.CreateTemp("", "dummy_key")
- require.NoError(t, err)
- defer func(name string) {
- err := os.Remove(name)
- if err != nil {
- t.Logf("Failed to remove temporary file: %v", err)
- }
- }(tmpFile.Name())
-
- _, err = tmpFile.WriteString("not a valid ssh key")
- require.NoError(t, err)
- err = tmpFile.Close()
- require.NoError(t, err)
-
- cfg := ConnectionConfig{
- Host: "localhost",
- Port: "22",
- User: "testuser",
- Password: "password",
- KeyFile: tmpFile.Name(),
- }
-
- service, err := New(cfg)
- assert.Error(t, err)
- assert.Nil(t, service)
- // We expect the key file to be prioritized, so we should get a parse error, not a "no auth method" error.
- assert.Contains(t, err.Error(), "unable to parse private key")
-}
diff --git a/pkg/io/webdav/client.go b/pkg/io/webdav/client.go
deleted file mode 100644
index 1c6aa52..0000000
--- a/pkg/io/webdav/client.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package webdav
-
-import "net/http"
-
-// Medium implements the io.Medium interface for the WebDAV protocol.
-type Medium struct {
- client *http.Client
- baseURL string // e.g., https://dav.example.com/remote.php/dav/files/username/
-}
-
-// ConnectionConfig holds the necessary details to connect to a WebDAV server.
-type ConnectionConfig struct {
- URL string // The full base URL of the WebDAV share.
- User string
- Password string
-}
diff --git a/pkg/io/webdav/webdav.go b/pkg/io/webdav/webdav.go
deleted file mode 100644
index db0ac66..0000000
--- a/pkg/io/webdav/webdav.go
+++ /dev/null
@@ -1,183 +0,0 @@
-package webdav
-
-import (
- "bytes"
- _ "context"
- "fmt"
- "io"
- "net/http"
- "path"
- "strings"
-)
-
-// New creates a new, connected instance of the WebDAV storage medium.
-func New(cfg ConnectionConfig) (*Medium, error) {
- transport := &authTransport{
- Username: cfg.User,
- Password: cfg.Password,
- Wrapped: http.DefaultTransport,
- }
-
- httpClient := &http.Client{Transport: transport}
-
- // Ping the server to ensure the connection and credentials are valid.
- // We do a PROPFIND on the root, which is a standard WebDAV operation.
- req, err := http.NewRequest("PROPFIND", cfg.URL, nil)
- if err != nil {
- return nil, fmt.Errorf("webdav: failed to create ping request: %w", err)
- }
- req.Header.Set("Depth", "0")
- resp, err := httpClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("webdav: connection test failed: %w", err)
- }
- resp.Body.Close()
- if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("webdav: connection test failed with status %s", resp.Status)
- }
-
- return &Medium{
- client: httpClient,
- baseURL: cfg.URL,
- }, nil
-}
-
-// Read retrieves the content of a file from the WebDAV server.
-func (m *Medium) Read(p string) (string, error) {
- url := m.resolveURL(p)
- resp, err := m.client.Get(url)
- if err != nil {
- return "", fmt.Errorf("webdav: GET request for %s failed: %w", p, err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return "", fmt.Errorf("webdav: failed to read %s, status: %s", p, resp.Status)
- }
-
- data, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", fmt.Errorf("webdav: failed to read response body for %s: %w", p, err)
- }
-
- return string(data), nil
-}
-
-// Write saves the given content to a file on the WebDAV server.
-func (m *Medium) Write(p, content string) error {
- // Ensure the parent directory exists first.
- dir := path.Dir(p)
- if dir != "." && dir != "/" {
- if err := m.EnsureDir(dir); err != nil {
- return err // This will be a detailed error from EnsureDir
- }
- }
-
- url := m.resolveURL(p)
- req, err := http.NewRequest("PUT", url, bytes.NewReader([]byte(content)))
- if err != nil {
- return fmt.Errorf("webdav: failed to create PUT request: %w", err)
- }
-
- resp, err := m.client.Do(req)
- if err != nil {
- return fmt.Errorf("webdav: PUT request for %s failed: %w", p, err)
- }
- defer resp.Body.Close()
-
- // StatusCreated (201) or StatusNoContent (204) are success codes for PUT.
- if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
- return fmt.Errorf("webdav: failed to write %s, status: %s", p, resp.Status)
- }
-
- return nil
-}
-
-// EnsureDir makes sure a directory exists on the WebDAV server, creating parent dirs as needed.
-func (m *Medium) EnsureDir(p string) error {
- // To mimic MkdirAll, we create each part of the path sequentially.
- parts := strings.Split(p, "/")
- currentPath := ""
- for _, part := range parts {
- if part == "" {
- continue
- }
- currentPath = path.Join(currentPath, part)
- url := m.resolveURL(currentPath) + "/" // MKCOL needs a trailing slash
-
- req, err := http.NewRequest("MKCOL", url, nil)
- if err != nil {
- return fmt.Errorf("webdav: failed to create MKCOL request for %s: %w", currentPath, err)
- }
-
- resp, err := m.client.Do(req)
- if err != nil {
- return fmt.Errorf("webdav: MKCOL request for %s failed: %w", currentPath, err)
- }
- resp.Body.Close()
-
- // 405 Method Not Allowed means it already exists, which is fine for us.
- // 201 Created is a success.
- if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusMethodNotAllowed {
- return fmt.Errorf("webdav: failed to create directory %s, status: %s", currentPath, resp.Status)
- }
- }
- return nil
-}
-
-// IsFile checks if a path exists and is a regular file on the WebDAV server.
-func (m *Medium) IsFile(p string) bool {
- url := m.resolveURL(p)
- req, err := http.NewRequest("PROPFIND", url, nil)
- if err != nil {
- return false
- }
- req.Header.Set("Depth", "0")
-
- resp, err := m.client.Do(req)
- if err != nil {
- return false
- }
- defer resp.Body.Close()
-
- // If we get anything other than a Multi-Status, it's probably not a file.
- if resp.StatusCode != http.StatusMultiStatus {
- return false
- }
-
- // A simple check: if the response body contains the string for a collection, it's a directory.
- // A more robust implementation would parse the XML response.
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return false
- }
-
- return !strings.Contains(string(body), "")
-}
-
-// resolveURL joins the base URL with a path segment, ensuring correct slashes.
-func (m *Medium) resolveURL(p string) string {
- return strings.TrimSuffix(m.baseURL, "/") + "/" + strings.TrimPrefix(p, "/")
-}
-
-// authTransport is a custom http.RoundTripper to inject Basic Auth.
-type authTransport struct {
- Username string
- Password string
- Wrapped http.RoundTripper
-}
-
-func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- req.SetBasicAuth(t.Username, t.Password)
- return t.Wrapped.RoundTrip(req)
-}
-
-// FileGet is a convenience function that reads a file from the medium.
-func (m *Medium) FileGet(path string) (string, error) {
- return m.Read(path)
-}
-
-// FileSet is a convenience function that writes a file to the medium.
-func (m *Medium) FileSet(path, content string) error {
- return m.Write(path, content)
-}
diff --git a/pkg/io/webdav/webdav_test.go b/pkg/io/webdav/webdav_test.go
deleted file mode 100644
index a0b2602..0000000
--- a/pkg/io/webdav/webdav_test.go
+++ /dev/null
@@ -1,155 +0,0 @@
-package webdav
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-// mockWebDAVServer creates a test HTTP server that mimics a WebDAV server.
-func mockWebDAVServer() *httptest.Server {
- handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case "PROPFIND":
- if r.URL.Path == "/" {
- w.WriteHeader(http.StatusMultiStatus)
- return
- }
- // For IsFile test
- if r.URL.Path == "/test.txt" {
- w.WriteHeader(http.StatusMultiStatus)
- fmt.Fprint(w, `
-
-
- /test.txt
-
-
-
-
- HTTP/1.1 200 OK
-
-
-`)
- return
- }
- if r.URL.Path == "/testdir/" {
- w.WriteHeader(http.StatusMultiStatus)
- fmt.Fprint(w, `
-
-
- /testdir/
-
-
-
-
- HTTP/1.1 200 OK
-
-
-`)
- return
- }
- http.NotFound(w, r)
- case "GET":
- if r.URL.Path == "/test.txt" {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, "Hello, WebDAV!")
- return
- }
- http.NotFound(w, r)
- case "PUT":
- if r.URL.Path == "/test.txt" {
- w.WriteHeader(http.StatusCreated)
- return
- }
- http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
- case "MKCOL":
- if r.URL.Path == "/testdir/" {
- w.WriteHeader(http.StatusCreated)
- return
- }
- http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
- default:
- http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
- }
- })
- return httptest.NewServer(handler)
-}
-
-func TestNew_Success(t *testing.T) {
- server := mockWebDAVServer()
- defer server.Close()
-
- cfg := ConnectionConfig{
- URL: server.URL,
- User: "user",
- Password: "password",
- }
-
- medium, err := New(cfg)
- assert.NoError(t, err)
- assert.NotNil(t, medium)
-}
-
-func TestRead(t *testing.T) {
- server := mockWebDAVServer()
- defer server.Close()
-
- cfg := ConnectionConfig{
- URL: server.URL,
- User: "user",
- Password: "password",
- }
- medium, err := New(cfg)
- assert.NoError(t, err)
- content, err := medium.Read("test.txt")
- assert.NoError(t, err)
- assert.Equal(t, "Hello, WebDAV!", content)
-}
-
-func TestWrite(t *testing.T) {
- server := mockWebDAVServer()
- defer server.Close()
-
- cfg := ConnectionConfig{
- URL: server.URL,
- User: "user",
- Password: "password",
- }
- medium, err := New(cfg)
- assert.NoError(t, err)
- err = medium.Write("test.txt", "Hello, WebDAV!")
- assert.NoError(t, err)
-}
-
-func TestEnsureDir(t *testing.T) {
- server := mockWebDAVServer()
- defer server.Close()
-
- cfg := ConnectionConfig{
- URL: server.URL,
- User: "user",
- Password: "password",
- }
- medium, err := New(cfg)
- assert.NoError(t, err)
- err = medium.EnsureDir("testdir")
- assert.NoError(t, err)
-}
-
-func TestIsFile(t *testing.T) {
- server := mockWebDAVServer()
- defer server.Close()
-
- cfg := ConnectionConfig{
- URL: server.URL,
- User: "user",
- Password: "password",
- }
- medium, err := New(cfg)
- assert.NoError(t, err)
- assert.True(t, medium.IsFile("test.txt"))
- assert.False(t, medium.IsFile("testdir"))
-}
diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go
deleted file mode 100644
index dd4329b..0000000
--- a/pkg/runtime/runtime_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package runtime_test
-
-import (
- "errors"
- "testing"
-
- "github.com/Snider/Core/pkg/crypt"
- "github.com/Snider/Core/pkg/runtime"
- "github.com/Snider/Core/pkg/workspace"
- "github.com/stretchr/testify/assert"
- "github.com/wailsapp/wails/v3/pkg/application"
-)
-
-func TestNew(t *testing.T) {
- testCases := []struct {
- name string
- app *application.App
- factories map[string]runtime.ServiceFactory
- expectErr bool
- expectErrStr string
- checkRuntime func(*testing.T, *runtime.Runtime)
- }{
- {
- name: "Good path",
- app: nil,
- factories: map[string]runtime.ServiceFactory{
- "crypt": func() (any, error) { return &crypt.Service{}, nil },
- "workspace": func() (any, error) { return &workspace.Service{}, nil },
- },
- expectErr: false,
- checkRuntime: func(t *testing.T, rt *runtime.Runtime) {
- assert.NotNil(t, rt)
- assert.NotNil(t, rt.Core)
- assert.NotNil(t, rt.Crypt)
- assert.NotNil(t, rt.Workspace)
- },
- },
- {
- name: "Factory returns an error",
- app: nil,
- factories: map[string]runtime.ServiceFactory{
- "crypt": func() (any, error) { return nil, errors.New("crypt service failed") },
- "workspace": func() (any, error) { return &workspace.Service{}, nil },
- },
- expectErr: true,
- expectErrStr: "failed to create service crypt: crypt service failed",
- },
- {
- name: "Factory returns wrong type",
- app: nil,
- factories: map[string]runtime.ServiceFactory{
- "crypt": func() (any, error) { return "not a crypt service", nil },
- "workspace": func() (any, error) { return &workspace.Service{}, nil },
- },
- expectErr: true,
- expectErrStr: "crypt service has unexpected type",
- },
- {
- name: "With non-nil app",
- app: &application.App{},
- factories: map[string]runtime.ServiceFactory{
- "crypt": func() (any, error) { return &crypt.Service{}, nil },
- "workspace": func() (any, error) { return &workspace.Service{}, nil },
- },
- expectErr: false,
- checkRuntime: func(t *testing.T, rt *runtime.Runtime) {
- assert.NotNil(t, rt)
- assert.NotNil(t, rt.Core)
- assert.NotNil(t, rt.Core.App)
- },
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- rt, err := runtime.NewWithFactories(tc.app, tc.factories)
-
- if tc.expectErr {
- assert.Error(t, err)
- assert.Contains(t, err.Error(), tc.expectErrStr)
- assert.Nil(t, rt)
- } else {
- assert.NoError(t, err)
- if tc.checkRuntime != nil {
- tc.checkRuntime(t, rt)
- }
- }
- })
- }
-}
diff --git a/pkg/workspace/local.go b/pkg/workspace/local.go
deleted file mode 100644
index 27769f7..0000000
--- a/pkg/workspace/local.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package workspace
-
-import "github.com/Snider/Core/pkg/io"
-
-// localMedium implements the Medium interface for the local disk.
-type localMedium struct{}
-
-// NewLocalMedium creates a new instance of the local storage medium.
-func NewLocalMedium() io.Medium {
- return &localMedium{}
-}
-
-// FileGet reads a file from the local disk.
-func (m *localMedium) FileGet(path string) (string, error) {
- return io.Read(io.Local, path)
-}
-
-// FileSet writes a file to the local disk.
-func (m *localMedium) FileSet(path, content string) error {
- return io.Write(io.Local, path, content)
-}
-
-// Read reads a file from the local disk.
-func (m *localMedium) Read(path string) (string, error) {
- return io.Read(io.Local, path)
-}
-
-// Write writes a file to the local disk.
-func (m *localMedium) Write(path, content string) error {
- return io.Write(io.Local, path, content)
-}
-
-// EnsureDir creates a directory on the local disk.
-func (m *localMedium) EnsureDir(path string) error {
- return io.EnsureDir(io.Local, path)
-}
-
-// IsFile checks if a path exists and is a file on the local disk.
-func (m *localMedium) IsFile(path string) bool {
- return io.IsFile(io.Local, path)
-}
diff --git a/pkg/workspace/workspace.go b/pkg/workspace/workspace.go
deleted file mode 100644
index 230db81..0000000
--- a/pkg/workspace/workspace.go
+++ /dev/null
@@ -1,227 +0,0 @@
-package workspace
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "path/filepath"
-
- "github.com/Snider/Core/pkg/core"
- "github.com/Snider/Core/pkg/crypt/lthn"
- "github.com/Snider/Core/pkg/crypt/openpgp"
- "github.com/Snider/Core/pkg/e"
- "github.com/Snider/Core/pkg/io"
- "github.com/wailsapp/wails/v3/pkg/application"
-)
-
-const (
- defaultWorkspace = "default"
- listFile = "list.json"
-)
-
-// Options holds configuration for the workspace service.
-type Options struct{}
-
-// Workspace represents a user's workspace.
-type Workspace struct {
- Name string
- Path string
-}
-
-// Service manages user workspaces.
-type Service struct {
- *core.Runtime[Options]
- activeWorkspace *Workspace
- workspaceList map[string]string // Maps Workspace ID to Public Key
- medium io.Medium
-}
-
-// newWorkspaceService contains the common logic for initializing a Service struct.
-// It no longer takes config and medium as arguments.
-func newWorkspaceService() (*Service, error) {
- s := &Service{
- workspaceList: make(map[string]string),
- }
- return s, nil
-}
-
-// New is the constructor for static dependency injection.
-// It creates a Service instance without initializing the core.Runtime field.
-// Dependencies are passed directly here.
-func New() (*Service, error) {
- s, err := newWorkspaceService()
- if err != nil {
- return nil, e.E("workspace.New", "failed to create new workspace service", err)
- }
- //s.medium = medium
- // Initialize the service after creation.
- // Note: ServiceStartup will now get config from s.Runtime.Config()
- //if err := s.ServiceStartup(context.Background(), application.ServiceOptions{}); err != nil {
- // return nil, e.E("workspace.New", "workspace service startup failed", err)
- //}
- return s, nil
-}
-
-// Register is the constructor for dynamic dependency injection (used with core.WithService).
-// It creates a Service instance and initializes its core.Runtime field.
-// Dependencies are injected during ServiceStartup.
-func Register(c *core.Core) (any, error) {
- s, err := newWorkspaceService()
- if err != nil {
- return nil, e.E("workspace.Register", "failed to create new workspace service", err)
- }
- s.Runtime = core.NewRuntime(c, Options{})
- return s, nil
-}
-
-// HandleIPCEvents processes IPC messages, including injecting dependencies on startup.
-func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
- switch m := msg.(type) {
- case map[string]any:
- if action, ok := m["action"].(string); ok && action == "workspace.switch_workspace" {
- return s.SwitchWorkspace(m["name"].(string))
- }
- case core.ActionServiceStartup:
- return s.ServiceStartup(context.Background(), application.ServiceOptions{})
- default:
- c.App.Logger.Error("Workspace: Unknown message type", "type", fmt.Sprintf("%T", m))
- }
- return nil
-}
-
-// getWorkspaceDir retrieves the WorkspaceDir from the config service.
-func (s *Service) getWorkspaceDir() (string, error) {
- var workspaceDir string
- if err := s.Config().Get("workspaceDir", &workspaceDir); err != nil {
- return "", e.E("workspace.getWorkspaceDir", "failed to get WorkspaceDir from config", err)
- }
- return workspaceDir, nil
-}
-
-// ServiceStartup initializes the service, loading the workspace list.
-func (s *Service) ServiceStartup(context.Context, application.ServiceOptions) error {
- var err error
- workspaceDir, err := s.getWorkspaceDir()
- if err != nil {
- return err
- }
-
- listPath := filepath.Join(workspaceDir, listFile)
- if listPath != "" {
- }
- //if s.medium.IsFile(listPath) {
- // content, err := s.medium.FileGet(listPath)
- // if err != nil {
- // return e.E("workspace.ServiceStartup", "failed to read workspace list", err)
- // }
- // if err := json.Unmarshal([]byte(content), &s.workspaceList); err != nil {
- // fmt.Printf("Warning: could not parse workspace list: %v\n", err)
- // s.workspaceList = make(map[string]string)
- // }
- //}
-
- return s.SwitchWorkspace(defaultWorkspace)
-}
-
-// CreateWorkspace creates a new, obfuscated workspace on the local medium.
-func (s *Service) CreateWorkspace(identifier, password string) (string, error) {
- workspaceDir, err := s.getWorkspaceDir()
- if err != nil {
- return "", err
- }
-
- realName := lthn.Hash(identifier)
- workspaceID := lthn.Hash(fmt.Sprintf("workspace/%s", realName))
- workspacePath := filepath.Join(workspaceDir, workspaceID)
-
- if _, exists := s.workspaceList[workspaceID]; exists {
- return "", e.E("workspace.CreateWorkspace", "workspace for this identifier already exists", nil)
- }
-
- dirsToCreate := []string{"config", "log", "data", "files", "keys"}
- for _, dir := range dirsToCreate {
- if err := s.medium.EnsureDir(filepath.Join(workspacePath, dir)); err != nil {
- return "", e.E("workspace.CreateWorkspace", fmt.Sprintf("failed to create workspace directory '%s'", dir), err)
- }
- }
-
- keyPair, err := openpgp.CreateKeyPair(workspaceID, password)
- if err != nil {
- return "", e.E("workspace.CreateWorkspace", "failed to create workspace key pair", err)
- }
-
- keyFiles := map[string]string{
- filepath.Join(workspacePath, "keys", "key.pub"): keyPair.PublicKey,
- filepath.Join(workspacePath, "keys", "key.priv"): keyPair.PrivateKey,
- }
- for path, content := range keyFiles {
- if err := s.medium.FileSet(path, content); err != nil {
- return "", e.E("workspace.CreateWorkspace", fmt.Sprintf("failed to write key file %s", path), err)
- }
- }
-
- s.workspaceList[workspaceID] = keyPair.PublicKey
- listData, err := json.MarshalIndent(s.workspaceList, "", " ")
- if err != nil {
- return "", e.E("workspace.CreateWorkspace", "failed to marshal workspace list", err)
- }
-
- listPath := filepath.Join(workspaceDir, listFile)
- if err := s.medium.FileSet(listPath, string(listData)); err != nil {
- return "", e.E("workspace.CreateWorkspace", "failed to write workspace list file", err)
- }
-
- return workspaceID, nil
-}
-
-// SwitchWorkspace changes the active workspace.
-func (s *Service) SwitchWorkspace(name string) error {
- workspaceDir, err := s.getWorkspaceDir()
- if err != nil {
- return err
- }
-
- if name != defaultWorkspace {
- if _, exists := s.workspaceList[name]; !exists {
- return e.E("workspace.SwitchWorkspace", fmt.Sprintf("workspace '%s' does not exist", name), nil)
- }
- }
-
- path := filepath.Join(workspaceDir, name)
- //if err := s.medium.EnsureDir(path); err != nil {
- // return e.E("workspace.SwitchWorkspace", "failed to ensure workspace directory exists", err)
- //}
-
- s.activeWorkspace = &Workspace{
- Name: name,
- Path: path,
- }
-
- return nil
-}
-
-// WorkspaceFileGet retrieves a file from the active workspace.
-func (s *Service) WorkspaceFileGet(filename string) (string, error) {
- if s.activeWorkspace == nil {
- return "", e.E("workspace.WorkspaceFileGet", "no active workspace", nil)
- }
- path := filepath.Join(s.activeWorkspace.Path, filename)
- content, err := s.medium.FileGet(path)
- if err != nil {
- return "", e.E("workspace.WorkspaceFileGet", "failed to get file", err)
- }
- return content, nil
-}
-
-// WorkspaceFileSet writes a file to the active workspace.
-func (s *Service) WorkspaceFileSet(filename, content string) error {
- if s.activeWorkspace == nil {
- return e.E("workspace.WorkspaceFileSet", "no active workspace", nil)
- }
- path := filepath.Join(s.activeWorkspace.Path, filename)
- err := s.medium.FileSet(path, content)
- if err != nil {
- return e.E("workspace.WorkspaceFileSet", "failed to set file", err)
- }
- return nil
-}
diff --git a/pkg/workspace/workspace_test.go b/pkg/workspace/workspace_test.go
deleted file mode 100644
index 3af293f..0000000
--- a/pkg/workspace/workspace_test.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package workspace
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "path/filepath"
- "testing"
-
- "github.com/Snider/Core/pkg/core"
- "github.com/stretchr/testify/assert"
- "github.com/wailsapp/wails/v3/pkg/application"
-)
-
-// mockConfig is a mock implementation of the core.Config interface for testing.
-type mockConfig struct {
- values map[string]interface{}
-}
-
-func (m *mockConfig) Get(key string, out any) error {
- val, ok := m.values[key]
- if !ok {
- return fmt.Errorf("key not found: %s", key)
- }
- // This is a simplified mock; a real one would use reflection to set `out`
- switch v := out.(type) {
- case *string:
- *v = val.(string)
- default:
- return fmt.Errorf("unsupported type in mock config Get")
- }
- return nil
-}
-
-func (m *mockConfig) Set(key string, v any) error {
- m.values[key] = v
- return nil
-}
-
-// MockMedium implements the Medium interface for testing purposes.
-type MockMedium struct {
- Files map[string]string
- Dirs map[string]bool
-}
-
-func NewMockMedium() *MockMedium {
- return &MockMedium{
- Files: make(map[string]string),
- Dirs: make(map[string]bool),
- }
-}
-
-func (m *MockMedium) FileGet(path string) (string, error) {
- content, ok := m.Files[path]
- if !ok {
- return "", assert.AnError // Simulate file not found error
- }
- return content, nil
-}
-
-func (m *MockMedium) FileSet(path, content string) error {
- m.Files[path] = content
- return nil
-}
-
-func (m *MockMedium) EnsureDir(path string) error {
- m.Dirs[path] = true
- return nil
-}
-
-func (m *MockMedium) IsFile(path string) bool {
- _, exists := m.Files[path]
- return exists
-}
-
-func (m *MockMedium) Read(path string) (string, error) {
- return m.FileGet(path)
-}
-
-func (m *MockMedium) Write(path, content string) error {
- return m.FileSet(path, content)
-}
-
-// newTestService creates a workspace service instance with mocked dependencies.
-func newTestService(t *testing.T, workspaceDir string) (*Service, *MockMedium) {
- coreInstance, err := core.New()
- assert.NoError(t, err)
-
- mockCfg := &mockConfig{values: map[string]interface{}{"workspaceDir": workspaceDir}}
- coreInstance.RegisterService("config", mockCfg)
-
- service, err := New()
- assert.NoError(t, err)
-
- service.Runtime = core.NewRuntime(coreInstance, Options{})
- mockMedium := NewMockMedium()
- service.medium = mockMedium
-
- return service, mockMedium
-}
-
-func TestServiceStartup(t *testing.T) {
- workspaceDir := "/tmp/workspace"
-
- t.Run("existing valid list.json", func(t *testing.T) {
- service, mockMedium := newTestService(t, workspaceDir)
-
- expectedWorkspaceList := map[string]string{
- "workspace1": "pubkey1",
- "workspace2": "pubkey2",
- }
- listContent, _ := json.MarshalIndent(expectedWorkspaceList, "", " ")
- listPath := filepath.Join(workspaceDir, listFile)
- mockMedium.Files[listPath] = string(listContent)
-
- err := service.ServiceStartup(context.Background(), application.ServiceOptions{})
-
- assert.NoError(t, err)
- // assert.Equal(t, expectedWorkspaceList, service.workspaceList) // This check is difficult with current implementation
- assert.NotNil(t, service.activeWorkspace)
- assert.Equal(t, defaultWorkspace, service.activeWorkspace.Name)
- })
-}
-
-func TestCreateAndSwitchWorkspace(t *testing.T) {
- workspaceDir := "/tmp/workspace"
- service, _ := newTestService(t, workspaceDir)
-
- // Create
- workspaceID, err := service.CreateWorkspace("test", "password")
- assert.NoError(t, err)
- assert.NotEmpty(t, workspaceID)
-
- // Switch
- err = service.SwitchWorkspace(workspaceID)
- assert.NoError(t, err)
- assert.Equal(t, workspaceID, service.activeWorkspace.Name)
-}
diff --git a/pkg/runtime/runtime.go b/runtime/runtime.go
similarity index 69%
rename from pkg/runtime/runtime.go
rename to runtime/runtime.go
index 80f315e..7a0a194 100644
--- a/pkg/runtime/runtime.go
+++ b/runtime/runtime.go
@@ -4,19 +4,15 @@ import (
"context"
"fmt"
- "github.com/Snider/Core/pkg/core"
- "github.com/Snider/Core/pkg/crypt"
- "github.com/Snider/Core/pkg/workspace"
+ "github.com/Snider/Core/core"
"github.com/wailsapp/wails/v3/pkg/application"
)
// Runtime is the container that holds all instantiated services.
// Its fields are the concrete types, allowing Wails to bind them directly.
type Runtime struct {
- app *application.App
- Core *core.Core
- Crypt *crypt.Service
- Workspace *workspace.Service
+ app *application.App
+ Core *core.Core
}
// ServiceFactory defines a function that creates a service instance.
@@ -29,7 +25,7 @@ func NewWithFactories(app *application.App, factories map[string]ServiceFactory)
core.WithWails(app),
}
- for _, name := range []string{"crypt", "workspace"} {
+ for _, name := range []string{} {
factory, ok := factories[name]
if !ok {
return nil, fmt.Errorf("service %s factory not provided", name)
@@ -49,20 +45,10 @@ func NewWithFactories(app *application.App, factories map[string]ServiceFactory)
}
// --- Type Assertions ---
- cryptSvc, ok := services["crypt"].(*crypt.Service)
- if !ok {
- return nil, fmt.Errorf("crypt service has unexpected type")
- }
- workspaceSvc, ok := services["workspace"].(*workspace.Service)
- if !ok {
- return nil, fmt.Errorf("workspace service has unexpected type")
- }
rt := &Runtime{
- app: app,
- Core: coreInstance,
- Crypt: cryptSvc,
- Workspace: workspaceSvc,
+ app: app,
+ Core: coreInstance,
}
return rt, nil
@@ -70,10 +56,7 @@ func NewWithFactories(app *application.App, factories map[string]ServiceFactory)
// New creates and wires together all application services.
func New(app *application.App) (*Runtime, error) {
- return NewWithFactories(app, map[string]ServiceFactory{
- "crypt": func() (any, error) { return crypt.New() },
- "workspace": func() (any, error) { return workspace.New() },
- })
+ return NewWithFactories(app, map[string]ServiceFactory{})
}
// ServiceName returns the name of the service. This is used by Wails to identify the service.
diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go
new file mode 100644
index 0000000..3993329
--- /dev/null
+++ b/runtime/runtime_test.go
@@ -0,0 +1,59 @@
+package runtime_test
+
+import (
+ "testing"
+
+ "github.com/Snider/Core/runtime"
+ "github.com/stretchr/testify/assert"
+ "github.com/wailsapp/wails/v3/pkg/application"
+)
+
+func TestNew(t *testing.T) {
+ testCases := []struct {
+ name string
+ app *application.App
+ factories map[string]runtime.ServiceFactory
+ expectErr bool
+ expectErrStr string
+ checkRuntime func(*testing.T, *runtime.Runtime)
+ }{
+ {
+ name: "Good path",
+ app: nil,
+ factories: map[string]runtime.ServiceFactory{},
+ expectErr: false,
+ checkRuntime: func(t *testing.T, rt *runtime.Runtime) {
+ assert.NotNil(t, rt)
+ assert.NotNil(t, rt.Core)
+ },
+ },
+ {
+ name: "With non-nil app",
+ app: &application.App{},
+ factories: map[string]runtime.ServiceFactory{},
+ expectErr: false,
+ checkRuntime: func(t *testing.T, rt *runtime.Runtime) {
+ assert.NotNil(t, rt)
+ assert.NotNil(t, rt.Core)
+ assert.NotNil(t, rt.Core.App)
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ rt, err := runtime.NewWithFactories(tc.app, tc.factories)
+
+ if tc.expectErr {
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), tc.expectErrStr)
+ assert.Nil(t, rt)
+ } else {
+ assert.NoError(t, err)
+ if tc.checkRuntime != nil {
+ tc.checkRuntime(t, rt)
+ }
+ }
+ })
+ }
+}