Repository: postlund/pyatv
Pull request #2414 (2024-06-24 04:54:02)

build(deps): Bump cryptography from 42.0.5 to 42.0.8 in /requirements
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.5 to 42.0.8.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst">cryptography's changelog</a>.</em></p>
<blockquote>
<p>42.0.8 - 2024-06-04</p>
<pre><code>
* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL 3.2.2.
<p>.. _v42-0-7:</p>
<p>42.0.7 - 2024-05-06
</code></pre></p>
<ul>
<li>Restored Windows 7 compatibility for our pre-built wheels. Note that we do
not test on Windows 7 and wheels for our next release will not support it.
Microsoft no longer provides support for Windows 7 and users are encouraged
to upgrade.</li>
</ul>
<p>.. _v42-0-6:</p>
<p>42.0.6 - 2024-05-04</p>
<pre><code>
* Fixed compilation when using LibreSSL 3.9.1.
<p>.. _v42-0-5:
</code></pre></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/pyca/cryptography/commit/761ef4b955697c63e83734096f672224224082e1"><code>761ef4b</code></a> bump for 42.0.8 release (<a href="https://redirect.github.com/pyca/cryptography/issues/11072">#11072</a>)</li>
<li><a href="https://github.com/pyca/cryptography/commit/0cc7fc384381ea09eea5f031d31b1417f9f40fdf"><code>0cc7fc3</code></a> Prepare for 42.0.7 release (<a href="https://redirect.github.com/pyca/cryptography/issues/10949">#10949</a>)</li>
<li><a href="https://github.com/pyca/cryptography/commit/cfad00478a0cc035ad722e68411bac7b6602e195"><code>cfad004</code></a> Prepare backports for 42.0.6 release (<a href="https://redirect.github.com/pyca/cryptography/issues/10929">#10929</a>)</li>
<li>See full diff in <a href="https://github.com/pyca/cryptography/compare/42.0.5...42.0.8">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=pip&previous-version=42.0.5&new-version=42.0.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/pyatv/auth/hap_srp.py b/pyatv/auth/hap_srp.py
index 0a7e613e..3453bfd1 100644
--- a/pyatv/auth/hap_srp.py
+++ b/pyatv/auth/hap_srp.py
@@ -91,7 +91,7 @@ class SRPAuthHandler:
             "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", self._shared
         )
 
-        chacha = chacha20.Chacha20Cipher(session_key, session_key)
+        chacha = chacha20.Chacha20Cipher8byteNonce(session_key, session_key)
         decrypted_tlv = read_tlv(chacha.decrypt(encrypted, nonce="PV-Msg02".encode()))
 
         identifier = decrypted_tlv[TlvValue.Identifier]
@@ -199,14 +199,14 @@ class SRPAuthHandler:
         if additional_data:
             tlv.update(additional_data)
 
-        chacha = chacha20.Chacha20Cipher(self._session_key, self._session_key)
+        chacha = chacha20.Chacha20Cipher8byteNonce(self._session_key, self._session_key)
         encrypted_data = chacha.encrypt(write_tlv(tlv), nonce="PS-Msg05".encode())
         log_binary(_LOGGER, "Data", Encrypted=encrypted_data)
         return encrypted_data
 
     def step4(self, encrypted_data):
         """Last pairing step."""
-        chacha = chacha20.Chacha20Cipher(self._session_key, self._session_key)
+        chacha = chacha20.Chacha20Cipher8byteNonce(self._session_key, self._session_key)
         decrypted_tlv_bytes = chacha.decrypt(encrypted_data, nonce="PS-Msg06".encode())
 
         if not decrypted_tlv_bytes:
diff --git a/pyatv/protocols/airplay/server_auth.py b/pyatv/protocols/airplay/server_auth.py
index e5839cee..9351273f 100644
--- a/pyatv/protocols/airplay/server_auth.py
+++ b/pyatv/protocols/airplay/server_auth.py
@@ -284,7 +284,7 @@ class BaseAirPlayServerAuth(http.HttpSimpleRouter, ABC):
             {TlvValue.Identifier: self.unique_id, TlvValue.Signature: signature}
         )
 
-        chacha = chacha20.Chacha20Cipher(session_key, session_key)
+        chacha = chacha20.Chacha20Cipher8byteNonce(session_key, session_key)
         encrypted = chacha.encrypt(tlv, nonce="PV-Msg02".encode())
 
         tlv = {
@@ -368,7 +368,7 @@ class BaseAirPlayServerAuth(http.HttpSimpleRouter, ABC):
             binascii.unhexlify(self.session.key),
         )
 
-        chacha = chacha20.Chacha20Cipher(session_key, session_key)
+        chacha = chacha20.Chacha20Cipher8byteNonce(session_key, session_key)
         decrypted_tlv_bytes = chacha.decrypt(
             pairing_data[TlvValue.EncryptedData], nonce="PS-Msg05".encode()
         )
@@ -396,7 +396,7 @@ class BaseAirPlayServerAuth(http.HttpSimpleRouter, ABC):
             }
         )
 
-        chacha = chacha20.Chacha20Cipher(session_key, session_key)
+        chacha = chacha20.Chacha20Cipher8byteNonce(session_key, session_key)
         encrypted = chacha.encrypt(tlv, nonce="PS-Msg06".encode())
 
         self.has_paired()
diff --git a/pyatv/protocols/mrp/connection.py b/pyatv/protocols/mrp/connection.py
index 0ba549ce..249c35f2 100644
--- a/pyatv/protocols/mrp/connection.py
+++ b/pyatv/protocols/mrp/connection.py
@@ -92,7 +92,7 @@ class MrpConnection(
 
     def enable_encryption(self, output_key: bytes, input_key: bytes) -> None:
         """Enable encryption with the specified keys."""
-        self._chacha = chacha20.Chacha20Cipher(output_key, input_key)
+        self._chacha = chacha20.Chacha20Cipher8byteNonce(output_key, input_key)
 
     @property
     def connected(self) -> bool:
diff --git a/pyatv/protocols/mrp/server_auth.py b/pyatv/protocols/mrp/server_auth.py
index c096b127..c43568bf 100644
--- a/pyatv/protocols/mrp/server_auth.py
+++ b/pyatv/protocols/mrp/server_auth.py
@@ -147,7 +147,7 @@ class MrpServerAuth(ABC):
             {TlvValue.Identifier: self.unique_id, TlvValue.Signature: signature}
         )
 
-        chacha = chacha20.Chacha20Cipher(session_key, session_key)
+        chacha = chacha20.Chacha20Cipher8byteNonce(session_key, session_key)
         encrypted = chacha.encrypt(tlv, nonce="PV-Msg02".encode())
 
         msg = messages.crypto_pairing(
@@ -227,7 +227,7 @@ class MrpServerAuth(ABC):
             }
         )
 
-        chacha = chacha20.Chacha20Cipher(session_key, session_key)
+        chacha = chacha20.Chacha20Cipher8byteNonce(session_key, session_key)
         encrypted = chacha.encrypt(tlv, nonce="PS-Msg06".encode())
 
         msg = messages.crypto_pairing(
diff --git a/pyatv/support/chacha20.py b/pyatv/support/chacha20.py
index 768da7a4..295faa4e 100644
--- a/pyatv/support/chacha20.py
+++ b/pyatv/support/chacha20.py
@@ -8,15 +8,11 @@ from chacha20poly1305_reuseable import ChaCha20Poly1305Reusable as ChaCha20Poly1
 
 NONCE_LENGTH = 12
 
-# The first 4 bytes are always 0, followed by 8 bytes of counter
-# for a total of 12 bytes.
-PACK_NONCE = partial(Struct("<LQ").pack, 0)
-
 
 class Chacha20Cipher:
     """CHACHA20 encryption/decryption layer."""
 
-    def __init__(self, out_key, in_key, nonce_length=8):
+    def __init__(self, out_key: bytes, in_key: bytes, nonce_length: int = 8) -> None:
         """Initialize a new Chacha20Cipher."""
         self._enc_out = ChaCha20Poly1305(out_key)
         self._enc_in = ChaCha20Poly1305(in_key)
@@ -31,7 +27,11 @@ class Chacha20Cipher:
         This is the nonce that will be used by encrypt in the _next_ call if no custom
         nonce is specified.
         """
-        return PACK_NONCE(self._out_counter)
+        nonce_length = self._nonce_length
+        nonce = self._out_counter.to_bytes(length=nonce_length, byteorder="little")
+        if nonce_length != NONCE_LENGTH:
+            return self._pad_nonce(nonce)
+        return nonce
 
     @property
     def in_nonce(self) -> bytes:
@@ -40,7 +40,15 @@ class Chacha20Cipher:
         This is the nonce that will be used by decrypt in the _next_ call if no custom
         nonce is specified.
         """
-        return PACK_NONCE(self._in_counter)
+        nonce_length = self._nonce_length
+        nonce = self._in_counter.to_bytes(length=nonce_length, byteorder="little")
+        if nonce_length != NONCE_LENGTH:
+            return self._pad_nonce(nonce)
+        return nonce
+
+    def _pad_nonce(self, nonce: bytes) -> bytes:
+        """Pad nonce to 12 bytes."""
+        return b"\x00" * (NONCE_LENGTH - len(nonce)) + nonce
 
     def encrypt(
         self, data: bytes, nonce: Optional[bytes] = None, aad: Optional[bytes] = None
@@ -50,7 +58,7 @@ class Chacha20Cipher:
             nonce = self.out_nonce
             self._out_counter += 1
         elif len(nonce) < NONCE_LENGTH:
-            nonce = b"\x00" * (NONCE_LENGTH - len(nonce)) + nonce
+            nonce = self._pad_nonce(nonce)
         return self._enc_out.encrypt(nonce, data, aad)
 
     def decrypt(
@@ -61,5 +69,38 @@ class Chacha20Cipher:
             nonce = self.in_nonce
             self._in_counter += 1
         elif len(nonce) < NONCE_LENGTH:
-            nonce = b"\x00" * (NONCE_LENGTH - len(nonce)) + nonce
+            nonce = self._pad_nonce(nonce)
         return self._enc_in.decrypt(nonce, data, aad)
+
+
+_PACK_NONCE_WITH_4_BYTE_PAD = partial(Struct("<LQ").pack, 0)
+
+
+class Chacha20Cipher8byteNonce(Chacha20Cipher):
+    """CHACHA20 encryption/decryption layer with an 8 byte counter.
+
+    The first 4 bytes are always 0, followed by 8 bytes of counter
+    for a total of 12 bytes.
+    """
+
+    def __init__(self, out_key: bytes, in_key: bytes) -> None:
+        """Initialize a new Chacha20Cipher8byteNonce."""
+        super().__init__(out_key, in_key, nonce_length=8)
+
+    @property
+    def out_nonce(self) -> bytes:
+        """Return next encrypt nonce.
+
+        This is the nonce that will be used by encrypt in the _next_ call if no custom
+        nonce is specified.
+        """
+        return _PACK_NONCE_WITH_4_BYTE_PAD(self._out_counter)
+
+    @property
+    def in_nonce(self) -> bytes:
+        """Return next decrypt nonce.
+
+        This is the nonce that will be used by decrypt in the _next_ call if no custom
+        nonce is specified.
+        """
+        return _PACK_NONCE_WITH_4_BYTE_PAD(self._in_counter)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 1d4ee1f9..3aabe044 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -1,6 +1,6 @@
 aiohttp==3.9.5
 async-timeout==4.0.3
-cryptography==42.0.5
+cryptography==42.0.8
 chacha20poly1305-reuseable==0.12.1
 ifaddr==0.2.0
 ifaddr==0.2.0


Test patch:
diff --git a/tests/fake_device/mrp.py b/tests/fake_device/mrp.py
index d9433ae6..5458a266 100644
--- a/tests/fake_device/mrp.py
+++ b/tests/fake_device/mrp.py
@@ -392,7 +392,7 @@ class FakeMrpService(MrpServerAuth, asyncio.Protocol):
 
     def enable_encryption(self, output_key: bytes, input_key: bytes) -> None:
         """Enable encryption with specified keys."""
-        self.chacha = chacha20.Chacha20Cipher(output_key, input_key)
+        self.chacha = chacha20.Chacha20Cipher8byteNonce(output_key, input_key)
         self.state.has_authenticated = True
 
     def send_to_client(self, message: ProtobufMessage) -> None:
diff --git a/tests/support/test_chacha20.py b/tests/support/test_chacha20.py
new file mode 100644
index 00000000..81fc3421
--- /dev/null
+++ b/tests/support/test_chacha20.py
@@ -0,0 +1,23 @@
+"""Unit tests for pyatv.support.chacha20."""
+
+import logging
+
+from pyatv.support import chacha20
+
+fake_key = b"k" * 32
+
+
+def test_12_bytes_nonce():
+    cipher = chacha20.Chacha20Cipher(fake_key, fake_key, 12)
+    assert len(cipher.out_nonce) == chacha20.NONCE_LENGTH
+    assert len(cipher.in_nonce) == chacha20.NONCE_LENGTH
+    result = cipher.encrypt(b"test")
+    assert cipher.decrypt(result) == b"test"
+
+
+def test_8_bytes_nonce():
+    cipher = chacha20.Chacha20Cipher8byteNonce(fake_key, fake_key)
+    assert len(cipher.out_nonce) == chacha20.NONCE_LENGTH
+    assert len(cipher.in_nonce) == chacha20.NONCE_LENGTH
+    result = cipher.encrypt(b"test")
+    assert cipher.decrypt(result) == b"test"