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
73 changes: 73 additions & 0 deletions store/keychain/keychain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package keychain
import (
"context"
"errors"
"runtime"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -253,6 +255,77 @@ func TestKeychain(t *testing.T) {
})
})

t.Run("save and get large JWT credential", func(t *testing.T) {
ks := setupKeychain(t, nil)
id := store.MustParseID("com.test.test/test/jwt-user")

// Construct a fake JWT large enough that, when UTF-16 encoded on
// Windows, it exceeds the 2560-byte blob limit and must be chunked.
// Each ASCII character becomes 2 bytes in UTF-16, so we need the
// marshaled string to exceed 1280 characters.
largePayload := strings.Repeat("eyJzdWIiOiJ1c2VyMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0", 20)
largeJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + largePayload + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

creds := &mocks.MockCredential{
Username: "alice",
Password: largeJWT,
}
t.Cleanup(func() {
require.NoError(t, ks.Delete(context.Background(), id))
})
require.NoError(t, ks.Save(t.Context(), id, creds))

secret, err := ks.Get(t.Context(), id)
require.NoError(t, err)

actual, ok := secret.(*mocks.MockCredential)
require.True(t, ok)
actual.Attributes = nil

assert.Equal(t, creds.Username, actual.Username)
assert.Equal(t, creds.Password, actual.Password)
})

t.Run("overwrite small credential with large JWT credential", func(t *testing.T) {
if runtime.GOOS == "darwin" {
// macOS AddItem does not update existing items; saving to an
// already-occupied key returns a duplicate-item error.
t.Skip("macOS does not support overwriting an existing credential")
}

ks := setupKeychain(t, nil)
id := store.MustParseID("com.test.test/test/overwrite-user")

smallCreds := &mocks.MockCredential{
Username: "alice",
Password: "short",
}
t.Cleanup(func() {
require.NoError(t, ks.Delete(context.Background(), id))
})
require.NoError(t, ks.Save(t.Context(), id, smallCreds))

largePayload := strings.Repeat("eyJzdWIiOiJ1c2VyMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0", 20)
largeJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + largePayload + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
largeCreds := &mocks.MockCredential{
Username: "alice",
Password: largeJWT,
}

// On Linux and Windows the store replaces/clobbers the existing entry.
require.NoError(t, ks.Save(t.Context(), id, largeCreds))

secret, err := ks.Get(t.Context(), id)
require.NoError(t, err)

actual, ok := secret.(*mocks.MockCredential)
require.True(t, ok)
actual.Attributes = nil

assert.Equal(t, largeCreds.Username, actual.Username)
assert.Equal(t, largeCreds.Password, actual.Password)
})

t.Run("delete credential", func(t *testing.T) {
ks := setupKeychain(t, nil)
id := store.MustParseID("com.test.test/test/bob")
Expand Down
78 changes: 78 additions & 0 deletions store/keychain/keychain_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import (

"github.com/danieljoos/wincred"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/docker/secrets-engine/store/mocks"
)

func TestChunkBlob(t *testing.T) {
Expand Down Expand Up @@ -72,6 +75,81 @@ func TestChunkBlob(t *testing.T) {
})
}

func TestEncodeDecodeSecret(t *testing.T) {
t.Run("roundtrip small credential", func(t *testing.T) {
cred := &mocks.MockCredential{
Username: "bob",
Password: "secret",
}
blob, err := encodeSecret(cred)
require.NoError(t, err)

result := &mocks.MockCredential{}
require.NoError(t, decodeSecret(blob, result))
assert.Equal(t, cred.Username, result.Username)
assert.Equal(t, cred.Password, result.Password)
})

t.Run("roundtrip large JWT credential exceeding maxBlobSize", func(t *testing.T) {
// Construct a fake JWT large enough to exceed maxBlobSize (2560 bytes)
// when UTF-16 encoded. Each ASCII character becomes 2 bytes in UTF-16,
// so the marshaled string must be longer than 1280 characters.
largePayload := strings.Repeat("eyJzdWIiOiJ1c2VyMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0", 20)
largeJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + largePayload + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

cred := &mocks.MockCredential{
Username: "alice",
Password: largeJWT,
}
blob, err := encodeSecret(cred)
require.NoError(t, err)
assert.Greater(t, len(blob), maxBlobSize, "JWT credential should exceed maxBlobSize when UTF-16 encoded")

// Verify that chunkBlob properly splits the oversized blob.
chunks := chunkBlob(blob, maxBlobSize)
assert.Greater(t, len(chunks), 1)

// Reassemble chunks and decode back to verify no data is lost.
var reassembled []byte
for _, chunk := range chunks {
reassembled = append(reassembled, chunk...)
}
result := &mocks.MockCredential{}
require.NoError(t, decodeSecret(reassembled, result))
assert.Equal(t, cred.Username, result.Username)
assert.Equal(t, cred.Password, result.Password)
})

t.Run("roundtrip multiple large JWTs as separate credentials", func(t *testing.T) {
largePayload := strings.Repeat("eyJzdWIiOiJ1c2VyMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0", 40)
veryLargeJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + largePayload + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

for _, tc := range []struct {
username string
password string
}{
{"user1", veryLargeJWT},
{"user2", veryLargeJWT},
} {
cred := &mocks.MockCredential{Username: tc.username, Password: tc.password}
blob, err := encodeSecret(cred)
require.NoError(t, err)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing chunk validation in third test

Unlike the second test at line 110 (which includes assert.Greater(t, len(chunks), 1)), this test doesn't verify the chunking behavior. Consider adding assertions to check:

  • len(chunks) > 1 to confirm chunking occurred
  • Each chunk is <= maxBlobSize to validate correctness
  • Optional: expected chunk count based on blob size

This would strengthen the test and catch potential chunking bugs.

assert.Greater(t, len(blob), maxBlobSize)

chunks := chunkBlob(blob, maxBlobSize)
var reassembled []byte
for _, chunk := range chunks {
reassembled = append(reassembled, chunk...)
}

result := &mocks.MockCredential{}
require.NoError(t, decodeSecret(reassembled, result))
assert.Equal(t, tc.username, result.Username)
assert.Equal(t, tc.password, result.Password)
}
})
}

func TestIsChunkCredential(t *testing.T) {
t.Run("returns true when chunk:index attribute is present", func(t *testing.T) {
attrs := []wincred.CredentialAttribute{
Expand Down
Loading