Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent replays of server data #78

Merged
merged 15 commits into from
Aug 27, 2020
2 changes: 1 addition & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (s *ssServer) loadConfig(filename string) error {
if !ok {
return fmt.Errorf("Only AEAD ciphers are supported. Found %v", keyConfig.Cipher)
}
cipherList.PushBack(&shadowsocks.CipherEntry{ID: keyConfig.ID, Cipher: aead})
cipherList.PushBack(shadowsocks.MakeCipherEntry(keyConfig.ID, aead, keyConfig.Secret))
}
for port := range s.ports {
portChanges[port] = portChanges[port] - 1
Expand Down
16 changes: 13 additions & 3 deletions shadowsocks/cipher_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@ const maxNonceSize = 12
// CipherEntry holds a Cipher with an identifier.
// The public fields are constant, but lastAddress is mutable under cipherList.mu.
type CipherEntry struct {
ID string
Cipher shadowaead.Cipher
lastClientIP net.IP
ID string
Cipher shadowaead.Cipher
SaltGenerator ServerSaltGenerator
lastClientIP net.IP
}

// MakeCipherEntry constructs a CipherEntry.
func MakeCipherEntry(id string, cipher shadowaead.Cipher, secret string) CipherEntry {
return CipherEntry{
ID: id,
Cipher: cipher,
SaltGenerator: NewServerSaltGenerator(cipher, secret),
}
}

// CipherList is a thread-safe collection of CipherEntry elements that allows for
Expand Down
28 changes: 0 additions & 28 deletions shadowsocks/cipher_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,11 @@ package shadowsocks

import (
"container/list"
"crypto/cipher"
"testing"

"github.com/shadowsocks/go-shadowsocks2/shadowaead"
)

type fakeAEAD struct {
cipher.AEAD
overhead, nonceSize int
}

func (a *fakeAEAD) NonceSize() int {
return a.nonceSize
}

func (a *fakeAEAD) Overhead() int {
return a.overhead
}

type fakeCipher struct {
shadowaead.Cipher
saltsize int
decrypter *fakeAEAD
}

func (c *fakeCipher) SaltSize() int {
return c.saltsize
}

func (c *fakeCipher) Decrypter(b []byte) (cipher.AEAD, error) {
return c.decrypter, nil
}

func TestIncompatibleCiphers(t *testing.T) {
l := list.New()
l.PushBack(&CipherEntry{
Expand Down
31 changes: 30 additions & 1 deletion shadowsocks/cipher_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package shadowsocks

import (
"container/list"
"crypto/cipher"
"fmt"

"github.com/shadowsocks/go-shadowsocks2/core"
Expand Down Expand Up @@ -43,7 +44,8 @@ func MakeTestCiphers(secrets []string) (CipherList, error) {
if err != nil {
return nil, fmt.Errorf("Failed to create cipher %v: %v", i, err)
}
l.PushBack(&CipherEntry{ID: cipherID, Cipher: cipher.(shadowaead.Cipher)})
entry := MakeCipherEntry(cipherID, cipher.(shadowaead.Cipher), secrets[i])
l.PushBack(&entry)
}
cipherList := NewCipherList()
cipherList.Update(l)
Expand All @@ -58,3 +60,30 @@ func MakeTestPayload(size int) []byte {
}
return payload
}

type fakeAEAD struct {
cipher.AEAD
overhead, nonceSize int
}

func (a *fakeAEAD) NonceSize() int {
return a.nonceSize
}

func (a *fakeAEAD) Overhead() int {
return a.overhead
}

type fakeCipher struct {
shadowaead.Cipher
saltsize int
decrypter *fakeAEAD
}

func (c *fakeCipher) SaltSize() int {
return c.saltsize
}

func (c *fakeCipher) Decrypter(b []byte) (cipher.AEAD, error) {
return c.decrypter, nil
}
132 changes: 132 additions & 0 deletions shadowsocks/salt_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2020 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package shadowsocks

import (
"bytes"
"crypto"
"crypto/hmac"
"crypto/rand"
"fmt"
"io"

"github.com/shadowsocks/go-shadowsocks2/shadowaead"
"golang.org/x/crypto/hkdf"
)

// SaltGenerator generates unique salts to use in Shadowsocks connections.
type SaltGenerator interface {
// Returns a new salt
GetSalt(salt []byte) error
}

type ServerSaltGenerator interface {
SaltGenerator
// IsServerSalt returns true if the salt was created by this generator
// and is marked as server-originated.
IsServerSalt(salt []byte) bool
}

// randomSaltGenerator generates a new random salt.
type randomSaltGenerator struct{}

// GetSalt outputs a random salt.
func (randomSaltGenerator) GetSalt(salt []byte) error {
_, err := rand.Read(salt)
return err
}

func (randomSaltGenerator) IsServerSalt(salt []byte) bool {
return false
}

// RandomSaltGenerator is a basic SaltGenerator.
var RandomSaltGenerator ServerSaltGenerator = randomSaltGenerator{}

// serverSaltGenerator generates unique salts that are secretly marked.
type serverSaltGenerator struct {
saltSize int
key []byte
}

// Number of bytes of salt to use as a marker. Increasing this value reduces
// the false positive rate, but increases the likelihood of salt collisions.
// Must be less than or equal to the cipher overhead.
const markLen = 4

// For a random salt to be secure, it needs at least 16 bytes (128 bits) of
// entropy. If adding the mark would reduce the entropy below this level,
// we generate an unmarked random salt.
const minEntropy = 16

// Constant to identify this marking scheme.
var serverSaltLabel = []byte("outline-server-salt")

// NewServerSaltGenerator returns a SaltGenerator whose output is apparently
// random, but is secretly marked as being issued by the server.
// This is useful to prevent the server from accepting its own output in a
// reflection attack.
func NewServerSaltGenerator(cipher shadowaead.Cipher, secret string) ServerSaltGenerator {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
if cipher.SaltSize()-markLen < minEntropy {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
// This cipher doesn't support server marking.
return RandomSaltGenerator
}

// Shadowsocks already uses HKDF-SHA1 to derive the AEAD key, so we use
// the same derivation with a different "info" to generate our HMAC key.
keySource := hkdf.New(crypto.SHA1.New, []byte(secret), nil, serverSaltLabel)
// The key can be any size, but matching the block size is most efficient.
key := make([]byte, crypto.SHA1.Size())
io.ReadFull(keySource, key)
return serverSaltGenerator{cipher.SaltSize(), key}
}

func (sg serverSaltGenerator) splitSalt(salt []byte) (prefix, mark []byte) {
prefixLen := len(salt) - markLen
return salt[:prefixLen], salt[prefixLen:]
}

// getTag takes in a salt prefix and returns the tag.
func (sg serverSaltGenerator) getTag(prefix []byte) []byte {
// Use HMAC-SHA1, even though SHA1 is broken, because HMAC-SHA1 is still
// secure, and we're already using HKDF-SHA1.
hmac := hmac.New(crypto.SHA1.New, sg.key)
hmac.Write(prefix) // Hash.Write never returns an error.
return hmac.Sum(nil)
}

// GetSalt returns an apparently random salt that can be identified
// as server-originated by anyone who knows the Shadowsocks key.
func (sg serverSaltGenerator) GetSalt(salt []byte) error {
if len(salt) != sg.saltSize {
return fmt.Errorf("Wrong salt size: %d != %d", len(salt), sg.saltSize)
}
prefix, mark := sg.splitSalt(salt)
if _, err := rand.Read(prefix); err != nil {
return err
}
tag := sg.getTag(prefix)
copy(mark, tag)
return nil
}

func (sg serverSaltGenerator) IsServerSalt(salt []byte) bool {
if len(salt) != sg.saltSize {
return false
}
prefix, mark := sg.splitSalt(salt)
tag := sg.getTag(prefix)
return bytes.Equal(tag[:markLen], mark)
}
Loading