From b40e95ca17f8c9f17af5f475d001c8ec33728e6d Mon Sep 17 00:00:00 2001 From: Ken D'Ambrosio Date: Mon, 11 May 2026 18:50:51 +0000 Subject: v1.01: replace bcrypt with PBKDF2-SHA256; update README and DESIGN docs Co-Authored-By: Claude Sonnet 4.6 --- DESIGN.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 17 deletions(-) (limited to 'DESIGN.md') diff --git a/DESIGN.md b/DESIGN.md index a4d9108..2edcc1a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -74,7 +74,7 @@ is set to unlimited because large video files may be uploaded via rsync. img/audio.svg ← placeholder thumbnail for audio files scripts/ update.rb ← post-upload scan/enrich script - set_password.rb ← bcrypt password setter + set_password.rb ← PBKDF2-SHA256 password setter cache/thumbs/ ← generated thumbnail cache (mirrored path structure) tmp/ ← Puma pid / state files log/ ← Puma stdout / stderr logs @@ -103,9 +103,11 @@ is set to unlimited because large video files may be uploaded via rsync. | `puma ~> 6.4` | Multi-threaded Rack application server | | `mini_magick ~> 4.12` | ImageMagick wrapper — thumbnail generation, EXIF-aware auto-orient | | `mini_exiftool ~> 2.10` | Reads EXIF `DateTimeOriginal` / `CreateDate` for chronological sorting | -| `bcrypt ~> 3.1` | Password hashing for the single admin account | | `rack-session ~> 2.0` | Cookie-based session support (required by Sinatra 4 separately) | +Password hashing uses `OpenSSL::PKCS5.pbkdf2_hmac` from Ruby's standard library +(100,000 iterations, SHA-256, 32-byte output). No native gem extension is required. + ### System tools | Tool | Purpose | @@ -257,12 +259,12 @@ no server call is needed to open or navigate the lightbox. `.hidden` (i.e. `display:none !important`). 3. The appropriate element for the media type has `.hidden` removed and its `src` set. For video and audio, playback starts immediately. -4. The URL hash is updated to `#photo=filename` so the link is shareable. - On page load, if that hash is present, the matching photo is opened - automatically. +4. The URL is updated to `?photo=filename` so the link is shareable and + the server can read it. On page load, if that parameter is present, + the matching photo is opened automatically. 5. Navigation: ← → arrow keys, on-screen buttons, or touch swipe (>50 px). 6. Closing: `Escape`, the ✕ button, or clicking outside the media. - Video/audio is paused; the hash is stripped from the URL. + Video/audio is paused; the `?photo=` param is stripped from the URL. ### Slideshow (`GET /slideshow/SomeAlbum`) @@ -282,14 +284,41 @@ page. `slideshow.js`: ### Admin authentication -A single bcrypt-hashed password is stored in `config.yml` -(`admin_password_hash`). `POST /admin/login` compares the submitted -password against the hash using `BCrypt::Password#==`. On success, +A single PBKDF2-SHA256 password hash is stored in `config.yml` +(`admin_password_hash`) in the format `pbkdf2_sha256$$$`. +`POST /admin/login` re-derives the key from the submitted password and compares +using `OpenSSL.fixed_length_secure_compare` (constant-time). On success, `session[:admin] = true` is set in an encrypted cookie. All admin routes call `require_admin!` which halts with the login form on failure. -The `return_to` parameter on the login form lets the visitor be redirected -back to the page they were trying to reach. +The `return_to` parameter on the login form redirects the visitor back to +the page they came from — the "Admin" nav link passes the current album's +edit URL so logging in lands directly in the right edit view. + +### Open Graph meta tags + +Every album page includes `og:title`, `og:url`, `og:type`, and (when +available) `og:description` and `og:image`. The image is resolved +deterministically in `browse_album`: + +- If the request includes `?photo=filename`, `og:image` points to the + full-resolution `/media/` URL for that specific file — so sharing a + photo link previews that exact photo. +- Otherwise, the first image from `@entries` (chronological/filename order) + is used, falling back to `cover_candidates(dir).first` if the album has + no direct media files. + +`request.base_url` is used to build absolute URLs, so the tags are correct +regardless of hostname or scheme. + +### Filtered slideshow + +When the album search filter is active on the root page, the Slideshow +button href is dynamically updated (via `album.js`) to include +`?dirs=name1,name2,...` — the `data-rel` values of the currently visible +album cards. `slideshow_view` in `app.rb` passes these to +`all_media_entries(top_dirs:)`, which walks only those specific +subdirectories instead of the full media tree. ### Saving album edits (`POST /admin/edit/SomeAlbum`) @@ -297,9 +326,9 @@ back to the page they were trying to reach. 2. Top-level fields (title, description, cover, sort_reverse, visible) are updated from form params. `blank_to_nil` converts empty strings to `nil` so omitted optional fields don't get stored as `""`. -3. Per-file fields (title, caption, visible) are updated by iterating the - `file_title[name]` / `file_caption[name]` / `file_visible[name]` param - hashes. +3. Per-file fields (caption, visible) are updated by iterating the + `file_visible[name]` param hash (present for every file via a hidden + input), merging `file_caption[name]` for each. 4. `atomic_write` writes the updated JSON to a `.tmp` file and renames it into place. 5. Redirect back to the same edit page (PRG pattern — prevents double-submit @@ -351,7 +380,7 @@ escape the media root gets a `404`. returns `403` unless the session has admin privileges. The browse routes filter hidden albums and files from the data sent to the browser. -**Admin password:** Stored as a bcrypt hash in `config.yml` (not in git). +**Admin password:** Stored as a PBKDF2-SHA256 hash in `config.yml` (not in git). The plaintext password is never persisted. Sessions use an encrypted cookie with a random secret also stored in `config.yml`. @@ -389,8 +418,8 @@ upload, run `update.rb` (it generates thumbnails as part of its scan). ### Configuration (`config.yml`) ```yaml -admin_password_hash: "$2a$12$..." # set with scripts/set_password.rb -session_secret: "random-hex-string" # set once at install time +admin_password_hash: "pbkdf2_sha256$100000$$" # set with scripts/set_password.rb +session_secret: "random-hex-string" # set once at install time ``` This file is not tracked in git. All three paths (`MEDIA_ROOT`, -- cgit v1.2.3