filanti 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ Supported algorithms:
9
9
  - ChaCha20-Poly1305 (excellent software performance)
10
10
  """
11
11
 
12
+ import base64
12
13
  import json
13
14
  from dataclasses import dataclass, asdict
14
15
  from enum import Enum
@@ -37,7 +38,12 @@ DEFAULT_ALGORITHM = EncryptionAlgorithm.AES_256_GCM
37
38
 
38
39
  # File format magic bytes
39
40
  FILANTI_MAGIC = b"FLNT"
40
- FORMAT_VERSION = 1
41
+ FORMAT_VERSION = 1 # Legacy v1 format version
42
+ FORMAT_VERSION_V2 = 2 # New v2 format with encrypted metadata
43
+
44
+ # Product identifier for header
45
+ PRODUCT_NAME = "FLNT"
46
+ HEADER_VERSION = "1.1.0"
41
47
 
42
48
  # Chunk size for streaming encryption (64 KB)
43
49
  CHUNK_SIZE = 65536
@@ -151,6 +157,37 @@ class EncryptionMetadata:
151
157
  return cls(**parsed)
152
158
 
153
159
 
160
+ @dataclass
161
+ class FileHeader:
162
+ """Minimal public header for encrypted files - base64 encoded.
163
+
164
+ Only exposes product name and version, no cryptographic details.
165
+ """
166
+
167
+ product: str = PRODUCT_NAME
168
+ version: str = HEADER_VERSION
169
+
170
+ def to_base64(self) -> bytes:
171
+ """Encode header as base64 bytes."""
172
+ data = {"p": self.product, "v": self.version}
173
+ json_bytes = json.dumps(data, separators=(",", ":")).encode("utf-8")
174
+ return base64.b64encode(json_bytes)
175
+
176
+ @classmethod
177
+ def from_base64(cls, data: bytes) -> "FileHeader":
178
+ """Decode header from base64 bytes."""
179
+ try:
180
+ json_bytes = base64.b64decode(data)
181
+ parsed = json.loads(json_bytes.decode("utf-8"))
182
+ return cls(product=parsed.get("p", "FLNT"), version=parsed.get("v", "1.0.0"))
183
+ except Exception as e:
184
+ raise EncryptionError(f"Invalid file header: {e}") from e
185
+
186
+ def validate(self) -> bool:
187
+ """Validate the header is from Filanti."""
188
+ return self.product == PRODUCT_NAME
189
+
190
+
154
191
  def _get_cipher(algorithm: EncryptionAlgorithm, key: bytes):
155
192
  """Get the appropriate cipher for the algorithm."""
156
193
  if algorithm == EncryptionAlgorithm.AES_256_GCM:
@@ -270,6 +307,8 @@ def encrypt_file(
270
307
  key: bytes,
271
308
  algorithm: EncryptionAlgorithm = DEFAULT_ALGORITHM,
272
309
  file_manager: FileManager | None = None,
310
+ remove_source: bool = False,
311
+ secure_delete: bool = True,
273
312
  ) -> EncryptionMetadata:
274
313
  """Encrypt a file using authenticated encryption.
275
314
 
@@ -279,6 +318,9 @@ def encrypt_file(
279
318
  key: Encryption key.
280
319
  algorithm: Encryption algorithm to use.
281
320
  file_manager: Optional FileManager instance.
321
+ remove_source: If True, delete original file after successful encryption.
322
+ secure_delete: If True and remove_source is True, securely overwrite
323
+ the original file before deletion (defense in depth).
282
324
 
283
325
  Returns:
284
326
  EncryptionMetadata for the encrypted file.
@@ -309,6 +351,13 @@ def encrypt_file(
309
351
  output = _build_encrypted_file(result.ciphertext, metadata)
310
352
  fm.write_bytes(output_path, output)
311
353
 
354
+ # Remove source file if requested
355
+ if remove_source:
356
+ if secure_delete:
357
+ fm.secure_delete(input_path)
358
+ else:
359
+ fm.delete(input_path)
360
+
312
361
  return metadata
313
362
 
314
363
  except (EncryptionError, FileOperationError):
@@ -328,6 +377,8 @@ def encrypt_file_with_password(
328
377
  algorithm: EncryptionAlgorithm = DEFAULT_ALGORITHM,
329
378
  kdf_params: KDFParams | None = None,
330
379
  file_manager: FileManager | None = None,
380
+ remove_source: bool = False,
381
+ secure_delete: bool = True,
331
382
  ) -> EncryptionMetadata:
332
383
  """Encrypt a file using a password.
333
384
 
@@ -338,6 +389,9 @@ def encrypt_file_with_password(
338
389
  algorithm: Encryption algorithm to use.
339
390
  kdf_params: Optional KDF parameters.
340
391
  file_manager: Optional FileManager instance.
392
+ remove_source: If True, delete original file after successful encryption.
393
+ secure_delete: If True and remove_source is True, securely overwrite
394
+ the original file before deletion (defense in depth).
341
395
 
342
396
  Returns:
343
397
  EncryptionMetadata for the encrypted file.
@@ -373,6 +427,13 @@ def encrypt_file_with_password(
373
427
  output = _build_encrypted_file(result.ciphertext, metadata)
374
428
  fm.write_bytes(output_path, output)
375
429
 
430
+ # Remove source file if requested
431
+ if remove_source:
432
+ if secure_delete:
433
+ fm.secure_delete(input_path)
434
+ else:
435
+ fm.delete(input_path)
436
+
376
437
  return metadata
377
438
 
378
439
  except (EncryptionError, FileOperationError):
@@ -386,12 +447,56 @@ def encrypt_file_with_password(
386
447
 
387
448
 
388
449
  def _build_encrypted_file(ciphertext: bytes, metadata: EncryptionMetadata) -> bytes:
389
- """Build encrypted file with header.
450
+ """Build encrypted file with v2 format (encrypted metadata).
451
+
452
+ File format v2:
453
+ - N bytes: Base64 header ({"p":"FLNT","v":"1.1.0"})
454
+ - 2 bytes: Header length (big-endian uint16)
455
+ - 12 bytes: Metadata nonce (for deriving metadata key and encryption)
456
+ - 4 bytes: Encrypted metadata length (big-endian uint32)
457
+ - M bytes: Encrypted metadata ciphertext
458
+ - K bytes: File ciphertext
459
+
460
+ The metadata is encrypted using AES-GCM with a key derived from:
461
+ SHA-256(metadata_nonce || "filanti-meta-v2")
462
+ """
463
+ from hashlib import sha256
464
+
465
+ # Create minimal public header
466
+ header = FileHeader()
467
+ header_b64 = header.to_base64()
468
+
469
+ # Generate a random nonce for metadata encryption
470
+ meta_nonce = generate_nonce(NONCE_SIZE_GCM)
471
+
472
+ # Derive metadata encryption key from the nonce
473
+ meta_key = sha256(meta_nonce + b"filanti-meta-v2").digest()
474
+
475
+ # Encrypt metadata using AES-GCM
476
+ meta_plaintext = metadata.to_bytes()
477
+ meta_cipher = AESGCM(meta_key)
478
+ meta_ciphertext = meta_cipher.encrypt(meta_nonce, meta_plaintext, None)
390
479
 
391
- File format:
480
+ # Assemble v2 file
481
+ parts = [
482
+ header_b64, # Base64 header
483
+ len(header_b64).to_bytes(2, "big"), # Header length (2 bytes)
484
+ meta_nonce, # Metadata nonce (12 bytes)
485
+ len(meta_ciphertext).to_bytes(4, "big"), # Encrypted metadata length (4 bytes)
486
+ meta_ciphertext, # Encrypted metadata
487
+ ciphertext, # File ciphertext
488
+ ]
489
+
490
+ return b"".join(parts)
491
+
492
+
493
+ def _build_encrypted_file_v1(ciphertext: bytes, metadata: EncryptionMetadata) -> bytes:
494
+ """Build encrypted file with legacy v1 format (plaintext metadata).
495
+
496
+ File format v1 (legacy):
392
497
  - 4 bytes: Magic ("FLNT")
393
498
  - 4 bytes: Metadata length (big-endian uint32)
394
- - N bytes: Metadata (JSON)
499
+ - N bytes: Metadata (JSON plaintext)
395
500
  - M bytes: Ciphertext
396
501
  """
397
502
  meta_bytes = metadata.to_bytes()
@@ -403,6 +508,8 @@ def _build_encrypted_file(ciphertext: bytes, metadata: EncryptionMetadata) -> by
403
508
  def parse_encrypted_file(data: bytes) -> tuple[EncryptionMetadata, bytes]:
404
509
  """Parse encrypted file header and extract ciphertext.
405
510
 
511
+ Supports both v1 (legacy) and v2 formats.
512
+
406
513
  Args:
407
514
  data: Encrypted file bytes.
408
515
 
@@ -415,6 +522,16 @@ def parse_encrypted_file(data: bytes) -> tuple[EncryptionMetadata, bytes]:
415
522
  if len(data) < 8:
416
523
  raise EncryptionError("Invalid encrypted file: too short")
417
524
 
525
+ # Check for v1 format (starts with raw FLNT magic bytes)
526
+ if data[:4] == FILANTI_MAGIC:
527
+ return _parse_encrypted_file_v1(data)
528
+
529
+ # Try v2 format (starts with base64-encoded header)
530
+ return _parse_encrypted_file_v2(data)
531
+
532
+
533
+ def _parse_encrypted_file_v1(data: bytes) -> tuple[EncryptionMetadata, bytes]:
534
+ """Parse legacy v1 format encrypted file."""
418
535
  if data[:4] != FILANTI_MAGIC:
419
536
  raise EncryptionError("Invalid encrypted file: bad magic bytes")
420
537
 
@@ -433,3 +550,92 @@ def parse_encrypted_file(data: bytes) -> tuple[EncryptionMetadata, bytes]:
433
550
 
434
551
  return metadata, ciphertext
435
552
 
553
+
554
+ def _parse_encrypted_file_v2(data: bytes) -> tuple[EncryptionMetadata, bytes]:
555
+ """Parse v2 format encrypted file with encrypted metadata.
556
+
557
+ File format v2:
558
+ - N bytes: Base64 header
559
+ - 2 bytes: Header length
560
+ - 12 bytes: Metadata nonce
561
+ - 4 bytes: Encrypted metadata length
562
+ - M bytes: Encrypted metadata
563
+ - K bytes: File ciphertext
564
+ """
565
+ from hashlib import sha256
566
+
567
+ try:
568
+ # Find header boundary by scanning for valid base64 header
569
+ header_b64 = None
570
+ header_len_pos = None
571
+
572
+ for try_len in range(20, 64):
573
+ if try_len + 2 > len(data):
574
+ break
575
+ potential_header = data[:try_len]
576
+ potential_len = int.from_bytes(data[try_len:try_len+2], "big")
577
+ if potential_len == try_len:
578
+ # Found matching header length
579
+ try:
580
+ FileHeader.from_base64(potential_header)
581
+ header_b64 = potential_header
582
+ header_len_pos = try_len
583
+ break
584
+ except Exception:
585
+ continue
586
+
587
+ if header_b64 is None:
588
+ raise EncryptionError("Invalid encrypted file: cannot parse v2 header")
589
+
590
+ # Parse and validate header
591
+ header = FileHeader.from_base64(header_b64)
592
+ if not header.validate():
593
+ raise EncryptionError("Invalid encrypted file: wrong product identifier")
594
+
595
+ offset = header_len_pos + 2 # Skip header + header length bytes
596
+
597
+ # Read metadata nonce (12 bytes)
598
+ if len(data) < offset + NONCE_SIZE_GCM:
599
+ raise EncryptionError("Invalid encrypted file: truncated")
600
+
601
+ meta_nonce = data[offset:offset + NONCE_SIZE_GCM]
602
+ offset += NONCE_SIZE_GCM
603
+
604
+ # Read encrypted metadata length
605
+ if len(data) < offset + 4:
606
+ raise EncryptionError("Invalid encrypted file: truncated")
607
+
608
+ encrypted_meta_len = int.from_bytes(data[offset:offset+4], "big")
609
+ offset += 4
610
+
611
+ # Read encrypted metadata
612
+ if len(data) < offset + encrypted_meta_len:
613
+ raise EncryptionError("Invalid encrypted file: truncated metadata")
614
+
615
+ meta_ciphertext = data[offset:offset + encrypted_meta_len]
616
+ offset += encrypted_meta_len
617
+
618
+ # Extract file ciphertext
619
+ ciphertext = data[offset:]
620
+
621
+ # Derive metadata decryption key
622
+ meta_key = sha256(meta_nonce + b"filanti-meta-v2").digest()
623
+
624
+ # Decrypt metadata
625
+ meta_cipher = AESGCM(meta_key)
626
+ try:
627
+ meta_plaintext = meta_cipher.decrypt(meta_nonce, meta_ciphertext, None)
628
+ except InvalidTag:
629
+ raise EncryptionError("Invalid encrypted file: metadata authentication failed")
630
+
631
+ # Parse metadata
632
+ metadata = EncryptionMetadata.from_bytes(meta_plaintext)
633
+
634
+ return metadata, ciphertext
635
+
636
+ except EncryptionError:
637
+ raise
638
+ except Exception as e:
639
+ raise EncryptionError(f"Invalid encrypted file format: {e}") from e
640
+
641
+
filanti/integrity/mac.py CHANGED
@@ -99,16 +99,69 @@ class IntegrityMetadata:
99
99
  """Serialize to JSON string."""
100
100
  return json.dumps(asdict(self), indent=indent, sort_keys=True)
101
101
 
102
+ def to_json_minimal(self) -> str:
103
+ """Serialize to minimal JSON (reduced information leakage)."""
104
+ # Only include essential fields: version, mac, algorithm
105
+ # Omit: filename, filesize, created_at
106
+ minimal = {
107
+ "v": "2", # Minimal format version
108
+ "m": self.mac,
109
+ "a": self._short_algorithm(self.algorithm) if self.algorithm else None,
110
+ }
111
+ return json.dumps(minimal, separators=(",", ":"))
112
+
113
+ @staticmethod
114
+ def _short_algorithm(algorithm: str) -> str:
115
+ """Convert algorithm to short form."""
116
+ short_map = {
117
+ "hmac-sha256": "hs256",
118
+ "hmac-sha384": "hs384",
119
+ "hmac-sha512": "hs512",
120
+ "hmac-sha3-256": "hs3256",
121
+ "hmac-blake2b": "hb2b",
122
+ }
123
+ return short_map.get(algorithm, algorithm)
124
+
125
+ @staticmethod
126
+ def _expand_algorithm(short: str) -> str:
127
+ """Convert short algorithm to full form."""
128
+ expand_map = {
129
+ "hs256": "hmac-sha256",
130
+ "hs384": "hmac-sha384",
131
+ "hs512": "hmac-sha512",
132
+ "hs3256": "hmac-sha3-256",
133
+ "hb2b": "hmac-blake2b",
134
+ }
135
+ return expand_map.get(short, short)
136
+
102
137
  @classmethod
103
138
  def from_json(cls, json_str: str) -> "IntegrityMetadata":
104
- """Deserialize from JSON string."""
139
+ """Deserialize from JSON string (supports v1 and v2/minimal formats)."""
105
140
  data = json.loads(json_str)
141
+
142
+ # Check for minimal v2 format
143
+ if "v" in data and data.get("v") == "2":
144
+ return cls(
145
+ version="2.0",
146
+ mac=data.get("m"),
147
+ algorithm=cls._expand_algorithm(data.get("a", "")),
148
+ )
149
+
150
+ # Standard v1 format
106
151
  return cls(**data)
107
152
 
108
- def save(self, path: str | Path) -> None:
109
- """Save metadata to file."""
153
+ def save(self, path: str | Path, minimal: bool = False) -> None:
154
+ """Save metadata to file.
155
+
156
+ Args:
157
+ path: Output path.
158
+ minimal: If True, use minimal format (reduced information leakage).
159
+ """
110
160
  path = Path(path)
111
- path.write_text(self.to_json(), encoding="utf-8")
161
+ if minimal:
162
+ path.write_text(self.to_json_minimal(), encoding="utf-8")
163
+ else:
164
+ path.write_text(self.to_json(), encoding="utf-8")
112
165
 
113
166
  @classmethod
114
167
  def load(cls, path: str | Path) -> "IntegrityMetadata":
@@ -384,6 +437,7 @@ def create_integrity_file(
384
437
  key: bytes,
385
438
  algorithm: MACAlgorithm | str = DEFAULT_ALGORITHM,
386
439
  output_path: str | Path | None = None,
440
+ minimal: bool = False,
387
441
  ) -> Path:
388
442
  """Create detached integrity metadata file.
389
443
 
@@ -392,6 +446,8 @@ def create_integrity_file(
392
446
  key: Secret key for HMAC.
393
447
  algorithm: MAC algorithm to use.
394
448
  output_path: Optional output path (default: file_path + '.mac')
449
+ minimal: If True, use minimal format (reduced information leakage).
450
+ Only includes version, MAC, and algorithm (no filename, filesize, timestamp).
395
451
 
396
452
  Returns:
397
453
  Path to the created integrity file.
@@ -408,13 +464,13 @@ def create_integrity_file(
408
464
  metadata = IntegrityMetadata(
409
465
  mac=result.to_hex(),
410
466
  algorithm=result.algorithm,
411
- filename=file_path.name,
412
- filesize=file_path.stat().st_size,
413
- created_at=result.created_at,
467
+ filename=file_path.name if not minimal else None,
468
+ filesize=file_path.stat().st_size if not minimal else None,
469
+ created_at=result.created_at if not minimal else None,
414
470
  )
415
471
 
416
472
  try:
417
- metadata.save(output_path)
473
+ metadata.save(output_path, minimal=minimal)
418
474
  return output_path
419
475
  except Exception as e:
420
476
  raise FileOperationError(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: filanti
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: A modular, security-focused file framework for encryption, hashing, and integrity verification
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -48,7 +48,7 @@ Description-Content-Type: text/markdown
48
48
 
49
49
  ## Overview
50
50
 
51
- **Filanti** is a production-ready Python framework providing secure-by-default primitives for:
51
+ **Filanti** is a Python framework providing secure-by-default primitives for:
52
52
 
53
53
  - **File Encryption** - AES-256-GCM, ChaCha20-Poly1305 with password-based encryption
54
54
  - **Asymmetric Encryption** - Hybrid encryption with X25519, RSA-OAEP for multi-recipient file exchange
@@ -359,6 +359,15 @@ with SecureString("my-password") as pwd:
359
359
 
360
360
  Secure secret injection for automation and CI/CD workflows. Avoid hardcoding passwords in scripts or command lines.
361
361
 
362
+ **Supported Patterns:**
363
+
364
+ | Pattern | Description | Example |
365
+ |---------|-------------|---------|
366
+ | `ENV:VAR` | Unix-style (original) | `ENV:MY_PASSWORD` |
367
+ | `$env:VAR` | PowerShell-style | `$env:MY_PASSWORD` |
368
+ | `${VAR}` | Shell variable expansion | `${MY_PASSWORD}` |
369
+ | `env.VAR` | Dot notation (cross-platform) | `env.MY_PASSWORD` |
370
+
362
371
  ```python
363
372
  import os
364
373
  from filanti.api import Filanti
@@ -366,12 +375,23 @@ from filanti.api import Filanti
366
375
  # Set secret in environment (done by CI/CD, Docker, etc.)
367
376
  os.environ["ENCRYPT_PASSWORD"] = "my-secure-password"
368
377
 
369
- # Use ENV reference - secret is resolved at runtime
370
- Filanti.encrypt("secret.txt", password="ENV:ENCRYPT_PASSWORD")
371
- Filanti.decrypt("secret.txt.enc", password="ENV:ENCRYPT_PASSWORD")
378
+ # All patterns are supported
379
+ Filanti.encrypt("secret.txt", password="ENV:ENCRYPT_PASSWORD") # Unix-style
380
+ Filanti.encrypt("secret.txt", password="$env:ENCRYPT_PASSWORD") # PowerShell
381
+ Filanti.encrypt("secret.txt", password="${ENCRYPT_PASSWORD}") # Shell-style
382
+ Filanti.encrypt("secret.txt", password="env.ENCRYPT_PASSWORD") # Dot notation
383
+
384
+ # Load secrets from .env file
385
+ Filanti.load_dotenv(".env")
386
+ Filanti.encrypt("secret.txt", password="ENV:SECRET_FROM_DOTENV")
387
+
388
+ # Or load during encryption
389
+ Filanti.encrypt("secret.txt", password="ENV:MY_KEY", dotenv_path=".env")
372
390
 
373
391
  # Check if value is an ENV reference
374
- Filanti.is_env_reference("ENV:MY_SECRET") # True
392
+ Filanti.is_env_reference("ENV:MY_SECRET") # True
393
+ Filanti.is_env_reference("$env:MY_SECRET") # True
394
+ Filanti.is_env_reference("${MY_SECRET}") # True
375
395
 
376
396
  # Resolve secret manually
377
397
  password = Filanti.resolve_secret("ENV:ENCRYPT_PASSWORD")
@@ -392,11 +412,25 @@ safe = Filanti.safe_json_output(data, secret_keys=["password"])
392
412
  # Set environment variable
393
413
  export ENCRYPT_PASSWORD="my-secure-password"
394
414
 
395
- # Use in CLI commands
415
+ # All pattern formats work in --password
396
416
  filanti encrypt secret.txt --password ENV:ENCRYPT_PASSWORD
397
- filanti decrypt secret.txt.enc --password ENV:ENCRYPT_PASSWORD
398
- filanti mac file.txt --key ENV:HMAC_KEY
399
- filanti sign document.pdf --key mykey --password ENV:KEY_PASSWORD
417
+ filanti encrypt secret.txt --password '$env:ENCRYPT_PASSWORD' # PowerShell
418
+ filanti encrypt secret.txt --password '${ENCRYPT_PASSWORD}' # Shell-style
419
+ filanti encrypt secret.txt --password env.ENCRYPT_PASSWORD # Dot notation
420
+
421
+ # PowerShell-friendly --env option (no special characters)
422
+ filanti encrypt secret.txt --env ENCRYPT_PASSWORD
423
+ filanti decrypt secret.txt.enc --env ENCRYPT_PASSWORD
424
+
425
+ # Load from .env file
426
+ filanti encrypt secret.txt --dotenv .env --env-key MY_PASSWORD
427
+ filanti mac file.txt --dotenv secrets.env --env-key HMAC_KEY
428
+
429
+ # All secret-accepting commands support these options:
430
+ # --password Literal or ENV pattern
431
+ # --env Variable name (PowerShell-friendly)
432
+ # --dotenv Path to .env file
433
+ # --env-key Variable name from .env file
400
434
  ```
401
435
 
402
436
  **Benefits:**
@@ -404,6 +438,43 @@ filanti sign document.pdf --key mykey --password ENV:KEY_PASSWORD
404
438
  - Works with CI/CD (GitHub Actions, GitLab CI, Jenkins)
405
439
  - Compatible with Docker/Kubernetes secrets
406
440
  - 12-factor app compliance
441
+ - PowerShell-native syntax support
442
+ - Cross-platform .env file loading
443
+
444
+ ### Secure File Deletion
445
+
446
+ Delete original files securely after encryption/decryption operations.
447
+
448
+ ```python
449
+ from filanti.api import Filanti
450
+
451
+ # Encrypt and securely delete original
452
+ Filanti.encrypt("secret.txt", password="my-pass", remove_source=True)
453
+ # Original file is securely overwritten before deletion
454
+
455
+ # Decrypt and remove encrypted file
456
+ Filanti.decrypt("secret.txt.enc", password="my-pass", remove_source=True)
457
+ # Encrypted file is securely deleted after decryption
458
+
459
+ # Use faster (non-secure) deletion
460
+ Filanti.encrypt("secret.txt", password="my-pass",
461
+ remove_source=True, secure_delete=False)
462
+ ```
463
+
464
+ **CLI Support:**
465
+
466
+ ```bash
467
+ # Encrypt and securely delete original
468
+ filanti encrypt secret.txt --password mypass --remove-source
469
+
470
+ # Decrypt and remove encrypted file
471
+ filanti decrypt secret.txt.enc --password mypass --remove-source
472
+
473
+ # Use faster (non-secure) deletion
474
+ filanti encrypt secret.txt --password mypass --remove-source --no-secure-delete
475
+ ```
476
+
477
+ > ⚠️ **Note:** Secure deletion provides defense-in-depth but has limitations on SSDs with wear-leveling, journaling filesystems, and cloud-synced folders. For maximum security, use full-disk encryption.
407
478
 
408
479
  ### Asymmetric / Hybrid Encryption
409
480
 
@@ -834,40 +905,72 @@ safe_output = redact_secret("Password is secret123", "secret123")
834
905
 
835
906
  ---
836
907
 
837
- ## Architecture
908
+ [//]: # (## Architecture)
838
909
 
839
- ```
840
- filanti/
841
- ├── core/
842
- │ ├── errors.py
843
- │ ├── file_manager.py
844
- ├── metadata.py
845
- │ ├── plugins.py
846
- │ ├── secrets.py
847
- │ └── secure_memory.py
848
-
849
- ├── crypto/
850
- │ ├── encryption.py
851
- │ ├── decryption.py
852
- │ ├── key_management.py
853
- │ ├── kdf.py
854
- │ ├── streaming.py
855
- │ └── asymmetric.py
856
-
857
- ├── hashing/
858
- │ └── crypto_hash.py
859
-
860
- ├── integrity/
861
- │ ├── checksum.py
862
- │ ├── mac.py
863
- │ └── signature.py
864
-
865
- ├── cli/
866
- └── main.py
867
-
868
- └── api/
869
- └── sdk.py
870
- ```
910
+ [//]: # ()
911
+ [//]: # (```)
912
+
913
+ [//]: # (filanti/)
914
+
915
+ [//]: # (├── core/ )
916
+
917
+ [//]: # (│ ├── errors.py )
918
+
919
+ [//]: # ( ├── file_manager.py )
920
+
921
+ [//]: # (│ ├── metadata.py )
922
+
923
+ [//]: # (│ ├── plugins.py )
924
+
925
+ [//]: # (│ ├── secrets.py )
926
+
927
+ [//]: # ( └── secure_memory.py )
928
+
929
+ [//]: # (│)
930
+
931
+ [//]: # (├── crypto/ )
932
+
933
+ [//]: # (│ ├── encryption.py )
934
+
935
+ [//]: # ( ├── decryption.py )
936
+
937
+ [//]: # (├── key_management.py )
938
+
939
+ [//]: # (│ ├── kdf.py )
940
+
941
+ [//]: # (│ ├── streaming.py )
942
+
943
+ [//]: # (│ └── asymmetric.py )
944
+
945
+ [//]: # (│)
946
+
947
+ [//]: # (├── hashing/ )
948
+
949
+ [//]: # (│ └── crypto_hash.py )
950
+
951
+ [//]: # (│)
952
+
953
+ [//]: # (├── integrity/ )
954
+
955
+ [//]: # (│ ├── checksum.py )
956
+
957
+ [//]: # (│ ├── mac.py )
958
+
959
+ [//]: # (│ └── signature.py )
960
+
961
+ [//]: # (│)
962
+
963
+ [//]: # (├── cli/ )
964
+
965
+ [//]: # (│ └── main.py )
966
+
967
+ [//]: # (│)
968
+
969
+ [//]: # (└── api/ )
970
+
971
+ [//]: # ( └── sdk.py )
972
+
973
+ [//]: # (```)
871
974
 
872
975
  ### Module Dependencies
873
976
 
@@ -1128,7 +1231,8 @@ encrypt_stream_file(input_path, output_path, key, chunk_size=16*1024) # 16 KB
1128
1231
 
1129
1232
  ---
1130
1233
 
1131
- ## Contributing
1234
+ ## Contributors || Acknowledgements
1235
+ [@stephenlb](https://github.com/stephenlb) Thanks for the inspiration and guidance on encryption and security best practices.
1132
1236
 
1133
1237
  ### Development Setup
1134
1238
 
@@ -1159,15 +1263,21 @@ pip install -e ".[dev]"
1159
1263
 
1160
1264
  [//]: # (```)
1161
1265
 
1162
- ### Pull Request Guidelines
1266
+ [//]: # (### Pull Request Guidelines)
1163
1267
 
1164
- 1. Write tests for new features
1165
- 2. Update documentation
1166
- 3. Follow existing code style
1167
- 4. Add type hints
1168
- 5. Run full test suite before submitting
1268
+ [//]: # ()
1269
+ [//]: # (1. Write tests for new features)
1169
1270
 
1170
- ---
1271
+ [//]: # (2. Update documentation)
1272
+
1273
+ [//]: # (3. Follow existing code style)
1274
+
1275
+ [//]: # (4. Add type hints)
1276
+
1277
+ [//]: # (5. Run full test suite before submitting)
1278
+
1279
+ [//]: # ()
1280
+ [//]: # (---)
1171
1281
 
1172
1282
  ## Changelog
1173
1283