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:
- Length is big-endian 2 bytes, masked by
0x3FFF(higher 2 bits reserved). - A single nonce buffer starts at zero (
make([]byte, aead.NonceSize())). - The nonce is incremented (little-endian) after each AEAD call (twice per record: once for length, once for payload).
// 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
- Use a key of the correct length for the selected suite.
- Salt is per connection (streams) or per packet (UDP) and is generated by
crypto/rand. - In streams, the nonce is a counter, do not reuse AEAD state across connections.
- UDP uses zero nonce with per-packet subkeys, packets are independent.
References
- Source:
go-shadowsocks2/shadowaead.