yoohoo
This commit is contained in:
267
internal/security/keystore.go
Normal file
267
internal/security/keystore.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// KeyStore handles secure storage and retrieval of API keys
|
||||
type KeyStore struct {
|
||||
configDir string
|
||||
}
|
||||
|
||||
// NewKeyStore creates a new KeyStore instance
|
||||
func NewKeyStore() (*KeyStore, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".config", "autotlp")
|
||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
return &KeyStore{
|
||||
configDir: configDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StoreAPIKey securely stores an API key
|
||||
func (ks *KeyStore) StoreAPIKey(provider, apiKey string) error {
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("API key cannot be empty")
|
||||
}
|
||||
|
||||
// Get or create master password
|
||||
masterPassword, err := ks.getMasterPassword()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get master password: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt the API key
|
||||
encryptedKey, err := ks.encrypt(apiKey, masterPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt API key: %w", err)
|
||||
}
|
||||
|
||||
// Store encrypted key
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
if err := os.WriteFile(keyFile, []byte(encryptedKey), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write key file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetrieveAPIKey retrieves and decrypts an API key
|
||||
func (ks *KeyStore) RetrieveAPIKey(provider string) (string, error) {
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
|
||||
// Check if key file exists
|
||||
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("API key not found for provider: %s", provider)
|
||||
}
|
||||
|
||||
// Read encrypted key
|
||||
encryptedKey, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read key file: %w", err)
|
||||
}
|
||||
|
||||
// Get master password
|
||||
masterPassword, err := ks.getMasterPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get master password: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt the API key
|
||||
apiKey, err := ks.decrypt(string(encryptedKey), masterPassword)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decrypt API key: %w", err)
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
// DeleteAPIKey removes a stored API key
|
||||
func (ks *KeyStore) DeleteAPIKey(provider string) error {
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
|
||||
if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete key file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListProviders returns a list of providers with stored keys
|
||||
func (ks *KeyStore) ListProviders() ([]string, error) {
|
||||
files, err := os.ReadDir(ks.configDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config directory: %w", err)
|
||||
}
|
||||
|
||||
var providers []string
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".key" {
|
||||
provider := file.Name()[:len(file.Name())-4] // Remove .key extension
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// getMasterPassword gets or creates a master password for encryption
|
||||
func (ks *KeyStore) getMasterPassword() (string, error) {
|
||||
passwordFile := filepath.Join(ks.configDir, ".master")
|
||||
|
||||
// Check if master password file exists
|
||||
if _, err := os.Stat(passwordFile); os.IsNotExist(err) {
|
||||
// Create new master password
|
||||
fmt.Print("Create a master password to secure your API keys: ")
|
||||
password, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print("Confirm master password: ")
|
||||
confirmPassword, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read confirmation password: %w", err)
|
||||
}
|
||||
|
||||
if password != confirmPassword {
|
||||
return "", fmt.Errorf("passwords do not match")
|
||||
}
|
||||
|
||||
// Hash and store the password
|
||||
hashedPassword := ks.hashPassword(password)
|
||||
if err := os.WriteFile(passwordFile, []byte(hashedPassword), 0600); err != nil {
|
||||
return "", fmt.Errorf("failed to store master password: %w", err)
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// Master password exists, prompt for it
|
||||
fmt.Print("Enter master password: ")
|
||||
password, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
|
||||
// Verify password
|
||||
storedHash, err := os.ReadFile(passwordFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read stored password hash: %w", err)
|
||||
}
|
||||
|
||||
if ks.hashPassword(password) != string(storedHash) {
|
||||
return "", fmt.Errorf("incorrect master password")
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// readPassword reads a password from stdin without echoing
|
||||
func (ks *KeyStore) readPassword() (string, error) {
|
||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println() // Add newline after password input
|
||||
return string(password), nil
|
||||
}
|
||||
|
||||
// hashPassword creates a hash of the password for verification
|
||||
func (ks *KeyStore) hashPassword(password string) string {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return base64.StdEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// encrypt encrypts data using AES-GCM
|
||||
func (ks *KeyStore) encrypt(plaintext, password string) (string, error) {
|
||||
// Create cipher
|
||||
key := sha256.Sum256([]byte(password))
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Generate nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decrypt decrypts data using AES-GCM
|
||||
func (ks *KeyStore) decrypt(ciphertext, password string) (string, error) {
|
||||
// Decode base64
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create cipher
|
||||
key := sha256.Sum256([]byte(password))
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract nonce
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext_bytes, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
// SecureInput prompts for secure input without echoing
|
||||
func SecureInput(prompt string) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
input, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println() // Add newline
|
||||
return string(input), nil
|
||||
}
|
249
internal/security/privileges.go
Normal file
249
internal/security/privileges.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
// PrivilegeManager handles privilege escalation and security operations
|
||||
type PrivilegeManager struct {
|
||||
logger *utils.Logger
|
||||
}
|
||||
|
||||
// NewPrivilegeManager creates a new privilege manager
|
||||
func NewPrivilegeManager(logger *utils.Logger) *PrivilegeManager {
|
||||
return &PrivilegeManager{
|
||||
logger: logger.WithComponent("security"),
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRoot ensures the current operation has root privileges
|
||||
func (pm *PrivilegeManager) RequireRoot(operation string) error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pm.logger.Info("Root privileges required", "operation", operation)
|
||||
return fmt.Errorf("operation '%s' requires root privileges", operation)
|
||||
}
|
||||
|
||||
// EscalateIfNeeded escalates privileges if not already running as root
|
||||
func (pm *PrivilegeManager) EscalateIfNeeded(args []string, operation string) error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pm.logger.Info("Escalating privileges", "operation", operation)
|
||||
|
||||
// Check if sudo is available
|
||||
if !utils.CommandExists("sudo") {
|
||||
return fmt.Errorf("sudo is required for privilege escalation but not available")
|
||||
}
|
||||
|
||||
// Inform user about privilege escalation
|
||||
fmt.Printf("\nPrivilege escalation required for: %s\n", operation)
|
||||
fmt.Println("This operation needs administrative privileges to:")
|
||||
|
||||
switch operation {
|
||||
case "install_tlp":
|
||||
fmt.Println("- Install TLP packages using system package manager")
|
||||
fmt.Println("- Enable and start TLP systemd service")
|
||||
fmt.Println("- Mask conflicting power management services")
|
||||
case "apply_config":
|
||||
fmt.Println("- Write TLP configuration to system directories")
|
||||
fmt.Println("- Reload TLP service with new configuration")
|
||||
case "system_info":
|
||||
fmt.Println("- Access hardware information from system files")
|
||||
fmt.Println("- Read power management settings")
|
||||
default:
|
||||
fmt.Printf("- Perform system operation: %s\n", operation)
|
||||
}
|
||||
|
||||
if !utils.GetUserConfirmation("Continue with privilege escalation?") {
|
||||
return fmt.Errorf("privilege escalation declined by user")
|
||||
}
|
||||
|
||||
// Execute with sudo
|
||||
return pm.executeSudo(args)
|
||||
}
|
||||
|
||||
// executeSudo executes the current program with sudo
|
||||
func (pm *PrivilegeManager) executeSudo(args []string) error {
|
||||
// Get current executable path
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Prepare sudo command
|
||||
sudoArgs := append([]string{executable}, args...)
|
||||
cmd := exec.Command("sudo", sudoArgs...)
|
||||
|
||||
// Preserve environment variables that might be needed
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// Connect stdio
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
pm.logger.Info("Executing with sudo", "command", cmd.String())
|
||||
|
||||
// Execute the command
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
// Get exit code from the elevated process
|
||||
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
||||
os.Exit(status.ExitStatus())
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("sudo execution failed: %w", err)
|
||||
}
|
||||
|
||||
// If we reach here, the elevated process completed successfully
|
||||
os.Exit(0)
|
||||
return nil // This line will never be reached
|
||||
}
|
||||
|
||||
// CheckSudoAccess verifies that the user can use sudo
|
||||
func (pm *PrivilegeManager) CheckSudoAccess() error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil // Already root
|
||||
}
|
||||
|
||||
if !utils.CommandExists("sudo") {
|
||||
return fmt.Errorf("sudo command not available")
|
||||
}
|
||||
|
||||
// Test sudo access with a harmless command
|
||||
cmd := exec.Command("sudo", "-n", "true")
|
||||
if err := cmd.Run(); err != nil {
|
||||
// -n flag failed, user needs to authenticate
|
||||
pm.logger.Debug("Sudo authentication required")
|
||||
return nil // This is expected for most users
|
||||
}
|
||||
|
||||
pm.logger.Debug("Sudo access confirmed without authentication")
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropPrivileges drops root privileges if running as root
|
||||
func (pm *PrivilegeManager) DropPrivileges() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return nil // Not running as root
|
||||
}
|
||||
|
||||
// Get the original user info from environment
|
||||
sudoUID := os.Getenv("SUDO_UID")
|
||||
sudoGID := os.Getenv("SUDO_GID")
|
||||
|
||||
if sudoUID == "" || sudoGID == "" {
|
||||
pm.logger.Warn("Cannot drop privileges: SUDO_UID/SUDO_GID not set")
|
||||
return nil // Don't fail, just log warning
|
||||
}
|
||||
|
||||
pm.logger.Info("Dropping root privileges", "uid", sudoUID, "gid", sudoGID)
|
||||
|
||||
// This is a placeholder - actual privilege dropping requires careful handling
|
||||
// and is typically done at the start of the program, not mid-execution
|
||||
pm.logger.Debug("Privilege dropping not implemented for mid-execution")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSystemAccess validates that the program has necessary system access
|
||||
func (pm *PrivilegeManager) ValidateSystemAccess() error {
|
||||
// Check read access to system information
|
||||
systemPaths := []string{
|
||||
"/proc/cpuinfo",
|
||||
"/proc/meminfo",
|
||||
"/sys/class/power_supply",
|
||||
"/sys/devices/system/cpu",
|
||||
}
|
||||
|
||||
for _, path := range systemPaths {
|
||||
if utils.FileExists(path) {
|
||||
// Try to read the file/directory
|
||||
if file, err := os.Open(path); err != nil {
|
||||
pm.logger.Warn("Limited access to system path", "path", path, "error", err)
|
||||
} else {
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SecureFilePermissions sets secure permissions on configuration files
|
||||
func (pm *PrivilegeManager) SecureFilePermissions(filePath string) error {
|
||||
// Set file permissions to be readable only by owner and group
|
||||
if err := os.Chmod(filePath, 0640); err != nil {
|
||||
return fmt.Errorf("failed to set secure permissions on %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
pm.logger.Debug("Set secure file permissions", "file", filePath, "mode", "0640")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateInput performs basic input validation for security
|
||||
func ValidateInput(input string, maxLength int, allowedChars string) error {
|
||||
if len(input) > maxLength {
|
||||
return fmt.Errorf("input too long: %d characters (max %d)", len(input), maxLength)
|
||||
}
|
||||
|
||||
if len(input) == 0 {
|
||||
return fmt.Errorf("input cannot be empty")
|
||||
}
|
||||
|
||||
// Basic validation against null bytes and control characters
|
||||
for i, r := range input {
|
||||
if r == 0 {
|
||||
return fmt.Errorf("null byte at position %d", i)
|
||||
}
|
||||
if r < 32 && r != 9 && r != 10 && r != 13 { // Allow tab, LF, CR
|
||||
return fmt.Errorf("control character at position %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeFilePath sanitizes file paths to prevent directory traversal
|
||||
func SanitizeFilePath(path string) (string, error) {
|
||||
// Basic path validation
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
// Check for directory traversal attempts
|
||||
if len(path) > 1 && (path[0] == '/' || path[1] == ':') {
|
||||
// Absolute path - this might be intentional, so we allow it
|
||||
// but log it for security awareness
|
||||
}
|
||||
|
||||
// Check for dangerous patterns
|
||||
dangerousPatterns := []string{
|
||||
"../",
|
||||
"..\\",
|
||||
"./",
|
||||
".\\",
|
||||
}
|
||||
|
||||
for _, pattern := range dangerousPatterns {
|
||||
if len(path) >= len(pattern) {
|
||||
for i := 0; i <= len(path)-len(pattern); i++ {
|
||||
if path[i:i+len(pattern)] == pattern {
|
||||
return "", fmt.Errorf("potentially dangerous path pattern: %s", pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
Reference in New Issue
Block a user