3.2.0 Big revamp
This commit is contained in:
500
frontends/api/API_UPDATE_SUMMARY_V3.2.0.md
Normal file
500
frontends/api/API_UPDATE_SUMMARY_V3.2.0.md
Normal 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!
|
||||
@@ -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)",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user