Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions age/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"filippo.io/age/armor"
"filippo.io/age/plugin"
"github.com/sirupsen/logrus"
"github.com/getsops/sops/v3/fsio"
"golang.org/x/crypto/ssh"

"github.com/getsops/sops/v3/logging"
Expand Down Expand Up @@ -423,7 +424,7 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
}

if ageKeyFile, ok := os.LookupEnv(SopsAgeKeyFileEnv); ok {
f, err := os.Open(ageKeyFile)
f, err := fsio.Open(ageKeyFile)
if err != nil {
errs = append(errs, fmt.Errorf("failed to open %s file: %w", SopsAgeKeyFileEnv, err))
} else {
Expand Down Expand Up @@ -456,7 +457,7 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
errs = append(errs, fmt.Errorf("user config directory could not be determined: %w", err))
} else if userConfigDir != "" {
ageKeyFilePath := filepath.Join(userConfigDir, filepath.FromSlash(SopsAgeKeyUserConfigPath))
f, err := os.Open(ageKeyFilePath)
f, err := fsio.Open(ageKeyFilePath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
errs = append(errs, fmt.Errorf("failed to open file: %w", err))
} else if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
Expand Down
17 changes: 3 additions & 14 deletions age/ssh_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ package age

import (
"fmt"
"io"
"os"

"filippo.io/age"
"filippo.io/age/agessh"
"github.com/getsops/sops/v3/fsio"
"golang.org/x/crypto/ssh"
)

Expand All @@ -26,15 +25,10 @@ import (
// error is returned.
func readPublicKeyFile(privateKeyPath string) (ssh.PublicKey, error) {
publicKeyPath := privateKeyPath + ".pub"
f, err := os.Open(publicKeyPath)
contents, err := fsio.Read(publicKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to obtain public %q key for %q SSH key: %w", publicKeyPath, privateKeyPath, err)
}
defer f.Close()
contents, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %w", publicKeyPath, err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(contents)
if err != nil {
return nil, fmt.Errorf("failed to parse %q: %w", publicKeyPath, err)
Expand All @@ -46,12 +40,7 @@ func readPublicKeyFile(privateKeyPath string) (ssh.PublicKey, error) {
// private key file. If the private key file is encrypted, it will configure
// the identity to prompt for a passphrase.
func parseSSHIdentityFromPrivateKeyFile(keyPath string) (age.Identity, error) {
keyFile, err := os.Open(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer keyFile.Close()
contents, err := io.ReadAll(keyFile)
contents, err := fsio.Read(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
publishcmd "github.com/getsops/sops/v3/cmd/sops/subcommand/publish"
"github.com/getsops/sops/v3/cmd/sops/subcommand/updatekeys"
"github.com/getsops/sops/v3/config"
"github.com/getsops/sops/v3/fsio"
"github.com/getsops/sops/v3/gcpkms"
"github.com/getsops/sops/v3/hckms"
"github.com/getsops/sops/v3/hcvault"
Expand Down Expand Up @@ -75,6 +76,8 @@ func warnMoreThanOnePositionalArgument(c *cli.Context) {
}

func main() {
defer fsio.ClearCache()

cli.VersionPrinter = version.PrintVersion
app := cli.NewApp()

Expand Down
88 changes: 88 additions & 0 deletions fsio/fsio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package fsio

import (
"bytes"
"io"
"os"
"path/filepath"
"sync"
)

type cacheEntry struct {
mu sync.RWMutex
data []byte
}

var fileStreamCache sync.Map

// ClearCache wipes the cached stream secrets from memory by overwriting
// the byte slices with zeros before deleting them from the map.
//
// If you are using SOPS as a library, you should call ClearCache after
// completing decryption/encryption operations to ensure no sensitive key
// data remains in memory.
func ClearCache() {
fileStreamCache.Range(func(key, value any) bool {
if entry, ok := value.(*cacheEntry); ok {
entry.mu.Lock()
for i := range entry.data {
entry.data[i] = 0
}
entry.mu.Unlock()
}
fileStreamCache.Delete(key)
return true
})
}

// Read reads a file from the given path. If it is a stream (e.g., /dev/fd/* or /proc/*)
// it caches the content in memory to avoid issues with multiple reads from the same stream.
func Read(path string) ([]byte, error) {
if absPath, err := filepath.Abs(path); err == nil {
path = absPath
}
fileInfo, err := os.Stat(path)
isStream := err == nil &&
(fileInfo.Mode()&os.ModeNamedPipe != 0 || fileInfo.Mode()&os.ModeCharDevice != 0 || fileInfo.Mode()&os.ModeSocket != 0)

if isStream {
if value, ok := fileStreamCache.Load(path); ok {
if entry, ok := value.(*cacheEntry); ok {
entry.mu.RLock()
defer entry.mu.RUnlock()
b := make([]byte, len(entry.data))
copy(b, entry.data)
return b, nil
}
}
}

b, err := os.ReadFile(path)
if err == nil && isStream {
cachedBytes := make([]byte, len(b))
copy(cachedBytes, b)
fileStreamCache.Store(path, &cacheEntry{data: cachedBytes})
}
return b, err
}

// Open opens a file from the given path. If it is a stream, it loads the content
// into the cache and returns a reader over the cached bytes.
func Open(path string) (io.ReadCloser, error) {
if absPath, err := filepath.Abs(path); err == nil {
path = absPath
}
fileInfo, err := os.Stat(path)
isStream := err == nil &&
(fileInfo.Mode()&os.ModeNamedPipe != 0 || fileInfo.Mode()&os.ModeCharDevice != 0 || fileInfo.Mode()&os.ModeSocket != 0)

if isStream {
b, err := Read(path)
if err != nil {
return nil, err
}
return io.NopCloser(bytes.NewReader(b)), nil
}

return os.Open(path)
}
31 changes: 31 additions & 0 deletions fsio/fsio_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package fsio

import (
"bytes"
"os"
"path/filepath"
"testing"
)

func TestReadRegularFile(t *testing.T) {
tempDir := t.TempDir()
filePath := filepath.Join(tempDir, "test.txt")
content := []byte("regular file content")

err := os.WriteFile(filePath, content, 0o600)
if err != nil {
t.Fatalf("failed to write temp file: %v", err)
}

b1, err := Read(filePath)
if err != nil {
t.Fatalf("Read failed: %v", err)
}
if !bytes.Equal(b1, content) {
t.Errorf("expected %q, got %q", content, b1)
}

if _, ok := fileStreamCache.Load(filePath); ok {
t.Error("expected regular file NOT to be cached, but it was")
}
}
Loading