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
191 changes: 0 additions & 191 deletions internal/oauth/encrypt.go

This file was deleted.

125 changes: 34 additions & 91 deletions internal/oauth/encrypt_test.go
Original file line number Diff line number Diff line change
@@ -1,103 +1,13 @@
package oauth

import (
"bytes"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
)

func TestLoadOrCreateSecretConcurrentConverges(t *testing.T) {
path := filepath.Join(t.TempDir(), "tok.json.secret")
const n = 16
secrets := make([][]byte, n)
errs := make([]error, n)
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
secrets[i], errs[i] = loadOrCreateSecret(path, true)
}(i)
}
wg.Wait()
// Exactly one creator wins; every racer must converge on the same on-disk
// secret rather than reading a half-published file or orphaning its own.
for i := 0; i < n; i++ {
if errs[i] != nil {
t.Fatalf("goroutine %d: %v", i, errs[i])
}
if !bytes.Equal(secrets[i], secrets[0]) {
t.Fatalf("goroutine %d got a divergent secret; concurrent create did not converge", i)
}
}
}

func TestAESGCMCrypterRoundTripAndTamper(t *testing.T) {
secretPath := filepath.Join(t.TempDir(), "tok.json.secret")
c := newAESGCMCrypter(secretPath)
plaintext := []byte(`{"schemaVersion":1,"tokens":{}}`)
blob, err := c.seal(plaintext)
if err != nil {
t.Fatalf("seal: %v", err)
}
if bytes.Contains(blob, plaintext) {
t.Fatal("sealed blob must not contain the plaintext")
}
got, err := c.open(blob)
if err != nil {
t.Fatalf("open: %v", err)
}
if !bytes.Equal(got, plaintext) {
t.Fatalf("round-trip mismatch: %q", got)
}
// Tamper: flipping any byte breaks the GCM tag.
tampered := append([]byte(nil), blob...)
tampered[len(tampered)-1] ^= 0xff
if _, err := c.open(tampered); err == nil {
t.Fatal("open must reject a tampered blob (GCM auth)")
}
}

func TestLoadOrCreateSecret(t *testing.T) {
path := filepath.Join(t.TempDir(), "tok.json.secret")
// Missing + create=false => fail closed (can't decrypt without the secret).
if _, err := loadOrCreateSecret(path, false); err == nil {
t.Fatal("missing secret with create=false must error")
}
secret, err := loadOrCreateSecret(path, true)
if err != nil {
t.Fatalf("create: %v", err)
}
if len(secret) != secretBytes {
t.Fatalf("secret length = %d, want %d", len(secret), secretBytes)
}
// Stable across reads.
again, err := loadOrCreateSecret(path, false)
if err != nil || !bytes.Equal(again, secret) {
t.Fatalf("secret not stable: %v", err)
}
if runtime.GOOS != "windows" {
info, err := os.Stat(path)
if err != nil {
t.Fatalf("stat secret: %v", err)
}
if perm := info.Mode().Perm(); perm != 0o600 {
t.Fatalf("secret file mode = %o, want 600", perm)
}
}
// Wrong-sized secret fails closed.
if err := os.WriteFile(path, []byte("short"), 0o600); err != nil {
t.Fatalf("corrupt secret: %v", err)
}
if _, err := loadOrCreateSecret(path, true); err == nil {
t.Fatal("wrong-sized secret must error")
}
}

func newEncryptedStore(t *testing.T) (*Store, string) {
t.Helper()
path := filepath.Join(t.TempDir(), "oauth-tokens.json")
Expand Down Expand Up @@ -202,3 +112,36 @@ func TestEncryptedStoreMissingSecretFailsClosed(t *testing.T) {
t.Fatal("missing secret must fail closed, not return empty")
}
}

// TestEncryptedStoreReclaimsStaleSecretLock reproduces a crash while the
// encrypted-file backend was creating its AES key. The key-creation lock is an
// O_EXCL ownership marker, not a kernel lock, so a process killed between
// creating and removing it leaves a file that every later Save waits on and
// then rejects forever. A stale marker must be reclaimed instead.
func TestEncryptedStoreReclaimsStaleSecretLock(t *testing.T) {
s, path := newEncryptedStore(t)
lockPath := path + ".secret.lock"
if err := os.WriteFile(lockPath, []byte("crashed-holder"), 0o600); err != nil {
t.Fatalf("write stale secret lock: %v", err)
}
stale := time.Now().Add(-2 * time.Minute)
if err := os.Chtimes(lockPath, stale, stale); err != nil {
t.Fatalf("backdate stale secret lock: %v", err)
}

if err := s.Save(ProviderKey("demo"), Token{AccessToken: "after-crash"}); err != nil {
t.Fatalf("Save must reclaim a stale secret lock, got: %v", err)
}
if _, err := os.Stat(lockPath); err == nil {
t.Fatal("stale secret lock was not cleaned up")
}

s2, err := NewStore(StoreOptions{FilePath: path, Encrypted: true})
if err != nil {
t.Fatalf("NewStore: %v", err)
}
token, ok, err := s2.Load(ProviderKey("demo"))
if err != nil || !ok || token.AccessToken != "after-crash" {
t.Fatalf("Load after stale-lock recovery = %+v ok=%v err=%v", token, ok, err)
}
}
Loading
Loading