diff --git a/gui/default/assets/css/overrides.css b/gui/default/assets/css/overrides.css index 8b0e9fb45..865aa4168 100644 --- a/gui/default/assets/css/overrides.css +++ b/gui/default/assets/css/overrides.css @@ -549,6 +549,18 @@ ul.three-columns li, ul.two-columns li { opacity: 1; } +.checkbox[disabled] { + background-color: #eeeeee; + opacity: 1; + margin-left: -5px; + padding-left: 5px; +} + +.checkbox[disabled] *, .checkbox[disabled] .help-block { + color: #999999; + cursor: not-allowed; +} + /* Make a "well" look more like a readonly text input when grouped with a button */ .input-group .well-sm { padding-top: 6px; diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index 20c3ec384..a681eb8bc 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -1709,6 +1709,13 @@ angular.module('syncthing.core') return $scope.currentDevice._editing == 'new'; } + $scope.editDeviceUntrustedChanged = function () { + if (currentDevice.untrusted) { + currentDevice.introducer = false; + currentDevice.autoAcceptFolders = false; + } + } + $scope.editDeviceExisting = function (deviceCfg) { $scope.currentDevice = $.extend({}, deviceCfg); $scope.currentDevice._editing = "existing"; diff --git a/gui/default/syncthing/device/editDeviceModalView.html b/gui/default/syncthing/device/editDeviceModalView.html index e74e1692c..972d37684 100644 --- a/gui/default/syncthing/device/editDeviceModalView.html +++ b/gui/default/syncthing/device/editDeviceModalView.html @@ -62,9 +62,9 @@
-
+
@@ -73,9 +73,9 @@
-
+
@@ -164,7 +164,7 @@
- +

All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.

diff --git a/lib/config/config.go b/lib/config/config.go index 21020c407..0534d5f91 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -284,7 +284,7 @@ func (cfg *Configuration) ensureMyDevice(myID protocol.DeviceID) { }) } -func (cfg *Configuration) prepareFoldersAndDevices(myID protocol.DeviceID) (map[protocol.DeviceID]bool, error) { +func (cfg *Configuration) prepareFoldersAndDevices(myID protocol.DeviceID) (map[protocol.DeviceID]*DeviceConfiguration, error) { existingDevices := cfg.prepareDeviceList() sharedFolders, err := cfg.prepareFolders(myID, existingDevices) @@ -297,7 +297,7 @@ func (cfg *Configuration) prepareFoldersAndDevices(myID protocol.DeviceID) (map[ return existingDevices, nil } -func (cfg *Configuration) prepareDeviceList() map[protocol.DeviceID]bool { +func (cfg *Configuration) prepareDeviceList() map[protocol.DeviceID]*DeviceConfiguration { // Ensure that the device list is // - free from duplicates // - no devices with empty ID @@ -309,14 +309,14 @@ func (cfg *Configuration) prepareDeviceList() map[protocol.DeviceID]bool { }) // Build a list of available devices - existingDevices := make(map[protocol.DeviceID]bool, len(cfg.Devices)) - for _, device := range cfg.Devices { - existingDevices[device.DeviceID] = true + existingDevices := make(map[protocol.DeviceID]*DeviceConfiguration, len(cfg.Devices)) + for i, device := range cfg.Devices { + existingDevices[device.DeviceID] = &cfg.Devices[i] } return existingDevices } -func (cfg *Configuration) prepareFolders(myID protocol.DeviceID, existingDevices map[protocol.DeviceID]bool) (map[protocol.DeviceID][]string, error) { +func (cfg *Configuration) prepareFolders(myID protocol.DeviceID, existingDevices map[protocol.DeviceID]*DeviceConfiguration) (map[protocol.DeviceID][]string, error) { // Prepare folders and check for duplicates. Duplicates are bad and // dangerous, can't currently be resolved in the GUI, and shouldn't // happen when configured by the GUI. We return with an error in that @@ -359,13 +359,13 @@ func (cfg *Configuration) prepareDevices(sharedFolders map[protocol.DeviceID][]s } } -func (cfg *Configuration) prepareIgnoredDevices(existingDevices map[protocol.DeviceID]bool) map[protocol.DeviceID]bool { +func (cfg *Configuration) prepareIgnoredDevices(existingDevices map[protocol.DeviceID]*DeviceConfiguration) map[protocol.DeviceID]bool { // The list of ignored devices should not contain any devices that have // been manually added to the config. newIgnoredDevices := cfg.IgnoredDevices[:0] ignoredDevices := make(map[protocol.DeviceID]bool, len(cfg.IgnoredDevices)) for _, dev := range cfg.IgnoredDevices { - if !existingDevices[dev.ID] { + if _, ok := existingDevices[dev.ID]; !ok { ignoredDevices[dev.ID] = true newIgnoredDevices = append(newIgnoredDevices, dev) } @@ -502,7 +502,7 @@ func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.Devi return devices } -func ensureExistingDevices(devices []FolderDeviceConfiguration, existingDevices map[protocol.DeviceID]bool) []FolderDeviceConfiguration { +func ensureExistingDevices(devices []FolderDeviceConfiguration, existingDevices map[protocol.DeviceID]*DeviceConfiguration) []FolderDeviceConfiguration { count := len(devices) i := 0 loop: @@ -553,6 +553,23 @@ loop: return devices[0:count] } +func ensureNoUntrustedTrustingSharing(f *FolderConfiguration, devices []FolderDeviceConfiguration, existingDevices map[protocol.DeviceID]*DeviceConfiguration) []FolderDeviceConfiguration { + for i := 0; i < len(devices); i++ { + dev := devices[i] + if dev.EncryptionPassword != "" { + // There's a password set, no check required + continue + } + if devCfg := existingDevices[dev.DeviceID]; devCfg.Untrusted { + l.Warnf("Folder %s (%s) is shared in trusted mode with untrusted device %s (%s); unsharing.", f.ID, f.Label, dev.DeviceID.Short(), devCfg.Name) + copy(devices[i:], devices[i+1:]) + devices = devices[:len(devices)-1] + i-- + } + } + return devices +} + func cleanSymlinks(filesystem fs.Filesystem, dir string) { if build.IsWindows { // We don't do symlinks on Windows. Additionally, there may @@ -611,7 +628,7 @@ func getFreePort(host string, ports ...int) (int, error) { return addr.Port, nil } -func (defaults *Defaults) prepare(myID protocol.DeviceID, existingDevices map[protocol.DeviceID]bool) { +func (defaults *Defaults) prepare(myID protocol.DeviceID, existingDevices map[protocol.DeviceID]*DeviceConfiguration) { ensureZeroForNodefault(&FolderConfiguration{}, &defaults.Folder) ensureZeroForNodefault(&DeviceConfiguration{}, &defaults.Device) defaults.Folder.prepare(myID, existingDevices) diff --git a/lib/config/config_test.go b/lib/config/config_test.go index b70aab91c..e0d6ef22d 100644 --- a/lib/config/config_test.go +++ b/lib/config/config_test.go @@ -1489,6 +1489,65 @@ func TestXattrFilter(t *testing.T) { } } +func TestUntrustedIntroducer(t *testing.T) { + fd, err := os.Open("testdata/untrustedintroducer.xml") + if err != nil { + t.Fatal(err) + } + cfg, _, err := ReadXML(fd, device1) + if err != nil { + t.Fatal(err) + } + + if len(cfg.Devices) != 2 { + // ourselves and the remote in the config + t.Fatal("Expected two devices") + } + + // Check that the introducer and auto-accept flags were negated + var foundUntrusted protocol.DeviceID + for _, d := range cfg.Devices { + if d.Name == "untrusted" { + foundUntrusted = d.DeviceID + if !d.Untrusted { + t.Error("untrusted device should be untrusted") + } + if d.Introducer { + t.Error("untrusted device should not be an introducer") + } + if d.AutoAcceptFolders { + t.Error("untrusted device should not auto-accept folders") + } + } + } + if foundUntrusted.Equals(protocol.EmptyDeviceID) { + t.Error("untrusted device not found") + } + + // Folder A has the device added without a password, which is not permitted + folderA := cfg.FolderMap()["a"] + for _, dev := range folderA.Devices { + if dev.DeviceID == foundUntrusted { + t.Error("untrusted device should not be in folder A") + } + } + + // Folder B has the device added with a password, this is just a sanity check + folderB := cfg.FolderMap()["b"] + found := false + for _, dev := range folderB.Devices { + if dev.DeviceID == foundUntrusted { + found = true + if dev.EncryptionPassword == "" { + t.Error("untrusted device should have a password in folder B") + } + } + } + if !found { + t.Error("untrusted device not found in folder B") + } +} + // Verify that opening a config with myID == protocol.EmptyDeviceID doesn't add that ID to the config. // Done in various places where config is needed, but the device ID isn't known. func TestLoadEmptyDeviceID(t *testing.T) { diff --git a/lib/config/deviceconfiguration.go b/lib/config/deviceconfiguration.go index d2751002e..54a30affc 100644 --- a/lib/config/deviceconfiguration.go +++ b/lib/config/deviceconfiguration.go @@ -34,6 +34,19 @@ func (cfg *DeviceConfiguration) prepare(sharedFolders []string) { } cfg.IgnoredFolders = sortedObservedFolderSlice(ignoredFolders) + + // A device cannot be simultaneously untrusted and an introducer, nor + // auto accept folders. + if cfg.Untrusted { + if cfg.Introducer { + l.Warnf("Device %s (%s) is both untrusted and an introducer, removing introducer flag", cfg.DeviceID.Short(), cfg.Name) + cfg.Introducer = false + } + if cfg.AutoAcceptFolders { + l.Warnf("Device %s (%s) is both untrusted and auto-accepting folders, removing auto-accept flag", cfg.DeviceID.Short(), cfg.Name) + cfg.AutoAcceptFolders = false + } + } } func (cfg *DeviceConfiguration) IgnoredFolder(folder string) bool { diff --git a/lib/config/folderconfiguration.go b/lib/config/folderconfiguration.go index a65df1177..d6e076727 100644 --- a/lib/config/folderconfiguration.go +++ b/lib/config/folderconfiguration.go @@ -181,14 +181,16 @@ func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID { return deviceIDs } -func (f *FolderConfiguration) prepare(myID protocol.DeviceID, existingDevices map[protocol.DeviceID]bool) { +func (f *FolderConfiguration) prepare(myID protocol.DeviceID, existingDevices map[protocol.DeviceID]*DeviceConfiguration) { // Ensure that // - any loose devices are not present in the wrong places // - there are no duplicate devices // - we are part of the devices + // - folder is not shared in trusted mode with an untrusted device f.Devices = ensureExistingDevices(f.Devices, existingDevices) f.Devices = ensureNoDuplicateFolderDevices(f.Devices) f.Devices = ensureDevicePresent(f.Devices, myID) + f.Devices = ensureNoUntrustedTrustingSharing(f, f.Devices, existingDevices) sort.Slice(f.Devices, func(a, b int) bool { return f.Devices[a].DeviceID.Compare(f.Devices[b].DeviceID) == -1 diff --git a/lib/config/testdata/untrustedintroducer.xml b/lib/config/testdata/untrustedintroducer.xml new file mode 100644 index 000000000..122f9a459 --- /dev/null +++ b/lib/config/testdata/untrustedintroducer.xml @@ -0,0 +1,17 @@ + + + + + + + + + a complex password + + + +
dynamic
+ true + true +
+