diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go
index 22eecad45..6efc44a37 100644
--- a/cmd/syncthing/gui.go
+++ b/cmd/syncthing/gui.go
@@ -353,7 +353,7 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
if curAcc := cfg.Options().URAccepted; newCfg.Options.URAccepted > curAcc {
// UR was enabled
newCfg.Options.URAccepted = usageReportVersion
- newCfg.Options.URUniqueID = randomString(6)
+ newCfg.Options.URUniqueID = randomString(8)
err := sendUsageReport(m)
if err != nil {
l.Infoln("Usage report:", err)
diff --git a/cmd/syncthing/gui_csrf.go b/cmd/syncthing/gui_csrf.go
index 7f32e705c..19159dc31 100644
--- a/cmd/syncthing/gui_csrf.go
+++ b/cmd/syncthing/gui_csrf.go
@@ -17,8 +17,6 @@ package main
import (
"bufio"
- "crypto/rand"
- "encoding/base64"
"fmt"
"net/http"
"os"
@@ -88,7 +86,7 @@ func validCsrfToken(token string) bool {
}
func newCsrfToken() string {
- token := randomString(30)
+ token := randomString(32)
csrfMut.Lock()
csrfTokens = append(csrfTokens, token)
@@ -140,13 +138,3 @@ func loadCsrfTokens() {
csrfTokens = append(csrfTokens, s.Text())
}
}
-
-func randomString(len int) string {
- bs := make([]byte, len)
- _, err := rand.Reader.Read(bs)
- if err != nil {
- l.Fatalln(err)
- }
-
- return base64.StdEncoding.EncodeToString(bs)
-}
diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go
index c1184a7b3..7dd259a05 100644
--- a/cmd/syncthing/main.go
+++ b/cmd/syncthing/main.go
@@ -21,7 +21,6 @@ import (
"fmt"
"io"
"log"
- "math/rand"
"net"
"net/http"
_ "net/http/pprof"
@@ -177,10 +176,6 @@ are mostly useful for developers. Use with care.
available CPU cores.`
)
-func init() {
- rand.Seed(time.Now().UnixNano())
-}
-
// Command line and environment options
var (
reset bool
@@ -383,6 +378,10 @@ func syncthingMain() {
}
}
+ // We reinitialize the predictable RNG with our device ID, to get a
+ // sequence that is always the same but unique to this syncthing instance.
+ predictableRandom.Seed(seedFromBytes(cert.Certificate[0]))
+
myID = protocol.NewDeviceID(cert.Certificate[0])
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
@@ -574,7 +573,7 @@ func syncthingMain() {
if opts.URUniqueID == "" {
// Previously the ID was generated from the node ID. We now need
// to generate a new one.
- opts.URUniqueID = randomString(6)
+ opts.URUniqueID = randomString(8)
cfg.SetOptions(opts)
cfg.Save()
}
@@ -781,11 +780,8 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
return 0
}
- // We seed the random number generator with the node ID to get a
- // repeatable sequence of random external ports.
- rnd := rand.NewSource(certSeed(cert.Certificate[0]))
for i := 0; i < 10; i++ {
- r := 1024 + int(rnd.Int63()%(65535-1024))
+ r := 1024 + predictableRandom.Intn(65535-1024)
err := igd.AddPortMapping(upnp.TCP, r, port, "syncthing", cfg.Options().UPnPLease*60)
if err == nil {
return r
diff --git a/cmd/syncthing/random.go b/cmd/syncthing/random.go
new file mode 100644
index 000000000..4bba8c939
--- /dev/null
+++ b/cmd/syncthing/random.go
@@ -0,0 +1,67 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see .
+
+package main
+
+import (
+ "crypto/md5"
+ cryptoRand "crypto/rand"
+ "encoding/binary"
+ mathRand "math/rand"
+)
+
+// randomCharset contains the characters that can make up a randomString().
+const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
+
+// predictableRandom is an RNG that will always have the same sequence. It
+// will be seeded with the device ID during startup, so that the sequence is
+// predictable but varies between instances.
+var predictableRandom = mathRand.New(mathRand.NewSource(42))
+
+func init() {
+ // The default RNG should be seeded with something good.
+ mathRand.Seed(randomInt64())
+}
+
+// randomString returns a string of random characters (taken from
+// randomCharset) of the specified length.
+func randomString(l int) string {
+ bs := make([]byte, l)
+ for i := range bs {
+ bs[i] = randomCharset[mathRand.Intn(len(randomCharset))]
+ }
+ return string(bs)
+}
+
+// randomInt64 returns a strongly random int64, slowly
+func randomInt64() int64 {
+ var bs [8]byte
+ n, err := cryptoRand.Reader.Read(bs[:])
+ if n != 8 || err != nil {
+ panic("randomness failure")
+ }
+ return seedFromBytes(bs[:])
+}
+
+// seedFromBytes calculates a weak 64 bit hash from the given byte slice,
+// suitable for use a predictable random seed.
+func seedFromBytes(bs []byte) int64 {
+ h := md5.New()
+ h.Write(bs)
+ s := h.Sum(nil)
+ // The MD5 hash of the byte slice is 16 bytes long. We interpret it as two
+ // uint64s and XOR them together.
+ return int64(binary.BigEndian.Uint64(s[0:]) ^ binary.BigEndian.Uint64(s[8:]))
+}
diff --git a/cmd/syncthing/random_test.go b/cmd/syncthing/random_test.go
new file mode 100644
index 000000000..a04b96e5f
--- /dev/null
+++ b/cmd/syncthing/random_test.go
@@ -0,0 +1,80 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see .
+
+package main
+
+import "testing"
+
+func TestPredictableRandom(t *testing.T) {
+ // predictable random sequence is predictable
+ e := 3440579354231278675
+ if v := predictableRandom.Int(); v != e {
+ t.Errorf("Unexpected random value %d != %d", v, e)
+ }
+}
+
+func TestSeedFromBytes(t *testing.T) {
+ // should always return the same seed for the same bytes
+ tcs := []struct {
+ bs []byte
+ v int64
+ }{
+ {[]byte("hello world"), -3639725434188061933},
+ {[]byte("hello worlx"), -2539100776074091088},
+ }
+
+ for _, tc := range tcs {
+ if v := seedFromBytes(tc.bs); v != tc.v {
+ t.Errorf("Unexpected seed value %d != %d", v, tc.v)
+ }
+ }
+}
+
+func TestRandomString(t *testing.T) {
+ for _, l := range []int{0, 1, 2, 3, 4, 8, 42} {
+ s := randomString(l)
+ if len(s) != l {
+ t.Errorf("Incorrect length %d != %s", len(s), l)
+ }
+ }
+
+ strings := make([]string, 1000)
+ for i := range strings {
+ strings[i] = randomString(8)
+ for j := range strings {
+ if i == j {
+ continue
+ }
+ if strings[i] == strings[j] {
+ t.Errorf("Repeated random string %q", strings[i])
+ }
+ }
+ }
+}
+
+func TestRandomInt64(t *testing.T) {
+ ints := make([]int64, 1000)
+ for i := range ints {
+ ints[i] = randomInt64()
+ for j := range ints {
+ if i == j {
+ continue
+ }
+ if ints[i] == ints[j] {
+ t.Errorf("Repeated random int64 %d", ints[i])
+ }
+ }
+ }
+}
diff --git a/cmd/syncthing/tls.go b/cmd/syncthing/tls.go
index bd8bb126e..de8d71af3 100644
--- a/cmd/syncthing/tls.go
+++ b/cmd/syncthing/tls.go
@@ -19,11 +19,9 @@ import (
"bufio"
"crypto/rand"
"crypto/rsa"
- "crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
- "encoding/binary"
"encoding/pem"
"io"
"math/big"
@@ -45,13 +43,6 @@ func loadCert(dir string, prefix string) (tls.Certificate, error) {
return tls.LoadX509KeyPair(cf, kf)
}
-func certSeed(bs []byte) int64 {
- hf := sha256.New()
- hf.Write(bs)
- id := hf.Sum(nil)
- return int64(binary.BigEndian.Uint64(id))
-}
-
func newCertificate(dir string, prefix string) {
l.Infoln("Generating RSA key and certificate...")