Skip to content
Open
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
57 changes: 48 additions & 9 deletions age/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/getsops/sops/v3/logging"
"github.com/google/shlex"
"sync"
)

const (
Expand Down Expand Up @@ -55,6 +56,8 @@ const (
// log is the global logger for any age MasterKey.
var log *logrus.Logger

var fileStreamCache sync.Map

func init() {
log = logging.NewLogger("AGE")
}
Expand Down Expand Up @@ -399,14 +402,48 @@ func getUserConfigDir() (string, error) {
return os.UserConfigDir()
}

// ClearFileStreamCache wipes the cached stream secrets from memory by overwriting
// the byte slices with zeros before deleting them from the map.
// This is critical for security to prevent keys from lingering in RAM.
func ClearFileStreamCache() {
fileStreamCache.Range(func(key, value interface{}) bool {
if byte, ok := value.([]byte); ok {
for i := range byte {
byte[i] = 0
}
}
fileStreamCache.Delete(key)
return true
})
}

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

if isStream {
if cached, ok := fileStreamCache.Load(path); ok {
return cached.([]byte), nil
}
}

b, err := os.ReadFile(path)
if err == nil && isStream {
fileStreamCache.Store(path, b)
}
return b, err
}

type identityReader struct {
reader io.Reader
allowMultipleKeysPerLine bool
}

// loadIdentities attempts to load the age identities based on runtime
// environment configurations (e.g. SopsAgeKeyEnv, SopsAgeKeyFileEnv,
// SopsAgeSshPrivateKeyFileEnv, SopsAgeKeyUserConfigPath). It will load all
// environment configurations (e.g. SopsAgeKeyUserConfigPath). It will load all
// found references, and expects at least one configuration to be present.
func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
identities, unusedLocations, errs := key.loadAgeSSHIdentities()
Expand All @@ -423,13 +460,12 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
}

if ageKeyFile, ok := os.LookupEnv(SopsAgeKeyFileEnv); ok {
f, err := os.Open(ageKeyFile)
b, err := readStreamSafe(ageKeyFile)
if err != nil {
errs = append(errs, fmt.Errorf("failed to open %s file: %w", SopsAgeKeyFileEnv, err))
} else {
defer f.Close()
readers[SopsAgeKeyFileEnv] = identityReader{
reader: f,
reader: bytes.NewReader(b),
allowMultipleKeysPerLine: false,
}
}
Expand All @@ -438,7 +474,10 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
}

if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok {
out, err := getOutputFromCmd(ageKeyCmd, []string{fmt.Sprintf("%s=%s", SopsAgeRecipientEnv, key.Recipient)})
out, err := getOutputFromCmd(
ageKeyCmd,
[]string{fmt.Sprintf("%s=%s", SopsAgeRecipientEnv, key.Recipient)},
)
if err != nil {
errs = append(errs, err)
} else {
Expand All @@ -456,15 +495,14 @@ 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)
b, err := readStreamSafe(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 {
unusedLocations = append(unusedLocations, ageKeyFilePath)
} else if err == nil {
defer f.Close()
readers[ageKeyFilePath] = identityReader{
reader: f,
reader: bytes.NewReader(b),
allowMultipleKeysPerLine: false,
}
}
Expand All @@ -481,6 +519,7 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
}
}
}

return identities, unusedLocations, errs
}

Expand Down
2 changes: 2 additions & 0 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func warnMoreThanOnePositionalArgument(c *cli.Context) {
}

func main() {
defer age.ClearFileStreamCache()

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

Expand Down