Skip to content

Commit ff604f9

Browse files
authored
Merge pull request #165 from oif/implement-reuse-detection-filter
Implement reuse detection for cipher
2 parents 8a9bda6 + 5d517aa commit ff604f9

File tree

11 files changed

+256
-2
lines changed

11 files changed

+256
-2
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.12
44

55
require (
66
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da
7+
github.com/riobard/go-bloom v0.0.0-20200213042214-218e1707c495
78
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
89
)
910

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ github.com/golang/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:5JyrLPvD/ZdaY
88
github.com/golang/sys v0.0.0-20190412213103-97732733099d h1:blRtD+FQOxZ6P7jigy+HS0R8zyGOMOv8TET4wCpzVwM=
99
github.com/golang/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1010
github.com/golang/text v0.3.0/go.mod h1:GUiq9pdJKRKKAZXiVgWFEvocYuREvC14NhI4OPgEjeE=
11+
github.com/riobard/go-bloom v0.0.0-20170218180955-2b113c64a69b h1:H9yjH/g5w8MOPjQR2zMSP/Md1kKtj/33fIht9ChC2OU=
12+
github.com/riobard/go-bloom v0.0.0-20170218180955-2b113c64a69b/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
13+
github.com/riobard/go-bloom v0.0.0-20200213042214-218e1707c495 h1:p7xbxYTzzfXghR1kpsJDeoVVRRWAotKc8u7FP/N48rU=
14+
github.com/riobard/go-bloom v0.0.0-20200213042214-218e1707c495/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=

internal/bloomring.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package internal
2+
3+
import (
4+
"hash/fnv"
5+
"sync"
6+
7+
"github.com/riobard/go-bloom"
8+
)
9+
10+
// simply use Double FNV here as our Bloom Filter hash
11+
func doubleFNV(b []byte) (uint64, uint64) {
12+
hx := fnv.New64()
13+
hx.Write(b)
14+
x := hx.Sum64()
15+
hy := fnv.New64a()
16+
hy.Write(b)
17+
y := hy.Sum64()
18+
return x, y
19+
}
20+
21+
type BloomRing struct {
22+
slotCapacity int
23+
slotPosition int
24+
slotCount int
25+
entryCounter int
26+
slots []bloom.Filter
27+
mutex sync.RWMutex
28+
}
29+
30+
func NewBloomRing(slot, capacity int, falsePositiveRate float64) *BloomRing {
31+
// Calculate entries for each slot
32+
r := &BloomRing{
33+
slotCapacity: capacity / slot,
34+
slotCount: slot,
35+
slots: make([]bloom.Filter, slot),
36+
}
37+
for i := 0; i < slot; i++ {
38+
r.slots[i] = bloom.New(r.slotCapacity, falsePositiveRate, doubleFNV)
39+
}
40+
return r
41+
}
42+
43+
func (r *BloomRing) Add(b []byte) {
44+
r.mutex.Lock()
45+
defer r.mutex.Unlock()
46+
slot := r.slots[r.slotPosition]
47+
if r.entryCounter > r.slotCapacity {
48+
// Move to next slot and reset
49+
r.slotPosition = (r.slotPosition + 1) % r.slotCount
50+
slot = r.slots[r.slotPosition]
51+
slot.Reset()
52+
r.entryCounter = 0
53+
}
54+
r.entryCounter++
55+
slot.Add(b)
56+
}
57+
58+
func (r *BloomRing) Test(b []byte) bool {
59+
r.mutex.RLock()
60+
defer r.mutex.RUnlock()
61+
for _, s := range r.slots {
62+
if s.Test(b) {
63+
return true
64+
}
65+
}
66+
return false
67+
}

internal/bloomring_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package internal_test
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"testing"
7+
8+
"github.com/shadowsocks/go-shadowsocks2/internal"
9+
)
10+
11+
var (
12+
bloomRingInstance *internal.BloomRing
13+
)
14+
15+
func TestMain(m *testing.M) {
16+
bloomRingInstance = internal.NewBloomRing(internal.DefaultSFSlot, int(internal.DefaultSFCapacity),
17+
internal.DefaultSFFPR)
18+
os.Exit(m.Run())
19+
}
20+
21+
func TestBloomRing_Add(t *testing.T) {
22+
defer func() {
23+
if any := recover(); any != nil {
24+
t.Fatalf("Should not got panic while adding item: %v", any)
25+
}
26+
}()
27+
bloomRingInstance.Add(make([]byte, 16))
28+
}
29+
30+
func TestBloomRing_Test(t *testing.T) {
31+
buf := []byte("shadowsocks")
32+
bloomRingInstance.Add(buf)
33+
if !bloomRingInstance.Test(buf) {
34+
t.Fatal("Test on filter missing")
35+
}
36+
}
37+
38+
func BenchmarkBloomRing(b *testing.B) {
39+
// Generate test samples with different length
40+
samples := make([][]byte, internal.DefaultSFCapacity-internal.DefaultSFSlot)
41+
var checkPoints [][]byte
42+
for i := 0; i < len(samples); i++ {
43+
samples[i] = []byte(fmt.Sprint(i))
44+
if i%1000 == 0 {
45+
checkPoints = append(checkPoints, samples[i])
46+
}
47+
}
48+
b.Logf("Generated %d samples and %d check points", len(samples), len(checkPoints))
49+
for i := 1; i < 16; i++ {
50+
b.Run(fmt.Sprintf("Slot%d", i), benchmarkBloomRing(samples, checkPoints, i))
51+
}
52+
}
53+
54+
func benchmarkBloomRing(samples, checkPoints [][]byte, slot int) func(*testing.B) {
55+
filter := internal.NewBloomRing(slot, int(internal.DefaultSFCapacity), internal.DefaultSFFPR)
56+
for _, sample := range samples {
57+
filter.Add(sample)
58+
}
59+
return func(b *testing.B) {
60+
b.ResetTimer()
61+
b.ReportAllocs()
62+
for i := 0; i < b.N; i++ {
63+
for _, cp := range checkPoints {
64+
filter.Test(cp)
65+
}
66+
}
67+
}
68+
}

internal/saltfilter.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strconv"
7+
)
8+
9+
// Those suggest value are all set according to
10+
// https://github.com/shadowsocks/shadowsocks-org/issues/44#issuecomment-281021054
11+
// Due to this package contains various internal implementation so const named with DefaultBR prefix
12+
const (
13+
DefaultSFCapacity = 1e6
14+
// FalsePositiveRate
15+
DefaultSFFPR = 1e-6
16+
DefaultSFSlot = 10
17+
)
18+
19+
const EnvironmentPrefix = "SHADOWSOCKS_"
20+
21+
// A shared instance used for checking salt repeat
22+
var saltfilter *BloomRing
23+
24+
func init() {
25+
var (
26+
finalCapacity = DefaultSFCapacity
27+
finalFPR = DefaultSFFPR
28+
finalSlot = float64(DefaultSFSlot)
29+
)
30+
for _, opt := range []struct {
31+
ENVName string
32+
Target *float64
33+
}{
34+
{
35+
ENVName: "CAPACITY",
36+
Target: &finalCapacity,
37+
},
38+
{
39+
ENVName: "FPR",
40+
Target: &finalFPR,
41+
},
42+
{
43+
ENVName: "SLOT",
44+
Target: &finalSlot,
45+
},
46+
} {
47+
envKey := EnvironmentPrefix + "SF_" + opt.ENVName
48+
env := os.Getenv(envKey)
49+
if env != "" {
50+
p, err := strconv.ParseFloat(env, 64)
51+
if err != nil {
52+
panic(fmt.Sprintf("Invalid envrionment `%s` setting in saltfilter: %s", envKey, env))
53+
}
54+
*opt.Target = p
55+
}
56+
}
57+
// Support disable saltfilter by given a negative capacity
58+
if finalCapacity <= 0 {
59+
return
60+
}
61+
saltfilter = NewBloomRing(int(finalSlot), int(finalCapacity), finalFPR)
62+
}
63+
64+
// TestSalt returns true if salt is repeated
65+
func TestSalt(b []byte) bool {
66+
// If nil means feature disabled, return false to bypass salt repeat detection
67+
if saltfilter == nil {
68+
return false
69+
}
70+
return saltfilter.Test(b)
71+
}
72+
73+
// AddSalt salt to filter
74+
func AddSalt(b []byte) {
75+
// If nil means feature disabled
76+
if saltfilter == nil {
77+
return
78+
}
79+
saltfilter.Add(b)
80+
}

shadowaead/cipher.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import (
44
"crypto/aes"
55
"crypto/cipher"
66
"crypto/sha1"
7+
"errors"
78
"io"
89
"strconv"
910

1011
"golang.org/x/crypto/chacha20poly1305"
1112
"golang.org/x/crypto/hkdf"
1213
)
1314

15+
// ErrRepeatedSalt means detected a reused salt
16+
var ErrRepeatedSalt = errors.New("repeated salt detected")
17+
1418
type Cipher interface {
1519
KeySize() int
1620
SaltSize() int

shadowaead/packet.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"io"
77
"net"
88
"sync"
9+
10+
"github.com/shadowsocks/go-shadowsocks2/internal"
911
)
1012

1113
// ErrShortPacket means that the packet is too short for a valid encrypted packet.
@@ -27,6 +29,7 @@ func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) {
2729
if err != nil {
2830
return nil, err
2931
}
32+
internal.AddSalt(salt)
3033

3134
if len(dst) < saltSize+len(plaintext)+aead.Overhead() {
3235
return nil, io.ErrShortBuffer
@@ -43,10 +46,14 @@ func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) {
4346
return nil, ErrShortPacket
4447
}
4548
salt := pkt[:saltSize]
49+
if internal.TestSalt(salt) {
50+
return nil, ErrRepeatedSalt
51+
}
4652
aead, err := ciph.Decrypter(salt)
4753
if err != nil {
4854
return nil, err
4955
}
56+
internal.AddSalt(salt)
5057
if len(pkt) < saltSize+aead.Overhead() {
5158
return nil, ErrShortPacket
5259
}

shadowaead/stream.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"crypto/rand"
77
"io"
88
"net"
9+
10+
"github.com/shadowsocks/go-shadowsocks2/internal"
911
)
1012

1113
// payloadSizeMask is the maximum size of payload in bytes.
@@ -203,11 +205,14 @@ func (c *streamConn) initReader() error {
203205
if _, err := io.ReadFull(c.Conn, salt); err != nil {
204206
return err
205207
}
206-
208+
if internal.TestSalt(salt) {
209+
return ErrRepeatedSalt
210+
}
207211
aead, err := c.Decrypter(salt)
208212
if err != nil {
209213
return err
210214
}
215+
internal.AddSalt(salt)
211216

212217
c.r = newReader(c.Conn, aead)
213218
return nil
@@ -244,6 +249,7 @@ func (c *streamConn) initWriter() error {
244249
if err != nil {
245250
return err
246251
}
252+
internal.AddSalt(salt)
247253
c.w = newWriter(c.Conn, aead)
248254
return nil
249255
}

shadowstream/cipher.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ package shadowstream
33
import (
44
"crypto/aes"
55
"crypto/cipher"
6+
"errors"
67
"strconv"
78

89
"github.com/aead/chacha20"
910
"github.com/aead/chacha20/chacha"
1011
)
1112

13+
// ErrRepeatedSalt means detected a reused salt
14+
var ErrRepeatedSalt = errors.New("repeated salt detected")
15+
1216
// Cipher generates a pair of stream ciphers for encryption and decryption.
1317
type Cipher interface {
1418
IVSize() int

shadowstream/packet.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"io"
77
"net"
88
"sync"
9+
10+
"github.com/shadowsocks/go-shadowsocks2/internal"
911
)
1012

1113
// ErrShortPacket means the packet is too short to be a valid encrypted packet.
@@ -23,7 +25,7 @@ func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) {
2325
if err != nil {
2426
return nil, err
2527
}
26-
28+
internal.AddSalt(iv)
2729
s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext)
2830
return dst[:len(iv)+len(plaintext)], nil
2931
}
@@ -39,6 +41,10 @@ func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) {
3941
return nil, io.ErrShortBuffer
4042
}
4143
iv := pkt[:s.IVSize()]
44+
if internal.TestSalt(iv) {
45+
return nil, ErrRepeatedSalt
46+
}
47+
internal.AddSalt(iv)
4248
s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):])
4349
return dst[:len(pkt)-len(iv)], nil
4450
}

shadowstream/stream.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"crypto/rand"
77
"io"
88
"net"
9+
10+
"github.com/shadowsocks/go-shadowsocks2/internal"
911
)
1012

1113
const bufSize = 32 * 1024
@@ -114,6 +116,10 @@ func (c *conn) initReader() error {
114116
if _, err := io.ReadFull(c.Conn, iv); err != nil {
115117
return err
116118
}
119+
if internal.TestSalt(iv) {
120+
return ErrRepeatedSalt
121+
}
122+
internal.AddSalt(iv)
117123
c.r = &reader{Reader: c.Conn, Stream: c.Decrypter(iv), buf: buf}
118124
}
119125
return nil
@@ -147,6 +153,7 @@ func (c *conn) initWriter() error {
147153
if _, err := c.Conn.Write(iv); err != nil {
148154
return err
149155
}
156+
internal.AddSalt(iv)
150157
c.w = &writer{Writer: c.Conn, Stream: c.Encrypter(iv), buf: buf}
151158
}
152159
return nil

0 commit comments

Comments
 (0)