Internet-Draft MFPK-ENC-V6 Format Intended status: Informational Expires: TBD Encrypted Multi-File Container Format (Streaming Binary V6) Abstract This document specifies the MFPK-ENC-V6 streaming encrypted multi- file container format. V6 is a complete redesign based on Rogaway et al.'s STREAM construction for nonce-based online authenticated encryption (nOAE), with per-entry key isolation via the SE3 construction (Hoang & Shen, CCS 2020). The format provides segment-by-segment authenticated encryption, resistance to chunk reordering and truncation, random-access decryption, and resistance to randomness failure. The password-to-key step uses Argon2id (RFC 9106) for memory-hard brute-force resistance. Per-entry subkey and nonce mask derivation use BLAKE3 keyed hashing as a fast PRF over the already-strong master key. V6 is NOT compatible with any previous version. Status of This Memo This document is not an IETF standard. It is published for informational purposes. Copyright Notice Copyright (c) 2025. All rights reserved. Table of Contents 1. Introduction 2. Notational Conventions 3. Cryptographic Primitives 4. Container Overview 5. Global Header 6. Nonce Structure and Generation 7. Key Derivation 8. Entry Record Structure 9. Segment-Based Encryption (STREAM / SE3) 10. File Content Segmentation 11. Paths and Metadata 12. Root Directory Entry 13. Indexing and Random-Access Decryption 14. Password Verification 15. Security Considerations 16. IANA Considerations 17. Versioning 18. Constants Summary 19. Migration from V5 20. Implementation Guidance References 1. Introduction MFPK-ENC-V6 is a streaming authenticated encryption container format for storing files and directories. Compared to V5, V6: - Implements Rogaway et al.'s STREAM construction for nOAE security, preventing chunk reordering, deletion, and truncation attacks. - Uses the SE3 construction (Hoang & Shen) for per-entry subkey and nonce mask derivation, providing misuse-resistance against CSPRNG failure. - Provides per-entry key isolation: each entry is encrypted under an independent subkey derived from the master key and a fresh random R. - Authenticates each segment independently with 128-bit Poly1305 tags. - Supports random-access decryption of any individual segment. - Uses ChaCha20-Poly1305 (RFC 8439) as the AEAD primitive. - Uses Argon2id (RFC 9106) for password-to-master-key derivation, providing strong resistance against GPU/ASIC brute-force attacks. - Uses BLAKE3 (keyed-hash mode) as a fast PRF for per-entry subkey and nonce mask derivation from the already-strong master key. - Eliminates BASE_PATH (redundant with FULL_PATH) present in V5. - Eliminates need for full-file buffering during decryption. V6 is a breaking change and is NOT compatible with V5 or earlier. The two-layer KDF design follows the security layering validated by Hoang & Shen: Argon2id is the memory-hard password-hardening step executed once at container open; BLAKE3 is the fast PRF executed per entry over the secret master key, not over the password. An attacker cannot reach BLAKE3 without first inverting Argon2id. 2. Notational Conventions The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. All multi-byte integers are little-endian unless otherwise specified. Notation: - || denotes concatenation - ⊕ denotes XOR - [n] denotes an n-byte sequence - AEAD.Encrypt(K, N, A, P) denotes authenticated encryption - AEAD.Decrypt(K, N, A, C) denotes authenticated decryption - ⌈x⌉ denotes the ceiling function 3. Cryptographic Primitives 3.1. AEAD Cipher: ChaCha20-Poly1305 (RFC 8439) * Nonce length: 12 bytes (96 bits) * Key length: 32 bytes (256 bits) * Tag length: 16 bytes (128 bits) * AAD: per-segment metadata (see Section 9.2) 3.2. Password KDF: Argon2id (RFC 9106) * Salt length: 32 bytes (256 bits) * Output length: 32 bytes (256 bits) * Iterations (t): stored in header as uint8 * Memory (m): stored in header as uint32 (KiB) * Parallelism (p): stored in header as uint8 * Password encoding: UTF-8 KDF parameters are written into the global header at container creation and read back verbatim on open. A reader MUST use the parameters from the header, not any compiled-in defaults. Recommended defaults for new containers: t=3, m=65536, p=4. 3.3. Per-Entry KDF: BLAKE3 (keyed-hash mode) * Key: 32-byte master_key (output of Argon2id) * Input: "MFPK-V6-SUBKEY-MASK" || R (domain-separated) * Output: 39 bytes (32-byte subkey || 7-byte mask_X) BLAKE3 is used here as a fast PRF over a secret key, not over a password. Its speed is appropriate because brute-force resistance is already provided by Argon2id at the master key layer. 3.4. Randomness All salts, master nonces R, and nonce prefixes P MUST be generated using a cryptographically secure random number generator (CSPRNG). 4. Container Overview A V6 container consists of: 1. Global Header (82 bytes, fixed) - Magic and version identifier - 32-byte Argon2id salt - Argon2id parameters (t, m, p) - Password verification structure 2. Entry Records (variable, sequential) - Each entry begins with SYNC_WORD for resynchronization - Entry header with per-entry nonce material and length fields - Segmented encrypted content (files only) The format supports: - Streaming encryption with O(1) memory (one segment buffer) - Random-access decryption of any segment - Segment-by-segment authentication - Append-only writes - Removal via rewrite-to-new-file 5. Global Header (fixed size: 82 bytes) Offset Size Description ------ ---- --------------------------------------------------------- 0 4 MAGIC_VERSION = 0x89 'M' 'F' 0x06 (bytes: 89 4D 46 06) 4 32 MASTER_SALT (32 bytes, CSPRNG, input to Argon2id) 36 1 ARGON2_T (uint8, Argon2id time cost / iterations) 37 4 ARGON2_M (uint32 LE, Argon2id memory cost in KiB) 41 1 ARGON2_P (uint8, Argon2id parallelism) 42 12 PWV_NONCE (ChaCha20-Poly1305 12-byte nonce) 54 28 PWV_CIPHERTEXT (12-byte plaintext + 16-byte tag) Password Verification: - PWV_MARKER = "MFPKV6-MARK!" (12 bytes ASCII) - Encrypted with the derived master_key and empty AAD. - Successful decryption verifies password correctness. - The marker is NEVER stored in plaintext. Total header size: 4 + 32 + 1 + 4 + 1 + 12 + 28 = 82 bytes. Note on MASTER_SALT size: 32 bytes (256 bits) satisfies RFC 9106's recommended minimum salt length for Argon2id. Note on KDF parameters: writers MUST record the exact parameters used at creation time. Readers MUST use the stored values and MUST NOT substitute compiled-in defaults. This allows parameters to be upgraded in future containers without a format version bump. 6. Nonce Structure and Generation (SE3 Construction) V6 uses the SE3 two-level nonce structure from Hoang & Shen to provide misuse-resistance against CSPRNG failure. 6.1. Master Nonce (R): 16 bytes Generated once per entry using CSPRNG. Input to per-entry KDF. R uniquely identifies each entry's cryptographic context. 6.2. Nonce Prefix (P): 7 bytes Generated once per entry using CSPRNG. Combined with derived mask X to produce effective prefix P*. 6.3. Effective Nonce Prefix (P*): 7 bytes P* = P ⊕ X where X is the 7-byte mask from BLAKE3 keyed-hash derivation. Even if P is constant (total CSPRNG failure), P* remains unique per entry because X is derived from the unique R value. 6.4. Segment Nonce: 12 bytes nonce_i = P*[0:4] || LE64(segment_index) - Bytes 0–3: first 4 bytes of P* (entry-unique prefix) - Bytes 4–11: 8-byte little-endian uint64 segment_index The last_flag is NOT encoded in the nonce; it is authenticated via the AAD (see Section 9.2). This is consistent with SE3 as analyzed in Theorem 4 of Hoang & Shen — the flag need only be authenticated, not encoded in the nonce itself. 6.5. Nonce Uniqueness Guarantee - Each entry has a unique (R, P) pair from CSPRNG. - Each segment within an entry has a unique segment_index. - P* uniqueness is guaranteed even under complete P failure. - Per-entry subkeys ensure cross-entry isolation. 7. Key Derivation 7.1. Master Key Derivation (Argon2id — slow, once per open) Input: - password: UTF-8 encoded user password - MASTER_SALT: 32-byte salt from global header - ARGON2_T: time cost from global header - ARGON2_M: memory cost (KiB) from global header - ARGON2_P: parallelism from global header Process: master_key = Argon2id( password = password_utf8, salt = MASTER_SALT, t = ARGON2_T, m = ARGON2_M, p = ARGON2_P, length = 32 ) Output: 32-byte master_key This step is intentionally slow to resist offline dictionary attacks. An attacker must execute this step for every guess, at a cost determined by the stored parameters. 7.2. Per-Entry Subkey and Mask Derivation (BLAKE3 — fast, per entry) Input: - master_key: 32-byte key from step 7.1 - R: 16-byte master nonce from entry header Process: derived = BLAKE3.keyed_hash( key = master_key, data = "MFPK-V6-SUBKEY-MASK" || R ) subkey = derived[0:32] // 32 bytes mask_X = derived[32:39] // 7 bytes Output: - subkey: 32-byte ChaCha20-Poly1305 key for this entry - mask_X: 7-byte nonce mask for this entry BLAKE3 operates as a PRF keyed by master_key. Speed is acceptable here because master_key has full 256-bit entropy (an attacker cannot brute-force BLAKE3 without master_key, and master_key requires Argon2id to obtain). 7.3. Effective Nonce Prefix P* = P ⊕ mask_X where P is the 7-byte CSPRNG-generated nonce prefix from the entry header, and mask_X is from step 7.2. 7.4. Security Layer Summary [password] --Argon2id--> [master_key] (brute-force wall) [master_key, R] --BLAKE3--> [subkey, mask_X] (fast PRF) [subkey, P*, counter, last_flag] --ChaCha20-Poly1305--> [ciphertext] 8. Entry Record Structure Each entry begins with SYNC_WORD for resilience and recovery. Constants: - SYNC_WORD = 0xA6 'S' 'T' 'R' (bytes: A6 53 54 52) - ENTRY_TYPE_FILE = 0x00 - ENTRY_TYPE_DIR = 0x01 - SEGMENT_SIZE = 65536 bytes (64 KiB) 8.1. Entry Header Layout (44 bytes fixed, then variable fields) Offset Size Field ------ ---- ------------------------------------------------------- 0 4 SYNC_WORD (A6 53 54 52) 4 1 ENTRY_TYPE (0x00=file, 0x01=directory) 5 16 MASTER_NONCE_R (random, input to per-entry KDF) 21 7 NONCE_PREFIX_P (random, XORed with mask_X to form P*) 28 8 FILE_SIZE (uint64 LE, logical plaintext bytes) Directories MUST have FILE_SIZE = 0. 36 4 NUM_SEGMENTS (uint32 LE, number of content segments) Directories MUST have NUM_SEGMENTS = 0. NUM_SEGMENTS = ⌈FILE_SIZE / SEGMENT_SIZE⌉ for files. Empty files (FILE_SIZE = 0) have NUM_SEGMENTS = 0. 40 2 FULLPATH_LEN (uint16 LE, length of ENCRYPTED_FULLPATH) 42 2 TIMESTAMP_LEN (uint16 LE, length of ENCRYPTED_TIMESTAMP) Directories MUST have TIMESTAMP_LEN = 0. 44 N1 ENCRYPTED_FULLPATH 44+N1 N2 ENCRYPTED_TIMESTAMP (absent for directories, N2=0) 44+N1+N2 ... SEGMENTS (files only) Notes: - FILE_SIZE is stored in plaintext. This is a known metadata leak (see Section 15.5). It is required for streaming skip support. - NUM_SEGMENTS is derivable from FILE_SIZE and SEGMENT_SIZE. It is stored explicitly for reader convenience and for skip calculations that do not require recomputing the ceiling. 8.2. Metadata Encryption (FULLPATH and TIMESTAMP) FULLPATH and TIMESTAMP are encrypted as special segments with segment_index = 0, distinguished by different last_flag values: - FULLPATH: segment_index = 0, last_flag = 0x02 - TIMESTAMP: segment_index = 0, last_flag = 0x03 Both use FILE_SIZE = 0 in their AAD (see Section 9.2). Format: nonce (12 bytes) || ciphertext || tag (16 bytes) The nonce is deterministic: P*[0:4] || LE64(0) Both FULLPATH and TIMESTAMP share segment_index=0 but differ in last_flag, which is authenticated in AAD. Combined with per-entry subkeys, cross-entry transplantation is prevented. 9. Segment-Based Encryption (STREAM / SE3) V6 implements the STREAM construction where each segment is independently encrypted and authenticated, preventing reordering, deletion, and truncation. 9.1. Segment Nonce Construction For segment i (1-indexed for content, 0 for metadata): nonce_i = P*[0:4] || LE64(i) The nonce encodes the segment counter directly, ensuring that any reordering is detected by nonce verification. 9.2. Per-Segment AAD AAD_i = entry_type (1 byte) || LE64(segment_index) (8 bytes) || last_flag (1 byte) || LE64(file_size) (8 bytes) Total AAD: 18 bytes per segment. Fields: - entry_type: 0x00 for file, 0x01 for directory metadata - segment_index: uint64 LE (matches counter in nonce) - last_flag: 0x00 non-final, 0x01 final content segment, 0x02 FULLPATH metadata, 0x03 TIMESTAMP metadata - file_size: logical plaintext file size (0 for metadata) The AAD binds each segment to: - Its position (segment_index prevents reordering) - Whether it is the last segment (last_flag prevents truncation) - The total file size (prevents cross-file segment transplant) - The entry type (prevents type confusion) Cross-entry transplantation is further prevented by per-entry subkeys derived from unique R values. 9.3. Encryption Process For segment i with plaintext P_i: nonce_i = P*[0:4] || LE64(i) AAD_i = entry_type || LE64(i) || last_flag || LE64(file_size) C_i = ChaCha20Poly1305.Encrypt(subkey, nonce_i, AAD_i, P_i) Stored on disk as: nonce_i (12 bytes) || C_i (plaintext_len + 16) 9.4. Decryption and Verification For each segment during decryption: 1. Read nonce (12 bytes) from disk. 2. Reconstruct expected nonce from P* and segment_index. 3. Verify stored nonce == expected nonce; abort on mismatch. 4. Reconstruct AAD from known fields. 5. Decrypt; abort immediately on authentication failure. 6. Release plaintext only after tag verification. 9.5. Segment Layout on Disk SEGMENT_RECORD = NONCE (12) || CIPHERTEXT_WITH_TAG (≤ 65536 + 16) Per-segment overhead: 28 bytes (12 nonce + 16 tag) 10. File Content Segmentation 10.1. Segment Size SEGMENT_SIZE = 65536 bytes (64 KiB) 10.2. Segmentation NUM_SEGMENTS = ⌈FILE_SIZE / SEGMENT_SIZE⌉ (0 for empty files) For i = 1 to NUM_SEGMENTS: if i < NUM_SEGMENTS: segment_plaintext_size = SEGMENT_SIZE else: segment_plaintext_size = FILE_SIZE mod SEGMENT_SIZE (or SEGMENT_SIZE if FILE_SIZE is a multiple) Segment i is FINAL (last_flag = 0x01) iff i == NUM_SEGMENTS. All other content segments use last_flag = 0x00. 10.3. Encrypted Segment Size encrypted_size(segment) = 12 + plaintext_size + 16 10.4. Total Encrypted Content Size For file of size S with N = NUM_SEGMENTS: total_encrypted = N * 28 + S 10.5. Skip Calculation (no decryption required) To skip past entry content without decryption: content_end = content_start + NUM_SEGMENTS * 28 + FILE_SIZE Where content_start is the byte offset immediately after ENCRYPTED_TIMESTAMP (or ENCRYPTED_FULLPATH for directories), i.e. entry_offset + 44 + FULLPATH_LEN + TIMESTAMP_LEN. 11. Paths and Metadata 11.1. FULLPATH Format - POSIX-style absolute path, e.g., "/dir/subdir/file.txt" - UTF-8 encoded - Maximum 4096 bytes (enforced: FULLPATH_LEN is uint16, and implementations MUST reject paths exceeding 4096 bytes) - BASE_PATH is NOT stored; it is always derivable as dirname(FULLPATH). Implementations SHOULD verify consistency. 11.2. Timestamp Format (files only) Plaintext: 8-byte little-endian IEEE-754 float64, Unix mtime. Encrypted using: - subkey derived from (master_key, R) - segment_index = 0, last_flag = 0x03 - AAD = 0x00 || LE64(0) || 0x03 || LE64(0) 11.3. Directory Entries - ENTRY_TYPE = 0x01 - FILE_SIZE = 0 - NUM_SEGMENTS = 0 - TIMESTAMP_LEN = 0 - No segment data after ENCRYPTED_FULLPATH 12. Root Directory Entry A well-formed container MUST include an explicit root directory entry as the first entry after the global header: - ENTRY_TYPE = 0x01 (directory) - FULLPATH = "/" - FILE_SIZE = 0 - NUM_SEGMENTS = 0 - TIMESTAMP_LEN = 0 13. Indexing and Random-Access Decryption 13.1. Sequential Indexing To build a complete index: 1. Read global header (82 bytes), verify MAGIC_VERSION. 2. Read ARGON2_T, ARGON2_M, ARGON2_P from header. 3. Derive master_key via Argon2id with stored parameters (Section 7.1). 4. Verify password via PWV_NONCE and PWV_CIPHERTEXT (Section 14). 5. Position at offset 82. 6. For each entry: a. Read and verify SYNC_WORD. b. Read fixed 40 bytes of entry header fields. c. Derive subkey and mask_X from master_key and R (Section 7.2). d. Compute P* = P ⊕ mask_X. e. Decrypt and verify ENCRYPTED_FULLPATH. f. Decrypt and verify ENCRYPTED_TIMESTAMP (files only). g. Record entry offset and metadata. h. Advance position: offset += 44 + FULLPATH_LEN + TIMESTAMP_LEN if FILE: offset += NUM_SEGMENTS * 28 + FILE_SIZE 13.2. Random-Access Decryption To decrypt segment i of a known file entry: 1. Retrieve entry metadata from index. 2. Derive subkey and mask_X from master_key and R. 3. Compute P* = P ⊕ mask_X. 4. Compute segment file offset: content_start = entry_offset + 44 + FULLPATH_LEN + TIMESTAMP_LEN offset = content_start + (i - 1) * (28 + SEGMENT_SIZE) Note: the final segment may be smaller than SEGMENT_SIZE. 5. Read nonce (12), ciphertext + tag from offset. 6. Verify stored nonce == P*[0:4] || LE64(i). 7. Construct AAD_i. 8. Decrypt: plaintext = ChaCha20Poly1305.Decrypt(subkey, nonce, AAD_i, C_i) 13.3. Resynchronization If entry parsing fails (bad ENTRY_TYPE, decryption failure, length overflow): - Scan forward for next SYNC_WORD occurrence. - Resume parsing from that position. - Mark skipped region as corrupted in index. 14. Password Verification 14.1. Verification Process 1. Read MASTER_SALT from header (offset 4, 32 bytes). 2. Read ARGON2_T (offset 36, 1 byte), ARGON2_M (offset 37, 4 bytes LE), ARGON2_P (offset 41, 1 byte). 3. Derive master_key using Argon2id with stored parameters (Section 7.1). 4. Read PWV_NONCE (offset 42, 12 bytes). 5. Read PWV_CIPHERTEXT (offset 54, 28 bytes). 6. Attempt: marker = ChaCha20Poly1305.Decrypt(master_key, PWV_NONCE, b"", PWV_CIPHERTEXT) 7. If decryption succeeds and marker == "MFPKV6-MARK!": correct password. If decryption fails or marker mismatch: incorrect password. 14.2. Security Note The 76-byte global header is sufficient for offline dictionary attacks against the password. Argon2id (64 MiB, t=3) raises the cost of each attempt to ~100ms on typical hardware, providing strong practical resistance. The salt ensures precomputation (rainbow table) attacks are infeasible. 15. Security Considerations 15.1. Nonce Uniqueness and SE3 Construction V6 employs the SE3 construction proven secure by Hoang & Shen: - Master nonce R provides per-entry uniqueness and key isolation. - Nonce prefix P with derived mask X prevents P collisions. - Even if P generation fails (constant P), security is maintained because X is derived from unique R via BLAKE3. - Each entry uses an independent subkey derived from unique R, preventing cross-entry ciphertext transplantation. 15.2. Segment Authentication (STREAM) - Each segment is independently authenticated with a 128-bit tag. - Segment reordering is detected via segment_index in both the nonce and the AAD. - Segment deletion is detected because the final segment is distinguished by last_flag = 0x01 in AAD. - Truncation attacks are prevented because the final segment must authenticate with last_flag = 0x01. - Early authentication failure aborts decryption immediately, releasing no plaintext from unauthenticated segments. 15.3. Key Derivation Security - Argon2id provides memory-hard brute-force resistance at the password layer (64 MiB memory cost per attempt). - BLAKE3 provides 256-bit PRF security at the per-entry layer, operating over the secret master_key (not the password). - Per-entry subkeys provide domain separation between entries. - A 32-byte salt prevents precomputation attacks against Argon2id. 15.4. Metadata Confidentiality - All paths are encrypted; no plaintext path appears in the container. Only ENTRY_TYPE (1 byte) is plaintext per entry. - Timestamps are encrypted for files. - FILE_SIZE and NUM_SEGMENTS are stored in plaintext. This is a known and accepted metadata leak required for streaming skip support (see Section 15.5). 15.5. Known Metadata Leaks The following fields are stored in plaintext and visible to an attacker with read access to the container file: - ENTRY_TYPE: reveals whether an entry is a file or directory. - FILE_SIZE: reveals the exact plaintext size of each file. - NUM_SEGMENTS: conveys the same information as FILE_SIZE. These leaks are inherent in the streaming/skip design and are considered acceptable for this format. Applications requiring full size confidentiality should pad plaintexts before encryption. 15.6. Implementation Requirements Implementations MUST: - Use a CSPRNG for all nonce and salt generation. - Zeroize sensitive key material (master_key, subkey) after use. - Verify authentication tags before releasing any plaintext. - Verify segment nonces match expected values during decryption. - Validate all length fields before allocation or reads. - Enforce the 4096-byte maximum path length. - Verify sequential segment indices during decryption. 16. IANA Considerations - File extension: ".mfpk" (unchanged) - MIME type: application/x-mfpk-v6 - Magic bytes: 89 4D 46 06 17. Versioning - MAGIC_VERSION: 89 4D 46 06 (last byte = 0x06 for V6) - V6 is NOT backward compatible with V5 or earlier. - Implementations MUST reject containers with unexpected magic bytes. 18. Constants Summary MAGIC_VERSION: 89 4D 46 06 SYNC_WORD: A6 53 54 52 HEADER_SIZE: 82 bytes MASTER_SALT_SIZE: 32 bytes (Argon2id salt) ARGON2_T_SIZE: 1 byte (uint8) ARGON2_M_SIZE: 4 bytes (uint32 LE, KiB) ARGON2_P_SIZE: 1 byte (uint8) MASTER_NONCE_SIZE: 16 bytes (R, per-entry) NONCE_PREFIX_SIZE: 7 bytes (P, per-entry) NONCE_MASK_SIZE: 7 bytes (X, derived) SEGMENT_NONCE_SIZE: 12 bytes (ChaCha20-Poly1305) TAG_SIZE: 16 bytes KEY_SIZE: 32 bytes PWV_MARKER: "MFPKV6-MARK!" (12 bytes ASCII) SEGMENT_SIZE: 65536 bytes (64 KiB) SEGMENT_OVERHEAD: 28 bytes (12 nonce + 16 tag) ENTRY_HEADER_FIXED: 44 bytes ENTRY_TYPE_FILE: 0x00 ENTRY_TYPE_DIRECTORY: 0x01 LAST_FLAG_NON_FINAL: 0x00 (non-final content segment) LAST_FLAG_FINAL: 0x01 (final content segment) LAST_FLAG_FULLPATH: 0x02 (FULLPATH metadata segment) LAST_FLAG_TIMESTAMP: 0x03 (TIMESTAMP metadata segment) Recommended Argon2id defaults for new containers: t (iterations): 3 m (memory KiB): 65536 (64 MiB) p (parallelism): 4 output length: 32 bytes (not stored; always 32) 19. Migration from V5 V5 and V6 are completely incompatible. Migration requires: 1. Decrypt entire V5 container using V5 Argon2id key. 2. Extract all files and metadata to secure temporary storage. 3. Create new V6 container with same or new password. 4. Re-encrypt all content using V6 format. 5. Verify integrity of migrated container. 6. Securely delete V5 container and temporary files. Key changes from V5: - AES-256-GCM replaced by ChaCha20-Poly1305. - Argon2id retained but salt size increased from 32 to 32 bytes (unchanged) and header now uses Argon2id correctly at master key layer rather than a fast hash. - Per-file encryption replaced by per-segment STREAM construction. - BASE_PATH field eliminated (derivable from FULLPATH). - No AAD in V5 replaced by full positional AAD in V6. - Header size changed from 72 bytes (V5) to 76 bytes (V6). 20. Implementation Guidance 20.1. Memory Requirements Minimum for encryption/decryption (after key derivation): - 64 KiB plaintext segment buffer - 64 KiB + 28 bytes ciphertext segment buffer - ~256 bytes cryptographic state - Total: ~130 KiB per concurrent operation Argon2id key derivation additionally requires 64 MiB RAM transiently during container open. 20.2. Streaming Encryption Algorithm 1. Choose Argon2id parameters (t, m, p); write to header. 2. Derive master_key via Argon2id(password, MASTER_SALT, t, m, p). 2. For each entry: a. Generate R (16 bytes) and P (7 bytes) from CSPRNG. b. Derive subkey and mask_X: BLAKE3.keyed_hash(master_key, "MFPK-V6-SUBKEY-MASK" || R). c. Compute P* = P ⊕ mask_X. d. Encrypt FULLPATH: segment_index=0, last_flag=0x02. e. Encrypt TIMESTAMP (files): segment_index=0, last_flag=0x03. f. Write entry header with R, P, FILE_SIZE, NUM_SEGMENTS, FULLPATH_LEN, TIMESTAMP_LEN. g. Write ENCRYPTED_FULLPATH and ENCRYPTED_TIMESTAMP. h. For i = 1 to NUM_SEGMENTS: - Read up to SEGMENT_SIZE plaintext bytes. - last_flag = 0x01 if i == NUM_SEGMENTS else 0x00. - nonce_i = P*[0:4] || LE64(i). - AAD_i = 0x00 || LE64(i) || last_flag || LE64(FILE_SIZE). - C_i = ChaCha20Poly1305.Encrypt(subkey, nonce_i, AAD_i, plaintext). - Write nonce_i || C_i. 20.3. Streaming Decryption Algorithm 1. Read Argon2id parameters from header. 2. Derive master_key via Argon2id(password, MASTER_SALT, t, m, p). 3. Verify PWV. 4. Read entry header, extract R, P, FILE_SIZE, NUM_SEGMENTS. 2. Derive subkey and mask_X. 3. Compute P* = P ⊕ mask_X. 4. Decrypt and verify FULLPATH and TIMESTAMP. 5. For i = 1 to NUM_SEGMENTS: a. Read nonce (12 bytes), then ciphertext + tag. b. Verify nonce == P*[0:4] || LE64(i); abort on mismatch. c. Reconstruct AAD_i. d. Decrypt; abort and discard output on auth failure. e. Write plaintext to output. 20.4. Atomic Operations - Write entries to a temporary file, then rename atomically. - fsync/fdatasync before rename for durability. - Truncate container on cancelled add operations. 20.5. Error Handling - Validate all length fields before allocation. - Check for integer overflow in size calculations. - Fail securely on authentication errors (no partial plaintext). - Log errors without leaking key material or plaintext. References [STREAM] Hoang, V.T., Reyhanitabar, R., Rogaway, P., Vizár, D., "Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance", CRYPTO 2015. [SE3] Hoang, V.T., Shen, Y., "Security of Streaming Encryption in Google's Tink Library", CCS 2020. https://eprint.iacr.org/2020/1019 [ChaCha20] Nir, Y., Langley, A., "ChaCha20 and Poly1305 for IETF Protocols", RFC 8439, 2018. [Argon2id] Biryukov, A., Dinu, D., Khovratovich, D., Josefsson, S., "Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications", RFC 9106, 2021. [BLAKE3] O'Connor, J., Aumasson, J-P., Neves, S., Wilcox-O'Hearn, Z., "BLAKE3: one function, fast everywhere", 2020. Author's Address MFPK Maintainer Email: mfpk-spec@example.com