steam-crypt app and fix the interactive executor
All checks were successful
Release and Deploy / build (push) Successful in 2m2s
Release and Deploy / deploy (push) Successful in 22s

This commit is contained in:
Fran Jurmanović
2025-08-17 16:26:28 +02:00
parent 384036bcdd
commit 044af60699
2 changed files with 162 additions and 10 deletions

93
cmd/steam-crypt/main.go Normal file
View File

@@ -0,0 +1,93 @@
package main
import (
"acc-server-manager/local/model"
"acc-server-manager/local/utl/configs"
"flag"
"fmt"
"os"
)
func main() {
var (
encrypt = flag.Bool("encrypt", false, "Encrypt a password")
decrypt = flag.Bool("decrypt", false, "Decrypt a password")
password = flag.String("password", "", "Password to encrypt/decrypt")
help = flag.Bool("help", false, "Show help")
)
flag.Parse()
if *help || (!*encrypt && !*decrypt) {
showHelp()
return
}
if *encrypt && *decrypt {
fmt.Fprintf(os.Stderr, "Error: Cannot specify both -encrypt and -decrypt\n")
os.Exit(1)
}
if *password == "" {
fmt.Fprintf(os.Stderr, "Error: Password is required\n")
showHelp()
os.Exit(1)
}
// Initialize configs to load encryption key
configs.Init()
if *encrypt {
encrypted, err := model.EncryptPassword(*password)
if err != nil {
fmt.Fprintf(os.Stderr, "Error encrypting password: %v\n", err)
os.Exit(1)
}
fmt.Println(encrypted)
}
if *decrypt {
decrypted, err := model.DecryptPassword(*password)
if err != nil {
fmt.Fprintf(os.Stderr, "Error decrypting password: %v\n", err)
os.Exit(1)
}
fmt.Println(decrypted)
}
}
func showHelp() {
fmt.Println("Steam Credentials Encryption/Decryption Utility")
fmt.Println()
fmt.Println("This utility encrypts and decrypts Steam credentials using the same")
fmt.Println("AES-256-GCM encryption used by the ACC Server Manager application.")
fmt.Println()
fmt.Println("Usage:")
fmt.Println(" steam-crypt -encrypt -password \"your_password\"")
fmt.Println(" steam-crypt -decrypt -password \"encrypted_string\"")
fmt.Println()
fmt.Println("Options:")
fmt.Println(" -encrypt Encrypt the provided password")
fmt.Println(" -decrypt Decrypt the provided encrypted string")
fmt.Println(" -password The password to encrypt or encrypted string to decrypt")
fmt.Println(" -help Show this help message")
fmt.Println()
fmt.Println("Environment Variables Required:")
fmt.Println(" ENCRYPTION_KEY - 32-byte encryption key (same as main application)")
fmt.Println(" APP_SECRET - Application secret (required by configs)")
fmt.Println(" APP_SECRET_CODE - Application secret code (required by configs)")
fmt.Println(" ACCESS_KEY - Access key (required by configs)")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" # Encrypt a password")
fmt.Println(" steam-crypt -encrypt -password \"mysteampassword\"")
fmt.Println()
fmt.Println(" # Decrypt an encrypted password")
fmt.Println(" steam-crypt -decrypt -password \"base64encryptedstring\"")
fmt.Println()
fmt.Println("Security Notes:")
fmt.Println(" - The encryption key must be exactly 32 bytes for AES-256")
fmt.Println(" - Uses AES-256-GCM for authenticated encryption")
fmt.Println(" - Each encryption includes a unique nonce for security")
fmt.Println(" - Passwords are validated for length and basic security")
}

View File

@@ -61,14 +61,33 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
} }
// Create channels for output monitoring // Create channels for output monitoring
outputDone := make(chan error) outputDone := make(chan error, 1)
cmdDone := make(chan error, 1)
// Monitor stdout and stderr for 2FA prompts // Monitor stdout and stderr for 2FA prompts
go e.monitorOutput(ctx, stdout, stderr, serverID, outputDone) go e.monitorOutput(ctx, stdout, stderr, serverID, outputDone)
// Wait for either the command to finish or output monitoring to complete // Wait for the command to finish in a separate goroutine
cmdErr := cmd.Wait() go func() {
outputErr := <-outputDone cmdDone <- cmd.Wait()
}()
// Wait for both command and output monitoring to complete
var cmdErr, outputErr error
completedCount := 0
for completedCount < 2 {
select {
case cmdErr = <-cmdDone:
completedCount++
logging.Info("Command execution completed")
case outputErr = <-outputDone:
completedCount++
logging.Info("Output monitoring completed")
case <-ctx.Done():
return ctx.Err()
}
}
if outputErr != nil { if outputErr != nil {
logging.Warn("Output monitoring error: %v", outputErr) logging.Warn("Output monitoring error: %v", outputErr)
@@ -78,45 +97,85 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
} }
func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) { func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) {
defer close(done) defer func() {
select {
case done <- nil:
default:
}
}()
// Create scanners for both outputs // Create scanners for both outputs
stdoutScanner := bufio.NewScanner(stdout) stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr) stderrScanner := bufio.NewScanner(stderr)
outputChan := make(chan string) outputChan := make(chan string, 100) // Buffered channel to prevent blocking
readersDone := make(chan struct{}, 2)
// Read from stdout // Read from stdout
go func() { go func() {
defer func() { readersDone <- struct{}{} }()
for stdoutScanner.Scan() { for stdoutScanner.Scan() {
line := stdoutScanner.Text() line := stdoutScanner.Text()
if e.LogOutput { if e.LogOutput {
logging.Info("STDOUT: %s", line) logging.Info("STDOUT: %s", line)
} }
outputChan <- line select {
case outputChan <- line:
case <-ctx.Done():
return
}
}
if err := stdoutScanner.Err(); err != nil {
logging.Warn("Stdout scanner error: %v", err)
} }
}() }()
// Read from stderr // Read from stderr
go func() { go func() {
defer func() { readersDone <- struct{}{} }()
for stderrScanner.Scan() { for stderrScanner.Scan() {
line := stderrScanner.Text() line := stderrScanner.Text()
if e.LogOutput { if e.LogOutput {
logging.Info("STDERR: %s", line) logging.Info("STDERR: %s", line)
} }
outputChan <- line select {
case outputChan <- line:
case <-ctx.Done():
return
}
}
if err := stderrScanner.Err(); err != nil {
logging.Warn("Stderr scanner error: %v", err)
} }
}() }()
// Monitor for 2FA prompts // Monitor for completion and 2FA prompts
readersFinished := 0
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
done <- ctx.Err() done <- ctx.Err()
return return
case <-readersDone:
readersFinished++
if readersFinished == 2 {
// Both readers are done, close output channel and finish monitoring
close(outputChan)
// Drain any remaining output
for line := range outputChan {
if e.is2FAPrompt(line) {
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
done <- err
return
}
}
}
return
}
case line, ok := <-outputChan: case line, ok := <-outputChan:
if !ok { if !ok {
done <- nil // Channel closed, we're done
return return
} }