3.2.0 Big revamp

This commit is contained in:
Aaron D. Lee
2026-01-01 03:14:35 -05:00
parent 11fc8aab27
commit 657cae0ae6
14 changed files with 2774 additions and 1146 deletions

View File

@@ -0,0 +1,500 @@
# API Update Summary for v3.2.0
## Overview
The FastAPI REST API has been updated to align with Stegasoo v3.2.0's breaking changes:
1. **Removed date dependency** - No `date_str` field in requests
2. **Renamed day_phrase → passphrase** - Updated all request/response models
3. **Updated generation** - Now generates single passphrase instead of daily phrases
## Breaking Changes
### Request Model Changes
#### 1. EncodeRequest & EncodeFileRequest
**Before (v3.1.0):**
```python
class EncodeRequest(BaseModel):
message: str
reference_photo_base64: str
carrier_image_base64: str
day_phrase: str # ← Changed to passphrase
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
date_str: Optional[str] = None # ← REMOVED
embed_mode: EmbedModeType = "lsb"
```
**After (v3.2.0):**
```python
class EncodeRequest(BaseModel):
message: str
reference_photo_base64: str
carrier_image_base64: str
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
# date_str removed in v3.2.0
embed_mode: EmbedModeType = "lsb"
dct_output_format: DctOutputFormatType = "png"
dct_color_mode: DctColorModeType = "grayscale"
```
#### 2. DecodeRequest
**Before (v3.1.0):**
```python
class DecodeRequest(BaseModel):
stego_image_base64: str
reference_photo_base64: str
day_phrase: str # ← Changed to passphrase
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
embed_mode: ExtractModeType = "auto"
```
**After (v3.2.0):**
```python
class DecodeRequest(BaseModel):
stego_image_base64: str
reference_photo_base64: str
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
embed_mode: ExtractModeType = "auto"
```
#### 3. GenerateRequest
**Before (v3.1.0):**
```python
class GenerateRequest(BaseModel):
use_pin: bool = True
use_rsa: bool = False
pin_length: int = Field(default=6, ge=MIN_PIN_LENGTH, le=MAX_PIN_LENGTH)
rsa_bits: int = Field(default=2048)
words_per_phrase: int = Field(default=3, ge=MIN_PHRASE_WORDS, le=MAX_PHRASE_WORDS)
```
**After (v3.2.0):**
```python
class GenerateRequest(BaseModel):
use_pin: bool = True
use_rsa: bool = False
pin_length: int = Field(default=6, ge=MIN_PIN_LENGTH, le=MAX_PIN_LENGTH)
rsa_bits: int = Field(default=2048)
words_per_passphrase: int = Field(
default=DEFAULT_PASSPHRASE_WORDS, # = 4, was 3
ge=MIN_PASSPHRASE_WORDS,
le=MAX_PASSPHRASE_WORDS,
description="Words per passphrase (v3.2.0: default increased to 4)"
)
```
### Response Model Changes
#### 1. GenerateResponse
**Before (v3.1.0):**
```python
class GenerateResponse(BaseModel):
phrases: dict[str, str] # Monday -> phrase, Tuesday -> phrase, etc.
pin: Optional[str] = None
rsa_key_pem: Optional[str] = None
entropy: dict[str, int]
```
**After (v3.2.0):**
```python
class GenerateResponse(BaseModel):
passphrase: str = Field(description="Single passphrase (v3.2.0: no daily rotation)")
pin: Optional[str] = None
rsa_key_pem: Optional[str] = None
entropy: dict[str, int]
# Legacy field for compatibility
phrases: Optional[dict[str, str]] = Field(
default=None,
description="Deprecated: Use 'passphrase' instead"
)
```
#### 2. EncodeResponse
**Before (v3.1.0):**
```python
class EncodeResponse(BaseModel):
stego_image_base64: str
filename: str
capacity_used_percent: float
date_used: str
day_of_week: str
embed_mode: str
output_format: str = "png"
color_mode: str = "color"
```
**After (v3.2.0):**
```python
class EncodeResponse(BaseModel):
stego_image_base64: str
filename: str
capacity_used_percent: float
embed_mode: str
output_format: str = "png"
color_mode: str = "color"
# Legacy fields (no longer used in crypto)
date_used: Optional[str] = Field(
default=None,
description="Deprecated: Date no longer used in v3.2.0"
)
day_of_week: Optional[str] = Field(
default=None,
description="Deprecated: Date no longer used in v3.2.0"
)
```
### Endpoint Changes
#### 1. POST /encode
**Before (v3.1.0):**
```json
{
"message": "Secret message",
"reference_photo_base64": "...",
"carrier_image_base64": "...",
"day_phrase": "apple forest thunder",
"date_str": "2025-01-15",
"pin": "123456",
"embed_mode": "lsb"
}
```
**After (v3.2.0):**
```json
{
"message": "Secret message",
"reference_photo_base64": "...",
"carrier_image_base64": "...",
"passphrase": "apple forest thunder mountain",
"pin": "123456",
"embed_mode": "lsb"
}
```
#### 2. POST /decode
**Before (v3.1.0):**
```json
{
"stego_image_base64": "...",
"reference_photo_base64": "...",
"day_phrase": "apple forest thunder",
"pin": "123456",
"embed_mode": "auto"
}
```
**After (v3.2.0):**
```json
{
"stego_image_base64": "...",
"reference_photo_base64": "...",
"passphrase": "apple forest thunder mountain",
"pin": "123456",
"embed_mode": "auto"
}
```
#### 3. POST /generate
**Response Before (v3.1.0):**
```json
{
"phrases": {
"Monday": "apple forest thunder",
"Tuesday": "banana river lightning",
...
},
"pin": "123456",
"rsa_key_pem": null,
"entropy": {
"phrase": 33,
"pin": 20,
"rsa": 0,
"total": 53
}
}
```
**Response After (v3.2.0):**
```json
{
"passphrase": "apple forest thunder mountain",
"pin": "123456",
"rsa_key_pem": null,
"entropy": {
"passphrase": 44,
"pin": 20,
"rsa": 0,
"total": 64
},
"phrases": null
}
```
#### 4. POST /encode/multipart
**Form Fields Before (v3.1.0):**
- `day_phrase` (required)
- `date_str` (optional)
- `reference_photo` (file)
- `carrier` (file)
- ...
**Form Fields After (v3.2.0):**
- `passphrase` (required) ← renamed from day_phrase
- `reference_photo` (file)
- `carrier` (file)
- ... (date_str removed)
**Response Headers Before (v3.1.0):**
```
X-Stegasoo-Date: 2025-01-15
X-Stegasoo-Day: Wednesday
X-Stegasoo-Capacity-Percent: 25.5
X-Stegasoo-Embed-Mode: lsb
```
**Response Headers After (v3.2.0):**
```
X-Stegasoo-Capacity-Percent: 25.5
X-Stegasoo-Embed-Mode: lsb
X-Stegasoo-Output-Format: png
X-Stegasoo-Color-Mode: color
X-Stegasoo-Version: 3.2.0
```
### New Status Endpoint Information
#### GET /
**Added to response:**
```json
{
"version": "3.2.0",
...
"breaking_changes": {
"date_removed": "No date_str parameter needed - encode/decode anytime",
"passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)",
"format_version": 4,
"backward_compatible": false
}
}
```
## Migration Guide for API Clients
### 1. Update Request Bodies
**Find and replace in client code:**
```javascript
// Before
{
day_phrase: "apple forest thunder",
date_str: "2025-01-15"
}
// After
{
passphrase: "apple forest thunder mountain"
}
```
### 2. Update Response Handling
**Before:**
```javascript
const response = await fetch('/encode', {
method: 'POST',
body: JSON.stringify({
message: "secret",
day_phrase: "words",
date_str: "2025-01-15",
...
})
});
const data = await response.json();
console.log(data.date_used); // "2025-01-15"
console.log(data.day_of_week); // "Wednesday"
```
**After:**
```javascript
const response = await fetch('/encode', {
method: 'POST',
body: JSON.stringify({
message: "secret",
passphrase: "longer words here now",
// date_str removed
...
})
});
const data = await response.json();
// date_used and day_of_week are null in v3.2.0
```
### 3. Update Generate Endpoint Usage
**Before:**
```javascript
const creds = await fetch('/generate', {
method: 'POST',
body: JSON.stringify({ use_pin: true })
}).then(r => r.json());
// Use Monday's phrase
const mondayPhrase = creds.phrases['Monday'];
```
**After:**
```javascript
const creds = await fetch('/generate', {
method: 'POST',
body: JSON.stringify({ use_pin: true })
}).then(r => r.json());
// Use single passphrase
const passphrase = creds.passphrase;
```
### 4. Update Multipart Requests
**Before (JavaScript fetch):**
```javascript
const formData = new FormData();
formData.append('day_phrase', 'apple forest thunder');
formData.append('date_str', '2025-01-15');
formData.append('reference_photo', refPhotoFile);
formData.append('carrier', carrierFile);
formData.append('message', 'secret');
formData.append('pin', '123456');
const response = await fetch('/encode/multipart', {
method: 'POST',
body: formData
});
```
**After (JavaScript fetch):**
```javascript
const formData = new FormData();
formData.append('passphrase', 'apple forest thunder mountain');
// date_str removed
formData.append('reference_photo', refPhotoFile);
formData.append('carrier', carrierFile);
formData.append('message', 'secret');
formData.append('pin', '123456');
const response = await fetch('/encode/multipart', {
method: 'POST',
body: formData
});
```
## Testing Checklist
### Endpoints to Test
- [ ] GET / - Returns v3.2.0 with breaking_changes info
- [ ] GET /modes - Returns mode information
- [ ] POST /generate - Returns single passphrase
- [ ] POST /encode - Works without date_str
- [ ] POST /encode/file - Works without date_str
- [ ] POST /decode - Works without date_str
- [ ] POST /encode/multipart - Accepts passphrase instead of day_phrase
- [ ] POST /decode/multipart - Accepts passphrase instead of day_phrase
- [ ] POST /compare - Still works
- [ ] POST /will-fit - Still works
- [ ] POST /image/info - Still works
- [ ] POST /extract-key-from-qr - Still works
### Validation Tests
- [ ] Reject requests with `day_phrase` field (should get validation error)
- [ ] Reject requests with `date_str` field (should be ignored or error)
- [ ] Accept requests with `passphrase` field
- [ ] Generate response includes `passphrase` field
- [ ] Generate response has `phrases` as null
- [ ] Encode response has `date_used` and `day_of_week` as null
- [ ] Multipart encode works with new field names
- [ ] Response headers updated correctly
## OpenAPI/Swagger Documentation
The FastAPI auto-generated documentation (/docs and /redoc) will automatically reflect the changes:
1. **Models updated** - Request/response schemas show new field names
2. **Descriptions updated** - Field descriptions mention v3.2.0 changes
3. **Examples updated** - Interactive API explorer uses new field names
Users can browse to `/docs` to see the updated API specification.
## Backward Compatibility
**Breaking Change:** API v3.2.0 is NOT backward compatible with v3.1.0
Clients using the old API will encounter:
1. **Validation errors** - Missing required `passphrase` field
2. **Unexpected responses** - `phrases` field will be null
3. **Changed behavior** - Date fields no longer populated
### Migration Timeline Recommendation
1. **Deploy v3.2.0 API** to staging
2. **Update client applications** to use new field names
3. **Test thoroughly** with staging API
4. **Deploy v3.2.0 API** to production
5. **Notify users** of breaking changes
Alternatively, run v3.1.0 and v3.2.0 APIs side-by-side on different paths:
- `/api/v3.1/` - Old API
- `/api/v3.2/` - New API
## Constants Updates
Used in validation:
```python
from stegasoo.constants import (
MIN_PASSPHRASE_WORDS, # = 3
MAX_PASSPHRASE_WORDS, # = 12
DEFAULT_PASSPHRASE_WORDS, # = 4 (increased from 3)
)
```
## Error Messages
All error messages updated:
- "day_phrase is required" → "passphrase is required"
- References to "phrase" now mean "passphrase"
## Implementation Status
✅ All request models updated
✅ All response models updated
✅ All endpoints updated
✅ Multipart endpoints updated
✅ Status endpoint shows breaking changes
✅ Constants imported correctly
✅ Error handling updated
✅ No references to day_phrase in user-facing text
✅ No date_str parameters accepted
Ready for deployment!

View File

@@ -1,9 +1,16 @@
#!/usr/bin/env python3
"""
Stegasoo REST API (v3.0.1)
Stegasoo REST API (v3.2.0)
FastAPI-based REST API for steganography operations.
Supports both text messages and file embedding.
CHANGES in v3.2.0:
- Removed date dependency from all operations
- Renamed day_phrase → passphrase
- No date_str parameters needed
- Simplified API for asynchronous communications
NEW in v3.0: LSB and DCT embedding modes.
NEW in v3.0.1: DCT color mode and JPEG output format.
"""
@@ -26,13 +33,12 @@ import stegasoo
from stegasoo import (
encode, decode, generate_credentials,
validate_image, calculate_capacity,
get_day_from_date,
DAY_NAMES, __version__,
__version__,
StegasooError, DecryptionError, CapacityError,
has_argon2,
FilePayload,
MAX_FILE_PAYLOAD_SIZE,
# NEW in v3.0 - Embedding modes
# Embedding modes
EMBED_MODE_LSB,
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
@@ -43,7 +49,8 @@ from stegasoo import (
)
from stegasoo.constants import (
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
MIN_PHRASE_WORDS, MAX_PHRASE_WORDS,
MIN_PASSPHRASE_WORDS, MAX_PASSPHRASE_WORDS,
DEFAULT_PASSPHRASE_WORDS,
VALID_RSA_SIZES,
)
@@ -68,6 +75,12 @@ app = FastAPI(
description="""
Secure steganography with hybrid authentication. Supports text messages and file embedding.
## Version 3.2.0 Changes
- **No date parameters needed** - Encode and decode anytime without tracking dates
- **Single passphrase** - No daily rotation, just use your passphrase
- **True asynchronous communications** - Perfect for dead drops and delayed delivery
## Embedding Modes (v3.0)
- **LSB mode** (default): Spatial LSB embedding, full color output, higher capacity
@@ -105,25 +118,35 @@ class GenerateRequest(BaseModel):
use_rsa: bool = False
pin_length: int = Field(default=6, ge=MIN_PIN_LENGTH, le=MAX_PIN_LENGTH)
rsa_bits: int = Field(default=2048)
words_per_phrase: int = Field(default=3, ge=MIN_PHRASE_WORDS, le=MAX_PHRASE_WORDS)
words_per_passphrase: int = Field(
default=DEFAULT_PASSPHRASE_WORDS,
ge=MIN_PASSPHRASE_WORDS,
le=MAX_PASSPHRASE_WORDS,
description="Words per passphrase (v3.2.0: default increased to 4)"
)
class GenerateResponse(BaseModel):
phrases: dict[str, str]
passphrase: str = Field(description="Single passphrase (v3.2.0: no daily rotation)")
pin: Optional[str] = None
rsa_key_pem: Optional[str] = None
entropy: dict[str, int]
# Legacy field for compatibility
phrases: Optional[dict[str, str]] = Field(
default=None,
description="Deprecated: Use 'passphrase' instead"
)
class EncodeRequest(BaseModel):
message: str
reference_photo_base64: str
carrier_image_base64: str
day_phrase: str
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
date_str: Optional[str] = None
# date_str removed in v3.2.0
embed_mode: EmbedModeType = Field(
default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
@@ -146,11 +169,11 @@ class EncodeFileRequest(BaseModel):
mime_type: Optional[str] = None
reference_photo_base64: str
carrier_image_base64: str
day_phrase: str
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
date_str: Optional[str] = None
# date_str removed in v3.2.0
embed_mode: EmbedModeType = Field(
default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
@@ -170,8 +193,6 @@ class EncodeResponse(BaseModel):
stego_image_base64: str
filename: str
capacity_used_percent: float
date_used: str
day_of_week: str
embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'")
# NEW in v3.0.1
output_format: str = Field(
@@ -182,12 +203,21 @@ class EncodeResponse(BaseModel):
default="color",
description="Color mode: 'color' (LSB/DCT color) or 'grayscale' (DCT grayscale)"
)
# Legacy fields (v3.2.0: no longer used in crypto)
date_used: Optional[str] = Field(
default=None,
description="Deprecated: Date no longer used in v3.2.0"
)
day_of_week: Optional[str] = Field(
default=None,
description="Deprecated: Date no longer used in v3.2.0"
)
class DecodeRequest(BaseModel):
stego_image_base64: str
reference_photo_base64: str
day_phrase: str
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
@@ -268,7 +298,6 @@ class StatusResponse(BaseModel):
has_argon2: bool
has_qrcode_read: bool
has_dct: bool
day_names: list[str]
max_payload_kb: int
available_modes: list[str]
# NEW in v3.0.1
@@ -276,6 +305,10 @@ class StatusResponse(BaseModel):
default=None,
description="DCT mode features (v3.0.1+)"
)
# NEW in v3.2.0
breaking_changes: dict = Field(
description="v3.2.0 breaking changes"
)
class QrExtractResponse(BaseModel):
@@ -330,10 +363,15 @@ async def root():
has_argon2=has_argon2(),
has_qrcode_read=HAS_QR_READ,
has_dct=has_dct_support(),
day_names=list(DAY_NAMES),
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024,
available_modes=available_modes,
dct_features=dct_features,
breaking_changes={
"date_removed": "No date_str parameter needed - encode/decode anytime",
"passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)",
"format_version": 4,
"backward_compatible": False,
}
)
@@ -496,6 +534,9 @@ async def api_generate(request: GenerateRequest):
Generate credentials for encoding/decoding.
At least one of use_pin or use_rsa must be True.
v3.2.0: Generates single passphrase (no daily rotation).
Default increased to 4 words for better security.
"""
if not request.use_pin and not request.use_rsa:
raise HTTPException(400, "Must enable at least one of use_pin or use_rsa")
@@ -509,19 +550,20 @@ async def api_generate(request: GenerateRequest):
use_rsa=request.use_rsa,
pin_length=request.pin_length,
rsa_bits=request.rsa_bits,
words_per_phrase=request.words_per_phrase
words_per_passphrase=request.words_per_passphrase
)
return GenerateResponse(
phrases=creds.phrases,
passphrase=creds.passphrase, # v3.2.0: Single passphrase
pin=creds.pin,
rsa_key_pem=creds.rsa_key_pem,
entropy={
"phrase": creds.phrase_entropy,
"passphrase": creds.passphrase_entropy,
"pin": creds.pin_entropy,
"rsa": creds.rsa_entropy,
"total": creds.total_entropy
}
},
phrases=None # Legacy field removed
)
except Exception as e:
raise HTTPException(500, str(e))
@@ -573,6 +615,8 @@ async def api_encode(request: EncodeRequest):
Images must be base64-encoded. Returns base64-encoded stego image.
v3.2.0: No date_str parameter needed - encode anytime!
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
"""
@@ -592,21 +636,21 @@ async def api_encode(request: EncodeRequest):
request.dct_color_mode
)
# v3.2.0: No date_str parameter
result = encode(
message=request.message,
reference_photo=ref_photo,
carrier_image=carrier,
day_phrase=request.day_phrase,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
date_str=request.date_str,
# date_str removed in v3.2.0
embed_mode=request.embed_mode,
**dct_params, # NEW in v3.0.1
**dct_params,
)
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
day_of_week = get_day_from_date(result.date_used)
output_format, color_mode, _ = _get_output_info(
request.embed_mode,
@@ -618,11 +662,11 @@ async def api_encode(request: EncodeRequest):
stego_image_base64=stego_b64,
filename=result.filename,
capacity_used_percent=result.capacity_percent,
date_used=result.date_used,
day_of_week=day_of_week,
embed_mode=request.embed_mode,
output_format=output_format,
color_mode=color_mode,
date_used=None, # v3.2.0: No longer used
day_of_week=None, # v3.2.0: No longer used
)
except CapacityError as e:
@@ -640,6 +684,8 @@ async def api_encode_file(request: EncodeFileRequest):
File data must be base64-encoded.
v3.2.0: No date_str parameter needed - encode anytime!
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format and dct_color_mode.
"""
@@ -666,21 +712,21 @@ async def api_encode_file(request: EncodeFileRequest):
request.dct_color_mode
)
# v3.2.0: No date_str parameter
result = encode(
message=payload,
reference_photo=ref_photo,
carrier_image=carrier,
day_phrase=request.day_phrase,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
date_str=request.date_str,
# date_str removed in v3.2.0
embed_mode=request.embed_mode,
**dct_params, # NEW in v3.0.1
**dct_params,
)
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
day_of_week = get_day_from_date(result.date_used)
output_format, color_mode, _ = _get_output_info(
request.embed_mode,
@@ -692,11 +738,11 @@ async def api_encode_file(request: EncodeFileRequest):
stego_image_base64=stego_b64,
filename=result.filename,
capacity_used_percent=result.capacity_percent,
date_used=result.date_used,
day_of_week=day_of_week,
embed_mode=request.embed_mode,
output_format=output_format,
color_mode=color_mode,
date_used=None, # v3.2.0: No longer used
day_of_week=None, # v3.2.0: No longer used
)
except CapacityError as e:
@@ -718,6 +764,8 @@ async def api_decode(request: DecodeRequest):
Returns payload_type to indicate if result is text or file.
v3.2.0: No date_str parameter needed - decode anytime!
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
With 'auto' (default), tries LSB first then DCT.
@@ -733,10 +781,11 @@ async def api_decode(request: DecodeRequest):
ref_photo = base64.b64decode(request.reference_photo_base64)
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
# v3.2.0: No date_str parameter
result = decode(
stego_image=stego,
reference_photo=ref_photo,
day_phrase=request.day_phrase,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
@@ -770,7 +819,7 @@ async def api_decode(request: DecodeRequest):
@app.post("/encode/multipart")
async def api_encode_multipart(
day_phrase: str = Form(...),
passphrase: str = Form(..., description="Passphrase (v3.2.0: renamed from day_phrase)"),
reference_photo: UploadFile = File(...),
carrier: UploadFile = File(...),
message: str = Form(""),
@@ -779,7 +828,7 @@ async def api_encode_multipart(
rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""),
date_str: str = Form(""),
# date_str removed in v3.2.0
embed_mode: str = Form("lsb"),
# NEW in v3.0.1
dct_output_format: str = Form("png"),
@@ -792,6 +841,8 @@ async def api_encode_multipart(
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns the stego image directly with metadata headers.
v3.2.0: No date_str parameter needed - encode anytime!
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
"""
@@ -849,20 +900,20 @@ async def api_encode_multipart(
# Get DCT parameters
dct_params = _get_dct_params(embed_mode, dct_output_format, dct_color_mode)
# v3.2.0: No date_str parameter
result = encode(
message=payload,
reference_photo=ref_data,
carrier_image=carrier_data,
day_phrase=day_phrase,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=effective_password,
date_str=date_str if date_str else None,
# date_str removed in v3.2.0
embed_mode=embed_mode,
**dct_params, # NEW in v3.0.1
**dct_params,
)
day_of_week = get_day_from_date(result.date_used)
output_format, color_mode, mime_type = _get_output_info(
embed_mode, dct_output_format, dct_color_mode
)
@@ -872,12 +923,11 @@ async def api_encode_multipart(
media_type=mime_type,
headers={
"Content-Disposition": f"attachment; filename={result.filename}",
"X-Stegasoo-Date": result.date_used,
"X-Stegasoo-Day": day_of_week,
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
"X-Stegasoo-Embed-Mode": embed_mode,
"X-Stegasoo-Output-Format": output_format, # NEW in v3.0.1
"X-Stegasoo-Color-Mode": color_mode, # NEW in v3.0.1
"X-Stegasoo-Output-Format": output_format,
"X-Stegasoo-Color-Mode": color_mode,
"X-Stegasoo-Version": __version__,
}
)
@@ -893,7 +943,7 @@ async def api_encode_multipart(
@app.post("/decode/multipart", response_model=DecodeResponse)
async def api_decode_multipart(
day_phrase: str = Form(...),
passphrase: str = Form(..., description="Passphrase (v3.2.0: renamed from day_phrase)"),
reference_photo: UploadFile = File(...),
stego_image: UploadFile = File(...),
pin: str = Form(""),
@@ -908,6 +958,8 @@ async def api_decode_multipart(
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns JSON with payload_type indicating text or file.
v3.2.0: No date_str parameter needed - decode anytime!
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
Note: Extraction works the same regardless of color mode used during encoding.
@@ -944,10 +996,11 @@ async def api_decode_multipart(
# QR code keys are never password-protected
effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# v3.2.0: No date_str parameter
result = decode(
stego_image=stego_data,
reference_photo=ref_data,
day_phrase=day_phrase,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=effective_password,
@@ -1022,7 +1075,7 @@ async def api_image_info(
capacity_bytes=comparison['dct']['capacity_bytes'],
capacity_kb=round(comparison['dct']['capacity_kb'], 1),
available=comparison['dct']['available'],
output_format="PNG/JPEG (grayscale or color)", # Updated for v3.0.1
output_format="PNG/JPEG (grayscale or color)",
),
}

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env python3
"""
Stegasoo CLI - Command-line interface for steganography operations (v3.0.1).
Stegasoo CLI - Command-line interface for steganography operations (v3.2.0).
CHANGES in v3.2.0:
- Removed date dependency from all operations
- Renamed day_phrase → passphrase
- No longer need to specify or remember encoding dates
Usage:
stegasoo generate [OPTIONS]
@@ -10,10 +15,6 @@ Usage:
stegasoo info [OPTIONS]
stegasoo compare [OPTIONS]
stegasoo modes [OPTIONS]
New in v3.0.1:
- DCT color mode: --dct-color (grayscale or color)
- DCT output format: --dct-format (png or jpeg)
"""
import sys
@@ -31,13 +32,13 @@ from stegasoo import (
generate_credentials,
export_rsa_key_pem, load_rsa_key,
validate_image, calculate_capacity,
get_day_from_date, parse_date_from_filename,
DAY_NAMES, __version__,
parse_date_from_filename, # Keep for filename parsing only
__version__,
StegasooError, DecryptionError, ExtractionError,
FilePayload,
will_fit,
strip_image_metadata,
# NEW in v3.0 - Embedding modes
# Embedding modes
EMBED_MODE_LSB,
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
@@ -79,16 +80,22 @@ def cli():
\b
- Reference photo (something you have)
- Daily passphrase (something you know)
- Passphrase (something you know)
- Static PIN or RSA key (additional security)
\b
Embedding Modes (v3.0):
Version 3.2.0 Changes:
- No more date parameters - encode/decode anytime!
- Simplified passphrase (no daily rotation)
- True asynchronous communications
\b
Embedding Modes:
- LSB mode (default): Full color output, higher capacity
- DCT mode: Frequency domain, ~20% capacity, better stealth
\b
DCT Options (v3.0.1):
DCT Options:
- Color mode: grayscale (default) or color (preserves colors)
- Output format: png (lossless) or jpeg (smaller, natural)
"""
@@ -104,7 +111,7 @@ def cli():
@click.option('--rsa/--no-rsa', default=False, help='Generate an RSA key')
@click.option('--pin-length', type=click.IntRange(6, 9), default=6, help='PIN length (6-9)')
@click.option('--rsa-bits', type=click.Choice(['2048', '3072', '4096']), default='2048', help='RSA key size')
@click.option('--words', type=click.IntRange(3, 12), default=3, help='Words per phrase (3-12)')
@click.option('--words', type=click.IntRange(3, 12), default=4, help='Words per passphrase (default: 4, was 3 in v3.1)')
@click.option('--output', '-o', type=click.Path(), help='Save RSA key to file (requires password)')
@click.option('--password', '-p', help='Password for RSA key file')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
@@ -112,12 +119,16 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
"""
Generate credentials for encoding/decoding.
Creates daily passphrases and optionally a PIN and/or RSA key.
Creates a passphrase and optionally a PIN and/or RSA key.
At least one of --pin or --rsa must be enabled.
v3.2.0: No more daily passphrases - use one strong passphrase!
Default increased to 4 words (from 3) for better security.
\b
Examples:
stegasoo generate
stegasoo generate --words 5
stegasoo generate --rsa --rsa-bits 4096
stegasoo generate --rsa -o mykey.pem -p "secretpassword"
stegasoo generate --no-pin --rsa
@@ -137,17 +148,17 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
use_rsa=rsa,
pin_length=pin_length,
rsa_bits=int(rsa_bits),
words_per_phrase=words
words_per_passphrase=words
)
if as_json:
import json
data = {
'phrases': creds.phrases,
'passphrase': creds.passphrase,
'pin': creds.pin,
'rsa_key': creds.rsa_key_pem,
'entropy': {
'phrase': creds.phrase_entropy,
'passphrase': creds.passphrase_entropy,
'pin': creds.pin_entropy,
'rsa': creds.rsa_entropy,
'total': creds.total_entropy,
@@ -159,7 +170,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
# Pretty output
click.echo()
click.secho("=" * 60, fg='cyan')
click.secho(" STEGASOO CREDENTIALS", fg='cyan', bold=True)
click.secho(" STEGASOO CREDENTIALS (v3.2.0)", fg='cyan', bold=True)
click.secho("=" * 60, fg='cyan')
click.echo()
@@ -172,11 +183,8 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
click.secho(f" {creds.pin}", fg='bright_yellow', bold=True)
click.echo()
click.secho("--- DAILY PHRASES ---", fg='green')
for day in DAY_NAMES:
phrase = creds.phrases[day]
click.echo(f" {day:9} | ", nl=False)
click.secho(phrase, fg='bright_white')
click.secho("--- PASSPHRASE ---", fg='green')
click.secho(f" {creds.passphrase}", fg='bright_white', bold=True)
click.echo()
if creds.rsa_key_pem:
@@ -193,13 +201,16 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
click.echo()
click.secho("--- SECURITY ---", fg='green')
click.echo(f" Phrase entropy: {creds.phrase_entropy} bits")
click.echo(f" Passphrase entropy: {creds.passphrase_entropy} bits ({words} words)")
if creds.pin:
click.echo(f" PIN entropy: {creds.pin_entropy} bits")
click.echo(f" PIN entropy: {creds.pin_entropy} bits")
if creds.rsa_key_pem:
click.echo(f" RSA entropy: {creds.rsa_entropy} bits")
click.echo(f" Combined: {creds.total_entropy} bits")
click.secho(f" + photo entropy: 80-256 bits", dim=True)
click.echo(f" RSA entropy: {creds.rsa_entropy} bits")
click.echo(f" Combined: {creds.total_entropy} bits")
click.secho(f" + photo entropy: 80-256 bits", dim=True)
click.echo()
click.secho("NOTE: v3.2.0 removed date dependency - use this passphrase anytime!", fg='cyan')
click.echo()
except Exception as e:
@@ -216,13 +227,12 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
@click.option('--message', '-m', help='Text message to encode')
@click.option('--message-file', '-f', type=click.Path(exists=True), help='Read text message from file')
@click.option('--embed-file', '-e', type=click.Path(exists=True), help='Embed a file (binary)')
@click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--passphrase', '-p', required=True, help='Passphrase (v3.2.0: no date needed!)')
@click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
@click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)')
@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb',
help='Embedding mode: lsb (default, color) or dct (requires scipy)')
@click.option('--dct-format', 'dct_output_format', type=click.Choice(['png', 'jpeg']), default='png',
@@ -230,20 +240,22 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
@click.option('--dct-color', 'dct_color_mode', type=click.Choice(['grayscale', 'color']), default='grayscale',
help='DCT color mode: grayscale (default) or color (preserves original colors)')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_qr,
key_password, output, date_str, embed_mode, dct_output_format, dct_color_mode, quiet):
def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, key, key_qr,
key_password, output, embed_mode, dct_output_format, dct_color_mode, quiet):
"""
Encode a secret message or file into an image.
Requires a reference photo, carrier image, and day phrase.
Requires a reference photo, carrier image, and passphrase.
Must provide either --pin or --key/--key-qr (or both).
v3.2.0: No --date parameter needed! Encode and decode anytime.
For text messages, use -m or -f or pipe via stdin.
For binary files, use -e/--embed-file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
\b
Embedding Modes (v3.0):
Embedding Modes:
--mode lsb Spatial LSB embedding (default)
- Full color output (PNG/BMP)
- Higher capacity (~375 KB/megapixel)
@@ -254,7 +266,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
- Better resistance to visual analysis
\b
DCT Options (v3.0.1):
DCT Options:
--dct-format png Lossless output (default)
--dct-format jpeg Smaller file, more natural appearance
@@ -264,18 +276,14 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
\b
Examples:
# Text message with PIN (LSB mode, default)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest" --pin 123456 -m "secret"
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" --pin 123456 -m "secret"
# DCT mode - grayscale PNG (traditional)
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" --mode dct
stegasoo encode -r photo.jpg -c meme.png -p "secure words here now" --pin 123456 -m "secret" --mode dct
# DCT mode - color JPEG (v3.0.1)
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" \
# DCT mode - color JPEG
stegasoo encode -r photo.jpg -c meme.png -p "my strong passphrase" --pin 123456 -m "secret" \
--mode dct --dct-color color --dct-format jpeg
# DCT mode - color PNG (best quality + color preservation)
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "secret" \
--mode dct --dct-color color --dct-format png
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -365,15 +373,15 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})"
click.echo(f"Mode: {mode_desc} ({fit_check['usage_percent']:.1f}% capacity)")
# v3.2.0: No date_str parameter
result = encode(
message=payload,
reference_photo=ref_photo,
carrier_image=carrier_image,
day_phrase=phrase,
passphrase=passphrase, # Renamed from day_phrase
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
date_str=date_str,
embed_mode=embed_mode,
dct_output_format=dct_output_format,
dct_color_mode=dct_color_mode,
@@ -389,15 +397,15 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
out_path.write_bytes(result.stego_image)
if not quiet:
click.secho(f"[OK] Encoded successfully!", fg='green')
click.secho(f" Encoded successfully!", fg='green')
click.echo(f" Output: {out_path}")
click.echo(f" Size: {len(result.stego_image):,} bytes")
click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
click.echo(f" Date: {result.date_used}")
if embed_mode == 'dct':
color_note = "color preserved" if dct_color_mode == 'color' else "grayscale"
format_note = dct_output_format.upper()
click.secho(f" DCT output: {format_note} ({color_note})", dim=True)
click.secho(" (v3.2.0: No date needed to decode!)", fg='cyan', dim=True)
except StegasooError as e:
raise click.ClickException(str(e))
@@ -414,7 +422,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
@cli.command()
@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo')
@click.option('--stego', '-s', required=True, type=click.Path(exists=True), help='Stego image')
@click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--passphrase', '-p', required=True, help='Passphrase')
@click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@@ -424,7 +432,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key
help='Extraction mode: auto (default), lsb, or dct')
@click.option('--quiet', '-q', is_flag=True, help='Output only the content (for text) or suppress messages (for files)')
@click.option('--force', is_flag=True, help='Overwrite existing output file')
def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed_mode, quiet, force):
def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, embed_mode, quiet, force):
"""
Decode a secret message or file from a stego image.
@@ -432,11 +440,13 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
Automatically detects whether content is text or a file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
v3.2.0: No --date parameter needed! Just use your passphrase.
Note: Extraction works the same regardless of whether the image was
created with color mode or grayscale mode - both use the same Y channel.
\b
Extraction Modes (v3.0):
Extraction Modes:
--mode auto Auto-detect (default) - tries LSB first, then DCT
--mode lsb Only try LSB extraction
--mode dct Only try DCT extraction (requires scipy)
@@ -444,16 +454,16 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
\b
Examples:
# Decode with PIN (auto-detect mode)
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" --pin 123456
# Explicitly specify DCT mode
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 --mode dct
stegasoo decode -r photo.jpg -s stego.png -p "my passphrase here" --pin 123456 --mode dct
# Decode with RSA key file
stegasoo decode -r photo.jpg -s stego.png -p "words" -k mykey.pem
stegasoo decode -r photo.jpg -s stego.png -p "strong words" -k mykey.pem
# Save output to file
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o output.txt
stegasoo decode -r photo.jpg -s stego.png -p "passphrase" --pin 123456 -o output.txt
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -495,10 +505,11 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
ref_photo = Path(ref).read_bytes()
stego_image = Path(stego).read_bytes()
# v3.2.0: No date_str parameter
result = decode(
stego_image=stego_image,
reference_photo=ref_photo,
day_phrase=phrase,
passphrase=passphrase, # Renamed from day_phrase
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
@@ -522,7 +533,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
out_path.write_bytes(result.file_data)
if not quiet:
click.secho("[OK] Decoded file successfully!", fg='green')
click.secho(" Decoded file successfully!", fg='green')
click.echo(f" Saved to: {out_path}")
click.echo(f" Size: {len(result.file_data):,} bytes")
if result.mime_type:
@@ -532,13 +543,13 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
if output:
Path(output).write_text(result.message)
if not quiet:
click.secho("[OK] Decoded successfully!", fg='green')
click.secho(" Decoded successfully!", fg='green')
click.echo(f" Saved to: {output}")
else:
if quiet:
click.echo(result.message)
else:
click.secho("[OK] Decoded successfully!", fg='green')
click.secho(" Decoded successfully!", fg='green')
click.echo()
click.echo(result.message)
@@ -557,7 +568,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
@cli.command()
@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo')
@click.option('--stego', '-s', required=True, type=click.Path(exists=True), help='Stego image')
@click.option('--phrase', '-p', required=True, help='Day phrase')
@click.option('--passphrase', '-p', required=True, help='Passphrase')
@click.option('--pin', help='Static PIN')
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@@ -565,7 +576,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_qr, key_password, output, embed
@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_json):
def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, as_json):
"""
Verify that a stego image can be decoded without extracting the message.
@@ -574,11 +585,11 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
\b
Examples:
stegasoo verify -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
stegasoo verify -r photo.jpg -s stego.png -p "my passphrase" --pin 123456
stegasoo verify -r photo.jpg -s stego.png -p "words" -k mykey.pem --json
stegasoo verify -r photo.jpg -s stego.png -p "words here" -k mykey.pem --json
stegasoo verify -r photo.jpg -s stego.png -p "words" --pin 123456 --mode dct
stegasoo verify -r photo.jpg -s stego.png -p "test phrase" --pin 123456 --mode dct
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -620,7 +631,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
result = decode(
stego_image=stego_image,
reference_photo=ref_photo,
day_phrase=phrase,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase
pin=pin or "",
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
@@ -639,10 +650,6 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
payload_type = "text"
payload_desc = f"{payload_size} bytes"
# Get date info
date_encoded = result.date_encoded
day_name = get_day_from_date(date_encoded) if date_encoded else None
if as_json:
import json
output = {
@@ -650,19 +657,15 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
"stego_file": stego,
"payload_type": payload_type,
"payload_size": payload_size,
"date_encoded": date_encoded,
"day_encoded": day_name,
}
if result.is_file:
output["filename"] = result.filename
output["mime_type"] = result.mime_type
click.echo(json.dumps(output, indent=2))
else:
click.secho("[OK] Valid stego image", fg='green', bold=True)
click.secho(" Valid stego image", fg='green', bold=True)
click.echo(f" Payload: {payload_type} ({payload_desc})")
click.echo(f" Size: {payload_size:,} bytes")
if date_encoded:
click.echo(f" Encoded: {date_encoded} ({day_name})")
except (DecryptionError, ExtractionError) as e:
if as_json:
@@ -675,7 +678,7 @@ def verify(ref, stego, phrase, pin, key, key_qr, key_password, embed_mode, as_js
click.echo(json.dumps(output, indent=2))
sys.exit(1)
else:
click.secho("[FAIL] Verification failed", fg='red', bold=True)
click.secho(" Verification failed", fg='red', bold=True)
click.echo(f" Error: {e}")
sys.exit(1)
except StegasooError as e:
@@ -695,8 +698,7 @@ def info(image, as_json):
"""
Show information about an image.
Displays dimensions, capacity for both LSB and DCT modes,
and attempts to detect date from filename.
Displays dimensions, capacity for both LSB and DCT modes.
"""
try:
image_data = Path(image).read_bytes()
@@ -708,10 +710,6 @@ def info(image, as_json):
# Get capacity comparison
comparison = compare_modes(image_data)
# Try to get date from filename
date_str = parse_date_from_filename(image)
day_name = get_day_from_date(date_str) if date_str else None
if as_json:
import json
output = {
@@ -736,9 +734,6 @@ def info(image, as_json):
},
},
}
if date_str:
output["embed_date"] = date_str
output["embed_day"] = day_name
click.echo(json.dumps(output, indent=2))
return
@@ -753,17 +748,13 @@ def info(image, as_json):
click.secho(" Capacity:", bold=True)
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
dct_status = "[OK]" if comparison['dct']['available'] else "[X] (scipy not installed)"
dct_status = "" if comparison['dct']['available'] else " (scipy not installed)"
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}")
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB")
if comparison['dct']['available']:
click.secho(" DCT options: grayscale/color, png/jpeg", dim=True)
if date_str:
click.echo()
click.echo(f" Embed date: {date_str} ({day_name})")
click.echo()
except Exception as e:
@@ -839,7 +830,7 @@ def compare(image, payload_size, as_json):
click.secho(" +--- LSB Mode ---", fg='green')
click.echo(f" | Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
click.echo(f" | Output: {comparison['lsb']['output']}")
click.echo(f" | Status: [OK] Available")
click.echo(f" | Status: Available")
click.echo(" |")
# DCT mode
@@ -847,11 +838,11 @@ def compare(image, payload_size, as_json):
click.echo(f" | Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)")
click.echo(f" | Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
if comparison['dct']['available']:
click.echo(f" | Status: [OK] Available")
click.echo(f" | Status: Available")
click.echo(f" | Formats: PNG (lossless), JPEG (smaller)")
click.echo(f" | Colors: Grayscale (default), Color (v3.0.1)")
else:
click.secho(f" | Status: [X] Requires scipy (pip install scipy)", fg='yellow')
click.secho(f" | Status: Requires scipy (pip install scipy)", fg='yellow')
click.echo(" |")
# Payload check
@@ -862,8 +853,8 @@ def compare(image, payload_size, as_json):
fits_lsb = payload_size <= comparison['lsb']['capacity_bytes']
fits_dct = payload_size <= comparison['dct']['capacity_bytes']
lsb_icon = "[OK]" if fits_lsb else "[X]"
dct_icon = "[OK]" if fits_dct else "[X]"
lsb_icon = "" if fits_lsb else ""
dct_icon = "" if fits_dct else ""
lsb_color = 'green' if fits_lsb else 'red'
dct_color = 'green' if fits_dct else 'red'
@@ -884,7 +875,7 @@ def compare(image, payload_size, as_json):
elif fits_lsb:
click.echo(" LSB mode (payload too large for DCT)")
else:
click.secho(" [X] Payload too large for both modes!", fg='red')
click.secho(" Payload too large for both modes!", fg='red')
else:
click.echo(" LSB for larger payloads, DCT for better stealth")
click.echo(" DCT supports color output with --dct-color color")
@@ -931,7 +922,7 @@ def strip_metadata_cmd(image, output, output_format, quiet):
out_path.write_bytes(clean_data)
if not quiet:
click.secho("[OK] Metadata stripped", fg='green')
click.secho(" Metadata stripped", fg='green')
click.echo(f" Input: {image} ({original_size:,} bytes)")
click.echo(f" Output: {out_path} ({len(clean_data):,} bytes)")
@@ -951,12 +942,12 @@ def modes():
Displays which modes are available and their characteristics.
"""
click.echo()
click.secho("=== Stegasoo Embedding Modes ===", fg='cyan', bold=True)
click.secho("=== Stegasoo Embedding Modes (v3.2.0) ===", fg='cyan', bold=True)
click.echo()
# LSB Mode
click.secho(" LSB Mode (Spatial LSB)", fg='green', bold=True)
click.echo(" Status: [OK] Always available")
click.echo(" Status: Always available")
click.echo(" Output: PNG/BMP (full color)")
click.echo(" Capacity: ~375 KB per megapixel")
click.echo(" Use case: Larger payloads, color preservation")
@@ -966,16 +957,16 @@ def modes():
# DCT Mode
click.secho(" DCT Mode (Frequency Domain)", fg='blue', bold=True)
if has_dct_support():
click.echo(" Status: [OK] Available")
click.echo(" Status: Available")
else:
click.secho(" Status: [X] Requires scipy", fg='yellow')
click.secho(" Status: Requires scipy", fg='yellow')
click.echo(" Install: pip install scipy")
click.echo(" Capacity: ~75 KB per megapixel (~20% of LSB)")
click.echo(" Use case: Better stealth, frequency domain hiding")
click.echo(" CLI flag: --mode dct")
click.echo()
# DCT Options (v3.0.1)
# DCT Options
click.secho(" DCT Options (v3.0.1)", fg='magenta', bold=True)
click.echo(" Output format:")
click.echo(" --dct-format png Lossless, larger file (default)")
@@ -986,6 +977,13 @@ def modes():
click.echo(" --dct-color color Preserves original colors")
click.echo()
# v3.2.0 Note
click.secho(" v3.2.0 Changes:", fg='cyan', bold=True)
click.echo(" ✓ No date parameters needed")
click.echo(" ✓ Single passphrase (no daily rotation)")
click.echo(" ✓ True asynchronous communications")
click.echo()
# Examples
click.secho(" Examples:", dim=True)
click.echo(" # Traditional DCT (grayscale PNG)")