Skip to content

Commit 5005f90

Browse files
committed
add support for CipherContext.reset_nonce
This only supports ChaCha20 and ciphers in CTR mode.
1 parent 8a7f27b commit 5005f90

File tree

7 files changed

+172
-2
lines changed

7 files changed

+172
-2
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ Changelog
6060
``datetime`` objects.
6161
* Added
6262
:func:`~cryptography.hazmat.primitives.asymmetric.rsa.rsa_recover_private_exponent`
63+
* Added :meth:`~cryptography.hazmat.primitives.ciphers.CipherContext.reset_nonce`
64+
for altering the ``nonce`` of a cipher context without initializing a new
65+
instance. See the docs for additional restrictions.
6366

6467
.. _v42-0-8:
6568

docs/hazmat/primitives/symmetric-encryption.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,27 @@ Interfaces
693693
:meth:`update` and :meth:`finalize` will raise an
694694
:class:`~cryptography.exceptions.AlreadyFinalized` exception.
695695

696+
.. method:: reset_nonce(nonce)
697+
698+
.. versionadded:: 43.0.0
699+
700+
This method allows changing the nonce for an already existing context.
701+
Normally the nonce is set when the context is created and internally
702+
incremented as data as passed. However, in some scenarios the same key
703+
is used repeatedly but the nonce changes non-sequentially (e.g. ``QUIC``),
704+
which requires updating the context with the new nonce.
705+
706+
This method only works for contexts using
707+
:class:`~cryptography.hazmat.primitives.ciphers.algorithms.ChaCha20` or
708+
:class:`~cryptography.hazmat.primitives.ciphers.modes.CTR` mode.
709+
710+
:param nonce: The nonce to update the context with.
711+
:type data: :term:`bytes-like`
712+
:raises cryptography.exceptions.UnsupportedAlgorithm: If the
713+
algorithm does not support updating the nonce.
714+
:raises ValueError: If the nonce is not the correct length for the
715+
algorithm.
716+
696717
.. class:: AEADCipherContext
697718

698719
When calling ``encryptor`` or ``decryptor`` on a ``Cipher`` object

src/cryptography/hazmat/primitives/ciphers/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ def finalize(self) -> bytes:
3333
Returns the results of processing the final block as bytes.
3434
"""
3535

36+
@abc.abstractmethod
37+
def reset_nonce(self, nonce: bytes) -> None:
38+
"""
39+
Resets the nonce for the cipher context to the provided value.
40+
Raises an exception if it does not support reset or if the
41+
provided nonce does not have a valid length.
42+
"""
43+
3644

3745
class AEADCipherContext(CipherContext, metaclass=abc.ABCMeta):
3846
@abc.abstractmethod

src/rust/src/backend/ciphers.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use pyo3::IntoPy;
1313
pub(crate) struct CipherContext {
1414
ctx: openssl::cipher_ctx::CipherCtx,
1515
py_mode: pyo3::PyObject,
16+
py_algorithm: pyo3::PyObject,
17+
side: openssl::symm::Mode,
1618
}
1719

1820
impl CipherContext {
@@ -113,9 +115,44 @@ impl CipherContext {
113115
Ok(CipherContext {
114116
ctx,
115117
py_mode: mode.into(),
118+
py_algorithm: algorithm.into(),
119+
side,
116120
})
117121
}
118122

123+
fn reset_nonce(&mut self, py: pyo3::Python<'_>, nonce: CffiBuf<'_>) -> CryptographyResult<()> {
124+
if !self
125+
.py_mode
126+
.bind(py)
127+
.is_instance(&types::MODE_WITH_NONCE.get(py)?)?
128+
&& !self
129+
.py_algorithm
130+
.bind(py)
131+
.is_instance(&types::CHACHA20.get(py)?)?
132+
{
133+
return Err(CryptographyError::from(
134+
exceptions::UnsupportedAlgorithm::new_err((
135+
"This algorithm or mode does not support resetting the nonce.",
136+
exceptions::Reasons::UNSUPPORTED_CIPHER,
137+
)),
138+
));
139+
}
140+
if nonce.as_bytes().len() != self.ctx.iv_length() {
141+
return Err(CryptographyError::from(
142+
pyo3::exceptions::PyValueError::new_err(format!(
143+
"Nonce must be {} bytes long",
144+
self.ctx.iv_length()
145+
)),
146+
));
147+
}
148+
let init_op = match self.side {
149+
openssl::symm::Mode::Encrypt => openssl::cipher_ctx::CipherCtxRef::encrypt_init,
150+
openssl::symm::Mode::Decrypt => openssl::cipher_ctx::CipherCtxRef::decrypt_init,
151+
};
152+
init_op(&mut self.ctx, None, None, Some(nonce.as_bytes()))?;
153+
Ok(())
154+
}
155+
119156
fn update<'p>(
120157
&mut self,
121158
py: pyo3::Python<'p>,
@@ -236,6 +273,10 @@ impl PyCipherContext {
236273
get_mut_ctx(self.ctx.as_mut())?.update(py, buf.as_bytes())
237274
}
238275

276+
fn reset_nonce(&mut self, py: pyo3::Python<'_>, nonce: CffiBuf<'_>) -> CryptographyResult<()> {
277+
get_mut_ctx(self.ctx.as_mut())?.reset_nonce(py, nonce)
278+
}
279+
239280
fn update_into(
240281
&mut self,
241282
py: pyo3::Python<'_>,
@@ -340,6 +381,10 @@ impl PyAEADEncryptionContext {
340381
})?
341382
.clone_ref(py))
342383
}
384+
385+
fn reset_nonce(&mut self, py: pyo3::Python<'_>, nonce: CffiBuf<'_>) -> CryptographyResult<()> {
386+
get_mut_ctx(self.ctx.as_mut())?.reset_nonce(py, nonce)
387+
}
343388
}
344389

345390
#[pyo3::pymethods]
@@ -468,6 +513,10 @@ impl PyAEADDecryptionContext {
468513
self.ctx = None;
469514
Ok(result)
470515
}
516+
517+
fn reset_nonce(&mut self, py: pyo3::Python<'_>, nonce: CffiBuf<'_>) -> CryptographyResult<()> {
518+
get_mut_ctx(self.ctx.as_mut())?.reset_nonce(py, nonce)
519+
}
471520
}
472521

473522
#[pyo3::pyfunction]

tests/hazmat/primitives/test_aes.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
import pytest
1010

11+
from cryptography.exceptions import AlreadyFinalized, _Reasons
1112
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
1213
from cryptography.hazmat.primitives.ciphers import algorithms, base, modes
1314

1415
from ...doubles import DummyMode
15-
from ...utils import load_nist_vectors
16+
from ...utils import load_nist_vectors, raises_unsupported_algorithm
1617
from .utils import _load_all_params, generate_encrypt_test
1718

1819

@@ -305,3 +306,43 @@ def test_alternate_aes_classes(mode, alg_cls, backend):
305306
dec = cipher.decryptor()
306307
pt = dec.update(ct) + dec.finalize()
307308
assert pt == data
309+
310+
311+
def test_reset_nonce(backend):
312+
data = b"helloworld" * 10
313+
nonce = b"\x00" * 16
314+
c = base.Cipher(
315+
algorithms.AES(b"\x00" * 16),
316+
modes.CTR(nonce),
317+
)
318+
enc = c.encryptor()
319+
ct1 = enc.update(data)
320+
assert len(ct1) == len(data)
321+
for _ in range(2):
322+
enc.reset_nonce(nonce)
323+
assert enc.update(data) == ct1
324+
enc.finalize()
325+
with pytest.raises(AlreadyFinalized):
326+
enc.reset_nonce(nonce)
327+
dec = c.decryptor()
328+
assert dec.update(ct1) == data
329+
for _ in range(2):
330+
dec.reset_nonce(nonce)
331+
assert dec.update(ct1) == data
332+
dec.finalize()
333+
with pytest.raises(AlreadyFinalized):
334+
dec.reset_nonce(nonce)
335+
336+
337+
def test_reset_nonce_invalid_mode(backend):
338+
iv = b"\x00" * 16
339+
c = base.Cipher(
340+
algorithms.AES(b"\x00" * 16),
341+
modes.CBC(iv),
342+
)
343+
enc = c.encryptor()
344+
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_CIPHER):
345+
enc.reset_nonce(iv)
346+
dec = c.decryptor()
347+
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_CIPHER):
348+
dec.reset_nonce(iv)

tests/hazmat/primitives/test_aes_gcm.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88

99
import pytest
1010

11+
from cryptography.exceptions import _Reasons
1112
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
1213
from cryptography.hazmat.primitives.ciphers import algorithms, base, modes
1314

14-
from ...utils import load_nist_vectors
15+
from ...utils import load_nist_vectors, raises_unsupported_algorithm
1516
from .utils import generate_aead_test
1617

1718

@@ -230,3 +231,16 @@ def test_alternate_aes_classes(self, alg, backend):
230231
dec = cipher.decryptor()
231232
pt = dec.update(ct) + dec.finalize_with_tag(enc.tag)
232233
assert pt == data
234+
235+
def test_reset_nonce_invalid_mode(self, backend):
236+
nonce = b"\x00" * 12
237+
c = base.Cipher(
238+
algorithms.AES(b"\x00" * 16),
239+
modes.GCM(nonce),
240+
)
241+
enc = c.encryptor()
242+
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_CIPHER):
243+
enc.reset_nonce(nonce)
244+
dec = c.decryptor()
245+
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_CIPHER):
246+
dec.reset_nonce(nonce)

tests/hazmat/primitives/test_chacha20.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import pytest
1111

12+
from cryptography.exceptions import AlreadyFinalized
1213
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
1314

1415
from ...utils import load_nist_vectors
@@ -90,3 +91,36 @@ def test_partial_blocks(self, backend):
9091
ct_partial_3 = enc_partial.update(pt[len_partial * 2 :])
9192

9293
assert ct_full == ct_partial_1 + ct_partial_2 + ct_partial_3
94+
95+
def test_reset_nonce(self, backend):
96+
data = b"helloworld" * 10
97+
key = b"\x00" * 32
98+
nonce = b"\x00" * 16
99+
cipher = Cipher(algorithms.ChaCha20(key, nonce), None)
100+
enc = cipher.encryptor()
101+
ct1 = enc.update(data)
102+
assert len(ct1) == len(data)
103+
for _ in range(2):
104+
enc.reset_nonce(nonce)
105+
assert enc.update(data) == ct1
106+
enc.finalize()
107+
with pytest.raises(AlreadyFinalized):
108+
enc.reset_nonce(nonce)
109+
dec = cipher.decryptor()
110+
assert dec.update(ct1) == data
111+
for _ in range(2):
112+
dec.reset_nonce(nonce)
113+
assert dec.update(ct1) == data
114+
dec.finalize()
115+
with pytest.raises(AlreadyFinalized):
116+
dec.reset_nonce(nonce)
117+
118+
def test_nonce_reset_invalid_length(self, backend):
119+
key = b"\x00" * 32
120+
nonce = b"\x00" * 16
121+
cipher = Cipher(algorithms.ChaCha20(key, nonce), None)
122+
enc = cipher.encryptor()
123+
with pytest.raises(ValueError):
124+
enc.reset_nonce(nonce[:-1])
125+
with pytest.raises(ValueError):
126+
enc.reset_nonce(nonce + b"\x00")

0 commit comments

Comments
 (0)