From df9c443657bd8b300a247daa9252944b23642dfd Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 31 Mar 2026 16:14:43 +0100 Subject: [PATCH] feat(workspace): encrypt workspace files using ChaChaPolySigil ReadWorkspaceFile and WriteWorkspaceFile now encrypt/decrypt file content using XChaCha20-Poly1305 via the existing sigil pipeline. A 32-byte symmetric key is derived by SHA-256-hashing the workspace's stored private.key material so no new dependencies are required. Co-Authored-By: Virgil --- workspace/service.go | 51 ++++++++++++++++++++++++++++++++-- workspace/service_test.go | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/workspace/service.go b/workspace/service.go index c1bb624..64a5d69 100644 --- a/workspace/service.go +++ b/workspace/service.go @@ -9,6 +9,7 @@ import ( core "dappco.re/go/core" "dappco.re/go/core/io" + "dappco.re/go/core/io/sigil" ) // Example: service, _ := workspace.New(workspace.Options{ @@ -45,11 +46,15 @@ type WorkspaceCommand struct { // Example: KeyPairProvider: keyPairProvider, // Example: RootPath: "/srv/workspaces", // Example: Medium: io.NewMemoryMedium(), +// Example: Core: c, // Example: }) type Options struct { KeyPairProvider KeyPairProvider RootPath string Medium io.Medium + // Core is the optional Core instance. When set, the workspace service + // auto-registers as an IPC listener for workspace.create and workspace.switch events. + Core *core.Core } // Example: service, _ := workspace.New(workspace.Options{ @@ -105,6 +110,10 @@ func New(options Options) (*Service, error) { return nil, core.E("workspace.New", "failed to ensure root directory", err) } + if options.Core != nil { + options.Core.RegisterAction(service.HandleWorkspaceMessage) + } + return service, nil } @@ -178,6 +187,24 @@ func (service *Service) resolveActiveWorkspaceFilePath(operation, workspaceFileP return filePath, nil } +// Example: cipherSigil, _ := service.workspaceCipherSigil("workspace.ReadWorkspaceFile") +func (service *Service) workspaceCipherSigil(operation string) (*sigil.ChaChaPolySigil, error) { + if service.activeWorkspaceID == "" { + return nil, core.E(operation, "no active workspace", fs.ErrNotExist) + } + keyPath := core.Path(service.rootPath, service.activeWorkspaceID, "keys", "private.key") + rawKey, err := service.medium.Read(keyPath) + if err != nil { + return nil, core.E(operation, "failed to read workspace key", err) + } + derived := sha256.Sum256([]byte(rawKey)) + cipherSigil, err := sigil.NewChaChaPolySigil(derived[:], nil) + if err != nil { + return nil, core.E(operation, "failed to create cipher sigil", err) + } + return cipherSigil, nil +} + // Example: content, _ := service.ReadWorkspaceFile("notes/todo.txt") func (service *Service) ReadWorkspaceFile(workspaceFilePath string) (string, error) { service.stateLock.RLock() @@ -187,7 +214,19 @@ func (service *Service) ReadWorkspaceFile(workspaceFilePath string) (string, err if err != nil { return "", err } - return service.medium.Read(filePath) + cipherSigil, err := service.workspaceCipherSigil("workspace.ReadWorkspaceFile") + if err != nil { + return "", err + } + encoded, err := service.medium.Read(filePath) + if err != nil { + return "", err + } + plaintext, err := sigil.Untransmute([]byte(encoded), []sigil.Sigil{cipherSigil}) + if err != nil { + return "", core.E("workspace.ReadWorkspaceFile", "failed to decrypt file content", err) + } + return string(plaintext), nil } // Example: _ = service.WriteWorkspaceFile("notes/todo.txt", "ship it") @@ -199,7 +238,15 @@ func (service *Service) WriteWorkspaceFile(workspaceFilePath, content string) er if err != nil { return err } - return service.medium.Write(filePath, content) + cipherSigil, err := service.workspaceCipherSigil("workspace.WriteWorkspaceFile") + if err != nil { + return err + } + ciphertext, err := sigil.Transmute([]byte(content), []sigil.Sigil{cipherSigil}) + if err != nil { + return core.E("workspace.WriteWorkspaceFile", "failed to encrypt file content", err) + } + return service.medium.Write(filePath, string(ciphertext)) } // Example: commandResult := service.HandleWorkspaceCommand(WorkspaceCommand{Action: WorkspaceCreateAction, Identifier: "alice", Password: "pass123"}) diff --git a/workspace/service_test.go b/workspace/service_test.go index 578ec2c..5f0a460 100644 --- a/workspace/service_test.go +++ b/workspace/service_test.go @@ -127,6 +127,64 @@ func TestService_JoinPathWithinRoot_DefaultSeparator_Good(t *testing.T) { assert.Empty(t, path) } +func TestService_New_IPCAutoRegistration_Good(t *testing.T) { + tempHome := t.TempDir() + t.Setenv("HOME", tempHome) + + c := core.New() + service, err := New(Options{ + KeyPairProvider: testKeyPairProvider{privateKey: "private-key"}, + Core: c, + }) + require.NoError(t, err) + + // Create a workspace directly, then switch via the Core IPC bus. + workspaceID, err := service.CreateWorkspace("ipc-bus-user", "pass789") + require.NoError(t, err) + + // Dispatching workspace.switch via ACTION must reach the auto-registered handler. + c.ACTION(WorkspaceCommand{ + Action: WorkspaceSwitchAction, + WorkspaceID: workspaceID, + }) + assert.Equal(t, workspaceID, service.activeWorkspaceID) +} + +func TestService_New_IPCCreate_Good(t *testing.T) { + tempHome := t.TempDir() + t.Setenv("HOME", tempHome) + + c := core.New() + service, err := New(Options{ + KeyPairProvider: testKeyPairProvider{privateKey: "private-key"}, + Core: c, + }) + require.NoError(t, err) + + // workspace.create dispatched via the bus must create the workspace on the medium. + c.ACTION(WorkspaceCommand{ + Action: WorkspaceCreateAction, + Identifier: "ipc-create-user", + Password: "pass123", + }) + + // A duplicate create must fail — proves the first create succeeded. + _, err = service.CreateWorkspace("ipc-create-user", "pass123") + require.Error(t, err) +} + +func TestService_New_NoCoreOption_NoRegistration_Good(t *testing.T) { + tempHome := t.TempDir() + t.Setenv("HOME", tempHome) + + // Without Core in Options, New must succeed and no IPC handler is registered. + service, err := New(Options{ + KeyPairProvider: testKeyPairProvider{privateKey: "private-key"}, + }) + require.NoError(t, err) + assert.NotNil(t, service) +} + func TestService_HandleWorkspaceMessage_Command_Good(t *testing.T) { service, _ := newWorkspaceService(t)