116 lines
3.7 KiB
Go
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
|
|
}
|