Skip to content

Commit 60ebfbf

Browse files
authored
Merge pull request #481 from docker/keychain/store/tests
test: keychain storage limits
2 parents ee58e6b + 9fa678c commit 60ebfbf

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

store/keychain/keychain_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package keychain
1717
import (
1818
"context"
1919
"errors"
20+
"runtime"
21+
"strings"
2022
"testing"
2123

2224
"github.com/stretchr/testify/assert"
@@ -253,6 +255,77 @@ func TestKeychain(t *testing.T) {
253255
})
254256
})
255257

258+
t.Run("save and get large JWT credential", func(t *testing.T) {
259+
ks := setupKeychain(t, nil)
260+
id := store.MustParseID("com.test.test/test/jwt-user")
261+
262+
// Construct a fake JWT large enough that, when UTF-16 encoded on
263+
// Windows, it exceeds the 2560-byte blob limit and must be chunked.
264+
// Each ASCII character becomes 2 bytes in UTF-16, so we need the
265+
// marshaled string to exceed 1280 characters.
266+
largePayload := strings.Repeat("eyJzdWIiOiJ1c2VyMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0", 20)
267+
largeJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + largePayload + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
268+
269+
creds := &mocks.MockCredential{
270+
Username: "alice",
271+
Password: largeJWT,
272+
}
273+
t.Cleanup(func() {
274+
require.NoError(t, ks.Delete(context.Background(), id))
275+
})
276+
require.NoError(t, ks.Save(t.Context(), id, creds))
277+
278+
secret, err := ks.Get(t.Context(), id)
279+
require.NoError(t, err)
280+
281+
actual, ok := secret.(*mocks.MockCredential)
282+
require.True(t, ok)
283+
actual.Attributes = nil
284+
285+
assert.Equal(t, creds.Username, actual.Username)
286+
assert.Equal(t, creds.Password, actual.Password)
287+
})
288+
289+
t.Run("overwrite small credential with large JWT credential", func(t *testing.T) {
290+
if runtime.GOOS == "darwin" {
291+
// macOS AddItem does not update existing items; saving to an
292+
// already-occupied key returns a duplicate-item error.
293+
t.Skip("macOS does not support overwriting an existing credential")
294+
}
295+
296+
ks := setupKeychain(t, nil)
297+
id := store.MustParseID("com.test.test/test/overwrite-user")
298+
299+
smallCreds := &mocks.MockCredential{
300+
Username: "alice",
301+
Password: "short",
302+
}
303+
t.Cleanup(func() {
304+
require.NoError(t, ks.Delete(context.Background(), id))
305+
})
306+
require.NoError(t, ks.Save(t.Context(), id, smallCreds))
307+
308+
largePayload := strings.Repeat("eyJzdWIiOiJ1c2VyMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0", 20)
309+
largeJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + largePayload + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
310+
largeCreds := &mocks.MockCredential{
311+
Username: "alice",
312+
Password: largeJWT,
313+
}
314+
315+
// On Linux and Windows the store replaces/clobbers the existing entry.
316+
require.NoError(t, ks.Save(t.Context(), id, largeCreds))
317+
318+
secret, err := ks.Get(t.Context(), id)
319+
require.NoError(t, err)
320+
321+
actual, ok := secret.(*mocks.MockCredential)
322+
require.True(t, ok)
323+
actual.Attributes = nil
324+
325+
assert.Equal(t, largeCreds.Username, actual.Username)
326+
assert.Equal(t, largeCreds.Password, actual.Password)
327+
})
328+
256329
t.Run("delete credential", func(t *testing.T) {
257330
ks := setupKeychain(t, nil)
258331
id := store.MustParseID("com.test.test/test/bob")

store/keychain/keychain_windows_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323

2424
"github.com/danieljoos/wincred"
2525
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
27+
28+
"github.com/docker/secrets-engine/store/mocks"
2629
)
2730

2831
func TestChunkBlob(t *testing.T) {
@@ -72,6 +75,81 @@ func TestChunkBlob(t *testing.T) {
7275
})
7376
}
7477

78+
func TestEncodeDecodeSecret(t *testing.T) {
79+
t.Run("roundtrip small credential", func(t *testing.T) {
80+
cred := &mocks.MockCredential{
81+
Username: "bob",
82+
Password: "secret",
83+
}
84+
blob, err := encodeSecret(cred)
85+
require.NoError(t, err)
86+
87+
result := &mocks.MockCredential{}
88+
require.NoError(t, decodeSecret(blob, result))
89+
assert.Equal(t, cred.Username, result.Username)
90+
assert.Equal(t, cred.Password, result.Password)
91+
})
92+
93+
t.Run("roundtrip large JWT credential exceeding maxBlobSize", func(t *testing.T) {
94+
// Construct a fake JWT large enough to exceed maxBlobSize (2560 bytes)
95+
// when UTF-16 encoded. Each ASCII character becomes 2 bytes in UTF-16,
96+
// so the marshaled string must be longer than 1280 characters.
97+
largePayload := strings.Repeat("eyJzdWIiOiJ1c2VyMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0", 20)
98+
largeJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + largePayload + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
99+
100+
cred := &mocks.MockCredential{
101+
Username: "alice",
102+
Password: largeJWT,
103+
}
104+
blob, err := encodeSecret(cred)
105+
require.NoError(t, err)
106+
assert.Greater(t, len(blob), maxBlobSize, "JWT credential should exceed maxBlobSize when UTF-16 encoded")
107+
108+
// Verify that chunkBlob properly splits the oversized blob.
109+
chunks := chunkBlob(blob, maxBlobSize)
110+
assert.Greater(t, len(chunks), 1)
111+
112+
// Reassemble chunks and decode back to verify no data is lost.
113+
var reassembled []byte
114+
for _, chunk := range chunks {
115+
reassembled = append(reassembled, chunk...)
116+
}
117+
result := &mocks.MockCredential{}
118+
require.NoError(t, decodeSecret(reassembled, result))
119+
assert.Equal(t, cred.Username, result.Username)
120+
assert.Equal(t, cred.Password, result.Password)
121+
})
122+
123+
t.Run("roundtrip multiple large JWTs as separate credentials", func(t *testing.T) {
124+
largePayload := strings.Repeat("eyJzdWIiOiJ1c2VyMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0", 40)
125+
veryLargeJWT := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + largePayload + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
126+
127+
for _, tc := range []struct {
128+
username string
129+
password string
130+
}{
131+
{"user1", veryLargeJWT},
132+
{"user2", veryLargeJWT},
133+
} {
134+
cred := &mocks.MockCredential{Username: tc.username, Password: tc.password}
135+
blob, err := encodeSecret(cred)
136+
require.NoError(t, err)
137+
assert.Greater(t, len(blob), maxBlobSize)
138+
139+
chunks := chunkBlob(blob, maxBlobSize)
140+
var reassembled []byte
141+
for _, chunk := range chunks {
142+
reassembled = append(reassembled, chunk...)
143+
}
144+
145+
result := &mocks.MockCredential{}
146+
require.NoError(t, decodeSecret(reassembled, result))
147+
assert.Equal(t, tc.username, result.Username)
148+
assert.Equal(t, tc.password, result.Password)
149+
}
150+
})
151+
}
152+
75153
func TestIsChunkCredential(t *testing.T) {
76154
t.Run("returns true when chunk:index attribute is present", func(t *testing.T) {
77155
attrs := []wincred.CredentialAttribute{

0 commit comments

Comments
 (0)