latest
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestCLIInstanceStateDir(t *testing.T) {
|
||||
// Test the CLI instance state directory functionality
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
// Create a test state directory
|
||||
testStateDir := "/test-state-dir"
|
||||
cli := NewCLIInstanceWithStateDir(fs, testStateDir)
|
||||
|
||||
if cli.GetStateDir() != testStateDir {
|
||||
t.Errorf("Expected state directory %q, got %q", testStateDir, cli.GetStateDir())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIInstanceWithFs(t *testing.T) {
|
||||
// Test creating CLI instance with custom filesystem
|
||||
fs := afero.NewMemMapFs()
|
||||
cli := NewCLIInstanceWithFs(fs)
|
||||
|
||||
// The state directory should be determined automatically
|
||||
stateDir := cli.GetStateDir()
|
||||
if stateDir == "" {
|
||||
t.Error("Expected non-empty state directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineStateDir(t *testing.T) {
|
||||
// Test the determineStateDir function
|
||||
|
||||
// Save original environment and restore it after test
|
||||
originalStateDir := os.Getenv(EnvStateDir)
|
||||
defer func() {
|
||||
if originalStateDir == "" {
|
||||
os.Unsetenv(EnvStateDir)
|
||||
} else {
|
||||
os.Setenv(EnvStateDir, originalStateDir)
|
||||
}
|
||||
}()
|
||||
|
||||
// Test with environment variable set
|
||||
testEnvDir := "/test-env-dir"
|
||||
os.Setenv(EnvStateDir, testEnvDir)
|
||||
|
||||
stateDir := determineStateDir("")
|
||||
if stateDir != testEnvDir {
|
||||
t.Errorf("Expected state directory %q from environment, got %q", testEnvDir, stateDir)
|
||||
}
|
||||
|
||||
// Test with custom config dir
|
||||
os.Unsetenv(EnvStateDir)
|
||||
customConfigDir := "/custom-config"
|
||||
stateDir = determineStateDir(customConfigDir)
|
||||
expectedDir := filepath.Join(customConfigDir, AppID)
|
||||
if stateDir != expectedDir {
|
||||
t.Errorf("Expected state directory %q with custom config, got %q", expectedDir, stateDir)
|
||||
}
|
||||
}
|
||||
12
internal/secret/constants.go
Normal file
12
internal/secret/constants.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package secret
|
||||
|
||||
const (
|
||||
// AppID is the unique identifier for this application
|
||||
AppID = "berlin.sneak.pkg.secret"
|
||||
|
||||
// Environment variable names
|
||||
EnvStateDir = "SB_SECRET_STATE_DIR"
|
||||
EnvMnemonic = "SB_SECRET_MNEMONIC"
|
||||
EnvUnlockPassphrase = "SB_UNLOCK_PASSPHRASE"
|
||||
EnvGPGKeyID = "SB_GPG_KEY_ID"
|
||||
)
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// encryptToRecipient encrypts data to a recipient using age
|
||||
func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
||||
// EncryptToRecipient encrypts data to a recipient using age
|
||||
func EncryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
||||
Debug("encryptToRecipient starting", "data_length", len(data))
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -43,6 +43,11 @@ func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// encryptToRecipient encrypts data to a recipient using age (internal version)
|
||||
func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
|
||||
return EncryptToRecipient(data, recipient)
|
||||
}
|
||||
|
||||
// decryptWithIdentity decrypts data with an identity using age
|
||||
func decryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) {
|
||||
r, err := age.Decrypt(bytes.NewReader(data), identity)
|
||||
@@ -81,18 +86,18 @@ func decryptWithPassphrase(encryptedData []byte, passphrase string) ([]byte, err
|
||||
// readPassphrase reads a passphrase securely from the terminal without echoing
|
||||
// This version is for unlocking and doesn't require confirmation
|
||||
func readPassphrase(prompt string) (string, error) {
|
||||
// Check if stderr is a terminal - if not, we can't prompt interactively
|
||||
if !term.IsTerminal(int(syscall.Stderr)) {
|
||||
return "", fmt.Errorf("cannot prompt for passphrase: stderr is not a terminal (running in non-interactive mode)")
|
||||
}
|
||||
|
||||
// Check if stdin is a terminal
|
||||
if !term.IsTerminal(int(syscall.Stdin)) {
|
||||
// Not a terminal - use shared line reader to avoid buffering conflicts
|
||||
return readLineFromStdin(prompt)
|
||||
// Not a terminal - never read passphrases from piped input for security reasons
|
||||
return "", fmt.Errorf("cannot read passphrase from non-terminal stdin (piped input or script). Please set the SB_UNLOCK_PASSPHRASE environment variable or run interactively")
|
||||
}
|
||||
|
||||
// Terminal input - use secure password reading
|
||||
// stdin is a terminal, check if stderr is also a terminal for interactive prompting
|
||||
if !term.IsTerminal(int(syscall.Stderr)) {
|
||||
return "", fmt.Errorf("cannot prompt for passphrase: stderr is not a terminal (running in non-interactive mode). Please set the SB_UNLOCK_PASSPHRASE environment variable")
|
||||
}
|
||||
|
||||
// Both stdin and stderr are terminals - use secure password reading
|
||||
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
|
||||
passphrase, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
|
||||
@@ -33,7 +33,7 @@ func initDebugLogging() {
|
||||
}
|
||||
|
||||
// Disable stderr buffering for immediate debug output when debugging is enabled
|
||||
syscall.Syscall(syscall.SYS_FCNTL, os.Stderr.Fd(), syscall.F_SETFL, syscall.O_SYNC)
|
||||
_, _, _ = syscall.Syscall(syscall.SYS_FCNTL, os.Stderr.Fd(), syscall.F_SETFL, syscall.O_SYNC)
|
||||
|
||||
// Check if STDERR is a TTY
|
||||
isTTY := term.IsTerminal(int(syscall.Stderr))
|
||||
|
||||
54
internal/secret/helpers.go
Normal file
54
internal/secret/helpers.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// generateRandomString generates a random string of the specified length using the given character set
|
||||
func generateRandomString(length int, charset string) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", fmt.Errorf("length must be positive")
|
||||
}
|
||||
|
||||
result := make([]byte, length)
|
||||
charsetLen := big.NewInt(int64(len(charset)))
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
randomIndex, err := rand.Int(rand.Reader, charsetLen)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random number: %w", err)
|
||||
}
|
||||
result[i] = charset[randomIndex.Int64()]
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// DetermineStateDir determines the state directory based on environment variables and OS
|
||||
func DetermineStateDir(customConfigDir string) string {
|
||||
// Check for environment variable first
|
||||
if envStateDir := os.Getenv(EnvStateDir); envStateDir != "" {
|
||||
return envStateDir
|
||||
}
|
||||
|
||||
// Use custom config dir if provided
|
||||
if customConfigDir != "" {
|
||||
return filepath.Join(customConfigDir, AppID)
|
||||
}
|
||||
|
||||
// Use os.UserConfigDir() which handles platform-specific directories:
|
||||
// - On Unix systems, it returns $XDG_CONFIG_HOME or $HOME/.config
|
||||
// - On Darwin, it returns $HOME/Library/Application Support
|
||||
// - On Windows, it returns %AppData%
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
// Fallback to a reasonable default if we can't determine user config dir
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
return filepath.Join(homeDir, ".config", AppID)
|
||||
}
|
||||
return filepath.Join(configDir, AppID)
|
||||
}
|
||||
Reference in New Issue
Block a user