diff --git a/age/keysource.go b/age/keysource.go index 9c928d86a..1ad1b11e7 100644 --- a/age/keysource.go +++ b/age/keysource.go @@ -21,6 +21,7 @@ import ( "github.com/getsops/sops/v3/logging" "github.com/google/shlex" + "sync" ) const ( @@ -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") } @@ -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() @@ -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, } } @@ -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 { @@ -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, } } @@ -481,6 +519,7 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) { } } } + return identities, unusedLocations, errs } diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 330c3bc8e..939772b4d 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -75,6 +75,8 @@ func warnMoreThanOnePositionalArgument(c *cli.Context) { } func main() { + defer age.ClearFileStreamCache() + cli.VersionPrinter = version.PrintVersion app := cli.NewApp()