diff --git a/armor/main.go b/armor/main.go index a44f7d8..be8f70e 100644 --- a/armor/main.go +++ b/armor/main.go @@ -49,7 +49,8 @@ scanning: } else if line == "" { continue } else { - err = fmt.Errorf("no header line found. Found '%v'", line) + // Don't include the line content in error to avoid printing binary junk + err = fmt.Errorf("no header line found") return } case ARMOR_PARSER_STATE_HEADER: @@ -59,7 +60,8 @@ scanning: state = ARMOR_PARSER_STATE_BODY continue } else { - err = fmt.Errorf("no end of header line. Found '%v'", line) + // Don't include the line content in error to avoid printing binary junk + err = fmt.Errorf("no end of header line") return } case ARMOR_PARSER_STATE_BODY: diff --git a/armor/main_test.go b/armor/main_test.go index bcf51a1..97661e8 100644 --- a/armor/main_test.go +++ b/armor/main_test.go @@ -77,3 +77,17 @@ func TestBodyDecode(t *testing.T) { require.NoError(t, err) require.Equal(t, expected, actual) } + +func TestParseBinaryDataDoesNotPrintJunk(t *testing.T) { + // Test that binary data doesn't get included in error messages + binaryData, err := os.ReadFile("../test_data/key.gpg") + require.NoError(t, err) + + _, err = Parse(bytes.NewReader(binaryData)) + require.Error(t, err) + + // Check that the error message doesn't contain binary junk + // The error should be a clean message + require.Contains(t, err.Error(), "no header line found") + require.NotContains(t, err.Error(), string(binaryData[:10])) +} diff --git a/main.go b/main.go index ae06141..fa840df 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,12 @@ package main import ( + "bytes" "flag" "fmt" "github.com/ericsuh/adapt/aptfile" "github.com/ericsuh/adapt/armor" + "io" "log" "net/http" "os" @@ -15,6 +17,13 @@ import ( "strings" ) +const ( + // GPG packet tag for Public-Key Encrypted Session Key Packet (old format) + gpgPacketTagOldFormat = 0x95 + // GPG packet tag for Public-Key Encrypted Session Key Packet (new format) + gpgPacketTagNewFormat = 0x99 +) + var ensuredAddAptRepository bool = false var needsUpdate bool = true @@ -322,10 +331,27 @@ func downloadGPGKey(url, destPath string) error { log.Printf("Error closing response body: %v", err2) } }() - dearm, err := armor.Parse(resp.Body) + + // Read the entire body first so we can check if it's binary or ASCII-armored + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return err } + + // Try to parse as ASCII-armored format + dearm, err := armor.Parse(bytes.NewReader(bodyBytes)) + if err != nil { + // If parsing fails, check if it's already in binary format + // GPG binary files start with packet tags 0x95 (old format) or 0x99 (new format) + if len(bodyBytes) > 0 && (bodyBytes[0] == gpgPacketTagOldFormat || bodyBytes[0] == gpgPacketTagNewFormat) { + // Appears to be binary GPG format, use it directly + // Note: 0644 permissions are appropriate for public keys in /usr/share/keyrings/ + err = os.WriteFile(destPath, bodyBytes, 0644) + return err + } + // Not ASCII-armored and not recognized binary format + return fmt.Errorf("failed to parse GPG key: %w", err) + } err = os.WriteFile(destPath, dearm, 0644) return err } diff --git a/main_test.go b/main_test.go index 62ccb7c..a52b2d6 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,12 @@ package main import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" "testing" ) @@ -51,3 +57,79 @@ func TestSanitizeFilename(t *testing.T) { }) } } + +func TestDownloadGPGKeyHandlesBinary(t *testing.T) { + // Read the binary GPG key from test data + binaryKey, err := os.ReadFile("test_data/key.gpg") + if err != nil { + t.Fatalf("Failed to read test key: %v", err) + } + + // Create a test HTTP server that returns binary GPG key + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/octet-stream") + _, _ = io.Copy(w, bytes.NewReader(binaryKey)) + })) + defer ts.Close() + + // Create a temp file for destination + tempDir := t.TempDir() + destPath := filepath.Join(tempDir, "test.gpg") + + // Download the key + err = downloadGPGKey(ts.URL, destPath) + if err != nil { + t.Fatalf("downloadGPGKey failed: %v", err) + } + + // Verify the downloaded file matches the original + downloaded, err := os.ReadFile(destPath) + if err != nil { + t.Fatalf("Failed to read downloaded file: %v", err) + } + + if !bytes.Equal(downloaded, binaryKey) { + t.Errorf("Downloaded key doesn't match original") + } +} + +func TestDownloadGPGKeyHandlesAsciiArmored(t *testing.T) { + // Read the ASCII-armored GPG key from test data + armoredKey, err := os.ReadFile("test_data/key.gpg.asc") + if err != nil { + t.Fatalf("Failed to read test key: %v", err) + } + + // Read expected binary output + expectedBinary, err := os.ReadFile("test_data/key.gpg") + if err != nil { + t.Fatalf("Failed to read expected binary: %v", err) + } + + // Create a test HTTP server that returns ASCII-armored GPG key + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + _, _ = io.Copy(w, bytes.NewReader(armoredKey)) + })) + defer ts.Close() + + // Create a temp file for destination + tempDir := t.TempDir() + destPath := filepath.Join(tempDir, "test.gpg") + + // Download the key + err = downloadGPGKey(ts.URL, destPath) + if err != nil { + t.Fatalf("downloadGPGKey failed: %v", err) + } + + // Verify the downloaded file matches the expected binary + downloaded, err := os.ReadFile(destPath) + if err != nil { + t.Fatalf("Failed to read downloaded file: %v", err) + } + + if !bytes.Equal(downloaded, expectedBinary) { + t.Errorf("Downloaded key doesn't match expected binary") + } +}