S04-A per-format columns Step 4: Move Maven's credential reads and writes behind RowEncryptor

Part of #417 (closed), S04-A per-format credential columns. This plan moves the three remote-repository tables off their interim tmp_plaintext_* columns onto the encrypted shape, puts reads and writes behind crypto.RowEncryptor, and registers each table with the namespace-key rotation sweep.

References: S04-A spec · S04-A per-format columns plan (see "Step 4").

Type: feat | Depends on: Step 3, and #513 — merging this step deploys it, and without the encryption block configured its fail-closed arms take every Maven remote credential path down in that environment. See Dependencies. Steps 7 and 10 inherit the same gate for their formats.

Scope

the store reads and writes ciphertext under Step 3's crypto.TableName; nothing touches tmp_plaintext_*; every arm of S13's fail-closed table behaves as specified, the health probe included; and the boot-time warn log names each Maven remote whose plaintext columns are still populated, in the bounded count-plus-sample shape Approach specifies.

Files

  • internal/datastore/maven_remote_credentials.go (Modify): the store, and the twin of npm's PgError.Detail comment at :93-95 and :116, both of which assert the row carries the plaintext pair
  • internal/datastore/maven_remote_repositories_errors.md (Modify): the sidecar :13 and :33 carry the same fact
  • internal/datastore/maven_remote_repositories_create.go (Modify)
  • internal/datastore/maven_remote_repositories_update.go (Modify)
  • internal/datastore/maven_remote_repositories_details.go (Modify): has_credentials becomes "the encrypted unit is populated", which is wrapped_dek IS NOT NULLnot the non-NULL-and-non-empty pair the projection carries today (:266-271). The non-empty half cannot survive: the ciphertext of "" is a non-NULL bytea, so no predicate over the encrypted columns can tell it from a real value. The write path is what keeps the old meaning — an empty component is rejected before it is encrypted, so the unit is either absent or genuinely populated — and mavenRemoteCredentialIsUsable retires with the SQL half rather than drifting from it, per the comment at :259-263 that ties the two spellings together. Steps 7 and 10 take the same definition
  • internal/format/maven/remote_health.go (Modify): the probe resolves credentials before composing its request and must fail rather than probe anonymously
  • internal/format/maven/remote_upstream.go (Modify): reads the pair back with Secret.Value() to build the Authorization header, so it moves with the store or every authenticated Maven remote fetch 401s once Step 5 drops the columns — the anonymous-fetch symptom its own comment at :300-304 describes, where "an anonymous fallback would instead pull without credentials on a remote that has them configured", not the colon-validation 401 at :305-309
  • cmd/artifact-registry/wire_remote.go (Modify): pass the encryptor and emit the leftover-plaintext log. Maven's credential store is composed here, not in wire_maven.go, which names no credential type

Shared seams

cmd/artifact-registry/wire_remote.go holds the encryptor hand-off shape that Steps 7 and 10 reuse. This step owns it and is the first to open the file, so both later steps carry a merge-order edge to this one and then touch only their own format's constructor call.

Acceptance

with the encryption block present, a credential set and a subsequent upstream fetch succeed and no plaintext appears in the row; with the block absent, a credential write is refused and a read of a credential-bearing row fails rather than fetching anonymously.

Tests

internal/datastore/maven_remote_credentials_test.go and _integration_test.go (Modify), plus a no-encryptor subtest per fail-closed arm in the S13 table, and one asserting the leftover-plaintext log carries a count, at most ten ids, and never a value, and one asserting a clean table emits a zero count at info rather than nothing, one asserting a failed count query emits its error line and lets boot proceed, plus one pinning the new has_credentials meaning: a row whose encrypted unit is populated reads true and a row with the unit absent reads false, with the write-side rejection of an empty component asserted in the same suite, so the retired predicate's guarantee has a new home. Add internal/datastore/maven_remote_details_integration_test.go (Modify), whose helper at :491 has seven uses pinning the flag. Also internal/datastore/maven_remote_repositories_test.go (Modify), which pins the generated SQL ten times including tmp_plaintext_username = $2::text (:67, :272-291, :306-359) — this step changes that SQL — and the seeding suites that write rows through these columns: internal/managementapi's remote integration tests and cmd/artifact-registry/wire_connection_test_sources_integration_test.go. Diff the resulting suite against the pre-change one for dropped subtests, not only added ones.