285 lines
7.7 KiB
Go
285 lines
7.7 KiB
Go
package router
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
|
|
"github.com/andybalholm/go-bit"
|
|
"golang.org/x/crypto/nacl/secretbox"
|
|
)
|
|
|
|
// Frame Encryptors
|
|
|
|
type Encryptor interface {
|
|
FrameOverhead() int
|
|
PacketOverhead() int
|
|
Bytes() ([]byte, error)
|
|
AppendFrame(src []byte, dst []byte, frame []byte)
|
|
TotalLen() int
|
|
}
|
|
|
|
type NonEncryptor struct {
|
|
buf []byte
|
|
bufTail []byte
|
|
buffered int
|
|
prefixLen int
|
|
}
|
|
|
|
type NaClEncryptor struct {
|
|
NonEncryptor
|
|
buf []byte
|
|
prefixLen int
|
|
sessionKey *[32]byte
|
|
nonce [24]byte
|
|
seqNo uint64
|
|
df bool
|
|
}
|
|
|
|
func NewNonEncryptor(prefix []byte) *NonEncryptor {
|
|
buf := make([]byte, MaxUDPPacketSize)
|
|
prefixLen := copy(buf, prefix)
|
|
return &NonEncryptor{
|
|
buf: buf,
|
|
bufTail: buf[prefixLen:],
|
|
buffered: prefixLen,
|
|
prefixLen: prefixLen}
|
|
}
|
|
|
|
func (ne *NonEncryptor) PacketOverhead() int {
|
|
return ne.prefixLen
|
|
}
|
|
|
|
func (ne *NonEncryptor) FrameOverhead() int {
|
|
return NameSize + NameSize + 2
|
|
}
|
|
|
|
func (ne *NonEncryptor) Bytes() ([]byte, error) {
|
|
buf := ne.buf[:ne.buffered]
|
|
ne.buffered = ne.prefixLen
|
|
ne.bufTail = ne.buf[ne.prefixLen:]
|
|
return buf, nil
|
|
}
|
|
|
|
func (ne *NonEncryptor) AppendFrame(src []byte, dst []byte, frame []byte) {
|
|
bufTail := ne.bufTail
|
|
srcLen := copy(bufTail, src)
|
|
bufTail = bufTail[srcLen:]
|
|
dstLen := copy(bufTail, dst)
|
|
bufTail = bufTail[dstLen:]
|
|
binary.BigEndian.PutUint16(bufTail, uint16(len(frame)))
|
|
bufTail = bufTail[2:]
|
|
copy(bufTail, frame)
|
|
ne.bufTail = bufTail[len(frame):]
|
|
ne.buffered += srcLen + dstLen + 2 + len(frame)
|
|
}
|
|
|
|
func (ne *NonEncryptor) TotalLen() int {
|
|
return ne.buffered
|
|
}
|
|
|
|
func NewNaClEncryptor(prefix []byte, sessionKey *[32]byte, outbound bool, df bool) *NaClEncryptor {
|
|
buf := make([]byte, MaxUDPPacketSize)
|
|
prefixLen := copy(buf, prefix)
|
|
ne := &NaClEncryptor{
|
|
NonEncryptor: *NewNonEncryptor([]byte{}),
|
|
buf: buf,
|
|
prefixLen: prefixLen,
|
|
sessionKey: sessionKey,
|
|
df: df}
|
|
if outbound {
|
|
ne.nonce[0] |= (1 << 7)
|
|
}
|
|
return ne
|
|
}
|
|
|
|
func (ne *NaClEncryptor) Bytes() ([]byte, error) {
|
|
plaintext, err := ne.NonEncryptor.Bytes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// We carry the DF flag in the (unencrypted portion of the)
|
|
// payload, rather than just extracting it from the packet headers
|
|
// at the receiving end, since we do not trust routers not to mess
|
|
// with headers. As we have different decryptors for non-DF and
|
|
// DF, that would result in hard to track down packet drops due to
|
|
// crypto errors.
|
|
seqNoAndDF := ne.seqNo
|
|
if ne.df {
|
|
seqNoAndDF |= (1 << 63)
|
|
}
|
|
ciphertext := ne.buf
|
|
binary.BigEndian.PutUint64(ciphertext[ne.prefixLen:], seqNoAndDF)
|
|
binary.BigEndian.PutUint64(ne.nonce[16:24], seqNoAndDF)
|
|
// Seal *appends* to ciphertext
|
|
ciphertext = secretbox.Seal(ciphertext[:ne.prefixLen+8], plaintext, &ne.nonce, ne.sessionKey)
|
|
ne.seqNo++
|
|
return ciphertext, nil
|
|
}
|
|
|
|
func (ne *NaClEncryptor) PacketOverhead() int {
|
|
return ne.prefixLen + 8 + secretbox.Overhead + ne.NonEncryptor.PacketOverhead()
|
|
}
|
|
|
|
func (ne *NaClEncryptor) TotalLen() int {
|
|
return ne.PacketOverhead() + ne.NonEncryptor.TotalLen()
|
|
}
|
|
|
|
// Frame Decryptors
|
|
|
|
type FrameConsumer func(src []byte, dst []byte, frame []byte)
|
|
|
|
type Decryptor interface {
|
|
IterateFrames([]byte, FrameConsumer) error
|
|
}
|
|
|
|
type NonDecryptor struct {
|
|
}
|
|
|
|
type NaClDecryptor struct {
|
|
NonDecryptor
|
|
sessionKey *[32]byte
|
|
instance *NaClDecryptorInstance
|
|
instanceDF *NaClDecryptorInstance
|
|
}
|
|
|
|
type NaClDecryptorInstance struct {
|
|
nonce [24]byte
|
|
currentWindow uint64
|
|
usedOffsets *bit.Set
|
|
previousUsedOffsets *bit.Set
|
|
}
|
|
|
|
func NewNaClDecryptorInstance(outbound bool) *NaClDecryptorInstance {
|
|
di := &NaClDecryptorInstance{usedOffsets: bit.New()}
|
|
if !outbound {
|
|
di.nonce[0] |= (1 << 7)
|
|
}
|
|
return di
|
|
}
|
|
|
|
type PacketDecodingError struct {
|
|
Desc string
|
|
}
|
|
|
|
func (pde PacketDecodingError) Error() string {
|
|
return fmt.Sprint("Failed to decode packet: ", pde.Desc)
|
|
}
|
|
|
|
func NewNonDecryptor() *NonDecryptor {
|
|
return &NonDecryptor{}
|
|
}
|
|
|
|
func (nd *NonDecryptor) IterateFrames(packet []byte, consumer FrameConsumer) error {
|
|
for len(packet) >= (2 + NameSize + NameSize) {
|
|
srcNameByte := packet[:NameSize]
|
|
packet = packet[NameSize:]
|
|
dstNameByte := packet[:NameSize]
|
|
packet = packet[NameSize:]
|
|
length := binary.BigEndian.Uint16(packet[:2])
|
|
packet = packet[2:]
|
|
if len(packet) < int(length) {
|
|
return PacketDecodingError{Desc: fmt.Sprintf("too short; expected frame of length %d, got %d", length, len(packet))}
|
|
}
|
|
frame := packet[:length]
|
|
packet = packet[length:]
|
|
consumer(srcNameByte, dstNameByte, frame)
|
|
}
|
|
if len(packet) > 0 {
|
|
return PacketDecodingError{Desc: fmt.Sprintf("%d octets of trailing garbage", len(packet))}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewNaClDecryptor(sessionKey *[32]byte, outbound bool) *NaClDecryptor {
|
|
return &NaClDecryptor{
|
|
NonDecryptor: *NewNonDecryptor(),
|
|
sessionKey: sessionKey,
|
|
instance: NewNaClDecryptorInstance(outbound),
|
|
instanceDF: NewNaClDecryptorInstance(outbound)}
|
|
}
|
|
|
|
func (nd *NaClDecryptor) IterateFrames(packet []byte, consumer FrameConsumer) error {
|
|
if len(packet) < 8 {
|
|
return PacketDecodingError{Desc: fmt.Sprintf("encrypted UDP packet too short; expected length >= 8, got %d", len(packet))}
|
|
}
|
|
buf, success := nd.decrypt(packet)
|
|
if !success {
|
|
return PacketDecodingError{Desc: fmt.Sprint("UDP packet decryption failed")}
|
|
}
|
|
return nd.NonDecryptor.IterateFrames(buf, consumer)
|
|
}
|
|
|
|
func (nd *NaClDecryptor) decrypt(buf []byte) ([]byte, bool) {
|
|
seqNoAndDF := binary.BigEndian.Uint64(buf[:8])
|
|
df := (seqNoAndDF & (1 << 63)) != 0
|
|
seqNo := seqNoAndDF & ((1 << 63) - 1)
|
|
var di *NaClDecryptorInstance
|
|
if df {
|
|
di = nd.instanceDF
|
|
} else {
|
|
di = nd.instance
|
|
}
|
|
binary.BigEndian.PutUint64(di.nonce[16:24], seqNoAndDF)
|
|
result, success := secretbox.Open(nil, buf[8:], &di.nonce, nd.sessionKey)
|
|
if !success {
|
|
return nil, false
|
|
}
|
|
// Drop duplicates. We do this *after* decryption since we must
|
|
// not advance our state unless decryption succeeded. Doing so
|
|
// would open an easy attack vector where an adversary could
|
|
// inject a packet with a sequence number of (1 << 63) - 1,
|
|
// causing all subsequent genuine packets to get dropped.
|
|
offset, usedOffsets := di.advanceState(seqNo)
|
|
if usedOffsets == nil || usedOffsets.Contains(offset) {
|
|
// We have detected a possible replay attack, but it is
|
|
// possible we may have just received a very old packet, or
|
|
// duplication may have occurred in the network. So let's just
|
|
// drop the packet silently.
|
|
return nil, true
|
|
}
|
|
usedOffsets.Add(offset)
|
|
return result, success
|
|
}
|
|
|
|
// We record seen message sequence numbers in a sliding window of
|
|
// 2*WindowSize which slides in WindowSize increments. This allows us
|
|
// to process out-of-order delivery within the window, while
|
|
// accurately discarding duplicates. By contrast any messages with
|
|
// sequence numbers below the window are discarded as potential
|
|
// duplicates.
|
|
//
|
|
// There are two sets, corresponding to the lower and upper half of
|
|
// the window. We slide the window s.t. that 2nd set always contains
|
|
// the highest seen sequence number. We do this regardless of how far
|
|
// ahead of the current window that sequence number might be, so we
|
|
// can cope with large gaps resulting from packet loss.
|
|
|
|
const (
|
|
WindowSize = 20 // bits
|
|
)
|
|
|
|
func (di *NaClDecryptorInstance) advanceState(seqNo uint64) (int, *bit.Set) {
|
|
var (
|
|
offset = int(seqNo & ((1 << WindowSize) - 1))
|
|
window = seqNo >> WindowSize
|
|
)
|
|
switch delta := int64(window - di.currentWindow); {
|
|
case delta < -1:
|
|
return offset, nil
|
|
case delta == -1:
|
|
return offset, di.previousUsedOffsets
|
|
default:
|
|
return offset, di.usedOffsets
|
|
case delta == +1:
|
|
di.currentWindow = window
|
|
di.previousUsedOffsets = di.usedOffsets
|
|
di.usedOffsets = bit.New()
|
|
return offset, di.usedOffsets
|
|
case delta > +1:
|
|
di.currentWindow = window
|
|
di.previousUsedOffsets = bit.New()
|
|
di.usedOffsets = bit.New()
|
|
return offset, di.usedOffsets
|
|
}
|
|
}
|