Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1acb5a3dcc | ||
|
|
14a73c63ac | ||
|
|
3d53282738 | ||
|
|
e831ae4884 | ||
|
|
4751d05e9f | ||
|
|
d15bcb8df4 | ||
|
|
6ec7de5604 | ||
|
|
1cdb2aca91 | ||
|
|
46de371c42 | ||
|
|
11c0d45548 | ||
|
|
7bb1029c0f | ||
|
|
e3f7f36e5e | ||
|
|
f200737088 | ||
|
|
6def318ba7 | ||
|
|
e203af6a73 | ||
|
|
6ba135098b | ||
|
|
903739c055 | ||
|
|
30fbb5016e | ||
|
|
041148e8fe | ||
|
|
90bedce379 | ||
|
|
021265f3cf | ||
|
|
ff42398509 | ||
|
|
a30ec33b98 | ||
|
|
252efbec7e | ||
|
|
6e906d5981 | ||
|
|
df6125d098 | ||
|
|
3d4a340305 | ||
|
|
0decb39b17 | ||
|
|
4291dfad38 | ||
|
|
ddee3583e8 | ||
|
|
3e2307cbcf | ||
|
|
cc745fbdfa | ||
|
|
3027706d49 | ||
|
|
39fbd617e6 | ||
|
|
de4cb0b3be | ||
|
|
add3951003 | ||
|
|
3858e234da | ||
|
|
03e8e3a840 | ||
|
|
55e78d0503 | ||
|
|
b13a9fcd3f | ||
|
|
96b49c68ec | ||
|
|
be8744179d | ||
|
|
f971b75d7e | ||
|
|
455c6dfd01 | ||
|
|
a00a154a1a | ||
|
|
8b3b331843 | ||
|
|
10c874374f | ||
|
|
0c1e87c7c0 | ||
|
|
d517a4dc8b | ||
|
|
6d59f3edfc | ||
|
|
17d0406be2 | ||
|
|
ef73280015 | ||
|
|
6338d6aab4 | ||
|
|
b9d0fac535 | ||
|
|
5c0a5bbba7 | ||
|
|
ba1a77f00b | ||
|
|
5e587df545 | ||
|
|
23456ac1e4 | ||
|
|
8be512ad7b | ||
|
|
f129500202 | ||
|
|
c37d743b3e | ||
|
|
5bdb625059 | ||
|
|
231ba97fde | ||
|
|
a70e88625f | ||
|
|
b6770c46e5 | ||
|
|
9f4318cc0f | ||
|
|
91dc665a77 | ||
|
|
6066df391b | ||
|
|
be5c95b59d | ||
|
|
09b1abddc7 | ||
|
|
0c9ea0e3f2 | ||
|
|
aebfb20dfc | ||
|
|
b935c474af | ||
|
|
73b34ba8b5 | ||
|
|
89d8fee5da | ||
|
|
0e270dadb3 | ||
|
|
e2002b6026 | ||
|
|
66ed11fb97 | ||
|
|
9cbb4600f8 | ||
|
|
c1c850c593 | ||
|
|
e029f00d66 | ||
|
|
34e417fb55 | ||
|
|
e7954c63e4 | ||
|
|
446789a16f | ||
|
|
2538126573 | ||
|
|
a91d127ed7 | ||
|
|
a0781b1cf7 | ||
|
|
5e32ecb35a | ||
|
|
3e5de98f60 | ||
|
|
c8956b9e43 | ||
|
|
a8f15f87c6 | ||
|
|
8a64db9fcc | ||
|
|
ab450955fe | ||
|
|
afd502dbf3 | ||
|
|
3f02e55ffd | ||
|
|
2ee824b02b | ||
|
|
189620e4fb | ||
|
|
ecad88e859 | ||
|
|
62bd31d0aa | ||
|
|
241cdadd25 | ||
|
|
85309a2044 | ||
|
|
a81a20f8ee |
@@ -25,7 +25,6 @@ rpi/
|
||||
*.img.xz
|
||||
*.img.zst
|
||||
*.img.zst.zip
|
||||
pishrink.sh
|
||||
|
||||
# Docs
|
||||
*.md
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -64,9 +64,13 @@ htmlcov/
|
||||
# Output test files.
|
||||
test_data/*.png
|
||||
|
||||
# Dev scripts (local convenience scripts - except validate-release.sh)
|
||||
# Dev scripts (local convenience scripts - except these)
|
||||
scripts/*
|
||||
!scripts/validate-release.sh
|
||||
!scripts/smoke-test.sh
|
||||
!scripts/setup-trusted-certs.sh
|
||||
!scripts/screenshots.sh
|
||||
!scripts/build.sh
|
||||
|
||||
# Web UI auth database and SSL certs
|
||||
instance/
|
||||
@@ -80,8 +84,8 @@ tests/
|
||||
*.img
|
||||
*.img.xz
|
||||
*.img.zst
|
||||
pishrink.sh
|
||||
*.img.zst.zip
|
||||
rpi/tools/pishrink.sh
|
||||
|
||||
# Temp file storage
|
||||
frontends/web/temp_files/
|
||||
|
||||
4
API.md
4
API.md
@@ -88,7 +88,7 @@ uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
|
||||
**Docker with channel key:**
|
||||
```bash
|
||||
STEGASOO_CHANNEL_KEY=XXXX-XXXX-... docker-compose up api
|
||||
STEGASOO_CHANNEL_KEY=XXXX-XXXX-... docker-compose -f docker/docker-compose.yml up api
|
||||
```
|
||||
|
||||
---
|
||||
@@ -843,7 +843,7 @@ curl -s -X POST "$BASE_URL/decode/multipart" \
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
### docker-compose.yml
|
||||
### docker/docker-compose.yml
|
||||
|
||||
```yaml
|
||||
x-common-env: &common-env
|
||||
|
||||
14
CLI.md
14
CLI.md
@@ -64,6 +64,18 @@ python -c "from stegasoo import has_dct_support; print('DCT:', 'available' if ha
|
||||
stegasoo channel show
|
||||
```
|
||||
|
||||
### Man Page
|
||||
|
||||
```bash
|
||||
# Install man page
|
||||
sudo mkdir -p /usr/local/share/man/man1
|
||||
sudo cp docs/stegasoo.1 /usr/local/share/man/man1/
|
||||
sudo mandb
|
||||
|
||||
# View
|
||||
man stegasoo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's New in v4.1.0
|
||||
@@ -798,7 +810,7 @@ stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
**docker-compose.yml:**
|
||||
**docker/docker-compose.yml:**
|
||||
```yaml
|
||||
x-common-env: &common-env
|
||||
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
|
||||
|
||||
39
DOCKER.md
39
DOCKER.md
@@ -6,14 +6,14 @@ Stegasoo provides Docker images for both the Web UI and REST API.
|
||||
|
||||
```bash
|
||||
# Build and start all services
|
||||
docker-compose up -d
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
|
||||
# Check status
|
||||
docker-compose ps
|
||||
docker-compose -f docker/docker-compose.yml ps
|
||||
```
|
||||
|
||||
Access:
|
||||
- **Web UI**: http://localhost:5000
|
||||
- **Web UI**: https://localhost:5000 (HTTPS with self-signed cert)
|
||||
- **REST API**: http://localhost:8000
|
||||
|
||||
## Services
|
||||
@@ -36,9 +36,12 @@ STEGASOO_CHANNEL_KEY=XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
|
||||
# Web UI authentication (default: enabled)
|
||||
STEGASOO_AUTH_ENABLED=true
|
||||
|
||||
# HTTPS support (default: disabled)
|
||||
STEGASOO_HTTPS_ENABLED=false
|
||||
# HTTPS support (default: enabled, generates self-signed cert)
|
||||
STEGASOO_HTTPS_ENABLED=true
|
||||
STEGASOO_HOSTNAME=localhost
|
||||
|
||||
# To disable HTTPS:
|
||||
# STEGASOO_HTTPS_ENABLED=false
|
||||
```
|
||||
|
||||
### Volume Mounts
|
||||
@@ -58,10 +61,10 @@ Uses a pre-built base image with all dependencies:
|
||||
|
||||
```bash
|
||||
# First time only: build the base image
|
||||
docker build -f Dockerfile.base -t stegasoo-base:latest .
|
||||
docker build -f docker/Dockerfile.base -t stegasoo-base:latest .
|
||||
|
||||
# Build services (fast - only copies app code)
|
||||
docker-compose build
|
||||
docker-compose -f docker/docker-compose.yml build
|
||||
```
|
||||
|
||||
### Full Build (No Base Image)
|
||||
@@ -69,26 +72,26 @@ docker-compose build
|
||||
If you don't have the base image, the Dockerfile will build all dependencies (slower):
|
||||
|
||||
```bash
|
||||
docker-compose build
|
||||
docker-compose -f docker/docker-compose.yml build
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
docker-compose -f docker/docker-compose.yml logs -f
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
docker-compose -f docker/docker-compose.yml down
|
||||
|
||||
# Rebuild after code changes
|
||||
docker-compose build && docker-compose up -d
|
||||
docker-compose -f docker/docker-compose.yml build && docker-compose -f docker/docker-compose.yml up -d
|
||||
|
||||
# Full rebuild (no cache)
|
||||
docker-compose build --no-cache
|
||||
docker-compose -f docker/docker-compose.yml build --no-cache
|
||||
```
|
||||
|
||||
## Resource Limits
|
||||
@@ -109,7 +112,7 @@ Both services include health checks:
|
||||
|
||||
Check health status:
|
||||
```bash
|
||||
docker-compose ps
|
||||
docker-compose -f docker/docker-compose.yml ps
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
@@ -126,7 +129,7 @@ For production, consider:
|
||||
```bash
|
||||
# Don't commit .env files with secrets
|
||||
export STEGASOO_CHANNEL_KEY=your-key
|
||||
docker-compose up -d
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
3. **Reverse proxy**: Put behind nginx/traefik for TLS termination
|
||||
@@ -142,12 +145,12 @@ For production, consider:
|
||||
### Container won't start
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose logs web
|
||||
docker-compose logs api
|
||||
docker-compose -f docker/docker-compose.yml logs web
|
||||
docker-compose -f docker/docker-compose.yml logs api
|
||||
```
|
||||
|
||||
### Out of memory
|
||||
Increase Docker's memory allocation or reduce worker count in Dockerfile.
|
||||
Increase Docker's memory allocation or reduce worker count in `docker/Dockerfile`.
|
||||
|
||||
### Permission errors
|
||||
The containers run as non-root user `stego` (UID 1000). Ensure volume permissions match.
|
||||
|
||||
30
INSTALL.md
30
INSTALL.md
@@ -154,10 +154,10 @@ Build and run individual containers.
|
||||
#### Build Images
|
||||
|
||||
```bash
|
||||
# Build all targets
|
||||
docker build -t stegasoo-web --target web .
|
||||
docker build -t stegasoo-api --target api .
|
||||
docker build -t stegasoo-cli --target cli .
|
||||
# From project root - build all targets
|
||||
docker build -t stegasoo-web --target web -f docker/Dockerfile .
|
||||
docker build -t stegasoo-api --target api -f docker/Dockerfile .
|
||||
docker build -t stegasoo-cli --target cli -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
#### Run Web UI
|
||||
@@ -214,17 +214,17 @@ The easiest way to run all services.
|
||||
|
||||
```bash
|
||||
# Start in background
|
||||
docker-compose up -d
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
|
||||
# Start specific service
|
||||
docker-compose up -d web
|
||||
docker-compose up -d api
|
||||
docker-compose -f docker/docker-compose.yml up -d web
|
||||
docker-compose -f docker/docker-compose.yml up -d api
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
docker-compose -f docker/docker-compose.yml logs -f
|
||||
|
||||
# Stop all
|
||||
docker-compose down
|
||||
docker-compose -f docker/docker-compose.yml down
|
||||
```
|
||||
|
||||
#### Authentication Configuration (v4.0.2)
|
||||
@@ -239,7 +239,7 @@ STEGASOO_HOSTNAME=localhost # Hostname for SSL cert
|
||||
STEGASOO_CHANNEL_KEY= # Optional channel key
|
||||
|
||||
# Then run
|
||||
docker-compose up -d web
|
||||
docker-compose -f docker/docker-compose.yml up -d web
|
||||
```
|
||||
|
||||
On first access, you'll be prompted to create an admin account. The database and SSL certs are persisted in Docker volumes.
|
||||
@@ -255,16 +255,16 @@ On first access, you'll be prompted to create an admin account. The database and
|
||||
|
||||
```bash
|
||||
# Build images and start
|
||||
docker-compose up -d --build
|
||||
docker-compose -f docker/docker-compose.yml up -d --build
|
||||
|
||||
# Force rebuild (no cache)
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
docker-compose -f docker/docker-compose.yml build --no-cache
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
#### Resource Configuration
|
||||
|
||||
The `docker-compose.yml` includes resource limits:
|
||||
The `docker/docker-compose.yml` includes resource limits:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -852,7 +852,7 @@ Argon2 needs 256MB per operation. Increase container memory:
|
||||
# Docker run
|
||||
docker run --memory=768m ...
|
||||
|
||||
# Docker Compose - edit docker-compose.yml
|
||||
# Docker Compose - edit docker/docker-compose.yml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
12
README.md
12
README.md
@@ -105,15 +105,18 @@ ruff check src/ tests/ frontends/
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
# Quick start
|
||||
docker-compose up -d
|
||||
# Quick start (HTTPS enabled by default)
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
|
||||
# Access
|
||||
# Web UI: http://localhost:5000
|
||||
# Web UI: https://localhost:5000 (self-signed cert)
|
||||
# REST API: http://localhost:8000
|
||||
|
||||
# Disable HTTPS if needed:
|
||||
STEGASOO_HTTPS_ENABLED=false docker-compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
See [DOCKER.md](DOCKER.md) for full documentation.
|
||||
See [DOCKER.md](DOCKER.md) and [docs/DOCKER_QUICKSTART.md](docs/DOCKER_QUICKSTART.md) for full documentation.
|
||||
|
||||
## Raspberry Pi
|
||||
|
||||
@@ -143,6 +146,7 @@ See [rpi/README.md](rpi/README.md) for manual installation.
|
||||
- [UNDER_THE_HOOD.md](UNDER_THE_HOOD.md) - Technical deep-dive
|
||||
- [CHANGELOG.md](CHANGELOG.md) - Version history
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contributor guide
|
||||
- `man stegasoo` - Man page (install: `sudo cp docs/stegasoo.1 /usr/local/share/man/man1/ && sudo mandb`)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ Pre-release validation checklist. Complete all items before tagging a release.
|
||||
|
||||
## Docker Validation
|
||||
|
||||
- [ ] Base image builds: `docker build -f Dockerfile.base -t stegasoo-base:latest .`
|
||||
- [ ] Web image builds: `docker-compose build web`
|
||||
- [ ] Container starts: `docker-compose up -d web`
|
||||
- [ ] Base image builds: `docker build -f docker/Dockerfile.base -t stegasoo-base:latest .`
|
||||
- [ ] Web image builds: `docker-compose -f docker/docker-compose.yml build web`
|
||||
- [ ] Container starts: `docker-compose -f docker/docker-compose.yml up -d web`
|
||||
- [ ] Web UI accessible at http://localhost:5000
|
||||
- [ ] Encode/decode works in container
|
||||
- [ ] Container stops cleanly: `docker-compose down`
|
||||
- [ ] Container stops cleanly: `docker-compose -f docker/docker-compose.yml down`
|
||||
|
||||
## Release Process
|
||||
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
## Stegasoo v4.1.5
|
||||
## Stegasoo v4.1.7
|
||||
|
||||
### Developer Experience
|
||||
- **Educational Code Comments**: Core modules now include detailed explanations
|
||||
- DCT: zig-zag coefficient diagrams, QIM embedding math, Reed-Solomon "Voyager" reference
|
||||
- LSB: visual bit manipulation examples, ChaCha20 pixel selection
|
||||
- Crypto: multi-factor KDF flow diagrams, Argon2id memory-hardness reasoning
|
||||
- CLI/Web: architectural patterns for future contributors
|
||||
### Mobile UI Polish
|
||||
- **PIN Entry**: Shrunk digit boxes for 9-digit PIN support on mobile
|
||||
- **Mode Selectors**: DCT/LSB buttons now use consistent button-group styling with icons
|
||||
- **Navbar**: Left-aligned collapsed menu, shortened channel fingerprint display (`ABCD-••••-3456`)
|
||||
- **Text Wrapping**: Fixed button text wrapping issues on narrow screens
|
||||
|
||||
### Raspberry Pi Improvements
|
||||
- **Streamlined Image Creation**: `pull-image.sh` now handles everything
|
||||
- Auto-resizes rootfs to exactly 16GB (consistent images from any SD card)
|
||||
- Disables Pi OS auto-expand
|
||||
- Compresses with zstd
|
||||
- Optional .zst.zip wrapper for GitHub releases
|
||||
- **16GB Minimum**: Pre-built images are now 16GB (was variable)
|
||||
- **Host Requirements**: `rpi/host-requirements.txt` documents all dependencies
|
||||
- **Test Automation**: `kickoff-pi-test.sh` for one-command flash+test cycles
|
||||
### Docker Improvements
|
||||
- **Reorganized**: Docker files moved to `docker/` directory
|
||||
- `docker/Dockerfile`
|
||||
- `docker/Dockerfile.base`
|
||||
- `docker/docker-compose.yml`
|
||||
- **DCT Fix**: Added Reed-Solomon (`reedsolo`) to Docker images - fixes DCT decode failures
|
||||
- **Quick Start**: New `docs/DOCKER_QUICKSTART.md` guide
|
||||
|
||||
### MOTD Polish
|
||||
- Dynamic temperature emoji (ice/cool/fire based on CPU temp)
|
||||
- Rocket emoji for service status
|
||||
- Cleaner formatting
|
||||
```bash
|
||||
# Build and run
|
||||
docker build -f docker/Dockerfile.base -t stegasoo-base:latest .
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
### Raspberry Pi
|
||||
- **First-Boot Wizard**: Can now load existing channel key (for joining team deployments)
|
||||
- **Project Cleanup**: Moved `pishrink.sh` to `rpi/tools/`
|
||||
|
||||
### UI Copy
|
||||
- Changed "Undetectable" to "Covertly Embedded" on encode page (more accurate)
|
||||
|
||||
### Raspberry Pi Image
|
||||
Download `stegasoo-rpi-4.1.5.img.zst.zip` from Releases.
|
||||
Download `stegasoo-rpi-4.1.7.img.zst.zip` from Releases.
|
||||
|
||||
```bash
|
||||
# Flash (auto-detects SD card)
|
||||
sudo ./rpi/flash-image.sh stegasoo-rpi-4.1.5.img.zst.zip
|
||||
sudo ./rpi/flash-image.sh stegasoo-rpi-4.1.7.img.zst.zip
|
||||
|
||||
# Or manual
|
||||
zstdcat stegasoo-rpi-4.1.5.img.zst | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
unzip -p stegasoo-rpi-4.1.7.img.zst.zip | zstdcat | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
```
|
||||
|
||||
Default login: `admin` / `stegasoo`
|
||||
@@ -39,8 +44,8 @@ First boot runs the setup wizard for WiFi, HTTPS, and channel key configuration.
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
docker-compose up -d web # Web UI on :5000
|
||||
docker-compose up -d api # REST API on :8000
|
||||
docker-compose -f docker/docker-compose.yml up -d web # Web UI on :5000
|
||||
docker-compose -f docker/docker-compose.yml up -d api # REST API on :8000
|
||||
```
|
||||
|
||||
### Full Changelog
|
||||
|
||||
@@ -177,7 +177,7 @@ python app.py
|
||||
### Docker Configuration
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
# docker/docker-compose.yml
|
||||
services:
|
||||
web:
|
||||
environment:
|
||||
@@ -360,7 +360,7 @@ gunicorn --bind 0.0.0.0:5000 --workers 2 --threads 4 --timeout 60 app:app
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
docker-compose up web
|
||||
docker-compose -f docker/docker-compose.yml up web
|
||||
```
|
||||
|
||||
### First-Time Setup
|
||||
@@ -1245,7 +1245,7 @@ volumes:
|
||||
```bash
|
||||
pip install scipy
|
||||
# Or rebuild Docker image
|
||||
docker-compose build --no-cache
|
||||
docker-compose -f docker/docker-compose.yml build --no-cache
|
||||
```
|
||||
|
||||
### Browser Compatibility
|
||||
|
||||
@@ -35,12 +35,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libzbar0 \
|
||||
libjpeg-dev \
|
||||
zlib1g-dev \
|
||||
curl \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install ALL dependencies (slow path)
|
||||
RUN pip install --no-cache-dir \
|
||||
cython numpy scipy>=1.10.0 jpegio>=0.2.0 \
|
||||
argon2-cffi>=23.0.0 pillow>=10.0.0 cryptography>=41.0.0 \
|
||||
reedsolo>=1.7.0 \
|
||||
flask>=3.0.0 gunicorn>=21.0.0 \
|
||||
fastapi>=0.100.0 "uvicorn[standard]>=0.20.0" python-multipart>=0.0.6 \
|
||||
qrcode>=7.3.0 pyzbar>=0.1.9 click>=8.0.0 lz4>=4.0.0
|
||||
@@ -57,6 +60,12 @@ FROM base AS web
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies (curl for healthcheck, openssl for cert generation)
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy application files (this is all that rebuilds normally!)
|
||||
COPY src/ src/
|
||||
COPY data/ data/
|
||||
@@ -66,6 +75,10 @@ COPY frontends/web/ frontends/web/
|
||||
# temp_files is for multi-worker temp file sharing
|
||||
RUN mkdir -p /tmp/stego_uploads /app/frontends/web/instance /app/frontends/web/certs /app/frontends/web/temp_files
|
||||
|
||||
# Copy and set up entrypoint (before switching to non-root user)
|
||||
COPY frontends/web/docker-entrypoint.sh /app/frontends/web/
|
||||
RUN chmod +x /app/frontends/web/docker-entrypoint.sh
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 stego && chown -R stego:stego /app /tmp/stego_uploads
|
||||
USER stego
|
||||
@@ -77,12 +90,12 @@ ENV PYTHONPATH=/app/src
|
||||
EXPOSE 5000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/')" || exit 1
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD curl -fsk https://localhost:5000/ || curl -fs http://localhost:5000/ || exit 1
|
||||
|
||||
# Run with gunicorn
|
||||
# Run with entrypoint (handles HTTPS/HTTP mode)
|
||||
WORKDIR /app/frontends/web
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--threads", "4", "--timeout", "120", "app:app"]
|
||||
ENTRYPOINT ["/app/frontends/web/docker-entrypoint.sh"]
|
||||
|
||||
# ============================================================================
|
||||
# API stage - REST API
|
||||
@@ -32,7 +32,8 @@ RUN pip install --no-cache-dir \
|
||||
jpegio>=0.2.0 \
|
||||
argon2-cffi>=23.0.0 \
|
||||
pillow>=10.0.0 \
|
||||
cryptography>=41.0.0
|
||||
cryptography>=41.0.0 \
|
||||
reedsolo>=1.7.0
|
||||
|
||||
# Install web/api framework packages (also stable)
|
||||
RUN pip install --no-cache-dir \
|
||||
@@ -8,7 +8,8 @@ services:
|
||||
# ============================================================================
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
target: web
|
||||
container_name: stegasoo-web
|
||||
ports:
|
||||
@@ -18,7 +19,9 @@ services:
|
||||
FLASK_ENV: production
|
||||
# Authentication (v4.0.2)
|
||||
STEGASOO_AUTH_ENABLED: ${STEGASOO_AUTH_ENABLED:-true}
|
||||
STEGASOO_HTTPS_ENABLED: ${STEGASOO_HTTPS_ENABLED:-false}
|
||||
# HTTPS enabled by default - generates self-signed cert if none provided
|
||||
# To disable: STEGASOO_HTTPS_ENABLED=false docker-compose up
|
||||
STEGASOO_HTTPS_ENABLED: ${STEGASOO_HTTPS_ENABLED:-true}
|
||||
STEGASOO_HOSTNAME: ${STEGASOO_HOSTNAME:-localhost}
|
||||
volumes:
|
||||
# Persist auth database and SSL certs (v4.0.2)
|
||||
@@ -37,7 +40,8 @@ services:
|
||||
# ============================================================================
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
target: api
|
||||
container_name: stegasoo-api
|
||||
ports:
|
||||
162
docs/DOCKER_QUICKSTART.md
Normal file
162
docs/DOCKER_QUICKSTART.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Docker Quickstart
|
||||
|
||||
Get Stegasoo running in Docker in under 5 minutes.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# From project root:
|
||||
|
||||
# Build web UI image
|
||||
sudo docker build -t stegasoo-web --target web -f docker/Dockerfile .
|
||||
|
||||
# Or build all targets
|
||||
sudo docker build -t stegasoo-api --target api -f docker/Dockerfile .
|
||||
sudo docker build -t stegasoo-cli --target cli -f docker/Dockerfile .
|
||||
|
||||
# Or use docker-compose
|
||||
sudo docker-compose -f docker/docker-compose.yml build
|
||||
```
|
||||
|
||||
## Run (Basic)
|
||||
|
||||
```bash
|
||||
# HTTP only, no auth
|
||||
sudo docker run -d \
|
||||
-p 5000:5000 \
|
||||
-e STEGASOO_AUTH_ENABLED=false \
|
||||
--name stegasoo \
|
||||
stegasoo-web
|
||||
```
|
||||
|
||||
Visit http://localhost:5000
|
||||
|
||||
## Run (Production)
|
||||
|
||||
```bash
|
||||
# HTTPS + Auth + Channel Key
|
||||
sudo docker run -d \
|
||||
-p 5000:5000 \
|
||||
-e STEGASOO_AUTH_ENABLED=true \
|
||||
-e STEGASOO_HTTPS_ENABLED=true \
|
||||
-e STEGASOO_HOSTNAME=stegasoo.local \
|
||||
-e STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 \
|
||||
-v stegasoo-data:/opt/stegasoo/frontends/web/instance \
|
||||
-v stegasoo-certs:/opt/stegasoo/frontends/web/certs \
|
||||
--name stegasoo \
|
||||
stegasoo-web
|
||||
```
|
||||
|
||||
Visit https://localhost:5000 (accept self-signed cert warning)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `STEGASOO_AUTH_ENABLED` | `true` | Require login |
|
||||
| `STEGASOO_HTTPS_ENABLED` | `false` | Enable HTTPS |
|
||||
| `STEGASOO_HOSTNAME` | `localhost` | Hostname for SSL cert |
|
||||
| `STEGASOO_CHANNEL_KEY` | *(none)* | Shared channel key (32 alphanumeric chars with dashes) |
|
||||
|
||||
## Docker Compose
|
||||
|
||||
Create `.env` file in project root:
|
||||
```bash
|
||||
STEGASOO_AUTH_ENABLED=true
|
||||
STEGASOO_HTTPS_ENABLED=true
|
||||
STEGASOO_HOSTNAME=stegasoo.local
|
||||
STEGASOO_CHANNEL_KEY=
|
||||
```
|
||||
|
||||
Run:
|
||||
```bash
|
||||
sudo docker-compose -f docker/docker-compose.yml up -d web
|
||||
```
|
||||
|
||||
## Custom SSL Certificates
|
||||
|
||||
### Use Your Own Certs
|
||||
|
||||
```bash
|
||||
# Stop container
|
||||
sudo docker stop stegasoo
|
||||
|
||||
# Copy certs to volume
|
||||
sudo docker run --rm -v stegasoo-certs:/certs -v $(pwd):/src alpine \
|
||||
sh -c "cp /src/your-cert.crt /certs/server.crt && cp /src/your-key.key /certs/server.key && chmod 600 /certs/server.key"
|
||||
|
||||
# Start container
|
||||
sudo docker start stegasoo
|
||||
```
|
||||
|
||||
### Use mkcert (Local Development)
|
||||
|
||||
```bash
|
||||
# Install mkcert
|
||||
brew install mkcert # macOS
|
||||
# or: sudo apt install mkcert # Debian/Ubuntu
|
||||
|
||||
# Create local CA and certs
|
||||
mkcert -install
|
||||
mkcert -cert-file server.crt -key-file server.key localhost 127.0.0.1 stegasoo.local
|
||||
|
||||
# Copy to Docker volume (see above)
|
||||
```
|
||||
|
||||
### Use Let's Encrypt (Public Server)
|
||||
|
||||
```bash
|
||||
# Get cert
|
||||
sudo certbot certonly --standalone -d yourdomain.com
|
||||
|
||||
# Copy to Docker volume
|
||||
sudo docker run --rm -v stegasoo-certs:/certs alpine \
|
||||
sh -c "cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /certs/server.crt && \
|
||||
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /certs/server.key && \
|
||||
chmod 600 /certs/server.key"
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
| Volume | Purpose |
|
||||
|--------|---------|
|
||||
| `stegasoo-data` | User database, settings |
|
||||
| `stegasoo-certs` | SSL certificates |
|
||||
|
||||
## Smoke Test
|
||||
|
||||
```bash
|
||||
# Check container logs
|
||||
sudo docker logs stegasoo
|
||||
|
||||
# Test HTTP endpoint
|
||||
curl -k https://localhost:5000/health
|
||||
|
||||
# Expected: {"status":"ok","version":"4.1.7",...}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Container won't start:**
|
||||
```bash
|
||||
sudo docker logs stegasoo
|
||||
```
|
||||
|
||||
**Out of memory:**
|
||||
```bash
|
||||
# Argon2 needs 256MB+ per operation
|
||||
sudo docker run --memory=768m ...
|
||||
```
|
||||
|
||||
**Certificate errors:**
|
||||
```bash
|
||||
# Regenerate self-signed cert
|
||||
sudo docker exec stegasoo rm -rf /opt/stegasoo/frontends/web/certs/*
|
||||
sudo docker restart stegasoo
|
||||
```
|
||||
|
||||
**Reset everything:**
|
||||
```bash
|
||||
sudo docker stop stegasoo && sudo docker rm stegasoo
|
||||
sudo docker volume rm stegasoo-data stegasoo-certs
|
||||
```
|
||||
340
docs/stegasoo.1
Normal file
340
docs/stegasoo.1
Normal file
@@ -0,0 +1,340 @@
|
||||
.\" Stegasoo man page
|
||||
.\" Generate with: groff -man -Tascii stegasoo.1
|
||||
.TH STEGASOO 1 "January 2026" "Stegasoo 4.1.7" "User Commands"
|
||||
.SH NAME
|
||||
stegasoo \- steganography with hybrid authentication
|
||||
.SH SYNOPSIS
|
||||
.B stegasoo
|
||||
[\fB\-v\fR|\fB\-\-version\fR]
|
||||
[\fB\-\-json\fR]
|
||||
[\fB\-h\fR|\fB\-\-help\fR]
|
||||
.I command
|
||||
[\fIargs\fR]
|
||||
.SH DESCRIPTION
|
||||
.B stegasoo
|
||||
hides messages and files in images using PIN + passphrase security.
|
||||
It uses LSB (Least Significant Bit) steganography with optional DCT
|
||||
(Discrete Cosine Transform) encoding for JPEG resilience.
|
||||
.PP
|
||||
Messages are encrypted using a hybrid authentication scheme that combines
|
||||
a reference photo (shared secret), passphrase, and PIN code.
|
||||
.SH GLOBAL OPTIONS
|
||||
.TP
|
||||
.BR \-v ", " \-\-version
|
||||
Show version and exit.
|
||||
.TP
|
||||
.B \-\-json
|
||||
Output results as JSON (where supported).
|
||||
.TP
|
||||
.BR \-h ", " \-\-help
|
||||
Show help message and exit.
|
||||
.SH COMMANDS
|
||||
.SS encode
|
||||
Encode a message or file into an image.
|
||||
.PP
|
||||
.B stegasoo encode
|
||||
.I carrier
|
||||
.B \-r
|
||||
.I reference
|
||||
[\fB\-m\fR \fImessage\fR | \fB\-f\fR \fIfile\fR]
|
||||
[\fIoptions\fR]
|
||||
.TP
|
||||
.BR \-r ", " \-\-reference " " \fIPATH\fR
|
||||
Reference photo (shared secret). Required.
|
||||
.TP
|
||||
.BR \-m ", " \-\-message " " \fITEXT\fR
|
||||
Message to encode.
|
||||
.TP
|
||||
.BR \-f ", " \-\-file " " \fIPATH\fR
|
||||
File to embed instead of message.
|
||||
.TP
|
||||
.BR \-o ", " \-\-output " " \fIPATH\fR
|
||||
Output image path.
|
||||
.TP
|
||||
.B \-\-passphrase " " \fITEXT\fR
|
||||
Passphrase (recommend 4+ words). Prompts if not provided.
|
||||
.TP
|
||||
.B \-\-pin " " \fITEXT\fR
|
||||
PIN code. Prompts if not provided.
|
||||
.TP
|
||||
.B \-\-dry\-run
|
||||
Show capacity usage without encoding.
|
||||
.PP
|
||||
.B Examples:
|
||||
.nf
|
||||
stegasoo encode photo.png -r ref.jpg -m "Secret" --passphrase --pin
|
||||
stegasoo encode photo.png -r ref.jpg -f doc.pdf -o encoded.png
|
||||
.fi
|
||||
.SS decode
|
||||
Decode a message or file from an image.
|
||||
.PP
|
||||
.B stegasoo decode
|
||||
.I image
|
||||
.B \-r
|
||||
.I reference
|
||||
[\fIoptions\fR]
|
||||
.TP
|
||||
.BR \-r ", " \-\-reference " " \fIPATH\fR
|
||||
Reference photo (shared secret). Required.
|
||||
.TP
|
||||
.B \-\-passphrase " " \fITEXT\fR
|
||||
Passphrase. Prompts if not provided.
|
||||
.TP
|
||||
.B \-\-pin " " \fITEXT\fR
|
||||
PIN code. Prompts if not provided.
|
||||
.TP
|
||||
.BR \-o ", " \-\-output " " \fIPATH\fR
|
||||
Output path for file payloads.
|
||||
.PP
|
||||
.B Examples:
|
||||
.nf
|
||||
stegasoo decode encoded.png -r ref.jpg --passphrase --pin
|
||||
stegasoo decode encoded.png -r ref.jpg -o ./extracted/
|
||||
.fi
|
||||
.SS generate
|
||||
Generate random credentials (passphrase + PIN + optional channel key).
|
||||
.PP
|
||||
.B stegasoo generate
|
||||
[\fIoptions\fR]
|
||||
.TP
|
||||
.B \-\-words " " \fIINTEGER\fR
|
||||
Number of words in passphrase (default: 4).
|
||||
.TP
|
||||
.B \-\-pin\-length " " \fIINTEGER\fR
|
||||
PIN length (default: 6).
|
||||
.TP
|
||||
.B \-\-channel\-key
|
||||
Also generate a 256-bit channel key.
|
||||
.PP
|
||||
.B Examples:
|
||||
.nf
|
||||
stegasoo generate
|
||||
stegasoo generate --words 6 --pin-length 8
|
||||
stegasoo generate --channel-key
|
||||
.fi
|
||||
.SS info
|
||||
Show version, features, and system information.
|
||||
.PP
|
||||
.B stegasoo info
|
||||
[\fB\-\-full\fR]
|
||||
.TP
|
||||
.B \-\-full
|
||||
Show full system information (CPU, temperature, disk on Pi).
|
||||
.SS batch
|
||||
Batch operations on multiple images.
|
||||
.PP
|
||||
.B stegasoo batch
|
||||
.I subcommand
|
||||
[\fIargs\fR]
|
||||
.TP
|
||||
.B batch encode
|
||||
Encode message into multiple images.
|
||||
.RS
|
||||
.PP
|
||||
.B stegasoo batch encode
|
||||
.I images...
|
||||
[\fB\-m\fR \fImessage\fR | \fB\-f\fR \fIfile\fR]
|
||||
[\fIoptions\fR]
|
||||
.PP
|
||||
Options: \fB\-m\fR, \fB\-f\fR, \fB\-o\fR/\fB\-\-output\-dir\fR, \fB\-\-suffix\fR, \fB\-\-passphrase\fR, \fB\-\-pin\fR,
|
||||
\fB\-r\fR/\fB\-\-recursive\fR, \fB\-j\fR/\fB\-\-jobs\fR, \fB\-v\fR/\fB\-\-verbose\fR.
|
||||
.RE
|
||||
.TP
|
||||
.B batch decode
|
||||
Decode messages from multiple images.
|
||||
.RS
|
||||
.PP
|
||||
.B stegasoo batch decode
|
||||
.I images...
|
||||
[\fIoptions\fR]
|
||||
.PP
|
||||
Options: \fB\-o\fR/\fB\-\-output\-dir\fR, \fB\-\-passphrase\fR, \fB\-\-pin\fR, \fB\-r\fR/\fB\-\-recursive\fR,
|
||||
\fB\-j\fR/\fB\-\-jobs\fR, \fB\-v\fR/\fB\-\-verbose\fR.
|
||||
.RE
|
||||
.TP
|
||||
.B batch check
|
||||
Check capacity of multiple images.
|
||||
.RS
|
||||
.PP
|
||||
.B stegasoo batch check
|
||||
.I images...
|
||||
[\fB\-r\fR/\fB\-\-recursive\fR]
|
||||
.RE
|
||||
.SS channel
|
||||
Manage channel keys for deployment isolation.
|
||||
.PP
|
||||
Channel keys bind encode/decode operations to a specific group or deployment.
|
||||
Messages encoded with one channel key can only be decoded by systems with
|
||||
the same channel key.
|
||||
.PP
|
||||
.B stegasoo channel
|
||||
.I subcommand
|
||||
[\fIargs\fR]
|
||||
.TP
|
||||
.B channel generate
|
||||
Generate a new random channel key.
|
||||
.RS
|
||||
.PP
|
||||
Options: \fB\-\-save\fR (project config), \fB\-\-save\-user\fR (user config).
|
||||
.RE
|
||||
.TP
|
||||
.B channel show
|
||||
Show the current channel key.
|
||||
.RS
|
||||
.PP
|
||||
Options: \fB\-\-key\fR \fITEXT\fR (show specific key instead).
|
||||
.RE
|
||||
.TP
|
||||
.B channel qr
|
||||
Display channel key as QR code.
|
||||
.RS
|
||||
.PP
|
||||
Options: \fB\-\-key\fR \fITEXT\fR, \fB\-\-format\fR [\fIascii\fR|\fIpng\fR], \fB\-o\fR/\fB\-\-output\fR \fIPATH\fR.
|
||||
.RE
|
||||
.TP
|
||||
.B channel status
|
||||
Show channel key status and configuration.
|
||||
.TP
|
||||
.B channel clear
|
||||
Remove channel key configuration.
|
||||
.RS
|
||||
.PP
|
||||
Options: \fB\-\-project\fR, \fB\-\-user\fR.
|
||||
.RE
|
||||
.SS admin
|
||||
Web UI administration commands.
|
||||
.PP
|
||||
.B stegasoo admin
|
||||
.I subcommand
|
||||
[\fIargs\fR]
|
||||
.TP
|
||||
.B admin generate\-key
|
||||
Generate a new recovery key (for reference only).
|
||||
.RS
|
||||
.PP
|
||||
Options: \fB\-\-qr\fR (show QR code in terminal).
|
||||
.RE
|
||||
.TP
|
||||
.B admin recover
|
||||
Reset admin password using recovery key.
|
||||
.RS
|
||||
.PP
|
||||
Options: \fB\-\-db\fR \fIPATH\fR (path to stegasoo.db), \fB\-\-password\fR \fITEXT\fR.
|
||||
.RE
|
||||
.SS tools
|
||||
Image security tools.
|
||||
.PP
|
||||
.B stegasoo tools
|
||||
.I subcommand
|
||||
[\fIargs\fR]
|
||||
.TP
|
||||
.B tools capacity
|
||||
Show steganography capacity for an image.
|
||||
.RS
|
||||
.PP
|
||||
.B stegasoo tools capacity
|
||||
.I image
|
||||
[\fB\-\-json\fR]
|
||||
.RE
|
||||
.TP
|
||||
.B tools exif
|
||||
View or edit EXIF metadata.
|
||||
.RS
|
||||
.PP
|
||||
.B stegasoo tools exif
|
||||
.I image
|
||||
[\fB\-\-clear\fR] [\fB\-\-set\fR \fIFIELD=VALUE\fR] [\fB\-o\fR \fIPATH\fR] [\fB\-\-json\fR]
|
||||
.RE
|
||||
.TP
|
||||
.B tools peek
|
||||
Check if image contains Stegasoo hidden data.
|
||||
.RS
|
||||
.PP
|
||||
.B stegasoo tools peek
|
||||
.I image
|
||||
[\fB\-\-json\fR]
|
||||
.RE
|
||||
.TP
|
||||
.B tools strip
|
||||
Strip EXIF/metadata from an image.
|
||||
.RS
|
||||
.PP
|
||||
.B stegasoo tools strip
|
||||
.I image
|
||||
[\fB\-o\fR \fIPATH\fR] [\fB\-\-format\fR [\fIpng\fR|\fIbmp\fR]]
|
||||
.RE
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B STEGASOO_CHANNEL_KEY
|
||||
Channel key for encode/decode operations. Overrides config file settings.
|
||||
.TP
|
||||
.B STEGASOO_HTTPS_ENABLED
|
||||
Enable HTTPS for web UI (Docker/service mode).
|
||||
.TP
|
||||
.B STEGASOO_HOSTNAME
|
||||
Hostname for SSL certificate generation.
|
||||
.SH FILES
|
||||
.TP
|
||||
.I ~/.stegasoo/channel.key
|
||||
User's channel key configuration (encrypted).
|
||||
.TP
|
||||
.I .stegasoo.toml
|
||||
Project-level configuration file.
|
||||
.TP
|
||||
.I frontends/web/instance/stegasoo.db
|
||||
Web UI SQLite database (accounts, settings).
|
||||
.SH EXAMPLES
|
||||
.SS Basic encode/decode workflow
|
||||
.nf
|
||||
# Generate credentials
|
||||
stegasoo generate
|
||||
|
||||
# Encode a secret message
|
||||
stegasoo encode vacation.png -r selfie.jpg -m "Meet at noon"
|
||||
|
||||
# Decode the message (on another system with same reference photo)
|
||||
stegasoo decode vacation_steg.png -r selfie.jpg
|
||||
.fi
|
||||
.SS Using channel keys for team isolation
|
||||
.nf
|
||||
# Generate and save a channel key
|
||||
stegasoo channel generate --save-user
|
||||
|
||||
# Share the key with your team
|
||||
stegasoo channel qr -o team-key.png
|
||||
|
||||
# Now all encode/decode operations use this channel
|
||||
stegasoo encode photo.png -r ref.jpg -m "Team secret"
|
||||
.fi
|
||||
.SS Batch processing
|
||||
.nf
|
||||
# Check capacity of all PNGs in a directory
|
||||
stegasoo batch check ./photos/*.png
|
||||
|
||||
# Encode same message into multiple images
|
||||
stegasoo batch encode ./photos/ -r ref.jpg -m "Secret" -o ./encoded/
|
||||
.fi
|
||||
.SH SECURITY
|
||||
Stegasoo uses multiple layers of security:
|
||||
.IP \(bu 2
|
||||
Reference photo provides a visual shared secret
|
||||
.IP \(bu 2
|
||||
Passphrase (recommend 4+ words) for strong encryption
|
||||
.IP \(bu 2
|
||||
PIN code adds additional entropy
|
||||
.IP \(bu 2
|
||||
Channel keys isolate different deployments
|
||||
.IP \(bu 2
|
||||
AES-256 encryption for payload data
|
||||
.PP
|
||||
For maximum security, share the reference photo out-of-band (in person,
|
||||
secure messenger) and use a strong passphrase.
|
||||
.SH SEE ALSO
|
||||
.BR openssl (1),
|
||||
.BR qrencode (1)
|
||||
.SH BUGS
|
||||
Report bugs at: https://github.com/adlee-was-taken/stegasoo/issues
|
||||
.SH AUTHOR
|
||||
Written by the Stegasoo contributors.
|
||||
.SH COPYRIGHT
|
||||
Copyright \(co 2024-2026. MIT License.
|
||||
75
frontends/web/docker-entrypoint.sh
Normal file
75
frontends/web/docker-entrypoint.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Docker entrypoint for Stegasoo Web UI
|
||||
# Handles SSL certificate generation and gunicorn startup
|
||||
#
|
||||
# Supports mkcert for browser-trusted certificates (no warning screen)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
CERT_DIR="/app/frontends/web/certs"
|
||||
CERT_FILE="$CERT_DIR/cert.pem"
|
||||
KEY_FILE="$CERT_DIR/key.pem"
|
||||
HOSTNAME="${STEGASOO_HOSTNAME:-localhost}"
|
||||
|
||||
# Generate SSL certificates
|
||||
# Priority: 1) Existing certs, 2) mkcert (trusted), 3) openssl (self-signed)
|
||||
generate_certs() {
|
||||
if [ -f "$CERT_FILE" ] && [ -f "$KEY_FILE" ]; then
|
||||
echo "Using existing SSL certificates."
|
||||
return
|
||||
fi
|
||||
|
||||
mkdir -p "$CERT_DIR"
|
||||
|
||||
# Try mkcert first (creates browser-trusted certs)
|
||||
if command -v mkcert &> /dev/null; then
|
||||
echo "Generating trusted certificate with mkcert for $HOSTNAME..."
|
||||
cd "$CERT_DIR"
|
||||
mkcert -key-file key.pem -cert-file cert.pem "$HOSTNAME" localhost 127.0.0.1 ::1
|
||||
echo "Trusted certificate generated."
|
||||
echo ""
|
||||
echo " To trust on other devices, install the CA cert from:"
|
||||
echo " $(mkcert -CAROOT)/rootCA.pem"
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
|
||||
# Fallback to self-signed (shows browser warning)
|
||||
echo "Generating self-signed SSL certificate for $HOSTNAME..."
|
||||
echo "(Install mkcert for browser-trusted certs without warnings)"
|
||||
|
||||
openssl req -x509 -newkey rsa:2048 \
|
||||
-keyout "$KEY_FILE" \
|
||||
-out "$CERT_FILE" \
|
||||
-sha256 -days 365 -nodes \
|
||||
-subj "/CN=$HOSTNAME" \
|
||||
-addext "subjectAltName=DNS:$HOSTNAME,DNS:localhost,IP:127.0.0.1" \
|
||||
2>/dev/null
|
||||
|
||||
echo "Self-signed certificate generated."
|
||||
}
|
||||
|
||||
# Start gunicorn with appropriate settings
|
||||
if [ "${STEGASOO_HTTPS_ENABLED:-false}" = "true" ]; then
|
||||
echo "HTTPS mode enabled"
|
||||
generate_certs
|
||||
|
||||
exec gunicorn \
|
||||
--bind 0.0.0.0:5000 \
|
||||
--workers 2 \
|
||||
--threads 4 \
|
||||
--timeout 120 \
|
||||
--certfile "$CERT_FILE" \
|
||||
--keyfile "$KEY_FILE" \
|
||||
app:app
|
||||
else
|
||||
echo "HTTP mode (HTTPS disabled)"
|
||||
exec gunicorn \
|
||||
--bind 0.0.0.0:5000 \
|
||||
--workers 2 \
|
||||
--threads 4 \
|
||||
--timeout 120 \
|
||||
app:app
|
||||
fi
|
||||
@@ -16,7 +16,7 @@
|
||||
--overlay-dark: rgba(0, 0, 0, 0.3);
|
||||
--overlay-light: rgba(255, 255, 255, 0.05);
|
||||
--day-highlight: #E3FF54; /* Bright yellow/green for day of week */
|
||||
--header-gold: #fee862; /* Halfway between light straw and 24k gold */
|
||||
--header-gold: #e5d058; /* Muted gold - less harsh on varied monitors */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -116,6 +116,31 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Form Labels - Gold
|
||||
---------------------------------------------------------------------------- */
|
||||
.card .form-label {
|
||||
color: #d9c580;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Dropdown selects - ensure chevron is visible in dark mode */
|
||||
.form-select,
|
||||
select.form-select {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23d9c580' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: right 0.75rem center !important;
|
||||
background-size: 16px 12px !important;
|
||||
padding-right: 2.25rem !important;
|
||||
}
|
||||
|
||||
/* Payload type toggle - gold text when selected */
|
||||
.btn-check:checked + .btn-outline-primary {
|
||||
color: #d9c580 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Security Factor Boxes - Matches drop-zone dashed border style
|
||||
---------------------------------------------------------------------------- */
|
||||
@@ -153,11 +178,22 @@ body {
|
||||
z-index: 1030; /* Above page content for dropdowns */
|
||||
}
|
||||
|
||||
.navbar > .container {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* Ensure navbar dropdown appears above all page content */
|
||||
.navbar .dropdown-menu {
|
||||
z-index: 1031;
|
||||
}
|
||||
|
||||
/* Left-align collapsed navbar menu on mobile */
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar-collapse .navbar-nav {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Nav Icons - Floating Label on Hover (label floats below, no layout shift)
|
||||
---------------------------------------------------------------------------- */
|
||||
@@ -192,26 +228,22 @@ body {
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-4px);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 1px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
color: var(--header-gold);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
background: linear-gradient(135deg, rgba(74, 40, 96, 0.95) 0%, rgba(85, 112, 212, 0.9) 100%);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
|
||||
transition: opacity 0.2s ease,
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
.nav-expand:hover {
|
||||
background: linear-gradient(135deg, rgba(74, 40, 96, 0.5) 0%, rgba(85, 112, 212, 0.4) 100%);
|
||||
box-shadow: 0 0 12px rgba(102, 126, 234, 0.25),
|
||||
background: linear-gradient(135deg, rgba(74, 40, 96, 0.25) 0%, rgba(85, 112, 212, 0.2) 100%);
|
||||
box-shadow: 0 0 8px rgba(102, 126, 234, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@@ -1228,7 +1260,8 @@ footer {
|
||||
---------------------------------------------------------------------------- */
|
||||
#rsaQrSection {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#rsaQrSection .drop-zone {
|
||||
@@ -1854,7 +1887,7 @@ footer {
|
||||
.tools-ribbon-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tools-ribbon-divider {
|
||||
@@ -1871,8 +1904,8 @@ footer {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 48px;
|
||||
width: 64px;
|
||||
height: 52px;
|
||||
padding: 0.25rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
@@ -1888,15 +1921,18 @@ footer {
|
||||
}
|
||||
|
||||
.tool-icon-btn span {
|
||||
font-size: 0.6rem;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.tool-icon-btn:hover {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background: rgba(255, 230, 150, 0.1);
|
||||
border-color: rgba(255, 230, 150, 0.3);
|
||||
color: var(--header-gold);
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.33);
|
||||
}
|
||||
|
||||
.tool-icon-btn.active {
|
||||
|
||||
@@ -271,8 +271,7 @@
|
||||
<div class="card-body">
|
||||
<p class="small mb-2">Uses server-configured key if available, otherwise public mode.</p>
|
||||
<ul class="small mb-0">
|
||||
<li>Set via <code>STEGASOO_CHANNEL_KEY</code> env var</li>
|
||||
<li>Or <code>channel_key</code> in config file</li>
|
||||
<li>Server admin configures the shared key</li>
|
||||
<li>All users share the same channel</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -321,7 +320,6 @@
|
||||
<i class="bi bi-shield-lock me-2"></i>
|
||||
<strong>This server has a channel key configured:</strong>
|
||||
<code class="ms-2">{{ channel_fingerprint }}</code>
|
||||
<span class="text-muted ms-2">({{ channel_source }})</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info mt-3 mb-0">
|
||||
|
||||
@@ -11,14 +11,19 @@
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Stegasoo" height="36" class="me-2">
|
||||
<span style="position: relative; display: inline-block; margin-top: -14px;">
|
||||
<span class="fw-bold title-gold">Stegasoo</span>
|
||||
<span class="badge bg-success" style="position: absolute; font-size: 0.45rem; bottom: -8px; right: 6px;">v4.1</span>
|
||||
</span>
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/" style="padding-left: 6px; margin-right: 8px;">
|
||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Stegasoo" height="28">
|
||||
</a>
|
||||
{% if channel_configured %}
|
||||
<span class="badge bg-success bg-opacity-25 small" style="padding-left: 0.35rem;" title="Private Channel: {{ channel_fingerprint }}">
|
||||
<i class="bi bi-shield-lock me-2" style="color: #6ee7b7;"></i><code style="font-size: 0.7rem; font-weight: 300; color: #c9a860;">{{ channel_fingerprint[:4] }}-••••-{{ channel_fingerprint[-4:] }}</code>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary bg-opacity-25 small text-muted" style="padding-left: 0.35rem;" title="Public Channel: No shared channel key configured. Messages use only passphrase and PIN for encryption.">
|
||||
<i class="bi bi-globe me-1"></i>Public Channel
|
||||
</span>
|
||||
{% endif %}
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
@@ -6,20 +6,25 @@
|
||||
<style>
|
||||
/* Accordion styling */
|
||||
.step-accordion .accordion-button {
|
||||
background: rgba(30, 40, 50, 0.6);
|
||||
background: rgba(35, 45, 55, 0.8);
|
||||
color: #fff;
|
||||
padding: 0.75rem 1rem;
|
||||
border-left: 3px solid transparent;
|
||||
border-left: 3px solid rgba(255, 230, 153, 0.3);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.step-accordion .accordion-button:hover {
|
||||
background: rgba(45, 55, 65, 0.9);
|
||||
border-left-color: rgba(255, 230, 153, 0.5);
|
||||
}
|
||||
.step-accordion .accordion-button:not(.collapsed) {
|
||||
background: linear-gradient(90deg, rgba(99, 179, 237, 0.15) 0%, rgba(40, 50, 60, 0.8) 40%, rgba(40, 50, 60, 0.8) 100%);
|
||||
background: linear-gradient(90deg, rgba(255, 230, 153, 0.12) 0%, rgba(40, 50, 60, 0.85) 40%, rgba(40, 50, 60, 0.85) 100%);
|
||||
color: #fff;
|
||||
box-shadow: inset 0 1px 0 rgba(99, 179, 237, 0.1);
|
||||
border-left: 3px solid rgba(99, 179, 237, 0.6);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 230, 153, 0.1);
|
||||
border-left: 3px solid #ffe699;
|
||||
}
|
||||
.step-accordion .accordion-button::after {
|
||||
filter: invert(1);
|
||||
filter: invert(1) sepia(1) saturate(2) hue-rotate(5deg) brightness(1.2);
|
||||
}
|
||||
.step-accordion .accordion-body {
|
||||
background: rgba(30, 40, 50, 0.4);
|
||||
@@ -106,46 +111,7 @@
|
||||
box-shadow: 0 0 20px rgba(246, 173, 85, 0.4) !important;
|
||||
}
|
||||
|
||||
/* QR Crop Animation */
|
||||
.qr-crop-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 120px;
|
||||
}
|
||||
.qr-crop-container img {
|
||||
display: block;
|
||||
max-height: 180px;
|
||||
max-width: 180px;
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.qr-crop-container .qr-original { opacity: 1; }
|
||||
.qr-crop-container .qr-cropped {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.3);
|
||||
opacity: 0;
|
||||
max-height: 160px;
|
||||
min-width: 140px;
|
||||
min-height: 140px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.qr-crop-container.scan-complete .qr-original {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
filter: blur(4px);
|
||||
}
|
||||
.qr-crop-container.scan-complete .qr-cropped {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
/* QR Crop Animation - uses .qr-scan-container from style.css */
|
||||
</style>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
@@ -273,20 +239,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extraction Mode (compact inline) -->
|
||||
<!-- Extraction Mode -->
|
||||
<div class="d-flex gap-2 align-items-center flex-wrap mb-2">
|
||||
<label class="mode-btn mode-btn-sm active" id="autoModeCard" for="modeAuto">
|
||||
<input class="form-check-input" type="radio" name="embed_mode" id="modeAuto" value="auto" checked>
|
||||
<i class="bi bi-magic text-success"></i> Auto
|
||||
</label>
|
||||
<label class="mode-btn mode-btn-sm" id="lsbModeCard" for="modeLsb">
|
||||
<input class="form-check-input" type="radio" name="embed_mode" id="modeLsb" value="lsb">
|
||||
<i class="bi bi-grid-3x3-gap text-primary"></i> LSB
|
||||
</label>
|
||||
<label class="mode-btn mode-btn-sm {% if not has_dct %}opacity-50{% endif %}" id="dctModeCard" for="modeDct">
|
||||
<input class="form-check-input" type="radio" name="embed_mode" id="modeDct" value="dct" {% if not has_dct %}disabled{% endif %}>
|
||||
<i class="bi bi-soundwave text-warning"></i> DCT
|
||||
</label>
|
||||
<div class="btn-group" role="group">
|
||||
<input type="radio" class="btn-check" name="embed_mode" id="modeAuto" value="auto" checked>
|
||||
<label class="btn btn-outline-secondary text-nowrap" for="modeAuto"><i class="bi bi-magic me-1"></i>Auto</label>
|
||||
<input type="radio" class="btn-check" name="embed_mode" id="modeLsb" value="lsb">
|
||||
<label class="btn btn-outline-secondary text-nowrap" for="modeLsb"><i class="bi bi-grid-3x3-gap me-1"></i>LSB</label>
|
||||
<input type="radio" class="btn-check" name="embed_mode" id="modeDct" value="dct" {% if not has_dct %}disabled{% endif %}>
|
||||
<label class="btn btn-outline-secondary text-nowrap" for="modeDct" id="dctModeLabel"><i class="bi bi-soundwave me-1"></i>DCT</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text" id="modeHint">
|
||||
<i class="bi bi-lightning me-1"></i>Tries LSB first, then DCT
|
||||
@@ -390,7 +352,7 @@
|
||||
<i class="bi bi-qr-code-scan fs-5 d-block text-muted mb-1"></i>
|
||||
<span class="text-muted small">Drop QR image</span>
|
||||
</div>
|
||||
<div class="qr-scan-container qr-crop-container d-none" id="qrCropContainer">
|
||||
<div class="qr-scan-container d-none" id="qrCropContainer">
|
||||
<img class="qr-original" id="qrOriginal" alt="Original">
|
||||
<img class="qr-cropped" id="qrCropped" alt="Cropped">
|
||||
</div>
|
||||
@@ -548,15 +510,10 @@ document.querySelector('input[name="rsa_key"]')?.addEventListener('change', upda
|
||||
// MODE SWITCHING
|
||||
// ============================================================================
|
||||
|
||||
const modeRadios = document.querySelectorAll('input[name="embed_mode"]');
|
||||
const modeBtns = { 'auto': document.getElementById('autoModeCard'), 'lsb': document.getElementById('lsbModeCard'), 'dct': document.getElementById('dctModeCard') };
|
||||
|
||||
modeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
Object.values(modeBtns).forEach(btn => btn?.classList.remove('active'));
|
||||
modeBtns[radio.value]?.classList.add('active');
|
||||
});
|
||||
});
|
||||
// Apply disabled styling to DCT if not available
|
||||
if (document.getElementById('modeDct')?.disabled) {
|
||||
document.getElementById('dctModeLabel')?.classList.add('disabled', 'text-muted');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LOADING STATE
|
||||
|
||||
@@ -6,20 +6,25 @@
|
||||
<style>
|
||||
/* Accordion styling */
|
||||
.step-accordion .accordion-button {
|
||||
background: rgba(30, 40, 50, 0.6);
|
||||
background: rgba(35, 45, 55, 0.8);
|
||||
color: #fff;
|
||||
padding: 0.75rem 1rem;
|
||||
border-left: 3px solid transparent;
|
||||
border-left: 3px solid rgba(255, 230, 153, 0.3);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.step-accordion .accordion-button:hover {
|
||||
background: rgba(45, 55, 65, 0.9);
|
||||
border-left-color: rgba(255, 230, 153, 0.5);
|
||||
}
|
||||
.step-accordion .accordion-button:not(.collapsed) {
|
||||
background: linear-gradient(90deg, rgba(99, 179, 237, 0.15) 0%, rgba(40, 50, 60, 0.8) 40%, rgba(40, 50, 60, 0.8) 100%);
|
||||
background: linear-gradient(90deg, rgba(255, 230, 153, 0.12) 0%, rgba(40, 50, 60, 0.85) 40%, rgba(40, 50, 60, 0.85) 100%);
|
||||
color: #fff;
|
||||
box-shadow: inset 0 1px 0 rgba(99, 179, 237, 0.1);
|
||||
border-left: 3px solid rgba(99, 179, 237, 0.6);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 230, 153, 0.1);
|
||||
border-left: 3px solid #ffe699;
|
||||
}
|
||||
.step-accordion .accordion-button::after {
|
||||
filter: invert(1);
|
||||
filter: invert(1) sepia(1) saturate(2) hue-rotate(5deg) brightness(1.2);
|
||||
}
|
||||
.step-accordion .accordion-body {
|
||||
background: rgba(30, 40, 50, 0.4);
|
||||
@@ -106,46 +111,7 @@
|
||||
box-shadow: 0 0 20px rgba(246, 173, 85, 0.4) !important;
|
||||
}
|
||||
|
||||
/* QR Crop Animation */
|
||||
.qr-crop-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 120px;
|
||||
}
|
||||
.qr-crop-container img {
|
||||
display: block;
|
||||
max-height: 180px;
|
||||
max-width: 180px;
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.qr-crop-container .qr-original { opacity: 1; }
|
||||
.qr-crop-container .qr-cropped {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.3);
|
||||
opacity: 0;
|
||||
max-height: 160px;
|
||||
min-width: 140px;
|
||||
min-height: 140px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.qr-crop-container.scan-complete .qr-original {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
filter: blur(4px);
|
||||
}
|
||||
.qr-crop-container.scan-complete .qr-cropped {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
/* QR Crop Animation - uses .qr-scan-container from style.css */
|
||||
</style>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
@@ -202,7 +168,7 @@
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-image me-1"></i> Carrier Image
|
||||
<i class="bi bi-file-earmark-image me-1"></i> Carrier Image
|
||||
</label>
|
||||
<div class="drop-zone pixel-container" id="carrierDropZone">
|
||||
<input type="file" name="carrier" accept="image/*" required id="carrierInput">
|
||||
@@ -240,23 +206,20 @@
|
||||
|
||||
<!-- Embedding Mode (compact inline) -->
|
||||
<div class="d-flex gap-2 align-items-center flex-wrap mb-2">
|
||||
<label class="mode-btn mode-btn-sm {% if not has_dct %}opacity-50{% endif %} {% if has_dct %}active{% endif %}" id="dctModeCard" for="modeDct">
|
||||
<input class="form-check-input" type="radio" name="embed_mode" id="modeDct" value="dct" {% if has_dct %}checked{% endif %} {% if not has_dct %}disabled{% endif %}>
|
||||
<i class="bi bi-soundwave text-warning"></i> DCT
|
||||
</label>
|
||||
<label class="mode-btn mode-btn-sm {% if not has_dct %}active{% endif %}" id="lsbModeCard" for="modeLsb">
|
||||
<input class="form-check-input" type="radio" name="embed_mode" id="modeLsb" value="lsb" {% if not has_dct %}checked{% endif %}>
|
||||
<i class="bi bi-grid-3x3-gap text-primary"></i> LSB
|
||||
</label>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<input type="radio" class="btn-check" name="embed_mode" id="modeDct" value="dct" {% if has_dct %}checked{% endif %} {% if not has_dct %}disabled{% endif %}>
|
||||
<label class="btn btn-outline-secondary btn-sm text-nowrap" for="modeDct" id="dctModeLabel"><i class="bi bi-soundwave me-1"></i>DCT</label>
|
||||
<input type="radio" class="btn-check" name="embed_mode" id="modeLsb" value="lsb" {% if not has_dct %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary btn-sm text-nowrap" for="modeLsb"><i class="bi bi-grid-3x3-gap me-1"></i>LSB</label>
|
||||
</div>
|
||||
<span class="text-muted d-none d-sm-inline">|</span>
|
||||
<span class="d-flex gap-2 align-items-center" id="outputOptions">
|
||||
<span class="text-muted">|</span>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<input type="radio" class="btn-check" name="dct_color_mode" id="colorMode" value="color" checked>
|
||||
<label class="btn btn-outline-secondary btn-sm" for="colorMode">Color</label>
|
||||
<input type="radio" class="btn-check" name="dct_color_mode" id="grayMode" value="grayscale">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="grayMode" id="grayModeLabel">Gray</label>
|
||||
</div>
|
||||
<span class="text-muted">|</span>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<input type="radio" class="btn-check" name="dct_output_format" id="jpegFormat" value="jpeg" checked>
|
||||
<label class="btn btn-outline-secondary btn-sm" for="jpegFormat" id="jpegFormatLabel">JPEG</label>
|
||||
@@ -426,7 +389,7 @@
|
||||
<i class="bi bi-qr-code-scan fs-5 d-block text-muted mb-1"></i>
|
||||
<span class="text-muted small">Drop QR image</span>
|
||||
</div>
|
||||
<div class="qr-scan-container qr-crop-container d-none" id="qrCropContainer">
|
||||
<div class="qr-scan-container d-none" id="qrCropContainer">
|
||||
<img class="qr-original" id="qrOriginal" alt="Original">
|
||||
<img class="qr-cropped" id="qrCropped" alt="Cropped">
|
||||
</div>
|
||||
@@ -471,7 +434,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<i class="bi bi-eye-slash fs-5 d-block mb-1 text-warning"></i>
|
||||
Undetectable
|
||||
Covertly Embedded
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -681,7 +644,7 @@ carrierInput?.addEventListener('change', function() {
|
||||
// ============================================================================
|
||||
|
||||
const modeRadios = document.querySelectorAll('input[name="embed_mode"]');
|
||||
const modeBtns = { 'dct': document.getElementById('dctModeCard'), 'lsb': document.getElementById('lsbModeCard') };
|
||||
const dctModeLabel = document.getElementById('dctModeLabel');
|
||||
const grayModeInput = document.getElementById('grayMode');
|
||||
const grayModeLabel = document.getElementById('grayModeLabel');
|
||||
const jpegFormatInput = document.getElementById('jpegFormat');
|
||||
@@ -689,6 +652,11 @@ const jpegFormatLabel = document.getElementById('jpegFormatLabel');
|
||||
const colorModeInput = document.getElementById('colorMode');
|
||||
const pngFormatInput = document.getElementById('pngFormat');
|
||||
|
||||
// Apply disabled styling to DCT if not available
|
||||
if (document.getElementById('modeDct')?.disabled) {
|
||||
dctModeLabel?.classList.add('disabled', 'text-muted');
|
||||
}
|
||||
|
||||
function updateOutputOptions(mode) {
|
||||
const isLsb = mode === 'lsb';
|
||||
if (isLsb) {
|
||||
@@ -711,11 +679,7 @@ function updateOutputOptions(mode) {
|
||||
}
|
||||
|
||||
modeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
Object.values(modeBtns).forEach(btn => btn?.classList.remove('active'));
|
||||
modeBtns[radio.value]?.classList.add('active');
|
||||
updateOutputOptions(radio.value);
|
||||
});
|
||||
radio.addEventListener('change', () => updateOutputOptions(radio.value));
|
||||
});
|
||||
|
||||
// Initialize output options based on initial mode
|
||||
|
||||
@@ -100,8 +100,8 @@
|
||||
<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">
|
||||
<i class="bi bi-shuffle me-1"></i>Generate
|
||||
<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>
|
||||
@@ -483,17 +483,17 @@
|
||||
/* Responsive */
|
||||
@media (max-width: 576px) {
|
||||
.pin-container, .passphrase-container {
|
||||
padding: 1rem 1.25rem;
|
||||
padding: 1rem 0.75rem;
|
||||
}
|
||||
|
||||
.pin-digit-box {
|
||||
width: 2.25rem;
|
||||
height: 2.75rem;
|
||||
font-size: 1.25rem;
|
||||
width: 1.9rem;
|
||||
height: 2.4rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.pin-digits-row {
|
||||
gap: 0.35rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.passphrase-text {
|
||||
|
||||
@@ -3,170 +3,64 @@
|
||||
{% block title %}Stegasoo - Secure Steganography{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.home-icon {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.home-icon i {
|
||||
font-size: 2.5rem;
|
||||
color: #fff;
|
||||
margin-bottom: 0.5rem;
|
||||
filter: drop-shadow(0 3px 2px rgba(0, 0, 0, 0.9));
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.home-icon span {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.home-icon:hover i {
|
||||
color: #e5d058;
|
||||
transform: translateY(-3px);
|
||||
filter: drop-shadow(0 5px 4px rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
.home-icon:hover span {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
color: #e5d058;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-end justify-content-center gap-4">
|
||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Stegasoo" height="155">
|
||||
<div style="margin-bottom: 40px;">
|
||||
<h1 class="display-4 fw-bold mb-2 title-gold">
|
||||
Stegasoo
|
||||
<span class="badge bg-success fs-6 ms-2">v4.1</span>
|
||||
</h1>
|
||||
<p class="lead text-muted mb-0">Hide encrypted data in plain sight.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 70vh;">
|
||||
|
||||
<!-- Channel Status Banner (v4.0.0) -->
|
||||
{% if channel_configured %}
|
||||
<div class="alert alert-success mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<!-- Hero -->
|
||||
<div class="d-flex align-items-center mb-4" style="gap: 8px;">
|
||||
<div class="position-relative">
|
||||
<img src="{{ url_for('static', filename='logo.svg') }}" alt="Stegasoo" height="80">
|
||||
<span class="badge bg-success position-absolute" style="bottom: 1px; left: -6px; font-size: 0.6rem;">v4.1</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-shield-lock me-2"></i>
|
||||
<strong>Private Channel Mode</strong>
|
||||
</div>
|
||||
<div class="key-capsule">
|
||||
<span class="badge led-badge-yellow"><span class="led-indicator led-yellow me-1"></span>Key Loaded</span>
|
||||
<code class="small ms-2">{{ channel_fingerprint }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<!-- Encode Card -->
|
||||
<div class="col-md-4">
|
||||
<a href="/encode" class="text-decoration-none card-link">
|
||||
<div class="card h-100 feature-card">
|
||||
<div class="card-header text-center py-3">
|
||||
<i class="bi bi-lock-fill fs-1 embossed-icon"></i>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Encode</h5>
|
||||
<p class="card-text text-muted">
|
||||
Hide encrypted messages or files inside images
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Decode Card -->
|
||||
<div class="col-md-4">
|
||||
<a href="/decode" class="text-decoration-none card-link">
|
||||
<div class="card h-100 feature-card">
|
||||
<div class="card-header text-center py-3">
|
||||
<i class="bi bi-unlock-fill fs-1 embossed-icon"></i>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Decode</h5>
|
||||
<p class="card-text text-muted">
|
||||
Extract and decrypt hidden data from stego images
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Generate Card -->
|
||||
<div class="col-md-4">
|
||||
<a href="/generate" class="text-decoration-none card-link">
|
||||
<div class="card h-100 feature-card">
|
||||
<div class="card-header text-center py-3">
|
||||
<i class="bi bi-key-fill fs-1 embossed-icon"></i>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Generate</h5>
|
||||
<p class="card-text text-muted">
|
||||
Create passphrases, PINs, and RSA keys
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<h1 class="display-5 fw-bold title-gold mb-0">Stegasoo</h1>
|
||||
<p class="text-muted mb-0 small" style="margin-top: 3px; padding-left: 3px; font-size: 0.85rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);">Hide encrypted data in plain sight.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Embedding Modes -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Embedding Modes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-6 mb-3 mb-md-0">
|
||||
<div class="p-3 bg-dark rounded h-100">
|
||||
<i class="bi bi-soundwave text-warning fs-2 d-block mb-2"></i>
|
||||
<strong>DCT Mode</strong>
|
||||
<span class="badge bg-success ms-1">Default</span>
|
||||
<div class="small text-muted mt-2">
|
||||
Survives JPEG recompression<br>
|
||||
Best for social media
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 bg-dark rounded h-100">
|
||||
<i class="bi bi-grid-3x3-gap text-primary fs-2 d-block mb-2"></i>
|
||||
<strong>LSB Mode</strong>
|
||||
<div class="small text-muted mt-2">
|
||||
Higher capacity (~375 KB/MP)<br>
|
||||
Best for email & file transfer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Action Icons -->
|
||||
<div class="d-flex gap-4">
|
||||
<a href="/encode" class="home-icon"><i class="bi bi-lock-fill"></i><span>Encode</span></a>
|
||||
<a href="/decode" class="home-icon"><i class="bi bi-unlock-fill"></i><span>Decode</span></a>
|
||||
<a href="/generate" class="home-icon"><i class="bi bi-key-fill"></i><span>Generate</span></a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-diagram-3 me-2"></i>How It Works</h5>
|
||||
<a href="/about" class="btn btn-sm btn-outline-light">Learn More</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-primary"><i class="bi bi-key me-2"></i>You Provide</h6>
|
||||
<ul class="list-unstyled small">
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-image text-info me-2"></i>
|
||||
<strong>Reference Photo</strong>: shared secret
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-chat-quote text-info me-2"></i>
|
||||
<strong>Passphrase</strong>: 4+ words
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-123 text-info me-2"></i>
|
||||
<strong>PIN</strong>: 6-9 digits (or RSA key)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-primary"><i class="bi bi-shield-check me-2"></i>Security</h6>
|
||||
<ul class="list-unstyled small">
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-lock text-success me-2"></i>
|
||||
AES-256-GCM encryption
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-memory text-success me-2"></i>
|
||||
Argon2id key derivation (256MB)
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-shuffle text-success me-2"></i>
|
||||
Pseudo-random embedding
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-broadcast text-success me-2"></i>
|
||||
<strong>Channel keys</strong> for group isolation
|
||||
<span class="badge bg-info ms-1">v4.1</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -110,7 +110,7 @@ lsblk
|
||||
sudo ./rpi/pull-image.sh /dev/sdX stegasoo-rpi-4.1.5.img.zst
|
||||
```
|
||||
|
||||
The script automatically resizes rootfs to 16GB, disables auto-expand, and compresses.
|
||||
The script automatically resizes rootfs to 16GB (for smaller download), preserves auto-expand, and compresses.
|
||||
|
||||
## Step 10: Distribute
|
||||
|
||||
|
||||
@@ -204,8 +204,8 @@ sudo ./rpi/pull-image.sh /dev/sdX stegasoo-rpi-4.1.5.img.zst
|
||||
```
|
||||
|
||||
The `pull-image.sh` script automatically:
|
||||
- Resizes rootfs to exactly 16GB (consistent image size)
|
||||
- Disables Pi OS auto-expand
|
||||
- Resizes rootfs to exactly 16GB (for smaller download)
|
||||
- Preserves auto-expand (image fills SD card on first boot)
|
||||
- Compresses with zstd for fast decompression
|
||||
|
||||
### 6. Distribute
|
||||
|
||||
89
rpi/build-runtime-tarball.sh
Executable file
89
rpi/build-runtime-tarball.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build Stegasoo Pi Runtime Environment Tarball
|
||||
# Run this ON THE PI after a successful from-source build
|
||||
#
|
||||
# Creates: stegasoo-rpi-runtime-env-arm64.tar.zst (~50-60MB)
|
||||
# Contains: pyenv + Python 3.12 + venv with all dependencies
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/stegasoo}"
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-/tmp}"
|
||||
OUTPUT_FILE="$OUTPUT_DIR/stegasoo-rpi-runtime-env-arm64.tar.zst"
|
||||
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ Stegasoo Pi Runtime Tarball Builder ║${NC}"
|
||||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# Verify we're on ARM64
|
||||
ARCH=$(uname -m)
|
||||
if [[ "$ARCH" != "aarch64" ]]; then
|
||||
echo -e "${RED}Error: This script must be run on ARM64 (aarch64)${NC}"
|
||||
echo "Current architecture: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify pyenv exists
|
||||
if [[ ! -d "$HOME/.pyenv" ]]; then
|
||||
echo -e "${RED}Error: pyenv not found at ~/.pyenv${NC}"
|
||||
echo "Run a from-source build first: ./rpi/setup.sh --no-prebuilt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify venv exists
|
||||
if [[ ! -d "$INSTALL_DIR/venv" ]]; then
|
||||
echo -e "${RED}Error: venv not found at $INSTALL_DIR/venv${NC}"
|
||||
echo "Run a from-source build first: ./rpi/setup.sh --no-prebuilt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Clean caches from venv
|
||||
echo -e "${GREEN}[1/4]${NC} Cleaning caches from venv..."
|
||||
VENV_SIZE_BEFORE=$(du -sh "$INSTALL_DIR/venv" | cut -f1)
|
||||
find "$INSTALL_DIR/venv/" -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$INSTALL_DIR/venv/" -type d -name 'tests' -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$INSTALL_DIR/venv/" -type d -name 'test' -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$INSTALL_DIR/venv/" -type f -name '*.pyc' -delete 2>/dev/null || true
|
||||
VENV_SIZE_AFTER=$(du -sh "$INSTALL_DIR/venv" | cut -f1)
|
||||
echo " venv: $VENV_SIZE_BEFORE → $VENV_SIZE_AFTER"
|
||||
|
||||
# Step 2: Create venv tarball
|
||||
echo -e "${GREEN}[2/4]${NC} Creating venv tarball..."
|
||||
cd "$INSTALL_DIR"
|
||||
tar -cf - venv/ | zstd -19 -T0 > "$HOME/stegasoo-venv.tar.zst"
|
||||
VENV_TAR_SIZE=$(ls -lh "$HOME/stegasoo-venv.tar.zst" | awk '{print $5}')
|
||||
echo " Created: ~/stegasoo-venv.tar.zst ($VENV_TAR_SIZE)"
|
||||
|
||||
# Step 3: Create combined tarball
|
||||
echo -e "${GREEN}[3/4]${NC} Creating combined runtime tarball..."
|
||||
cd "$HOME"
|
||||
tar -cf - .pyenv stegasoo-venv.tar.zst | zstd -19 -T0 > "$OUTPUT_FILE"
|
||||
|
||||
# Cleanup intermediate file
|
||||
rm "$HOME/stegasoo-venv.tar.zst"
|
||||
|
||||
# Step 4: Summary
|
||||
FINAL_SIZE=$(ls -lh "$OUTPUT_FILE" | awk '{print $5}')
|
||||
echo -e "${GREEN}[4/4]${NC} Done!"
|
||||
echo ""
|
||||
echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e " Output: ${YELLOW}$OUTPUT_FILE${NC}"
|
||||
echo -e " Size: ${YELLOW}$FINAL_SIZE${NC}"
|
||||
echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo "To pull to your host machine:"
|
||||
echo " scp $(whoami)@$(hostname).local:$OUTPUT_FILE ./"
|
||||
echo ""
|
||||
echo "To use in setup.sh, copy to:"
|
||||
echo " rpi/stegasoo-rpi-runtime-env-arm64.tar.zst"
|
||||
echo ""
|
||||
echo "Or upload to GitHub releases for automatic download."
|
||||
@@ -53,6 +53,48 @@ echo ""
|
||||
|
||||
gum confirm "Ready to begin setup?" || exit 0
|
||||
|
||||
# =============================================================================
|
||||
# Step 0: Expand Filesystem
|
||||
# =============================================================================
|
||||
|
||||
clear
|
||||
gum style \
|
||||
--foreground 212 --bold \
|
||||
"Step 0: Expand Filesystem"
|
||||
echo ""
|
||||
|
||||
# Get current and total size
|
||||
ROOT_DEV=$(findmnt -n -o SOURCE /)
|
||||
CURRENT_SIZE=$(df -h / | awk 'NR==2 {print $2}')
|
||||
TOTAL_SIZE=$(lsblk -b -d -o SIZE $(echo "$ROOT_DEV" | sed 's/[0-9]*$//') 2>/dev/null | tail -1 | awk '{printf "%.0fG", $1/1024/1024/1024}')
|
||||
|
||||
gum style --foreground 245 "\
|
||||
The filesystem is currently $CURRENT_SIZE but your SD card may be larger.
|
||||
Expanding will use all available space on the SD card."
|
||||
echo ""
|
||||
gum style --foreground 245 "Current: $CURRENT_SIZE"
|
||||
echo ""
|
||||
|
||||
if gum confirm "Expand filesystem to fill SD card?" --default=true; then
|
||||
# Get the disk device (strip partition number) and partition number
|
||||
DISK_DEV=$(echo "$ROOT_DEV" | sed 's/p\?[0-9]*$//')
|
||||
PART_NUM=$(echo "$ROOT_DEV" | grep -o '[0-9]*$')
|
||||
|
||||
echo ""
|
||||
gum style --foreground 245 "Expanding partition..."
|
||||
sudo growpart "$DISK_DEV" "$PART_NUM" 2>&1 || true
|
||||
|
||||
gum style --foreground 245 "Expanding filesystem..."
|
||||
sudo resize2fs "$ROOT_DEV" 2>&1
|
||||
|
||||
NEW_SIZE=$(df -h / | awk 'NR==2 {print $2}')
|
||||
echo ""
|
||||
gum style --foreground 82 "✓ Expanded to: $NEW_SIZE"
|
||||
else
|
||||
gum style --foreground 214 "→ Skipped (run 'sudo growpart /dev/sdX 2 && sudo resize2fs /dev/sdX2' later)"
|
||||
fi
|
||||
sleep 1
|
||||
|
||||
# =============================================================================
|
||||
# Configuration Variables
|
||||
# =============================================================================
|
||||
@@ -137,7 +179,13 @@ This is useful if you want to share encoded images only with
|
||||
specific people (family, team, etc)."
|
||||
echo ""
|
||||
|
||||
if gum confirm "Generate a private channel key?" --default=false; then
|
||||
CHANNEL_CHOICE=$(gum choose \
|
||||
"Skip (public mode)" \
|
||||
"Generate new key" \
|
||||
"Enter existing key")
|
||||
|
||||
case "$CHANNEL_CHOICE" in
|
||||
"Generate new key")
|
||||
echo ""
|
||||
# Generate key to temp file (gum spin doesn't capture stdout well)
|
||||
KEY_FILE=$(mktemp)
|
||||
@@ -179,11 +227,53 @@ if gum confirm "Generate a private channel key?" --default=false; then
|
||||
echo ""
|
||||
gum confirm "Continue" --default=true --affirmative="OK" --negative=""
|
||||
fi
|
||||
else
|
||||
gum style --foreground 214 "→ Using public mode"
|
||||
sleep 0.5
|
||||
;;
|
||||
|
||||
"Enter existing key")
|
||||
echo ""
|
||||
gum style --foreground 245 "Enter the channel key from your team/deployment."
|
||||
gum style --foreground 245 "Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
ENTERED_KEY=$(gum input --placeholder "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456" --width 50)
|
||||
|
||||
if [ -z "$ENTERED_KEY" ]; then
|
||||
gum style --foreground 214 "→ Cancelled, using public mode"
|
||||
CHANNEL_KEY=""
|
||||
break
|
||||
fi
|
||||
|
||||
# Validate the key using Python
|
||||
VENV_PYTHON="$INSTALL_DIR/venv/bin/python"
|
||||
if "$VENV_PYTHON" -c "from stegasoo.channel import validate_channel_key, format_channel_key; k='$ENTERED_KEY'; exit(0 if validate_channel_key(k) else 1)" 2>/dev/null; then
|
||||
# Get formatted key
|
||||
CHANNEL_KEY=$("$VENV_PYTHON" -c "from stegasoo.channel import format_channel_key; print(format_channel_key('$ENTERED_KEY'))" 2>/dev/null)
|
||||
echo ""
|
||||
gum style --foreground 82 "✓ Channel key accepted!"
|
||||
gum style --foreground 245 "Key: $CHANNEL_KEY"
|
||||
break
|
||||
else
|
||||
echo ""
|
||||
gum style --foreground 196 "Invalid key format. Please check and try again."
|
||||
gum style --foreground 245 "Expected: 32 alphanumeric characters (with or without dashes)"
|
||||
echo ""
|
||||
if ! gum confirm "Try again?" --default=true; then
|
||||
gum style --foreground 214 "→ Using public mode"
|
||||
CHANNEL_KEY=""
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
;;
|
||||
|
||||
*)
|
||||
gum style --foreground 214 "→ Using public mode"
|
||||
CHANNEL_KEY=""
|
||||
sleep 0.5
|
||||
;;
|
||||
esac
|
||||
|
||||
# =============================================================================
|
||||
# Step 4: Overclock Configuration
|
||||
# =============================================================================
|
||||
|
||||
@@ -249,16 +249,9 @@ if [ -n "$MOUNTED" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Ask about wiping
|
||||
# Ask about wiping (defer actual wipe until after final confirmation)
|
||||
echo
|
||||
read -p "Wipe partition table first? (recommended if having issues) [y/N] " wipe_confirm
|
||||
if [[ "$wipe_confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Wiping partition table..."
|
||||
sudo wipefs -a "$SELECTED"
|
||||
sudo dd if=/dev/zero of="$SELECTED" bs=1M count=10 status=none
|
||||
sync
|
||||
echo " Wiped clean"
|
||||
fi
|
||||
|
||||
# Final confirmation
|
||||
echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||
@@ -272,73 +265,65 @@ if [[ ! $REPLY == "yes" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Now wipe if requested
|
||||
if [[ "$wipe_confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Wiping partition table..."
|
||||
sudo wipefs -af "$SELECTED" 2>/dev/null || true
|
||||
sync
|
||||
echo " Wiped"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Flashing image to $SELECTED...${NC}"
|
||||
echo ""
|
||||
|
||||
# Try rpi-imager first (faster, native support for compressed images)
|
||||
if command -v rpi-imager &> /dev/null; then
|
||||
echo -e "${YELLOW}Using rpi-imager...${NC}"
|
||||
if rpi-imager --cli --disable-verify "$IMAGE" "$SELECTED"; then
|
||||
# rpi-imager succeeded
|
||||
:
|
||||
else
|
||||
echo -e "${YELLOW}rpi-imager failed, falling back to dd...${NC}"
|
||||
# Fall through to dd
|
||||
USE_DD=true
|
||||
fi
|
||||
else
|
||||
USE_DD=true
|
||||
fi
|
||||
|
||||
# Fallback to dd
|
||||
if [ "$USE_DD" = true ]; then
|
||||
if [ "$HAS_PV" = true ]; then
|
||||
echo -e "${YELLOW}Using dd with progress...${NC}"
|
||||
# Flash with dd (status=progress shows actual write progress)
|
||||
echo -e "${YELLOW}Flashing (this may take several minutes for SD cards)...${NC}"
|
||||
if [ "$COMPRESSED" = true ]; then
|
||||
case "$COMP_TYPE" in
|
||||
xz) pv "$IMAGE" | xzcat | dd of="$SELECTED" bs=4M conv=fsync 2>/dev/null ;;
|
||||
zst) pv "$IMAGE" | zstdcat | dd of="$SELECTED" bs=4M conv=fsync 2>/dev/null ;;
|
||||
gz) pv "$IMAGE" | zcat | dd of="$SELECTED" bs=4M conv=fsync 2>/dev/null ;;
|
||||
xz) xzcat "$IMAGE" | sudo dd of="$SELECTED" bs=1M status=progress ;;
|
||||
zst) zstdcat "$IMAGE" | sudo dd of="$SELECTED" bs=1M status=progress ;;
|
||||
gz) zcat "$IMAGE" | sudo dd of="$SELECTED" bs=1M status=progress ;;
|
||||
esac
|
||||
else
|
||||
pv "$IMAGE" | dd of="$SELECTED" bs=4M conv=fsync 2>/dev/null
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}Using dd (no progress - install pv for progress bar)...${NC}"
|
||||
if [ "$COMPRESSED" = true ]; then
|
||||
case "$COMP_TYPE" in
|
||||
xz) xzcat "$IMAGE" | dd of="$SELECTED" bs=4M conv=fsync status=progress ;;
|
||||
zst) zstdcat "$IMAGE" | dd of="$SELECTED" bs=4M conv=fsync status=progress ;;
|
||||
gz) zcat "$IMAGE" | dd of="$SELECTED" bs=4M conv=fsync status=progress ;;
|
||||
esac
|
||||
else
|
||||
dd if="$IMAGE" of="$SELECTED" bs=4M conv=fsync status=progress
|
||||
fi
|
||||
fi
|
||||
sudo dd if="$IMAGE" of="$SELECTED" bs=1M status=progress
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Syncing...${NC}"
|
||||
sync
|
||||
|
||||
# Inject WiFi config if config.json was loaded
|
||||
if [ "$HAS_CONFIG" = true ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}Configuring WiFi from config.json...${NC}"
|
||||
|
||||
# Wait for partitions to appear
|
||||
sleep 2
|
||||
partprobe "$SELECTED" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Determine boot partition
|
||||
# Determine partition names
|
||||
if [[ "$SELECTED" == *"nvme"* ]] || [[ "$SELECTED" == *"mmcblk"* ]]; then
|
||||
BOOT_PART="${SELECTED}p1"
|
||||
ROOT_PART="${SELECTED}p2"
|
||||
else
|
||||
BOOT_PART="${SELECTED}1"
|
||||
ROOT_PART="${SELECTED}2"
|
||||
fi
|
||||
|
||||
# Validate and repair filesystems
|
||||
echo ""
|
||||
echo -e "${YELLOW}Validating filesystems...${NC}"
|
||||
|
||||
echo " Checking boot partition ($BOOT_PART)..."
|
||||
sudo fsck.vfat -a "$BOOT_PART" 2>&1 | grep -v "^$" || true
|
||||
|
||||
echo " Checking root partition ($ROOT_PART)..."
|
||||
sudo e2fsck -f -y "$ROOT_PART" 2>&1 | tail -5 || true
|
||||
|
||||
echo -e "${GREEN} ✓ Filesystems validated${NC}"
|
||||
|
||||
# Inject WiFi config if config.json was loaded
|
||||
if [ "$HAS_CONFIG" = true ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}Configuring WiFi from config.json...${NC}"
|
||||
|
||||
if [ -b "$BOOT_PART" ]; then
|
||||
MOUNT_DIR=$(mktemp -d)
|
||||
if mount "$BOOT_PART" "$MOUNT_DIR" 2>/dev/null; then
|
||||
|
||||
@@ -123,25 +123,6 @@ else
|
||||
echo -e "${GREEN} Rootfs already ~16GB${NC}"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Disable auto-expand on first boot
|
||||
# ============================================================================
|
||||
echo
|
||||
echo -e "${YELLOW}Disabling auto-expand...${NC}"
|
||||
TEMP_ROOT=$(mktemp -d)
|
||||
mount "$ROOT_PART" "$TEMP_ROOT"
|
||||
|
||||
# Remove resize2fs_once service if it exists
|
||||
rm -f "$TEMP_ROOT/etc/init.d/resize2fs_once"
|
||||
rm -f "$TEMP_ROOT/etc/rc3.d/S01resize2fs_once"
|
||||
|
||||
# Disable the systemd resize service
|
||||
rm -f "$TEMP_ROOT/etc/systemd/system/multi-user.target.wants/rpi-resizerootfs.service"
|
||||
|
||||
umount "$TEMP_ROOT"
|
||||
rmdir "$TEMP_ROOT"
|
||||
echo -e "${GREEN} Auto-expand disabled${NC}"
|
||||
|
||||
# ============================================================================
|
||||
# Pull image
|
||||
# ============================================================================
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Stegasoo Pi Test Kickoff Script
|
||||
# Automates: flash -> wait for boot -> setup -> test
|
||||
# Stegasoo Remote Pi Build Script
|
||||
# Waits for Pi to be reachable, then sets up Stegasoo
|
||||
#
|
||||
# Usage: ./kickoff-pi-test.sh <image.img.zst> </dev/sdX>
|
||||
# Usage: ./remote-build-pi.sh [host] [user] [pass]
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Pi connection settings
|
||||
PI_HOST="stegasoo.local"
|
||||
PI_USER="admin"
|
||||
PI_PASS="stegasoo"
|
||||
# Pi connection settings (defaults)
|
||||
PI_HOST="${1:-stegasoo.local}"
|
||||
PI_USER="${2:-admin}"
|
||||
PI_PASS="${3:-stegasoo}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
@@ -26,10 +26,9 @@ NC='\033[0m'
|
||||
# Helper functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Wait for Pi to be reachable
|
||||
wait_for_pi() {
|
||||
local attempt=1
|
||||
ssh-keygen -R "$PI_HOST" 2>/dev/null
|
||||
ssh-keygen -R "$PI_HOST" 2>/dev/null || true
|
||||
|
||||
echo "Waiting for $PI_USER@$PI_HOST..."
|
||||
while ! sshpass -p "$PI_PASS" ssh -o ConnectTimeout=2 -o StrictHostKeyChecking=no -o BatchMode=no -o UserKnownHostsFile=/dev/null "$PI_USER@$PI_HOST" "exit" 2>/dev/null; do
|
||||
@@ -39,29 +38,25 @@ wait_for_pi() {
|
||||
done
|
||||
|
||||
printf "\r${GREEN}✓ Ready after %d attempts${NC}\n" "$attempt"
|
||||
printf '\a' # Terminal bell
|
||||
printf '\a'
|
||||
}
|
||||
|
||||
# Run command on Pi (non-interactive)
|
||||
run_on_pi() {
|
||||
sshpass -p "$PI_PASS" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$PI_USER@$PI_HOST" "$@"
|
||||
}
|
||||
|
||||
# Run command on Pi (interactive/PTY)
|
||||
run_on_pi_interactive() {
|
||||
sshpass -p "$PI_PASS" ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$PI_USER@$PI_HOST" "$@"
|
||||
}
|
||||
|
||||
# Copy file to Pi
|
||||
scp_to_pi() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
sshpass -p "$PI_PASS" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$src" "$PI_USER@$PI_HOST:$dst"
|
||||
}
|
||||
|
||||
# Interactive SSH session
|
||||
ssh_pi() {
|
||||
ssh-keygen -R "$PI_HOST" 2>/dev/null
|
||||
ssh-keygen -R "$PI_HOST" 2>/dev/null || true
|
||||
sshpass -p "$PI_PASS" ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$PI_USER@$PI_HOST" "$@"
|
||||
}
|
||||
|
||||
@@ -69,89 +64,45 @@ ssh_pi() {
|
||||
# Main
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Usage: $0 <image.img.zst> </dev/sdX>"
|
||||
echo ""
|
||||
echo "Example: $0 stegasoo-v4.1.img.zst /dev/sda"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE="$1"
|
||||
DEVICE="$2"
|
||||
|
||||
if [[ ! -f "$IMAGE" ]]; then
|
||||
echo -e "${RED}Error: Image file not found: $IMAGE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -b "$DEVICE" ]]; then
|
||||
echo -e "${RED}Error: Device not found: $DEVICE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║ Stegasoo Pi Test Kickoff ║${NC}"
|
||||
echo -e "${CYAN}║ Stegasoo Remote Pi Build ║${NC}"
|
||||
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e "Image: ${YELLOW}$IMAGE${NC}"
|
||||
echo -e "Device: ${YELLOW}$DEVICE${NC}"
|
||||
echo -e "Host: ${YELLOW}$PI_HOST${NC}"
|
||||
echo -e "User: ${YELLOW}$PI_USER${NC}"
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 1: Flash the image
|
||||
# Step 1: Wait for Pi to be ready
|
||||
# -----------------------------------------------------------------------------
|
||||
echo -e "${GREEN}[1/8]${NC} Flashing image..."
|
||||
echo ""
|
||||
|
||||
# Auto-answer: "yes" for confirm, "y" for wipe, "y" for resize
|
||||
printf 'yes\ny\ny\n' | "$SCRIPT_DIR/flash-stock-img.sh" "$IMAGE" "$DEVICE"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}[2/8]${NC} Flash complete! Waiting for SD card insertion..."
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 2: Wait for user to insert SD card
|
||||
# -----------------------------------------------------------------------------
|
||||
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} Insert SD card into Pi and power on${NC}"
|
||||
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
read -p "Press ENTER when Pi is booting..."
|
||||
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 3: Wait for Pi to be ready
|
||||
# -----------------------------------------------------------------------------
|
||||
echo -e "${GREEN}[3/8]${NC} Waiting for Pi to boot..."
|
||||
echo -e "${GREEN}[1/6]${NC} Waiting for Pi..."
|
||||
echo ""
|
||||
|
||||
wait_for_pi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 4: Pre-setup (install dependencies)
|
||||
# Step 2: Install dependencies
|
||||
# -----------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo -e "${GREEN}[4/8]${NC} Installing dependencies on Pi..."
|
||||
echo -e "${GREEN}[2/6]${NC} Installing dependencies on Pi..."
|
||||
echo ""
|
||||
|
||||
run_on_pi "sudo chown admin:admin /opt && sudo apt-get update && sudo apt-get install -y git zstd jq"
|
||||
run_on_pi "sudo chown admin:admin /opt && sudo apt-get update && sudo apt-get install -y git zstd jq ca-certificates && sudo update-ca-certificates"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 5: Clone repo
|
||||
# Step 3: Clone repo
|
||||
# -----------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo -e "${GREEN}[5/8]${NC} Cloning Stegasoo repo..."
|
||||
echo -e "${GREEN}[3/6]${NC} Cloning Stegasoo repo..."
|
||||
echo ""
|
||||
|
||||
run_on_pi "cd /opt && git clone -b 4.1 https://github.com/adlee-was-taken/stegasoo.git stegasoo"
|
||||
run_on_pi "cd /opt && rm -rf stegasoo && git clone https://github.com/adlee-was-taken/stegasoo.git stegasoo"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 6: Copy pre-built tarball
|
||||
# Step 4: Copy pre-built tarball
|
||||
# -----------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo -e "${GREEN}[6/8]${NC} Copying pre-built tarball to Pi..."
|
||||
echo -e "${GREEN}[4/6]${NC} Copying pre-built tarball to Pi..."
|
||||
echo ""
|
||||
|
||||
TARBALL="$SCRIPT_DIR/stegasoo-rpi-runtime-env-arm64.tar.zst"
|
||||
@@ -164,19 +115,19 @@ else
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 7: Run setup
|
||||
# Step 5: Run setup
|
||||
# -----------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo -e "${GREEN}[7/8]${NC} Running setup.sh on Pi..."
|
||||
echo -e "${GREEN}[5/6]${NC} Running setup.sh on Pi..."
|
||||
echo ""
|
||||
|
||||
run_on_pi_interactive "cd /opt/stegasoo && ./rpi/setup.sh"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 8: Test it works
|
||||
# Step 6: Test it works
|
||||
# -----------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo -e "${GREEN}[8/8]${NC} Testing Stegasoo..."
|
||||
echo -e "${GREEN}[6/6]${NC} Testing Stegasoo..."
|
||||
echo ""
|
||||
|
||||
run_on_pi "sudo systemctl start stegasoo && sleep 2 && curl -sk https://localhost:5000 | head -5"
|
||||
@@ -186,7 +137,7 @@ echo -e "${GREEN}═════════════════════
|
||||
echo -e "${GREEN} Build complete! Pi is ready for testing.${NC}"
|
||||
echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e "Access: ${YELLOW}https://stegasoo.local:5000${NC}"
|
||||
echo -e "Access: ${YELLOW}https://$PI_HOST:5000${NC}"
|
||||
echo ""
|
||||
read -p "Press ENTER to SSH into Pi for manual testing..."
|
||||
|
||||
57
rpi/setup.sh
57
rpi/setup.sh
@@ -184,6 +184,20 @@ else
|
||||
echo " gum already installed"
|
||||
fi
|
||||
|
||||
# Install mkcert for browser-trusted certificates (no warning screen!)
|
||||
echo " Installing mkcert for trusted HTTPS certificates..."
|
||||
if ! command -v mkcert &>/dev/null; then
|
||||
sudo apt-get install -y libnss3-tools
|
||||
# Download mkcert for ARM64
|
||||
sudo curl -sL "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-arm64" -o /usr/local/bin/mkcert
|
||||
sudo chmod +x /usr/local/bin/mkcert
|
||||
# Install local CA (makes certs trusted on this Pi)
|
||||
mkcert -install 2>/dev/null || true
|
||||
echo " mkcert installed"
|
||||
else
|
||||
echo " mkcert already installed"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[4/12]${NC} Cloning Stegasoo..."
|
||||
|
||||
# Clone Stegasoo first (needed to check for pre-built tarball)
|
||||
@@ -414,6 +428,14 @@ if [ -f "$INSTALL_DIR/rpi/skel/.bashrc" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install man page
|
||||
if [ -f "$INSTALL_DIR/docs/stegasoo.1" ]; then
|
||||
sudo mkdir -p /usr/local/share/man/man1
|
||||
sudo cp "$INSTALL_DIR/docs/stegasoo.1" /usr/local/share/man/man1/
|
||||
sudo mandb -q 2>/dev/null || true
|
||||
echo " Installed man page (man stegasoo)"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[12/12]${NC} Setting up login banner..."
|
||||
|
||||
# Create dynamic MOTD script
|
||||
@@ -543,9 +565,15 @@ echo ""
|
||||
read -p "Generate a private channel key? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# Generate channel key using the CLI
|
||||
CHANNEL_KEY=$($INSTALL_DIR/venv/bin/python -c "from stegasoo.channel import generate_channel_key; print(generate_channel_key())")
|
||||
# Generate channel key and save encrypted to config
|
||||
CHANNEL_KEY=$($INSTALL_DIR/venv/bin/python -c "
|
||||
from stegasoo.channel import generate_channel_key, set_channel_key
|
||||
key = generate_channel_key()
|
||||
set_channel_key(key, 'user') # Saves encrypted to ~/.stegasoo/channel.key
|
||||
print(key)
|
||||
")
|
||||
echo -e " ${GREEN}✓${NC} Channel key generated: ${YELLOW}$CHANNEL_KEY${NC}"
|
||||
echo -e " ${GREEN}✓${NC} Key saved (encrypted) to ~/.stegasoo/channel.key"
|
||||
echo ""
|
||||
echo -e " ${RED}IMPORTANT: Save this key!${NC} You'll need to share it with anyone"
|
||||
echo " who should be able to decode your images."
|
||||
@@ -593,7 +621,26 @@ if [ "$ENABLE_HTTPS" = "true" ]; then
|
||||
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
||||
PI_HOSTNAME=$(hostname)
|
||||
|
||||
# Generate cert with SANs for IP, hostname, and localhost
|
||||
# Try mkcert first (creates browser-trusted certs - no warning screen!)
|
||||
if command -v mkcert &> /dev/null; then
|
||||
echo " Using mkcert for browser-trusted certificates..."
|
||||
cd "$CERT_DIR"
|
||||
mkcert -key-file server.key -cert-file server.crt \
|
||||
"$PI_HOSTNAME" "$PI_HOSTNAME.local" localhost "$LOCAL_IP" 127.0.0.1 ::1
|
||||
|
||||
# Copy CA to web-accessible location for easy device setup
|
||||
CA_ROOT=$(mkcert -CAROOT)
|
||||
CA_DIR="$INSTALL_DIR/frontends/web/static/ca"
|
||||
mkdir -p "$CA_DIR"
|
||||
cp "$CA_ROOT/rootCA.pem" "$CA_DIR/"
|
||||
|
||||
echo -e " ${GREEN}✓${NC} Trusted certificates generated with mkcert"
|
||||
echo -e " ${CYAN}Tip:${NC} New devices can get the CA from: http://$PI_HOSTNAME.local/static/ca/rootCA.pem"
|
||||
else
|
||||
# Fallback to self-signed (shows browser warning)
|
||||
echo " Using self-signed certificate (browser will show warning)"
|
||||
echo " Tip: Install mkcert for trusted certs without warnings"
|
||||
|
||||
openssl req -x509 -newkey rsa:2048 \
|
||||
-keyout "$CERT_DIR/server.key" \
|
||||
-out "$CERT_DIR/server.crt" \
|
||||
@@ -602,10 +649,12 @@ if [ "$ENABLE_HTTPS" = "true" ]; then
|
||||
-addext "subjectAltName=DNS:$PI_HOSTNAME,DNS:$PI_HOSTNAME.local,DNS:localhost,IP:$LOCAL_IP,IP:127.0.0.1" \
|
||||
2>/dev/null
|
||||
|
||||
echo -e " ${GREEN}✓${NC} Self-signed certificates generated"
|
||||
fi
|
||||
|
||||
# Fix permissions
|
||||
chmod 600 "$CERT_DIR/server.key"
|
||||
chown -R "$USER:$USER" "$CERT_DIR"
|
||||
echo -e " ${GREEN}✓${NC} SSL certificates generated"
|
||||
fi
|
||||
|
||||
# Setup port 443 redirect if requested
|
||||
|
||||
87
scripts/build.sh
Executable file
87
scripts/build.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# Stegasoo Build Script
|
||||
# Usage: ./build.sh [base|fast|full|clean]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DOCKER_DIR="$PROJECT_DIR/docker"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Detect docker compose command
|
||||
if docker compose version &>/dev/null; then
|
||||
COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose &>/dev/null; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
echo -e "${RED}Error: docker compose not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we need sudo
|
||||
SUDO=""
|
||||
if ! docker ps &>/dev/null; then
|
||||
SUDO="sudo"
|
||||
fi
|
||||
|
||||
COMPOSE_FILE="$DOCKER_DIR/docker-compose.yml"
|
||||
|
||||
case "${1:-fast}" in
|
||||
base)
|
||||
echo -e "${YELLOW}Building base image (this takes 5-10 minutes)...${NC}"
|
||||
$SUDO docker build -f "$DOCKER_DIR/Dockerfile.base" -t stegasoo-base:latest .
|
||||
echo -e "${GREEN}Base image built! Future builds will be fast.${NC}"
|
||||
echo ""
|
||||
echo "Optional: Push to registry for team use:"
|
||||
echo " docker tag stegasoo-base:latest yourregistry/stegasoo-base:latest"
|
||||
echo " docker push yourregistry/stegasoo-base:latest"
|
||||
;;
|
||||
|
||||
fast)
|
||||
if ! $SUDO docker image inspect stegasoo-base:latest >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}Base image not found. Building it first (one-time)...${NC}"
|
||||
$0 base
|
||||
fi
|
||||
echo -e "${CYAN}Fast build using base image...${NC}"
|
||||
$SUDO $COMPOSE_CMD -f "$COMPOSE_FILE" build
|
||||
echo -e "${GREEN}Done! Start with: $COMPOSE_CMD -f docker/docker-compose.yml up -d${NC}"
|
||||
;;
|
||||
|
||||
full)
|
||||
echo -e "${YELLOW}Full build from scratch (slow)...${NC}"
|
||||
$SUDO $COMPOSE_CMD -f "$COMPOSE_FILE" build --no-cache
|
||||
echo -e "${GREEN}Done! Start with: $COMPOSE_CMD -f docker/docker-compose.yml up -d${NC}"
|
||||
;;
|
||||
|
||||
clean)
|
||||
echo -e "${YELLOW}Cleaning up...${NC}"
|
||||
$SUDO $COMPOSE_CMD -f "$COMPOSE_FILE" down --rmi local -v 2>/dev/null || true
|
||||
$SUDO docker rmi stegasoo-base:latest 2>/dev/null || true
|
||||
echo -e "${GREEN}Cleaned!${NC}"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo -e "${CYAN}Stegasoo Build Script${NC}"
|
||||
echo ""
|
||||
echo "Usage: $0 [command]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " base Build the base image (one-time, 5-10 min)"
|
||||
echo " fast Fast build using base image (default, ~10 sec)"
|
||||
echo " full Full rebuild from scratch (slow, no base needed)"
|
||||
echo " clean Remove all images and volumes"
|
||||
echo ""
|
||||
echo "Typical workflow:"
|
||||
echo " 1. First time: $0 base"
|
||||
echo " 2. Daily dev: $0 fast"
|
||||
echo " 3. Deps change: $0 base"
|
||||
;;
|
||||
esac
|
||||
93
scripts/screenshots.sh
Executable file
93
scripts/screenshots.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# Capture Web UI screenshots for documentation
|
||||
# Requires: chromium, imagemagick
|
||||
# Usage: ./scripts/screenshots.sh [base_url]
|
||||
#
|
||||
# Modes:
|
||||
# Default (auth disabled): Captures main UI pages
|
||||
# With auth: Also captures login/setup/account pages
|
||||
#
|
||||
# Start server with: STEGASOO_AUTH_ENABLED=false python frontends/web/app.py
|
||||
|
||||
set -e
|
||||
|
||||
BASE_URL="${1:-http://localhost:5000}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
OUTPUT_DIR="$PROJECT_DIR/data"
|
||||
WINDOW_SIZE="1280,900"
|
||||
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ Stegasoo Screenshot Capture ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Base URL: $BASE_URL"
|
||||
echo "Output: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Check dependencies
|
||||
for cmd in chromium magick curl; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
echo "Error: $cmd not found"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if server is running
|
||||
if ! curl -s "$BASE_URL" > /dev/null 2>&1; then
|
||||
echo "Error: Server not responding at $BASE_URL"
|
||||
echo "Start with: STEGASOO_AUTH_ENABLED=false python frontends/web/app.py"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Capture a single screenshot
|
||||
capture() {
|
||||
local name="$1"
|
||||
local route="$2"
|
||||
local url="$BASE_URL$route"
|
||||
|
||||
printf " %-20s <- %s\n" "$name" "$route"
|
||||
chromium --headless --screenshot="$OUTPUT_DIR/$name.png" \
|
||||
--window-size="$WINDOW_SIZE" --hide-scrollbars \
|
||||
--disable-gpu --no-sandbox \
|
||||
"$url" 2>/dev/null
|
||||
}
|
||||
|
||||
echo "Capturing main pages..."
|
||||
echo ""
|
||||
|
||||
# Core pages (always capture)
|
||||
capture "WebUI" "/"
|
||||
capture "WebUI_Encode" "/encode"
|
||||
capture "WebUI_Decode" "/decode"
|
||||
capture "WebUI_Generate" "/generate"
|
||||
capture "WebUI_Tools" "/tools"
|
||||
capture "WebUI_About" "/about"
|
||||
|
||||
echo ""
|
||||
echo "Capturing auth pages..."
|
||||
echo ""
|
||||
|
||||
# Auth pages (may redirect if auth disabled, that's OK)
|
||||
capture "WebUI_Login" "/login"
|
||||
capture "WebUI_Setup" "/setup"
|
||||
capture "WebUI_Account" "/account"
|
||||
capture "WebUI_Recover" "/recover"
|
||||
|
||||
echo ""
|
||||
echo "Converting to webp..."
|
||||
echo ""
|
||||
|
||||
for png in "$OUTPUT_DIR"/WebUI*.png; do
|
||||
[ -f "$png" ] || continue
|
||||
name=$(basename "$png" .png)
|
||||
printf " %-20s -> %s.webp\n" "$name.png" "$name"
|
||||
magick "$png" -quality 85 "$OUTPUT_DIR/$name.webp"
|
||||
rm -f "$png"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done! Screenshots:"
|
||||
echo ""
|
||||
ls -lh "$OUTPUT_DIR"/WebUI*.webp 2>/dev/null | awk '{print " " $NF " (" $5 ")"}'
|
||||
echo ""
|
||||
149
scripts/setup-trusted-certs.sh
Executable file
149
scripts/setup-trusted-certs.sh
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Setup trusted HTTPS certificates for Stegasoo
|
||||
# Uses mkcert to create browser-trusted certs (no warning screens!)
|
||||
#
|
||||
# Usage: ./setup-trusted-certs.sh [hostname]
|
||||
#
|
||||
# This script:
|
||||
# 1. Installs mkcert if needed
|
||||
# 2. Creates a local CA (one-time)
|
||||
# 3. Generates certs for your hostname
|
||||
# 4. Shows how to trust the CA on other devices
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
HOSTNAME="${1:-stegasoo.local}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$SCRIPT_DIR/.."
|
||||
CERT_DIR="$PROJECT_ROOT/frontends/web/certs"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║ Stegasoo Trusted Certificate Setup ║${NC}"
|
||||
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# Check/install mkcert
|
||||
install_mkcert() {
|
||||
if command -v mkcert &> /dev/null; then
|
||||
echo -e "${GREEN}✓${NC} mkcert already installed"
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Installing mkcert...${NC}"
|
||||
|
||||
# Detect OS and install
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
if command -v brew &> /dev/null; then
|
||||
brew install mkcert
|
||||
else
|
||||
echo -e "${RED}Please install Homebrew first: https://brew.sh${NC}"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ -f /etc/debian_version ]]; then
|
||||
# Debian/Ubuntu/Raspberry Pi OS
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libnss3-tools
|
||||
|
||||
# Download mkcert binary
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
if [[ "$ARCH" == "arm64" ]] || [[ "$ARCH" == "aarch64" ]]; then
|
||||
MKCERT_URL="https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-linux-arm64"
|
||||
else
|
||||
MKCERT_URL="https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-linux-amd64"
|
||||
fi
|
||||
|
||||
sudo curl -L "$MKCERT_URL" -o /usr/local/bin/mkcert
|
||||
sudo chmod +x /usr/local/bin/mkcert
|
||||
elif [[ -f /etc/arch-release ]]; then
|
||||
# Arch Linux
|
||||
sudo pacman -S mkcert
|
||||
else
|
||||
echo -e "${RED}Unsupported OS. Please install mkcert manually:${NC}"
|
||||
echo " https://github.com/FiloSottile/mkcert#installation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓${NC} mkcert installed"
|
||||
}
|
||||
|
||||
# Install local CA
|
||||
setup_ca() {
|
||||
echo ""
|
||||
echo -e "${CYAN}Setting up local Certificate Authority...${NC}"
|
||||
|
||||
if mkcert -install 2>/dev/null; then
|
||||
echo -e "${GREEN}✓${NC} Local CA installed in system trust store"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} Could not auto-install CA (may need manual browser import)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate certificates
|
||||
generate_certs() {
|
||||
echo ""
|
||||
echo -e "${CYAN}Generating trusted certificate for: ${YELLOW}$HOSTNAME${NC}"
|
||||
|
||||
mkdir -p "$CERT_DIR"
|
||||
cd "$CERT_DIR"
|
||||
|
||||
# Generate cert for hostname + common local names
|
||||
mkcert -key-file key.pem -cert-file cert.pem \
|
||||
"$HOSTNAME" \
|
||||
localhost \
|
||||
127.0.0.1 \
|
||||
::1
|
||||
|
||||
echo -e "${GREEN}✓${NC} Certificates generated in: $CERT_DIR"
|
||||
}
|
||||
|
||||
# Show CA location for other devices
|
||||
show_ca_info() {
|
||||
CA_ROOT=$(mkcert -CAROOT)
|
||||
CA_FILE="$CA_ROOT/rootCA.pem"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Setup Complete!${NC}"
|
||||
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo "Your certificates are ready. Browsers on THIS machine will trust them."
|
||||
echo ""
|
||||
echo -e "${YELLOW}To trust on OTHER devices (phones, tablets, other computers):${NC}"
|
||||
echo ""
|
||||
echo " 1. Copy the CA certificate to that device:"
|
||||
echo -e " ${CYAN}$CA_FILE${NC}"
|
||||
echo ""
|
||||
echo " 2. Import it as a trusted CA:"
|
||||
echo " - iOS: AirDrop/email the file, Settings > Profile Downloaded > Install"
|
||||
echo " - Android: Settings > Security > Install from storage"
|
||||
echo " - Windows: Double-click > Install > Trusted Root CAs"
|
||||
echo " - macOS: Double-click > Keychain Access > Trust Always"
|
||||
echo " - Linux: Copy to /usr/local/share/ca-certificates/ && update-ca-certificates"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Quick copy command:${NC}"
|
||||
echo " scp $CA_FILE user@device:/path/"
|
||||
echo ""
|
||||
|
||||
# Offer to serve CA file via HTTP for easy phone download
|
||||
echo -e "${YELLOW}Or serve the CA for easy phone download:${NC}"
|
||||
echo " python3 -m http.server 8080 -d $CA_ROOT"
|
||||
echo " Then visit: http://$(hostname -I | awk '{print $1}'):8080/rootCA.pem"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main
|
||||
install_mkcert
|
||||
setup_ca
|
||||
generate_certs
|
||||
show_ca_info
|
||||
333
scripts/smoke-test.sh
Executable file
333
scripts/smoke-test.sh
Executable file
@@ -0,0 +1,333 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Stegasoo Smoke Test
|
||||
# Tests all core functionality against a running instance (Pi, Docker, or dev)
|
||||
#
|
||||
# Usage: ./smoke-test.sh [host] [port] [user] [pass]
|
||||
#
|
||||
# Examples:
|
||||
# ./smoke-test.sh # Pi default (stegasoo.local:443)
|
||||
# ./smoke-test.sh localhost 5000 # Docker default
|
||||
# ./smoke-test.sh 192.168.1.100 5000 # Custom host
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
HOST="${1:-stegasoo.local}"
|
||||
PORT="${2:-443}"
|
||||
USER="${3:-admin}"
|
||||
PASS="${4:-stegasoo}"
|
||||
|
||||
# Build URL (don't include :443 since it's default for https)
|
||||
if [ "$PORT" = "443" ]; then
|
||||
BASE_URL="https://$HOST"
|
||||
else
|
||||
BASE_URL="https://$HOST:$PORT"
|
||||
fi
|
||||
COOKIE_JAR="/tmp/stegasoo_smoke_cookies.txt"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEST_DATA="$SCRIPT_DIR/../test_data"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
log_test() {
|
||||
echo -e "${CYAN}[TEST]${NC} $1"
|
||||
}
|
||||
|
||||
log_pass() {
|
||||
echo -e "${GREEN}[PASS]${NC} $1"
|
||||
PASSED=$((PASSED + 1))
|
||||
}
|
||||
|
||||
log_fail() {
|
||||
echo -e "${RED}[FAIL]${NC} $1"
|
||||
FAILED=$((FAILED + 1))
|
||||
}
|
||||
|
||||
curl_get() {
|
||||
curl -sk "$BASE_URL$1" -b "$COOKIE_JAR" -c "$COOKIE_JAR" "${@:2}"
|
||||
}
|
||||
|
||||
curl_post() {
|
||||
curl -sk -X POST "$BASE_URL$1" -b "$COOKIE_JAR" -c "$COOKIE_JAR" "${@:2}"
|
||||
}
|
||||
|
||||
wait_for_job() {
|
||||
local endpoint="$1"
|
||||
local job_id="$2"
|
||||
local max_polls="${3:-30}"
|
||||
|
||||
for i in $(seq 1 $max_polls); do
|
||||
sleep 1
|
||||
result=$(curl_get "$endpoint/$job_id")
|
||||
if echo "$result" | grep -q '"status":\s*"complete"'; then
|
||||
echo "$result"
|
||||
return 0
|
||||
fi
|
||||
if echo "$result" | grep -q '"status":\s*"error"'; then
|
||||
echo "$result"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
echo '{"status":"timeout"}'
|
||||
return 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Tests
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
test_connectivity() {
|
||||
log_test "Connectivity to $BASE_URL"
|
||||
if curl -sk --connect-timeout 5 "$BASE_URL" -o /dev/null; then
|
||||
log_pass "Server reachable"
|
||||
else
|
||||
log_fail "Cannot reach server"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_setup_or_login() {
|
||||
log_test "Setup/Login"
|
||||
|
||||
# Check if setup needed
|
||||
response=$(curl_get "/" -L -o /dev/null -w "%{url_effective}")
|
||||
|
||||
if echo "$response" | grep -q "/setup"; then
|
||||
log_test "Completing first-time setup..."
|
||||
curl_post "/setup" \
|
||||
-d "username=$USER" \
|
||||
-d "password=$PASS" \
|
||||
-d "password_confirm=$PASS" \
|
||||
-L -o /dev/null
|
||||
fi
|
||||
|
||||
# Login
|
||||
curl_get "/login" -o /dev/null # Get session
|
||||
curl_post "/login" \
|
||||
-d "username=$USER" \
|
||||
-d "password=$PASS" \
|
||||
-L -o /dev/null
|
||||
|
||||
# Verify logged in
|
||||
code=$(curl_get "/encode" -o /dev/null -w "%{http_code}")
|
||||
if [ "$code" = "200" ]; then
|
||||
log_pass "Authenticated successfully"
|
||||
else
|
||||
log_fail "Authentication failed (got $code)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_pages() {
|
||||
log_test "Page accessibility"
|
||||
|
||||
local pages="encode decode generate tools about"
|
||||
local all_pass=true
|
||||
|
||||
for page in $pages; do
|
||||
code=$(curl_get "/$page" -o /dev/null -w "%{http_code}")
|
||||
if [ "$code" = "200" ]; then
|
||||
echo -e " ${GREEN}✓${NC} /$page"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} /$page ($code)"
|
||||
all_pass=false
|
||||
fi
|
||||
done
|
||||
|
||||
if $all_pass; then
|
||||
log_pass "All pages accessible"
|
||||
else
|
||||
log_fail "Some pages inaccessible"
|
||||
fi
|
||||
}
|
||||
|
||||
test_encode_decode_dct() {
|
||||
log_test "DCT Encode/Decode round trip"
|
||||
|
||||
local message="DCT smoke test $(date +%s)"
|
||||
|
||||
# Encode
|
||||
response=$(curl_post "/encode" \
|
||||
-F "reference_photo=@$TEST_DATA/ref.jpg" \
|
||||
-F "carrier=@$TEST_DATA/carrier.jpg" \
|
||||
-F "message=$message" \
|
||||
-F "passphrase=tower booty sunny windy" \
|
||||
-F "pin=727643678" \
|
||||
-F "embed_mode=dct" \
|
||||
-F "channel_key=auto" \
|
||||
-F "async=true")
|
||||
|
||||
job_id=$(echo "$response" | grep -oP '"job_id":\s*"[^"]+"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$job_id" ]; then
|
||||
log_fail "DCT encode - no job ID returned"
|
||||
return
|
||||
fi
|
||||
|
||||
# Wait for encode
|
||||
result=$(wait_for_job "/encode/status" "$job_id" 15)
|
||||
if ! echo "$result" | grep -q '"status":\s*"complete"'; then
|
||||
log_fail "DCT encode timeout or error"
|
||||
return
|
||||
fi
|
||||
|
||||
file_id=$(echo "$result" | grep -oP '"file_id":\s*"[^"]+"' | cut -d'"' -f4)
|
||||
curl_get "/encode/download/$file_id" -o /tmp/stego_dct_test.jpg
|
||||
|
||||
echo -e " ${GREEN}✓${NC} Encoded $(ls -lh /tmp/stego_dct_test.jpg | awk '{print $5}')"
|
||||
|
||||
# Decode
|
||||
response=$(curl_post "/decode" \
|
||||
-F "reference_photo=@$TEST_DATA/ref.jpg" \
|
||||
-F "stego_image=@/tmp/stego_dct_test.jpg" \
|
||||
-F "passphrase=tower booty sunny windy" \
|
||||
-F "pin=727643678" \
|
||||
-F "embed_mode=auto" \
|
||||
-F "channel_key=auto" \
|
||||
-F "async=true")
|
||||
|
||||
job_id=$(echo "$response" | grep -oP '"job_id":\s*"[^"]+"' | cut -d'"' -f4)
|
||||
|
||||
# Wait for decode (DCT is slower on Pi)
|
||||
result=$(wait_for_job "/decode/status" "$job_id" 60)
|
||||
|
||||
if echo "$result" | grep -q "$message"; then
|
||||
log_pass "DCT round trip - message verified"
|
||||
else
|
||||
log_fail "DCT decode - message mismatch"
|
||||
echo " Expected: $message"
|
||||
echo " Got: $result"
|
||||
fi
|
||||
}
|
||||
|
||||
test_encode_decode_lsb() {
|
||||
log_test "LSB Encode/Decode round trip"
|
||||
|
||||
local message="LSB smoke test $(date +%s)"
|
||||
|
||||
# Encode
|
||||
response=$(curl_post "/encode" \
|
||||
-F "reference_photo=@$TEST_DATA/ref.jpg" \
|
||||
-F "carrier=@$TEST_DATA/carrier.jpg" \
|
||||
-F "message=$message" \
|
||||
-F "passphrase=tower booty sunny windy" \
|
||||
-F "pin=727643678" \
|
||||
-F "embed_mode=lsb" \
|
||||
-F "channel_key=auto" \
|
||||
-F "async=true")
|
||||
|
||||
job_id=$(echo "$response" | grep -oP '"job_id":\s*"[^"]+"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$job_id" ]; then
|
||||
log_fail "LSB encode - no job ID returned"
|
||||
return
|
||||
fi
|
||||
|
||||
result=$(wait_for_job "/encode/status" "$job_id" 10)
|
||||
if ! echo "$result" | grep -q '"status":\s*"complete"'; then
|
||||
log_fail "LSB encode timeout or error"
|
||||
return
|
||||
fi
|
||||
|
||||
file_id=$(echo "$result" | grep -oP '"file_id":\s*"[^"]+"' | cut -d'"' -f4)
|
||||
curl_get "/encode/download/$file_id" -o /tmp/stego_lsb_test.png
|
||||
|
||||
echo -e " ${GREEN}✓${NC} Encoded $(ls -lh /tmp/stego_lsb_test.png | awk '{print $5}')"
|
||||
|
||||
# Decode
|
||||
response=$(curl_post "/decode" \
|
||||
-F "reference_photo=@$TEST_DATA/ref.jpg" \
|
||||
-F "stego_image=@/tmp/stego_lsb_test.png" \
|
||||
-F "passphrase=tower booty sunny windy" \
|
||||
-F "pin=727643678" \
|
||||
-F "embed_mode=lsb" \
|
||||
-F "channel_key=auto" \
|
||||
-F "async=true")
|
||||
|
||||
job_id=$(echo "$response" | grep -oP '"job_id":\s*"[^"]+"' | cut -d'"' -f4)
|
||||
result=$(wait_for_job "/decode/status" "$job_id" 15)
|
||||
|
||||
if echo "$result" | grep -q "$message"; then
|
||||
log_pass "LSB round trip - message verified"
|
||||
else
|
||||
log_fail "LSB decode - message mismatch"
|
||||
fi
|
||||
}
|
||||
|
||||
test_tools() {
|
||||
log_test "Tools endpoints"
|
||||
|
||||
# Capacity check
|
||||
response=$(curl_post "/api/tools/capacity" \
|
||||
-F "image=@$TEST_DATA/carrier.jpg" \
|
||||
-w "%{http_code}" -o /tmp/capacity_result.json)
|
||||
|
||||
if [ "$response" = "200" ]; then
|
||||
echo -e " ${GREEN}✓${NC} Capacity check"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Capacity check ($response)"
|
||||
fi
|
||||
|
||||
# EXIF read
|
||||
response=$(curl_post "/api/tools/exif" \
|
||||
-F "image=@$TEST_DATA/carrier.jpg" \
|
||||
-w "%{http_code}" -o /tmp/exif_result.json)
|
||||
|
||||
if [ "$response" = "200" ]; then
|
||||
echo -e " ${GREEN}✓${NC} EXIF read"
|
||||
log_pass "Tools API works"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} EXIF read ($response)"
|
||||
log_fail "Tools API failed"
|
||||
fi
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║ Stegasoo Smoke Test ║${NC}"
|
||||
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e "Target: ${YELLOW}$BASE_URL${NC}"
|
||||
echo -e "User: ${YELLOW}$USER${NC}"
|
||||
echo ""
|
||||
|
||||
# Clean up
|
||||
rm -f "$COOKIE_JAR" /tmp/stego_*_test.* /tmp/exif_stripped.jpg
|
||||
|
||||
# Run tests
|
||||
test_connectivity
|
||||
test_setup_or_login
|
||||
test_pages
|
||||
test_encode_decode_lsb
|
||||
test_encode_decode_dct
|
||||
test_tools
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "Results: ${GREEN}$PASSED passed${NC}, ${RED}$FAILED failed${NC}"
|
||||
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
# Clean up
|
||||
rm -f "$COOKIE_JAR"
|
||||
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -7,7 +7,7 @@ Changes in v4.0.0:
|
||||
- encode() and decode() now accept channel_key parameter
|
||||
"""
|
||||
|
||||
__version__ = "4.1.3"
|
||||
__version__ = "4.1.7"
|
||||
|
||||
# Core functionality
|
||||
# Channel key management (v4.0.0)
|
||||
|
||||
@@ -47,6 +47,80 @@ CONFIG_LOCATIONS = [
|
||||
Path.home() / ".stegasoo" / "channel.key", # User config
|
||||
]
|
||||
|
||||
# Encrypted config marker
|
||||
ENCRYPTED_PREFIX = "ENC:"
|
||||
|
||||
|
||||
def _get_machine_key() -> bytes:
|
||||
"""
|
||||
Get a machine-specific key for encrypting stored channel keys.
|
||||
|
||||
Uses /etc/machine-id on Linux, falls back to hostname hash.
|
||||
This ties the encrypted key to this specific machine.
|
||||
"""
|
||||
machine_id = None
|
||||
|
||||
# Try Linux machine-id
|
||||
try:
|
||||
machine_id = Path("/etc/machine-id").read_text().strip()
|
||||
except (OSError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Fallback to hostname
|
||||
if not machine_id:
|
||||
import socket
|
||||
machine_id = socket.gethostname()
|
||||
|
||||
# Hash to get consistent 32 bytes
|
||||
return hashlib.sha256(machine_id.encode()).digest()
|
||||
|
||||
|
||||
def _encrypt_for_storage(plaintext: str) -> str:
|
||||
"""
|
||||
Encrypt a channel key for storage using machine-specific key.
|
||||
|
||||
Returns ENC: prefixed base64 string.
|
||||
"""
|
||||
import base64
|
||||
|
||||
key = _get_machine_key()
|
||||
plaintext_bytes = plaintext.encode()
|
||||
|
||||
# XOR with key (cycling if needed)
|
||||
encrypted = bytes(
|
||||
pb ^ key[i % len(key)]
|
||||
for i, pb in enumerate(plaintext_bytes)
|
||||
)
|
||||
|
||||
return ENCRYPTED_PREFIX + base64.b64encode(encrypted).decode()
|
||||
|
||||
|
||||
def _decrypt_from_storage(stored: str) -> str | None:
|
||||
"""
|
||||
Decrypt a stored channel key.
|
||||
|
||||
Returns None if decryption fails or format is invalid.
|
||||
"""
|
||||
import base64
|
||||
|
||||
if not stored.startswith(ENCRYPTED_PREFIX):
|
||||
# Not encrypted, return as-is (legacy plaintext)
|
||||
return stored
|
||||
|
||||
try:
|
||||
encrypted = base64.b64decode(stored[len(ENCRYPTED_PREFIX):])
|
||||
key = _get_machine_key()
|
||||
|
||||
# XOR to decrypt
|
||||
decrypted = bytes(
|
||||
eb ^ key[i % len(key)]
|
||||
for i, eb in enumerate(encrypted)
|
||||
)
|
||||
|
||||
return decrypted.decode()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def generate_channel_key() -> str:
|
||||
"""
|
||||
@@ -154,11 +228,13 @@ def get_channel_key() -> str | None:
|
||||
else:
|
||||
debug.print(f"Warning: Invalid {CHANNEL_KEY_ENV_VAR} format, ignoring")
|
||||
|
||||
# 2. Check config files
|
||||
# 2. Check config files (may be encrypted)
|
||||
for config_path in CONFIG_LOCATIONS:
|
||||
if config_path.exists():
|
||||
try:
|
||||
key = config_path.read_text().strip()
|
||||
stored = config_path.read_text().strip()
|
||||
# Decrypt if encrypted, otherwise use as-is (legacy)
|
||||
key = _decrypt_from_storage(stored)
|
||||
if key and validate_channel_key(key):
|
||||
debug.print(f"Channel key from {config_path}: {get_channel_fingerprint(key)}")
|
||||
return format_channel_key(key)
|
||||
@@ -200,8 +276,9 @@ def set_channel_key(key: str, location: str = "project") -> Path:
|
||||
# Create directory if needed
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write key with newline
|
||||
config_path.write_text(formatted + "\n")
|
||||
# Encrypt and write (tied to this machine's identity)
|
||||
encrypted = _encrypt_for_storage(formatted)
|
||||
config_path.write_text(encrypted + "\n")
|
||||
|
||||
# Set restrictive permissions (owner read/write only)
|
||||
try:
|
||||
@@ -334,11 +411,12 @@ def get_channel_status() -> dict:
|
||||
for config_path in CONFIG_LOCATIONS:
|
||||
if config_path.exists():
|
||||
try:
|
||||
file_key = config_path.read_text().strip()
|
||||
if file_key and format_channel_key(file_key) == key:
|
||||
stored = config_path.read_text().strip()
|
||||
file_key = _decrypt_from_storage(stored)
|
||||
if file_key and validate_channel_key(file_key) and format_channel_key(file_key) == key:
|
||||
source = str(config_path)
|
||||
break
|
||||
except (OSError, PermissionError):
|
||||
except (OSError, PermissionError, ValueError):
|
||||
continue
|
||||
|
||||
return {
|
||||
|
||||
@@ -80,12 +80,6 @@ from .batch import (
|
||||
batch_capacity_check,
|
||||
print_batch_result,
|
||||
)
|
||||
from .compression import (
|
||||
HAS_LZ4,
|
||||
CompressionAlgorithm,
|
||||
algorithm_name,
|
||||
get_available_algorithms,
|
||||
)
|
||||
from .constants import (
|
||||
DEFAULT_PASSPHRASE_WORDS, # v3.2.0: renamed from DEFAULT_PHRASE_WORDS
|
||||
DEFAULT_PIN_LENGTH,
|
||||
@@ -183,19 +177,10 @@ def cli(ctx, json_output):
|
||||
help="Passphrase (recommend 4+ words)",
|
||||
)
|
||||
@click.option("--pin", prompt=True, hide_input=True, confirmation_prompt=True, help="PIN code")
|
||||
@click.option(
|
||||
"--compress/--no-compress", default=True, help="Enable/disable compression (default: enabled)"
|
||||
)
|
||||
@click.option(
|
||||
"--algorithm",
|
||||
type=click.Choice(["zlib", "lz4", "none"]),
|
||||
default="zlib",
|
||||
help="Compression algorithm",
|
||||
)
|
||||
@click.option("--dry-run", is_flag=True, help="Show capacity usage without encoding")
|
||||
@click.pass_context
|
||||
def encode(
|
||||
ctx, carrier, reference, message, file_payload, output, passphrase, pin, compress, algorithm, dry_run
|
||||
ctx, carrier, reference, message, file_payload, output, passphrase, pin, dry_run
|
||||
):
|
||||
"""
|
||||
Encode a message or file into an image.
|
||||
@@ -214,18 +199,6 @@ def encode(
|
||||
if not message and not file_payload:
|
||||
raise click.UsageError("Either --message or --file is required")
|
||||
|
||||
# Parse compression algorithm
|
||||
algo_map = {
|
||||
"zlib": CompressionAlgorithm.ZLIB,
|
||||
"lz4": CompressionAlgorithm.LZ4,
|
||||
"none": CompressionAlgorithm.NONE,
|
||||
}
|
||||
compression_algo = algo_map[algorithm] if compress else CompressionAlgorithm.NONE
|
||||
|
||||
if algorithm == "lz4" and not HAS_LZ4:
|
||||
click.echo("Warning: LZ4 not available, falling back to zlib", err=True)
|
||||
compression_algo = CompressionAlgorithm.ZLIB
|
||||
|
||||
# Calculate payload size
|
||||
if file_payload:
|
||||
payload_size = Path(file_payload).stat().st_size
|
||||
@@ -247,7 +220,6 @@ def encode(
|
||||
"capacity_bytes": capacity_bytes,
|
||||
"payload_type": payload_type,
|
||||
"payload_size": payload_size,
|
||||
"compression": algorithm_name(compression_algo),
|
||||
"usage_percent": round(payload_size / capacity_bytes * 100, 1),
|
||||
"fits": payload_size < capacity_bytes,
|
||||
}
|
||||
@@ -259,7 +231,6 @@ def encode(
|
||||
click.echo(f"Reference: {reference}")
|
||||
click.echo(f"Capacity: {capacity_bytes:,} bytes ({capacity_bytes//1024} KB)")
|
||||
click.echo(f"Payload: {payload_size:,} bytes ({payload_type})")
|
||||
click.echo(f"Compression: {algorithm_name(compression_algo)}")
|
||||
click.echo(f"Usage: {result['usage_percent']}%")
|
||||
click.echo(f"Status: {'✓ Fits' if result['fits'] else '✗ Too large'}")
|
||||
return
|
||||
@@ -306,7 +277,6 @@ def encode(
|
||||
"reference": reference,
|
||||
"output": output,
|
||||
"payload_type": payload_type,
|
||||
"compression": algorithm_name(compression_algo),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
@@ -314,7 +284,6 @@ def encode(
|
||||
else:
|
||||
click.echo(f"✓ Encoded {payload_type} to {output}")
|
||||
click.echo(f" Reference: {reference}")
|
||||
click.echo(f" Compression: {algorithm_name(compression_algo)}")
|
||||
|
||||
except Exception as e:
|
||||
if ctx.obj.get("json"):
|
||||
@@ -474,13 +443,6 @@ def batch():
|
||||
help="Passphrase (recommend 4+ words)",
|
||||
)
|
||||
@click.option("--pin", prompt=True, hide_input=True, confirmation_prompt=True, help="PIN code")
|
||||
@click.option("--compress/--no-compress", default=True, help="Enable/disable compression")
|
||||
@click.option(
|
||||
"--algorithm",
|
||||
type=click.Choice(["zlib", "lz4", "none"]),
|
||||
default="zlib",
|
||||
help="Compression algorithm",
|
||||
)
|
||||
@click.option("-r", "--recursive", is_flag=True, help="Search directories recursively")
|
||||
@click.option("-j", "--jobs", default=4, help="Parallel workers (default: 4)")
|
||||
@click.option("-v", "--verbose", is_flag=True, help="Show detailed output")
|
||||
@@ -494,8 +456,6 @@ def batch_encode(
|
||||
suffix,
|
||||
passphrase,
|
||||
pin,
|
||||
compress,
|
||||
algorithm,
|
||||
recursive,
|
||||
jobs,
|
||||
verbose,
|
||||
@@ -530,7 +490,6 @@ def batch_encode(
|
||||
output_dir=Path(output_dir) if output_dir else None,
|
||||
output_suffix=suffix,
|
||||
credentials=credentials,
|
||||
compress=compress,
|
||||
recursive=recursive,
|
||||
progress_callback=progress if not ctx.obj.get("json") else None,
|
||||
)
|
||||
@@ -821,10 +780,6 @@ def info(ctx, full):
|
||||
"fingerprint": channel_fingerprint,
|
||||
"source": channel_source,
|
||||
} if channel_fingerprint else None,
|
||||
"compression": {
|
||||
"available": [algorithm_name(a) for a in get_available_algorithms()],
|
||||
"lz4_installed": HAS_LZ4,
|
||||
},
|
||||
"limits": {
|
||||
"max_message_bytes": MAX_MESSAGE_SIZE,
|
||||
"max_file_payload_bytes": MAX_FILE_PAYLOAD_SIZE,
|
||||
@@ -859,7 +814,7 @@ def info(ctx, full):
|
||||
masked = f"{channel_fingerprint[:4]}••••••••{channel_fingerprint[-4:]}"
|
||||
click.echo(f" Channel: {masked}")
|
||||
else:
|
||||
click.echo(" Channel: \033[33mpublic\033[0m")
|
||||
click.echo(" Channel: public")
|
||||
|
||||
# DCT
|
||||
dct_status = "\033[32m✓ enabled\033[0m" if has_dct else "\033[31m✗ disabled\033[0m"
|
||||
|
||||
Reference in New Issue
Block a user