Repository: borgbackup/borg

Issue:
When changing passphrase, is the old one properly erased ?
I didn't find the relevant code, but looking at the `key` entry in my repository's `config` file, I assume that it's the encrypted key of the repository.

If I change the repository's passphrase, will the old value in `config` be securely erased, so that the old one cannot be recovered by reading the unallocated sector on the disk ? Will the config file's rewrite always overwrite the old value ?

I find it very important for security to erase the old key, as changing a too weak or compromised passphrase would be useless if the old version of the encrypted key could be recovered.

Thank you.


Discussion:
There are two somewhat conflicting design targets here.

One is: I want to change my passphrase and have the old encrypted key securely (more or less) erased.

The other is: I want to change my passphrase, but I don't want to brick my repository if the USB cable comes loose while doing that.

Borg does the second thing currently.

On some file/storage systems it's not straightforward to erase an existing file (e.g. on a CoW file system you need to be aware of the fact when _creating_ the file, setting the NOCOW flag on the file, *before* writing the (original) contents -- changing it later has _no_ effect, on Linux/btrfs; NOCOW is also platform-dependent), but on most file systems we could implement it like this:

1. Repository.save_config creates a hardlink to the old config file
2. Writes the new file using current approach (SaveFile)
3. Write into the hardlinked file from 1.) with random garbage / zeroes

On simple things, like ext4 on a local hard drive, this will overwrite the original key. In other cases it wouldn't.
What about securely erasing if possible in that case, and showing a warning if it can't be securely erased ? Otherwise users could have an old key with a compromised or weak passphrase sitting somewhere without knowing it... It can possibly defeat borg's encryption.
Well the fact ("can I erase this?" [or more precisely: "if I overwrite this, will the physical data be overwritten?"]) is nothing you can ask the OS (and it couldn't tell you reliably, it doesn't have enough information to do so in many cases), so that would require a fair amount of platform-dependent heuristics to figure out.
While you might be able work with some filesystems to help deletion, there are still a bunch of different possibilities how data might leak. Any flash based disk will happily (and completely in-transparent to the OS) keep copies and logs at places.
You can check sfill from secure-delete (Basically just overwriting all your free space - you could do this with dd). Or go with full disk encryption for another layer of security (things mentioned above apply there too)

Or just choosing a good password. :)
I'm not sure this is within Borg's job description; secure erasure of deleted files is the job of `shred` or even a simple `dd`. I securely erase deleted files on my hard drive every so often anyway by `cat`ing `/dev/urandom` into a temporary file and then deleting it. Even that doesn't defeat the wear leveling of most SSDs; the controller can (and will) swap data into other blocks to avoid hot spots, and one can't control this from the OS.

Resolving patch:
diff --git a/src/borg/helpers.py b/src/borg/helpers.py
--- a/src/borg/helpers.py
+++ b/src/borg/helpers.py
@@ -2256,3 +2256,13 @@ def json_dump(obj):
 
 def json_print(obj):
     print(json_dump(obj))
+
+
+def secure_erase(path):
+    """Attempt to securely erase a file by writing random data over it before deleting it."""
+    with open(path, 'r+b') as fd:
+        length = os.stat(fd.fileno()).st_size
+        fd.write(os.urandom(length))
+        fd.flush()
+        os.fsync(fd.fileno())
+    os.unlink(path)
diff --git a/src/borg/repository.py b/src/borg/repository.py
--- a/src/borg/repository.py
+++ b/src/borg/repository.py
@@ -18,6 +18,7 @@
 from .helpers import ProgressIndicatorPercent
 from .helpers import bin_to_hex
 from .helpers import yes
+from .helpers import secure_erase
 from .locking import Lock, LockError, LockErrorT
 from .logger import create_logger
 from .lrucache import LRUCache
@@ -182,9 +183,27 @@ def create(self, path):
 
     def save_config(self, path, config):
         config_path = os.path.join(path, 'config')
+        old_config_path = os.path.join(path, 'config.old')
+
+        if os.path.isfile(old_config_path):
+            logger.warning("Old config file not securely erased on previous config update")
+            secure_erase(old_config_path)
+
+        if os.path.isfile(config_path):
+            try:
+                os.link(config_path, old_config_path)
+            except OSError as e:
+                if e.errno in (errno.EMLINK, errno.EPERM):
+                    logger.warning("Hardlink failed, cannot securely erase old config file")
+                else:
+                    raise
+
         with SaveFile(config_path) as fd:
             config.write(fd)
 
+        if os.path.isfile(old_config_path):
+            secure_erase(old_config_path)
+
     def save_key(self, keydata):
         assert self.config
         keydata = keydata.decode('utf-8')  # remote repo: msgpack issue #99, getting bytes


Test patch: