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
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 hcvault.ClearFileStreamCache()

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

Expand Down
80 changes: 63 additions & 17 deletions hcvault/keysource.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package hcvault

import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"time"

"github.com/hashicorp/vault/api"
Expand Down Expand Up @@ -107,6 +106,12 @@ var (
// defaultTokenFile is the name of the file in the user's home directory
// where a Vault token is expected to be stored.
defaultTokenFile = ".vault-token"
// SopsVaultTokenFileEnv can be set as an environment variable pointing to a
// vault token file.
SopsVaultTokenFileEnv = "SOPS_VAULT_TOKEN_FILE"
// vaultTokenStreamCache is a cache for vault token file streams, to avoid
// EOF errors when multiple vault keys attempt to read the same ephemeral token.
vaultTokenStreamCache sync.Map
)

// Token used for authenticating towards a Vault server.
Expand Down Expand Up @@ -431,30 +436,71 @@ func vaultClient(address, token string, hc *http.Client) (*api.Client, error) {
return client, nil
}

// userVaultToken returns the token from `$HOME/.vault-token` if the file
// exists. It returns an error if the file exists but cannot be read from.
// If the file does not exist, it returns an empty string.
// readTokenStreamSafe reads a file from the given path.
// If it is a stream, it reads the content and caches it in memory.
func readTokenStreamSafe(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 := vaultTokenStreamCache.Load(path); ok {
return cached.([]byte), nil
}
}

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

// ClearFileStreamCache clears the cache for vault token file streams
// zeroing out the byte slices before deleting them from the cache to prevent
// sensitive data from lingering in memory.
func ClearFileStreamCache() {
vaultTokenStreamCache.Range(func(key, value any) bool {
if byte, ok := value.([]byte); ok {
for i := range byte {
byte[i] = 0
}
}
vaultTokenStreamCache.Delete(key)
return true
})

}

// userVaultToken attempts to read the Vault token from the file specified in
// the SOPS_VAULT_TOKEN_FILE environment variable, or from the default location in
// $HOME/.vault-token if the environment variable is not set. It returns the
// token, or an error if the file cannot be read.
func userVaultToken() (string, error) {
homePath, err := homedir.Dir()
if err != nil {
return "", fmt.Errorf("error getting user's home directory: %w", err)
var tokenPath string
isTokenEnvSet := false

if tokenPathEnv, ok := os.LookupEnv(SopsVaultTokenFileEnv); ok && tokenPathEnv != "" {
tokenPath = tokenPathEnv
isTokenEnvSet = true
} else {
homePath, err := homedir.Dir()
if err != nil {
return "", fmt.Errorf("error getting user's home directory: %w", err)
}
tokenPath = filepath.Join(homePath, defaultTokenFile)
}
tokenPath := filepath.Join(homePath, defaultTokenFile)

f, err := os.Open(tokenPath)
b, err := readTokenStreamSafe(tokenPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
if isTokenEnvSet {
return "", fmt.Errorf("token file specified in %s does not exist: %w", SopsVaultTokenFileEnv, err)
}
return "", nil
}
return "", err
}
defer f.Close()

buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, f); err != nil {
return "", err
}
return strings.TrimSpace(buf.String()), nil
return strings.TrimSpace(string(b)), nil
}

// engineAndKeyFromPath returns the engine path and key name from the full
Expand Down
Loading