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 <virgil@lethean.io>
This commit is contained in:
parent
c713bafd48
commit
df9c443657
2 changed files with 107 additions and 2 deletions
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue