Skip to content

Commit 98e1664

Browse files
committed
fix(oauth): refuse keyring indexes over the reader key cap on write
writeKeyIndex already refused over-cap chunk counts, but a large set of short keys can stay under the chunk limit while exceeding maxKeyringIndexKeys. Cap the key count before publishing so Save cannot persist an index that readKeyIndex then rejects. Cover the write-side path and multi-chunk accumulation on the reader.
1 parent 3161577 commit 98e1664

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

‎internal/oauth/store.go‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -860,10 +860,15 @@ func (b keyringBlob) readKeyIndex() ([]string, bool, int, error) {
860860
// only after the header stops referencing them (best-effort: an unreferenced
861861
// chunk is never read).
862862
func (b keyringBlob) writeKeyIndex(keys []string, priorChunks int) (int, error) {
863+
// Refuse to publish an index the reader would reject: readKeyIndex caps both
864+
// total keys and chunk count, and a header beyond either would make every
865+
// later Load/Status/Save/Delete fail before it could recover. Check the key
866+
// count before chunking so a large set of short keys that still fit under
867+
// maxKeyringIndexChunks cannot strand the store unreadable.
868+
if len(keys) > maxKeyringIndexKeys {
869+
return 0, errKeyringIndexTooManyKeys(len(keys))
870+
}
863871
chunks := chunkIndexKeys(keys)
864-
// Refuse to publish an index the reader would reject: readKeyIndex caps
865-
// headers at maxKeyringIndexChunks, and a header beyond it would make
866-
// every later Load/Status/Save/Delete fail before it could recover.
867872
if len(chunks) > maxKeyringIndexChunks {
868873
return 0, fmt.Errorf("oauth: keyring key index needs %d chunks, over the %d-chunk cap readers accept; too many stored credentials", len(chunks), maxKeyringIndexChunks)
869874
}

‎internal/oauth/store_keyring_test.go‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,19 +851,48 @@ func TestStoreKeyringReadIndexRejectsOversizedKeyList(t *testing.T) {
851851
t.Fatal(err)
852852
}
853853
ckr.data[keyringService+"/"+keyringIndexAccount] = base64.StdEncoding.EncodeToString(header)
854+
ckr.gets = 0
854855
if _, _, _, err := blob.readKeyIndex(); err == nil {
855856
t.Fatal("expected an oversized key list in a chunk-0 header to be rejected")
856857
}
858+
if ckr.gets != 1 {
859+
t.Fatalf("readKeyIndex issued %d gets for an oversized header keys list; want header lookup only", ckr.gets)
860+
}
857861

858862
// The pre-chunking bare-array format must be capped the same way.
859863
legacyArray, err := json.Marshal(tooMany)
860864
if err != nil {
861865
t.Fatal(err)
862866
}
863867
ckr.data[keyringService+"/"+keyringIndexAccount] = base64.StdEncoding.EncodeToString(legacyArray)
868+
ckr.gets = 0
864869
if _, _, _, err := blob.readKeyIndex(); err == nil {
865870
t.Fatal("expected an oversized legacy-format key array to be rejected")
866871
}
872+
if ckr.gets != 1 {
873+
t.Fatalf("readKeyIndex issued %d gets for an oversized legacy array; want header lookup only", ckr.gets)
874+
}
875+
876+
// Accumulation across continuation chunks must hit the same total cap: a
877+
// small header plus an oversized chunk-1 would otherwise fan out past the
878+
// bound after the per-header check has already passed.
879+
headerOK, err := json.Marshal(keyIndexHeader{Version: 1, Chunks: 2, Keys: []string{ProviderKey("seed")}})
880+
if err != nil {
881+
t.Fatal(err)
882+
}
883+
chunk1, err := json.Marshal(tooMany)
884+
if err != nil {
885+
t.Fatal(err)
886+
}
887+
ckr.data[keyringService+"/"+keyringIndexAccount] = base64.StdEncoding.EncodeToString(headerOK)
888+
ckr.data[keyringService+"/"+keyringIndexAccount+"-1"] = base64.StdEncoding.EncodeToString(chunk1)
889+
ckr.gets = 0
890+
if _, _, _, err := blob.readKeyIndex(); err == nil {
891+
t.Fatal("expected an oversized key list accumulated across chunks to be rejected")
892+
}
893+
if ckr.gets != 2 {
894+
t.Fatalf("readKeyIndex issued %d gets for a header+oversize-chunk; want header and chunk-1 only", ckr.gets)
895+
}
867896
}
868897

869898
// TestKeyringFallbackLockPathIsPerUser covers the fallback taken when no config
@@ -969,3 +998,23 @@ func TestStoreKeyringWriteIndexRejectsOverCapChunks(t *testing.T) {
969998
t.Fatalf("over-cap index write must publish nothing, found %d entries", len(kr.data))
970999
}
9711000
}
1001+
1002+
// TestStoreKeyringWriteIndexRejectsOverCapKeys: short keys can still fit under
1003+
// the chunk-count cap while exceeding maxKeyringIndexKeys. writeKeyIndex must
1004+
// refuse that set before publishing, matching the reader-side total key cap.
1005+
func TestStoreKeyringWriteIndexRejectsOverCapKeys(t *testing.T) {
1006+
kr := newFakeKR()
1007+
b := keyringBlob{kr: kr, service: keyringService, legacyAccount: keyringLegacyAccount, indexAccount: keyringIndexAccount}
1008+
keys := make([]string, maxKeyringIndexKeys+1)
1009+
for i := range keys {
1010+
// Short keys pack densely into chunks so the chunk-count check alone
1011+
// would not catch this over-cap set.
1012+
keys[i] = fmt.Sprintf("p%d", i)
1013+
}
1014+
if _, err := b.writeKeyIndex(keys, 0); err == nil {
1015+
t.Fatal("writeKeyIndex published a key count readKeyIndex would refuse")
1016+
}
1017+
if len(kr.data) != 0 {
1018+
t.Fatalf("over-cap key write must publish nothing, found %d entries", len(kr.data))
1019+
}
1020+
}

0 commit comments

Comments
 (0)