Skip to content

fix: accept bytearray in ctr256_encrypt/decrypt (pyrogram compatibility) - #5

Merged
joyccn merged 4 commits into
masterfrom
fix/ctr-bytearray
Aug 13, 2026
Merged

fix: accept bytearray in ctr256_encrypt/decrypt (pyrogram compatibility)#5
joyccn merged 4 commits into
masterfrom
fix/ctr-bytearray

Conversation

@joyccn

@joyccn joyccn commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Fixes #4.

Problem

ctr256_encrypt/ctr256_decrypt rejected bytearray for data/key/iv/state because the PyO3 0.29 &[u8] extractor only accepts bytes:

TypeError: 'bytearray' object is not an instance of 'bytes'

pyrogram and its forks (kurigram, pyrofork, ...) always pass bytearray — their obfuscated TCP transports and CDN downloads reuse the same iv/state objects across calls and rely on them being mutated in place to carry the CTR counter forward. The crash hit at connect time, so bots could not connect at all with this library as the crypto backend.

Fix

  • ctr256_encrypt/ctr256_decrypt now accept bytes (zero-copy) or bytearray for data, key, iv, and state.
  • When iv/state are bytearray, the advanced counter and residual byte offset are written back in place after the operation, matching TgCrypto's stateful semantics (verified against pyrogram's pyaes fallback algorithm).
  • Validation unchanged: key=32, iv=16, state in 0..15.

Verification

  • 5 new Python tests: bytearray acceptance, in-place mutation, chunked-stream == one-shot, 512 KB CDN-style chunked roundtrip, validation errors.
  • Full obfuscated TCP connect simulation (64-byte nonce handshake + packet streaming with state carry) roundtrips correctly.
  • 13/13 Python tests, 19/19 Rust tests, cargo fmt and clippy -D warnings clean.

Co-authored-by: whoarchie esanovalr.k@gmail.com

Summary by Sourcery

Add TgCrypto-compatible bytearray support and in-place state carry semantics to AES-256-CTR bindings and bump the library version.

New Features:

  • Allow ctr256_encrypt/ctr256_decrypt to accept both bytes and bytearray for data, key, iv, and state, with bytearray treated as stateful inputs whose iv/state are updated in place.

Enhancements:

  • Introduce internal buffer handling utilities to support safe bytearray mutation under GIL management in the Python bindings.

Build:

  • Bump crate and Python package versions from 1.3.0 to 1.3.1 in Cargo and pyproject configuration.

Documentation:

  • Document bytearray-compatible CTR usage and state carry behavior, including examples and a 1.3.1 changelog entry describing the new capabilities.

Tests:

  • Add Python tests covering bytearray acceptance, in-place iv/state mutation, chunked streaming equivalence, large roundtrip behavior, and explicit validation/type errors.

@sourcery-ai

sourcery-ai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds TgCrypto-compatible bytearray support to AES-256-CTR Python bindings, including safe in-place IV/state mutation semantics, tests, docs, and version bump.

Sequence diagram for ctr256_encrypt bytearray state carry

sequenceDiagram
    actor PythonCaller
    participant ctr256_encrypt
    participant BufferInput
    participant tgcryptors_core
    participant write_back

    PythonCaller->>ctr256_encrypt: ctr256_encrypt(data,key,iv,state)
    ctr256_encrypt->>BufferInput: BufferInput::from_any(data,"Data")
    ctr256_encrypt->>BufferInput: BufferInput::from_any(key,"Key")
    ctr256_encrypt->>BufferInput: BufferInput::from_any(iv,"IV")
    ctr256_encrypt->>BufferInput: BufferInput::from_any(state,"State")
    ctr256_encrypt->>BufferInput: BufferInput::as_bytes() for key
    ctr256_encrypt->>BufferInput: BufferInput::as_bytes() for iv
    ctr256_encrypt->>BufferInput: BufferInput::as_bytes() for state
    ctr256_encrypt->>BufferInput: BufferInput::as_bytes() for data
    ctr256_encrypt->>tgcryptors_core: ctr256_encrypt_into(data_slice,key_arr,iv_arr,state_val,dest)
    tgcryptors_core-->>ctr256_encrypt: returns (next_iv,next_state)
    ctr256_encrypt->>BufferInput: BufferInput::into_source() for iv
    ctr256_encrypt->>BufferInput: BufferInput::into_source() for state
    alt iv is bytearray
        ctr256_encrypt->>write_back: write_back(iv_source,next_iv)
    end
    alt state is bytearray
        ctr256_encrypt->>write_back: write_back(state_source,[next_state])
    end
    ctr256_encrypt-->>PythonCaller: returns ciphertext PyBytes
Loading

File-Level Changes

Change Details Files
Support both bytes and bytearray for ctr256_encrypt/ctr256_decrypt, with safe in-place IV/state updates when bytearray is used.
  • Introduce BufferInput helper enum to uniformly handle bytes/bytearray arguments while preserving zero-copy for bytes and copying bytearray contents for safe use during GIL release.
  • Change ctr256_encrypt/ctr256_decrypt Python signatures from &[u8] to PyAny-bound arguments and extract them via BufferInput::from_any with explicit TypeError on invalid types.
  • Use BufferInput::as_bytes for validation and encryption, and capture updated IV and state from core ctr implementation during execute_zerocopy.
  • Add write_back helper using pyo3 critical_section to overwrite original bytearray contents with the advanced IV and residual state, preserving TgCrypto semantics.
tgcryptors-python/src/lib.rs
Extend Python tests to cover bytearray behavior, error handling, and bump exposed version.
  • Add tests verifying acceptance of bytearray for data/key/iv/state, in-place mutation of iv/state, chunked CTR streams matching one-shot behavior, and large chunked roundtrip correctness.
  • Add tests ensuring explicit validation error messages for invalid bytearray lengths and TypeError for non-bytes-like inputs.
  • Update test asserting tgcrypto.version to expect 1.3.1.
tests/test_python_api.py
Document TgCrypto-compatible bytearray mode and record the change in project metadata.
  • Extend README CTR section with examples and explanation of bytearray support and stateful in-place IV/state carry semantics used by pyrogram and forks.
  • Add 1.3.1 changelog entry describing bytearray support and the pyrogram crash fix.
  • Bump workspace and Python package versions from 1.3.0 to 1.3.1 in Cargo.toml and pyproject.toml; update lockfiles accordingly.
README.md
Cargo.toml
pyproject.toml
Cargo.lock
uv.lock

Assessment against linked issues

Issue Objective Addressed Explanation
#4 Modify ctr256_encrypt/ctr256_decrypt to accept bytearray (in addition to bytes) for iv/state so that pyrogram-based projects no longer crash with a TypeError.
#4 Ensure the AES-CTR counter state (iv and residual offset) can be carried forward between calls, e.g. by mutating iv/state in place or otherwise exposing updated state, matching TgCrypto’s stateful semantics.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tgcryptors-python/src/lib.rs" line_range="98-109" />
<code_context>
+/// The buffer length is guaranteed to match `value` because extraction copied
+/// the exact same length and Python code cannot legally resize the object
+/// while this function holds it inside the critical section.
+fn write_back(byte_array: &Bound<'_, PyByteArray>, value: &[u8]) {
+    with_critical_section(byte_array.as_any(), || {
+        // SAFETY: the critical section prevents concurrent mutation of the
+        // buffer, and the buffer was not resized since extraction.
+        unsafe { byte_array.as_bytes_mut() }.copy_from_slice(value);
+    });
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** write_back relies on implicit invariants about buffer length that could fail noisily if future callers misuse it.

The function depends on `value.len() == byte_array.len()`, and `copy_from_slice` will panic if that ever stops being true. Current call sites (`copy_array` / `validate_ctr_state`) enforce the invariant, but future reuse or changes to the CTR logic could easily break it. Adding an explicit `debug_assert_eq!(byte_array.len(), value.len());` before the unsafe block would make violations fail immediately and closer to the source.

```suggestion
/// Overwrite the contents of a `bytearray` with `value`.
///
/// The buffer length is guaranteed to match `value` because extraction copied
/// the exact same length and Python code cannot legally resize the object
/// while this function holds it inside the critical section.
fn write_back(byte_array: &Bound<'_, PyByteArray>, value: &[u8]) {
    with_critical_section(byte_array.as_any(), || {
        debug_assert_eq!(
            byte_array.len(),
            value.len(),
            "write_back: bytearray length ({}) must match value length ({})",
            byte_array.len(),
            value.len()
        );

        // SAFETY: the critical section prevents concurrent mutation of the
        // buffer, and the buffer was not resized since extraction and the
        // lengths are asserted to match.
        unsafe { byte_array.as_bytes_mut() }.copy_from_slice(value);
    });
}
```
</issue_to_address>

### Comment 2
<location path="tests/test_python_api.py" line_range="115" />
<code_context>
+
+        self.assertEqual(plaintext, data)
+
+    def test_ctr_bytearray_mutates_iv_and_state_in_place(self) -> None:
+        data = self.data + b"xyz"  # 67 bytes, not block aligned
+        iv = bytearray(self.iv_cbc)
+        state = bytearray(1)
+
+        tgcrypto.ctr256_encrypt(data, self.key, iv, state)
+
+        self.assertEqual(state[0], len(data) % 16)
+        self.assertNotEqual(iv, bytearray(self.iv_cbc))
+
     def test_ige_stream_matches_one_shot(self) -> None:
</code_context>
<issue_to_address>
**suggestion (testing):** Add a complementary test that bytes inputs remain stateless and do not mutate iv/state.

This test covers in-place mutation for bytearray iv/state and the expected state offset. Please also add a test that calls `ctr256_encrypt` with iv/state (and optionally data/key) as `bytes`, asserting that those original objects remain unchanged across calls, so both mutable (bytearray) and immutable (bytes) behaviors are explicitly verified.

```suggestion
    def test_ctr_bytes_do_not_mutate_iv_or_state(self) -> None:
        data = self.data + b"xyz"  # 67 bytes, not block aligned
        iv = self.iv_cbc
        state = b"\x00"

        ciphertext1 = tgcrypto.ctr256_encrypt(data, self.key, iv, state)
        ciphertext2 = tgcrypto.ctr256_encrypt(data, self.key, iv, state)

        self.assertEqual(iv, self.iv_cbc)
        self.assertEqual(state, b"\x00")
        self.assertEqual(ciphertext1, ciphertext2)

    def test_ige_stream_matches_one_shot(self) -> None:
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tgcryptors-python/src/lib.rs
Comment thread tests/test_python_api.py
joyccn and others added 4 commits August 13, 2026 08:12
ctr256_encrypt and ctr256_decrypt rejected bytearray arguments because
the PyO3 0.29 &[u8] extractor only accepts bytes. pyrogram and its
forks pass bytearray for data, key, iv, and state and rely on iv/state
being mutated in place to carry the CTR counter across calls.

Accept bytes or bytearray for all four arguments. bytearray iv/state
are copied up front (the GIL is released during the operation) and the
advanced counter and residual byte offset are written back in place
afterwards, matching TgCrypto's stateful semantics.

Co-authored-by: whoarchie <esanovalr.k@gmail.com>
Verify bytearray acceptance for data/key/iv/state, in-place mutation of
iv/state across calls, chunked streams matching the one-shot result, a
large CDN-style roundtrip, and that bytes inputs stay stateless.

Co-authored-by: whoarchie <esanovalr.k@gmail.com>
Explain the TgCrypto-compatible bytearray mode: data/key/iv/state may be
bytearray, and iv/state are updated in place so the stream continues
across chunked calls, as pyrogram and its forks require.

Co-authored-by: whoarchie <esanovalr.k@gmail.com>
Co-authored-by: whoarchie <esanovalr.k@gmail.com>
@joyccn
joyccn force-pushed the fix/ctr-bytearray branch from a8cb825 to e8ac821 Compare August 13, 2026 01:12
@joyccn
joyccn merged commit 9d35455 into master Aug 13, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ctr256_encrypt/decrypt reject bytearray for iv/state — breaks pyrogram and forks

1 participant