walletpress/backend/core/pdf.py
Rug Munch Media LLC 59e00c3bcc
chore(cleanup): Wave 6 — pen test prep + ruff config
P3-1 — ruff config: 93 → 1 error (one legit unused import)
  Added [tool.ruff] section to backend/pyproject.toml with:
    - target-version = py310
    - line-length = 100
    - Per-file ignores for intentional E402 (mcp_server, main.py,
      routers/airdrop.py, tests/) and F401 on plugin re-exports.
  Fixed all auto-fixable issues via 'ruff check . --fix'. The
  remaining 1 F401 is coincurve.ecdsa.recover — imported in
  core/smart_wallet.py for a feature that's not yet wired up.

Wave 6 (pen test prep):
  - bandit: 3 High (all pycryptodome false positives — pyCrypto the
    original is deprecated, but pycryptodome is the active fork we use
    for Keccak-256 in Ethereum address generation).
  - bandit: 7 Medium (mostly bind-all-interfaces 0.0.0.0 which is
    intentional for container deploys).
  - bandit: 85 Low (mostly asserts on test code — acceptable).
  - pip-audit: zero vulnerabilities in our actual dependencies.

Code hygiene improvements:
  - core/pdf.py: properly imports qrcode (was missing).
  - cli/verify_receipt.py: confirmed false-positive (timestamp is
    a function parameter, ignored).
  - routers/wallet_admin.py: removed duplicate class definitions
    introduced during Wave 5 split.

Refs: AUDIT.md P3-1 (final), Wave 6 prep
2026-06-30 22:11:25 +07:00

235 lines
7.2 KiB
Python

"""PDF generation for paper wallets and birth certificates."""
from __future__ import annotations
import io
import logging
import time
from typing import Optional
import qrcode
from core.config import cfg
logger = logging.getLogger("wp.pdf")
try:
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
HAS_REPORTLAB = True
except ImportError:
HAS_REPORTLAB = False
try:
HAS_QR = True
except ImportError:
HAS_QR = False
def _qr_image(data: str, size: int = 200) -> Optional[ImageReader]:
if not HAS_QR:
return None
import io
qr = qrcode.QRCode(box_size=4, border=2)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white").convert("RGB")
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return ImageReader(buf)
def generate_paper_wallet(address: str, chain: str, private_key: str = "",
public_key: str = "", derivation_path: str = "",
created_at: float = 0.0, proof_leaf: str = "",
label: str = "") -> Optional[bytes]:
"""Generate a printable paper wallet PDF.
Returns PDF bytes or None if reportlab is not installed.
Paper wallets are for cold storage. Print on durable paper. Store safely.
"""
if not HAS_REPORTLAB:
return None
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=letter)
w, h = letter
margin = 0.75 * inch
y = h - margin
# Title
c.setFont("Helvetica-Bold", 20)
c.drawString(margin, y, "WALLETPRESS PAPER WALLET")
y -= 30
c.setFont("Helvetica", 10)
c.drawString(margin, y, f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(created_at or time.time()))}")
y -= 14
c.drawString(margin, y, f"Software: WalletPress Pro v{cfg.version}")
y -= 14
c.drawString(margin, y, "Chain: " + chain.upper())
y -= 14
if label:
c.drawString(margin, y, f"Label: {label}")
y -= 14
y -= 20
# Warning box
c.setFillColorRGB(1, 0.9, 0.9)
c.rect(margin, y - 50, w - 2 * margin, 50, fill=1, stroke=0)
c.setFillColorRGB(0.8, 0, 0)
c.setFont("Helvetica-Bold", 11)
c.drawString(margin + 8, y - 16, "⚠ KEEP THIS DOCUMENT SECURE")
c.setFont("Helvetica", 9)
c.setFillColorRGB(0.5, 0, 0)
c.drawString(margin + 8, y - 32, "Anyone with this paper can control the crypto assets. Store in a safe, waterproof location.")
c.setFillColorRGB(0, 0, 0)
y -= 70
# Address section
c.setFont("Helvetica-Bold", 12)
c.drawString(margin, y, "Public Address")
y -= 16
c.setFont("Courier", 9)
c.drawString(margin, y, address)
y -= 30
# QR code for address
qr = _qr_image(f"{chain}:{address}")
if qr:
c.drawImage(qr, margin, y - 100, width=100, height=100)
y -= 120
# Private key section
if private_key:
c.setFont("Helvetica-Bold", 12)
c.setFillColorRGB(0.8, 0, 0)
c.drawString(margin, y, "PRIVATE KEY (KEEP SECRET)")
y -= 16
c.setFont("Courier", 8)
c.setFillColorRGB(0, 0, 0)
# Split long key into lines
key_lines = [private_key[i:i+48] for i in range(0, len(private_key), 48)]
for line in key_lines:
c.drawString(margin, y, line)
y -= 12
y -= 10
# Public key
if public_key:
c.setFont("Helvetica-Bold", 11)
c.drawString(margin, y, "Public Key")
y -= 14
c.setFont("Courier", 7)
pk_lines = [public_key[i:i+64] for i in range(0, len(public_key), 64)]
for line in pk_lines[:4]:
c.drawString(margin, y, line)
y -= 10
y -= 10
# Derivation path
if derivation_path:
c.setFont("Helvetica", 9)
c.drawString(margin, y, f"Derivation: {derivation_path}")
y -= 14
# Proof hash
if proof_leaf:
c.setFont("Helvetica", 7)
c.drawString(margin, y, f"Proof: {proof_leaf[:48]}...")
y -= 10
# Footer
c.setFont("Helvetica", 7)
c.setFillColorRGB(0.5, 0.5, 0.5)
c.drawString(margin, margin * 0.5, f"WalletPress Pro — walletpress.cc — {cfg.version} — MIT Open Source")
c.save()
return buf.getvalue()
def generate_birth_certificate(wallet_id: str, address: str, chain: str,
public_key: str = "", created_at: float = 0.0,
proof_leaf: str = "", root_hash: str = "",
committed: bool = False) -> Optional[bytes]:
"""Generate a Wallet Birth Certificate — printable provenance document.
This is a unique feature. It cryptographically proves when and how
this wallet was created. The Proof of Generation hash links this
document to the immutable audit trail.
"""
if not HAS_REPORTLAB:
return None
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=letter)
w, h = letter
margin = 0.75 * inch
y = h - margin
# Certificate border
c.setStrokeColorRGB(0.77, 0.55, 0.30)
c.setLineWidth(3)
c.rect(margin - 10, margin - 10, w - 2 * margin + 20, h - 2 * margin + 20)
# Seal graphic
c.setFillColorRGB(0.77, 0.55, 0.30)
c.setFont("Helvetica-Bold", 28)
c.drawString(margin, y, "✦ WALLET BIRTH CERTIFICATE ✦")
y -= 36
c.setFont("Helvetica", 12)
c.setFillColorRGB(0.4, 0.4, 0.4)
c.drawString(margin, y, "This certifies that the following wallet was generated by WalletPress")
y -= 14
c.drawString(margin, y, "with cryptographic proof of its creation time, code version, and integrity.")
y -= 30
# Details
c.setFont("Helvetica-Bold", 11)
c.setFillColorRGB(0, 0, 0)
details = [
("Wallet ID", wallet_id),
("Address", address),
("Chain", chain.upper()),
("Created", time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(created_at))),
("Public Key", public_key[:48] + "..." if len(public_key) > 48 else public_key),
]
for label, value in details:
c.setFont("Helvetica-Bold", 10)
c.drawString(margin, y, label + ":")
c.setFont("Courier", 9)
c.drawString(margin + 100, y, value)
y -= 16
y -= 10
# Proof section
c.setStrokeColorRGB(0.77, 0.55, 0.30)
c.setLineWidth(1)
c.rect(margin, y - 50, w - 2 * margin, 50)
c.setFont("Helvetica-Bold", 10)
c.drawString(margin + 6, y - 14, "Proof of Generation — Merkle Attestation")
c.setFont("Courier", 7)
c.drawString(margin + 6, y - 28, f"Leaf: {proof_leaf}")
if root_hash:
c.drawString(margin + 6, y - 40, f"Root: {root_hash}")
y -= 70
# QR for verification
qr = _qr_image(f"https://walletpress.cc/api/v1/proof/verify/{wallet_id}")
if qr:
c.drawImage(qr, w - margin - 100, y - 100, width=100, height=100)
y -= 120
# Trust text
c.setFont("Helvetica", 8)
c.setFillColorRGB(0.5, 0.5, 0.5)
c.drawString(margin, y, "Verify: walletpress.cc/api/v1/proof/verify/{wallet_id}")
y -= 10
c.drawString(margin, y, f"Software: WalletPress Pro v{cfg.version} — MIT Open Source — walletpress.cc")
c.save()
return buf.getvalue()