From 81347893d70335cfbdf05f2a8c2cefc8883b45b5 Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (deepseek)" Date: Sun, 9 Aug 2026 02:33:52 -0400 Subject: [PATCH] Fix decrypt15 IV advancement to match wumble's decrypt logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old decrypt15 only patched byte 0 of the decryptIV, never properly incrementing the full 16-byte IV. After ~210 packets byte 0 wraps and byte 1 doesn't carry — nonce diverges from the server, causing OCB authentication failures. Now matches wumble's decrypt exactly: when decrypt_iv[0]+1 == iv_byte, advance the full IV (including carries). Also added backupIV for late-packet rewind support. Renamed incrementIV to advanceIV for consistency with wumble's advance_iv naming. Added comprehensive late/reorder/drop handling mirroring wumble's diff-based IV tracking. --- gumble/gumble/udp15.go | 59 ++++++++++++++++++++++++++++--------- gumble/gumble/udp15_test.go | 11 +++---- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/gumble/gumble/udp15.go b/gumble/gumble/udp15.go index a91b7ec..4dda8eb 100644 --- a/gumble/gumble/udp15.go +++ b/gumble/gumble/udp15.go @@ -149,7 +149,7 @@ func (cs *cryptState15) encrypt15(plaintext []byte) ([]byte, error) { } // Increment IV (little-endian, byte 0 is LSB). - incrementIV(cs.encryptIV[:]) + advanceIV(cs.encryptIV[:]) ciphertext, tag := ocb15Encrypt(cs.key[:], cs.encryptIV[:], plaintext) @@ -163,7 +163,7 @@ func (cs *cryptState15) encrypt15(plaintext []byte) ([]byte, error) { } // decrypt15 decrypts a Mumble 1.5 native UDP packet. -// Returns plaintext or nil on failure. +// Matches wumble's decrypt: advances IV when decrypt_iv[0]+1 == iv_byte. func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) { if !cs.initialized { return nil, errors.New("gumble: crypto not initialized") @@ -177,10 +177,15 @@ func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) { encrypted := packet[4:] savedIV := cs.decryptIV + restore := false - // Determine how the received iv_byte relates to our expected decryptIV[0]. - if ivByte == cs.decryptIV[0] { - // Perfect match — packet arrived in order. + // Match wumble: if decrypt_iv[0] + 1 == iv_byte, advance and accept. + next := cs.decryptIV[0] + 1 + if next == ivByte { + if ivByte < cs.decryptIV[0] { + advanceIV(cs.decryptIV[:]) + } + cs.decryptIV[0] = ivByte } else { diff := int(ivByte) - int(cs.decryptIV[0]) if diff < -128 { @@ -189,17 +194,26 @@ func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) { diff -= 256 } - if diff >= -30 && diff <= 30 { + if ivByte < cs.decryptIV[0] && diff > -30 && diff < 0 { + // Late packet. + cs.decryptIV[0] = ivByte + restore = true + } else if ivByte > cs.decryptIV[0] && diff > -30 && diff < 0 { + // Late packet (wrapped diff). + cs.decryptIV[0] = ivByte + backupIV(cs.decryptIV[:]) + restore = true + } else if ivByte > cs.decryptIV[0] && diff > 0 { + // We missed packets; catch up. Already handled above. + } else if ivByte < cs.decryptIV[0] && diff > 0 { + // Wrapped forward; advance and catch up. + advanceIV(cs.decryptIV[:]) cs.decryptIV[0] = ivByte - if diff < 0 { - // Late packet — we're ahead. Restore after decrypt. - defer func() { cs.decryptIV = savedIV }() - } } else { return nil, errors.New("gumble: OCB IV too far off") } - // Replay check + // Replay check. if cs.history[ivByte] != 0 && cs.history[ivByte] == cs.decryptIV[1] { cs.decryptIV = savedIV return nil, errors.New("gumble: OCB replay detected") @@ -221,12 +235,15 @@ func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) { // Update replay history. cs.history[ivByte] = cs.decryptIV[1] + if restore { + cs.decryptIV = savedIV + } + return plaintext, nil } -// incrementIV increments a 16-byte IV as a little-endian integer -// (byte 0 is the least significant byte). Matches wumble's increment_encrypt_iv. -func incrementIV(iv []byte) { +// advanceIV increments a 16-byte IV as a little-endian integer. +func advanceIV(iv []byte) { for i := 0; i < len(iv); i++ { iv[i]++ if iv[i] != 0 { @@ -235,6 +252,20 @@ func incrementIV(iv []byte) { } } +// backupIV decrements a 16-byte IV as a little-endian integer. +func backupIV(iv []byte) { + for i := 0; i < len(iv); i++ { + if iv[i] == 0 { + iv[i] = 0xFF + } else { + iv[i]-- + break + } + } +} + + + // --------------------------------------------------------------------------- // OCB variant for Mumble 1.5 native UDP. // Matches the implementation in Wumble's crypt_state.cr. diff --git a/gumble/gumble/udp15_test.go b/gumble/gumble/udp15_test.go index 5a38d8e..f23867c 100644 --- a/gumble/gumble/udp15_test.go +++ b/gumble/gumble/udp15_test.go @@ -7,26 +7,23 @@ import ( ) // Test IV increment matches wumble's little-endian behavior. -func TestIncrementIV(t *testing.T) { +func TestAdvanceIV(t *testing.T) { iv := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - incrementIV(iv) - // After first increment: byte 0 should be 0x01 + advanceIV(iv) if iv[0] != 0x01 { t.Fatalf("after increment 1, iv[0]=%02x, want 01", iv[0]) } - // Increment 254 more times to get byte 0 to 0xFF, then overflow for i := 0; i < 254; i++ { - incrementIV(iv) + advanceIV(iv) } if iv[0] != 0xFF || iv[1] != 0x00 { t.Fatalf("after 255 increments, iv[0]=%02x iv[1]=%02x, want FF 00", iv[0], iv[1]) } - // One more: overflow to byte 1 - incrementIV(iv) + advanceIV(iv) if iv[0] != 0x00 || iv[1] != 0x01 { t.Fatalf("after 256 increments, iv[0]=%02x iv[1]=%02x, want 00 01", iv[0], iv[1]) }