package web
import (
"os"
"strings"
"testing"
"time"
)
// A minted secret survives exactly one collection. "Shown once" was a
// claim the query string could not keep: a reload re-read the URL and
// showed it again, and so did anyone holding the history entry.
func TestVaultedSecretIsCollectedOnce(t *testing.T) {
v := newSecretVault()
nonce := v.stash("dgn_abc_def")
if nonce == "" || strings.Contains(nonce, "dgn_") {
t.Fatalf("the nonce is not a stand-in for the secret: %q", nonce)
}
if got := v.take(nonce); got != "dgn_abc_def" {
t.Fatalf("first collection got %q", got)
}
if got := v.take(nonce); got != "" {
t.Errorf("a second collection still yielded %q", got)
}
}
// Nothing to show means nothing in the URL, rather than a nonce that
// resolves to an empty string and a page claiming a secret exists.
func TestVaultRefusesToStashNothing(t *testing.T) {
v := newSecretVault()
if n := v.stash(""); n != "" {
t.Errorf("an empty secret got nonce %q", n)
}
if got := v.take(""); got != "" {
t.Errorf("an empty nonce yielded %q", got)
}
if got := v.take("cafebabe"); got != "" {
t.Errorf("an unknown nonce yielded %q", got)
}
}
// An uncollected secret does not sit in memory indefinitely, and an
// expired one is not handed over late.
func TestVaultedSecretExpires(t *testing.T) {
v := newSecretVault()
nonce := v.stash("s3cret")
v.mu.Lock()
e := v.m[nonce]
e.at = time.Now().Add(-secretTTL - time.Second)
v.m[nonce] = e
v.mu.Unlock()
if got := v.take(nonce); got != "" {
t.Errorf("an expired secret was handed over: %q", got)
}
v.mu.Lock()
n := len(v.m)
v.mu.Unlock()
if n != 0 {
t.Errorf("expired entries were kept: %d", n)
}
}
// Two mints do not collide, and collecting one does not disturb the
// other — a forge mints for several repositories at once.
func TestVaultKeepsMintsApart(t *testing.T) {
v := newSecretVault()
a, b := v.stash("first"), v.stash("second")
if a == b {
t.Fatal("two mints share a nonce")
}
if got := v.take(b); got != "second" {
t.Fatalf("wrong secret for b: %q", got)
}
if got := v.take(a); got != "first" {
t.Errorf("collecting b disturbed a: %q", got)
}
}
// The regression itself: no code path may put a minted credential into
// a URL. This reads the source rather than a rendered page, because the
// leak was in what the handler HANDED the browser, and every one of the
// four sites looked reasonable on its own.
func TestNoMintedSecretTravelsInAQueryString(t *testing.T) {
src := readWebSource(t)
for _, bad := range []string{
`q.Set("s", sub.Secret)`,
`q.Set("s", secret)`,
`"?rt="+url.QueryEscape(tk.Token)`,
`"?wt="+url.QueryEscape(tok.Token)`,
} {
if strings.Contains(src, bad) {
t.Errorf("a credential is back in a URL: %s", bad)
}
}
// And the collectors go through the vault, so a stale link cannot
// re-reveal what it once carried.
for _, want := range []string{
`mintedSecrets.take(req.URL.Query().Get("s"))`,
`mintedSecrets.take(req.URL.Query().Get("rt"))`,
`mintedSecrets.take(req.URL.Query().Get("wt"))`,
} {
if !strings.Contains(src, want) {
t.Errorf("a secret is not collected through the vault: %s", want)
}
}
}
// readWebSource reads this package's main source, so a test can assert
// on what the handlers hand the browser rather than on a rendered page.
func readWebSource(t *testing.T) string {
t.Helper()
b, err := os.ReadFile("web.go")
if err != nil {
t.Fatal(err)
}
return string(b)
}