Skip to content

Commit e5e1b83

Browse files
committed
add support for CipherContext.update_nonce
This only supports ChaCha20 and ciphers in CTR mode.
1 parent 027845c commit e5e1b83

File tree

7 files changed

+167
-2
lines changed

7 files changed

+167
-2
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Changelog
2626
and :class:`~cryptography.hazmat.primitives.ciphers.algorithms.ARC4` into
2727
:doc:`/hazmat/decrepit/index` and deprecated them in the ``cipher`` module.
2828
They will be removed from the ``cipher`` module in 48.0.0.
29+
* Added :meth:`~cryptography.hazmat.primitives.ciphers.CipherContext.update_nonce`
30+
for altering the ``nonce`` of a cipher context without initializing a new
31+
instance. See the docs for additional restrictions.
2932

3033
.. _v42-0-3:
3134

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:: update_nonce(nonce)
697+
698+
.. versionadded:: 43.0.0
699+
700+
This method allows updating 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 update_nonce(self, nonce: bytes) -> None:
38+
"""
39+
Updates 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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use pyo3::IntoPy;
1212
struct CipherContext {
1313
ctx: openssl::cipher_ctx::CipherCtx,
1414
py_mode: pyo3::PyObject,
15+
side: openssl::symm::Mode,
16+
update_nonce_allowed: bool,
1517
}
1618

1719
impl CipherContext {
@@ -41,6 +43,7 @@ impl CipherContext {
4143
}
4244
};
4345

46+
let mut update_nonce_allowed = false;
4447
let iv_nonce = if mode.is_instance(types::MODE_WITH_INITIALIZATION_VECTOR.get(py)?)? {
4548
Some(
4649
mode.getattr(pyo3::intern!(py, "initialization_vector"))?
@@ -52,11 +55,13 @@ impl CipherContext {
5255
.extract::<CffiBuf<'_>>()?,
5356
)
5457
} else if mode.is_instance(types::MODE_WITH_NONCE.get(py)?)? {
58+
update_nonce_allowed = true;
5559
Some(
5660
mode.getattr(pyo3::intern!(py, "nonce"))?
5761
.extract::<CffiBuf<'_>>()?,
5862
)
5963
} else if algorithm.is_instance(types::CHACHA20.get(py)?)? {
64+
update_nonce_allowed = true;
6065
Some(
6166
algorithm
6267
.getattr(pyo3::intern!(py, "nonce"))?
@@ -111,9 +116,36 @@ impl CipherContext {
111116
Ok(CipherContext {
112117
ctx,
113118
py_mode: mode.into(),
119+
side,
120+
update_nonce_allowed,
114121
})
115122
}
116123

124+
fn update_nonce(&mut self, nonce: CffiBuf<'_>) -> CryptographyResult<()> {
125+
if !self.update_nonce_allowed {
126+
return Err(CryptographyError::from(
127+
exceptions::UnsupportedAlgorithm::new_err((
128+
"This algorithm or mode does not support resetting the nonce.",
129+
exceptions::Reasons::UNSUPPORTED_CIPHER,
130+
)),
131+
));
132+
}
133+
if nonce.as_bytes().len() != self.ctx.iv_length() {
134+
return Err(CryptographyError::from(
135+
pyo3::exceptions::PyValueError::new_err(format!(
136+
"Nonce must be {} bytes long",
137+
self.ctx.iv_length()
138+
)),
139+
));
140+
}
141+
let init_op = match self.side {
142+
openssl::symm::Mode::Encrypt => openssl::cipher_ctx::CipherCtxRef::encrypt_init,
143+
openssl::symm::Mode::Decrypt => openssl::cipher_ctx::CipherCtxRef::decrypt_init,
144+
};
145+
init_op(&mut self.ctx, None, None, Some(nonce.as_bytes()))?;
146+
Ok(())
147+
}
148+
117149
fn update<'p>(
118150
&mut self,
119151
py: pyo3::Python<'p>,
@@ -234,6 +266,10 @@ impl PyCipherContext {
234266
get_mut_ctx(self.ctx.as_mut())?.update(py, buf.as_bytes())
235267
}
236268

269+
fn update_nonce(&mut self, nonce: CffiBuf<'_>) -> CryptographyResult<()> {
270+
get_mut_ctx(self.ctx.as_mut())?.update_nonce(nonce)
271+
}
272+
237273
fn update_into(
238274
&mut self,
239275
py: pyo3::Python<'_>,
@@ -338,6 +374,10 @@ impl PyAEADEncryptionContext {
338374
})?
339375
.clone_ref(py))
340376
}
377+
378+
fn update_nonce(&mut self, nonce: CffiBuf<'_>) -> CryptographyResult<()> {
379+
get_mut_ctx(self.ctx.as_mut())?.update_nonce(nonce)
380+
}
341381
}
342382

343383
#[pyo3::prelude::pymethods]
@@ -466,6 +506,10 @@ impl PyAEADDecryptionContext {
466506
self.ctx = None;
467507
Ok(result)
468508
}
509+
510+
fn update_nonce(&mut self, nonce: CffiBuf<'_>) -> CryptographyResult<()> {
511+
get_mut_ctx(self.ctx.as_mut())?.update_nonce(nonce)
512+
}
469513
}
470514

471515
#[pyo3::prelude::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_update_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.update_nonce(nonce)
323+
assert enc.update(data) == ct1
324+
enc.finalize()
325+
with pytest.raises(AlreadyFinalized):
326+
enc.update_nonce(nonce)
327+
dec = c.decryptor()
328+
assert dec.update(ct1) == data
329+
for _ in range(2):
330+
dec.update_nonce(nonce)
331+
assert dec.update(ct1) == data
332+
dec.finalize()
333+
with pytest.raises(AlreadyFinalized):
334+
dec.update_nonce(nonce)
335+
336+
337+
def test_update_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.update_nonce(iv)
346+
dec = c.decryptor()
347+
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_CIPHER):
348+
dec.update_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_update_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.update_nonce(nonce)
244+
dec = c.decryptor()
245+
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_CIPHER):
246+
dec.update_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_update_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.update_nonce(nonce)
105+
assert enc.update(data) == ct1
106+
enc.finalize()
107+
with pytest.raises(AlreadyFinalized):
108+
enc.update_nonce(nonce)
109+
dec = cipher.decryptor()
110+
assert dec.update(ct1) == data
111+
for _ in range(2):
112+
dec.update_nonce(nonce)
113+
assert dec.update(ct1) == data
114+
dec.finalize()
115+
with pytest.raises(AlreadyFinalized):
116+
dec.update_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.update_nonce(nonce[:-1])
125+
with pytest.raises(ValueError):
126+
enc.update_nonce(nonce + b"\x00")

0 commit comments

Comments
 (0)