From 23859ae0724818a93d599d0d833fedce26f583ca Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 28 Jun 2026 03:30:03 +0000 Subject: [PATCH] fix: preserve spaces in arguments during encryption/decryption - Base64 encode each argument individually before joining - Use newline as separator instead of space (newline won't appear in base64) - This fixes the issue where arguments with spaces were incorrectly split Fixes #19 --- cargs.go | 20 ++++++- cargs_test.go | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 2 + 4 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 cargs_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/cargs.go b/cargs.go index fdff06d..a5dda87 100644 --- a/cargs.go +++ b/cargs.go @@ -28,8 +28,13 @@ func Init(key []byte, flag string) { var newArgs []string newArgs = append(newArgs, os.Args[0]) if len(os.Args) > 2 && os.Args[1] == flag { - // encode args - input := []byte(strings.Join(os.Args[2:], " ")) + // encode args - base64 encode each argument individually to preserve spaces + // use newline as separator since it won't appear in base64 encoding + var encodedArgs []string + for _, arg := range os.Args[2:] { + encodedArgs = append(encodedArgs, base64.StdEncoding.EncodeToString([]byte(arg))) + } + input := []byte(strings.Join(encodedArgs, "\n")) output := make([]byte, len(input)) salsa20.XORKeyStream(output, input, nonce, &key32) fmt.Printf("cargs output: %s\n", base64.StdEncoding.EncodeToString(output)) @@ -42,7 +47,16 @@ func Init(key []byte, flag string) { } output := make([]byte, len(input)) salsa20.XORKeyStream(output, input, nonce, &key32) - os.Args = append(os.Args[:1], strings.Split(string(output), " ")...) + // decode args - split by newline and base64 decode each argument + var decodedArgs []string + for _, encodedArg := range strings.Split(string(output), "\n") { + decoded, err := base64.StdEncoding.DecodeString(encodedArg) + if err != nil { + os.Exit(1) + } + decodedArgs = append(decodedArgs, string(decoded)) + } + os.Args = append(os.Args[:1], decodedArgs...) } else { os.Exit(0) } diff --git a/cargs_test.go b/cargs_test.go new file mode 100644 index 0000000..9e6364c --- /dev/null +++ b/cargs_test.go @@ -0,0 +1,159 @@ +//go:build cargs + +package cargs + +import ( + "crypto/md5" + "encoding/base64" + "fmt" + "golang.org/x/crypto/salsa20" + "os" + "strings" + "testing" +) + +// Helper function to replicate the encryption logic for testing +func encryptArgs(args []string, key []byte, flag string) string { + keyA := md5.Sum(key) + keyB := md5.Sum(append(keyA[:], []byte(flag)...)) + key32 := [32]byte{} + copy(key32[0:], keyA[:]) + copy(key32[16:], keyB[:]) + nonce := key32[8:16] + + // Base64 encode each argument individually + // use newline as separator since it won't appear in base64 encoding + var encodedArgs []string + for _, arg := range args { + encodedArgs = append(encodedArgs, base64.StdEncoding.EncodeToString([]byte(arg))) + } + input := []byte(strings.Join(encodedArgs, "\n")) + output := make([]byte, len(input)) + salsa20.XORKeyStream(output, input, nonce, &key32) + return base64.StdEncoding.EncodeToString(output) +} + +// Helper function to replicate the decryption logic for testing +func decryptArgs(encrypted string, key []byte, flag string) []string { + keyA := md5.Sum(key) + keyB := md5.Sum(append(keyA[:], []byte(flag)...)) + key32 := [32]byte{} + copy(key32[0:], keyA[:]) + copy(key32[16:], keyB[:]) + nonce := key32[8:16] + + input, err := base64.StdEncoding.DecodeString(encrypted) + if err != nil { + return nil + } + output := make([]byte, len(input)) + salsa20.XORKeyStream(output, input, nonce, &key32) + + // decode args - split by newline and base64 decode each argument + var decodedArgs []string + for _, encodedArg := range strings.Split(string(output), "\n") { + decoded, err := base64.StdEncoding.DecodeString(encodedArg) + if err != nil { + return nil + } + decodedArgs = append(decodedArgs, string(decoded)) + } + return decodedArgs +} + +func TestArgsWithSpacesEncryptionDecryption(t *testing.T) { + key := []byte("cargsRandomKey") + flag := "getarg" + + testCases := []struct { + name string + args []string + }{ + { + name: "simple args without spaces", + args: []string{"--name", "john", "--age", "30"}, + }, + { + name: "args with single space", + args: []string{"--name", "hello world", "--msg", "test"}, + }, + { + name: "args with multiple consecutive spaces", + args: []string{"--msg", "multiple spaces", "--name", "john"}, + }, + { + name: "args with leading and trailing spaces", + args: []string{"--msg", " leading space", "--name", "trailing space "}, + }, + { + name: "empty string argument", + args: []string{"--empty", "", "--name", "test"}, + }, + { + name: "all special characters", + args: []string{"--msg", "!@#$%^&*()", "--path", "/usr/local/bin"}, + }, + { + name: "realistic command line", + args: []string{"--config", "/path/to/config file.json", "--name", "My Application"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + encrypted := encryptArgs(tc.args, key, flag) + decrypted := decryptArgs(encrypted, key, flag) + + if len(decrypted) != len(tc.args) { + t.Errorf("Expected %d args, got %d", len(tc.args), len(decrypted)) + t.Errorf("Original: %v", tc.args) + t.Errorf("Decrypted: %v", decrypted) + } + + for i, arg := range tc.args { + if i >= len(decrypted) { + t.Errorf("Missing argument at index %d: expected %q", i, arg) + continue + } + if decrypted[i] != arg { + t.Errorf("Arg %d mismatch:\n expected: %q\n got: %q", i, arg, decrypted[i]) + } + } + }) + } +} + +func TestFullCycle(t *testing.T) { + // Simulate the full cycle: encrypt then decrypt + programName := "/path/to/program" + key := []byte("testEncryptionKey123") + flag := "run" + + originalArgs := []string{programName, flag, "--input", "hello world", "--output", "my output file.txt", "--verbose"} + + // Step 1: Simulate encryption (what happens when user runs: program run --input "hello world" ...) + os.Args = originalArgs + + // Capture the encrypted output + var encryptedOutput string + fmt.Fprintf(os.Stdout, "") // Ensure stdout is available + + // Use our helper to encrypt + argsToEncrypt := os.Args[2:] // Skip program name and flag + encryptedOutput = encryptArgs(argsToEncrypt, key, flag) + + // Step 2: Simulate decryption (what happens when user runs: program ) + decryptedArgs := decryptArgs(encryptedOutput, key, flag) + + // Verify the decrypted args match the original args + if len(decryptedArgs) != len(argsToEncrypt) { + t.Fatalf("Arg count mismatch: expected %d, got %d", len(argsToEncrypt), len(decryptedArgs)) + } + + for i := range argsToEncrypt { + if decryptedArgs[i] != argsToEncrypt[i] { + t.Errorf("Arg %d mismatch:\n expected: %q\n got: %q", i, argsToEncrypt[i], decryptedArgs[i]) + } + } +} + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..28f789b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/12end/cargs + +go 1.21 + +require golang.org/x/crypto v0.31.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9615005 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=