diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index c260aca..61daf9c 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -13,4 +13,4 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13925f8d5 # v4 - name: Build (includes make check) - run: docker build . + run: docker build --ulimit memlock=-1:-1 . diff --git a/Dockerfile b/Dockerfile index a9346f6..63a0210 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,8 @@ RUN apk add --no-cache \ gcc \ musl-dev \ make \ - git + git \ + gnupg # Set working directory WORKDIR /build @@ -20,8 +21,8 @@ RUN go mod download # Copy source code COPY . . -# Install golangci-lint for checks -RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee +# Install golangci-lint for checks (binary install to avoid Go version constraints) +RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6 # Run all checks (lint, vet, test, build) RUN make check diff --git a/internal/cli/integration_test.go b/internal/cli/integration_test.go index d995e58..16e634c 100644 --- a/internal/cli/integration_test.go +++ b/internal/cli/integration_test.go @@ -48,7 +48,7 @@ func TestMain(m *testing.M) { code := m.Run() // Clean up the binary - os.Remove(filepath.Join(projectRoot, "secret")) + _ = os.Remove(filepath.Join(projectRoot, "secret")) os.Exit(code) } @@ -450,10 +450,10 @@ func test02ListVaults(t *testing.T, runSecret func(...string) (string, error)) { func test03CreateVault(t *testing.T, tempDir string, runSecret func(...string) (string, error)) { // Set environment variables for vault creation - os.Setenv("SB_SECRET_MNEMONIC", testMnemonic) - os.Setenv("SB_UNLOCK_PASSPHRASE", "test-passphrase") - defer os.Unsetenv("SB_SECRET_MNEMONIC") - defer os.Unsetenv("SB_UNLOCK_PASSPHRASE") + _ = os.Setenv("SB_SECRET_MNEMONIC", testMnemonic) + _ = os.Setenv("SB_UNLOCK_PASSPHRASE", "test-passphrase") + defer func() { _ = os.Unsetenv("SB_SECRET_MNEMONIC") }() + defer func() { _ = os.Unsetenv("SB_UNLOCK_PASSPHRASE") }() // Create work vault output, err := runSecret("vault", "create", "work") @@ -489,6 +489,7 @@ func test03CreateVault(t *testing.T, tempDir string, runSecret func(...string) ( assert.Contains(t, output, "work", "should list work vault") } +//nolint:unused // TODO: re-enable when vault import is implemented func test04ImportMnemonic(t *testing.T, tempDir, testMnemonic, testPassphrase string, runSecretWithEnv func(map[string]string, ...string) (string, error)) { // Import mnemonic into work vault output, err := runSecretWithEnv(map[string]string{ @@ -1667,9 +1668,9 @@ func test19DisasterRecovery(t *testing.T, tempDir, secretPath, testMnemonic stri assert.Equal(t, testSecretValue, strings.TrimSpace(toolOutput), "tool output should match original") // Clean up temporary files - os.Remove(ltPrivKeyPath) - os.Remove(versionPrivKeyPath) - os.Remove(decryptedValuePath) + _ = os.Remove(ltPrivKeyPath) + _ = os.Remove(versionPrivKeyPath) + _ = os.Remove(decryptedValuePath) } func test20VersionTimestamps(t *testing.T, tempDir, secretPath, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error)) { @@ -1788,7 +1789,7 @@ func test23ErrorHandling(t *testing.T, tempDir, secretPath, testMnemonic string, // Add secret without mnemonic or unlocker unsetMnemonic := os.Getenv("SB_SECRET_MNEMONIC") - os.Unsetenv("SB_SECRET_MNEMONIC") + _ = os.Unsetenv("SB_SECRET_MNEMONIC") cmd := exec.Command(secretPath, "add", "test/nomnemonic") cmd.Env = []string{ fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir), @@ -2128,7 +2129,7 @@ func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string, versionsPath := filepath.Join(secretPath, "versions") if _, err := os.Stat(versionsPath); os.IsNotExist(err) { // This is a malformed secret directory, remove it - os.RemoveAll(secretPath) + _ = os.RemoveAll(secretPath) } } } @@ -2178,7 +2179,7 @@ func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string, require.NoError(t, err, "restore vaults should succeed") // Restore currentvault - os.Remove(currentVaultSrc) + _ = os.Remove(currentVaultSrc) restoredData := readFile(t, currentVaultDst) writeFile(t, currentVaultSrc, restoredData) @@ -2284,6 +2285,7 @@ func verifyFileExists(t *testing.T, path string) { } // verifyFileNotExists checks if a file does not exist at the given path +//nolint:unused // kept for future use func verifyFileNotExists(t *testing.T, path string) { t.Helper() _, err := os.Stat(path) diff --git a/internal/secret/derivation_index_test.go b/internal/secret/derivation_index_test.go index ad86553..3835f7f 100644 --- a/internal/secret/derivation_index_test.go +++ b/internal/secret/derivation_index_test.go @@ -1,3 +1,5 @@ +//go:build darwin + package secret import ( diff --git a/internal/secret/helpers.go b/internal/secret/helpers.go index 580aba8..5321f37 100644 --- a/internal/secret/helpers.go +++ b/internal/secret/helpers.go @@ -1,33 +1,11 @@ package secret import ( - "crypto/rand" "fmt" - "math/big" "os" "path/filepath" ) -// generateRandomString generates a random string of the specified length using the given character set -func generateRandomString(length int, charset string) (string, error) { - if length <= 0 { - return "", fmt.Errorf("length must be positive") - } - - result := make([]byte, length) - charsetLen := big.NewInt(int64(len(charset))) - - for i := range length { - randomIndex, err := rand.Int(rand.Reader, charsetLen) - if err != nil { - return "", fmt.Errorf("failed to generate random number: %w", err) - } - result[i] = charset[randomIndex.Int64()] - } - - return string(result), nil -} - // DetermineStateDir determines the state directory based on environment variables and OS. // It returns an error if no usable directory can be determined. func DetermineStateDir(customConfigDir string) (string, error) { diff --git a/internal/secret/helpers_darwin.go b/internal/secret/helpers_darwin.go new file mode 100644 index 0000000..435e665 --- /dev/null +++ b/internal/secret/helpers_darwin.go @@ -0,0 +1,29 @@ +//go:build darwin + +package secret + +import ( + "crypto/rand" + "fmt" + "math/big" +) + +// generateRandomString generates a random string of the specified length using the given character set +func generateRandomString(length int, charset string) (string, error) { + if length <= 0 { + return "", fmt.Errorf("length must be positive") + } + + result := make([]byte, length) + charsetLen := big.NewInt(int64(len(charset))) + + for i := range length { + randomIndex, err := rand.Int(rand.Reader, charsetLen) + if err != nil { + return "", fmt.Errorf("failed to generate random number: %w", err) + } + result[i] = charset[randomIndex.Int64()] + } + + return string(result), nil +} diff --git a/internal/secret/passphrase_test.go b/internal/secret/passphrase_test.go index c00ab14..a0a2167 100644 --- a/internal/secret/passphrase_test.go +++ b/internal/secret/passphrase_test.go @@ -24,7 +24,7 @@ func TestPassphraseUnlockerWithRealFS(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tempDir) // Clean up after test + defer func() { _ = os.RemoveAll(tempDir) }() // Clean up after test // Use the real filesystem fs := afero.NewOsFs() @@ -155,7 +155,7 @@ func TestPassphraseUnlockerWithRealFS(t *testing.T) { }) // Unset the environment variable to test interactive prompt - os.Unsetenv(secret.EnvUnlockPassphrase) + _ = os.Unsetenv(secret.EnvUnlockPassphrase) // Test getting identity from prompt (this would require mocking the prompt) // For real integration tests, we'd need to provide a way to mock the passphrase input diff --git a/internal/secret/pgpunlock_test.go b/internal/secret/pgpunlock_test.go index bdf278f..1291534 100644 --- a/internal/secret/pgpunlock_test.go +++ b/internal/secret/pgpunlock_test.go @@ -1,3 +1,5 @@ +//go:build darwin + package secret_test import ( @@ -140,7 +142,7 @@ func TestPGPUnlockerWithRealFS(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tempDir) // Clean up after test + defer func() { _ = os.RemoveAll(tempDir) }() // Clean up after test // Create a temporary GNUPGHOME gnupgHomeDir := filepath.Join(tempDir, "gnupg") diff --git a/internal/secret/validation_darwin_test.go b/internal/secret/validation_darwin_test.go new file mode 100644 index 0000000..6c6c32d --- /dev/null +++ b/internal/secret/validation_darwin_test.go @@ -0,0 +1,148 @@ +//go:build darwin + +package secret + +import ( + "testing" +) + +func TestValidateKeychainItemName(t *testing.T) { + tests := []struct { + name string + itemName string + wantErr bool + }{ + // Valid cases + { + name: "valid simple name", + itemName: "my-secret-key", + wantErr: false, + }, + { + name: "valid name with dots", + itemName: "com.example.app.key", + wantErr: false, + }, + { + name: "valid name with underscores", + itemName: "my_secret_key_123", + wantErr: false, + }, + { + name: "valid alphanumeric", + itemName: "Secret123Key", + wantErr: false, + }, + { + name: "valid with hyphen at start", + itemName: "-my-key", + wantErr: false, + }, + { + name: "valid with dot at start", + itemName: ".hidden-key", + wantErr: false, + }, + + // Invalid cases + { + name: "empty item name", + itemName: "", + wantErr: true, + }, + { + name: "item name with spaces", + itemName: "my secret key", + wantErr: true, + }, + { + name: "item name with semicolon", + itemName: "key;rm -rf /", + wantErr: true, + }, + { + name: "item name with pipe", + itemName: "key|cat /etc/passwd", + wantErr: true, + }, + { + name: "item name with backticks", + itemName: "key`whoami`", + wantErr: true, + }, + { + name: "item name with dollar sign", + itemName: "key$(whoami)", + wantErr: true, + }, + { + name: "item name with quotes", + itemName: "key\"name", + wantErr: true, + }, + { + name: "item name with single quotes", + itemName: "key'name", + wantErr: true, + }, + { + name: "item name with backslash", + itemName: "key\\name", + wantErr: true, + }, + { + name: "item name with newline", + itemName: "key\nname", + wantErr: true, + }, + { + name: "item name with carriage return", + itemName: "key\rname", + wantErr: true, + }, + { + name: "item name with ampersand", + itemName: "key&echo test", + wantErr: true, + }, + { + name: "item name with redirect", + itemName: "key>/tmp/test", + wantErr: true, + }, + { + name: "item name with null byte", + itemName: "key\x00name", + wantErr: true, + }, + { + name: "item name with parentheses", + itemName: "key(test)", + wantErr: true, + }, + { + name: "item name with brackets", + itemName: "key[test]", + wantErr: true, + }, + { + name: "item name with asterisk", + itemName: "key*", + wantErr: true, + }, + { + name: "item name with question mark", + itemName: "key?", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateKeychainItemName(tt.itemName) + if (err != nil) != tt.wantErr { + t.Errorf("validateKeychainItemName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/secret/validation_test.go b/internal/secret/validation_test.go index 877810f..4a4c301 100644 --- a/internal/secret/validation_test.go +++ b/internal/secret/validation_test.go @@ -155,143 +155,3 @@ func TestValidateGPGKeyID(t *testing.T) { } } -func TestValidateKeychainItemName(t *testing.T) { - tests := []struct { - name string - itemName string - wantErr bool - }{ - // Valid cases - { - name: "valid simple name", - itemName: "my-secret-key", - wantErr: false, - }, - { - name: "valid name with dots", - itemName: "com.example.app.key", - wantErr: false, - }, - { - name: "valid name with underscores", - itemName: "my_secret_key_123", - wantErr: false, - }, - { - name: "valid alphanumeric", - itemName: "Secret123Key", - wantErr: false, - }, - { - name: "valid with hyphen at start", - itemName: "-my-key", - wantErr: false, - }, - { - name: "valid with dot at start", - itemName: ".hidden-key", - wantErr: false, - }, - - // Invalid cases - { - name: "empty item name", - itemName: "", - wantErr: true, - }, - { - name: "item name with spaces", - itemName: "my secret key", - wantErr: true, - }, - { - name: "item name with semicolon", - itemName: "key;rm -rf /", - wantErr: true, - }, - { - name: "item name with pipe", - itemName: "key|cat /etc/passwd", - wantErr: true, - }, - { - name: "item name with backticks", - itemName: "key`whoami`", - wantErr: true, - }, - { - name: "item name with dollar sign", - itemName: "key$(whoami)", - wantErr: true, - }, - { - name: "item name with quotes", - itemName: "key\"name", - wantErr: true, - }, - { - name: "item name with single quotes", - itemName: "key'name", - wantErr: true, - }, - { - name: "item name with backslash", - itemName: "key\\name", - wantErr: true, - }, - { - name: "item name with newline", - itemName: "key\nname", - wantErr: true, - }, - { - name: "item name with carriage return", - itemName: "key\rname", - wantErr: true, - }, - { - name: "item name with ampersand", - itemName: "key&echo test", - wantErr: true, - }, - { - name: "item name with redirect", - itemName: "key>/tmp/test", - wantErr: true, - }, - { - name: "item name with null byte", - itemName: "key\x00name", - wantErr: true, - }, - { - name: "item name with parentheses", - itemName: "key(test)", - wantErr: true, - }, - { - name: "item name with brackets", - itemName: "key[test]", - wantErr: true, - }, - { - name: "item name with asterisk", - itemName: "key*", - wantErr: true, - }, - { - name: "item name with question mark", - itemName: "key?", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateKeychainItemName(tt.itemName) - if (err != nil) != tt.wantErr { - t.Errorf("validateKeychainItemName() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -}