Draft
Conversation
97bf0cf to
b5da725
Compare
* feat(bordercrypt): add constants, types, and error foundations Scaffold the bordercrypt crate with size constants derived from crypto-aead and pq-rerand, a validated SessionIndex newtype, and the unified BordercryptError enum. * feat(bordercrypt): add domain separation strings Unique labels for every KDF/AEAD context. Block-level functions write into a caller-provided buffer to avoid per-block allocations. * feat(bordercrypt): add BlockStorage/KeypairStorage traits and MemoryStorage backend * feat(bordercrypt): add FsStorage backend for native integration tests * fix(bordercrypt): pin pq-rerand, validate block file size, add missing tests - Pin pq-rerand to exact git rev - Validate block file size is multiple of BLOCK_SIZE - Add const assertion SessionIndex fits SESSION_COUNT - Use SESSION_COUNT constant in test instead of hard-coded 5 - Zeroize keypair bytes on read (read_keypair returns Zeroizing<Vec<u8>>) - Add zeroize dependency
* feat(bordercrypt): add BlockStorage/KeypairStorage traits and MemoryStorage backend * feat(bordercrypt): add FsStorage backend for native integration tests * feat(bordercrypt): add KDF and pq-rerand crypto wrappers - Add derive_block_aead_key (HKDF-SHA256 per-block key derivation) - Add pq-rerand wrapper: keygen, encrypt, decrypt, rerand - Add PqPublicKey/PqSecretKey with serialization - Zeroize all sensitive intermediates and return types - PqSecretKey: manual Drop zeroize, no Debug/Clone - Compile-time assert PQ_CT_SIZE == BLOCK_SIZE - 14 new tests (5 KDF + 9 pq-rerand) * feat(bordercrypt): use compile-time NTT tables Replace 4 runtime NttContext::new() calls with a single const NTT_CTX. Point pq-rerand dep to local clone pending upstream merge. * refactor(bordercrypt): delegate key serialization to pq-rerand Bump pq-rerand to 1d5f9d1 which adds ZeroizeOnDrop on SecretKey and to_bytes/from_bytes on both key types. Remove the manual Drop impl, PK_BYTE_SIZE/SK_BYTE_SIZE constants, and field-by-field serialization loops from pq.rs — the newtypes now forward to upstream methods. * test(bordercrypt): add different_domains KDF test Addresses Copilot review comment on PR #432 — verifies that different domain strings produce different derived keys. * chore(bordercrypt): clean up redundant doc comments in pq.rs Remove size constraint comments (enforced by type signatures), remove "does not implement Debug/Clone" comment (compiler enforces), add OsRng justification comment. * chore(bordercrypt): remove design justification comments Strip inline doc comments that explain design rationale rather than documenting API behavior. Design decisions belong in spec docs. * chore(bordercrypt): add #[must_use] to pq free functions pq_keygen, pq_encrypt, pq_decrypt, pq_rerand are all pure functions whose results should never be silently discarded. * fix(bordercrypt): restore security doc comment on BordercryptError * fix(bordercrypt): add error message to compile-time size assertion * fix(bordercrypt): add explicit getrandom/js dep for WASM portability OsRng delegates to getrandom internally, which needs the `js` feature on wasm32 to use crypto.getRandomValues. Previously this worked via the transitive crypto-aead dependency; make it explicit so bordercrypt is self-contained.
Binary format: [version: u32 BE] [pq_pk] [sk_nonce: 16B] [sk_ct: remaining] with serialize/deserialize and storage helpers.
Double encryption: AEAD (AES-SIV) for integrity + pq-rerand for post-quantum deniability. Cover blocks use throwaway AEAD keys.
Password → Argon2id → HKDF → try AEAD-unwrap each session's secret key in random order. Reads total_data_length from block 0 on success.
- Add Zeroize + ZeroizeOnDrop to UnlockedSession, PqSecretKey, SessionIndex - Add Zeroize impl for PqPublicKey (needed for UnlockedSession derive) - Use let-else pattern for fallible operations in unlock loop - Move sk_wrap_aead_key construction outside the loop - Use `for i in indices` instead of `for &i in &indices` - Add ROOT_BLOCK_KEY_SIZE constant (spec alignment) - Add TODO for decrypt_session_data_block spec gap
…try-all-sessions - derive_block_aead_key returns (key, block_scope) tuple, internalizes buffer - encrypt_block/decrypt_block/create_cover_block take aad_root (block scope) and append :block_aead internally - Add ZeroizeOnDrop to PqPublicKey (manual Drop + ZeroizeOnDrop impl) - Extract decrypt_session_data_block with version check - unlock_session tries all slots even after match (timing protection) - Clippy fix: deref in MemoryStorage::write_block
…t BLOCK_AEAD_SUFFIX const - Inline decrypt_session_data_block into read_total_length in unlock.rs (the only caller), removing the duplicate that will exist in read.rs - Add empty blockstream guard (return 0) for freshly allocated sessions - Extract ":block_aead" suffix as BLOCK_AEAD_SUFFIX const in block.rs
Implements decrypt_session_data_block, read_total_length, and read_session_data with cross-block pread semantics.
- Introduced read_total_length_raw to handle decryption of block 0 and version checks. - Updated unlock_session to utilize read_total_length_raw, improving clarity and reducing redundancy. - Adjusted slice calculations in read_session_data for better handling of block boundaries. - Ensured compatibility with freshly allocated sessions by returning 0 when no blocks exist.
- Merge read_total_length_raw + read_total_length into single function with block_count == 0 check inside (removes wrapper indirection) - Remove if/else guard in unlock.rs — handled by read_total_length now - Rename aead_key → aead_sk to match spec variable name - Rewrite slice computation with block_end, take_start, take_end for 1-to-1 mapping to spec §10.2
The wasm-bordercrypt generated package.json creates a package boundary inside the source tree, causing Vite to resolve React from the wrong location in browser tests. resolve.dedupe forces a single React instance.
* ci: increase test thread stack to 16 MiB for pq-rerand NTT operations * feat(bordercrypt): add shrink_session_data (spec §14) Shrink session data to a new total, converting freed blocks to cover blocks indistinguishable from blocks allocated by other sessions. All touched block indices are updated across ALL sessions in randomized order for snapshot resistance. * refactor(bordercrypt): align shrink with spec — block_scope, Zeroizing coercion - Fix shrink_session_data: use block_scope instead of block_aead_aad for cover blocks (create_cover_block appends :block_aead internally) - Fix Zeroizing<[u8; N]> coercion: use .as_ref() for copy_from_slice * refactor(bordercrypt): align write path with spec — timing uniformity, naming - Compute read_session_version_and_pk and block_scope unconditionally for all sessions in encrypt/extend/shrink loops (spec §11.2, §12.3, §14.3) - Rename end_pos → end_pos_excl to mirror spec variable names - Remove shrink new_total==0 special case — always try decrypt first per spec §14.3 - Deduplicate read_total_length into shared pub(crate) read_total_length_raw - Use .saturating_sub() / .min() for idiomatic slice bounds * refactor(bordercrypt): align write.rs variable names with spec Rename variables to match spec naming conventions: - aead_key → aead_sk (§11.2, §12.3) - buf → cur_aad_root (§11.2, §12.2, §12.3, §14.3) - existing_ct → cur_ct (§11.2, §14.3) - block_start/block_end → block_start_pos/block_end_pos (§13.2) - for &i in &indices → for i in indices (Rust best practices) Also update read_total_length call sites in tests to match the unified 6-arg signature from the read-path refactor. * chores(bordercrypt): cargo fmt * fix(bordercrypt): handle empty writes that extend logical length (spec §13.2) The data.is_empty() early return was a spec deviation: calling write_session_data(offset=1000, data=b"") should extend total_data_length to 1000, allocate blocks, and update the block 0 length header. The naive early return skipped all of that. Split into two cases: - empty && new_total == old_total: true no-op - empty && new_total > old_total: allocate blocks, update header * fix(bordercrypt): address PR #467 review — spec alignment, expect messages, edge cases - .expect() messages now describe what went wrong, not what was expected (10 instances) - Add spec comment: "Genuine block content is random padding; header is set only for block 0." - Add atomicity note to encrypt_session_data_block doc comment - Add assert!(w_start < w_end) per spec - Empty writes are now always no-ops (remove ftruncate semantics not in spec) - Add 5 boundary edge-case tests for required_last_block calculation
* feat(bordercrypt): add lifecycle and cover traffic Implements provision_storage (non-unlockable slots), allocate_session (password-protected slot creation), and cover_traffic_tick (periodic rerandomization for activity masking). * refactor(bordercrypt): align lifecycle with spec — block_scope, field names, empty session - Fix cover_traffic_tick: use block_scope instead of block_aead_aad (create_cover_block appends :block_aead internally) - Fix allocate_session: use pq_rerand_pk/pq_rerand_sk field names - Handle freshly allocated sessions with no blocks in read_total_length * fix(bordercrypt): timing uniformity and style fixes in lifecycle.rs - Hoist block_scope before match in cover_traffic_tick (spec §15 requires unconditional computation for timing uniformity) - Replace .expect() with ? for SessionIndex::new in prod code - Use `for i in indices` instead of `for &i in &indices` (Copy types) - Wrap two_sessions_different_passwords test in larger stack thread * refactor(bordercrypt): align cover_traffic_tick variable names with spec Rename buf → cur_aad_root and existing_ct → cur_ct to mirror §15 pseudocode variable names. * chores(bordercrypt): cargo fmt * fix(bordercrypt): zeroize serialized secret key in allocate_session Wrap pq_rerand_sk.to_bytes() in Zeroizing<> to prevent serialized secret key bytes lingering in freed heap memory. * fix(bordercrypt): use random bytes for provisioned sk_ct Replace AEAD encrypt under throwaway key with pure random bytes for sk_nonce and sk_ct in provision_storage. Add init_blockstream() to BlockStorage trait for explicit blockstream creation at provisioning. * Update wasm/bordercrypt/src/storage.rs Co-authored-by: Soulthym <thybault.alabarbe@gmail.com> * fix(bordercrypt): use AEAD encryption for provisioned sk_ct, not random bytes Pure random bytes are structurally distinguishable from real AEAD ciphertext (different size due to auth tag), breaking plausible deniability. Use actual AEAD encryption under an ephemeral throwaway key so decoy slots are indistinguishable from allocated ones. Reverts the approach from d1670b9 while keeping init_blockstream. * refactor(bordercrypt): extract derive_session_keys and KeypairFile::build_wrapped helpers Extract duplicated password KDF flow into kdf::derive_session_keys() and AEAD wrapping into KeypairFile::build_wrapped(), reducing ~110 lines of repeated crypto boilerplate across lifecycle, unlock, and test helpers. Also fix pre-existing test stack overflows from ML-KEM by adding run_with_stack() helper uniformly across all PQ-heavy tests. --------- Co-authored-by: Soulthym <thybault.alabarbe@gmail.com>
9 end-to-end scenarios: happy path, session isolation, snapshot resistance, cover traffic preservation, corruption healing, blockstream alignment, brute force resistance, and re-unlock.
Add run() helper that spawns every test on a 4 MiB thread, fixing stack overflow on the default thread stack from PQ crypto operations.
- keypair file sizes indistinguishable (provisioned vs allocated) - re-allocating a slot invalidates the old password - shrink then grow past original size - all SESSION_COUNT slots allocated and independently readable - cover traffic touches all sessions (50 ticks, not just "at least one")
b5da725 to
3df21d4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.