fieldwitness/frontends/web/templates/stego/generate.html
Aaron D. Lee 490f9d4a1d Rebrand SooSeF to FieldWitness
Complete project rebrand for better positioning in the press freedom
and digital security space. FieldWitness communicates both field
deployment and evidence testimony — appropriate for the target audience
of journalists, NGOs, and human rights organizations.

Rename mapping:
- soosef → fieldwitness (package, CLI, all imports)
- soosef.stegasoo → fieldwitness.stego
- soosef.verisoo → fieldwitness.attest
- ~/.soosef/ → ~/.fwmetadata/ (innocuous data dir name)
- SOOSEF_DATA_DIR → FIELDWITNESS_DATA_DIR
- SoosefConfig → FieldWitnessConfig
- SoosefError → FieldWitnessError

Also includes:
- License switch from MIT to GPL-3.0
- C2PA bridge module (Phase 0-2 MVP): cert.py, export.py, vendor_assertions.py
- README repositioned to lead with provenance/federation, stego backgrounded
- Threat model skeleton at docs/security/threat-model.md
- Planning docs: docs/planning/c2pa-integration.md, docs/planning/gtm-feasibility.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:05:13 -04:00

536 lines
25 KiB
HTML

{% extends "base.html" %}
{% block title %}Generate Credentials - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center" data-page="generate">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-key-fill me-2"></i>Generate Credentials</h5>
</div>
<div class="card-body">
{% if not generated %}
<!-- Generation Form -->
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-4">
<label class="form-label">Words per Passphrase</label>
<input type="range" class="form-range" name="words_per_passphrase"
min="{{ min_passphrase_words }}" max="12" value="{{ default_passphrase_words }}" id="wordsRange">
<div class="d-flex justify-content-between small text-muted">
<span>{{ min_passphrase_words }} (~33 bits)</span>
<span id="wordsValue" class="text-primary fw-bold">{{ default_passphrase_words }} words (~44 bits)</span>
<span>12 (132 bits)</span>
</div>
<div class="form-text">
<i class="bi bi-shield-check me-1"></i>
Recommended: <strong>{{ recommended_passphrase_words }}+ words</strong> for good security
</div>
</div>
<hr>
<h6 class="text-muted mb-3">SECURITY FACTORS <span class="text-warning small">(select at least one)</span></h6>
<div class="row">
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="use_pin"
id="usePinCheck" checked>
<label class="form-check-label" for="usePinCheck">
<i class="bi bi-123 me-1"></i> Generate PIN
</label>
</div>
<div class="mt-2" id="pinOptions">
<label class="form-label small">PIN Length</label>
<select name="pin_length" class="form-select form-select-sm">
<option value="6" selected>6 digits (~20 bits)</option>
<option value="7">7 digits (~23 bits)</option>
<option value="8">8 digits (~26 bits)</option>
<option value="9">9 digits (~30 bits)</option>
</select>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="use_rsa"
id="useRsaCheck">
<label class="form-check-label" for="useRsaCheck">
<i class="bi bi-file-earmark-lock me-1"></i> Generate RSA Key
</label>
</div>
<div class="mt-2 d-none" id="rsaOptions">
<label class="form-label small">Key Size</label>
<select name="rsa_bits" class="form-select form-select-sm" id="rsaBitsSelect">
<option value="2048" selected>2048 bits (~128 bits entropy)</option>
<option value="3072">3072 bits (~128 bits entropy)</option>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100 mt-4">
<i class="bi bi-shuffle me-2"></i>Generate Credentials
</button>
</form>
<!-- Channel Key Accordion (Advanced) -->
<div class="accordion mt-4" id="advancedAccordion">
<div class="accordion-item bg-dark">
<h2 class="accordion-header">
<button class="accordion-button collapsed bg-dark text-light" type="button"
data-bs-toggle="collapse" data-bs-target="#channelKeyCollapse">
<i class="bi bi-broadcast me-2"></i>Channel Key
<span class="badge bg-info ms-2">Advanced</span>
</button>
</h2>
<div id="channelKeyCollapse" class="accordion-collapse collapse" data-bs-parent="#advancedAccordion">
<div class="accordion-body">
<p class="text-muted small mb-3">
Channel keys create private encoding channels. Only users with the same key can decode each other's images.
<a href="{{ url_for('about') }}#channel-keys" class="text-info">Learn more</a>
</p>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" class="form-control font-monospace" id="channelKeyGenerated"
placeholder="Click Generate to create a key" readonly>
<button class="btn btn-outline-primary" type="button" id="generateChannelKeyBtn" title="Generate Channel Key">
<i class="bi bi-shuffle"></i>
</button>
<button class="btn btn-outline-secondary" type="button" id="copyChannelKeyBtn" disabled title="Copy to clipboard">
<i class="bi bi-clipboard"></i>
</button>
</div>
<div class="form-text mt-2">
<i class="bi bi-info-circle me-1"></i>
After generating, configure this key in your server's environment or use <strong>Custom</strong> channel mode when encoding/decoding.
</div>
</div>
</div>
</div>
</div>
{% else %}
<!-- Generated Credentials Display -->
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Memorize these credentials!</strong> They will not be shown again.
<br><small>Do not screenshot or save to an unencrypted file.</small>
</div>
{% if pin %}
<div class="mb-4">
<h6 class="text-muted"><i class="bi bi-123 me-2"></i>STATIC PIN</h6>
<div class="text-center">
<div class="pin-container d-inline-block">
<div class="pin-digits-row" id="pinDigits">
{% for digit in pin %}
<span class="pin-digit-box">{{ digit }}</span>
{% endfor %}
</div>
<div class="pin-buttons mt-3">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="togglePinVisibility()">
<i class="bi bi-eye-slash" id="pinToggleIcon"></i>
<span id="pinToggleText">Hide</span>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="copyPin()">
<i class="bi bi-clipboard" id="pinCopyIcon"></i>
<span id="pinCopyText">Copy</span>
</button>
</div>
</div>
</div>
</div>
{% endif %}
<div class="mb-4">
<h6 class="text-muted">
<i class="bi bi-chat-quote me-2"></i>PASSPHRASE
</h6>
<div class="passphrase-container">
<div class="passphrase-display" id="passphraseDisplay">
<code class="passphrase-text">{{ passphrase }}</code>
</div>
<div class="passphrase-buttons mt-3">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="togglePassphraseVisibility()">
<i class="bi bi-eye-slash" id="passphraseToggleIcon"></i>
<span id="passphraseToggleText">Hide</span>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="copyPassphrase()">
<i class="bi bi-clipboard" id="passphraseCopyIcon"></i>
<span id="passphraseCopyText">Copy</span>
</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="toggleMemoryAid()">
<i class="bi bi-lightbulb" id="memoryAidIcon"></i>
<span id="memoryAidText">Memory Aid</span>
</button>
</div>
</div>
<!-- Memory Aid Story -->
<div class="memory-aid-container mt-3 d-none" id="memoryAidContainer">
<div class="card bg-dark border-primary">
<div class="card-header bg-primary text-white">
<i class="bi bi-book me-2"></i>Memory Story
</div>
<div class="card-body">
<p class="memory-story mb-3" id="memoryStory">
<!-- Story will be generated by JavaScript -->
</p>
<div class="form-text">
<i class="bi bi-info-circle me-1"></i>
This story is generated from your passphrase to help you remember it.
The words appear in order within the narrative.
</div>
<button type="button" class="btn btn-sm btn-outline-light mt-2" onclick="regenerateStory()">
<i class="bi bi-arrow-repeat me-1"></i>Generate Different Story
</button>
</div>
</div>
</div>
<div class="alert alert-info mt-3 mb-0">
<small class="text-muted">
({{ words_per_passphrase }} words = ~{{ passphrase_entropy }} bits entropy)
</small>
</div>
</div>
{% if rsa_key_pem %}
<div class="mb-4">
<h6 class="text-muted"><i class="bi bi-file-earmark-lock me-2"></i>RSA PRIVATE KEY ({{ rsa_bits }} bits)</h6>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#keyTextTab" type="button">
<i class="bi bi-file-text me-1"></i>PEM Text
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#keyDownloadTab" type="button">
<i class="bi bi-download me-1"></i>Download
</button>
</li>
{% if has_qrcode and qr_token %}
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#keyQrTab" type="button">
<i class="bi bi-qr-code me-1"></i>QR Code
</button>
</li>
{% endif %}
</ul>
<div class="tab-content border border-top-0 rounded-bottom p-3 bg-dark">
<!-- PEM Text Tab -->
<div class="tab-pane fade show active" id="keyTextTab" role="tabpanel">
<pre class="bg-black p-2 rounded small mb-2" style="max-height: 200px; overflow-y: auto;"><code id="rsaKeyDisplay">{{ rsa_key_pem }}</code></pre>
<button class="btn btn-sm btn-outline-light"
onclick="navigator.clipboard.writeText(document.getElementById('rsaKeyDisplay').textContent)">
<i class="bi bi-clipboard me-1"></i>Copy to Clipboard
</button>
</div>
<!-- Download Tab -->
<div class="tab-pane fade" id="keyDownloadTab" role="tabpanel">
<form action="{{ url_for('download_key') }}" method="POST" class="row g-2 align-items-end">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="key_pem" value="{{ rsa_key_pem }}">
<div class="col-md-8">
<label class="form-label small">Password to encrypt the key file</label>
<input type="password" name="key_password" class="form-control"
placeholder="Min 8 characters" minlength="8" required>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-download me-1"></i>Download .pem
</button>
</div>
</form>
<div class="form-text mt-2">
The downloaded file will be password-protected (AES-256 encrypted).
</div>
</div>
{% if has_qrcode and qr_token %}
<!-- QR Code Tab -->
<div class="tab-pane fade" id="keyQrTab" role="tabpanel">
<div class="text-center">
<p class="small text-muted mb-3">
Scan this QR code to transfer the RSA key to another device.
<br><strong>Warning:</strong> This is the unencrypted private key!
</p>
<div class="qr-container d-inline-block p-3 bg-white rounded mb-3">
<img src="{{ url_for('generate_qr', token=qr_token) }}"
alt="RSA Key QR Code"
class="img-fluid"
style="max-width: 300px;"
id="qrCodeImage">
</div>
<div>
<a href="{{ url_for('generate_qr_download', token=qr_token) }}"
class="btn btn-outline-primary">
<i class="bi bi-download me-1"></i>Download QR Code
</a>
<button class="btn btn-outline-secondary ms-2" onclick="printQrCode()">
<i class="bi bi-printer me-1"></i>Print
</button>
</div>
<div class="alert alert-warning small mt-3 mb-0 text-start">
<i class="bi bi-shield-exclamation me-1"></i>
<strong>Security note:</strong> The QR code contains your unencrypted private key.
Only scan in a secure environment. Consider using the password-protected download instead.
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="mb-4">
<h6 class="text-muted"><i class="bi bi-shield-check me-2"></i>SECURITY SUMMARY</h6>
<div class="row text-center">
<div class="col">
<div class="p-2 bg-dark rounded">
<div class="small text-muted">Passphrase</div>
<div class="fs-5 text-info">{{ passphrase_entropy }} bits</div>
<div class="small text-muted">{{ words_per_passphrase }} words</div>
</div>
</div>
{% if pin_entropy %}
<div class="col">
<div class="p-2 bg-dark rounded">
<div class="small text-muted">PIN</div>
<div class="fs-5 text-warning">{{ pin_entropy }} bits</div>
</div>
</div>
{% endif %}
{% if rsa_entropy %}
<div class="col">
<div class="p-2 bg-dark rounded">
<div class="small text-muted">RSA</div>
<div class="fs-5 text-primary">{{ rsa_entropy }} bits</div>
</div>
</div>
{% endif %}
<div class="col">
<div class="p-2 bg-dark rounded">
<div class="small text-muted">Total</div>
<div class="fs-5 text-success">{{ total_entropy }} bits</div>
</div>
</div>
</div>
<div class="form-text text-center mt-2">
+ reference photo entropy (~80-256 bits)
</div>
</div>
<div class="d-grid gap-2">
<a href="{{ url_for('generate') }}" class="btn btn-outline-primary">
<i class="bi bi-arrow-repeat me-2"></i>Generate New Credentials
</a>
<a href="{{ url_for('encode') }}" class="btn btn-success">
<i class="bi bi-lock me-2"></i>Start Encoding
</a>
</div>
{% endif %}
</div>
</div>
{% if not generated %}
<div class="card mt-4">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="bi bi-info-circle me-2"></i>About Credentials</h6>
<ul class="small text-muted mb-0">
<li class="mb-2">
<strong>Passphrase</strong> is a single phrase you use each time
</li>
<li class="mb-2">
<strong>PIN</strong> is static and adds another factor both parties must know
</li>
<li class="mb-2">
<strong>RSA key</strong> adds asymmetric cryptography for additional security
</li>
<li class="mb-0">
You need <strong>at least one</strong> of PIN or RSA key (or both)
</li>
</ul>
</div>
</div>
{% endif %}
</div>
</div>
<style>
.pin-container {
background: linear-gradient(145deg, #1e1e2e 0%, #2d2d44 100%);
border: 1px solid #ffc107;
border-radius: 16px;
padding: 1.5rem 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3), 0 0 40px rgba(255, 193, 7, 0.1);
}
.pin-digits-row {
display: flex;
justify-content: center;
gap: 0.5rem;
}
.pin-digit-box {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3.5rem;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 1.75rem;
font-weight: bold;
color: #ffc107;
text-shadow: 0 0 10px rgba(255, 193, 7, 0.5);
transition: filter 0.3s ease, transform 0.2s ease;
}
.pin-digit-box:hover {
transform: translateY(-2px);
border-color: rgba(255, 193, 7, 0.6);
}
.pin-digits-row.blurred .pin-digit-box {
filter: blur(8px);
user-select: none;
}
.pin-buttons .btn {
min-width: 80px;
}
/* Passphrase Container */
.passphrase-container {
background: linear-gradient(145deg, #1e1e2e 0%, #2d2d44 100%);
border: 1px solid #0dcaf0;
border-radius: 16px;
padding: 1.5rem 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3), 0 0 40px rgba(13, 202, 240, 0.1);
}
.passphrase-display {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(13, 202, 240, 0.3);
border-radius: 12px;
padding: 1.5rem;
text-align: center;
transition: filter 0.3s ease;
}
.passphrase-display.blurred {
filter: blur(8px);
user-select: none;
}
.passphrase-text {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 1.5rem;
font-weight: bold;
color: #0dcaf0;
text-shadow: 0 0 10px rgba(13, 202, 240, 0.5);
word-wrap: break-word;
display: block;
line-height: 1.6;
}
.passphrase-buttons {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.passphrase-buttons .btn {
min-width: 100px;
}
/* Memory Aid */
.memory-story {
font-size: 1.1rem;
line-height: 1.8;
color: #e9ecef;
}
.memory-story .passphrase-word {
font-weight: bold;
color: #0dcaf0;
text-decoration: underline;
text-decoration-style: wavy;
text-decoration-color: rgba(13, 202, 240, 0.5);
}
/* Responsive */
@media (max-width: 576px) {
.pin-container, .passphrase-container {
padding: 1rem 0.75rem;
}
.pin-digit-box {
width: 1.9rem;
height: 2.4rem;
font-size: 1.15rem;
}
.pin-digits-row {
gap: 0.25rem;
}
.passphrase-text {
font-size: 1.2rem;
}
.memory-story {
font-size: 1rem;
}
}
</style>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/fieldwitness.js') }}"></script>
<script src="{{ url_for('static', filename='js/generate.js') }}"></script>
{% if generated %}
<script>
// Page-specific data from Jinja
const passphraseWords = '{{ passphrase|default("", true) }}'.split(' ').filter(w => w.length > 0);
function copyPin() {
Stego.copyToClipboard(
'{{ pin|default("", true) }}',
document.getElementById('pinCopyIcon'),
document.getElementById('pinCopyText')
);
}
function copyPassphrase() {
Stego.copyToClipboard(
'{{ passphrase|default("", true) }}',
document.getElementById('passphraseCopyIcon'),
document.getElementById('passphraseCopyText')
);
}
function toggleMemoryAid() {
StegoGenerate.toggleMemoryAid(passphraseWords);
}
function regenerateStory() {
StegoGenerate.regenerateStory(passphraseWords);
}
</script>
{% endif %}
{% endblock %}