mirror of
https://gitea.com/gitea/tea.git
synced 2026-08-05 23:07:39 +02:00
refactor(credstore): embed credential store and drop sdk-go dependency
- Embed the minimal credstore subset used by tea (SecureStore, EncryptedFileStore, KeyringStore, FileStore) as modules/credstore so external SDK renames can no longer break the build - Keep the on-disk format fully compatible: AES-256-GCM values with the v1: prefix, credentials.json / credentials.json.enc paths, and the Token JSON field names are unchanged, verified by a ciphertext fixture generated with sdk-go v1.1.0 - Store the keyring master key under a tea-owned account name - Reuse the existing kernel-level filelock module instead of the upstream lockfile protocol, removing a stale-lock race - Cover roundtrip, keyring-unavailable fallback, and fixture decryption with tests using a mocked keyring - Remove github.com/go-signet/sdk-go and promote github.com/zalando/go-keyring to a direct dependency Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package credstore
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
// newLargeTestToken builds a token whose access token is several KB,
|
||||
// mimicking real JWTs with large groups claims that exceed the Windows
|
||||
// Credential Manager 2560-byte blob limit.
|
||||
func newLargeTestToken(clientID string) Token {
|
||||
return Token{
|
||||
AccessToken: "header." + strings.Repeat("groups-claim-payload-", 300) + ".sig",
|
||||
RefreshToken: "test-refresh-token",
|
||||
TokenType: "Bearer",
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour).Truncate(time.Second),
|
||||
ClientID: clientID,
|
||||
}
|
||||
}
|
||||
|
||||
func newTestEncryptedStore(t *testing.T) (*EncryptedFileStore[Token], string) {
|
||||
t.Helper()
|
||||
keyring.MockInit()
|
||||
path := filepath.Join(t.TempDir(), "tokens.enc")
|
||||
return NewEncryptedFileStore[Token]("test-service", path, JSONCodec[Token]{}), path
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreSaveAndLoad(t *testing.T) {
|
||||
store, _ := newTestEncryptedStore(t)
|
||||
|
||||
tok := newLargeTestToken("test-client")
|
||||
require.NoError(t, store.Save(tok.ClientID, tok))
|
||||
|
||||
loaded, err := store.Load("test-client")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tok.AccessToken, loaded.AccessToken)
|
||||
assert.Equal(t, tok.RefreshToken, loaded.RefreshToken)
|
||||
assert.True(t, tok.ExpiresAt.Equal(loaded.ExpiresAt))
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreFileContainsNoPlaintext(t *testing.T) {
|
||||
store, path := newTestEncryptedStore(t)
|
||||
|
||||
tok := newLargeTestToken("test-client")
|
||||
require.NoError(t, store.Save(tok.ClientID, tok))
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(raw), "groups-claim-payload")
|
||||
assert.NotContains(t, string(raw), tok.RefreshToken)
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreKeyringHoldsOnlySmallMasterKey(t *testing.T) {
|
||||
store, _ := newTestEncryptedStore(t)
|
||||
|
||||
tok := newLargeTestToken("test-client")
|
||||
require.NoError(t, store.Save(tok.ClientID, tok))
|
||||
|
||||
// The token itself must not be in the keyring.
|
||||
_, err := keyring.Get("test-service", "test-client")
|
||||
assert.ErrorIs(t, err, keyring.ErrNotFound)
|
||||
|
||||
// Only the 44-byte base64 master key may live in the keyring —
|
||||
// well under the Windows Credential Manager 2560-byte blob limit.
|
||||
encoded, err := keyring.Get("test-service", masterKeyUser)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, encoded, 44)
|
||||
key, err := base64.StdEncoding.DecodeString(encoded)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, key, 32)
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreLoadNotFound(t *testing.T) {
|
||||
store, _ := newTestEncryptedStore(t)
|
||||
|
||||
_, err := store.Load("nonexistent")
|
||||
assert.ErrorIs(t, err, ErrNotFound)
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreDelete(t *testing.T) {
|
||||
store, _ := newTestEncryptedStore(t)
|
||||
|
||||
tok := newLargeTestToken("test-client")
|
||||
require.NoError(t, store.Save(tok.ClientID, tok))
|
||||
require.NoError(t, store.Delete("test-client"))
|
||||
|
||||
_, err := store.Load("test-client")
|
||||
assert.ErrorIs(t, err, ErrNotFound)
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreSaveEmptyClientID(t *testing.T) {
|
||||
store, _ := newTestEncryptedStore(t)
|
||||
|
||||
err := store.Save("", newLargeTestToken("x"))
|
||||
assert.ErrorIs(t, err, ErrEmptyClientID)
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreProbe(t *testing.T) {
|
||||
store, _ := newTestEncryptedStore(t)
|
||||
|
||||
assert.True(t, store.Probe())
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreCorruptMasterKeyFailsLoad(t *testing.T) {
|
||||
store, _ := newTestEncryptedStore(t)
|
||||
|
||||
tok := newLargeTestToken("test-client")
|
||||
require.NoError(t, store.Save(tok.ClientID, tok))
|
||||
|
||||
// A fresh store whose keyring holds a corrupted key must fail to decrypt
|
||||
// rather than return garbage or mint a new key.
|
||||
require.NoError(t, keyring.Set("test-service", masterKeyUser, "not-base64!"))
|
||||
fresh := NewEncryptedFileStore[Token]("test-service", store.file.filePath, JSONCodec[Token]{})
|
||||
assert.False(t, fresh.Probe())
|
||||
_, err := fresh.Load("test-client")
|
||||
require.Error(t, err)
|
||||
assert.NotErrorIs(t, err, ErrNotFound)
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreNilCodecPanics(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
NewEncryptedFileStore[string]("svc", "path", nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEncryptedFileStoreString(t *testing.T) {
|
||||
store, path := newTestEncryptedStore(t)
|
||||
assert.Equal(t, "encrypted-file: "+path, store.String())
|
||||
}
|
||||
Reference in New Issue
Block a user