From 044af60699f8da7d0c923c1091c6f0007f0c857c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Sun, 17 Aug 2025 16:26:28 +0200 Subject: [PATCH] steam-crypt app and fix the interactive executor --- cmd/steam-crypt/main.go | 93 +++++++++++++++++++++++ local/utl/command/interactive_executor.go | 79 ++++++++++++++++--- 2 files changed, 162 insertions(+), 10 deletions(-) create mode 100644 cmd/steam-crypt/main.go diff --git a/cmd/steam-crypt/main.go b/cmd/steam-crypt/main.go new file mode 100644 index 0000000..c78499e --- /dev/null +++ b/cmd/steam-crypt/main.go @@ -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") +} \ No newline at end of file diff --git a/local/utl/command/interactive_executor.go b/local/utl/command/interactive_executor.go index 675712a..4b4efa5 100644 --- a/local/utl/command/interactive_executor.go +++ b/local/utl/command/interactive_executor.go @@ -61,14 +61,33 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser } // 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 go e.monitorOutput(ctx, stdout, stderr, serverID, outputDone) - // Wait for either the command to finish or output monitoring to complete - cmdErr := cmd.Wait() - outputErr := <-outputDone + // Wait for the command to finish in a separate goroutine + go func() { + 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 { 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) { - defer close(done) + defer func() { + select { + case done <- nil: + default: + } + }() // Create scanners for both outputs stdoutScanner := bufio.NewScanner(stdout) 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 go func() { + defer func() { readersDone <- struct{}{} }() for stdoutScanner.Scan() { line := stdoutScanner.Text() if e.LogOutput { 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 go func() { + defer func() { readersDone <- struct{}{} }() for stderrScanner.Scan() { line := stderrScanner.Text() if e.LogOutput { 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 { select { case <-ctx.Done(): done <- ctx.Err() 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: if !ok { - done <- nil + // Channel closed, we're done return }