Fix decrypt15 IV advancement to match wumble's decrypt logic

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.
This commit is contained in:
Brandon McGinty (deepseek)
2026-08-09 02:33:52 -04:00
committed by Brandon McGinty
parent e93087afeb
commit 81347893d7
2 changed files with 49 additions and 21 deletions
+45 -14
View File
@@ -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.
+4 -7
View File
@@ -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])
}