mirror of
https://github.com/overcuriousity/trace.git
synced 2025-12-20 04:52:21 +00:00
This commit addresses 20 bugs discovered during comprehensive code review, focusing on data integrity, concurrent access, and user experience. CRITICAL FIXES: - Fix GPG key listing to support keys with multiple UIDs (crypto.py:40) - Implement cross-platform file locking to prevent concurrent access corruption (storage.py) - Fix evidence detail delete logic that could delete wrong note (tui.py:2481-2497) - Add corrupted JSON handling with user prompt and automatic backup (storage.py, tui.py) DATA INTEGRITY: - Fix IOC/Hash pattern false positives by checking longest hashes first (models.py:32-95) - Fix URL pattern to exclude trailing punctuation (models.py:81, 152, 216) - Improve IOC overlap detection with proper range tracking (models.py) - Fix note deletion to use note_id instead of object identity (tui.py:2498-2619) - Add state validation to detect and clear orphaned references (storage.py:355-384) SCROLLING & NAVIGATION: - Fix evidence detail view to support full scrolling instead of "last N" (tui.py:816-847) - Fix filter reset index bounds bug (tui.py:1581-1654) - Add scroll_offset validation after all operations (tui.py:1608-1654) - Fix division by zero in scroll calculations (tui.py:446-478) - Validate selection bounds across all views (tui.py:_validate_selection_bounds) EXPORT & CLI: - Fix multi-line note export with proper markdown indentation (cli.py:129-143) - Add stderr warnings for GPG signature failures (cli.py:61, 63) - Validate active context and show warnings in CLI (cli.py:12-44) TESTING: - Update tests to support new lock file mechanism (test_models.py) - All existing tests pass with new changes Breaking changes: None Backward compatible: Yes (existing data files work unchanged)
89 lines
2.8 KiB
Python
89 lines
2.8 KiB
Python
import subprocess
|
|
import hashlib
|
|
|
|
class Crypto:
|
|
@staticmethod
|
|
def list_gpg_keys():
|
|
"""
|
|
List available GPG secret keys.
|
|
Returns a list of tuples: (key_id, user_id)
|
|
"""
|
|
try:
|
|
proc = subprocess.Popen(
|
|
['gpg', '--list-secret-keys', '--with-colons'],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
stdout, stderr = proc.communicate(timeout=10)
|
|
|
|
if proc.returncode != 0:
|
|
return []
|
|
|
|
keys = []
|
|
current_key_id = None
|
|
|
|
for line in stdout.split('\n'):
|
|
fields = line.split(':')
|
|
if len(fields) < 2:
|
|
continue
|
|
|
|
# sec = secret key
|
|
if fields[0] == 'sec':
|
|
# Key ID is in field 4 (short) or we can extract from field 5 (fingerprint)
|
|
current_key_id = fields[4] if len(fields) > 4 else None
|
|
|
|
# uid = user ID
|
|
elif fields[0] == 'uid' and current_key_id:
|
|
user_id = fields[9] if len(fields) > 9 else "Unknown"
|
|
keys.append((current_key_id, user_id))
|
|
# Don't reset current_key_id - allow multiple UIDs per key
|
|
|
|
return keys
|
|
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return [] # GPG not installed or timed out
|
|
|
|
@staticmethod
|
|
def sign_content(content: str, key_id: str = None) -> str:
|
|
"""
|
|
Signs the content using GPG.
|
|
|
|
Args:
|
|
content: The content to sign
|
|
key_id: Optional GPG key ID to use. If None, uses default key.
|
|
|
|
Returns:
|
|
The clearsigned content or empty string if GPG fails.
|
|
"""
|
|
try:
|
|
# Build command
|
|
cmd = ['gpg', '--clearsign', '--output', '-']
|
|
|
|
# Add specific key if provided
|
|
if key_id:
|
|
cmd.extend(['--local-user', key_id])
|
|
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
stdout, stderr = proc.communicate(input=content, timeout=10)
|
|
|
|
if proc.returncode != 0:
|
|
# Fallback: maybe no key is found or gpg error
|
|
# In a real app we might want to log this 'stderr'
|
|
return ""
|
|
|
|
return stdout
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return "" # GPG not installed or timed out
|
|
|
|
@staticmethod
|
|
def hash_content(content: str, timestamp: float) -> str:
|
|
data = f"{timestamp}:{content}".encode('utf-8')
|
|
return hashlib.sha256(data).hexdigest()
|