Recover UDP crypto after packet loss

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-12 16:30:38 -04:00
committed by Brandon McGinty
parent 90f8a1ca5f
commit 4510c25350
2 changed files with 85 additions and 3 deletions
+16 -3
View File
@@ -292,10 +292,13 @@ func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) {
backupIV(cs.decryptIV[:]) backupIV(cs.decryptIV[:])
restore = true restore = true
} else if ivByte > cs.decryptIV[0] && diff > 0 { } 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 { } else if ivByte < cs.decryptIV[0] && diff > 0 {
// Wrapped forward; advance and catch up. // We missed packets across a low-byte wrap. The IV's higher
advanceIV(cs.decryptIV[:]) // bytes must advance even though the received low byte is set
// below rather than incremented.
advanceIVHighBytes(cs.decryptIV[:])
cs.decryptIV[0] = ivByte cs.decryptIV[0] = ivByte
} else { } else {
return nil, errors.New("gumble: OCB IV too far off") 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. // backupIV decrements a 16-byte IV as a little-endian integer.
func backupIV(iv []byte) { func backupIV(iv []byte) {
for i := 0; i < len(iv); i++ { for i := 0; i < len(iv); i++ {
+69
View File
@@ -32,6 +32,75 @@ func TestAdvanceIV(t *testing.T) {
} }
// Regression coverage for the native IV carry path at the 255->256 wrap. // 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) { func TestCryptState15DecryptsAcrossIVByteWrap(t *testing.T) {
key := mustDecodeHex("93360b0f86a926c4561563469026eb94") key := mustDecodeHex("93360b0f86a926c4561563469026eb94")
clientNonce := mustDecodeHex("ff000000000000000000000000000000") clientNonce := mustDecodeHex("ff000000000000000000000000000000")