116 lines
3.7 KiB
Go

package security
import (
"bufio"
"crypto/rand"
"fmt"
"log"
"os"
"strings"
)
// GetOrCreatePassword retrieves a password from kopia.env or creates a new one if it doesn't exist
func GetOrCreatePassword(serviceName string, length int) (string, error) {
// Define the expected key in the env file
// Replace dots with underscores for environment variable name
safeServiceName := strings.ReplaceAll(serviceName, ".", "_")
passwordKey := fmt.Sprintf("KOPIA_%s_PASSWORD", strings.ToUpper(safeServiceName))
// Try to read from kopia.env first
kopiaEnvPath := "kopia.env"
if _, err := os.Stat(kopiaEnvPath); err == nil {
// File exists, check if the password is already there
file, err := os.Open(kopiaEnvPath)
if err != nil {
return "", fmt.Errorf("failed to open kopia.env: %w", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, passwordKey+"=") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
// Found the password, remove quotes if present
password := parts[1]
password = strings.Trim(password, "\"")
return password, nil
}
}
}
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("error reading kopia.env: %w", err)
}
}
// Password not found or file doesn't exist, generate a new password
password, err := GenerateSecurePassword(length)
if err != nil {
return "", fmt.Errorf("failed to generate password: %w", err)
}
// Create or append to kopia.env
file, err := os.OpenFile(kopiaEnvPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return "", fmt.Errorf("failed to open kopia.env for writing: %w", err)
}
defer file.Close()
// Write the new password entry with quotes
passwordEntry := fmt.Sprintf("%s=\"%s\"\n", passwordKey, password)
if _, err := file.WriteString(passwordEntry); err != nil {
return "", fmt.Errorf("failed to write to kopia.env: %w", err)
}
log.Printf("Created new password for service %s and stored in kopia.env", serviceName)
return password, nil
}
// GenerateSecurePassword creates a cryptographically secure random password
// using only safe characters that work well with command line tools
func GenerateSecurePassword(length int) (string, error) {
// Use a more robust but safe character set
// Avoiding characters that might cause shell interpretation issues
// No quotes, backslashes, spaces, or common special chars that need escaping
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
result := make([]byte, length)
// Create a secure source of randomness
randomBytes := make([]byte, length)
if _, err := rand.Read(randomBytes); err != nil {
return "", err
}
// Ensure minimum complexity requirements
// At least 2 uppercase, 2 lowercase, 2 digits
if length >= 6 {
// Force first characters to meet minimum requirements
result[0] = 'A' + byte(randomBytes[0]%26) // Uppercase
result[1] = 'A' + byte(randomBytes[1]%26) // Uppercase
result[2] = 'a' + byte(randomBytes[2]%26) // Lowercase
result[3] = 'a' + byte(randomBytes[3]%26) // Lowercase
result[4] = '0' + byte(randomBytes[4]%10) // Digit
result[5] = '0' + byte(randomBytes[5]%10) // Digit
// Fill the rest with random chars
for i := 6; i < length; i++ {
result[i] = chars[int(randomBytes[i])%len(chars)]
}
// Shuffle the result to avoid predictable pattern
for i := length - 1; i > 0; i-- {
j := int(randomBytes[i]) % (i + 1)
result[i], result[j] = result[j], result[i]
}
} else {
// For very short passwords just use random chars
for i := 0; i < length; i++ {
result[i] = chars[int(randomBytes[i])%len(chars)]
}
}
return string(result), nil
}