This commit is contained in:
2025-05-29 11:02:22 -07:00
parent 345709a306
commit e95609ce69
15 changed files with 1693 additions and 1588 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}
}

View 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"
)

View File

@@ -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 {

View File

@@ -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))

View 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)
}