✨ Add Cipher mode 2 (AEAD, STREAM-based GCM) support to libpna#3192
✨ Add Cipher mode 2 (AEAD, STREAM-based GCM) support to libpna#3192ChanTsune wants to merge 13 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds AES-GCM and Camellia-GCM STREAM encryption with HKDF-derived keys, segmented authenticated I/O, archive integration, preserved headers, public AEAD errors, encrypted rename handling, and extensive validation tests. ChangesGCM STREAM archive encryption
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant EntryBuilder
participant WriterContext
participant KeyDerivation
participant GcmEncryptWriter
participant Archive
EntryBuilder->>WriterContext: header bytes and stream kind
WriterContext->>KeyDerivation: master key, salt, header, PHSF
KeyDerivation-->>WriterContext: stream key
WriterContext->>GcmEncryptWriter: stream key, nonce prefix, segment size
GcmEncryptWriter->>GcmEncryptWriter: segment plaintext and derive nonce
GcmEncryptWriter->>Archive: ciphertext and authentication tag
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces authenticated encryption support using AES-GCM and Camellia-GCM in STREAM mode (Cipher Mode 2) for PNA archives. It adds key and nonce derivation primitives via HKDF-SHA-256, implements segment-based encryption and decryption readers/writers, and introduces a dedicated AeadError type for handling AEAD-specific failures. Additionally, raw headers are now retained in entries to ensure key derivation binds correctly to the exact on-wire bytes. Feedback on the changes highlights a performance optimization in GcmDecryptReader to reuse buffers instead of allocating a new vector for each segment read, which can be up to 64 MiB.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/src/entry.rs (1)
1142-1165: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftPreserve
with_namefor borrowed entry types.The new
T: From<Vec<u8>>bound removes this public method fromNormalEntry<&[u8]>returned by slice APIs and from other existing representations. Store the small raw header independently as owned bytes so renaming remains available without this source-breaking bound.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/src/entry.rs` around lines 1142 - 1165, Remove the T: From<Vec<u8>> bound from Entry::with_name so it remains available for borrowed representations such as NormalEntry<&[u8]>. Store the serialized header bytes as an owned byte buffer independently of T, while preserving the existing header rename and returned-entry behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/src/cipher/gcm.rs`:
- Around line 5-12: Replace the Camellia path’s use of aes_gcm::AesGcm in
GcmEncryptWriter and its related encryption/decryption code with a backend
explicitly implementing Camellia-GCM. Update the associated trait bounds,
imports, and construction sites to use that backend, and validate
interoperability with published Camellia-GCM known-answer vectors before
release.
In `@lib/src/entry.rs`:
- Around line 344-349: The serialization path currently re-encodes headers
instead of preserving the original authenticated bytes. Update FHED/SHED writing
for unchanged entries to emit header_raw, while ensuring mutation APIs refresh
header_raw when headers change; add a regression test using a noncanonical
external header and retained ciphertext to verify decryption after re-emission.
In `@lib/src/entry/read.rs`:
- Around line 47-50: Update the encryption-mode handling in the entry-reading
logic to validate reserved or unsupported cipher modes before accessing the
optional phsf value, returning the required Unsupported error for unknown modes
even when PHSF is absent. Preserve the existing missing-PHSF InvalidData
behavior for supported AES and Camellia modes, and add coverage for the
missing-PHSF reserved-mode case in the relevant tests.
---
Outside diff comments:
In `@lib/src/entry.rs`:
- Around line 1142-1165: Remove the T: From<Vec<u8>> bound from Entry::with_name
so it remains available for borrowed representations such as NormalEntry<&[u8]>.
Store the serialized header bytes as an owned byte buffer independently of T,
while preserving the existing header rename and returned-entry behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3d0191da-b9ce-4152-bd03-4e07388832b1
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (13)
lib/Cargo.tomllib/src/archive.rslib/src/archive/write.rslib/src/cipher.rslib/src/cipher/aead.rslib/src/cipher/gcm.rslib/src/entry.rslib/src/entry/builder.rslib/src/entry/options.rslib/src/entry/read.rslib/src/entry/write.rslib/src/error.rslib/src/lib.rs
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cli/src/command/core.rs`:
- Around line 2057-2079: Ensure the GCM rename branch preserves the original
entry kind before calling re_encrypt_entry_with_name. Reject GCM renames for
directory or link entries, or add kind-specific rebuilding based on data_kind;
do not allow re_encrypt_entry_with_name to convert them into regular files.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f242bbbc-3a4f-49fc-b110-e50cdd1bef78
📒 Files selected for processing (8)
cli/src/command/core.rscli/tests/cli/stdio.rscli/tests/cli/stdio/encrypted_rename.rslib/src/archive.rslib/src/cipher/gcm.rslib/src/entry.rslib/src/entry/write.rslib/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (4)
- lib/src/error.rs
- lib/src/cipher/gcm.rs
- lib/src/archive.rs
- lib/src/entry.rs
9607f5b to
fe488f4
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
cli/src/command/core.rs (1)
2070-2084: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject non-file GCM renames before rebuilding.
This path still sends every renamed GCM entry to a helper that unconditionally calls
EntryBuilder::new_file, converting directories or links into regular files. Reject non-file kinds before this call, or rebuild with the corresponding kind-specific constructor.Also applies to: 2194-2201
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cli/src/command/core.rs` around lines 2070 - 2084, The GCM rename path can convert directories or links into regular files through re_encrypt_entry_with_name. Before invoking this helper in the encrypted rename branch, validate that entry is a regular file and reject non-file kinds; apply the same guard in the corresponding path around the additional reported location.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/src/entry/write.rs`:
- Around line 236-242: Update the Camellia GCM branch in the writer construction
around EncryptGcmCamellia256Writer, and apply the same change to the matching
reader, so neither path instantiates aes_gcm::AesGcm with Camellia256. Replace
that backend with a vetted Camellia-GCM implementation or separately reviewed
generic GCM construction while preserving the existing header, nonce-prefix,
segment-size, and key-stream behavior.
---
Duplicate comments:
In `@cli/src/command/core.rs`:
- Around line 2070-2084: The GCM rename path can convert directories or links
into regular files through re_encrypt_entry_with_name. Before invoking this
helper in the encrypted rename branch, validate that entry is a regular file and
reject non-file kinds; apply the same guard in the corresponding path around the
additional reported location.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: dbbf4b2c-324d-422a-a009-457016938bc5
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
cli/src/command/core.rscli/tests/cli/stdio.rscli/tests/cli/stdio/encrypted_rename.rslib/Cargo.tomllib/src/archive.rslib/src/archive/write.rslib/src/cipher.rslib/src/cipher/aead.rslib/src/cipher/gcm.rslib/src/entry.rslib/src/entry/builder.rslib/src/entry/options.rslib/src/entry/read.rslib/src/entry/write.rslib/src/error.rslib/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (11)
- cli/tests/cli/stdio.rs
- lib/src/lib.rs
- cli/tests/cli/stdio/encrypted_rename.rs
- lib/src/error.rs
- lib/src/entry/read.rs
- lib/src/entry/options.rs
- lib/src/entry/builder.rs
- lib/src/cipher/gcm.rs
- lib/src/cipher.rs
- lib/src/archive.rs
- lib/src/entry.rs
245251b to
3ebe16d
Compare
3ebe16d to
b5a7c57
Compare
Assign wire value 2 to Galois/Counter mode (AEAD, STREAM-based): add the associated constant, exclude 2 from the reserved range, and render it in Debug output.
AeadError distinguishes malformed datastream layout, GCM tag verification failure, and truncation (a partial tail too short to be a final segment, after at least one verified segment; a cut that leaves a plausible final segment surfaces as AuthenticationFailure). All variants convert to io::ErrorKind::InvalidData, deliberately including Truncation: readers treat UnexpectedEof as a clean end of stream, which would let a truncated authenticated datastream terminate entry iteration without an error.
HKDF-SHA-256 derives a per-stream key from the master key, the stream salt, and an entry context binding the on-wire FHED/SHED and PHSF bytes; each segment 96-bit nonce is derived from the stream nonce prefix, a segment counter, and a final-segment marker. StreamHeader owns the on-wire 43-byte stream header layout (salt(32) || nonce_prefix(7) || segment_size(u32 BE)) via to_bytes/try_from_bytes, including the segment size range check.
Encrypts fixed-size segments with per-segment derived nonces and a final-segment marker, per the STREAM construction. The constructor validates the segment size range (a zero segment size would make the write loop emit tag-only segments without consuming input until counter overflow).
Reads one segment ahead so a segment is decrypted with the final flag set only when no datastream bytes follow it; only tag-verified plaintext is ever returned to the caller. Segment buffers use fallible allocation: the segment size in the unauthenticated stream header controls the allocation, so a crafted archive could otherwise abort the process on exhaustion.
CipherPayload distinguishes the block modes (KDF output used directly as the key, IV as the datastream prefix) from GCM (per-stream key derived from K_master and a StreamHeader written as the prefix). WriteOptions gains a GCM segment size, and a fully-defaulted Argon2id is upgraded to the GCM spec profile (t=3, m=64 MiB, p=4) via effective_hash_algorithm; a fresh WriteOptions must be built per archive so no two archives reuse the KDF salt.
Keep the on-wire FHED/SHED chunk data alongside the parsed header so AEAD stream-key derivation can bind to the exact bytes read from the archive, as the spec requires: correctness must not depend on parse/serialize being a lossless round trip for archives written by other tools. The retained field is never used when writing.
Add a GCM branch to `decrypt_reader` that reads the 43-byte stream header, validates the segment size before deriving any key, recovers K_master (through the shared derived-key cache), derives the per-stream key from the retained header bytes and PHSF, and builds the segment decryptor. `decrypt_reader` now takes the read options plus the raw header chunk bytes and the stream kind (per-entry or solid), threaded through `NormalEntry::reader` and `SolidEntry::entries`/`into_entries_with_options`.
…ests Cover per-entry and solid roundtrips across algorithms and compressions, tamper and truncation detection (including a datastream cut exactly at an inner entry boundary, which must surface as an error rather than ending iteration cleanly), segment-boundary payload sizes, a datastream shorter than the stream header, salt/nonce-prefix uniqueness across entries, and decryptability of an entry copied verbatim into another archive.
Renaming a cipher mode 2 (GCM) encrypted entry makes its data unreadable because the stream key is bound to the FHED bytes. try_with_name owns the rename (refreshing header_raw from the renamed header) and returns an error for such entries; with_name delegates to it and panics instead of silently producing an undecryptable entry.
Renaming a cipher mode 2 (GCM) entry during @archive copy silently produced an undecryptable entry because the stream key is bound to the header bytes. Renames now decrypt and re-encrypt with the source entry's algorithm and compression when the password is available, and fail otherwise. A no-op rename keeps the raw header bytes untouched. Re-encryption WriteOptions are cached per (compression, encryption, cipher mode) so the KDF runs once per combination instead of per entry.
b5a7c57 to
30a8fe9
Compare
Summary
Add Cipher mode 2 (AEAD: AES-256-GCM / Camellia-256-GCM with a STREAM-based segmented layout) to libpna. Stream keys are derived per datastream via HKDF-SHA-256 from the KDF output and bound to the FHED/SHED and PHSF chunk bytes.
Notes
AeadError::{Malformed, AuthenticationFailure, Truncation}, recoverable by downcasting fromio::Error. A wrong password and tampering are intentionally indistinguishable.Unsupported.CipherModegains aGCMvariant (breaking for exhaustive matches; wire value 2 now parses asGCMinstead ofReserved(2)).NormalEntry::with_namemakes its data undecryptable (documented on the method).Summary by CodeRabbit
AeadErrorto improve visibility into AEAD decryption failures (authentication failure, malformed data, truncation).