Repository: borgbackup/borg

Issue:
Borg key mismatch
## Have you checked borgbackup docs, FAQ, and open Github issues?

Yes

## Is this a BUG / ISSUE report or a QUESTION?

Bug/Issue

## System information. For client/server mode post info for both machines.

#### Your borg version (borg -V).
borg 1.1.17 (Client and server match)

#### Operating system (distribution) and version.
Archlinux/Centos/TrueNAS(BSD-based)

#### Hardware / network configuration, and filesystems used.
Central NAS uploading to Rsync.net nightly. ZFS filesystem on client and server.

#### How much data is handled by borg?
250ish-GB

#### Full borg commandline that lead to the problem (leave away excludes and passwords)
Unknown - backup command is: 

```
export BORG_KEY_FILE=/root/borg.key
export BORG_RSH='ssh -i /root/.ssh/rsync'
/usr/local/bin/borg create --list --stats --compression lzma --exclude-caches \
--filter AME \
--exclude-if-present .borg-ignore \
--remote-path 'borg1' \
-p ssh://USERNAME@HOST.rsync.net/data1/home/USERNAME/borg::'{hostname}-{now}' /mnt/Media
```

## Describe the problem you're observing.
I've been using BORG for about 2-ish years now on the same backup. Recently I migrated to TrueNAS, and was able to continue my backups. However, last night while experimenting, I did... something? And now I'm getting a key mismatch on my main backup. Things that I did last night:

1) Regular backup
2) Borg mount
2.1) Successfully mounted and restored a backed-up file
Note: Below here, I was testing how BORG creates new files for the purposes of determining the cost of a nightly sync of a 2TB repo to AWS Deep-Glacier
3) borg init --encryption=keyfile /mnt/Temp/backup
3.1) After this, my repokey in /root/borg.key DID change, at exactly the same time that I did the borg init. This was unexpected, I would have thought that this would create a new repository with the same key.
4) borg create --list --stats /mnt/Temp/backup::first /mnt/Media/Camera (12GB folder)
4.1) Backup created successfully
5) cp /mnt/Temp/backup /mnt/Temp/backup0
6) cp /mnt/Media/Secrets /mnt/Media/Camera (1GB folder to simulate added, non-duplicated data)
7) borg create --list --stats /mnt/Temp/backup::second /mnt/Media/Camera 
8) diff /mnt/Temp/backup /mnt/Temp/backup0
9) borg create --list --stats /mnt/Temp/backup::third /mnt/Media/Camera 
10) diff /mnt/Temp/backup /mnt/Temp/backup0

At this point, my nightly backup ran with this error: 
```
Mismatch between repository ssh://USERNAME@HOST.rsync.net/data1/home/USERNAME/borg and key file /root/borg.key.
```

I verified that the key I had at /root/borg.key did indeed NOT match the key I had in an immutable archive (Signal self-text message) or on any of my other machines or cold storage (I keep about 6 backups of this file for obvious reasons). I restored the known, good key and was still getting the same error message.

I also tried to access the repository from a blank machine that had never connected to this repository, with the same error. 

I then ran borg check against the repository (with debug flag, without -repair) and it reported no errors until it tried to decrypt using the borg key.

I also checked that all keys are the same, and they are. One had an extra newline at the end of file, removing that didn't resolve the issue.

BORG key format: 
```
BORG_KEY aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
```

#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.

I haven't tried as I don't have another repository to check.

#### Include any warning/errors/backtraces from the system logs
From clean machine (Fresh install of BORG prior to running borg list)
```
borg list --remote-path borg1 --debug ssh://USERNAME@HOST.rsync.net/data1/home/USERNAME/borg                                               
using builtin fallback logging configuration
35 self tests completed in 0.05 seconds
SSH command line: ['ssh', '-i', '~/.ssh/rsync', 'USERNAME@HOST.rsync.net', 'borg1', 'serve', '--umask=077', '--debug']
Remote: using builtin fallback logging configuration
Remote: borg selftest disabled via BORG_SELFTEST env variable
Remote: using builtin fallback logging configuration
Remote: Initialized logging system for JSON-based protocol
Remote: Resolving repository path b'/data1/home/USERNAME/borg'
Remote: Resolved repository path to '/data1/home/USERNAME/borg'
Remote: Verified integrity of /data1/home/USERNAME/borg/index.2827
RemoteRepository: 213 B bytes sent, 25.70 kB bytes received, 4 messages sent
Mismatch between repository ssh://USERNAME@HOST.rsync.net/data1/home/USERNAME/borg and key file /home/USERNAME/backup/borg.key.
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/borg/archiver.py", line 4703, in main
    exit_code = archiver.run(args)
  File "/usr/lib/python3.9/site-packages/borg/archiver.py", line 4635, in run
    return set_ec(func(args))
  File "/usr/lib/python3.9/site-packages/borg/archiver.py", line 164, in wrapper
    kwargs['manifest'], kwargs['key'] = Manifest.load(repository, compatibility)
  File "/usr/lib/python3.9/site-packages/borg/helpers.py", line 382, in load
    key = key_factory(repository, cdata)
  File "/usr/lib/python3.9/site-packages/borg/crypto/key.py", line 134, in key_factory
    return identify_key(manifest_data).detect(repository, manifest_data)
  File "/usr/lib/python3.9/site-packages/borg/crypto/key.py", line 592, in detect
    target = key.find_key()
  File "/usr/lib/python3.9/site-packages/borg/crypto/key.py", line 726, in find_key
    return self.sanity_check(os.path.abspath(keyfile), id)
  File "/usr/lib/python3.9/site-packages/borg/crypto/key.py", line 719, in sanity_check
    raise KeyfileMismatchError(self.repository._location.canonical_path(), filename)
borg.crypto.key.KeyfileMismatchError: Mismatch between repository ssh://USERNAME@HOST.rsync.net/data1/home/USERNAME/borg and key file /home/USERNAME/backup/borg.key.

Platform: Linux localhost 5.14.9-arch2-1 #1 SMP PREEMPT Fri, 01 Oct 2021 19:03:20 +0000 x86_64
Linux: Unknown Linux  
Borg: 1.1.17  Python: CPython 3.9.7 msgpack: 0.5.6.+borg1
PID: 1912669  CWD: /home/USERNAME
sys.argv: ['/usr/bin/borg', 'list', '--remote-path', 'borg1', '--debug', 'ssh://USERNAME@HOST.rsync.net/data1/home/USERNAME/borg']
SSH_ORIGINAL_COMMAND: None
```


Discussion:
keyfile mode and repokey mode are 2 different things:
- keyfile stores the key somewhere on the client, usually in the user's home as long as you do not tell borg otherwise (as you do)
- repokey stores the key inside the repo (on the repo server), to be precise: in `<repodir>/config`

Yeah sorry, I had to type that from memory. I used keyfile encryption for the new repo as well. I'm still not able to access my old backups, even with the correct key though, and I have no clue why. I did confirm that borg init will overwrite existing keyfiles though, which seems very dangerous.
Is there any way to recover the old repo though? I do still have the key used to create it, but Borg is reporting a mismatch, and I have no clue why.
Ok so I dug through the code to identify where borg does the key check. If I understand it correctly, the key inside of the config.json file (Which is already in hex) should match the hex-formatted bytes in the first X characters of the key. Those do mismatch, and neither have been updated in a year. If that's the case, somehow I backed up multiple borg keys. Is my reading of the code correct? In short by doing:

xxd borg.key

cat config.json

I should be able to find the repo ID in the borg key output from XXD?
Ok I was able to confirm that the borg key was incorrect, I had been backing up the wrong key, so that was my bad. However, it still seems like a bug that you're able to overwrite your keyfile by creating a new repository. 
If you override the key path/filename, borg just does what you say and uses that.

If you don't override, I guess borg never overwrites any keys.

Resolving patch:
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -687,12 +687,12 @@ def create(cls, repository, args):
         key.init_from_random_data()
         key.init_ciphers()
         target = key.get_new_target(args)
-        key.save(target, passphrase)
+        key.save(target, passphrase, create=True)
         logger.info('Key in "%s" created.' % target)
         logger.info('Keep this key safe. Your data will be inaccessible without it.')
         return key
 
-    def save(self, target, passphrase):
+    def save(self, target, passphrase, create=False):
         raise NotImplementedError
 
     def get_new_target(self, args):
@@ -753,7 +753,12 @@ def load(self, target, passphrase):
             self.target = target
         return success
 
-    def save(self, target, passphrase):
+    def save(self, target, passphrase, create=False):
+        if create and os.path.isfile(target):
+            # if a new keyfile key repository is created, ensure that an existing keyfile of another
+            # keyfile key repo is not accidentally overwritten by careless use of the BORG_KEY_FILE env var.
+            # see issue #6036
+            raise Error('Aborting because key in "%s" already exists.' % target)
         key_data = self._save(passphrase)
         with SaveFile(target) as fd:
             fd.write('%s %s\n' % (self.FILE_ID, bin_to_hex(self.repository_id)))
@@ -793,7 +798,7 @@ def load(self, target, passphrase):
             self.target = target
         return success
 
-    def save(self, target, passphrase):
+    def save(self, target, passphrase, create=False):
         self.logically_encrypted = passphrase != ''
         key_data = self._save(passphrase)
         key_data = key_data.encode('utf-8')  # remote repo: msgpack issue #99, giving bytes
@@ -831,8 +836,8 @@ def load(self, target, passphrase):
         self.logically_encrypted = False
         return success
 
-    def save(self, target, passphrase):
-        super().save(target, passphrase)
+    def save(self, target, passphrase, create=False):
+        super().save(target, passphrase, create=create)
         self.logically_encrypted = False
 
     def extract_nonce(self, payload):
diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py
--- a/src/borg/testsuite/archiver.py
+++ b/src/borg/testsuite/archiver.py
@@ -32,6 +32,7 @@
     pass
 
 import borg
+import borg.helpers
 from .. import xattr, helpers, platform
 from ..archive import Archive, ChunkBuffer, flags_noatime, flags_normal
 from ..archiver import Archiver, parse_storage_quota, PURE_PYTHON_MSGPACK_WARNING
@@ -2572,6 +2573,25 @@ def test_init_nested_repositories(self):
             with pytest.raises(Repository.AlreadyExists):
                 self.cmd('init', '--encryption=repokey', self.repository_location + '/nested')
 
+    def test_init_refuse_to_overwrite_keyfile(self):
+        """BORG_KEY_FILE=something borg init should quit if "something" already exists.
+
+        See https://github.com/borgbackup/borg/pull/6046"""
+        keyfile = os.path.join(self.tmpdir, 'keyfile')
+        with environment_variable(BORG_KEY_FILE=keyfile):
+            self.cmd('init', '--encryption=keyfile', self.repository_location + '0')
+            with open(keyfile) as file:
+                before = file.read()
+            arg = ('init', '--encryption=keyfile', self.repository_location + '1')
+            if self.FORK_DEFAULT:
+                self.cmd(*arg, exit_code=2)
+            else:
+                with pytest.raises(borg.helpers.Error):
+                    self.cmd(*arg)
+            with open(keyfile) as file:
+                after = file.read()
+            assert before == after
+
     def check_cache(self):
         # First run a regular borg check
         self.cmd('check', self.repository_location)


Test patch: