Scope

Note of the shadowaead package in go-shadowsocks2.

Cipher and KDF

The package exposes a Cipher interface with KeySize, SaltSize, per-salt Encrypter and Decrypter factories. A concrete metaCipher HKDF-derives a subkey from a pre-shared key (PSK) and the random salt:

// cipher.go
func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
    subkey := make([]byte, a.KeySize())
    hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
    return a.makeAEAD(subkey)
}

func (a *metaCipher) SaltSize() int {
    if ks := a.KeySize(); ks > 16 { return ks }
    return 16
}

// AESGCM: key length 16/24/32; ChaCha20-Poly1305: key length 32

hkdfSHA1 is used to fill subkey and panics on unexpected I/O failure (from io.ReadFull on the HKDF reader), which should not happen in normal operation.

Stream (TCP)

A stream connection begins with a random salt sent in cleartext by the writer, the reader consumes it and initializes AEAD. After that, the stream is a sequence of records. Each record has:

[encrypted 2-byte length][length tag][encrypted payload][payload tag]

Key points from the code:

// stream.go
const payloadSizeMask = 0x3FFF

func (w *writer) ReadFrom(r io.Reader) (n int64, err error) {
    payloadBuf := buf[2+w.Overhead() : 2+w.Overhead()+payloadSizeMask]
    nr, _ := r.Read(payloadBuf)
    buf[0], buf[1] = byte(nr>>8), byte(nr)
    w.Seal(buf[:0], w.nonce, buf[:2], nil)
    increment(w.nonce)
    w.Seal(payloadBuf[:0], w.nonce, payloadBuf[:nr], nil)
    increment(w.nonce)
    _, _ = w.Writer.Write(buf[:2+w.Overhead()+nr+w.Overhead()])
}

func (r *reader) read() (int, error) {
    buf := r.buf[:2+r.Overhead()]
    _, _ = io.ReadFull(r.Reader, buf)
    _, err := r.Open(buf[:0], r.nonce, buf, nil)
    increment(r.nonce)
    size := (int(buf[0])<<8 + int(buf[1])) & payloadSizeMask

    buf = r.buf[:size+r.Overhead()]
    _, _ = io.ReadFull(r.Reader, buf)
    _, err = r.Open(buf[:0], r.nonce, buf, nil)
    increment(r.nonce)
    return size, err
}

func increment(b []byte) { // little-endian counter
    for i := range b { b[i]++; if b[i] != 0 { return } }
}

Salt handling in streams:

// streamConn.initWriter
salt := make([]byte, c.SaltSize())
io.ReadFull(rand.Reader, salt)
aead, _ := c.Encrypter(salt)
c.Conn.Write(salt)
internal.AddSalt(salt)

// streamConn.initReader
io.ReadFull(c.Conn, salt)
aead, _ := c.Decrypter(salt)
if internal.CheckSalt(salt) { return ErrRepeatedSalt }

internal.AddSalt and CheckSalt track salts to detect reuse within the process.

Packet (UDP)

UDP packets are handled independently. Each packet carries its own random salt and uses a zero nonce for AEAD:

// packet.go
var _zerononce [128]byte

func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) {
    salt := dst[:ciph.SaltSize()]
    io.ReadFull(rand.Reader, salt)
    aead, _ := ciph.Encrypter(salt)
    internal.AddSalt(salt)
    b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil)
    return dst[:saltSize+len(b)], nil
}

func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) {
    salt := pkt[:ciph.SaltSize()]
    aead, _ := ciph.Decrypter(salt)
    if internal.CheckSalt(salt) { return nil, ErrRepeatedSalt }
    b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil)
    return b, err
}

The code enforces size checks and returns ErrShortPacket or io.ErrShortBuffer where applicable.

Public API

NewConn(net.Conn, Cipher) net.Conn wraps a stream-oriented connection with the AEAD framing described above.

NewPacketConn(net.PacketConn, Cipher) net.PacketConn wraps a packet-oriented connection.

A specialized udpConn also exposes WriteToUDPAddrPort and ReadFromUDPAddrPort.

Errors and Safety

ErrRepeatedSalt returned when a salt is detected as reused by internal.CheckSalt.

ErrShortPacket basic sanity check for UDP input size.

AES-GCM requires 16/24/32 key length, and ChaCha20-Poly1305 requires 32.

Practical Notes

References