Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/ei/ei_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"time"

Expand Down Expand Up @@ -507,8 +508,36 @@ func APICall(reqURL string, request proto.Message) []byte {
return decodedAuthBuf.Message
}

func getSecureHash(data []byte) (string, error) {
binaryPath := "./secure_hasher"
info, err := os.Stat(binaryPath)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("secure hasher binary not found at %s", binaryPath)
}
return "", fmt.Errorf("failed to stat secure hasher binary: %w", err)
}
if info.IsDir() {
return "", fmt.Errorf("secure hasher path is a directory: %s", binaryPath)
}
Comment on lines +512 to +522
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSecureHash hard-codes a relative path (./secure_hasher). This is brittle in production (depends on current working directory) and makes deployments harder to reason about. Consider making the hasher path configurable (config/env/flag) and/or resolving it to an absolute path once at startup, rather than re-stat'ing a relative path on every call.

Copilot uses AI. Check for mistakes.

cmd := exec.Command(binaryPath)

// Pipe the raw bytes into the binary's stdin
cmd.Stdin = bytes.NewReader(data)

var out bytes.Buffer
cmd.Stdout = &out

if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to execute hasher: %w", err)
Comment on lines +532 to +533
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On hasher failures, the error returned from cmd.Run() won't include any stderr output, which can make diagnosing deployment issues difficult. Consider capturing cmd.Stderr (buffer) and including it in the returned error when execution fails.

Suggested change
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to execute hasher: %w", err)
var errBuf bytes.Buffer
cmd.Stderr = &errBuf
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to execute hasher: %w; stderr: %s", err, errBuf.String())

Copilot uses AI. Check for mistakes.
}
Comment on lines +524 to +534
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec.Command(binaryPath) is run without any timeout/cancellation. If the external hasher hangs (or blocks on stdin), APIAuthenticatedCall will hang the request path. Use exec.CommandContext with a reasonable timeout (e.g., a few seconds) so the API call can fail fast and the bot remains responsive.

Copilot uses AI. Check for mistakes.

return strings.TrimSpace(out.String()), nil
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSecureHash returns whatever the external process writes to stdout after trimming whitespace, but APIAuthenticatedCall assumes it's a valid auth code. Add basic validation (e.g., non-empty, expected hex length/charset) and return an error if the output is malformed to avoid sending bad auth requests upstream.

Suggested change
return strings.TrimSpace(out.String()), nil
hash := strings.TrimSpace(out.String())
if hash == "" {
return "", fmt.Errorf("secure hasher returned empty output")
}
// Validate that the output looks like a hex-encoded hash (e.g., SHA-256).
for i := 0; i < len(hash); i++ {
c := hash[i]
if !((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F')) {
return "", fmt.Errorf("secure hasher returned non-hex character %q", c)
}
}
// Enforce an expected hash length (64 characters is typical for SHA-256).
if len(hash) != 64 {
return "", fmt.Errorf("secure hasher returned unexpected length %d", len(hash))
}
return hash, nil

Copilot uses AI. Check for mistakes.
}

// APIAuthenticatedCall wraps the request in an AuthenticatedMessage before sending, then decodes the response the same way as APICall.
func APIAuthenticatedCall(reqURL string, userID string, request proto.Message) []byte {
func APIAuthenticatedCall(reqURL string, request proto.Message) []byte {
enc := base64.StdEncoding

innerBin, err := proto.Marshal(request)
Expand All @@ -517,9 +546,15 @@ func APIAuthenticatedCall(reqURL string, userID string, request proto.Message) [
return nil
}

secureHash, err := getSecureHash(innerBin)
if err != nil {
log.Print(err)
return nil
}

authMsg := &AuthenticatedMessage{
Message: []byte(enc.EncodeToString(innerBin)),
UserId: &userID,
Message: innerBin,
Code: &secureHash,
}
Comment on lines 555 to 558
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change alters the request envelope semantics: AuthenticatedMessage.Message used to contain base64-encoded innerBin and UserId was populated; now the raw protobuf bytes are sent and user_id is omitted in favor of code. Because this is an external API contract, please ensure the server expects raw message bytes (not base64 text) and does not require user_id; otherwise these calls will fail at runtime. If both formats must be supported, consider keeping backward-compatible fields or gating behavior by endpoint/version.

Copilot uses AI. Check for mistakes.
Comment on lines 539 to 558
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIAuthenticatedCall now depends on executing an external binary (secure_hasher), but there are no tests covering the request-envelope creation (hash generation + AuthenticatedMessage fields). Since there are existing tests in this package, consider adding a unit test that verifies APIAuthenticatedCall builds the correct AuthenticatedMessage (ideally by injecting/mocking the hash function so tests don't shell out).

Copilot uses AI. Check for mistakes.

reqBin, err := proto.Marshal(authMsg)
Expand Down Expand Up @@ -578,9 +613,5 @@ func APIAuthenticatedCall(reqURL string, userID string, request proto.Message) [
decodedAuthBuf.Message = buf.Bytes()
}

if decodedAuthBuf.GetMessage() == nil {
decodedAuthBuf.Message = []byte(decodedAuthBuf.GetCode())
}

return decodedAuthBuf.Message
}
2 changes: 1 addition & 1 deletion src/ei/ei_api_leaderboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func GetLeaderboardFromAPI(eggIncID string, scope string, grade Contract_PlayerG
Grade: &grade,
}

payload := APIAuthenticatedCall(reqURL, eiUserID, &leaderboardRequest)
payload := APIAuthenticatedCall(reqURL, &leaderboardRequest)
if payload == nil {
log.Print("GetLeaderboardFromAPI: APICall returned nil response")
return nil
Expand Down
Loading