Repository: borgbackup/borg

Issue:
scp-style repo URL parsing broken for ip v6 addrs
Reported by @jeanrjc:

Hello, sorry to comment on this issue, but maybe I did not understand something.

The following does not work with borg 1.1.9:

```bash
$ borg check --archives-only backupNC@[2a02:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:/media/pi/myCloudDrive/borgBKP --debug
using builtin fallback logging configuration
35 self tests completed in 1.18 seconds
SSH command line: ['ssh', 'backupNC@2a02', 'borg', 'serve', '--umask=077', '--debug']
Remote: ssh: Could not resolve hostname 2a02: Name or service not known
Connection closed by remote host. Is borg working on the server?
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 4455, in main
    exit_code = archiver.run(args)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 4387, in run
    return set_ec(func(args))
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 134, in wrapper
    make_parent_dirs=make_parent_dirs, args=args)
  File "/usr/lib/python3/dist-packages/borg/remote.py", line 577, in __init__
    raise ConnectionClosedWithHint('Is borg working on the server?') from None
borg.remote.ConnectionClosedWithHint: Connection closed by remote host. Is borg working on the server?

Platform: Linux nextcloudpi 5.10.103-v7l+ #1529 SMP Tue Mar 8 12:24:00 GMT 2022 armv7l
Linux: debian 10.12 
Borg: 1.1.9  Python: CPython 3.7.3
PID: 4155  CWD: /home/pi
sys.argv: ['/usr/bin/borg', 'check', '--archives-only', 'backupNC@[2a02:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:/media/pi/myCloudDrive/borgBKP', '--debug']
SSH_ORIGINAL_COMMAND: None
```

The "SSH command line" is obviously wrong, what did I miss ? 

Thanks


Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/borgbackup/borg/pull/6527?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) Report
> Merging [#6527](https://codecov.io/gh/borgbackup/borg/pull/6527?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) (58b22e8) into [1.1-maint](https://codecov.io/gh/borgbackup/borg/commit/ae24d527bac40e12d1dfa46d528252db30395f66?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) (ae24d52) will **decrease** coverage by `0.76%`.
> The diff coverage is `100.00%`.

```diff
@@              Coverage Diff              @@
##           1.1-maint    #6527      +/-   ##
=============================================
- Coverage      79.78%   79.01%   -0.77%     
=============================================
  Files             27       27              
  Lines          10511    10499      -12     
  Branches        2151     2029     -122     
=============================================
- Hits            8386     8296      -90     
- Misses          1601     1667      +66     
- Partials         524      536      +12     
```


| [Impacted Files](https://codecov.io/gh/borgbackup/borg/pull/6527?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) | Coverage Δ | |
|---|---|---|
| [src/borg/helpers.py](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvaGVscGVycy5weQ==) | `86.99% <100.00%> (-0.38%)` | :arrow_down: |
| [src/borg/platform/base.py](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvcGxhdGZvcm0vYmFzZS5weQ==) | `79.16% <100.00%> (-2.89%)` | :arrow_down: |
| [src/borg/xattr.py](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcveGF0dHIucHk=) | `60.97% <0.00%> (-20.19%)` | :arrow_down: |
| [src/borg/platform/\_\_init\_\_.py](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvcGxhdGZvcm0vX19pbml0X18ucHk=) | `73.68% <0.00%> (-16.32%)` | :arrow_down: |
| [src/borg/nanorst.py](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvbmFub3JzdC5weQ==) | `86.07% <0.00%> (-2.68%)` | :arrow_down: |
| [src/borg/archive.py](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvYXJjaGl2ZS5weQ==) | `80.20% <0.00%> (-1.48%)` | :arrow_down: |
| [src/borg/upgrader.py](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvdXBncmFkZXIucHk=) | `62.06% <0.00%> (-1.15%)` | :arrow_down: |
| [src/borg/repository.py](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvcmVwb3NpdG9yeS5weQ==) | `85.31% <0.00%> (-0.24%)` | :arrow_down: |
| ... and [7 more](https://codecov.io/gh/borgbackup/borg/pull/6527/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/borgbackup/borg/pull/6527?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/borgbackup/borg/pull/6527?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup). Last update [ffbe366...58b22e8](https://codecov.io/gh/borgbackup/borg/pull/6527?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup).


Patch:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
index 83269ff0..e5f8f515 100644
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -1385,37 +1385,19 @@ def _delete_repository(self, args, repository):
 
         if not args.cache_only:
             if args.forced == 0:  # without --force, we let the user see the archives list and confirm.
-                id = bin_to_hex(repository.id)
-                location = repository._location.canonical_path()
                 msg = []
                 try:
                     manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)
-                    n_archives = len(manifest.archives)
-                    msg.append("You requested to completely DELETE the following repository "
-                               "*including* {} archives it contains:".format(n_archives))
                 except NoManifestError:
-                    n_archives = None
-                    msg.append("You requested to completely DELETE the following repository "
-                               "*including* all archives it may contain:")
-
-                msg.append(DASHES)
-                msg.append("Repository ID: {}".format(id))
-                msg.append("Location: {}".format(location))
-
-                msg.append("")
-                msg.append("Archives:")
-
-                if n_archives is not None:
-                    if n_archives > 0:
-                        for archive_info in manifest.archives.list(sort_by=['ts']):
-                            msg.append(format_archive(archive_info))
-                    else:
-                        msg.append("This repository seems to not have any archives.")
+                    msg.append("You requested to completely DELETE the repository *including* all archives it may "
+                               "contain.")
+                    msg.append("This repository seems to have no manifest, so we can't tell anything about its "
+                               "contents.")
                 else:
-                    msg.append("This repository seems to have no manifest, so we can't "
-                               "tell anything about its contents.")
-
-                msg.append(DASHES)
+                    msg.append("You requested to completely DELETE the repository *including* all archives it "
+                               "contains:")
+                    for archive_info in manifest.archives.list(sort_by=['ts']):
+                        msg.append(format_archive(archive_info))
                 msg.append("Type 'YES' if you understand this and want to continue: ")
                 msg = '\n'.join(msg)
                 if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES',),
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
index 81e49d72..1b16810b 100644
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -641,7 +641,7 @@ def decrypt_key_file(self, data, passphrase):
         assert enc_key.algorithm == 'sha256'
         key = passphrase.kdf(enc_key.salt, enc_key.iterations, 32)
         data = AES(is_encrypt=False, key=key).decrypt(enc_key.data)
-        if compare_digest(hmac_sha256(key, data), enc_key.hash):
+        if hmac_sha256(key, data) == enc_key.hash:
             return data
 
     def encrypt_key_file(self, data, passphrase):
diff --git a/src/borg/helpers.py b/src/borg/helpers.py
index 322d4168..acf828c1 100644
--- a/src/borg/helpers.py
+++ b/src/borg/helpers.py
@@ -1100,24 +1100,33 @@ class Location:
             (?P<archive>[^/]+)                              # archive name must not contain "/"
         )?$"""                                              # must match until the end
 
+    # host NAME, or host IP ADDRESS (v4 or v6, v6 must be in square brackets)
+    host_re = r"""
+        (?P<host>(
+            (?!\[)[^:/]+(?<!\])     # hostname or v4 addr, not containing : or / (does not match v6 addr: no brackets!)
+            |
+            \[[0-9a-fA-F:.]+\])     # ipv6 address in brackets
+        )
+    """
+
     # regexes for misc. kinds of supported location specifiers:
     ssh_re = re.compile(r"""
-        (?P<proto>ssh)://                                   # ssh://
-        """ + optional_user_re + r"""                       # user@  (optional)
-        (?P<host>([^:/]+|\[[0-9a-fA-F:.]+\]))(?::(?P<port>\d+))?  # host or host:port or [ipv6] or [ipv6]:port
-        """ + abs_path_re + optional_archive_re, re.VERBOSE)  # path or path::archive
+        (?P<proto>ssh)://                                       # ssh://
+        """ + optional_user_re + host_re + r"""                 # user@  (optional), host name or address
+        (?::(?P<port>\d+))?                                     # :port (optional)
+        """ + abs_path_re + optional_archive_re, re.VERBOSE)    # path or path::archive
 
     file_re = re.compile(r"""
         (?P<proto>file)://                                  # file://
         """ + file_path_re + optional_archive_re, re.VERBOSE)  # servername/path, path or path::archive
 
-    # note: scp_re is also use for local paths
+    # note: scp_re is also used for local paths
     scp_re = re.compile(r"""
         (
-            """ + optional_user_re + r"""                   # user@  (optional)
-            (?P<host>([^:/]+|\[[0-9a-fA-F:.]+\])):          # host: (don't match / or [ipv6] in host to disambiguate from file:)
-        )?                                                  # user@host: part is optional
-        """ + scp_path_re + optional_archive_re, re.VERBOSE)  # path with optional archive
+            """ + optional_user_re + host_re + r"""             # user@  (optional), host name or address
+            :                                                   # : (required!)
+        )?                                                      # user@host: part is optional
+        """ + scp_path_re + optional_archive_re, re.VERBOSE)    # path with optional archive
 
     # get the repo from BORG_REPO env and the optional archive from param.
     # if the syntax requires giving REPOSITORY (see "borg mount"),


Test patch:
diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py
index fa4af1de..e1dca810 100644
--- a/src/borg/testsuite/helpers.py
+++ b/src/borg/testsuite/helpers.py
@@ -95,6 +95,10 @@ def test_ssh(self, monkeypatch, keys_dir):
         assert repr(Location('ssh://user@[2001:db8::192.0.2.1]/some/path')) == \
             "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)"
         assert Location('ssh://user@[2001:db8::192.0.2.1]/some/path').to_key_filename() == keys_dir + '2001_db8__192_0_2_1__some_path'
+        assert repr(Location('ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]/some/path')) == \
+            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path', archive=None)"
+        assert repr(Location('ssh://user@[2a02:0001:0002:0003:0004:0005:0006:0007]:1234/some/path')) == \
+            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path', archive=None)"
 
     def test_file(self, monkeypatch, keys_dir):
         monkeypatch.delenv('BORG_REPO', raising=False)
@@ -127,6 +131,8 @@ def test_scp(self, monkeypatch, keys_dir):
         assert repr(Location('user@[2001:db8::192.0.2.1]:/some/path')) == \
             "Location(proto='ssh', user='user', host='2001:db8::192.0.2.1', port=None, path='/some/path', archive=None)"
         assert Location('user@[2001:db8::192.0.2.1]:/some/path').to_key_filename() == keys_dir + '2001_db8__192_0_2_1__some_path'
+        assert repr(Location('user@[2a02:0001:0002:0003:0004:0005:0006:0007]:/some/path')) == \
+            "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=None, path='/some/path', archive=None)"
 
     def test_smb(self, monkeypatch, keys_dir):
         monkeypatch.delenv('BORG_REPO', raising=False)