refactor(ax): trim prose comments to examples
This commit is contained in:
parent
f8988c51cb
commit
347c4b1b57
14 changed files with 29 additions and 190 deletions
|
|
@ -1,9 +1,7 @@
|
|||
// Package datanode provides an io.Medium implementation backed by Borg's DataNode.
|
||||
//
|
||||
// medium := datanode.New()
|
||||
// _ = medium.Write("jobs/run.log", "started")
|
||||
// snapshot, _ := medium.Snapshot()
|
||||
// restored, _ := datanode.FromTar(snapshot)
|
||||
// medium := datanode.New()
|
||||
// _ = medium.Write("jobs/run.log", "started")
|
||||
// snapshot, _ := medium.Snapshot()
|
||||
// restored, _ := datanode.FromTar(snapshot)
|
||||
package datanode
|
||||
|
||||
import (
|
||||
|
|
@ -36,7 +34,7 @@ var (
|
|||
// snapshot, _ := medium.Snapshot()
|
||||
type Medium struct {
|
||||
dataNode *borgdatanode.DataNode
|
||||
directorySet map[string]bool // explicit directories that exist without file contents
|
||||
directorySet map[string]bool
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +99,6 @@ func normaliseEntryPath(filePath string) string {
|
|||
return filePath
|
||||
}
|
||||
|
||||
// --- io.Medium interface ---
|
||||
|
||||
func (medium *Medium) Read(filePath string) (string, error) {
|
||||
medium.lock.RLock()
|
||||
defer medium.lock.RUnlock()
|
||||
|
|
@ -139,7 +135,6 @@ func (medium *Medium) Write(filePath, content string) error {
|
|||
}
|
||||
medium.dataNode.AddData(filePath, []byte(content))
|
||||
|
||||
// ensure parent directories are tracked
|
||||
medium.ensureDirsLocked(path.Dir(filePath))
|
||||
return nil
|
||||
}
|
||||
|
|
@ -160,8 +155,6 @@ func (medium *Medium) EnsureDir(filePath string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ensureDirsLocked marks a directory and all ancestors as existing.
|
||||
// Caller must hold medium.lock.
|
||||
func (medium *Medium) ensureDirsLocked(directoryPath string) {
|
||||
for directoryPath != "" && directoryPath != "." {
|
||||
medium.directorySet[directoryPath] = true
|
||||
|
|
@ -226,7 +219,6 @@ func (medium *Medium) Delete(filePath string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Remove the file by creating a new DataNode without it
|
||||
if err := medium.removeFileLocked(filePath); err != nil {
|
||||
return core.E("datanode.Delete", core.Concat("failed to delete file: ", filePath), err)
|
||||
}
|
||||
|
|
@ -253,7 +245,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
|
|||
found = true
|
||||
}
|
||||
|
||||
// Remove all files under prefix
|
||||
entries, err := medium.collectAllLocked()
|
||||
if err != nil {
|
||||
return core.E("datanode.DeleteAll", core.Concat("failed to inspect tree: ", filePath), err)
|
||||
|
|
@ -267,7 +258,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Remove explicit directories under prefix
|
||||
for directoryPath := range medium.directorySet {
|
||||
if directoryPath == filePath || core.HasPrefix(directoryPath, prefix) {
|
||||
delete(medium.directorySet, directoryPath)
|
||||
|
|
@ -466,7 +456,7 @@ func (medium *Medium) Exists(filePath string) bool {
|
|||
|
||||
filePath = normaliseEntryPath(filePath)
|
||||
if filePath == "" {
|
||||
return true // root always exists
|
||||
return true
|
||||
}
|
||||
_, err := medium.dataNode.Stat(filePath)
|
||||
if err == nil {
|
||||
|
|
@ -490,9 +480,6 @@ func (medium *Medium) IsDir(filePath string) bool {
|
|||
return medium.directorySet[filePath]
|
||||
}
|
||||
|
||||
// --- internal helpers ---
|
||||
|
||||
// hasPrefixLocked checks if any file path starts with prefix. Caller holds lock.
|
||||
func (medium *Medium) hasPrefixLocked(prefix string) (bool, error) {
|
||||
entries, err := medium.collectAllLocked()
|
||||
if err != nil {
|
||||
|
|
@ -511,7 +498,6 @@ func (medium *Medium) hasPrefixLocked(prefix string) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// collectAllLocked returns all file paths in the DataNode. Caller holds lock.
|
||||
func (medium *Medium) collectAllLocked() ([]string, error) {
|
||||
var names []string
|
||||
err := dataNodeWalkDir(medium.dataNode, ".", func(filePath string, entry fs.DirEntry, err error) error {
|
||||
|
|
@ -542,9 +528,6 @@ func (medium *Medium) readFileLocked(filePath string) ([]byte, error) {
|
|||
return data, nil
|
||||
}
|
||||
|
||||
// removeFileLocked removes a single file by rebuilding the DataNode.
|
||||
// This is necessary because Borg's DataNode doesn't expose a Remove method.
|
||||
// Caller must hold medium.lock write lock.
|
||||
func (medium *Medium) removeFileLocked(target string) error {
|
||||
entries, err := medium.collectAllLocked()
|
||||
if err != nil {
|
||||
|
|
@ -565,8 +548,6 @@ func (medium *Medium) removeFileLocked(target string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// --- writeCloser buffers writes and flushes to DataNode on Close ---
|
||||
|
||||
type writeCloser struct {
|
||||
medium *Medium
|
||||
path string
|
||||
|
|
@ -587,8 +568,6 @@ func (writer *writeCloser) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// --- fs types for explicit directories ---
|
||||
|
||||
type dirEntry struct {
|
||||
name string
|
||||
}
|
||||
|
|
|
|||
10
doc.go
10
doc.go
|
|
@ -1,7 +1,5 @@
|
|||
// Package io exposes CoreGO's storage surface.
|
||||
//
|
||||
// medium, _ := io.NewSandboxed("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// backup, _ := io.NewSandboxed("/srv/backup")
|
||||
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
|
||||
// medium, _ := io.NewSandboxed("/srv/app")
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
// backup, _ := io.NewSandboxed("/srv/backup")
|
||||
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
|
||||
package io
|
||||
|
|
|
|||
7
io.go
7
io.go
|
|
@ -188,7 +188,6 @@ type MemoryMedium struct {
|
|||
modTimes map[string]time.Time
|
||||
}
|
||||
|
||||
// MockMedium is a compatibility alias for MemoryMedium.
|
||||
type MockMedium = MemoryMedium
|
||||
|
||||
var _ Medium = (*MemoryMedium)(nil)
|
||||
|
|
@ -203,8 +202,6 @@ func NewMemoryMedium() *MemoryMedium {
|
|||
}
|
||||
}
|
||||
|
||||
// NewMockMedium is a compatibility alias for NewMemoryMedium.
|
||||
//
|
||||
// Example: medium := io.NewMockMedium()
|
||||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
func NewMockMedium() *MemoryMedium {
|
||||
|
|
@ -396,14 +393,12 @@ func (medium *MemoryMedium) WriteStream(path string) (goio.WriteCloser, error) {
|
|||
return medium.Create(path)
|
||||
}
|
||||
|
||||
// MemoryFile implements fs.File for MemoryMedium.
|
||||
type MemoryFile struct {
|
||||
name string
|
||||
content []byte
|
||||
offset int64
|
||||
}
|
||||
|
||||
// MockFile is a compatibility alias for MemoryFile.
|
||||
type MockFile = MemoryFile
|
||||
|
||||
func (file *MemoryFile) Stat() (fs.FileInfo, error) {
|
||||
|
|
@ -423,14 +418,12 @@ func (file *MemoryFile) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// MemoryWriteCloser implements WriteCloser for MemoryMedium.
|
||||
type MemoryWriteCloser struct {
|
||||
medium *MemoryMedium
|
||||
path string
|
||||
data []byte
|
||||
}
|
||||
|
||||
// MockWriteCloser is a compatibility alias for MemoryWriteCloser.
|
||||
type MockWriteCloser = MemoryWriteCloser
|
||||
|
||||
func (writeCloser *MemoryWriteCloser) Write(data []byte) (int, error) {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ var unrestrictedFileSystem = (&core.Fs{}).NewUnrestricted()
|
|||
// _ = medium.Write("config/app.yaml", "port: 8080")
|
||||
func New(root string) (*Medium, error) {
|
||||
absoluteRoot := absolutePath(root)
|
||||
// Example: local.New("/srv/app") resolves macOS "/var" to "/private/var" before sandbox checks.
|
||||
if resolvedRoot, err := resolveSymlinksPath(absoluteRoot); err == nil {
|
||||
absoluteRoot = resolvedRoot
|
||||
}
|
||||
|
|
@ -181,16 +180,12 @@ func (medium *Medium) sandboxedPath(path string) string {
|
|||
return core.Path(currentWorkingDir(), normalisePath(path))
|
||||
}
|
||||
|
||||
// Use a cleaned absolute path to resolve all .. and . internally
|
||||
// before joining with the root. This is a standard way to sandbox paths.
|
||||
clean := cleanSandboxPath(path)
|
||||
|
||||
// If root is "/", allow absolute paths through
|
||||
if medium.filesystemRoot == dirSeparator() {
|
||||
return clean
|
||||
}
|
||||
|
||||
// Join cleaned relative path with root
|
||||
return core.Path(medium.filesystemRoot, core.TrimPrefix(clean, dirSeparator()))
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +194,6 @@ func (medium *Medium) validatePath(path string) (string, error) {
|
|||
return medium.sandboxedPath(path), nil
|
||||
}
|
||||
|
||||
// Split the cleaned path into components
|
||||
parts := splitPathParts(cleanSandboxPath(path))
|
||||
current := medium.filesystemRoot
|
||||
|
||||
|
|
@ -208,16 +202,12 @@ func (medium *Medium) validatePath(path string) (string, error) {
|
|||
realNext, err := resolveSymlinksPath(next)
|
||||
if err != nil {
|
||||
if core.Is(err, syscall.ENOENT) {
|
||||
// Part doesn't exist, we can't follow symlinks anymore.
|
||||
// Since the path is already Cleaned and current is safe,
|
||||
// appending a component to current will not escape.
|
||||
current = next
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Verify the resolved part is still within the root
|
||||
if !isWithinRoot(medium.filesystemRoot, realNext) {
|
||||
logSandboxEscape(medium.filesystemRoot, path, realNext)
|
||||
return "", fs.ErrPermission
|
||||
|
|
|
|||
28
node/node.go
28
node/node.go
|
|
@ -129,20 +129,14 @@ func (node *Node) WalkNode(root string, fn fs.WalkDirFunc) error {
|
|||
|
||||
// Example: options := node.WalkOptions{MaxDepth: 1, SkipErrors: true}
|
||||
type WalkOptions struct {
|
||||
// MaxDepth limits how many directory levels to descend. 0 means unlimited.
|
||||
MaxDepth int
|
||||
// Filter, if set, is called for each entry. Return true to include the
|
||||
// entry (and descend into it if it is a directory).
|
||||
Filter func(entryPath string, entry fs.DirEntry) bool
|
||||
// SkipErrors suppresses errors (e.g. nonexistent root) instead of
|
||||
// propagating them through the callback.
|
||||
MaxDepth int
|
||||
Filter func(entryPath string, entry fs.DirEntry) bool
|
||||
SkipErrors bool
|
||||
}
|
||||
|
||||
// Example: _ = nodeTree.WalkWithOptions(".", callback, node.WalkOptions{MaxDepth: 1, SkipErrors: true})
|
||||
func (node *Node) WalkWithOptions(root string, fn fs.WalkDirFunc, options WalkOptions) error {
|
||||
if options.SkipErrors {
|
||||
// If root doesn't exist, silently return nil.
|
||||
if _, err := node.Stat(root); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -160,7 +154,6 @@ func (node *Node) WalkWithOptions(root string, fn fs.WalkDirFunc, options WalkOp
|
|||
|
||||
result := fn(entryPath, entry, err)
|
||||
|
||||
// After visiting a directory at MaxDepth, prevent descending further.
|
||||
if result == nil && options.MaxDepth > 0 && entry != nil && entry.IsDir() && entryPath != root {
|
||||
rel := core.TrimPrefix(entryPath, root)
|
||||
rel = core.TrimPrefix(rel, "/")
|
||||
|
|
@ -181,7 +174,6 @@ func (node *Node) ReadFile(name string) ([]byte, error) {
|
|||
if !ok {
|
||||
return nil, core.E("node.ReadFile", core.Concat("path not found: ", name), fs.ErrNotExist)
|
||||
}
|
||||
// Return a copy to prevent callers from mutating internal state.
|
||||
result := make([]byte, len(file.content))
|
||||
copy(result, file.content)
|
||||
return result, nil
|
||||
|
|
@ -217,7 +209,6 @@ func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) erro
|
|||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
// Single file copy
|
||||
file, ok := node.files[sourcePath]
|
||||
if !ok {
|
||||
return core.E("node.CopyTo", core.Concat("path not found: ", sourcePath), fs.ErrNotExist)
|
||||
|
|
@ -225,7 +216,6 @@ func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) erro
|
|||
return target.Write(destPath, string(file.content))
|
||||
}
|
||||
|
||||
// Directory: walk and copy all files underneath
|
||||
prefix := sourcePath
|
||||
if prefix != "" && !core.HasSuffix(prefix, "/") {
|
||||
prefix += "/"
|
||||
|
|
@ -247,8 +237,6 @@ func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
// ---------- Medium interface: fs.FS methods ----------
|
||||
|
||||
func (node *Node) Open(name string) (fs.File, error) {
|
||||
name = core.TrimPrefix(name, "/")
|
||||
if dataFile, ok := node.files[name]; ok {
|
||||
|
|
@ -289,7 +277,6 @@ func (node *Node) ReadDir(name string) ([]fs.DirEntry, error) {
|
|||
name = ""
|
||||
}
|
||||
|
||||
// Disallow reading a file as a directory.
|
||||
if info, err := node.Stat(name); err == nil && !info.IsDir() {
|
||||
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
|
||||
}
|
||||
|
|
@ -332,8 +319,6 @@ func (node *Node) ReadDir(name string) ([]fs.DirEntry, error) {
|
|||
return entries, nil
|
||||
}
|
||||
|
||||
// ---------- Medium interface: read/write ----------
|
||||
|
||||
func (node *Node) Read(filePath string) (string, error) {
|
||||
filePath = core.TrimPrefix(filePath, "/")
|
||||
file, ok := node.files[filePath]
|
||||
|
|
@ -365,8 +350,6 @@ func (node *Node) EnsureDir(_ string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ---------- Medium interface: existence checks ----------
|
||||
|
||||
func (node *Node) Exists(filePath string) bool {
|
||||
_, err := node.Stat(filePath)
|
||||
return err == nil
|
||||
|
|
@ -386,8 +369,6 @@ func (node *Node) IsDir(filePath string) bool {
|
|||
return info.IsDir()
|
||||
}
|
||||
|
||||
// ---------- Medium interface: mutations ----------
|
||||
|
||||
func (node *Node) Delete(filePath string) error {
|
||||
filePath = core.TrimPrefix(filePath, "/")
|
||||
if _, ok := node.files[filePath]; ok {
|
||||
|
|
@ -443,8 +424,6 @@ func (node *Node) List(filePath string) ([]fs.DirEntry, error) {
|
|||
return node.ReadDir(filePath)
|
||||
}
|
||||
|
||||
// ---------- Medium interface: streams ----------
|
||||
|
||||
func (node *Node) Create(filePath string) (goio.WriteCloser, error) {
|
||||
filePath = core.TrimPrefix(filePath, "/")
|
||||
return &nodeWriter{node: node, path: filePath}, nil
|
||||
|
|
@ -472,9 +451,6 @@ func (node *Node) WriteStream(filePath string) (goio.WriteCloser, error) {
|
|||
return node.Create(filePath)
|
||||
}
|
||||
|
||||
// ---------- Internal types ----------
|
||||
|
||||
// nodeWriter buffers writes and commits them to the Node on Close.
|
||||
type nodeWriter struct {
|
||||
node *Node
|
||||
path string
|
||||
|
|
|
|||
12
s3/s3.go
12
s3/s3.go
|
|
@ -1,5 +1,3 @@
|
|||
// Package s3 stores io.Medium data in S3 objects.
|
||||
//
|
||||
// Example: client := awss3.NewFromConfig(aws.Config{Region: "us-east-1"})
|
||||
// Example: medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
|
||||
// Example: _ = medium.Write("reports/daily.txt", "done")
|
||||
|
|
@ -45,11 +43,8 @@ var _ coreio.Medium = (*Medium)(nil)
|
|||
|
||||
// Example: medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
|
||||
type Options struct {
|
||||
// Bucket is the target S3 bucket name.
|
||||
Bucket string
|
||||
// Client is the AWS S3 client or test double used for requests.
|
||||
Client Client
|
||||
// Prefix is prepended to every object key.
|
||||
Prefix string
|
||||
}
|
||||
|
||||
|
|
@ -109,8 +104,6 @@ func New(options Options) (*Medium, error) {
|
|||
}
|
||||
|
||||
func (medium *Medium) objectKey(filePath string) string {
|
||||
// Clean the path using a leading "/" to sandbox traversal attempts,
|
||||
// then strip the "/" prefix. This ensures ".." can't escape.
|
||||
clean := path.Clean("/" + filePath)
|
||||
if clean == "/" {
|
||||
clean = ""
|
||||
|
|
@ -181,7 +174,6 @@ func (medium *Medium) IsFile(filePath string) bool {
|
|||
if key == "" {
|
||||
return false
|
||||
}
|
||||
// A "file" in S3 is an object whose key does not end with "/"
|
||||
if core.HasSuffix(key, "/") {
|
||||
return false
|
||||
}
|
||||
|
|
@ -223,7 +215,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
|
|||
return core.E("s3.DeleteAll", "path is required", fs.ErrInvalid)
|
||||
}
|
||||
|
||||
// First, try deleting the exact key
|
||||
_, err := medium.client.DeleteObject(context.Background(), &awss3.DeleteObjectInput{
|
||||
Bucket: aws.String(medium.bucket),
|
||||
Key: aws.String(key),
|
||||
|
|
@ -232,7 +223,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
|
|||
return core.E("s3.DeleteAll", core.Concat("failed to delete object: ", key), err)
|
||||
}
|
||||
|
||||
// Then delete all objects under the prefix
|
||||
prefix := key
|
||||
if !core.HasSuffix(prefix, "/") {
|
||||
prefix += "/"
|
||||
|
|
@ -561,8 +551,6 @@ func (medium *Medium) IsDir(filePath string) bool {
|
|||
return len(listOutput.Contents) > 0 || len(listOutput.CommonPrefixes) > 0
|
||||
}
|
||||
|
||||
// --- Internal types ---
|
||||
|
||||
type fileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
|
|
|
|||
|
|
@ -14,35 +14,23 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// InvalidKeyError is returned when the encryption key is not 32 bytes.
|
||||
InvalidKeyError = core.E("sigil.InvalidKeyError", "invalid key size, must be 32 bytes", nil)
|
||||
|
||||
// CiphertextTooShortError is returned when the ciphertext is too short to decrypt.
|
||||
CiphertextTooShortError = core.E("sigil.CiphertextTooShortError", "ciphertext too short", nil)
|
||||
|
||||
// DecryptionFailedError is returned when decryption or authentication fails.
|
||||
DecryptionFailedError = core.E("sigil.DecryptionFailedError", "decryption failed", nil)
|
||||
|
||||
// NoKeyConfiguredError is returned when no encryption key has been set.
|
||||
NoKeyConfiguredError = core.E("sigil.NoKeyConfiguredError", "no encryption key configured", nil)
|
||||
)
|
||||
|
||||
// PreObfuscator customises the bytes mixed in before and after encryption.
|
||||
type PreObfuscator interface {
|
||||
// Obfuscate transforms plaintext before encryption using the provided entropy.
|
||||
// The entropy is typically the encryption nonce, ensuring the transformation
|
||||
// is unique per-encryption without additional random generation.
|
||||
Obfuscate(data []byte, entropy []byte) []byte
|
||||
|
||||
// Deobfuscate reverses the transformation after decryption.
|
||||
// Must be called with the same entropy used during Obfuscate.
|
||||
Deobfuscate(data []byte, entropy []byte) []byte
|
||||
}
|
||||
|
||||
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil(key)
|
||||
type XORObfuscator struct{}
|
||||
|
||||
// Obfuscate XORs the data with a key stream derived from the entropy.
|
||||
func (obfuscator *XORObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
|
|
@ -50,7 +38,6 @@ func (obfuscator *XORObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
|
|||
return obfuscator.transform(data, entropy)
|
||||
}
|
||||
|
||||
// Deobfuscate reverses the XOR transformation (XOR is symmetric).
|
||||
func (obfuscator *XORObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
|
|
@ -58,7 +45,6 @@ func (obfuscator *XORObfuscator) Deobfuscate(data []byte, entropy []byte) []byte
|
|||
return obfuscator.transform(data, entropy)
|
||||
}
|
||||
|
||||
// transform applies XOR with an entropy-derived key stream.
|
||||
func (obfuscator *XORObfuscator) transform(data []byte, entropy []byte) []byte {
|
||||
result := make([]byte, len(data))
|
||||
keyStream := obfuscator.deriveKeyStream(entropy, len(data))
|
||||
|
|
@ -68,12 +54,10 @@ func (obfuscator *XORObfuscator) transform(data []byte, entropy []byte) []byte {
|
|||
return result
|
||||
}
|
||||
|
||||
// deriveKeyStream creates a deterministic key stream from entropy.
|
||||
func (obfuscator *XORObfuscator) deriveKeyStream(entropy []byte, length int) []byte {
|
||||
stream := make([]byte, length)
|
||||
h := sha256.New()
|
||||
|
||||
// Generate key stream in 32-byte blocks
|
||||
blockNum := uint64(0)
|
||||
offset := 0
|
||||
for offset < length {
|
||||
|
|
@ -92,10 +76,8 @@ func (obfuscator *XORObfuscator) deriveKeyStream(entropy []byte, length int) []b
|
|||
return stream
|
||||
}
|
||||
|
||||
// ShuffleMaskObfuscator adds byte shuffling on top of XOR masking.
|
||||
type ShuffleMaskObfuscator struct{}
|
||||
|
||||
// Obfuscate shuffles bytes and applies a mask derived from entropy.
|
||||
func (obfuscator *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
|
|
@ -104,16 +86,13 @@ func (obfuscator *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte)
|
|||
result := make([]byte, len(data))
|
||||
copy(result, data)
|
||||
|
||||
// Generate permutation and mask from entropy
|
||||
perm := obfuscator.generatePermutation(entropy, len(data))
|
||||
mask := obfuscator.deriveMask(entropy, len(data))
|
||||
|
||||
// Apply mask first, then shuffle
|
||||
for i := range result {
|
||||
result[i] ^= mask[i]
|
||||
}
|
||||
|
||||
// Shuffle using Fisher-Yates with deterministic seed
|
||||
shuffled := make([]byte, len(data))
|
||||
for i, p := range perm {
|
||||
shuffled[i] = result[p]
|
||||
|
|
@ -122,7 +101,6 @@ func (obfuscator *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte)
|
|||
return shuffled
|
||||
}
|
||||
|
||||
// Deobfuscate reverses the shuffle and mask operations.
|
||||
func (obfuscator *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
|
|
@ -130,16 +108,13 @@ func (obfuscator *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte
|
|||
|
||||
result := make([]byte, len(data))
|
||||
|
||||
// Generate permutation and mask from entropy
|
||||
perm := obfuscator.generatePermutation(entropy, len(data))
|
||||
mask := obfuscator.deriveMask(entropy, len(data))
|
||||
|
||||
// Unshuffle first
|
||||
for i, p := range perm {
|
||||
result[p] = data[i]
|
||||
}
|
||||
|
||||
// Remove mask
|
||||
for i := range result {
|
||||
result[i] ^= mask[i]
|
||||
}
|
||||
|
|
@ -147,20 +122,17 @@ func (obfuscator *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte
|
|||
return result
|
||||
}
|
||||
|
||||
// generatePermutation creates a deterministic permutation from entropy.
|
||||
func (obfuscator *ShuffleMaskObfuscator) generatePermutation(entropy []byte, length int) []int {
|
||||
perm := make([]int, length)
|
||||
for i := range perm {
|
||||
perm[i] = i
|
||||
}
|
||||
|
||||
// Use entropy to seed a deterministic shuffle
|
||||
h := sha256.New()
|
||||
h.Write(entropy)
|
||||
h.Write([]byte("permutation"))
|
||||
seed := h.Sum(nil)
|
||||
|
||||
// Fisher-Yates shuffle with deterministic randomness
|
||||
for i := length - 1; i > 0; i-- {
|
||||
h.Reset()
|
||||
h.Write(seed)
|
||||
|
|
@ -175,7 +147,6 @@ func (obfuscator *ShuffleMaskObfuscator) generatePermutation(entropy []byte, len
|
|||
return perm
|
||||
}
|
||||
|
||||
// deriveMask creates a mask byte array from entropy.
|
||||
func (obfuscator *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int) []byte {
|
||||
mask := make([]byte, length)
|
||||
h := sha256.New()
|
||||
|
|
@ -199,12 +170,11 @@ func (obfuscator *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int)
|
|||
return mask
|
||||
}
|
||||
|
||||
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil(key)
|
||||
// Example: cipherSigil, _ := sigil.NewChaChaPolySigilWithObfuscator(key, &sigil.ShuffleMaskObfuscator{})
|
||||
type ChaChaPolySigil struct {
|
||||
Key []byte
|
||||
Obfuscator PreObfuscator
|
||||
randomReader goio.Reader // for testing injection
|
||||
randomReader goio.Reader
|
||||
}
|
||||
|
||||
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"))
|
||||
|
|
@ -244,7 +214,6 @@ func NewChaChaPolySigilWithObfuscator(key []byte, obfuscator PreObfuscator) (*Ch
|
|||
return cipherSigil, nil
|
||||
}
|
||||
|
||||
// In encrypts plaintext with the configured pre-obfuscator.
|
||||
func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
|
||||
if sigil.Key == nil {
|
||||
return nil, NoKeyConfiguredError
|
||||
|
|
@ -258,7 +227,6 @@ func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
|
|||
return nil, core.E("sigil.ChaChaPolySigil.In", "create cipher", err)
|
||||
}
|
||||
|
||||
// Generate nonce
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
reader := sigil.randomReader
|
||||
if reader == nil {
|
||||
|
|
@ -268,21 +236,16 @@ func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
|
|||
return nil, core.E("sigil.ChaChaPolySigil.In", "read nonce", err)
|
||||
}
|
||||
|
||||
// Pre-obfuscate the plaintext using nonce as entropy
|
||||
// This ensures CPU encryption routines never see raw plaintext
|
||||
obfuscated := data
|
||||
if sigil.Obfuscator != nil {
|
||||
obfuscated = sigil.Obfuscator.Obfuscate(data, nonce)
|
||||
}
|
||||
|
||||
// Encrypt the obfuscated data
|
||||
// Output: [nonce | ciphertext | auth tag]
|
||||
ciphertext := aead.Seal(nonce, nonce, obfuscated, nil)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// Out decrypts ciphertext and reverses the pre-obfuscation step.
|
||||
func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
|
||||
if sigil.Key == nil {
|
||||
return nil, NoKeyConfiguredError
|
||||
|
|
@ -301,17 +264,14 @@ func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
|
|||
return nil, CiphertextTooShortError
|
||||
}
|
||||
|
||||
// Extract nonce from ciphertext
|
||||
nonce := data[:aead.NonceSize()]
|
||||
ciphertext := data[aead.NonceSize():]
|
||||
|
||||
// Decrypt
|
||||
obfuscated, err := aead.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, core.E("sigil.ChaChaPolySigil.Out", "decrypt ciphertext", DecryptionFailedError)
|
||||
}
|
||||
|
||||
// Deobfuscate using the same nonce as entropy
|
||||
plaintext := obfuscated
|
||||
if sigil.Obfuscator != nil {
|
||||
plaintext = sigil.Obfuscator.Deobfuscate(obfuscated, nonce)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
// Package sigil chains reversible byte transformations.
|
||||
//
|
||||
// hexSigil, _ := sigil.NewSigil("hex")
|
||||
// gzipSigil, _ := sigil.NewSigil("gzip")
|
||||
// encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
|
||||
// decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil})
|
||||
// hexSigil, _ := sigil.NewSigil("hex")
|
||||
// gzipSigil, _ := sigil.NewSigil("gzip")
|
||||
// encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
|
||||
// decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil})
|
||||
package sigil
|
||||
|
||||
import core "dappco.re/go/core"
|
||||
|
||||
// Sigil transforms byte slices.
|
||||
type Sigil interface {
|
||||
// Example: encoded, _ := hexSigil.In([]byte("payload"))
|
||||
In(data []byte) ([]byte, error)
|
||||
|
|
|
|||
|
|
@ -20,11 +20,8 @@ import (
|
|||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// ReverseSigil is a Sigil that reverses the bytes of the payload.
|
||||
// It is a symmetrical Sigil, meaning that the In and Out methods perform the same operation.
|
||||
type ReverseSigil struct{}
|
||||
|
||||
// In reverses the bytes of the data.
|
||||
func (sigil *ReverseSigil) In(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
|
|
@ -36,16 +33,12 @@ func (sigil *ReverseSigil) In(data []byte) ([]byte, error) {
|
|||
return reversed, nil
|
||||
}
|
||||
|
||||
// Out reverses the bytes of the data.
|
||||
func (sigil *ReverseSigil) Out(data []byte) ([]byte, error) {
|
||||
return sigil.In(data)
|
||||
}
|
||||
|
||||
// HexSigil is a Sigil that encodes/decodes data to/from hexadecimal.
|
||||
// The In method encodes the data, and the Out method decodes it.
|
||||
type HexSigil struct{}
|
||||
|
||||
// In encodes the data to hexadecimal.
|
||||
func (sigil *HexSigil) In(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
|
|
@ -55,7 +48,6 @@ func (sigil *HexSigil) In(data []byte) ([]byte, error) {
|
|||
return dst, nil
|
||||
}
|
||||
|
||||
// Out decodes the data from hexadecimal.
|
||||
func (sigil *HexSigil) Out(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
|
|
@ -65,11 +57,8 @@ func (sigil *HexSigil) Out(data []byte) ([]byte, error) {
|
|||
return dst, err
|
||||
}
|
||||
|
||||
// Base64Sigil is a Sigil that encodes/decodes data to/from base64.
|
||||
// The In method encodes the data, and the Out method decodes it.
|
||||
type Base64Sigil struct{}
|
||||
|
||||
// In encodes the data to base64.
|
||||
func (sigil *Base64Sigil) In(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
|
|
@ -79,7 +68,6 @@ func (sigil *Base64Sigil) In(data []byte) ([]byte, error) {
|
|||
return dst, nil
|
||||
}
|
||||
|
||||
// Out decodes the data from base64.
|
||||
func (sigil *Base64Sigil) Out(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
|
|
@ -89,13 +77,10 @@ func (sigil *Base64Sigil) Out(data []byte) ([]byte, error) {
|
|||
return dst[:n], err
|
||||
}
|
||||
|
||||
// GzipSigil is a Sigil that compresses/decompresses data using gzip.
|
||||
// The In method compresses the data, and the Out method decompresses it.
|
||||
type GzipSigil struct {
|
||||
outputWriter goio.Writer
|
||||
}
|
||||
|
||||
// In compresses the data using gzip.
|
||||
func (sigil *GzipSigil) In(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
|
|
@ -115,7 +100,6 @@ func (sigil *GzipSigil) In(data []byte) ([]byte, error) {
|
|||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// Out decompresses the data using gzip.
|
||||
func (sigil *GzipSigil) Out(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
|
|
@ -132,11 +116,8 @@ func (sigil *GzipSigil) Out(data []byte) ([]byte, error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// JSONSigil is a Sigil that compacts or indents JSON data.
|
||||
// The Out method is a no-op.
|
||||
type JSONSigil struct{ Indent bool }
|
||||
|
||||
// In compacts or indents the JSON data.
|
||||
func (sigil *JSONSigil) In(data []byte) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
|
|
@ -158,27 +139,20 @@ func (sigil *JSONSigil) In(data []byte) ([]byte, error) {
|
|||
return []byte(compact), nil
|
||||
}
|
||||
|
||||
// Out is a no-op for JSONSigil.
|
||||
func (sigil *JSONSigil) Out(data []byte) ([]byte, error) {
|
||||
// For simplicity, Out is a no-op. The primary use is formatting.
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// HashSigil is a Sigil that hashes the data using a specified algorithm.
|
||||
// The In method hashes the data, and the Out method is a no-op.
|
||||
type HashSigil struct {
|
||||
Hash crypto.Hash
|
||||
}
|
||||
|
||||
// Use NewHashSigil to hash payloads with a specific crypto.Hash.
|
||||
//
|
||||
// hashSigil := sigil.NewHashSigil(crypto.SHA256)
|
||||
// digest, _ := hashSigil.In([]byte("payload"))
|
||||
// hashSigil := sigil.NewHashSigil(crypto.SHA256)
|
||||
// digest, _ := hashSigil.In([]byte("payload"))
|
||||
func NewHashSigil(h crypto.Hash) *HashSigil {
|
||||
return &HashSigil{Hash: h}
|
||||
}
|
||||
|
||||
// In hashes the data.
|
||||
func (sigil *HashSigil) In(data []byte) ([]byte, error) {
|
||||
var hasher goio.Writer
|
||||
switch sigil.Hash {
|
||||
|
|
@ -219,7 +193,6 @@ func (sigil *HashSigil) In(data []byte) ([]byte, error) {
|
|||
case crypto.BLAKE2b_512:
|
||||
hasher, _ = blake2b.New512(nil)
|
||||
default:
|
||||
// MD5SHA1 is not supported as a direct hash
|
||||
return nil, core.E("sigil.HashSigil.In", "hash algorithm not available", nil)
|
||||
}
|
||||
|
||||
|
|
@ -227,16 +200,13 @@ func (sigil *HashSigil) In(data []byte) ([]byte, error) {
|
|||
return hasher.(interface{ Sum([]byte) []byte }).Sum(nil), nil
|
||||
}
|
||||
|
||||
// Out is a no-op for HashSigil.
|
||||
func (sigil *HashSigil) Out(data []byte) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Use NewSigil("hex") or NewSigil("gzip") to construct a sigil by name.
|
||||
//
|
||||
// hexSigil, _ := sigil.NewSigil("hex")
|
||||
// gzipSigil, _ := sigil.NewSigil("gzip")
|
||||
// transformed, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
|
||||
// hexSigil, _ := sigil.NewSigil("hex")
|
||||
// gzipSigil, _ := sigil.NewSigil("gzip")
|
||||
// transformed, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
|
||||
func NewSigil(name string) (Sigil, error) {
|
||||
switch name {
|
||||
case "reverse":
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
|
||||
_ "modernc.org/sqlite" // Pure Go SQLite driver
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Example: medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"})
|
||||
|
|
@ -26,9 +26,7 @@ type Medium struct {
|
|||
var _ coreio.Medium = (*Medium)(nil)
|
||||
|
||||
type Options struct {
|
||||
// Path is the SQLite database path. Use ":memory:" for tests.
|
||||
Path string
|
||||
// Table is the table name used for file storage. Empty defaults to "files".
|
||||
Path string
|
||||
Table string
|
||||
}
|
||||
|
||||
|
|
@ -564,8 +562,6 @@ func (medium *Medium) IsDir(filePath string) bool {
|
|||
return isDir
|
||||
}
|
||||
|
||||
// --- Internal types ---
|
||||
|
||||
type fileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
|
|
|
|||
10
store/doc.go
10
store/doc.go
|
|
@ -1,7 +1,5 @@
|
|||
// Package store maps grouped keys onto SQLite rows.
|
||||
//
|
||||
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
// _ = keyValueStore.Set("app", "theme", "midnight")
|
||||
// medium := keyValueStore.AsMedium()
|
||||
// _ = medium.Write("app/theme", "midnight")
|
||||
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
// _ = keyValueStore.Set("app", "theme", "midnight")
|
||||
// medium := keyValueStore.AsMedium()
|
||||
// _ = medium.Write("app/theme", "midnight")
|
||||
package store
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ func (medium *Medium) List(entryPath string) ([]fs.DirEntry, error) {
|
|||
}
|
||||
|
||||
if key != "" {
|
||||
return nil, nil // leaf node, nothing beneath
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
all, err := medium.store.GetAll(group)
|
||||
|
|
@ -276,8 +276,6 @@ func (medium *Medium) IsDir(entryPath string) bool {
|
|||
return err == nil && entryCount > 0
|
||||
}
|
||||
|
||||
// --- fs helper types ---
|
||||
|
||||
type keyValueFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
)
|
||||
|
||||
// Example: _, err := keyValueStore.Get("app", "theme")
|
||||
// err matches store.NotFoundError when the key is missing.
|
||||
var NotFoundError = errors.New("key not found")
|
||||
|
||||
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
|
||||
|
|
@ -20,7 +19,6 @@ type Store struct {
|
|||
}
|
||||
|
||||
type Options struct {
|
||||
// Path is the SQLite database path. Use ":memory:" for tests.
|
||||
Path string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
// Package workspace creates encrypted workspaces on top of io.Medium.
|
||||
//
|
||||
// Example: service, _ := workspace.New(workspace.Options{CryptProvider: cryptProvider})
|
||||
// workspaceID, _ := service.CreateWorkspace("alice", "pass123")
|
||||
// _ = service.SwitchWorkspace(workspaceID)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue