AuditPic is a photo authenticity verification system. Auditors use the Flutter app to capture photos that are cryptographically signed at capture time. Anyone can later verify that a photo has not been altered, either by scanning a QR code or visiting the public viewer.
┌──────────────────────────────────────────────────────────────────────┐
│ Mobile App (Flutter) │
│ │
│ CaptureScreen ──► ImageHashUtil (SHA-256) ──► PhotoApi (Dio) │
│ GalleryScreen ──► PhotoApi (QR code per photo, share/lock buttons) │
│ VerifyScreen ──► PhotoApi + ImageHashUtil │
└────────────────────────────┬─────────────────────────────────────────┘
│ HTTPS REST + JWT
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Backend (Spring Boot 3) │
│ │
│ POST /api/v1/photos │
│ ├── FieldEncryptionService (AES-256-GCM — verify client SHA-256) │
│ ├── SigningService (HMAC-SHA256 versioned signing) │
│ ├── MinioStorageService (upload original bytes) │
│ └── AiDetectionService (async AI manipulation check) │
│ │
│ GET /api/v1/photos/{id} (metadata; 404 for private) │
│ GET /api/v1/photos/{id}/verify-signature │
│ GET /api/v1/photos/{id}/download (presigned MinIO URL, owner) │
│ GET /api/v1/photos (authenticated list) │
│ PATCH /api/v1/photos/{id}/visibility (premium-only for private) │
│ DELETE /api/v1/photos/{id} (owner only) │
│ │
│ GET /public/photos/{id} (HTML viewer, rate-limited) │
│ GET /public/photos/{id}/image (watermarked PNG, rate-limited) │
│ └── WatermarkService (LSB steganography, embeds verificationId) │
│ │
│ POST /api/v1/auth/register|login|refresh │
└────────────┬──────────────────────────┬─────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ PostgreSQL 15 │ │ MinIO │
│ (metadata, │ │ (photo bytes, │
│ email encrypted)│ │ original only) │
└─────────────────┘ └──────────────────┘
ImageHashUtil.sha256Hex)multipart/form-data to POST /api/v1/photos:file — raw image bytessha256 — hex hash computed on devicecapturedAt — device timestampphotos/{userId}/{uuid}.jpg{sha256}:{signedAt.epochMilli} with HMAC-SHA256 using the current versioned keyphotos row to PostgreSQL (email encrypted at rest via AES-256-GCM){ verificationId, capturedAt, signedAt, sha256 } to Flutterhttps://api.auditpic.com/public/photos/{verificationId}GET /public/photos/{verificationId}WatermarkService (LSB watermark embedded)verificationId (or scans QR) in the Verify screenGET /api/v1/photos/{verificationId}/verify-signature → { valid: true/false }metadata.sha256| Concern | Choice | Reason | |---|---|---| | Mobile | Flutter 3.8+ | Cross-platform (iOS + Android) | | Backend | Spring Boot 3.3.5 / Java 17 | Standard workspace stack | | Database | PostgreSQL 15 | Relational, Flyway migrations | | Object storage | MinIO (local) / Firebase Storage (production) | S3-compatible, self-hostable | | Auth | JWT (JJWT 0.12.6), self-issued | Stateless, no Firebase dependency | | Signing | HMAC-SHA256 (versioned keys) | Fast, server-side, rotation-safe | | AI detection | Hive Moderation | Deepfake/AI-generated image detection | | PII encryption | AES-256-GCM | Email encrypted at rest; emailHash for lookup | | Watermarking | LSB steganography | Invisible, lossless for PNG |
User.email is stored encrypted in the database. User.emailHash (HMAC-SHA256 of the plaintext email) is stored separately for indexed lookup. Key: FIELD_ENCRYPTION_KEY (32-byte base64), required in all non-local environments.
Photo signatures use versioned HMAC-SHA256 keys. Each photos row stores hmac_key_version so old signatures remain verifiable during rotation. Automated quarterly rotation via .github/workflows/rotate-hmac.yml.
See CLAUDE.md for full rotation procedure.
RateLimitInterceptor implements a sliding-window rate limiter per IP:
X-Forwarded-For is only trusted when the direct connection comes from an RFC-1918 address (k8s ingress proxy). Public IPs cannot spoof the client IP.
auditpic): DDL privileges (CREATE TABLE, ALTER, etc.) — migration onlyauditpic_app): DML only (SELECT/INSERT/UPDATE/DELETE) — runtimeAll responses from the backend include:
Strict-Transport-Security: max-age=31536000; includeSubDomainsContent-Security-Policy, X-Frame-Options: DENYfeature/* ──PR──► develop ──CI green──► CD: staging (namespace: audit-pic-staging)
develop ──PR──► main ──CI green──► CD: production (namespace: audit-pic)
CI checks (all required): Backend unit tests (118), Flutter analyze+test, PR title (Conventional Commits), branch targeting (feature/* → develop), PR description.
Maestro Android UI tests run as non-blocking (emulator flakiness on hosted runners).
Secrets flow: 1Password → load-secrets-action → kubeseal (Bitnami Sealed Secrets) → k3s
| Version | Description | |---------|-------------| | V1 | Initial schema (photos, users) | | V2 | AI detection status enum | | V3 | Refresh tokens | | V4 | Photo retention days | | V5 | HMAC key version column on photos | | V6 | Photo visibility (is_public, is_premium) | | V7 | App role — auditpic_app DML-only | | V8 | Email AES-256-GCM encryption (email_hash lookup) |
Reacties