2020-11-09 14:33:32 +00:00
|
|
|
// Copyright (C) 2019 The Syncthing Authors.
|
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
|
|
package protocol
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2021-02-27 07:57:12 +00:00
|
|
|
"fmt"
|
2020-11-09 14:33:32 +00:00
|
|
|
"reflect"
|
2021-02-27 07:57:12 +00:00
|
|
|
"regexp"
|
2020-11-09 14:33:32 +00:00
|
|
|
"strings"
|
2021-03-07 17:44:21 +00:00
|
|
|
"sync"
|
2020-11-09 14:33:32 +00:00
|
|
|
"testing"
|
2020-12-09 17:16:14 +00:00
|
|
|
|
|
|
|
"github.com/syncthing/syncthing/lib/rand"
|
2021-03-07 17:44:21 +00:00
|
|
|
"github.com/syncthing/syncthing/lib/sha256"
|
2020-11-09 14:33:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestEnDecryptName(t *testing.T) {
|
2021-02-27 07:57:12 +00:00
|
|
|
pattern := regexp.MustCompile(
|
|
|
|
fmt.Sprintf("^[0-9A-V]%s/[0-9A-V]{2}/([0-9A-V]{%d}/)*[0-9A-V]{1,%d}$",
|
|
|
|
regexp.QuoteMeta(encryptedDirExtension),
|
|
|
|
maxPathComponent, maxPathComponent-1))
|
|
|
|
|
|
|
|
makeName := func(n int) string {
|
|
|
|
b := make([]byte, n)
|
|
|
|
for i := range b {
|
|
|
|
b[i] = byte('a' + i%26)
|
|
|
|
}
|
|
|
|
return string(b)
|
|
|
|
}
|
|
|
|
|
2020-11-09 14:33:32 +00:00
|
|
|
var key [32]byte
|
|
|
|
cases := []string{
|
|
|
|
"",
|
|
|
|
"foo",
|
|
|
|
"a longer name/with/slashes and spaces",
|
2021-02-27 07:57:12 +00:00
|
|
|
makeName(maxPathComponent),
|
|
|
|
makeName(1 + maxPathComponent),
|
|
|
|
makeName(2 * maxPathComponent),
|
|
|
|
makeName(1 + 2*maxPathComponent),
|
2020-11-09 14:33:32 +00:00
|
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
|
|
var prev string
|
|
|
|
for i := 0; i < 5; i++ {
|
|
|
|
enc := encryptName(tc, &key)
|
|
|
|
if prev != "" && prev != enc {
|
|
|
|
t.Error("name should always encrypt the same")
|
|
|
|
}
|
|
|
|
prev = enc
|
|
|
|
if tc != "" && strings.Contains(enc, tc) {
|
|
|
|
t.Error("shouldn't contain plaintext")
|
|
|
|
}
|
2021-02-27 07:57:12 +00:00
|
|
|
if !pattern.MatchString(enc) {
|
|
|
|
t.Fatalf("encrypted name %s doesn't match %s",
|
|
|
|
enc, pattern)
|
|
|
|
}
|
|
|
|
|
2020-11-09 14:33:32 +00:00
|
|
|
dec, err := decryptName(enc, &key)
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
if dec != tc {
|
|
|
|
t.Error("mismatch after decryption")
|
|
|
|
}
|
2021-02-27 07:57:12 +00:00
|
|
|
t.Logf("%q encrypts as %q", tc, enc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-04 12:36:26 +00:00
|
|
|
func TestKeyDerivation(t *testing.T) {
|
|
|
|
folderKey := KeyFromPassword("my folder", "my password")
|
|
|
|
encryptedName := encryptDeterministic([]byte("filename.txt"), folderKey, nil)
|
|
|
|
if base32Hex.EncodeToString(encryptedName) != "3T5957I4IOA20VEIEER6JSQG0PEPIRV862II3K7LOF75Q" {
|
|
|
|
t.Error("encrypted name mismatch")
|
|
|
|
}
|
|
|
|
|
|
|
|
fileKey := FileKey("filename.txt", folderKey)
|
|
|
|
// fmt.Println(base32Hex.EncodeToString(encryptBytes([]byte("hello world"), fileKey))) => A1IPD...
|
|
|
|
const encrypted = `A1IPD28ISL7VNPRSSSQM2L31L3IJPC08283RO89J5UG0TI9P38DO9RFGK12DK0KD7PKQP6U51UL2B6H96O`
|
|
|
|
bs, _ := base32Hex.DecodeString(encrypted)
|
|
|
|
dec, err := DecryptBytes(bs, fileKey)
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
if string(dec) != "hello world" {
|
|
|
|
t.Error("decryption mismatch")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-27 07:57:12 +00:00
|
|
|
func TestDecryptNameInvalid(t *testing.T) {
|
|
|
|
key := new([32]byte)
|
|
|
|
for _, c := range []string{
|
|
|
|
"T.syncthing-enc/OD",
|
|
|
|
"T.syncthing-enc/OD/",
|
|
|
|
"T.wrong-extension/OD/PHVDD67S7FI2K5QQMPSOFSK",
|
|
|
|
"OD/PHVDD67S7FI2K5QQMPSOFSK",
|
|
|
|
} {
|
|
|
|
if _, err := decryptName(c, key); err == nil {
|
|
|
|
t.Errorf("no error for %q", c)
|
2020-11-09 14:33:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestEnDecryptBytes(t *testing.T) {
|
|
|
|
var key [32]byte
|
|
|
|
cases := [][]byte{
|
|
|
|
{},
|
|
|
|
{1, 2, 3, 4, 5},
|
|
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
|
|
var prev []byte
|
|
|
|
for i := 0; i < 5; i++ {
|
|
|
|
enc := encryptBytes(tc, &key)
|
|
|
|
if bytes.Equal(enc, prev) {
|
|
|
|
t.Error("encryption should not repeat")
|
|
|
|
}
|
|
|
|
prev = enc
|
|
|
|
if len(tc) > 0 && bytes.Contains(enc, tc) {
|
|
|
|
t.Error("shouldn't contain plaintext")
|
|
|
|
}
|
|
|
|
dec, err := DecryptBytes(enc, &key)
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
|
|
|
if !bytes.Equal(dec, tc) {
|
|
|
|
t.Error("mismatch after decryption")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-13 15:57:36 +00:00
|
|
|
func encFileInfo() FileInfo {
|
|
|
|
return FileInfo{
|
2020-11-09 14:33:32 +00:00
|
|
|
Name: "hello",
|
|
|
|
Size: 45,
|
|
|
|
Permissions: 0755,
|
|
|
|
ModifiedS: 8080,
|
2021-10-06 08:26:54 +00:00
|
|
|
Sequence: 1000,
|
2020-11-09 14:33:32 +00:00
|
|
|
Blocks: []BlockInfo{
|
|
|
|
{
|
2021-02-02 19:15:14 +00:00
|
|
|
Offset: 0,
|
|
|
|
Size: 45,
|
|
|
|
Hash: []byte{1, 2, 3},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Offset: 45,
|
|
|
|
Size: 45,
|
|
|
|
Hash: []byte{1, 2, 3},
|
2020-11-09 14:33:32 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2021-03-13 15:57:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestEnDecryptFileInfo(t *testing.T) {
|
|
|
|
var key [32]byte
|
|
|
|
fi := encFileInfo()
|
2020-11-09 14:33:32 +00:00
|
|
|
|
|
|
|
enc := encryptFileInfo(fi, &key)
|
2021-02-02 19:15:14 +00:00
|
|
|
if bytes.Equal(enc.Blocks[0].Hash, enc.Blocks[1].Hash) {
|
|
|
|
t.Error("block hashes should not repeat when on different offsets")
|
|
|
|
}
|
2021-08-04 21:12:01 +00:00
|
|
|
if enc.RawBlockSize < MinBlockSize {
|
|
|
|
t.Error("Too small raw block size:", enc.RawBlockSize)
|
|
|
|
}
|
2021-10-06 08:26:54 +00:00
|
|
|
if enc.Sequence != fi.Sequence {
|
|
|
|
t.Error("encrypted fileinfo didn't maintain sequence number")
|
|
|
|
}
|
2021-02-02 19:15:14 +00:00
|
|
|
again := encryptFileInfo(fi, &key)
|
|
|
|
if !bytes.Equal(enc.Blocks[0].Hash, again.Blocks[0].Hash) {
|
|
|
|
t.Error("block hashes should remain stable (0)")
|
|
|
|
}
|
|
|
|
if !bytes.Equal(enc.Blocks[1].Hash, again.Blocks[1].Hash) {
|
|
|
|
t.Error("block hashes should remain stable (1)")
|
|
|
|
}
|
|
|
|
|
2021-10-06 08:26:54 +00:00
|
|
|
// Simulate the remote setting the sequence number when writing to db
|
|
|
|
enc.Sequence = 10
|
|
|
|
|
2020-11-09 14:33:32 +00:00
|
|
|
dec, err := DecryptFileInfo(enc, &key)
|
|
|
|
if err != nil {
|
|
|
|
t.Error(err)
|
|
|
|
}
|
2021-10-06 08:26:54 +00:00
|
|
|
if dec.Sequence != enc.Sequence {
|
|
|
|
t.Error("decrypted fileinfo didn't maintain sequence number")
|
|
|
|
}
|
|
|
|
dec.Sequence = fi.Sequence
|
2020-11-09 14:33:32 +00:00
|
|
|
if !reflect.DeepEqual(fi, dec) {
|
|
|
|
t.Error("mismatch after decryption")
|
|
|
|
}
|
|
|
|
}
|
2020-11-23 17:37:27 +00:00
|
|
|
|
2021-03-13 15:57:36 +00:00
|
|
|
func TestEncryptedFileInfoConsistency(t *testing.T) {
|
|
|
|
var key [32]byte
|
|
|
|
files := []FileInfo{
|
|
|
|
encFileInfo(),
|
|
|
|
encFileInfo(),
|
|
|
|
}
|
|
|
|
files[1].SetIgnored()
|
|
|
|
for i, f := range files {
|
|
|
|
enc := encryptFileInfo(f, &key)
|
|
|
|
if err := checkFileInfoConsistency(enc); err != nil {
|
|
|
|
t.Errorf("%v: %v", i, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-23 17:37:27 +00:00
|
|
|
func TestIsEncryptedParent(t *testing.T) {
|
2020-12-09 17:16:14 +00:00
|
|
|
comp := rand.String(maxPathComponent)
|
2020-11-23 17:37:27 +00:00
|
|
|
cases := []struct {
|
|
|
|
path string
|
|
|
|
is bool
|
|
|
|
}{
|
|
|
|
{"", false},
|
|
|
|
{".", false},
|
|
|
|
{"/", false},
|
|
|
|
{"12" + encryptedDirExtension, false},
|
|
|
|
{"1" + encryptedDirExtension, true},
|
|
|
|
{"1" + encryptedDirExtension + "/b", false},
|
|
|
|
{"1" + encryptedDirExtension + "/bc", true},
|
|
|
|
{"1" + encryptedDirExtension + "/bcd", false},
|
|
|
|
{"1" + encryptedDirExtension + "/bc/foo", false},
|
|
|
|
{"1.12/22", false},
|
2020-12-09 17:16:14 +00:00
|
|
|
{"1" + encryptedDirExtension + "/bc/" + comp, true},
|
|
|
|
{"1" + encryptedDirExtension + "/bc/" + comp + "/" + comp, true},
|
|
|
|
{"1" + encryptedDirExtension + "/bc/" + comp + "a", false},
|
|
|
|
{"1" + encryptedDirExtension + "/bc/" + comp + "/a/" + comp, false},
|
2020-11-23 17:37:27 +00:00
|
|
|
}
|
|
|
|
for _, tc := range cases {
|
2021-04-11 13:29:43 +00:00
|
|
|
if res := IsEncryptedParent(strings.Split(tc.path, "/")); res != tc.is {
|
2020-11-23 17:37:27 +00:00
|
|
|
t.Errorf("%v: got %v, expected %v", tc.path, res, tc.is)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-07 17:44:21 +00:00
|
|
|
|
|
|
|
var benchmarkFileKey struct {
|
|
|
|
key [keySize]byte
|
|
|
|
sync.Once
|
|
|
|
}
|
|
|
|
|
|
|
|
func BenchmarkFileKey(b *testing.B) {
|
|
|
|
benchmarkFileKey.Do(func() {
|
|
|
|
sha256.SelectAlgo()
|
|
|
|
rand.Read(benchmarkFileKey.key[:])
|
|
|
|
})
|
|
|
|
|
|
|
|
b.ResetTimer()
|
|
|
|
b.ReportAllocs()
|
|
|
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
FileKey("a_kind_of_long_filename.ext", &benchmarkFileKey.key)
|
|
|
|
}
|
|
|
|
}
|