mirror of
https://gitea.com/gitea/tea.git
synced 2026-08-05 23:07:39 +02:00
f6d939a8df
- 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>
141 lines
4.2 KiB
Go
141 lines
4.2 KiB
Go
// 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())
|
|
}
|