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