Browse Source
Port the WRAP layer. Obfuscate DTLS packets before they enter TURN ChannelData with per-packet ChaCha20-XOR, and fail fast on invalid WRAP keys. Co-Authored-By: Anton Monakhov <[email protected]>pull/181/head
7 changed files with 382 additions and 18 deletions
@ -0,0 +1,78 @@ |
|||
// SPDX-License-Identifier: MIT
|
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"encoding/hex" |
|||
"errors" |
|||
"fmt" |
|||
|
|||
"golang.org/x/crypto/chacha20" |
|||
) |
|||
|
|||
const ( |
|||
wrapNonceLen = 12 |
|||
wrapKeyLen = 32 |
|||
) |
|||
|
|||
func genWrapKeyHex() (string, error) { |
|||
key := make([]byte, wrapKeyLen) |
|||
if _, err := rand.Read(key); err != nil { |
|||
return "", fmt.Errorf("wrap: key gen: %w", err) |
|||
} |
|||
return hex.EncodeToString(key), nil |
|||
} |
|||
|
|||
func decodeWrapKey(enabled bool, raw string) ([]byte, error) { |
|||
if !enabled { |
|||
return nil, nil |
|||
} |
|||
if raw == "" { |
|||
return nil, errors.New("-wrap requires -wrap-key") |
|||
} |
|||
key, err := hex.DecodeString(raw) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("-wrap-key invalid hex: %w", err) |
|||
} |
|||
if len(key) != wrapKeyLen { |
|||
return nil, fmt.Errorf("-wrap-key must decode to %d bytes (got %d)", wrapKeyLen, len(key)) |
|||
} |
|||
return key, nil |
|||
} |
|||
|
|||
func wrapPacket(key, payload []byte) ([]byte, error) { |
|||
if len(key) != wrapKeyLen { |
|||
return nil, fmt.Errorf("wrap: key must be %d bytes (got %d)", wrapKeyLen, len(key)) |
|||
} |
|||
out := make([]byte, wrapNonceLen+len(payload)) |
|||
if _, err := rand.Read(out[:wrapNonceLen]); err != nil { |
|||
return nil, fmt.Errorf("wrap: nonce gen: %w", err) |
|||
} |
|||
cipher, err := chacha20.NewUnauthenticatedCipher(key, out[:wrapNonceLen]) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("wrap: cipher init: %w", err) |
|||
} |
|||
cipher.XORKeyStream(out[wrapNonceLen:], payload) |
|||
return out, nil |
|||
} |
|||
|
|||
func unwrapPacket(key, wire, dst []byte) (int, error) { |
|||
if len(key) != wrapKeyLen { |
|||
return 0, fmt.Errorf("wrap: key must be %d bytes (got %d)", wrapKeyLen, len(key)) |
|||
} |
|||
if len(wire) < wrapNonceLen { |
|||
return 0, errors.New("wrap: short packet (no nonce)") |
|||
} |
|||
nonce := wire[:wrapNonceLen] |
|||
ciphertext := wire[wrapNonceLen:] |
|||
if len(ciphertext) > len(dst) { |
|||
return 0, errors.New("wrap: dst buffer too small") |
|||
} |
|||
cipher, err := chacha20.NewUnauthenticatedCipher(key, nonce) |
|||
if err != nil { |
|||
return 0, fmt.Errorf("wrap: cipher init: %w", err) |
|||
} |
|||
cipher.XORKeyStream(dst[:len(ciphertext)], ciphertext) |
|||
return len(ciphertext), nil |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"bytes" |
|||
"strings" |
|||
"testing" |
|||
) |
|||
|
|||
func TestWrapPacketRoundTrip(t *testing.T) { |
|||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen) |
|||
payload := []byte("dtls record bytes") |
|||
|
|||
wire, err := wrapPacket(key, payload) |
|||
if err != nil { |
|||
t.Fatalf("wrapPacket returned error: %v", err) |
|||
} |
|||
if len(wire) != len(payload)+wrapNonceLen { |
|||
t.Fatalf("wire len = %d, want %d", len(wire), len(payload)+wrapNonceLen) |
|||
} |
|||
if bytes.Contains(wire, payload) { |
|||
t.Fatalf("wrapped packet contains plaintext payload") |
|||
} |
|||
|
|||
dst := make([]byte, len(payload)) |
|||
n, err := unwrapPacket(key, wire, dst) |
|||
if err != nil { |
|||
t.Fatalf("unwrapPacket returned error: %v", err) |
|||
} |
|||
if n != len(payload) { |
|||
t.Fatalf("unwrapped len = %d, want %d", n, len(payload)) |
|||
} |
|||
if !bytes.Equal(dst[:n], payload) { |
|||
t.Fatalf("round trip mismatch: got %q want %q", dst[:n], payload) |
|||
} |
|||
} |
|||
|
|||
func TestDecodeWrapKeyRequiresValidKeyWhenEnabled(t *testing.T) { |
|||
if key, err := decodeWrapKey(false, ""); err != nil || key != nil { |
|||
t.Fatalf("disabled decodeWrapKey = (%v, %v), want (nil, nil)", key, err) |
|||
} |
|||
|
|||
if _, err := decodeWrapKey(true, ""); err == nil { |
|||
t.Fatalf("decodeWrapKey accepted empty key") |
|||
} |
|||
|
|||
shortHex := strings.Repeat("ab", wrapKeyLen-1) |
|||
if _, err := decodeWrapKey(true, shortHex); err == nil { |
|||
t.Fatalf("decodeWrapKey accepted short key") |
|||
} |
|||
|
|||
fullHex := strings.Repeat("ab", wrapKeyLen) |
|||
key, err := decodeWrapKey(true, fullHex) |
|||
if err != nil { |
|||
t.Fatalf("decodeWrapKey returned error: %v", err) |
|||
} |
|||
if len(key) != wrapKeyLen { |
|||
t.Fatalf("decoded key len = %d, want %d", len(key), wrapKeyLen) |
|||
} |
|||
} |
|||
|
|||
func TestUnwrapPacketRejectsShortPacket(t *testing.T) { |
|||
key := bytes.Repeat([]byte{0x42}, wrapKeyLen) |
|||
if _, err := unwrapPacket(key, []byte("short"), make([]byte, 16)); err == nil { |
|||
t.Fatalf("unwrapPacket accepted short packet") |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
// SPDX-License-Identifier: MIT
|
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"errors" |
|||
"fmt" |
|||
"net" |
|||
"time" |
|||
|
|||
dtlsnet "github.com/pion/dtls/v3/pkg/net" |
|||
pionudp "github.com/pion/transport/v4/udp" |
|||
"golang.org/x/crypto/chacha20" |
|||
) |
|||
|
|||
const ( |
|||
wrapNonceLen = 12 |
|||
wrapKeyLen = 32 |
|||
) |
|||
|
|||
func listenWrapped(addr *net.UDPAddr, key []byte) (dtlsnet.PacketListener, error) { |
|||
if len(key) != wrapKeyLen { |
|||
return nil, fmt.Errorf("wrap: key must be %d bytes (got %d)", wrapKeyLen, len(key)) |
|||
} |
|||
inner, err := pionudp.Listen("udp", addr) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("wrap: udp listen: %w", err) |
|||
} |
|||
return &wrapPacketListener{ |
|||
inner: dtlsnet.PacketListenerFromListener(inner), |
|||
key: key, |
|||
}, nil |
|||
} |
|||
|
|||
type wrapPacketListener struct { |
|||
inner dtlsnet.PacketListener |
|||
key []byte |
|||
} |
|||
|
|||
func (l *wrapPacketListener) Accept() (net.PacketConn, net.Addr, error) { |
|||
pc, addr, err := l.inner.Accept() |
|||
if err != nil { |
|||
return pc, addr, err |
|||
} |
|||
return &wrapPacketConn{inner: pc, key: l.key}, addr, nil |
|||
} |
|||
|
|||
func (l *wrapPacketListener) Close() error { return l.inner.Close() } |
|||
func (l *wrapPacketListener) Addr() net.Addr { return l.inner.Addr() } |
|||
|
|||
type wrapPacketConn struct { |
|||
inner net.PacketConn |
|||
key []byte |
|||
} |
|||
|
|||
func (c *wrapPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { |
|||
buf := make([]byte, len(p)+wrapNonceLen) |
|||
n, addr, err := c.inner.ReadFrom(buf) |
|||
if err != nil { |
|||
return 0, addr, err |
|||
} |
|||
if n < wrapNonceLen { |
|||
return 0, addr, errors.New("wrap: short packet (no nonce)") |
|||
} |
|||
nonce := buf[:wrapNonceLen] |
|||
ciphertext := buf[wrapNonceLen:n] |
|||
if len(ciphertext) > len(p) { |
|||
return 0, addr, errors.New("wrap: read buffer too small") |
|||
} |
|||
cipher, err := chacha20.NewUnauthenticatedCipher(c.key, nonce) |
|||
if err != nil { |
|||
return 0, addr, fmt.Errorf("wrap: cipher init: %w", err) |
|||
} |
|||
cipher.XORKeyStream(p[:len(ciphertext)], ciphertext) |
|||
return len(ciphertext), addr, nil |
|||
} |
|||
|
|||
func (c *wrapPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { |
|||
out := make([]byte, wrapNonceLen+len(p)) |
|||
if _, err := rand.Read(out[:wrapNonceLen]); err != nil { |
|||
return 0, fmt.Errorf("wrap: nonce gen: %w", err) |
|||
} |
|||
cipher, err := chacha20.NewUnauthenticatedCipher(c.key, out[:wrapNonceLen]) |
|||
if err != nil { |
|||
return 0, fmt.Errorf("wrap: cipher init: %w", err) |
|||
} |
|||
cipher.XORKeyStream(out[wrapNonceLen:], p) |
|||
if _, err := c.inner.WriteTo(out, addr); err != nil { |
|||
return 0, err |
|||
} |
|||
return len(p), nil |
|||
} |
|||
|
|||
func (c *wrapPacketConn) Close() error { return c.inner.Close() } |
|||
func (c *wrapPacketConn) LocalAddr() net.Addr { return c.inner.LocalAddr() } |
|||
func (c *wrapPacketConn) SetDeadline(t time.Time) error { return c.inner.SetDeadline(t) } |
|||
func (c *wrapPacketConn) SetReadDeadline(t time.Time) error { return c.inner.SetReadDeadline(t) } |
|||
func (c *wrapPacketConn) SetWriteDeadline(t time.Time) error { return c.inner.SetWriteDeadline(t) } |
|||
Loading…
Reference in new issue