From 4510c25350fd7c2d61811b7a8765a9daef0df178 Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (chatgpt)" Date: Wed, 12 Aug 2026 16:30:38 -0400 Subject: [PATCH] Recover UDP crypto after packet loss --- gumble/gumble/udp15.go | 19 ++++++++-- gumble/gumble/udp15_test.go | 69 +++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/gumble/gumble/udp15.go b/gumble/gumble/udp15.go index 7433750..bb0c792 100644 --- a/gumble/gumble/udp15.go +++ b/gumble/gumble/udp15.go @@ -292,10 +292,13 @@ func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) { backupIV(cs.decryptIV[:]) restore = true } else if ivByte > cs.decryptIV[0] && diff > 0 { - // We missed packets; catch up. Already handled above. + // We missed packets; move the low IV byte forward. + cs.decryptIV[0] = ivByte } else if ivByte < cs.decryptIV[0] && diff > 0 { - // Wrapped forward; advance and catch up. - advanceIV(cs.decryptIV[:]) + // We missed packets across a low-byte wrap. The IV's higher + // bytes must advance even though the received low byte is set + // below rather than incremented. + advanceIVHighBytes(cs.decryptIV[:]) cs.decryptIV[0] = ivByte } else { return nil, errors.New("gumble: OCB IV too far off") @@ -340,6 +343,16 @@ func advanceIV(iv []byte) { } } +// advanceIVHighBytes advances all but the low IV byte as a little-endian integer. +func advanceIVHighBytes(iv []byte) { + for i := 1; i < len(iv); i++ { + iv[i]++ + if iv[i] != 0 { + break + } + } +} + // backupIV decrements a 16-byte IV as a little-endian integer. func backupIV(iv []byte) { for i := 0; i < len(iv); i++ { diff --git a/gumble/gumble/udp15_test.go b/gumble/gumble/udp15_test.go index 186dc91..78d2ff3 100644 --- a/gumble/gumble/udp15_test.go +++ b/gumble/gumble/udp15_test.go @@ -32,6 +32,75 @@ func TestAdvanceIV(t *testing.T) { } // Regression coverage for the native IV carry path at the 255->256 wrap. +func TestCryptState15DecryptsAfterMissedPackets(t *testing.T) { + key := mustDecodeHex("93360b0f86a926c4561563469026eb94") + nonce := mustDecodeHex("10000000000000000000000000000000") + out, in := &cryptState15{}, &cryptState15{} + if err := out.setup15(key, nonce, nonce); err != nil { + t.Fatal(err) + } + if err := in.setup15(key, nonce, nonce); err != nil { + t.Fatal(err) + } + + first, err := out.encrypt15([]byte("first")) + if err != nil { + t.Fatal(err) + } + if _, err := in.decrypt15(first); err != nil { + t.Fatal(err) + } + for i := 0; i < 3; i++ { + if _, err := out.encrypt15([]byte("dropped")); err != nil { + t.Fatal(err) + } + } + last, err := out.encrypt15([]byte("after loss")) + if err != nil { + t.Fatal(err) + } + plain, err := in.decrypt15(last) + if err != nil || !bytes.Equal(plain, []byte("after loss")) { + t.Fatalf("decrypt after missed packets = %q, %v", plain, err) + } +} + +func TestCryptState15DecryptsAfterMissedPacketsAcrossIVByteWrap(t *testing.T) { + key := mustDecodeHex("93360b0f86a926c4561563469026eb94") + nonce := mustDecodeHex("fa000000000000000000000000000000") + out, in := &cryptState15{}, &cryptState15{} + if err := out.setup15(key, nonce, nonce); err != nil { + t.Fatal(err) + } + if err := in.setup15(key, nonce, nonce); err != nil { + t.Fatal(err) + } + + first, err := out.encrypt15([]byte("before wrap")) + if err != nil { + t.Fatal(err) + } + if _, err := in.decrypt15(first); err != nil { + t.Fatal(err) + } + for i := 0; i < 6; i++ { + if _, err := out.encrypt15([]byte("dropped")); err != nil { + t.Fatal(err) + } + } + last, err := out.encrypt15([]byte("after wrap")) + if err != nil { + t.Fatal(err) + } + plain, err := in.decrypt15(last) + if err != nil || !bytes.Equal(plain, []byte("after wrap")) { + t.Fatalf("decrypt after missed packets across IV wrap = %q, %v", plain, err) + } + if in.decryptIV[0] != 2 || in.decryptIV[1] != 1 { + t.Fatalf("unexpected IV after wrapped loss: %x", in.decryptIV[:2]) + } +} + func TestCryptState15DecryptsAcrossIVByteWrap(t *testing.T) { key := mustDecodeHex("93360b0f86a926c4561563469026eb94") clientNonce := mustDecodeHex("ff000000000000000000000000000000")