Skip to content

✨ Add Cipher mode 2 (AEAD, STREAM-based GCM) support to libpna#3192

Open
ChanTsune wants to merge 13 commits into
mainfrom
lib/feature/cipher-mode-2-gcm
Open

✨ Add Cipher mode 2 (AEAD, STREAM-based GCM) support to libpna#3192
ChanTsune wants to merge 13 commits into
mainfrom
lib/feature/cipher-mode-2-gcm

Conversation

@ChanTsune

@ChanTsune ChanTsune commented Jul 11, 2026

Copy link
Copy Markdown
Owner

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

  • Decryption failures are classified as AeadError::{Malformed, AuthenticationFailure, Truncation}, recoverable by downcasting from io::Error. A wrong password and tampering are intentionally indistinguishable.
  • Fixes an existing bug where unknown cipher modes were silently decrypted as Camellia-CTR; they now fail with Unsupported.
  • CipherMode gains a GCM variant (breaking for exhaustive matches; wire value 2 now parses as GCM instead of Reserved(2)).
  • Because the FHED bytes are bound into the stream key, renaming a GCM entry via NormalEntry::with_name makes its data undecryptable (documented on the method).
  • GCM with default Argon2id parameters uses the spec profile (m=64 MiB, t=3, p=4).
  • CBC/CTR behavior is unchanged. The CLI does not expose GCM yet (follow-up PR).

Summary by CodeRabbit

  • New Features
    • Added authenticated GCM STREAM encryption/decryption (AES-256 and Camellia-256), supporting both per-entry and solid archives.
    • Introduced GCM cipher-mode selection with configurable datastream segment sizing.
    • Exposed AeadError to improve visibility into AEAD decryption failures (authentication failure, malformed data, truncation).
  • Bug Fixes
    • Improved encrypted-entry rename behavior for cipher mode 2: renames are re-encrypted when a password is provided; authentication failures are enforced for invalid/tampered cases.
  • Tests
    • Expanded GCM and CLI encrypted-rename coverage, including password/no-password, tampering, and truncation scenarios.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file lib This issue is about lib crate labels Jul 11, 2026
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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.

Changes

GCM STREAM archive encryption

Layer / File(s) Summary
GCM format, key derivation, and AEAD errors
lib/Cargo.toml, lib/src/cipher/aead.rs, lib/src/error.rs, lib/src/lib.rs
Adds GCM dependencies, stream headers, HKDF-SHA256 derivation, segment nonces, and public AEAD error conversion and re-export.
Segmented GCM encryption and decryption
lib/src/cipher/gcm.rs
Implements segmented AES-GCM and Camellia-GCM writers/readers with authentication, truncation, malformed-input, and persistent-error handling.
GCM options and cipher dispatch
lib/src/entry/options.rs, lib/src/cipher.rs
Adds GCM mode serialization, configurable segment sizes, GCM KDF profile selection, and cipher enum dispatch.
Archive GCM writer integration
lib/src/entry/write.rs, lib/src/archive/write.rs, lib/src/entry/builder.rs
Derives stream keys from header context and stream kind, emits stream headers, and wires GCM writers into per-entry and solid archive construction.
Entry header preservation and archive decryption
lib/src/entry.rs, lib/src/entry/read.rs
Retains raw SHED/FHED bytes, binds decryption to headers and stream kind, parses GCM headers, and defines GCM rename behavior.
Archive validation and encrypted rename workflows
lib/src/archive.rs, cli/src/command/core.rs, cli/tests/cli/stdio/*
Tests round trips, tampering, truncation, malformed streams, and CLI copying and renaming of encrypted entries.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding cipher mode 2 GCM/AEAD support to libpna.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lib/feature/cipher-mode-2-gcm

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the break API braking change label Jul 11, 2026
Comment thread lib/src/cipher/aead.rs Dismissed
Comment thread lib/src/cipher/aead.rs Dismissed
Comment thread lib/src/cipher/aead.rs Dismissed
Comment thread lib/src/cipher/aead.rs Dismissed
Comment thread lib/src/cipher/aead.rs Dismissed
Comment thread lib/src/cipher/aead.rs Dismissed
Comment thread lib/src/cipher/aead.rs Dismissed
Comment thread lib/src/entry/read.rs Fixed
Comment thread lib/src/entry/write.rs Fixed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread lib/src/cipher/gcm.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 lift

Preserve with_name for borrowed entry types.

The new T: From<Vec<u8>> bound removes this public method from NormalEntry<&[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

📥 Commits

Reviewing files that changed from the base of the PR and between 1e00ea9 and f7d2ffe.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • lib/Cargo.toml
  • lib/src/archive.rs
  • lib/src/archive/write.rs
  • lib/src/cipher.rs
  • lib/src/cipher/aead.rs
  • lib/src/cipher/gcm.rs
  • lib/src/entry.rs
  • lib/src/entry/builder.rs
  • lib/src/entry/options.rs
  • lib/src/entry/read.rs
  • lib/src/entry/write.rs
  • lib/src/error.rs
  • lib/src/lib.rs

Comment thread lib/src/cipher/gcm.rs
Comment thread lib/src/entry.rs
Comment thread lib/src/entry/read.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8f14912 and d9bc341.

📒 Files selected for processing (8)
  • cli/src/command/core.rs
  • cli/tests/cli/stdio.rs
  • cli/tests/cli/stdio/encrypted_rename.rs
  • lib/src/archive.rs
  • lib/src/cipher/gcm.rs
  • lib/src/entry.rs
  • lib/src/entry/write.rs
  • lib/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

Comment thread cli/src/command/core.rs
Comment thread cli/tests/cli/bsdtar/encrypted_rename.rs Fixed
Comment thread cli/src/command/core.rs Dismissed
Comment thread cli/src/command/core.rs Dismissed
Comment thread lib/src/entry/write.rs Dismissed
@ChanTsune
ChanTsune force-pushed the lib/feature/cipher-mode-2-gcm branch from 9607f5b to fe488f4 Compare July 16, 2026 08:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
cli/src/command/core.rs (1)

2070-2084: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject 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

📥 Commits

Reviewing files that changed from the base of the PR and between 954aec0 and fe488f4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • cli/src/command/core.rs
  • cli/tests/cli/stdio.rs
  • cli/tests/cli/stdio/encrypted_rename.rs
  • lib/Cargo.toml
  • lib/src/archive.rs
  • lib/src/archive/write.rs
  • lib/src/cipher.rs
  • lib/src/cipher/aead.rs
  • lib/src/cipher/gcm.rs
  • lib/src/entry.rs
  • lib/src/entry/builder.rs
  • lib/src/entry/options.rs
  • lib/src/entry/read.rs
  • lib/src/entry/write.rs
  • lib/src/error.rs
  • lib/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

Comment thread lib/src/entry/write.rs
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.
@ChanTsune
ChanTsune force-pushed the lib/feature/cipher-mode-2-gcm branch from b5a7c57 to 30a8fe9 Compare July 18, 2026 06:51
Comment thread cli/tests/cli/bsdtar/encrypted_rename.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

break API braking change cli This issue is about cli application dependencies Pull requests that update a dependency file lib This issue is about lib crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants