hgfhfd
This commit is contained in:
+29
-15
@@ -15,14 +15,22 @@
|
||||
<span class="pv-stat-label">IP logging</span>
|
||||
@if(pv.log_ips)<span class="pv-pill is-on">On</span>@else<span class="pv-pill is-off">Off</span>@endif
|
||||
</div>
|
||||
@if(pv.log_ips)
|
||||
<div class="pv-stat">
|
||||
<span class="pv-stat-label">IP storage</span>
|
||||
@if(pv.ip_mode === 'hashed')<span class="pv-pill is-good">Hashed (HMAC-SHA256)</span>@elseif(pv.ip_mode === 'raw')<span class="pv-pill is-warn">Plain text</span>@else<span class="pv-pill is-good">Not stored</span>@endif
|
||||
@if(pv.hash_ips)<span class="pv-pill is-good">Hashed (HMAC-SHA256)</span>@else<span class="pv-pill is-warn">Plain text</span>@endif
|
||||
</div>
|
||||
@endif
|
||||
<div class="pv-stat">
|
||||
<span class="pv-stat-label">Anonymous login</span>
|
||||
@if(pv.anon)<span class="pv-pill is-on">Enabled</span>@else<span class="pv-pill is-off">Disabled</span>@endif
|
||||
</div>
|
||||
@if(pv.anon)
|
||||
<div class="pv-stat">
|
||||
<span class="pv-stat-label">Device fingerprinting</span>
|
||||
@if(pv.hw)<span class="pv-pill is-on">On</span>@else<span class="pv-pill is-good">Off</span>@endif
|
||||
</div>
|
||||
@endif
|
||||
<div class="pv-stat">
|
||||
<span class="pv-stat-label">Transport</span>
|
||||
@if(pv.https)<span class="pv-pill is-good">HTTPS</span>@else<span class="pv-pill is-warn">HTTP</span>@endif
|
||||
@@ -31,7 +39,7 @@
|
||||
<p class="pv-small">
|
||||
@if(pv.ip_mode === 'hashed')IP addresses are logged, but only ever written to the database as <code>HMAC-SHA256(ip, server_secret)</code>. The raw address is not persisted.@endif
|
||||
@if(pv.ip_mode === 'raw')IP addresses are logged and written to the database in plain text.@endif
|
||||
@if(pv.ip_mode === 'off')IP addresses are not written to the database. They are only held in memory while a request is processed (e.g. to check bans and rate limits).@endif
|
||||
@if(pv.ip_mode === 'off')IP addresses are not stored. The only exception is brute-force protection: login attempts keep a keyed hash of the IP for {{ pv.ret.login }}, and a moderator ban stores the hash of the banned address.@endif
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -41,21 +49,21 @@
|
||||
<p>A cleanup job runs hourly and deletes or blanks data older than these periods.</p>
|
||||
<div class="pv-table">
|
||||
<div class="pv-row pv-row-head"><span>Data</span><span>Kept for</span><span>What happens after</span></div>
|
||||
<div class="pv-row"><span>Stored IP addresses</span><span class="@if(pv.ret_on.ip)pv-ok@else pv-warn@endif">{{ pv.ret.ip }}</span><span><code>user_ips</code> rows deleted; IP columns on sessions, anonymous identities, uploads, comments, reports and ToS acceptances set to <code>NULL</code>.</span></div>
|
||||
<div class="pv-row"><span>Anonymous activity log</span><span class="@if(pv.ret_on.activity)pv-ok@else pv-warn@endif">{{ pv.ret.activity }}</span><span>Rows deleted (action, IP, identity and device fingerprint).</span></div>
|
||||
@if(pv.log_ips)<div class="pv-row"><span>Stored IP addresses</span><span class="@if(pv.ret_on.ip)pv-ok@else pv-warn@endif">{{ pv.ret.ip }}</span><span><code>user_ips</code> rows deleted; IP columns on sessions, anonymous identities, uploads, comments, reports and ToS acceptances set to <code>NULL</code>.</span></div>@endif
|
||||
<div class="pv-row"><span>Anonymous activity log</span><span class="@if(pv.ret_on.activity)pv-ok@else pv-warn@endif">{{ pv.ret.activity }}</span><span>Rows deleted (action, identity fingerprint@if(pv.hw), device fingerprint@endif@if(pv.log_ips), IP@endif).</span></div>
|
||||
<div class="pv-row"><span>Login attempts</span><span class="@if(pv.ret_on.login)pv-ok@else pv-warn@endif">{{ pv.ret.login }}</span><span>Rows deleted (hashed IP, attempted username, result).</span></div>
|
||||
<div class="pv-row"><span>Unused sessions</span><span class="@if(pv.ret_on.sessions)pv-ok@else pv-warn@endif">{{ pv.ret.sessions }}</span><span>Session deleted after this long without use; that device is logged out.</span></div>
|
||||
<div class="pv-row"><span>Device fingerprint</span><span class="@if(pv.ret_on.fp)pv-ok@else pv-warn@endif">{{ pv.ret.fp }}</span><span>Cleared from anonymous identities that haven't been used for this long.</span></div>
|
||||
@if(pv.hw)<div class="pv-row"><span>Device fingerprint</span><span class="@if(pv.ret_on.fp)pv-ok@else pv-warn@endif">{{ pv.ret.fp }}</span><span>Cleared from anonymous identities that haven't been used for this long.</span></div>@endif
|
||||
<div class="pv-row"><span>Active bans</span><span>Until expiry</span><span>Banned IP hashes and fingerprints are kept until the ban expires or is lifted.</span></div>
|
||||
<div class="pv-row"><span>Your content</span><span>Until deleted</span><span>Uploads, comments, favourites and your account itself.</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- ── IP addresses ── --}}
|
||||
{{-- ── IP addresses (only when IPs are stored) ── --}}
|
||||
@if(pv.log_ips)
|
||||
<section class="pv-sec">
|
||||
<h2><i class="fa-solid fa-network-wired"></i> IP addresses</h2>
|
||||
<p>The client IP is taken from the first of <code>CF-Connecting-IP</code>, <code>True-Client-IP</code>, <code>X-Client-IP</code>, <code>X-Real-IP</code>, <code>X-Forwarded-For</code> (first entry) or the TCP peer address.</p>
|
||||
@if(pv.log_ips)
|
||||
<p>With IP logging on, the IP is recorded @if(pv.hash_ips)as an HMAC@else in plain text@endif in:</p>
|
||||
<ul class="pv-list">
|
||||
<li><code>user_sessions.ip</code>: updated on each request of a logged-in session</li>
|
||||
@@ -63,30 +71,33 @@
|
||||
<li><code>anon_identities.created_ip / last_ip</code> and <code>anon_activity_log.ip</code> for anonymous identities</li>
|
||||
<li><code>items.uploader_ip</code>, <code>comments.ip</code>, <code>reports.reporter_ip</code></li>
|
||||
</ul>
|
||||
@endif
|
||||
@if(pv.hash_ips)
|
||||
<p><strong>Hashing:</strong> <code>HMAC-SHA256</code> keyed with a server-side secret, stored as 64 hex characters. The same IP always yields the same hash, which is what makes bans and abuse correlation work; without the secret the hash can't be reversed or recomputed. This is pseudonymisation, not anonymisation: whoever holds the secret could test candidate IPs against it.</p>
|
||||
<p><strong>Hashing:</strong> <code>HMAC-SHA256</code> keyed with a server-side secret, stored as 64 hex characters. The same IP always produces the same hash, which is what lets bans and rate limits work. Without the secret a hash can't be reversed or even recomputed, so a leaked database reveals no IP addresses.@if(pv.ip_secret_env) The secret is supplied through the server environment and is not part of the configuration file or database backups.@endif</p>
|
||||
<p class="pv-small">Legally, hashed IPs are still <em>pseudonymised</em> personal data (GDPR Art. 4(5)), not anonymous: the operator, who holds the secret, can check whether a given IP matches a stored hash. That is how a ban recognises a returning address, and it is why hashed IPs are also subject to the retention period above.</p>
|
||||
@endif
|
||||
<p><strong>Always, regardless of the logging setting:</strong> login and registration attempts store an HMAC of the IP in <code>login_attempts</code> for brute-force rate limiting, and a moderator ban stores the banned IP's hash in <code>banned_ips</code>.</p>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if(pv.anon)
|
||||
{{-- ── Anonymous login ── --}}
|
||||
<section class="pv-sec">
|
||||
<h2><i class="fa-solid fa-user-secret"></i> Anonymous login (WebAuthn passkey)</h2>
|
||||
<p>No email, password or name is involved. <em>Login as Anonymous</em> creates a standard WebAuthn passkey in your authenticator (browser, OS, Bitwarden, iCloud Keychain, …).</p>
|
||||
<p class="pv-note"><i class="fa-solid fa-key"></i> <span><strong>Hardware security keys work too.</strong> A FIDO2 key such as a <strong>YubiKey</strong> (YubiKey 5 series, Security Key series) can hold the passkey: choose "security key" / "USB or NFC" when your browser asks where to save it, then plug in or tap the key. The private key is generated on the YubiKey and can never be exported from it, so the passkey is bound to that physical key and does not sync. The key may ask for its PIN and a touch. Since a lost key means a lost identity, register a second passkey (another YubiKey or a password manager) as a backup.</span></p>
|
||||
<h3>Registration</h3>
|
||||
<ol class="pv-steps">
|
||||
<li>The server sends creation options: relying party <code>{{ pv.domain }}</code>, a random single-use challenge, algorithm <strong>ES256</strong> (ECDSA P-256, COSE <code>-7</code>), <code>attestation: "none"</code>, and a user handle of 16 random bytes named <code>anon@{{ pv.domain }}</code> / "Anonymous". Nothing about you goes into it.</li>
|
||||
<li>Your authenticator generates a key pair. The <strong>private key never leaves the authenticator</strong>.</li>
|
||||
<li>The server verifies the response and stores: the <strong>credential ID</strong>, the <strong>public key</strong> (SPKI), the signature counter, and the authenticator's <strong>AAGUID</strong> (identifies the authenticator model, e.g. a password manager; with attestation "none" it is often all zeros).</li>
|
||||
<li>The server verifies the response and stores: the <strong>credential ID</strong>, the <strong>public key</strong> (SPKI), the signature counter, and the authenticator's <strong>AAGUID</strong> (identifies the authenticator model, e.g. a password manager; with attestation "none" it is often all zeros; for a YubiKey it can reveal the key model, e.g. "YubiKey 5 NFC", but never its serial number).</li>
|
||||
<li>Your identity is derived from the credential ID: <code>SHA256:base64(SHA-256(credential_id))</code>; the account name is <code>anon_</code> plus the first 8 hex characters of that hash (e.g. <code>anon_1ad1e20c</code>).</li>
|
||||
</ol>
|
||||
<h3>Login</h3>
|
||||
<p>The server issues a random single-use challenge; your authenticator signs it with the private key and the server verifies the signature with the stored public key. Up to 4 passkeys can be attached to one identity. If all of them are lost, the identity cannot be recovered: nothing else links it to you.</p>
|
||||
</section>
|
||||
|
||||
{{-- ── Device fingerprint ── --}}
|
||||
{{-- ── Device fingerprint (only when enabled) ── --}}
|
||||
@if(pv.hw)
|
||||
<section class="pv-sec">
|
||||
<h2><i class="fa-solid fa-microchip"></i> Device fingerprint</h2>
|
||||
<p>At anonymous login and when adding a passkey, your browser computes a device fingerprint used <strong>only for ban enforcement</strong> (so a banned user can't just create a new identity). It is not used for advertising or cross-site tracking.</p>
|
||||
@@ -101,11 +112,12 @@
|
||||
</ul>
|
||||
<p>The values are concatenated and hashed <strong>in the browser</strong> with SHA-256; only <code>HW:<64 hex></code> is sent. The raw values never reach the server. The hash is cached in <code>localStorage</code> (<code>f0ck_anon_hw_fp</code>) and stored server-side in <code>anon_identities.hw_fingerprint</code> and the activity log, and compared against banned device hashes.</p>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
{{-- ── Activity log ── --}}
|
||||
<section class="pv-sec">
|
||||
<h2><i class="fa-solid fa-list-check"></i> Anonymous activity log</h2>
|
||||
<p><code>anon_activity_log</code> records these actions of anonymous identities: login/session handshake, favourite, unfavourite, favourites import. Each row holds the action, target item, time, identity fingerprint, device fingerprint, and the IP @if(pv.ip_mode === 'hashed')(hashed)@endif @if(pv.ip_mode === 'off')(empty, as IP logging is off)@endif. It exists for moderation and ban cascades.</p>
|
||||
<p><code>anon_activity_log</code> records these actions of anonymous identities: login/session handshake, favourite, unfavourite, favourites import. Each row holds the action, target item, time, identity fingerprint@if(pv.hw), device fingerprint@endif@if(pv.log_ips), IP@if(pv.hash_ips) (hashed)@endif@endif. It exists for moderation and ban cascades.</p>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@@ -115,7 +127,7 @@
|
||||
<ul class="pv-list">
|
||||
<li><strong><code>session</code> cookie</strong>: 32 random bytes; the server stores only its SHA-256, so a database leak doesn't expose usable sessions. Flags: <code>HttpOnly</code>, <code>SameSite=Lax</code>@if(pv.https), <code>Secure</code>@endif.</li>
|
||||
<li>Per session the server keeps: user agent string, creation time, last-used time, the last path requested, a CSRF token@if(pv.log_ips), and the IP@endif.</li>
|
||||
<li><code>localStorage</code>: UI preferences, and for anonymous users the device fingerprint hash.</li>
|
||||
<li><code>localStorage</code>: UI preferences@if(pv.hw), and for anonymous users the device fingerprint hash@endif.</li>
|
||||
<li><code>f0ck_banned</code> cookie / <code>f0ck_anon_tombstone</code>: only set if you are banned, to show the ban notice.</li>
|
||||
</ul>
|
||||
<p>Registered accounts: passwords are hashed with <strong>scrypt</strong> (random 16-byte salt, 64-byte key). The plain password is never stored.</p>
|
||||
@@ -123,7 +135,7 @@
|
||||
|
||||
<section class="pv-sec">
|
||||
<h2><i class="fa-solid fa-ban"></i> Not collected</h2>
|
||||
<p>For anonymous identities: no email, real name, phone number or password. No third-party analytics, trackers or ad networks are involved in authentication.</p>
|
||||
<p>For anonymous identities: no email, real name, phone number or password@if(pv.anon)@if(!pv.hw), and no device fingerprint: device fingerprinting is disabled on this instance@endif@endif. No third-party analytics, trackers or ad networks are involved in authentication.</p>
|
||||
</section>
|
||||
|
||||
<p class="pv-foot">Questions? See <a href="/about">About</a>@if(mail) or write to <a href="mailto:{!! mail !!}">{!! mail !!}</a>@endif.</p>
|
||||
@@ -152,10 +164,12 @@
|
||||
.pv a { color: var(--pv-accent); }
|
||||
.pv code { padding: 1px 5px; font-size: 0.86em; background: var(--pv-surface); border: 1px solid var(--pv-border); word-break: break-word; }
|
||||
.pv-small { font-size: 0.88em; color: var(--pv-muted); }
|
||||
.pv-note { display: flex; gap: 10px; margin: 0 0 12px; padding: 10px 14px; font-size: 0.9em; background: var(--pv-surface); border: 1px solid var(--pv-border); border-left: 3px solid var(--pv-accent); }
|
||||
.pv-note > i { color: var(--pv-accent); margin-top: 4px; }
|
||||
.pv-steps, .pv-list { margin: 0 0 12px; padding-left: 22px; }
|
||||
.pv-steps li, .pv-list li { margin-bottom: 6px; }
|
||||
|
||||
.pv-status { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; margin-bottom: 12px; background: var(--pv-border); border: 1px solid var(--pv-border); }
|
||||
.pv-status { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px; margin-bottom: 12px; background: var(--pv-border); border: 1px solid var(--pv-border); }
|
||||
.pv-stat { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; background: var(--bg, #000); }
|
||||
.pv-stat-label { font-size: 0.72em; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: var(--pv-muted); }
|
||||
.pv-pill { align-self: flex-start; padding: 3px 9px; font-size: 0.78em; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; border: 1px solid currentColor; }
|
||||
|
||||
Reference in New Issue
Block a user