mirror of
https://github.com/overcuriousity/trace.git
synced 2025-12-20 04:52:21 +00:00
Add GPG signature verification and first-run setup wizard
This commit adds comprehensive GPG signature verification functionality and a first-run setup wizard for configuring GPG signing: **GPG Verification Features:** - Added `Crypto.verify_signature()` to verify GPG clearsigned messages - Added `Crypto.is_gpg_available()` to detect GPG installation - Added `Note.verify_signature()` method to verify note signatures - Verification returns status (verified/failed/unsigned) and signer info **TUI Enhancements:** - Display verification symbols in note lists: ✓ (verified), ✗ (failed), ? (unsigned) - Updated note detail view to show verification status with signer information - Added 'v' key binding in note detail view to trigger verification dialog - Verification dialog shows detailed status and helpful error messages **First-Run Wizard:** - Created `gpg_wizard.py` module with interactive setup wizard - Wizard runs on first application startup (when settings.json doesn't exist) - Detects GPG availability and informs user if not installed - Lists available secret keys and allows user to select signing key - Gracefully handles missing GPG or no available keys - Settings can be manually edited later via ~/.trace/settings.json **Implementation Details:** - GPG key ID is now stored in settings as `gpg_key_id` - All note displays show verification status for better chain-of-custody - External verification possible via standard GPG tools on exported notes - Follows existing codebase patterns (atomic writes, graceful degradation) Files modified: - trace/crypto.py: Added verification and availability check functions - trace/models/__init__.py: Added Note.verify_signature() method - trace/gpg_wizard.py: New first-run setup wizard module - trace/cli.py: Integrated wizard before TUI launch - trace/tui_app.py: Added verification display and dialog
This commit is contained in:
@@ -161,6 +161,10 @@ def main():
|
|||||||
quick_add_note(args.note)
|
quick_add_note(args.note)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check for first run and run GPG wizard if needed
|
||||||
|
from .gpg_wizard import check_and_run_wizard
|
||||||
|
check_and_run_wizard()
|
||||||
|
|
||||||
# Launch TUI (with optional direct navigation to active context)
|
# Launch TUI (with optional direct navigation to active context)
|
||||||
try:
|
try:
|
||||||
from .tui_app import run_tui
|
from .tui_app import run_tui
|
||||||
|
|||||||
@@ -2,6 +2,93 @@ import subprocess
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
class Crypto:
|
class Crypto:
|
||||||
|
@staticmethod
|
||||||
|
def is_gpg_available() -> bool:
|
||||||
|
"""
|
||||||
|
Check if GPG is available on the system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if GPG is available, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
['gpg', '--version'],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
stdout, stderr = proc.communicate(timeout=5)
|
||||||
|
return proc.returncode == 0
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def verify_signature(signed_content: str) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Verify a GPG clearsigned message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signed_content: The clearsigned content to verify
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple of (verified: bool, signer_info: str)
|
||||||
|
- verified: True if signature is valid, False otherwise
|
||||||
|
- signer_info: Information about the signer (key ID, name) or error message
|
||||||
|
"""
|
||||||
|
if not signed_content or not signed_content.strip():
|
||||||
|
return False, "No signature present"
|
||||||
|
|
||||||
|
# Check if content looks like a GPG signed message
|
||||||
|
if "-----BEGIN PGP SIGNED MESSAGE-----" not in signed_content:
|
||||||
|
return False, "Not a GPG signed message"
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
['gpg', '--verify'],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
stdout, stderr = proc.communicate(input=signed_content, timeout=10)
|
||||||
|
|
||||||
|
if proc.returncode == 0:
|
||||||
|
# Parse signer info from stderr (GPG outputs verification info to stderr)
|
||||||
|
signer_info = "Unknown signer"
|
||||||
|
for line in stderr.split('\n'):
|
||||||
|
if "Good signature from" in line:
|
||||||
|
# Extract the signer name/email
|
||||||
|
parts = line.split('"')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
signer_info = parts[1]
|
||||||
|
break
|
||||||
|
elif "using" in line:
|
||||||
|
# Try to get key ID
|
||||||
|
if "key" in line.lower():
|
||||||
|
signer_info = line.strip()
|
||||||
|
|
||||||
|
return True, signer_info
|
||||||
|
else:
|
||||||
|
# Signature verification failed
|
||||||
|
error_msg = "Verification failed"
|
||||||
|
for line in stderr.split('\n'):
|
||||||
|
if "BAD signature" in line:
|
||||||
|
error_msg = "BAD signature"
|
||||||
|
break
|
||||||
|
elif "no public key" in line or "public key not found" in line:
|
||||||
|
error_msg = "Public key not found in keyring"
|
||||||
|
break
|
||||||
|
elif "Can't check signature" in line:
|
||||||
|
error_msg = "Cannot check signature"
|
||||||
|
break
|
||||||
|
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||||
|
return False, "GPG not available or timeout"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Error: {str(e)}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_gpg_keys():
|
def list_gpg_keys():
|
||||||
"""
|
"""
|
||||||
|
|||||||
131
trace/gpg_wizard.py
Normal file
131
trace/gpg_wizard.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"""First-run GPG setup wizard for trace application"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from .crypto import Crypto
|
||||||
|
from .storage import StateManager
|
||||||
|
|
||||||
|
|
||||||
|
def run_gpg_wizard():
|
||||||
|
"""
|
||||||
|
Run the first-time GPG setup wizard.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Settings to save (gpg_enabled, gpg_key_id)
|
||||||
|
"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("Welcome to trace - Forensic Note Taking Tool")
|
||||||
|
print("="*60)
|
||||||
|
print("\nFirst-time setup: GPG Signature Configuration\n")
|
||||||
|
print("trace can digitally sign all notes using GPG for authenticity")
|
||||||
|
print("and integrity verification. This is useful for legal evidence")
|
||||||
|
print("and chain-of-custody documentation.\n")
|
||||||
|
|
||||||
|
# Check if GPG is available
|
||||||
|
gpg_available = Crypto.is_gpg_available()
|
||||||
|
|
||||||
|
if not gpg_available:
|
||||||
|
print("⚠ GPG is not installed or not available on your system.")
|
||||||
|
print("\nTo use GPG signing, please install GPG:")
|
||||||
|
print(" - Linux: apt install gnupg / yum install gnupg")
|
||||||
|
print(" - macOS: brew install gnupg")
|
||||||
|
print(" - Windows: Install Gpg4win (https://gpg4win.org)")
|
||||||
|
print("\nYou can enable GPG signing later by editing ~/.trace/settings.json")
|
||||||
|
print("\nPress Enter to continue without GPG signing...")
|
||||||
|
input()
|
||||||
|
return {"pgp_enabled": False, "gpg_key_id": None}
|
||||||
|
|
||||||
|
# GPG is available - ask if user wants to enable it
|
||||||
|
print("✓ GPG is available on your system.\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
response = input("Do you want to enable GPG signing for notes? (y/n): ").strip().lower()
|
||||||
|
if response in ['y', 'yes']:
|
||||||
|
enable_gpg = True
|
||||||
|
break
|
||||||
|
elif response in ['n', 'no']:
|
||||||
|
enable_gpg = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Please enter 'y' or 'n'")
|
||||||
|
|
||||||
|
if not enable_gpg:
|
||||||
|
print("\nGPG signing disabled. You can enable it later in settings.")
|
||||||
|
return {"pgp_enabled": False, "gpg_key_id": None}
|
||||||
|
|
||||||
|
# List available GPG keys
|
||||||
|
print("\nSearching for GPG secret keys...\n")
|
||||||
|
keys = Crypto.list_gpg_keys()
|
||||||
|
|
||||||
|
if not keys:
|
||||||
|
print("⚠ No GPG secret keys found in your keyring.")
|
||||||
|
print("\nTo use GPG signing, you need to generate a GPG key first:")
|
||||||
|
print(" - Use 'gpg --gen-key' (Linux/macOS)")
|
||||||
|
print(" - Use Kleopatra (Windows)")
|
||||||
|
print("\nAfter generating a key, you can enable GPG signing by editing")
|
||||||
|
print("~/.trace/settings.json and setting 'gpg_enabled': true")
|
||||||
|
print("\nPress Enter to continue without GPG signing...")
|
||||||
|
input()
|
||||||
|
return {"pgp_enabled": False, "gpg_key_id": None}
|
||||||
|
|
||||||
|
# Display available keys
|
||||||
|
print("Available GPG keys:\n")
|
||||||
|
for i, (key_id, user_id) in enumerate(keys, 1):
|
||||||
|
print(f" {i}. {user_id}")
|
||||||
|
print(f" Key ID: {key_id}\n")
|
||||||
|
|
||||||
|
# Let user select a key
|
||||||
|
selected_key = None
|
||||||
|
|
||||||
|
if len(keys) == 1:
|
||||||
|
print(f"Only one key found. Using: {keys[0][1]}")
|
||||||
|
selected_key = keys[0][0]
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
choice = input(f"Select a key (1-{len(keys)}, or 0 to use default key): ").strip()
|
||||||
|
choice_num = int(choice)
|
||||||
|
|
||||||
|
if choice_num == 0:
|
||||||
|
print("Using GPG default key (no specific key ID)")
|
||||||
|
selected_key = None
|
||||||
|
break
|
||||||
|
elif 1 <= choice_num <= len(keys):
|
||||||
|
selected_key = keys[choice_num - 1][0]
|
||||||
|
print(f"Selected: {keys[choice_num - 1][1]}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f"Please enter a number between 0 and {len(keys)}")
|
||||||
|
except ValueError:
|
||||||
|
print("Please enter a valid number")
|
||||||
|
|
||||||
|
print("\n✓ GPG signing enabled!")
|
||||||
|
if selected_key:
|
||||||
|
print(f" Using key: {selected_key}")
|
||||||
|
else:
|
||||||
|
print(" Using default GPG key")
|
||||||
|
|
||||||
|
print("\nSetup complete. Starting trace...\n")
|
||||||
|
|
||||||
|
return {"pgp_enabled": True, "gpg_key_id": selected_key}
|
||||||
|
|
||||||
|
|
||||||
|
def check_and_run_wizard():
|
||||||
|
"""
|
||||||
|
Check if this is first run and run wizard if needed.
|
||||||
|
Returns True if wizard was run, False otherwise.
|
||||||
|
"""
|
||||||
|
state_manager = StateManager()
|
||||||
|
settings = state_manager.get_settings()
|
||||||
|
|
||||||
|
# Check if wizard has already been run (presence of any GPG setting indicates setup was done)
|
||||||
|
if "pgp_enabled" in settings:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# First run - run wizard
|
||||||
|
wizard_settings = run_gpg_wizard()
|
||||||
|
|
||||||
|
# Save settings
|
||||||
|
for key, value in wizard_settings.items():
|
||||||
|
state_manager.set_setting(key, value)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -4,7 +4,7 @@ import time
|
|||||||
import hashlib
|
import hashlib
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List, Optional, Dict
|
from typing import List, Optional, Dict, Tuple
|
||||||
|
|
||||||
from .extractors import TagExtractor, IOCExtractor
|
from .extractors import TagExtractor, IOCExtractor
|
||||||
|
|
||||||
@@ -32,6 +32,23 @@ class Note:
|
|||||||
data = f"{self.timestamp}:{self.content}".encode('utf-8')
|
data = f"{self.timestamp}:{self.content}".encode('utf-8')
|
||||||
self.content_hash = hashlib.sha256(data).hexdigest()
|
self.content_hash = hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
def verify_signature(self) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Verify the GPG signature of this note.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple of (verified: bool, info: str)
|
||||||
|
- verified: True if signature is valid, False if invalid or unsigned
|
||||||
|
- info: Signer information or error/status message
|
||||||
|
"""
|
||||||
|
# Import here to avoid circular dependency
|
||||||
|
from ..crypto import Crypto
|
||||||
|
|
||||||
|
if not self.signature:
|
||||||
|
return False, "unsigned"
|
||||||
|
|
||||||
|
return Crypto.verify_signature(self.signature)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extract_iocs_from_text(text):
|
def extract_iocs_from_text(text):
|
||||||
"""Extract IOCs from text and return as list of (ioc, type) tuples"""
|
"""Extract IOCs from text and return as list of (ioc, type) tuples"""
|
||||||
|
|||||||
123
trace/tui_app.py
123
trace/tui_app.py
@@ -118,6 +118,73 @@ class TUI:
|
|||||||
self.flash_message = msg
|
self.flash_message = msg
|
||||||
self.flash_time = time.time()
|
self.flash_time = time.time()
|
||||||
|
|
||||||
|
def verify_note_signature(self):
|
||||||
|
"""Show detailed verification dialog for current note"""
|
||||||
|
if not self.current_note:
|
||||||
|
return
|
||||||
|
|
||||||
|
verified, info = self.current_note.verify_signature()
|
||||||
|
|
||||||
|
# Prepare dialog content
|
||||||
|
if not self.current_note.signature:
|
||||||
|
title = "Note Signature Status"
|
||||||
|
message = [
|
||||||
|
"This note is unsigned.",
|
||||||
|
"",
|
||||||
|
"To sign notes, enable GPG signing in settings",
|
||||||
|
"and ensure you have a GPG key configured."
|
||||||
|
]
|
||||||
|
elif verified:
|
||||||
|
title = "✓ Signature Verified"
|
||||||
|
message = [
|
||||||
|
"The note's signature is valid.",
|
||||||
|
"",
|
||||||
|
f"Signed by: {info}",
|
||||||
|
"",
|
||||||
|
"This note has not been tampered with since signing."
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
title = "✗ Signature Verification Failed"
|
||||||
|
message = [
|
||||||
|
"The note's signature could not be verified.",
|
||||||
|
"",
|
||||||
|
f"Reason: {info}",
|
||||||
|
"",
|
||||||
|
"Possible causes:",
|
||||||
|
"- Public key not in keyring",
|
||||||
|
"- Note content was modified after signing",
|
||||||
|
"- Signature is corrupted"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Display dialog (reuse pattern from other dialogs)
|
||||||
|
h, w = self.stdscr.getmaxyx()
|
||||||
|
dialog_h = min(len(message) + 6, h - 4)
|
||||||
|
dialog_w = min(max(len(line) for line in [title] + message) + 6, w - 4)
|
||||||
|
start_y = (h - dialog_h) // 2
|
||||||
|
start_x = (w - dialog_w) // 2
|
||||||
|
|
||||||
|
# Create dialog window
|
||||||
|
dialog = curses.newwin(dialog_h, dialog_w, start_y, start_x)
|
||||||
|
dialog.box()
|
||||||
|
|
||||||
|
# Title
|
||||||
|
dialog.attron(curses.A_BOLD)
|
||||||
|
title_x = (dialog_w - len(title)) // 2
|
||||||
|
dialog.addstr(1, title_x, title)
|
||||||
|
dialog.attroff(curses.A_BOLD)
|
||||||
|
|
||||||
|
# Message
|
||||||
|
for i, line in enumerate(message):
|
||||||
|
dialog.addstr(3 + i, 2, line)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
footer = "Press any key to close"
|
||||||
|
footer_x = (dialog_w - len(footer)) // 2
|
||||||
|
dialog.addstr(dialog_h - 2, footer_x, footer, curses.color_pair(3))
|
||||||
|
|
||||||
|
dialog.refresh()
|
||||||
|
dialog.getch()
|
||||||
|
|
||||||
def _save_nav_position(self):
|
def _save_nav_position(self):
|
||||||
"""Save current navigation position before changing views"""
|
"""Save current navigation position before changing views"""
|
||||||
# Create a key based on current view and context
|
# Create a key based on current view and context
|
||||||
@@ -291,6 +358,22 @@ class TUI:
|
|||||||
|
|
||||||
return ellipsis[:max_width]
|
return ellipsis[:max_width]
|
||||||
|
|
||||||
|
def _get_verification_symbol(self, note):
|
||||||
|
"""
|
||||||
|
Get verification symbol for a note: ✓ (verified), ✗ (failed), ? (unsigned)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
note: The Note object to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Verification symbol
|
||||||
|
"""
|
||||||
|
if not note.signature:
|
||||||
|
return "?"
|
||||||
|
|
||||||
|
verified, _ = note.verify_signature()
|
||||||
|
return "✓" if verified else "✗"
|
||||||
|
|
||||||
def _display_line_with_highlights(self, y, x_start, line, is_selected=False, win=None):
|
def _display_line_with_highlights(self, y, x_start, line, is_selected=False, win=None):
|
||||||
"""
|
"""
|
||||||
Display a line with intelligent highlighting.
|
Display a line with intelligent highlighting.
|
||||||
@@ -823,7 +906,9 @@ class TUI:
|
|||||||
|
|
||||||
# Format note content
|
# Format note content
|
||||||
note_content = note.content.replace('\n', ' ').replace('\r', ' ')
|
note_content = note.content.replace('\n', ' ').replace('\r', ' ')
|
||||||
display_str = f"- {note_content}"
|
# Add verification symbol
|
||||||
|
verify_symbol = self._get_verification_symbol(note)
|
||||||
|
display_str = f"{verify_symbol} {note_content}"
|
||||||
display_str = self._safe_truncate(display_str, self.width - 6)
|
display_str = self._safe_truncate(display_str, self.width - 6)
|
||||||
|
|
||||||
# Display with smart highlighting (IOCs take priority over selection)
|
# Display with smart highlighting (IOCs take priority over selection)
|
||||||
@@ -888,7 +973,9 @@ class TUI:
|
|||||||
note = notes[idx]
|
note = notes[idx]
|
||||||
# Replace newlines with spaces for single-line display
|
# Replace newlines with spaces for single-line display
|
||||||
note_content = note.content.replace('\n', ' ').replace('\r', ' ')
|
note_content = note.content.replace('\n', ' ').replace('\r', ' ')
|
||||||
display_str = f"- {note_content}"
|
# Add verification symbol
|
||||||
|
verify_symbol = self._get_verification_symbol(note)
|
||||||
|
display_str = f"{verify_symbol} {note_content}"
|
||||||
# Truncate safely for Unicode
|
# Truncate safely for Unicode
|
||||||
display_str = self._safe_truncate(display_str, self.width - 6)
|
display_str = self._safe_truncate(display_str, self.width - 6)
|
||||||
|
|
||||||
@@ -982,7 +1069,9 @@ class TUI:
|
|||||||
if len(content_preview) > 50:
|
if len(content_preview) > 50:
|
||||||
content_preview = content_preview[:50] + "..."
|
content_preview = content_preview[:50] + "..."
|
||||||
|
|
||||||
display_str = f"[{timestamp_str}] {content_preview}"
|
# Add verification symbol
|
||||||
|
verify_symbol = self._get_verification_symbol(note)
|
||||||
|
display_str = f"{verify_symbol} [{timestamp_str}] {content_preview}"
|
||||||
display_str = self._safe_truncate(display_str, self.width - 6)
|
display_str = self._safe_truncate(display_str, self.width - 6)
|
||||||
|
|
||||||
if idx == self.selected_index:
|
if idx == self.selected_index:
|
||||||
@@ -1080,7 +1169,9 @@ class TUI:
|
|||||||
timestamp_str = time.ctime(note.timestamp)
|
timestamp_str = time.ctime(note.timestamp)
|
||||||
content_preview = note.content[:60].replace('\n', ' ') + "..." if len(note.content) > 60 else note.content.replace('\n', ' ')
|
content_preview = note.content[:60].replace('\n', ' ') + "..." if len(note.content) > 60 else note.content.replace('\n', ' ')
|
||||||
|
|
||||||
display_str = f"[{timestamp_str}] {content_preview}"
|
# Add verification symbol
|
||||||
|
verify_symbol = self._get_verification_symbol(note)
|
||||||
|
display_str = f"{verify_symbol} [{timestamp_str}] {content_preview}"
|
||||||
display_str = self._safe_truncate(display_str, self.width - 6)
|
display_str = self._safe_truncate(display_str, self.width - 6)
|
||||||
|
|
||||||
if idx == self.selected_index:
|
if idx == self.selected_index:
|
||||||
@@ -1150,12 +1241,26 @@ class TUI:
|
|||||||
self.stdscr.addstr(current_y, 2, f"Hash: {hash_display}", curses.A_DIM)
|
self.stdscr.addstr(current_y, 2, f"Hash: {hash_display}", curses.A_DIM)
|
||||||
current_y += 1
|
current_y += 1
|
||||||
|
|
||||||
# Signature
|
# Signature and verification status
|
||||||
if self.current_note.signature:
|
if self.current_note.signature:
|
||||||
self.stdscr.addstr(current_y, 2, "Signature: [GPG signed]", curses.color_pair(2))
|
verified, info = self.current_note.verify_signature()
|
||||||
|
if verified:
|
||||||
|
sig_display = f"Signature: ✓ Verified ({info})"
|
||||||
|
self.stdscr.addstr(current_y, 2, sig_display, curses.color_pair(2))
|
||||||
|
else:
|
||||||
|
if info == "unsigned":
|
||||||
|
sig_display = "Signature: ? Unsigned"
|
||||||
|
self.stdscr.addstr(current_y, 2, sig_display, curses.color_pair(3))
|
||||||
|
else:
|
||||||
|
sig_display = f"Signature: ✗ Failed ({info})"
|
||||||
|
self.stdscr.addstr(current_y, 2, sig_display, curses.color_pair(4))
|
||||||
|
current_y += 1
|
||||||
|
else:
|
||||||
|
# No signature present
|
||||||
|
self.stdscr.addstr(current_y, 2, "Signature: ? Unsigned", curses.color_pair(3))
|
||||||
current_y += 1
|
current_y += 1
|
||||||
|
|
||||||
self.stdscr.addstr(self.height - 3, 2, "[d] Delete [b] Back", curses.color_pair(3))
|
self.stdscr.addstr(self.height - 3, 2, "[d] Delete [b] Back [v] Verify", curses.color_pair(3))
|
||||||
|
|
||||||
def draw_help(self):
|
def draw_help(self):
|
||||||
"""Draw the help screen with keyboard shortcuts and features"""
|
"""Draw the help screen with keyboard shortcuts and features"""
|
||||||
@@ -1633,6 +1738,10 @@ class TUI:
|
|||||||
self.view_evidence_notes()
|
self.view_evidence_notes()
|
||||||
else:
|
else:
|
||||||
self.view_evidence_notes()
|
self.view_evidence_notes()
|
||||||
|
elif self.current_view == "note_detail":
|
||||||
|
# Verify signature in note detail view
|
||||||
|
if self.current_note:
|
||||||
|
self.verify_note_signature()
|
||||||
|
|
||||||
# Delete
|
# Delete
|
||||||
elif key == ord('d'):
|
elif key == ord('d'):
|
||||||
|
|||||||
Reference in New Issue
Block a user