Compare commits
418 Commits
2_2_1
...
worktree-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1630d044aa | ||
|
|
c2a0a731d7 | ||
|
|
89de839fd8 | ||
|
|
49566292ba | ||
|
|
9f0e0afeb6 | ||
|
|
398a359778 | ||
|
|
86aa5cbddf | ||
|
|
2f54f80214 | ||
|
|
1cd2656e60 | ||
|
|
ce728cec6e | ||
|
|
555735a4fd | ||
|
|
08b70043e4 | ||
|
|
d395e5731e | ||
|
|
110b160e68 | ||
|
|
b09f607d34 | ||
|
|
34ede3815f | ||
|
|
3b5ab41ce9 | ||
|
|
525bcec3c9 | ||
|
|
afc8c93923 | ||
|
|
38bef32750 | ||
|
|
4e3acfca20 | ||
|
|
2ebc42f2cd | ||
|
|
1e07630b49 | ||
|
|
67037ae196 | ||
|
|
5a68840725 | ||
|
|
ebc999b2b3 | ||
|
|
f46ef01f5f | ||
|
|
0d76780deb | ||
|
|
d34919e32f | ||
|
|
a4038589b0 | ||
|
|
db763f1464 | ||
|
|
27c5b08d41 | ||
|
|
28cb9bb9b3 | ||
|
|
889df881ba | ||
|
|
c058d116b8 | ||
|
|
fae86887e2 | ||
|
|
5e45b2c5c1 | ||
|
|
71088989f3 | ||
|
|
530e5debef | ||
|
|
3b062458e3 | ||
|
|
5e65035ca4 | ||
|
|
de9d1de881 | ||
|
|
8d90a888cf | ||
|
|
b0914778e3 | ||
|
|
7e5462ea6e | ||
|
|
e085a8ffe9 | ||
|
|
2d7fbd1e0d | ||
|
|
32842f6b73 | ||
|
|
3fd3204552 | ||
|
|
175362ce4c | ||
|
|
2ed108f3a0 | ||
|
|
167e1a6ff5 | ||
|
|
f2f3e2eefc | ||
|
|
5c685cba67 | ||
|
|
4e819b80cc | ||
|
|
ea86216648 | ||
|
|
8de5659fa6 | ||
|
|
de0bf2410d | ||
|
|
8b948d00a4 | ||
|
|
6d88453b69 | ||
|
|
ea57bdf302 | ||
|
|
55d54717f8 | ||
|
|
c0fe85ac83 | ||
|
|
e9e4d1aab9 | ||
|
|
1acb5a3dcc | ||
|
|
14a73c63ac | ||
|
|
3d53282738 | ||
|
|
e831ae4884 | ||
|
|
4751d05e9f | ||
|
|
d15bcb8df4 | ||
|
|
6ec7de5604 | ||
|
|
1cdb2aca91 | ||
|
|
46de371c42 | ||
|
|
11c0d45548 | ||
|
|
7bb1029c0f | ||
|
|
e3f7f36e5e | ||
|
|
f200737088 | ||
|
|
6def318ba7 | ||
|
|
e203af6a73 | ||
|
|
6ba135098b | ||
|
|
903739c055 | ||
|
|
30fbb5016e | ||
|
|
041148e8fe | ||
|
|
90bedce379 | ||
|
|
021265f3cf | ||
|
|
ff42398509 | ||
|
|
a30ec33b98 | ||
|
|
252efbec7e | ||
|
|
6e906d5981 | ||
|
|
df6125d098 | ||
|
|
3d4a340305 | ||
|
|
0decb39b17 | ||
|
|
4291dfad38 | ||
|
|
ddee3583e8 | ||
|
|
3e2307cbcf | ||
|
|
cc745fbdfa | ||
|
|
3027706d49 | ||
|
|
39fbd617e6 | ||
|
|
de4cb0b3be | ||
|
|
add3951003 | ||
|
|
3858e234da | ||
|
|
03e8e3a840 | ||
|
|
55e78d0503 | ||
|
|
b13a9fcd3f | ||
|
|
96b49c68ec | ||
|
|
be8744179d | ||
|
|
f971b75d7e | ||
|
|
455c6dfd01 | ||
|
|
a00a154a1a | ||
|
|
8b3b331843 | ||
|
|
10c874374f | ||
|
|
0c1e87c7c0 | ||
|
|
d517a4dc8b | ||
|
|
6d59f3edfc | ||
|
|
17d0406be2 | ||
|
|
ef73280015 | ||
|
|
6338d6aab4 | ||
|
|
b9d0fac535 | ||
|
|
5c0a5bbba7 | ||
|
|
ba1a77f00b | ||
|
|
5e587df545 | ||
|
|
23456ac1e4 | ||
|
|
8be512ad7b | ||
|
|
f129500202 | ||
|
|
c37d743b3e | ||
|
|
5bdb625059 | ||
|
|
231ba97fde | ||
|
|
a70e88625f | ||
|
|
b6770c46e5 | ||
|
|
9f4318cc0f | ||
|
|
91dc665a77 | ||
|
|
6066df391b | ||
|
|
be5c95b59d | ||
|
|
09b1abddc7 | ||
|
|
0c9ea0e3f2 | ||
|
|
aebfb20dfc | ||
|
|
b935c474af | ||
|
|
73b34ba8b5 | ||
|
|
89d8fee5da | ||
|
|
0e270dadb3 | ||
|
|
e2002b6026 | ||
|
|
66ed11fb97 | ||
|
|
9cbb4600f8 | ||
|
|
c1c850c593 | ||
|
|
e029f00d66 | ||
|
|
34e417fb55 | ||
|
|
e7954c63e4 | ||
|
|
446789a16f | ||
|
|
2538126573 | ||
|
|
a91d127ed7 | ||
|
|
a0781b1cf7 | ||
|
|
5e32ecb35a | ||
|
|
3e5de98f60 | ||
|
|
c8956b9e43 | ||
|
|
a8f15f87c6 | ||
|
|
8a64db9fcc | ||
|
|
ab450955fe | ||
|
|
afd502dbf3 | ||
|
|
3f02e55ffd | ||
|
|
2ee824b02b | ||
|
|
189620e4fb | ||
|
|
ecad88e859 | ||
|
|
62bd31d0aa | ||
|
|
241cdadd25 | ||
|
|
85309a2044 | ||
|
|
a81a20f8ee | ||
|
|
9c88f53cd0 | ||
|
|
3f8c2a6957 | ||
|
|
22cf27d7f6 | ||
|
|
4d8575ce33 | ||
|
|
28b539bcd9 | ||
|
|
6b82069dc8 | ||
|
|
52e1a3dfbf | ||
|
|
4a27d0c182 | ||
|
|
36931518ce | ||
|
|
f79c63428b | ||
|
|
cc29de4200 | ||
|
|
c14f3f75cb | ||
|
|
aa99a258f4 | ||
|
|
93420704e8 | ||
|
|
6e4eb5464e | ||
|
|
d04670e352 | ||
|
|
fda1cdad51 | ||
|
|
b48ccc5d16 | ||
|
|
15ed63cafa | ||
|
|
869d7ee8e3 | ||
|
|
3ee8c1d22a | ||
|
|
b96564358a | ||
|
|
01afb3da66 | ||
|
|
a98df5f9a0 | ||
|
|
70da348bce | ||
|
|
90ba8543a7 | ||
|
|
da3aea992c | ||
|
|
ae47ff4932 | ||
|
|
eb16eb1db2 | ||
|
|
c65d9e6682 | ||
|
|
eeb44eae94 | ||
|
|
26d4b82c91 | ||
|
|
7efeaf02e8 | ||
|
|
925fb05cbd | ||
|
|
29a02265a1 | ||
|
|
d58f3c6fb6 | ||
|
|
cc46993d80 | ||
|
|
893a044eaa | ||
|
|
9f03b69408 | ||
|
|
cce2007c6e | ||
|
|
52f43d3a86 | ||
|
|
85a7092d55 | ||
|
|
4b37a81087 | ||
|
|
31941dc3f5 | ||
|
|
9a7e4ddce7 | ||
|
|
0424dd34d5 | ||
|
|
2127b916f3 | ||
|
|
f8e65890e5 | ||
|
|
5861ab0e1e | ||
|
|
5309a08aaf | ||
|
|
d8fb95b68e | ||
|
|
c0b6865790 | ||
|
|
6e7ae0d6f9 | ||
|
|
6a5b12f98e | ||
|
|
d8eb7b0160 | ||
|
|
962c04084b | ||
|
|
597a9c6411 | ||
|
|
67b25a43a6 | ||
|
|
65a663fe3b | ||
|
|
fc6e4eb805 | ||
|
|
50f07a0ce9 | ||
|
|
7accd26821 | ||
|
|
075e10792c | ||
|
|
9a790de5c3 | ||
|
|
3c91c92a4d | ||
|
|
9d1bc7f829 | ||
|
|
d8118d688b | ||
|
|
b6acee1acb | ||
|
|
b9baf35dfa | ||
|
|
561f03ffde | ||
|
|
038347a505 | ||
|
|
e026d1a4db | ||
|
|
3f93e7a752 | ||
|
|
cdc7ffd3bf | ||
|
|
6c3bc995f1 | ||
|
|
2d3ed8a79a | ||
|
|
040c44fec6 | ||
|
|
832d8be025 | ||
|
|
7088623d2c | ||
|
|
44a3ca8a0f | ||
|
|
7a35ac3df7 | ||
|
|
f69475b406 | ||
|
|
559dcd3dcf | ||
|
|
b1ddfaa75b | ||
|
|
4843ec8c22 | ||
|
|
ac08011236 | ||
|
|
12c4b091fb | ||
|
|
c2c2c924e1 | ||
|
|
df7ad06a08 | ||
|
|
166b936ee5 | ||
|
|
7138455f8d | ||
|
|
9ab3260298 | ||
|
|
763f7bf603 | ||
|
|
1059e17f4e | ||
|
|
7cb42e189a | ||
|
|
8c283bc4e5 | ||
|
|
664362bea5 | ||
|
|
4733e3b4dd | ||
|
|
24aec00613 | ||
|
|
0e0aa996bc | ||
|
|
255ae4f30d | ||
|
|
7647ca11d1 | ||
|
|
01e9e5af0a | ||
|
|
39e5daa022 | ||
|
|
54e097c050 | ||
|
|
a3ff8dace1 | ||
|
|
e4cf96bb7c | ||
|
|
597c95070c | ||
|
|
dba5a08476 | ||
|
|
6ceda6c287 | ||
|
|
c2575f973b | ||
|
|
8208ec2955 | ||
|
|
909dc14a92 | ||
|
|
bb91e41d3d | ||
|
|
c54a96894c | ||
|
|
da044017d7 | ||
|
|
d0ec99d5b5 | ||
|
|
aac8037c04 | ||
|
|
7a5092b945 | ||
|
|
e52a709080 | ||
|
|
70fe8fce62 | ||
|
|
d44575deec | ||
|
|
d0d48236ff | ||
|
|
5891285493 | ||
|
|
5501c7e0ba | ||
|
|
038fd6ceac | ||
|
|
8622f1a850 | ||
|
|
710b3a6a98 | ||
|
|
c965a5f8da | ||
|
|
00cda4d929 | ||
|
|
05e2286d02 | ||
|
|
46cbf98a23 | ||
|
|
58673c04fe | ||
|
|
dd07972014 | ||
|
|
1f40eeff9e | ||
|
|
dc09bac489 | ||
|
|
46489dd276 | ||
|
|
9088caa23d | ||
|
|
75b6203525 | ||
|
|
404d7885f4 | ||
|
|
a8db991052 | ||
|
|
ea2948e5d2 | ||
|
|
05278ca55f | ||
|
|
c551078c37 | ||
|
|
b7d86201ca | ||
|
|
07b0bc0b75 | ||
|
|
d8b8e4f5c2 | ||
|
|
143a8bdc65 | ||
|
|
ac92fa36b5 | ||
|
|
c82dcf26f2 | ||
|
|
65a496a9d4 | ||
|
|
25a432fcf3 | ||
|
|
a58dd54ba8 | ||
|
|
05c542d808 | ||
|
|
5e5d6e60de | ||
|
|
d898f6d7b1 | ||
|
|
00dd15b8fb | ||
|
|
419b491737 | ||
|
|
b568026253 | ||
|
|
127d3e54a6 | ||
|
|
de41c0731e | ||
|
|
f3d5699e15 | ||
|
|
298f387c9a | ||
|
|
fcb71303df | ||
|
|
abcff74dd4 | ||
|
|
355a988405 | ||
|
|
fb55878727 | ||
|
|
81d3f37f09 | ||
|
|
3537e8cdf9 | ||
|
|
d71f615d66 | ||
|
|
ed1d230b4e | ||
|
|
13f145c3d5 | ||
|
|
80dc22f150 | ||
|
|
01f0173dd4 | ||
|
|
5df9b9dac8 | ||
|
|
2f1ac3a747 | ||
|
|
8e5f01754f | ||
|
|
823b8824ea | ||
|
|
f4c1aa1912 | ||
|
|
e502f42fb8 | ||
|
|
08e42719ee | ||
|
|
21023099b0 | ||
|
|
8a41796d1b | ||
|
|
7b33501495 | ||
|
|
a8f6ae1dd2 | ||
|
|
b199f03f83 | ||
|
|
b97622956c | ||
|
|
3044c08fe3 | ||
|
|
5042c7d555 | ||
|
|
aa8788168e | ||
|
|
899d043892 | ||
|
|
6fb63edc61 | ||
|
|
e74f12c24d | ||
|
|
272d0e6ef0 | ||
|
|
f38bf4a1c6 | ||
|
|
fee3133f9c | ||
|
|
b058d8bf66 | ||
|
|
916a2e0e7b | ||
|
|
cccb40dc3a | ||
|
|
b60880c8b3 | ||
|
|
c96c595c78 | ||
|
|
e129c38fd8 | ||
|
|
0d7b5a14cb | ||
|
|
45b99d2c5e | ||
|
|
c6f816d61f | ||
|
|
83e9bd6fa1 | ||
|
|
5188492c77 | ||
|
|
8bb70e5667 | ||
|
|
82ac1dcda4 | ||
|
|
464e13567d | ||
|
|
0b19a41b5e | ||
|
|
61c5178752 | ||
|
|
6b1b306f61 | ||
|
|
267547caba | ||
|
|
2ff28034f5 | ||
|
|
4cba75fe06 | ||
|
|
d03b3dea4b | ||
|
|
cf247d207f | ||
|
|
28d77957eb | ||
|
|
89b4809489 | ||
|
|
79ab165b95 | ||
|
|
4194d6923a | ||
|
|
08e19a3bfd | ||
|
|
dea7472018 | ||
|
|
e8863d15d7 | ||
|
|
e4256cd037 | ||
|
|
948a582e5d | ||
|
|
afa88bc73b | ||
|
|
221678d934 | ||
|
|
faf3efac0b | ||
|
|
9c45e0d0f8 | ||
|
|
6b21190f97 | ||
|
|
d94ee7be90 | ||
|
|
6fa4b447db | ||
|
|
1bb3589baf | ||
|
|
cfd1d8fb66 | ||
|
|
c1beaf3611 | ||
|
|
ef7478b30a | ||
|
|
12929bf326 | ||
|
|
a001f227ec | ||
|
|
3898031480 | ||
|
|
657cae0ae6 | ||
|
|
11fc8aab27 | ||
|
|
6d64c69f08 | ||
|
|
6bd38ccf57 | ||
|
|
d8fe25a121 | ||
|
|
3869391336 | ||
|
|
1b914f0409 | ||
|
|
2b7abc52c1 | ||
|
|
66f7d54db5 | ||
|
|
34376b2dfe | ||
|
|
4eefc946c4 |
52
.dockerignore
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info
|
||||||
|
.eggs
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Instance data (user creates fresh)
|
||||||
|
frontends/web/instance/
|
||||||
|
frontends/web/certs/
|
||||||
|
instance/
|
||||||
|
|
||||||
|
# Test data
|
||||||
|
test_data/
|
||||||
|
tests/
|
||||||
|
|
||||||
|
# Pi-specific
|
||||||
|
rpi/
|
||||||
|
*.img
|
||||||
|
*.img.xz
|
||||||
|
*.img.zst
|
||||||
|
*.img.zst.zip
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
*.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Dev scripts and old files
|
||||||
|
scripts/
|
||||||
|
old_files/
|
||||||
|
*_old
|
||||||
|
*_old.*
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
frontends/web/temp_files/
|
||||||
|
*.db
|
||||||
98
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report a bug or unexpected behavior
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug", "triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to report a bug! Please fill out the form below.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Bug Description
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
placeholder: Describe the bug...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. Run command '...'
|
||||||
|
2. Upload image '...'
|
||||||
|
3. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
description: What actually happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: interface
|
||||||
|
attributes:
|
||||||
|
label: Interface
|
||||||
|
description: Which interface are you using?
|
||||||
|
options:
|
||||||
|
- CLI
|
||||||
|
- Web UI
|
||||||
|
- REST API
|
||||||
|
- Python Library
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Stegasoo Version
|
||||||
|
description: Run `stegasoo --version` or check the web UI footer
|
||||||
|
placeholder: "4.0.1"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: python-version
|
||||||
|
attributes:
|
||||||
|
label: Python Version
|
||||||
|
description: Run `python --version`
|
||||||
|
placeholder: "3.11.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
placeholder: "Ubuntu 22.04 / Windows 11 / macOS 14"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Error Logs
|
||||||
|
description: Paste any relevant error messages or tracebacks.
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context, screenshots, or files here.
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Security Vulnerability
|
||||||
|
url: https://github.com/adlee-was-taken/stegasoo/security/advisories/new
|
||||||
|
about: Report security vulnerabilities privately
|
||||||
|
- name: Documentation
|
||||||
|
url: https://github.com/adlee-was-taken/stegasoo#readme
|
||||||
|
about: Check the documentation before opening an issue
|
||||||
62
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or enhancement
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for suggesting a feature! Please fill out the form below.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem Statement
|
||||||
|
description: Is your feature request related to a problem? Describe it.
|
||||||
|
placeholder: I'm always frustrated when...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Proposed Solution
|
||||||
|
description: Describe the solution you'd like.
|
||||||
|
placeholder: I would like to be able to...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives Considered
|
||||||
|
description: Describe any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: interface
|
||||||
|
attributes:
|
||||||
|
label: Affected Interface(s)
|
||||||
|
description: Which interface(s) would this feature affect?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- CLI
|
||||||
|
- Web UI
|
||||||
|
- REST API
|
||||||
|
- Python Library
|
||||||
|
- Core Library
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority
|
||||||
|
attributes:
|
||||||
|
label: Priority
|
||||||
|
description: How important is this feature to you?
|
||||||
|
options:
|
||||||
|
- Nice to have
|
||||||
|
- Would improve my workflow
|
||||||
|
- Critical for my use case
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context, mockups, or examples here.
|
||||||
46
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- Describe your changes in detail -->
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
|
||||||
|
<!-- Mark the relevant option with an 'x' -->
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change that adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||||
|
- [ ] Documentation update
|
||||||
|
- [ ] Refactoring (no functional changes)
|
||||||
|
- [ ] CI/CD or build changes
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
<!-- Link any related issues here -->
|
||||||
|
|
||||||
|
Fixes #
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
<!-- Describe how you tested your changes -->
|
||||||
|
|
||||||
|
- [ ] I have added tests that prove my fix/feature works
|
||||||
|
- [ ] Existing tests pass locally with my changes
|
||||||
|
- [ ] I have tested manually (describe below)
|
||||||
|
|
||||||
|
### Manual Testing Steps
|
||||||
|
|
||||||
|
<!-- If applicable, describe manual testing performed -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] My code follows the project's style guidelines
|
||||||
|
- [ ] I have performed a self-review of my code
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] I have updated the documentation accordingly
|
||||||
|
- [ ] I have updated CHANGELOG.md (if user-facing changes)
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
- [ ] Any dependent changes have been merged and published
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<!-- If applicable, add screenshots to help explain your changes -->
|
||||||
3
.github/workflows/release.yml
vendored
@@ -37,7 +37,8 @@ jobs:
|
|||||||
publish:
|
publish:
|
||||||
needs: test # Only run if tests pass
|
needs: test # Only run if tests pass
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment: pypi
|
||||||
|
|
||||||
# Required for PyPI trusted publishing (recommended)
|
# Required for PyPI trusted publishing (recommended)
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false # Don't cancel other jobs if one fails
|
fail-fast: false # Don't cancel other jobs if one fails
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
python-version: ["3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# 1. Get the code
|
# 1. Get the code
|
||||||
|
|||||||
52
.gitignore
vendored
@@ -35,6 +35,12 @@ old_files/
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*_old
|
||||||
|
*_old.*
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.coverage
|
.coverage
|
||||||
@@ -48,7 +54,7 @@ htmlcov/
|
|||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.local
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Distribution
|
# Distribution
|
||||||
@@ -58,6 +64,44 @@ htmlcov/
|
|||||||
# Output test files.
|
# Output test files.
|
||||||
test_data/*.png
|
test_data/*.png
|
||||||
|
|
||||||
#Project root scripts.
|
# Dev scripts (local convenience scripts - except these)
|
||||||
rbld_containers.sh
|
scripts/*
|
||||||
quick_web.sh
|
!scripts/validate-release.sh
|
||||||
|
!scripts/smoke-test.sh
|
||||||
|
!scripts/setup-trusted-certs.sh
|
||||||
|
!scripts/screenshots.sh
|
||||||
|
!scripts/build.sh
|
||||||
|
|
||||||
|
# Web UI auth database and SSL certs
|
||||||
|
instance/
|
||||||
|
frontends/web/instance/
|
||||||
|
frontends/web/certs/
|
||||||
|
|
||||||
|
# Tests (private)
|
||||||
|
tests/
|
||||||
|
|
||||||
|
# RPi image build artifacts
|
||||||
|
*.img
|
||||||
|
*.img.xz
|
||||||
|
*.img.zst
|
||||||
|
*.img.zst.zip
|
||||||
|
rpi/tools/pishrink.sh
|
||||||
|
|
||||||
|
# Temp file storage
|
||||||
|
frontends/web/temp_files/
|
||||||
|
rpi/config.json
|
||||||
|
|
||||||
|
# Pre-built Pi tarballs and images (release assets, too large for git)
|
||||||
|
rpi/*.tar.zst
|
||||||
|
rpi/*.tar.zst.zip
|
||||||
|
rpi/*.img
|
||||||
|
rpi/*.img.zst
|
||||||
|
rpi/*.img.zst.zip
|
||||||
|
|
||||||
|
# AUR build artifacts
|
||||||
|
aur-upload/
|
||||||
|
aur/.SRCINFO
|
||||||
|
aur/*.pkg.tar.zst
|
||||||
|
|
||||||
|
# Docker pre-built images and deps (release assets, too large for git)
|
||||||
|
docker/*.tar.zst
|
||||||
|
|||||||
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
882
API.md
Normal file
@@ -0,0 +1,882 @@
|
|||||||
|
# Stegasoo REST API Documentation (v4.0.2)
|
||||||
|
|
||||||
|
Complete REST API reference for Stegasoo steganography operations.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [What's New in v4.0.0](#whats-new-in-v400)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Base URL](#base-url)
|
||||||
|
- [Endpoints](#endpoints)
|
||||||
|
- [GET /](#get--status)
|
||||||
|
- [GET /modes](#get-modes)
|
||||||
|
- [GET /channel/status](#get-channelstatus)
|
||||||
|
- [POST /channel/generate](#post-channelgenerate)
|
||||||
|
- [POST /channel/set](#post-channelset)
|
||||||
|
- [DELETE /channel](#delete-channel)
|
||||||
|
- [POST /generate](#post-generate)
|
||||||
|
- [POST /encode](#post-encode-json)
|
||||||
|
- [POST /encode/file](#post-encodefile)
|
||||||
|
- [POST /encode/multipart](#post-encodemultipart)
|
||||||
|
- [POST /decode](#post-decode-json)
|
||||||
|
- [POST /decode/multipart](#post-decodemultipart)
|
||||||
|
- [POST /compare](#post-compare)
|
||||||
|
- [POST /will-fit](#post-will-fit)
|
||||||
|
- [POST /image/info](#post-imageinfo)
|
||||||
|
- [Channel Keys](#channel-keys)
|
||||||
|
- [Data Models](#data-models)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Code Examples](#code-examples)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Stegasoo REST API provides programmatic access to all steganography operations:
|
||||||
|
|
||||||
|
- **Generate** credentials (passphrase, PINs, RSA keys)
|
||||||
|
- **Encode** messages or files into images (LSB or DCT mode)
|
||||||
|
- **Decode** messages or files from images (auto-detects mode)
|
||||||
|
- **Channel keys** for deployment/group isolation (v4.0.0)
|
||||||
|
- **Analyze** image capacity and compare modes
|
||||||
|
|
||||||
|
The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's New in v4.0.0
|
||||||
|
|
||||||
|
Version 4.0.0 adds **channel key** support for deployment/group isolation:
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| Channel keys | 256-bit keys that isolate message groups |
|
||||||
|
| New endpoints | `/channel/status`, `/channel/generate`, `/channel/set`, `DELETE /channel` |
|
||||||
|
| Encode/decode param | `channel_key` parameter on all encode/decode endpoints |
|
||||||
|
| Response headers | `X-Stegasoo-Channel-Mode` and `X-Stegasoo-Channel-Fingerprint` |
|
||||||
|
|
||||||
|
**Key benefits:**
|
||||||
|
- ✅ Isolate messages between teams, deployments, or groups
|
||||||
|
- ✅ Same credentials can't decode messages from different channels
|
||||||
|
- ✅ Backward compatible (public mode = no channel key)
|
||||||
|
|
||||||
|
**Breaking change:** v4.0.0 messages (with channel key) cannot be decoded by v3.x installations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From PyPI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install stegasoo[api]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Server
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
```bash
|
||||||
|
cd frontends/api
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker with channel key:**
|
||||||
|
```bash
|
||||||
|
STEGASOO_CHANNEL_KEY=XXXX-XXXX-... docker-compose -f docker/docker-compose.yml up api
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
| Environment | URL |
|
||||||
|
|-------------|-----|
|
||||||
|
| Local Development | `http://localhost:8000` |
|
||||||
|
| Docker | `http://localhost:8000` |
|
||||||
|
| Production | Configure as needed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### GET / (Status)
|
||||||
|
|
||||||
|
Check API status and configuration.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "4.0.2",
|
||||||
|
"has_argon2": true,
|
||||||
|
"has_qrcode_read": true,
|
||||||
|
"has_dct": true,
|
||||||
|
"max_payload_kb": 500,
|
||||||
|
"available_modes": ["lsb", "dct"],
|
||||||
|
"dct_features": {
|
||||||
|
"output_formats": ["png", "jpeg"],
|
||||||
|
"color_modes": ["grayscale", "color"]
|
||||||
|
},
|
||||||
|
"channel": {
|
||||||
|
"mode": "private",
|
||||||
|
"configured": true,
|
||||||
|
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456",
|
||||||
|
"source": "~/.stegasoo/channel.key"
|
||||||
|
},
|
||||||
|
"breaking_changes": {
|
||||||
|
"v4_channel_key": "Messages encoded with channel key require same key to decode",
|
||||||
|
"format_version": 5,
|
||||||
|
"backward_compatible": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /modes
|
||||||
|
|
||||||
|
Get available embedding modes and channel status.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lsb": {
|
||||||
|
"available": true,
|
||||||
|
"name": "Spatial LSB",
|
||||||
|
"description": "Embed in pixel LSBs, outputs PNG/BMP",
|
||||||
|
"output_format": "PNG (color)",
|
||||||
|
"capacity_ratio": "100%"
|
||||||
|
},
|
||||||
|
"dct": {
|
||||||
|
"available": true,
|
||||||
|
"name": "DCT Domain",
|
||||||
|
"output_formats": ["png", "jpeg"],
|
||||||
|
"color_modes": ["grayscale", "color"],
|
||||||
|
"capacity_ratio": "~20% of LSB",
|
||||||
|
"requires": "scipy"
|
||||||
|
},
|
||||||
|
"channel": {
|
||||||
|
"mode": "private",
|
||||||
|
"configured": true,
|
||||||
|
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /channel/status
|
||||||
|
|
||||||
|
Get current channel key status. **New in v4.0.0.**
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `reveal` | boolean | `false` | Include full key in response |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "private",
|
||||||
|
"configured": true,
|
||||||
|
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456",
|
||||||
|
"source": "~/.stegasoo/channel.key",
|
||||||
|
"key": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With `reveal=true`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "private",
|
||||||
|
"configured": true,
|
||||||
|
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456",
|
||||||
|
"source": "~/.stegasoo/channel.key",
|
||||||
|
"key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### cURL Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show status
|
||||||
|
curl http://localhost:8000/channel/status
|
||||||
|
|
||||||
|
# Reveal full key
|
||||||
|
curl "http://localhost:8000/channel/status?reveal=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /channel/generate
|
||||||
|
|
||||||
|
Generate a new channel key. **New in v4.0.0.**
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `save` | boolean | `false` | Save to user config |
|
||||||
|
| `save_project` | boolean | `false` | Save to project config |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456",
|
||||||
|
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456",
|
||||||
|
"saved": true,
|
||||||
|
"save_location": "~/.stegasoo/channel.key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### cURL Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Just generate (don't save)
|
||||||
|
curl -X POST http://localhost:8000/channel/generate
|
||||||
|
|
||||||
|
# Generate and save to user config
|
||||||
|
curl -X POST "http://localhost:8000/channel/generate?save=true"
|
||||||
|
|
||||||
|
# Generate and save to project config
|
||||||
|
curl -X POST "http://localhost:8000/channel/generate?save_project=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /channel/set
|
||||||
|
|
||||||
|
Set/save a channel key to config. **New in v4.0.0.**
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456",
|
||||||
|
"location": "user"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Default | Description |
|
||||||
|
|-------|------|---------|-------------|
|
||||||
|
| `key` | string | required | Channel key |
|
||||||
|
| `location` | string | `"user"` | `"user"` or `"project"` |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"location": "~/.stegasoo/channel.key",
|
||||||
|
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DELETE /channel
|
||||||
|
|
||||||
|
Clear channel key from config. **New in v4.0.0.**
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `location` | string | `"user"` | `"user"`, `"project"`, or `"all"` |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"mode": "public",
|
||||||
|
"still_configured": false,
|
||||||
|
"remaining_source": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### cURL Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear user config
|
||||||
|
curl -X DELETE http://localhost:8000/channel
|
||||||
|
|
||||||
|
# Clear project config
|
||||||
|
curl -X DELETE "http://localhost:8000/channel?location=project"
|
||||||
|
|
||||||
|
# Clear all
|
||||||
|
curl -X DELETE "http://localhost:8000/channel?location=all"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /generate
|
||||||
|
|
||||||
|
Generate credentials for encoding/decoding.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"use_pin": true,
|
||||||
|
"use_rsa": false,
|
||||||
|
"pin_length": 6,
|
||||||
|
"rsa_bits": 2048,
|
||||||
|
"words_per_passphrase": 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"passphrase": "abandon ability able about",
|
||||||
|
"pin": "847293",
|
||||||
|
"rsa_key_pem": null,
|
||||||
|
"entropy": {
|
||||||
|
"passphrase": 44,
|
||||||
|
"pin": 19,
|
||||||
|
"rsa": 0,
|
||||||
|
"total": 63
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /encode (JSON)
|
||||||
|
|
||||||
|
Encode a text message into an image.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Secret message here",
|
||||||
|
"reference_photo_base64": "iVBORw0KGgo...",
|
||||||
|
"carrier_image_base64": "iVBORw0KGgo...",
|
||||||
|
"passphrase": "apple forest thunder mountain",
|
||||||
|
"pin": "123456",
|
||||||
|
"rsa_key_base64": null,
|
||||||
|
"rsa_password": null,
|
||||||
|
"channel_key": null,
|
||||||
|
"embed_mode": "lsb",
|
||||||
|
"dct_output_format": "png",
|
||||||
|
"dct_color_mode": "grayscale"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Channel Key Parameter (v4.0.0)
|
||||||
|
|
||||||
|
| Value | Effect |
|
||||||
|
|-------|--------|
|
||||||
|
| `null` | Auto mode - use server-configured key |
|
||||||
|
| `""` (empty string) | Public mode - no channel isolation |
|
||||||
|
| `"XXXX-XXXX-..."` | Explicit key - use this specific key |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"stego_image_base64": "iVBORw0KGgo...",
|
||||||
|
"filename": "a1b2c3d4.png",
|
||||||
|
"capacity_used_percent": 12.4,
|
||||||
|
"embed_mode": "lsb",
|
||||||
|
"output_format": "png",
|
||||||
|
"color_mode": "color",
|
||||||
|
"channel_mode": "private",
|
||||||
|
"channel_fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /encode/file
|
||||||
|
|
||||||
|
Encode a file into an image (JSON with base64).
|
||||||
|
|
||||||
|
Same parameters as `/encode`, plus:
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `file_data_base64` | string | ✓ | Base64-encoded file data |
|
||||||
|
| `filename` | string | ✓ | Original filename |
|
||||||
|
| `mime_type` | string | | MIME type |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /encode/multipart
|
||||||
|
|
||||||
|
Encode using multipart form data (file uploads).
|
||||||
|
|
||||||
|
#### Form Fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `passphrase` | string | ✓ | Passphrase |
|
||||||
|
| `reference_photo` | file | ✓ | Reference photo |
|
||||||
|
| `carrier` | file | ✓ | Carrier image |
|
||||||
|
| `message` | string | * | Text message |
|
||||||
|
| `payload_file` | file | * | Binary file to embed |
|
||||||
|
| `pin` | string | | Static PIN |
|
||||||
|
| `rsa_key` | file | | RSA key (.pem) |
|
||||||
|
| `rsa_key_qr` | file | | RSA key (QR code image) |
|
||||||
|
| `rsa_password` | string | | RSA key password |
|
||||||
|
| `channel_key` | string | | `"auto"` (default), `"none"=public`, or explicit key |
|
||||||
|
| `embed_mode` | string | | `"lsb"` or `"dct"` |
|
||||||
|
| `dct_output_format` | string | | `"png"` or `"jpeg"` |
|
||||||
|
| `dct_color_mode` | string | | `"grayscale"` or `"color"` |
|
||||||
|
|
||||||
|
\* Provide either `message` or `payload_file`
|
||||||
|
|
||||||
|
#### Channel Key in Multipart
|
||||||
|
|
||||||
|
For form data, the channel_key field uses strings:
|
||||||
|
|
||||||
|
| Value | Effect |
|
||||||
|
|-------|--------|
|
||||||
|
| `"auto"` | Use server config (default) |
|
||||||
|
| `"none"` | Public mode |
|
||||||
|
| `"XXXX-XXXX-..."` | Explicit key |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Returns the stego image directly with headers:
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: image/png
|
||||||
|
Content-Disposition: attachment; filename=a1b2c3d4.png
|
||||||
|
X-Stegasoo-Capacity-Percent: 12.4
|
||||||
|
X-Stegasoo-Embed-Mode: lsb
|
||||||
|
X-Stegasoo-Channel-Mode: private
|
||||||
|
X-Stegasoo-Channel-Fingerprint: ABCD-••••-...-3456
|
||||||
|
X-Stegasoo-Version: 4.0.2
|
||||||
|
|
||||||
|
<binary image data>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### cURL Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Encode with auto channel key (default)
|
||||||
|
curl -X POST http://localhost:8000/encode/multipart \
|
||||||
|
-F "passphrase=apple forest thunder mountain" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "message=Secret message" \
|
||||||
|
-F "reference_photo=@reference.jpg" \
|
||||||
|
-F "carrier=@carrier.png" \
|
||||||
|
--output stego.png
|
||||||
|
|
||||||
|
# Encode with explicit channel key
|
||||||
|
curl -X POST http://localhost:8000/encode/multipart \
|
||||||
|
-F "passphrase=words here" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "message=Team message" \
|
||||||
|
-F "channel_key=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456" \
|
||||||
|
-F "reference_photo=@reference.jpg" \
|
||||||
|
-F "carrier=@carrier.png" \
|
||||||
|
--output stego.png
|
||||||
|
|
||||||
|
# Encode in public mode (no channel isolation)
|
||||||
|
curl -X POST http://localhost:8000/encode/multipart \
|
||||||
|
-F "passphrase=words here" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "message=Public message" \
|
||||||
|
-F "channel_key=none" \
|
||||||
|
-F "reference_photo=@reference.jpg" \
|
||||||
|
-F "carrier=@carrier.png" \
|
||||||
|
--output stego.png
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /decode (JSON)
|
||||||
|
|
||||||
|
Decode a message or file from a stego image.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"stego_image_base64": "iVBORw0KGgo...",
|
||||||
|
"reference_photo_base64": "iVBORw0KGgo...",
|
||||||
|
"passphrase": "apple forest thunder mountain",
|
||||||
|
"pin": "123456",
|
||||||
|
"rsa_key_base64": null,
|
||||||
|
"rsa_password": null,
|
||||||
|
"channel_key": null,
|
||||||
|
"embed_mode": "auto"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (Text)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payload_type": "text",
|
||||||
|
"message": "Secret message here",
|
||||||
|
"file_data_base64": null,
|
||||||
|
"filename": null,
|
||||||
|
"mime_type": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (File)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payload_type": "file",
|
||||||
|
"message": null,
|
||||||
|
"file_data_base64": "UEsDBBQAAAA...",
|
||||||
|
"filename": "document.pdf",
|
||||||
|
"mime_type": "application/pdf"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /decode/multipart
|
||||||
|
|
||||||
|
Decode using multipart form data.
|
||||||
|
|
||||||
|
#### Form Fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `passphrase` | string | ✓ | Passphrase |
|
||||||
|
| `reference_photo` | file | ✓ | Reference photo |
|
||||||
|
| `stego_image` | file | ✓ | Stego image to decode |
|
||||||
|
| `pin` | string | | Static PIN |
|
||||||
|
| `rsa_key` | file | | RSA key (.pem) |
|
||||||
|
| `rsa_key_qr` | file | | RSA key (QR code image) |
|
||||||
|
| `rsa_password` | string | | RSA key password |
|
||||||
|
| `channel_key` | string | | `"auto"` (default), `"none"=public`, or explicit key |
|
||||||
|
| `embed_mode` | string | | `"auto"`, `"lsb"`, or `"dct"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Channel Keys
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
Channel keys provide **deployment/group isolation**. Messages encoded with a channel key can only be decoded with the same key.
|
||||||
|
|
||||||
|
### Key Format
|
||||||
|
|
||||||
|
```
|
||||||
|
ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
|
||||||
|
└──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘
|
||||||
|
8 groups of 4 alphanumeric characters (256 bits)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage Locations
|
||||||
|
|
||||||
|
Keys are checked in order:
|
||||||
|
|
||||||
|
| Priority | Location | Best For |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| 1 | `STEGASOO_CHANNEL_KEY` env var | Docker, CI/CD |
|
||||||
|
| 2 | `./config/channel.key` | Project-specific |
|
||||||
|
| 3 | `~/.stegasoo/channel.key` | User default |
|
||||||
|
|
||||||
|
### API Parameter Values
|
||||||
|
|
||||||
|
#### JSON Endpoints (`/encode`, `/decode`)
|
||||||
|
|
||||||
|
| Value | Effect |
|
||||||
|
|-------|--------|
|
||||||
|
| `null` | Auto - use server config |
|
||||||
|
| `""` | Public mode |
|
||||||
|
| `"XXXX-..."` | Explicit key |
|
||||||
|
|
||||||
|
#### Multipart Endpoints (`/encode/multipart`, `/decode/multipart`)
|
||||||
|
|
||||||
|
| Value | Effect |
|
||||||
|
|-------|--------|
|
||||||
|
| `"auto"` | Use server config (default) |
|
||||||
|
| `"none"` | Public mode |
|
||||||
|
| `"XXXX-..."` | Explicit key |
|
||||||
|
|
||||||
|
### Workflow Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Generate a channel key for the team
|
||||||
|
KEY=$(curl -s -X POST http://localhost:8000/channel/generate | jq -r '.key')
|
||||||
|
echo "Team key: $KEY"
|
||||||
|
|
||||||
|
# 2. Distribute to team members (securely!)
|
||||||
|
|
||||||
|
# 3. Each deployment sets the key
|
||||||
|
export STEGASOO_CHANNEL_KEY=$KEY
|
||||||
|
|
||||||
|
# 4. Encode - automatically uses server key
|
||||||
|
curl -X POST http://localhost:8000/encode/multipart \
|
||||||
|
-F "passphrase=team passphrase" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "message=Team secret" \
|
||||||
|
-F "reference_photo=@ref.jpg" \
|
||||||
|
-F "carrier=@carrier.png" \
|
||||||
|
--output stego.png
|
||||||
|
|
||||||
|
# 5. Decode - automatically uses server key
|
||||||
|
curl -X POST http://localhost:8000/decode/multipart \
|
||||||
|
-F "passphrase=team passphrase" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "reference_photo=@ref.jpg" \
|
||||||
|
-F "stego_image=@stego.png"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### ChannelStatusResponse
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "private",
|
||||||
|
"configured": true,
|
||||||
|
"fingerprint": "ABCD-••••-...-3456",
|
||||||
|
"source": "~/.stegasoo/channel.key",
|
||||||
|
"key": "ABCD-1234-..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### EncodeResponse (v4.0.0)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"stego_image_base64": "string",
|
||||||
|
"filename": "string",
|
||||||
|
"capacity_used_percent": 12.4,
|
||||||
|
"embed_mode": "lsb",
|
||||||
|
"output_format": "png",
|
||||||
|
"color_mode": "color",
|
||||||
|
"channel_mode": "private",
|
||||||
|
"channel_fingerprint": "ABCD-••••-...-3456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DecodeResponse
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payload_type": "text",
|
||||||
|
"message": "string",
|
||||||
|
"file_data_base64": null,
|
||||||
|
"filename": null,
|
||||||
|
"mime_type": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### HTTP Status Codes
|
||||||
|
|
||||||
|
| Code | Meaning | Use Case |
|
||||||
|
|------|---------|----------|
|
||||||
|
| 200 | OK | Successful operation |
|
||||||
|
| 400 | Bad Request | Invalid input, capacity error, invalid channel key |
|
||||||
|
| 401 | Unauthorized | Decryption failed, channel key mismatch |
|
||||||
|
| 500 | Internal Error | Unexpected server error |
|
||||||
|
| 501 | Not Implemented | Feature unavailable |
|
||||||
|
|
||||||
|
### Channel Key Errors
|
||||||
|
|
||||||
|
| Status | Error | Cause |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| 400 | "Invalid channel key format" | Key doesn't match `XXXX-XXXX-...` pattern |
|
||||||
|
| 401 | "Message encoded with channel key but none configured" | Need to provide channel key |
|
||||||
|
| 401 | "Message encoded without channel key" | Use `channel_key=""` or `"none"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:8000"
|
||||||
|
|
||||||
|
# Check channel status
|
||||||
|
status = requests.get(f"{BASE_URL}/channel/status").json()
|
||||||
|
print(f"Channel mode: {status['mode']}")
|
||||||
|
print(f"Fingerprint: {status.get('fingerprint', 'N/A')}")
|
||||||
|
|
||||||
|
# Generate channel key
|
||||||
|
response = requests.post(f"{BASE_URL}/channel/generate?save=true")
|
||||||
|
key_info = response.json()
|
||||||
|
print(f"Generated: {key_info['fingerprint']}")
|
||||||
|
|
||||||
|
# Encode with channel key (auto from server)
|
||||||
|
with open("ref.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
|
||||||
|
response = requests.post(f"{BASE_URL}/encode/multipart", files={
|
||||||
|
"reference_photo": ref,
|
||||||
|
"carrier": carrier,
|
||||||
|
}, data={
|
||||||
|
"message": "Team secret",
|
||||||
|
"passphrase": "apple forest thunder",
|
||||||
|
"pin": "123456",
|
||||||
|
# channel_key defaults to "auto" (use server config)
|
||||||
|
})
|
||||||
|
|
||||||
|
with open("stego.png", "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
print(f"Channel mode: {response.headers.get('X-Stegasoo-Channel-Mode')}")
|
||||||
|
|
||||||
|
# Encode with explicit channel key
|
||||||
|
with open("ref.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
|
||||||
|
response = requests.post(f"{BASE_URL}/encode/multipart", files={
|
||||||
|
"reference_photo": ref,
|
||||||
|
"carrier": carrier,
|
||||||
|
}, data={
|
||||||
|
"message": "Using explicit key",
|
||||||
|
"passphrase": "words here",
|
||||||
|
"pin": "123456",
|
||||||
|
"channel_key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Decode
|
||||||
|
with open("ref.jpg", "rb") as ref, open("stego.png", "rb") as stego:
|
||||||
|
response = requests.post(f"{BASE_URL}/decode/multipart", files={
|
||||||
|
"reference_photo": ref,
|
||||||
|
"stego_image": stego,
|
||||||
|
}, data={
|
||||||
|
"passphrase": "apple forest thunder",
|
||||||
|
"pin": "123456",
|
||||||
|
# channel_key defaults to "auto"
|
||||||
|
})
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
print(f"Decoded: {result.get('message')}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const axios = require('axios');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const BASE_URL = 'http://localhost:8000';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Check channel status
|
||||||
|
const status = await axios.get(`${BASE_URL}/channel/status`);
|
||||||
|
console.log('Channel:', status.data.mode);
|
||||||
|
|
||||||
|
// Encode with auto channel key
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('passphrase', 'apple forest thunder');
|
||||||
|
form.append('pin', '123456');
|
||||||
|
form.append('message', 'Secret');
|
||||||
|
form.append('reference_photo', fs.createReadStream('ref.jpg'));
|
||||||
|
form.append('carrier', fs.createReadStream('carrier.png'));
|
||||||
|
// channel_key defaults to "auto" (use server config)
|
||||||
|
|
||||||
|
const response = await axios.post(`${BASE_URL}/encode/multipart`, form, {
|
||||||
|
headers: form.getHeaders(),
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync('stego.png', response.data);
|
||||||
|
console.log('Channel mode:', response.headers['x-stegasoo-channel-mode']);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
### cURL / Bash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
BASE_URL="http://localhost:8000"
|
||||||
|
|
||||||
|
# Check channel status
|
||||||
|
echo "Channel status:"
|
||||||
|
curl -s "$BASE_URL/channel/status" | jq .
|
||||||
|
|
||||||
|
# Generate and save channel key
|
||||||
|
echo "Generating channel key..."
|
||||||
|
curl -s -X POST "$BASE_URL/channel/generate?save=true" | jq .
|
||||||
|
|
||||||
|
# Encode (channel_key defaults to "auto")
|
||||||
|
echo "Encoding..."
|
||||||
|
curl -s -X POST "$BASE_URL/encode/multipart" \
|
||||||
|
-F "passphrase=apple forest thunder" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "message=Secret message" \
|
||||||
|
-F "reference_photo=@ref.jpg" \
|
||||||
|
-F "carrier=@carrier.png" \
|
||||||
|
--output stego.png
|
||||||
|
|
||||||
|
echo "Encoded to stego.png"
|
||||||
|
|
||||||
|
# Decode
|
||||||
|
echo "Decoding..."
|
||||||
|
curl -s -X POST "$BASE_URL/decode/multipart" \
|
||||||
|
-F "passphrase=apple forest thunder" \
|
||||||
|
-F "pin=123456" \
|
||||||
|
-F "reference_photo=@ref.jpg" \
|
||||||
|
-F "stego_image=@stego.png" | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Configuration
|
||||||
|
|
||||||
|
### docker/docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
x-common-env: &common-env
|
||||||
|
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: api
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
```
|
||||||
|
|
||||||
|
### .env (gitignored)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate key for .env
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:8000/channel/generate | \
|
||||||
|
jq -r '"STEGASOO_CHANNEL_KEY=\(.key)"' >> .env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [CLI Documentation](CLI.md) - Command-line interface
|
||||||
|
- [Web UI Documentation](WEB_UI.md) - Browser interface
|
||||||
|
- [README](../README.md) - Project overview
|
||||||
214
CHANGELOG.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Stegasoo will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [4.1.5] - 2026-01-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Developer Documentation**: Educational comments throughout core modules
|
||||||
|
- DCT module: zig-zag diagrams, QIM explanation, Reed-Solomon deep dive
|
||||||
|
- LSB module: visual bit embedding examples, ChaCha20 pixel selection
|
||||||
|
- Crypto module: multi-factor KDF flow diagrams, Argon2id reasoning
|
||||||
|
- CLI module: Click patterns (groups, JSON output, secure input)
|
||||||
|
- Web UI module: Flask architecture, subprocess isolation, async jobs
|
||||||
|
- **Pi Test Automation**: `rpi/kickoff-pi-test.sh` script
|
||||||
|
- One command to flash, wait for boot, setup, and smoke test
|
||||||
|
- Self-contained (no dotfile dependencies)
|
||||||
|
- **v4.2 Wishlist**: `WISHLIST-4.2.md` for blue-sky ideas (GPU acceleration)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Pi MOTD Improvements**:
|
||||||
|
- Dynamic temperature emoji (ice/cool/fire based on temp)
|
||||||
|
- Rocket emoji for service status, globe emoji for URL
|
||||||
|
- Shortened Debian boilerplate message
|
||||||
|
- Fixed escaped variable syntax in heredoc
|
||||||
|
|
||||||
|
## [4.1.3] - 2026-01-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Docker Deployment**: Production-ready containerization
|
||||||
|
- `docker-compose.yml` for Web UI (port 5000) and REST API (port 8000)
|
||||||
|
- Multi-stage builds with base image for faster rebuilds
|
||||||
|
- Health checks, resource limits (768MB), and volume persistence
|
||||||
|
- Comprehensive `DOCKER.md` documentation
|
||||||
|
- **Raspberry Pi First-Boot Wizard**: Interactive TUI setup experience
|
||||||
|
- `gum` TUI toolkit for styled prompts and spinners
|
||||||
|
- WiFi configuration, HTTPS setup, channel key generation
|
||||||
|
- Overclock presets (Pi 5: 2.8/3.0 GHz with cooling recommendations)
|
||||||
|
- Port 443 redirect option for clean HTTPS URLs
|
||||||
|
- Styled banners with purple→blue gradient and gold logo
|
||||||
|
- **Pi Image Distribution**: Scripts for SD card imaging
|
||||||
|
- `sanitize-for-image.sh` removes credentials, SSH keys, user data
|
||||||
|
- Soft reset mode for testing without clearing WiFi
|
||||||
|
- Auto-validates sanitization before imaging
|
||||||
|
- **Unit Tests**: Comprehensive pytest test suite
|
||||||
|
- Tests for encode/decode, LSB/DCT modes, channel keys
|
||||||
|
- Validation, generation, compression, edge cases
|
||||||
|
- 29 tests covering core library functionality
|
||||||
|
- **Release Validation**: `scripts/validate-release.sh` for pre-release checks
|
||||||
|
- **Custom SSL Documentation**: Guide for replacing certs, Let's Encrypt setup
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Pi MOTD shows CPU speed and temperature when overclocked
|
||||||
|
- Mobile UI polish and responsive improvements
|
||||||
|
- Standardized ASCII banners across all Pi scripts
|
||||||
|
- Setup script uses pyenv for Python 3.12 (Pi OS ships 3.13)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **SSL certificate generation**: Wizard and setup now generate certs when HTTPS enabled
|
||||||
|
- DCT decode reliability improvements
|
||||||
|
- Fixed `gum --inline` flag compatibility (not supported in all versions)
|
||||||
|
- Wizard banner alignment and spacing issues
|
||||||
|
- Better error handling in app.py for SSL failures
|
||||||
|
|
||||||
|
## [4.1.0] - 2026-01-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Admin Recovery System**: Password reset for locked-out admins
|
||||||
|
- Recovery key generated during setup (32-char alphanumeric)
|
||||||
|
- Multiple backup options: text file, QR code, stego image
|
||||||
|
- QR codes obfuscated (XOR'd with magic header hash)
|
||||||
|
- Stego backups hide key in an image using Stegasoo itself
|
||||||
|
- CLI: `stegasoo admin recover --db path/to/db`
|
||||||
|
- **EXIF Editor**: Full metadata editing in Tools page
|
||||||
|
- View all EXIF fields from uploaded image
|
||||||
|
- Inline editing of individual fields
|
||||||
|
- Clear all metadata with one click
|
||||||
|
- Download cleaned image
|
||||||
|
- CLI: `stegasoo tools exif image.jpg [--clear] [--set Field=Value]`
|
||||||
|
- **Multi-User Support**: Admin can create up to 16 additional users
|
||||||
|
- Role-based access control (admin/user)
|
||||||
|
- Admin user management page
|
||||||
|
- Temp password generation for new users
|
||||||
|
- **Saved Channel Keys**: Users can save/manage channel keys in account page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Architecture**: Consolidated `resolve_channel_key()` to library layer
|
||||||
|
- Single source of truth in `src/stegasoo/channel.py`
|
||||||
|
- CLI, API, WebUI now use thin wrappers
|
||||||
|
- **DCT Pre-Check**: Fail fast with helpful error before expensive encoding
|
||||||
|
- **Toast Notifications**: Auto-dismiss after 20 seconds with fade animation
|
||||||
|
- `RECOVERY_OBFUSCATION_KEY` constant added to `constants.py`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- DCT payload size error now caught early with clear message
|
||||||
|
|
||||||
|
## [4.0.2] - 2026-01-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Web UI Authentication**: Single-admin login with SQLite3 user storage
|
||||||
|
- First-run setup wizard for admin account creation
|
||||||
|
- Account management page for password changes
|
||||||
|
- `@login_required` decorator protects encode/decode/generate routes
|
||||||
|
- Argon2id password hashing (lighter 64MB for fast login)
|
||||||
|
- **Optional HTTPS**: Auto-generated self-signed certificates for home network deployment
|
||||||
|
- Configurable via `STEGASOO_HTTPS_ENABLED` environment variable
|
||||||
|
- Certificates stored in `frontends/web/certs/`
|
||||||
|
- New environment variables: `STEGASOO_AUTH_ENABLED`, `STEGASOO_HTTPS_ENABLED`, `STEGASOO_HOSTNAME`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- PIN entry column widened in encode/decode forms (col-md-4 → col-md-6)
|
||||||
|
- Channel options column narrowed (col-md-8 → col-md-6)
|
||||||
|
- QR preview panels enlarged for better text readability
|
||||||
|
- Consistent font sizing across all preview panel banners (0.7rem filename, 0.6rem data, 0.65rem badges)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- QR preview text too small to read in encode/decode templates
|
||||||
|
- Inconsistent label sizes between reference/carrier/stego panels
|
||||||
|
|
||||||
|
## [4.0.1] - 2025-01-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed numpy binary incompatibility on Python 3.10 (jpegio/scipy)
|
||||||
|
- Fixed BatchCredentials test failures with missing `reference_photo` parameter
|
||||||
|
- Graceful handling when DCT dependencies have version mismatches
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Applied `ruff` linter fixes across entire codebase (~400 issues)
|
||||||
|
- Applied `black` formatter to all Python files
|
||||||
|
- Modernized type hints: `Optional[X]` → `X | None`
|
||||||
|
- Updated ruff config to use `[tool.ruff.lint]` section
|
||||||
|
- Moved documentation files to repository root
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Removed obsolete debug/diagnostic scripts
|
||||||
|
- Cleaned up backup files and dev scripts
|
||||||
|
|
||||||
|
## [4.0.0] - 2024-12-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Refreshed Web UI with modern, snazzy interface
|
||||||
|
- Improved user experience across all pages
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Major version bump for breaking API changes
|
||||||
|
- Simplified passphrase handling (single passphrase instead of day-based)
|
||||||
|
- Removed date_str parameter from encoding
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Various bug fixes for Web UI
|
||||||
|
- CLI updates and improvements
|
||||||
|
|
||||||
|
## [3.2.0] - 2024-12-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Big revamp of the encoding system
|
||||||
|
- Home and about page improvements
|
||||||
|
- UNDER_THE_HOOD.md documentation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Renamed `phrase` → `passphrase` in API
|
||||||
|
- Updated Web UI styling
|
||||||
|
|
||||||
|
## [3.0.2] - 2024-12-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Full experimental DCT steganography support
|
||||||
|
- jpegio integration for better JPEG manipulation
|
||||||
|
- DCT/LSB mode selector in Web UI
|
||||||
|
|
||||||
|
## [3.0.0] - 2024-12-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- DCT (Discrete Cosine Transform) steganography mode
|
||||||
|
- Support for JPEG carriers without quality loss
|
||||||
|
- Channel key feature for private messaging
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Complete rewrite of steganography engine
|
||||||
|
- New hybrid authentication system
|
||||||
|
|
||||||
|
## [2.0.0] - 2024-12-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Web UI frontend
|
||||||
|
- REST API (FastAPI)
|
||||||
|
- Batch processing support
|
||||||
|
- RSA key authentication option
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Migrated to hybrid photo + passphrase + PIN authentication
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-12-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release
|
||||||
|
- LSB steganography
|
||||||
|
- AES-256-GCM encryption
|
||||||
|
- CLI interface
|
||||||
|
- Basic PIN authentication
|
||||||
|
|
||||||
|
[4.1.5]: https://github.com/adlee-was-taken/stegasoo/compare/v4.1.3...v4.1.5
|
||||||
|
[4.1.3]: https://github.com/adlee-was-taken/stegasoo/compare/v4.1.0...v4.1.3
|
||||||
|
[4.1.0]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.2...v4.1.0
|
||||||
|
[4.0.2]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.1...v4.0.2
|
||||||
|
[4.0.1]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.0...v4.0.1
|
||||||
|
[4.0.0]: https://github.com/adlee-was-taken/stegasoo/compare/v3.2.0...v4.0.0
|
||||||
|
[3.2.0]: https://github.com/adlee-was-taken/stegasoo/compare/v3.0.2...v3.2.0
|
||||||
|
[3.0.2]: https://github.com/adlee-was-taken/stegasoo/compare/v3.0.0...v3.0.2
|
||||||
|
[3.0.0]: https://github.com/adlee-was-taken/stegasoo/compare/v2.0.0...v3.0.0
|
||||||
|
[2.0.0]: https://github.com/adlee-was-taken/stegasoo/compare/v1.0.0...v2.0.0
|
||||||
|
[1.0.0]: https://github.com/adlee-was-taken/stegasoo/releases/tag/v1.0.0
|
||||||
921
CLI.md
Normal file
@@ -0,0 +1,921 @@
|
|||||||
|
# Stegasoo CLI Documentation (v4.1.0)
|
||||||
|
|
||||||
|
Complete command-line interface reference for Stegasoo steganography operations.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [What's New in v4.1.0](#whats-new-in-v410)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Commands](#commands)
|
||||||
|
- [generate](#generate-command)
|
||||||
|
- [encode](#encode-command)
|
||||||
|
- [decode](#decode-command)
|
||||||
|
- [verify](#verify-command)
|
||||||
|
- [channel](#channel-command)
|
||||||
|
- [admin](#admin-command)
|
||||||
|
- [tools](#tools-command)
|
||||||
|
- [info](#info-command)
|
||||||
|
- [compare](#compare-command)
|
||||||
|
- [modes](#modes-command)
|
||||||
|
- [Channel Keys](#channel-keys)
|
||||||
|
- [Embedding Modes](#embedding-modes)
|
||||||
|
- [Security Factors](#security-factors)
|
||||||
|
- [Workflow Examples](#workflow-examples)
|
||||||
|
- [Piping & Scripting](#piping--scripting)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Exit Codes](#exit-codes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From PyPI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CLI only
|
||||||
|
pip install stegasoo[cli]
|
||||||
|
|
||||||
|
# CLI with DCT support
|
||||||
|
pip install stegasoo[cli,dct]
|
||||||
|
|
||||||
|
# With all extras
|
||||||
|
pip install stegasoo[all]
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/example/stegasoo.git
|
||||||
|
cd stegasoo
|
||||||
|
pip install -e ".[cli,dct]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo --version
|
||||||
|
stegasoo --help
|
||||||
|
|
||||||
|
# Check DCT support
|
||||||
|
python -c "from stegasoo import has_dct_support; print('DCT:', 'available' if has_dct_support() else 'requires scipy')"
|
||||||
|
|
||||||
|
# Check channel key status
|
||||||
|
stegasoo channel show
|
||||||
|
```
|
||||||
|
|
||||||
|
### Man Page
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install man page
|
||||||
|
sudo mkdir -p /usr/local/share/man/man1
|
||||||
|
sudo cp docs/stegasoo.1 /usr/local/share/man/man1/
|
||||||
|
sudo mandb
|
||||||
|
|
||||||
|
# View
|
||||||
|
man stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's New in v4.1.0
|
||||||
|
|
||||||
|
Version 4.1.0 adds **admin recovery** and **tools** commands:
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| Admin recovery | Reset admin password using recovery key |
|
||||||
|
| EXIF tools | View, edit, and strip image metadata |
|
||||||
|
| Peek tool | Quick stego detection check |
|
||||||
|
| Strip tool | Remove hidden data from images |
|
||||||
|
|
||||||
|
**New commands:**
|
||||||
|
- `stegasoo admin recover` - Reset admin password with recovery key
|
||||||
|
- `stegasoo tools exif` - View/edit EXIF metadata
|
||||||
|
- `stegasoo tools peek` - Check for hidden data
|
||||||
|
- `stegasoo tools strip` - Remove stego data from image
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's New in v4.0.0
|
||||||
|
|
||||||
|
Version 4.0.0 added **channel key** support for deployment/group isolation:
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| Channel keys | 256-bit keys that isolate message groups |
|
||||||
|
| Deployment isolation | Different deployments can't read each other's messages |
|
||||||
|
| CLI management | New `stegasoo channel` command group |
|
||||||
|
| Flexible override | Use server config, explicit key, or public mode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Generate credentials (do this once, memorize results)
|
||||||
|
stegasoo generate
|
||||||
|
|
||||||
|
# 2. (Optional) Set up channel key for deployment isolation
|
||||||
|
stegasoo channel generate --save
|
||||||
|
|
||||||
|
# 3. Encode a message (uses configured channel key automatically)
|
||||||
|
stegasoo encode \
|
||||||
|
--ref secret_photo.jpg \
|
||||||
|
--carrier meme.png \
|
||||||
|
--passphrase "apple forest thunder mountain" \
|
||||||
|
--pin 123456 \
|
||||||
|
--message "Meet at midnight"
|
||||||
|
|
||||||
|
# 4. Decode a message (uses same channel key)
|
||||||
|
stegasoo decode \
|
||||||
|
--ref secret_photo.jpg \
|
||||||
|
--stego stego_abc123.png \
|
||||||
|
--passphrase "apple forest thunder mountain" \
|
||||||
|
--pin 123456
|
||||||
|
|
||||||
|
# 5. Decode without channel key (public mode)
|
||||||
|
stegasoo decode \
|
||||||
|
--ref secret_photo.jpg \
|
||||||
|
--stego public_stego.png \
|
||||||
|
--passphrase "words here now" \
|
||||||
|
--pin 123456 \
|
||||||
|
--no-channel
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Generate Command
|
||||||
|
|
||||||
|
Generate credentials for encoding/decoding operations.
|
||||||
|
|
||||||
|
#### Synopsis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo generate [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
| Option | Short | Type | Default | Description |
|
||||||
|
|--------|-------|------|---------|-------------|
|
||||||
|
| `--pin/--no-pin` | | flag | `--pin` | Generate a PIN |
|
||||||
|
| `--rsa/--no-rsa` | | flag | `--no-rsa` | Generate an RSA key |
|
||||||
|
| `--pin-length` | | 6-9 | 6 | PIN length in digits |
|
||||||
|
| `--rsa-bits` | | choice | 2048 | RSA key size (2048, 3072) |
|
||||||
|
| `--words` | | 3-12 | 4 | Words in passphrase |
|
||||||
|
| `--output` | `-o` | path | | Save RSA key to file |
|
||||||
|
| `--password` | `-p` | string | | Password for RSA key file |
|
||||||
|
| `--json` | | flag | | Output as JSON |
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic generation with PIN (default)
|
||||||
|
stegasoo generate
|
||||||
|
|
||||||
|
# Generate with more words for higher security
|
||||||
|
stegasoo generate --words 6
|
||||||
|
|
||||||
|
# Generate with RSA key
|
||||||
|
stegasoo generate --rsa --rsa-bits 3072
|
||||||
|
|
||||||
|
# Save RSA key to encrypted file
|
||||||
|
stegasoo generate --rsa -o mykey.pem -p "mysecretpassword"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Encode Command
|
||||||
|
|
||||||
|
Encode a secret message or file into an image.
|
||||||
|
|
||||||
|
#### Synopsis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo encode [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
| Option | Short | Type | Required | Default | Description |
|
||||||
|
|--------|-------|------|----------|---------|-------------|
|
||||||
|
| `--ref` | `-r` | path | ✓ | | Reference photo |
|
||||||
|
| `--carrier` | `-c` | path | ✓ | | Carrier image |
|
||||||
|
| `--passphrase` | `-p` | string | ✓ | | Passphrase |
|
||||||
|
| `--message` | `-m` | string | | | Message to encode |
|
||||||
|
| `--message-file` | `-f` | path | | | Read message from file |
|
||||||
|
| `--embed-file` | `-e` | path | | | Embed a binary file |
|
||||||
|
| `--pin` | | string | * | | Static PIN (6-9 digits) |
|
||||||
|
| `--key` | `-k` | path | * | | RSA key file |
|
||||||
|
| `--key-qr` | | path | * | | RSA key from QR code |
|
||||||
|
| `--key-password` | | string | | | RSA key password |
|
||||||
|
| `--channel` | | string | | auto | Channel key (v4.0.0) |
|
||||||
|
| `--channel-file` | | path | | | Read channel key from file |
|
||||||
|
| `--no-channel` | | flag | | | Force public mode |
|
||||||
|
| `--output` | `-o` | path | | | Output filename |
|
||||||
|
| `--mode` | | choice | | `lsb` | Embedding mode |
|
||||||
|
| `--dct-format` | | choice | | `png` | DCT output format |
|
||||||
|
| `--dct-color` | | choice | | `grayscale` | DCT color mode |
|
||||||
|
| `--quiet` | `-q` | flag | | | Suppress output |
|
||||||
|
|
||||||
|
\* At least one of `--pin`, `--key`, or `--key-qr` is required.
|
||||||
|
|
||||||
|
#### Channel Key Options
|
||||||
|
|
||||||
|
| Option | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| *(none)* | Use server-configured key (auto mode) |
|
||||||
|
| `--channel KEY` | Use explicit channel key |
|
||||||
|
| `--channel auto` | Same as no option |
|
||||||
|
| `--channel-file F` | Read channel key from file |
|
||||||
|
| `--no-channel` | Force public mode (no isolation) |
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic encoding (uses server channel key if configured)
|
||||||
|
stegasoo encode \
|
||||||
|
-r photo.jpg -c meme.png \
|
||||||
|
-p "correct horse battery staple" \
|
||||||
|
--pin 847293 \
|
||||||
|
-m "The package arrives Tuesday"
|
||||||
|
|
||||||
|
# With explicit channel key
|
||||||
|
stegasoo encode \
|
||||||
|
-r photo.jpg -c meme.png \
|
||||||
|
-p "correct horse battery staple" \
|
||||||
|
--pin 847293 \
|
||||||
|
-m "Secret message" \
|
||||||
|
--channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
|
||||||
|
|
||||||
|
# Public mode (no channel isolation)
|
||||||
|
stegasoo encode \
|
||||||
|
-r photo.jpg -c meme.png \
|
||||||
|
-p "correct horse battery staple" \
|
||||||
|
--pin 847293 \
|
||||||
|
-m "Public message" \
|
||||||
|
--no-channel
|
||||||
|
|
||||||
|
# DCT mode for social media
|
||||||
|
stegasoo encode \
|
||||||
|
-r photo.jpg -c meme.png \
|
||||||
|
-p "words here" --pin 847293 \
|
||||||
|
-m "Secret" \
|
||||||
|
--mode dct --dct-format jpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Decode Command
|
||||||
|
|
||||||
|
Decode a secret message or file from a stego image.
|
||||||
|
|
||||||
|
#### Synopsis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo decode [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
| Option | Short | Type | Required | Default | Description |
|
||||||
|
|--------|-------|------|----------|---------|-------------|
|
||||||
|
| `--ref` | `-r` | path | ✓ | | Reference photo |
|
||||||
|
| `--stego` | `-s` | path | ✓ | | Stego image |
|
||||||
|
| `--passphrase` | `-p` | string | ✓ | | Passphrase |
|
||||||
|
| `--pin` | | string | * | | Static PIN |
|
||||||
|
| `--key` | `-k` | path | * | | RSA key file |
|
||||||
|
| `--key-qr` | | path | * | | RSA key from QR code |
|
||||||
|
| `--key-password` | | string | | | RSA key password |
|
||||||
|
| `--channel` | | string | | auto | Channel key (v4.0.0) |
|
||||||
|
| `--channel-file` | | path | | | Read channel key from file |
|
||||||
|
| `--no-channel` | | flag | | | Force public mode |
|
||||||
|
| `--output` | `-o` | path | | | Save output to file |
|
||||||
|
| `--mode` | | choice | | `auto` | Extraction mode |
|
||||||
|
| `--quiet` | `-q` | flag | | | Minimal output |
|
||||||
|
| `--force` | | flag | | | Overwrite existing file |
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic decoding (uses server channel key)
|
||||||
|
stegasoo decode \
|
||||||
|
-r photo.jpg -s stego.png \
|
||||||
|
-p "correct horse battery staple" \
|
||||||
|
--pin 847293
|
||||||
|
|
||||||
|
# With explicit channel key
|
||||||
|
stegasoo decode \
|
||||||
|
-r photo.jpg -s stego.png \
|
||||||
|
-p "words here" --pin 847293 \
|
||||||
|
--channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
|
||||||
|
|
||||||
|
# Decode public image (no channel key was used)
|
||||||
|
stegasoo decode \
|
||||||
|
-r photo.jpg -s stego.png \
|
||||||
|
-p "words here" --pin 847293 \
|
||||||
|
--no-channel
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
stegasoo decode \
|
||||||
|
-r photo.jpg -s stego.png \
|
||||||
|
-p "words" --pin 123456 \
|
||||||
|
-o decoded.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Verify Command
|
||||||
|
|
||||||
|
Verify credentials without extracting the message.
|
||||||
|
|
||||||
|
#### Synopsis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo verify [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
Same as `decode`, minus `--output` and `--force`. Adds `--json` for JSON output.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick verification
|
||||||
|
stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456
|
||||||
|
|
||||||
|
# With explicit channel key
|
||||||
|
stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456 \
|
||||||
|
--channel ABCD-1234-...
|
||||||
|
|
||||||
|
# JSON output
|
||||||
|
stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456 --json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Channel Command
|
||||||
|
|
||||||
|
Manage channel keys for deployment/group isolation.
|
||||||
|
|
||||||
|
#### Subcommands
|
||||||
|
|
||||||
|
| Subcommand | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| `generate` | Create a new channel key |
|
||||||
|
| `show` | Display current channel key status |
|
||||||
|
| `set` | Save a channel key to config |
|
||||||
|
| `clear` | Remove channel key from config |
|
||||||
|
|
||||||
|
#### channel generate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo channel generate [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| `--save` | `-s` | Save to user config (~/.stegasoo/channel.key) |
|
||||||
|
| `--save-project` | | Save to project config (./config/channel.key) |
|
||||||
|
| `--env` | `-e` | Output as environment variable export |
|
||||||
|
| `--quiet` | `-q` | Output only the key |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Just display a new key
|
||||||
|
stegasoo channel generate
|
||||||
|
|
||||||
|
# Save to user config
|
||||||
|
stegasoo channel generate --save
|
||||||
|
|
||||||
|
# Add to .env file
|
||||||
|
stegasoo channel generate --env >> .env
|
||||||
|
|
||||||
|
# For scripts
|
||||||
|
KEY=$(stegasoo channel generate -q)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### channel show
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo channel show [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| `--reveal` | `-r` | Show full key (not just fingerprint) |
|
||||||
|
| `--json` | | Output as JSON |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show status (fingerprint only)
|
||||||
|
stegasoo channel show
|
||||||
|
|
||||||
|
# Reveal full key
|
||||||
|
stegasoo channel show --reveal
|
||||||
|
|
||||||
|
# JSON for scripts
|
||||||
|
stegasoo channel show --json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
```
|
||||||
|
─── CHANNEL KEY STATUS ───
|
||||||
|
|
||||||
|
Mode: PRIVATE
|
||||||
|
Fingerprint: ABCD-••••-••••-••••-••••-••••-••••-3456
|
||||||
|
Source: ~/.stegasoo/channel.key
|
||||||
|
|
||||||
|
Messages require this channel key to decode.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### channel set
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo channel set [KEY] [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| `--file` | `-f` | Read key from file |
|
||||||
|
| `--project` | `-p` | Save to project config instead of user |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set from command line
|
||||||
|
stegasoo channel set ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
|
||||||
|
|
||||||
|
# Set from file
|
||||||
|
stegasoo channel set --file channel.key
|
||||||
|
|
||||||
|
# Set in project config
|
||||||
|
stegasoo channel set XXXX-... --project
|
||||||
|
```
|
||||||
|
|
||||||
|
#### channel clear
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo channel clear [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| `--project` | `-p` | Clear project config |
|
||||||
|
| `--all` | | Clear both user and project configs |
|
||||||
|
| `--force` | `-f` | Skip confirmation |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear user config (with confirmation)
|
||||||
|
stegasoo channel clear
|
||||||
|
|
||||||
|
# Clear project config
|
||||||
|
stegasoo channel clear --project
|
||||||
|
|
||||||
|
# Clear all configs without confirmation
|
||||||
|
stegasoo channel clear --all --force
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Info Command
|
||||||
|
|
||||||
|
Show information about an image file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo info IMAGE [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Compare Command
|
||||||
|
|
||||||
|
Compare embedding mode capacities for an image.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo compare IMAGE [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Modes Command
|
||||||
|
|
||||||
|
Show available embedding modes and their status.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo modes
|
||||||
|
```
|
||||||
|
|
||||||
|
Now also displays channel key status.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Admin Command
|
||||||
|
|
||||||
|
Manage Web UI admin accounts and recovery.
|
||||||
|
|
||||||
|
#### Subcommands
|
||||||
|
|
||||||
|
| Subcommand | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| `recover` | Reset admin password using recovery key |
|
||||||
|
|
||||||
|
#### admin recover
|
||||||
|
|
||||||
|
Reset the admin password for a Web UI database.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo admin recover --db PATH [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Short | Type | Required | Description |
|
||||||
|
|--------|-------|------|----------|-------------|
|
||||||
|
| `--db` | `-d` | path | ✓ | Path to stegasoo.db file |
|
||||||
|
| `--key` | `-k` | string | | Recovery key (prompted if not provided) |
|
||||||
|
| `--password` | `-p` | string | | New password (prompted if not provided) |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive mode (prompts for key and password)
|
||||||
|
stegasoo admin recover --db frontends/web/instance/stegasoo.db
|
||||||
|
|
||||||
|
# Non-interactive mode
|
||||||
|
stegasoo admin recover \
|
||||||
|
--db /path/to/stegasoo.db \
|
||||||
|
--key "XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" \
|
||||||
|
--password "NewSecurePassword123"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recovery process:**
|
||||||
|
1. The recovery key is verified against the database hash
|
||||||
|
2. If valid, the admin password is reset
|
||||||
|
3. User can now log in with the new password
|
||||||
|
|
||||||
|
**Note:** Recovery keys are instance-bound. A key from one database won't work on another.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tools Command
|
||||||
|
|
||||||
|
Image utilities and analysis tools.
|
||||||
|
|
||||||
|
#### Subcommands
|
||||||
|
|
||||||
|
| Subcommand | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| `exif` | View/edit EXIF metadata |
|
||||||
|
| `peek` | Check for hidden data |
|
||||||
|
| `strip` | Remove stego data from image |
|
||||||
|
|
||||||
|
#### tools exif
|
||||||
|
|
||||||
|
View and edit EXIF metadata in images.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo tools exif IMAGE [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `--clear` | flag | Remove all EXIF metadata |
|
||||||
|
| `--set FIELD=VALUE` | string | Set a specific EXIF field |
|
||||||
|
| `--output` / `-o` | path | Output filename (default: overwrites input) |
|
||||||
|
| `--json` | flag | Output as JSON |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View all EXIF data
|
||||||
|
stegasoo tools exif photo.jpg
|
||||||
|
|
||||||
|
# View as JSON
|
||||||
|
stegasoo tools exif photo.jpg --json
|
||||||
|
|
||||||
|
# Clear all metadata
|
||||||
|
stegasoo tools exif photo.jpg --clear -o clean.jpg
|
||||||
|
|
||||||
|
# Set specific fields
|
||||||
|
stegasoo tools exif photo.jpg \
|
||||||
|
--set "Artist=John Doe" \
|
||||||
|
--set "Copyright=2026" \
|
||||||
|
-o tagged.jpg
|
||||||
|
|
||||||
|
# Remove GPS data only
|
||||||
|
stegasoo tools exif photo.jpg \
|
||||||
|
--set "GPSLatitude=" \
|
||||||
|
--set "GPSLongitude=" \
|
||||||
|
-o no-gps.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
#### tools peek
|
||||||
|
|
||||||
|
Check if an image contains hidden Stegasoo data.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo tools peek IMAGE [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `--json` | flag | Output as JSON |
|
||||||
|
| `--quiet` / `-q` | flag | Exit code only (0=found, 1=not found) |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for hidden data
|
||||||
|
stegasoo tools peek suspicious.png
|
||||||
|
|
||||||
|
# Script-friendly check
|
||||||
|
if stegasoo tools peek image.png -q; then
|
||||||
|
echo "Contains hidden data"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### tools strip
|
||||||
|
|
||||||
|
Remove hidden stego data from an image (destructive).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo tools strip IMAGE [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `--output` / `-o` | path | Output filename |
|
||||||
|
| `--force` / `-f` | flag | Overwrite without confirmation |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Strip and save to new file
|
||||||
|
stegasoo tools strip stego.png -o clean.png
|
||||||
|
|
||||||
|
# Strip in place (with confirmation)
|
||||||
|
stegasoo tools strip stego.png
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Channel Keys
|
||||||
|
|
||||||
|
Channel keys provide **deployment/group isolation** - messages encoded with a channel key can only be decoded by systems with the same key.
|
||||||
|
|
||||||
|
### Key Format
|
||||||
|
|
||||||
|
```
|
||||||
|
ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
|
||||||
|
└──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘
|
||||||
|
8 groups of 4 alphanumeric characters (256 bits)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage Locations
|
||||||
|
|
||||||
|
Channel keys are checked in this order:
|
||||||
|
|
||||||
|
| Priority | Location | Best For |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| 1 | `STEGASOO_CHANNEL_KEY` env var | Docker, CI/CD |
|
||||||
|
| 2 | `./config/channel.key` | Project-specific |
|
||||||
|
| 3 | `~/.stegasoo/channel.key` | User default |
|
||||||
|
|
||||||
|
### Modes
|
||||||
|
|
||||||
|
| Mode | Description | CLI Option |
|
||||||
|
|------|-------------|------------|
|
||||||
|
| **Auto** | Use server-configured key | *(default)* |
|
||||||
|
| **Explicit** | Use specific key | `--channel KEY` |
|
||||||
|
| **Public** | No channel isolation | `--no-channel` |
|
||||||
|
|
||||||
|
### Fingerprints
|
||||||
|
|
||||||
|
For security, full keys aren't displayed by default. Instead, a fingerprint is shown:
|
||||||
|
|
||||||
|
```
|
||||||
|
Full key: ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
|
||||||
|
Fingerprint: ABCD-••••-••••-••••-••••-••••-••••-3456
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
**Team isolation:**
|
||||||
|
```bash
|
||||||
|
# Team A
|
||||||
|
export STEGASOO_CHANNEL_KEY=AAAA-1111-...
|
||||||
|
|
||||||
|
# Team B
|
||||||
|
export STEGASOO_CHANNEL_KEY=BBBB-2222-...
|
||||||
|
|
||||||
|
# Messages from Team A can only be decoded by Team A
|
||||||
|
```
|
||||||
|
|
||||||
|
**Development vs Production:**
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
./config/channel.key contains DEV-KEY-...
|
||||||
|
|
||||||
|
# Production
|
||||||
|
STEGASOO_CHANNEL_KEY=PROD-KEY-... in Docker
|
||||||
|
|
||||||
|
# Dev messages can't be decoded in production
|
||||||
|
```
|
||||||
|
|
||||||
|
**Public messages:**
|
||||||
|
```bash
|
||||||
|
# Anyone with credentials can decode
|
||||||
|
stegasoo encode ... --no-channel
|
||||||
|
stegasoo decode ... --no-channel
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Embedding Modes
|
||||||
|
|
||||||
|
### LSB Mode (Default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo encode ... --mode lsb
|
||||||
|
```
|
||||||
|
|
||||||
|
| Aspect | Details |
|
||||||
|
|--------|---------|
|
||||||
|
| **Capacity** | ~375 KB for 1920×1080 |
|
||||||
|
| **Output** | PNG only |
|
||||||
|
| **Best For** | Maximum capacity |
|
||||||
|
|
||||||
|
### DCT Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stegasoo encode ... --mode dct --dct-format jpeg --dct-color color
|
||||||
|
```
|
||||||
|
|
||||||
|
| Aspect | Details |
|
||||||
|
|--------|---------|
|
||||||
|
| **Capacity** | ~65 KB for 1920×1080 |
|
||||||
|
| **Output** | PNG or JPEG |
|
||||||
|
| **Best For** | Social media, stealth |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Factors
|
||||||
|
|
||||||
|
| Factor | Description | Entropy |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| Reference Photo | Shared image | ~80-256 bits |
|
||||||
|
| Passphrase | BIP-39 words | ~44 bits (4 words) |
|
||||||
|
| Static PIN | Numeric (6-9) | ~20 bits (6 digits) |
|
||||||
|
| RSA Key | Shared key file | ~128 bits |
|
||||||
|
| Channel Key (v4.0.0) | Deployment isolation | ~256 bits |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Examples
|
||||||
|
|
||||||
|
### Team Setup with Channel Key
|
||||||
|
|
||||||
|
**Initial setup (team lead):**
|
||||||
|
```bash
|
||||||
|
# Generate team channel key
|
||||||
|
stegasoo channel generate -q > team_channel.key
|
||||||
|
|
||||||
|
# Distribute to team members securely
|
||||||
|
# (encrypted email, secure file share, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Team member setup:**
|
||||||
|
```bash
|
||||||
|
# Save received key
|
||||||
|
stegasoo channel set --file team_channel.key
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
stegasoo channel show
|
||||||
|
```
|
||||||
|
|
||||||
|
**Daily use:**
|
||||||
|
```bash
|
||||||
|
# Channel key is used automatically
|
||||||
|
stegasoo encode -r ref.jpg -c meme.png -p "phrase" --pin 123456 -m "Team message"
|
||||||
|
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
**docker/docker-compose.yml:**
|
||||||
|
```yaml
|
||||||
|
x-common-env: &common-env
|
||||||
|
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
api:
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
```
|
||||||
|
|
||||||
|
**.env (gitignored):**
|
||||||
|
```bash
|
||||||
|
STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD Pipeline
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate key for CI
|
||||||
|
CHANNEL_KEY=$(stegasoo channel generate -q)
|
||||||
|
|
||||||
|
# Use in pipeline
|
||||||
|
STEGASOO_CHANNEL_KEY=$CHANNEL_KEY stegasoo encode ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Piping & Scripting
|
||||||
|
|
||||||
|
### Extract channel key for scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get just the key
|
||||||
|
KEY=$(stegasoo channel show --json | jq -r '.key // empty')
|
||||||
|
|
||||||
|
# Get fingerprint
|
||||||
|
FINGERPRINT=$(stegasoo channel show --json | jq -r '.fingerprint // "none"')
|
||||||
|
|
||||||
|
# Check if configured
|
||||||
|
if stegasoo channel show --json | jq -e '.configured' > /dev/null; then
|
||||||
|
echo "Channel key is configured"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate and use immediately
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate, save, and use
|
||||||
|
stegasoo channel generate --save
|
||||||
|
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "message"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Channel Key Errors
|
||||||
|
|
||||||
|
| Error | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| "Invalid channel key format" | Key doesn't match pattern | Use `stegasoo channel generate` |
|
||||||
|
| "Message encoded with channel key but none configured" | Missing channel key | Set key or use `--channel` |
|
||||||
|
| "Message encoded without channel key" | Used `--no-channel` to encode | Decode with `--no-channel` |
|
||||||
|
| "Channel key mismatch" | Wrong key | Verify correct key |
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current channel status
|
||||||
|
stegasoo channel show
|
||||||
|
|
||||||
|
# Try decoding with explicit key
|
||||||
|
stegasoo decode ... --channel XXXX-XXXX-...
|
||||||
|
|
||||||
|
# Try decoding without channel key
|
||||||
|
stegasoo decode ... --no-channel
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 0 | Success |
|
||||||
|
| 1 | General error / decryption failed |
|
||||||
|
| 2 | Invalid arguments/options |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `STEGASOO_CHANNEL_KEY` | Channel key for deployment isolation (v4.0.0) |
|
||||||
|
| `PYTHONPATH` | Include `src/` for development |
|
||||||
|
| `STEGASOO_DEBUG` | Enable debug output (set to `1`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [API Documentation](API.md) - Python API reference
|
||||||
|
- [Web UI Documentation](WEB_UI.md) - Browser interface guide
|
||||||
|
- [README](../README.md) - Project overview and security model
|
||||||
54
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying and enforcing our standards
|
||||||
|
of acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the project maintainers. All complaints will be reviewed and
|
||||||
|
investigated promptly and fairly.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
|
||||||
|
version 2.0.
|
||||||
165
CONTRIBUTING.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Contributing to Stegasoo
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Stegasoo! This document provides guidelines and information for contributors.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.10 - 3.12
|
||||||
|
- Git
|
||||||
|
- Docker (optional, for container testing)
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/adlee-was-taken/stegasoo.git
|
||||||
|
cd stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create a virtual environment**
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install development dependencies**
|
||||||
|
```bash
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Install pre-commit hooks**
|
||||||
|
```bash
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
We use the following tools to maintain code quality:
|
||||||
|
|
||||||
|
- **Black** - Code formatting (line length: 100)
|
||||||
|
- **Ruff** - Linting
|
||||||
|
- **MyPy** - Type checking
|
||||||
|
|
||||||
|
Run all checks before committing:
|
||||||
|
```bash
|
||||||
|
black src/ tests/ frontends/
|
||||||
|
ruff check src/ tests/ frontends/
|
||||||
|
mypy src/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
pytest --cov=stegasoo --cov-report=term-missing
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/test_stegasoo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Hints
|
||||||
|
|
||||||
|
All new code should include type hints:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def encode_message(
|
||||||
|
message: str,
|
||||||
|
carrier_image: bytes,
|
||||||
|
passphrase: str,
|
||||||
|
pin: str = "",
|
||||||
|
) -> EncodeResult:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Making Changes
|
||||||
|
|
||||||
|
### Branch Naming
|
||||||
|
|
||||||
|
- `feature/description` - New features
|
||||||
|
- `fix/description` - Bug fixes
|
||||||
|
- `docs/description` - Documentation updates
|
||||||
|
- `refactor/description` - Code refactoring
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
Write clear, concise commit messages:
|
||||||
|
|
||||||
|
```
|
||||||
|
Add channel key validation for private messaging
|
||||||
|
|
||||||
|
- Implement validate_channel_key() function
|
||||||
|
- Add tests for valid/invalid key formats
|
||||||
|
- Update CLI to support --channel-key flag
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Request Process
|
||||||
|
|
||||||
|
1. **Create a feature branch** from `main`
|
||||||
|
2. **Make your changes** with appropriate tests
|
||||||
|
3. **Ensure all checks pass** (tests, linting, formatting)
|
||||||
|
4. **Submit a PR** with a clear description
|
||||||
|
5. **Address review feedback** promptly
|
||||||
|
|
||||||
|
### PR Checklist
|
||||||
|
|
||||||
|
- [ ] Tests added/updated for changes
|
||||||
|
- [ ] Documentation updated if needed
|
||||||
|
- [ ] CHANGELOG.md updated for user-facing changes
|
||||||
|
- [ ] All CI checks passing
|
||||||
|
- [ ] No merge conflicts with `main`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
stegasoo/
|
||||||
|
├── src/stegasoo/ # Core library
|
||||||
|
│ ├── crypto.py # Encryption/decryption
|
||||||
|
│ ├── steganography.py # LSB embedding
|
||||||
|
│ ├── dct_steganography.py # DCT embedding
|
||||||
|
│ └── ...
|
||||||
|
├── frontends/
|
||||||
|
│ ├── cli/ # Command-line interface
|
||||||
|
│ ├── web/ # Flask web UI
|
||||||
|
│ └── api/ # FastAPI REST API
|
||||||
|
├── tests/ # Test suite
|
||||||
|
└── examples/ # Usage examples
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
### Bug Reports
|
||||||
|
|
||||||
|
Please include:
|
||||||
|
- Python version and OS
|
||||||
|
- Stegasoo version (`stegasoo --version`)
|
||||||
|
- Minimal reproduction steps
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Error messages/tracebacks
|
||||||
|
|
||||||
|
### Feature Requests
|
||||||
|
|
||||||
|
Please include:
|
||||||
|
- Use case description
|
||||||
|
- Proposed solution (if any)
|
||||||
|
- Alternatives considered
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
If you discover a security vulnerability, please see [SECURITY.md](SECURITY.md) for responsible disclosure guidelines. **Do not open a public issue for security vulnerabilities.**
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Feel free to open a discussion or issue if you have questions about contributing.
|
||||||
|
|
||||||
|
Thank you for helping make Stegasoo better!
|
||||||
156
DOCKER.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Docker Deployment
|
||||||
|
|
||||||
|
Stegasoo provides Docker images for both the Web UI and REST API.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start all services
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
docker-compose -f docker/docker-compose.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- **Web UI**: https://localhost:5000 (HTTPS with self-signed cert)
|
||||||
|
- **REST API**: http://localhost:8000
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
| Service | Port | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `web` | 5000 | Flask Web UI with authentication |
|
||||||
|
| `api` | 8000 | FastAPI REST API |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file or set these variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Channel key for private group communication (optional)
|
||||||
|
STEGASOO_CHANNEL_KEY=XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
|
||||||
|
|
||||||
|
# Web UI authentication (default: enabled)
|
||||||
|
STEGASOO_AUTH_ENABLED=true
|
||||||
|
|
||||||
|
# HTTPS support (default: enabled, generates self-signed cert)
|
||||||
|
STEGASOO_HTTPS_ENABLED=true
|
||||||
|
STEGASOO_HOSTNAME=localhost
|
||||||
|
|
||||||
|
# To disable HTTPS:
|
||||||
|
# STEGASOO_HTTPS_ENABLED=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume Mounts
|
||||||
|
|
||||||
|
Persistent data is stored in Docker volumes:
|
||||||
|
|
||||||
|
| Volume | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `stegasoo-web-data` | User database, session data |
|
||||||
|
| `stegasoo-web-certs` | SSL certificates (if HTTPS enabled) |
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
### Standard Build (Recommended)
|
||||||
|
|
||||||
|
Uses a pre-built base image with all dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First time only: build the base image
|
||||||
|
docker build -f docker/Dockerfile.base -t stegasoo-base:latest .
|
||||||
|
|
||||||
|
# Build services (fast - only copies app code)
|
||||||
|
docker-compose -f docker/docker-compose.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Build (No Base Image)
|
||||||
|
|
||||||
|
If you don't have the base image, the Dockerfile will build all dependencies (slower):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker/docker-compose.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start services
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose -f docker/docker-compose.yml logs -f
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker-compose -f docker/docker-compose.yml down
|
||||||
|
|
||||||
|
# Rebuild after code changes
|
||||||
|
docker-compose -f docker/docker-compose.yml build && docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
|
||||||
|
# Full rebuild (no cache)
|
||||||
|
docker-compose -f docker/docker-compose.yml build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource Limits
|
||||||
|
|
||||||
|
Each container is configured with:
|
||||||
|
- **Memory limit**: 768 MB
|
||||||
|
- **Memory reservation**: 384 MB
|
||||||
|
|
||||||
|
This accounts for Argon2id's 256 MB RAM requirement during key derivation.
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
Both services include health checks:
|
||||||
|
- Interval: 30 seconds
|
||||||
|
- Timeout: 10 seconds
|
||||||
|
- Start period: 5 seconds
|
||||||
|
- Retries: 3
|
||||||
|
|
||||||
|
Check health status:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker/docker-compose.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
For production, consider:
|
||||||
|
|
||||||
|
1. **Enable HTTPS**:
|
||||||
|
```bash
|
||||||
|
STEGASOO_HTTPS_ENABLED=true
|
||||||
|
STEGASOO_HOSTNAME=your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use secrets for channel key**:
|
||||||
|
```bash
|
||||||
|
# Don't commit .env files with secrets
|
||||||
|
export STEGASOO_CHANNEL_KEY=your-key
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Reverse proxy**: Put behind nginx/traefik for TLS termination
|
||||||
|
|
||||||
|
4. **Backup volumes**:
|
||||||
|
```bash
|
||||||
|
docker run --rm -v stegasoo-web-data:/data -v $(pwd):/backup \
|
||||||
|
alpine tar czf /backup/stegasoo-backup.tar.gz /data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container won't start
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose -f docker/docker-compose.yml logs web
|
||||||
|
docker-compose -f docker/docker-compose.yml logs api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Out of memory
|
||||||
|
Increase Docker's memory allocation or reduce worker count in `docker/Dockerfile`.
|
||||||
|
|
||||||
|
### Permission errors
|
||||||
|
The containers run as non-root user `stego` (UID 1000). Ensure volume permissions match.
|
||||||
950
INSTALL.md
Normal file
@@ -0,0 +1,950 @@
|
|||||||
|
# Stegasoo Installation Guide
|
||||||
|
|
||||||
|
Complete installation instructions for all platforms and deployment methods.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [Quick Install](#quick-install)
|
||||||
|
- [Installation Methods](#installation-methods)
|
||||||
|
- [From Source (Development)](#from-source-development)
|
||||||
|
- [From PyPI](#from-pypi)
|
||||||
|
- [Docker](#docker)
|
||||||
|
- [Docker Compose](#docker-compose)
|
||||||
|
- [Optional Dependencies](#optional-dependencies)
|
||||||
|
- [Platform-Specific Notes](#platform-specific-notes)
|
||||||
|
- [Verification](#verification)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Python Version Requirements
|
||||||
|
|
||||||
|
| Python Version | Status | Notes |
|
||||||
|
|----------------|--------|-------|
|
||||||
|
| 3.10 | ❌ Not Supported | Dropped in v4.2.1 |
|
||||||
|
| 3.11 | ✅ Supported | Minimum version |
|
||||||
|
| 3.12 | ✅ Supported | Recommended |
|
||||||
|
| 3.13 | ✅ Supported | |
|
||||||
|
| 3.14 | ✅ Supported | Tested on Arch |
|
||||||
|
|
||||||
|
**Note:** v4.2.1 switched from `jpegio` to `jpeglib` for DCT steganography, enabling Python 3.11-3.14 support.
|
||||||
|
|
||||||
|
### Minimum Requirements
|
||||||
|
|
||||||
|
| Requirement | Value |
|
||||||
|
|-------------|-------|
|
||||||
|
| Python | 3.11-3.14 |
|
||||||
|
| RAM | 512 MB minimum (256MB for Argon2) |
|
||||||
|
| Disk | ~100 MB |
|
||||||
|
|
||||||
|
### System Dependencies
|
||||||
|
|
||||||
|
**Linux (Debian/Ubuntu):**
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
python3.12 \
|
||||||
|
python3.12-venv \
|
||||||
|
python3-pip \
|
||||||
|
python3-dev \
|
||||||
|
libzbar0 \
|
||||||
|
libjpeg-dev \
|
||||||
|
build-essential
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux (Arch):**
|
||||||
|
```bash
|
||||||
|
# Use pyenv for Python version management
|
||||||
|
curl https://pyenv.run | bash
|
||||||
|
pyenv install 3.12
|
||||||
|
pyenv local 3.12
|
||||||
|
|
||||||
|
sudo pacman -S zbar libjpeg-turbo base-devel
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
brew install python@3.12 zbar jpeg
|
||||||
|
xcode-select --install # For compilation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
- Install Python 3.12 from [python.org](https://python.org)
|
||||||
|
- Install Visual Studio Build Tools for compilation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone and install everything
|
||||||
|
git clone https://github.com/adlee-was-taken/stegasoo.git
|
||||||
|
cd stegasoo
|
||||||
|
|
||||||
|
# Create venv with Python 3.12 (critical!)
|
||||||
|
python3.12 -m venv venv
|
||||||
|
source venv/bin/activate # Linux/macOS
|
||||||
|
# or: venv\Scripts\activate # Windows
|
||||||
|
|
||||||
|
# Install all dependencies
|
||||||
|
pip install -e ".[all]"
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
stegasoo --version
|
||||||
|
python -c "from stegasoo import has_dct_support; print(f'DCT: {has_dct_support()}')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation Methods
|
||||||
|
|
||||||
|
### From Source (Development)
|
||||||
|
|
||||||
|
Best for development or customization.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/adlee-was-taken/stegasoo.git
|
||||||
|
cd stegasoo
|
||||||
|
|
||||||
|
# Create virtual environment with Python 3.12 (recommended)
|
||||||
|
python3.12 -m venv venv
|
||||||
|
source venv/bin/activate # Linux/macOS
|
||||||
|
# or: venv\Scripts\activate # Windows
|
||||||
|
|
||||||
|
# Verify Python version
|
||||||
|
python -V # Should show 3.12.x
|
||||||
|
|
||||||
|
# Install core library only
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Install with specific extras
|
||||||
|
pip install -e ".[cli]" # Command-line interface
|
||||||
|
pip install -e ".[web]" # Flask web UI + DCT support
|
||||||
|
pip install -e ".[api]" # FastAPI REST API + DCT support
|
||||||
|
pip install -e ".[dct]" # DCT steganography only
|
||||||
|
pip install -e ".[compression]" # LZ4 compression
|
||||||
|
|
||||||
|
# Install everything
|
||||||
|
pip install -e ".[all]"
|
||||||
|
|
||||||
|
# Install with development tools
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### From PyPI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Core only
|
||||||
|
pip install stegasoo
|
||||||
|
|
||||||
|
# With extras
|
||||||
|
pip install stegasoo[cli]
|
||||||
|
pip install stegasoo[web]
|
||||||
|
pip install stegasoo[api]
|
||||||
|
pip install stegasoo[all]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Build and run individual containers.
|
||||||
|
|
||||||
|
#### Build Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From project root - build all targets
|
||||||
|
docker build -t stegasoo-web --target web -f docker/Dockerfile .
|
||||||
|
docker build -t stegasoo-api --target api -f docker/Dockerfile .
|
||||||
|
docker build -t stegasoo-cli --target cli -f docker/Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run Web UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name stegasoo-web \
|
||||||
|
-p 5000:5000 \
|
||||||
|
--memory=768m \
|
||||||
|
stegasoo-web
|
||||||
|
|
||||||
|
# Visit http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run REST API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name stegasoo-api \
|
||||||
|
-p 8000:8000 \
|
||||||
|
--memory=768m \
|
||||||
|
stegasoo-api
|
||||||
|
|
||||||
|
# Docs at http://localhost:8000/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive shell
|
||||||
|
docker run -it --rm stegasoo-cli /bin/bash
|
||||||
|
|
||||||
|
# Run commands directly
|
||||||
|
docker run --rm stegasoo-cli --help
|
||||||
|
docker run --rm stegasoo-cli generate --pin --words 4
|
||||||
|
|
||||||
|
# With volume for files
|
||||||
|
docker run --rm \
|
||||||
|
-v $(pwd)/images:/data \
|
||||||
|
stegasoo-cli encode \
|
||||||
|
-r /data/ref.jpg \
|
||||||
|
-c /data/carrier.png \
|
||||||
|
-p "passphrase words here more" \
|
||||||
|
--pin 123456 \
|
||||||
|
-m "Secret message" \
|
||||||
|
-o /data/stego.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
The easiest way to run all services.
|
||||||
|
|
||||||
|
#### Start All Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start in background
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
|
||||||
|
# Start specific service
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d web
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d api
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose -f docker/docker-compose.yml logs -f
|
||||||
|
|
||||||
|
# Stop all
|
||||||
|
docker-compose -f docker/docker-compose.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Authentication Configuration (v4.0.2)
|
||||||
|
|
||||||
|
The Web UI supports optional authentication. Configure via environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env file (create in project root)
|
||||||
|
STEGASOO_AUTH_ENABLED=true # Enable login (default: true)
|
||||||
|
STEGASOO_HTTPS_ENABLED=false # Enable HTTPS (default: false)
|
||||||
|
STEGASOO_HOSTNAME=localhost # Hostname for SSL cert
|
||||||
|
STEGASOO_CHANNEL_KEY= # Optional channel key
|
||||||
|
|
||||||
|
# Then run
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d web
|
||||||
|
```
|
||||||
|
|
||||||
|
On first access, you'll be prompted to create an admin account. The database and SSL certs are persisted in Docker volumes.
|
||||||
|
|
||||||
|
#### Services
|
||||||
|
|
||||||
|
| Service | URL | Description |
|
||||||
|
|---------|-----|-------------|
|
||||||
|
| `web` | http://localhost:5000 | Flask Web UI |
|
||||||
|
| `api` | http://localhost:8000 | FastAPI REST API |
|
||||||
|
|
||||||
|
#### Build and Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build images and start
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d --build
|
||||||
|
|
||||||
|
# Force rebuild (no cache)
|
||||||
|
docker-compose -f docker/docker-compose.yml build --no-cache
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resource Configuration
|
||||||
|
|
||||||
|
The `docker/docker-compose.yml` includes resource limits:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 768M # For Argon2 + scipy
|
||||||
|
reservations:
|
||||||
|
memory: 384M
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust based on your available RAM:
|
||||||
|
|
||||||
|
| Available RAM | Recommended Limit | Workers |
|
||||||
|
|---------------|-------------------|---------|
|
||||||
|
| 2 GB | 768M | 2 |
|
||||||
|
| 4 GB | 1G | 3 |
|
||||||
|
| 8 GB+ | 1.5G | 4 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optional Dependencies
|
||||||
|
|
||||||
|
### DCT Steganography (scipy + jpegio)
|
||||||
|
|
||||||
|
DCT mode enables JPEG-resilient steganography. It's automatically included with `[web]`, `[api]`, and `[all]` extras.
|
||||||
|
|
||||||
|
#### Install via pip
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# scipy is straightforward
|
||||||
|
pip install scipy numpy
|
||||||
|
|
||||||
|
# jpegio - MUST use Python 3.12 or earlier!
|
||||||
|
pip install jpegio
|
||||||
|
|
||||||
|
# If pip fails, build from source
|
||||||
|
pip install cython numpy
|
||||||
|
git clone https://github.com/dwgoon/jpegio.git
|
||||||
|
cd jpegio
|
||||||
|
python setup.py install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux Build Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
python3-dev \
|
||||||
|
libjpeg-dev \
|
||||||
|
cython3
|
||||||
|
```
|
||||||
|
|
||||||
|
#### macOS Build Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install jpeg cython
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Verify DCT Support
|
||||||
|
|
||||||
|
```python
|
||||||
|
from stegasoo import has_dct_support
|
||||||
|
from stegasoo.dct_steganography import has_jpegio_support
|
||||||
|
|
||||||
|
print(f"DCT support (scipy): {has_dct_support()}")
|
||||||
|
print(f"JPEG native (jpegio): {has_jpegio_support()}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
DCT support (scipy): True
|
||||||
|
JPEG native (jpegio): True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compression (lz4)
|
||||||
|
|
||||||
|
Optional LZ4 compression for messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install lz4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Platform-Specific Notes
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
Most straightforward installation. Use your package manager for system dependencies.
|
||||||
|
|
||||||
|
**Ubuntu/Debian:**
|
||||||
|
```bash
|
||||||
|
sudo apt-get install python3.12 python3.12-venv python3-dev libzbar0 libjpeg-dev
|
||||||
|
python3.12 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install stegasoo[all]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fedora/RHEL:**
|
||||||
|
```bash
|
||||||
|
sudo dnf install python3.12 python3-devel zbar libjpeg-devel
|
||||||
|
python3.12 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install stegasoo[all]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arch (using pyenv):**
|
||||||
|
```bash
|
||||||
|
# Install pyenv
|
||||||
|
curl https://pyenv.run | bash
|
||||||
|
|
||||||
|
# Add to ~/.bashrc or ~/.zshrc
|
||||||
|
export PATH="$HOME/.pyenv/bin:$PATH"
|
||||||
|
eval "$(pyenv init -)"
|
||||||
|
|
||||||
|
# Install Python 3.12
|
||||||
|
pyenv install 3.12
|
||||||
|
cd ~/Sources/stegasoo
|
||||||
|
pyenv local 3.12
|
||||||
|
|
||||||
|
# Create venv and install
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install stegasoo[all]
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Homebrew if needed
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
brew install python@3.12 zbar jpeg
|
||||||
|
|
||||||
|
# Create venv
|
||||||
|
python3.12 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install Stegasoo
|
||||||
|
pip install stegasoo[all]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apple Silicon (M1/M2/M3):**
|
||||||
|
|
||||||
|
jpegio may need native compilation:
|
||||||
|
```bash
|
||||||
|
# Ensure you have native Python
|
||||||
|
arch -arm64 brew install python@3.12
|
||||||
|
arch -arm64 python3.12 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install jpegio
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Windows users have three options, listed from easiest to most complex:
|
||||||
|
|
||||||
|
#### Option 1: Docker Desktop (Recommended)
|
||||||
|
|
||||||
|
The easiest way to run Stegasoo on Windows. No Python installation needed.
|
||||||
|
|
||||||
|
1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||||
|
2. Enable WSL2 backend when prompted
|
||||||
|
3. Clone and run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://github.com/adlee-was-taken/stegasoo.git
|
||||||
|
cd stegasoo
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d web
|
||||||
|
```
|
||||||
|
|
||||||
|
Access at http://localhost:5000
|
||||||
|
|
||||||
|
#### Option 2: WSL2 (Windows Subsystem for Linux)
|
||||||
|
|
||||||
|
Run the Linux version natively on Windows.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Install WSL2 with Ubuntu
|
||||||
|
wsl --install -d Ubuntu
|
||||||
|
|
||||||
|
# Open Ubuntu terminal, then follow Linux instructions:
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y python3.12 python3.12-venv libzbar0 libjpeg-dev
|
||||||
|
git clone https://github.com/adlee-was-taken/stegasoo.git
|
||||||
|
cd stegasoo
|
||||||
|
python3.12 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -e ".[all]"
|
||||||
|
stegasoo --version
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 3: Native Windows (Advanced)
|
||||||
|
|
||||||
|
Native Windows installation requires Visual Studio Build Tools for compiling C extensions.
|
||||||
|
|
||||||
|
1. Install Python 3.11 or 3.12 from [python.org](https://python.org)
|
||||||
|
2. Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with "Desktop development with C++"
|
||||||
|
3. Install from pip:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv venv
|
||||||
|
.\venv\Scripts\activate
|
||||||
|
pip install stegasoo[cli] # CLI only (easiest)
|
||||||
|
# or
|
||||||
|
pip install stegasoo[all] # Full install (may require additional setup)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Native Windows installation may have issues with `jpegio` (DCT mode). Docker or WSL2 is recommended for full functionality.
|
||||||
|
|
||||||
|
### Raspberry Pi
|
||||||
|
|
||||||
|
Stegasoo works on Raspberry Pi 4/5 (4GB+ RAM recommended for Web UI).
|
||||||
|
|
||||||
|
#### Step 1: Install System Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libbz2-dev \
|
||||||
|
libreadline-dev \
|
||||||
|
libsqlite3-dev \
|
||||||
|
libncursesw5-dev \
|
||||||
|
xz-utils \
|
||||||
|
tk-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libxmlsec1-dev \
|
||||||
|
libffi-dev \
|
||||||
|
liblzma-dev \
|
||||||
|
libzbar0 \
|
||||||
|
libjpeg-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Install Python 3.12 via pyenv
|
||||||
|
|
||||||
|
Raspberry Pi OS ships with Python 3.13, which is **not compatible** with jpegio. Install Python 3.12:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install pyenv
|
||||||
|
curl https://pyenv.run | bash
|
||||||
|
|
||||||
|
# Add to ~/.bashrc
|
||||||
|
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
|
||||||
|
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
|
||||||
|
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
|
# Install Python 3.12 (takes ~10 minutes on Pi 5)
|
||||||
|
pyenv install 3.12
|
||||||
|
pyenv global 3.12
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Build jpegio for ARM
|
||||||
|
|
||||||
|
The upstream jpegio has x86-specific build flags. Patch and build from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone jpegio
|
||||||
|
git clone https://github.com/dwgoon/jpegio.git
|
||||||
|
cd jpegio
|
||||||
|
|
||||||
|
# Patch for ARM (removes x86-specific -m64 flag)
|
||||||
|
sed -i "s/cargs.append('-m64')/pass # ARM fix/" setup.py
|
||||||
|
|
||||||
|
# Build and install
|
||||||
|
pip install .
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Install Stegasoo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone Stegasoo
|
||||||
|
git clone https://github.com/adlee-was-taken/stegasoo.git
|
||||||
|
cd stegasoo
|
||||||
|
|
||||||
|
# Create venv with Python 3.12
|
||||||
|
~/.pyenv/versions/3.12.*/bin/python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install (jpegio already installed, skip it)
|
||||||
|
pip install -e ".[web]" --no-deps
|
||||||
|
pip install argon2-cffi cryptography pillow flask gunicorn scipy numpy pyzbar qrcode
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 5: Run the Web UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontends/web
|
||||||
|
|
||||||
|
# Optional: Enable authentication
|
||||||
|
export STEGASOO_AUTH_ENABLED=true
|
||||||
|
|
||||||
|
# Optional: Enable HTTPS for local network security
|
||||||
|
export STEGASOO_HTTPS_ENABLED=true
|
||||||
|
export STEGASOO_HOSTNAME=raspberrypi.local
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
python app.py
|
||||||
|
# Access at http://<pi-ip>:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Verify Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "
|
||||||
|
import stegasoo
|
||||||
|
from stegasoo.dct_steganography import has_jpegio_support
|
||||||
|
print(f'Stegasoo: {stegasoo.__version__}')
|
||||||
|
print(f'Argon2: {stegasoo.has_argon2()}')
|
||||||
|
print(f'DCT: {stegasoo.has_dct_support()}')
|
||||||
|
print(f'jpegio: {has_jpegio_support()}')
|
||||||
|
"
|
||||||
|
# Expected: All True
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
- **RAM**: Web UI needs ~768MB free for Argon2 + scipy operations
|
||||||
|
- **Performance**: Argon2 operations take 3-5 seconds on Pi 5 (vs ~2s on desktop)
|
||||||
|
- **Python 3.13**: Not supported due to jpegio C extension incompatibility
|
||||||
|
- **First run**: Will prompt you to create an admin account
|
||||||
|
- **HTTPS**: Generates self-signed certificate (browsers will warn)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom SSL Certificates
|
||||||
|
|
||||||
|
By default, Stegasoo generates a self-signed certificate for HTTPS. To use your own certificate (e.g., from Let's Encrypt or your organization's CA):
|
||||||
|
|
||||||
|
### Replace Self-Signed Certificates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop the service
|
||||||
|
sudo systemctl stop stegasoo
|
||||||
|
|
||||||
|
# Backup existing certs (optional)
|
||||||
|
mv /opt/stegasoo/frontends/web/certs /opt/stegasoo/frontends/web/certs.bak
|
||||||
|
|
||||||
|
# Create new certs directory
|
||||||
|
mkdir -p /opt/stegasoo/frontends/web/certs
|
||||||
|
|
||||||
|
# Copy your certificates (adjust paths as needed)
|
||||||
|
cp /path/to/your/certificate.crt /opt/stegasoo/frontends/web/certs/server.crt
|
||||||
|
cp /path/to/your/private.key /opt/stegasoo/frontends/web/certs/server.key
|
||||||
|
|
||||||
|
# Set permissions (key must be readable by service user)
|
||||||
|
chmod 600 /opt/stegasoo/frontends/web/certs/server.key
|
||||||
|
chown -R $(whoami):$(whoami) /opt/stegasoo/frontends/web/certs
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
sudo systemctl start stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate New Self-Signed Certificate
|
||||||
|
|
||||||
|
If your certificate expires or you need to regenerate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop service
|
||||||
|
sudo systemctl stop stegasoo
|
||||||
|
|
||||||
|
# Generate new cert with SANs
|
||||||
|
CERT_DIR="/opt/stegasoo/frontends/web/certs"
|
||||||
|
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
|
||||||
|
openssl req -x509 -newkey rsa:2048 \
|
||||||
|
-keyout "$CERT_DIR/server.key" \
|
||||||
|
-out "$CERT_DIR/server.crt" \
|
||||||
|
-days 365 -nodes \
|
||||||
|
-subj "/O=Stegasoo/CN=$HOSTNAME" \
|
||||||
|
-addext "subjectAltName=DNS:$HOSTNAME,DNS:$HOSTNAME.local,DNS:localhost,IP:$LOCAL_IP,IP:127.0.0.1"
|
||||||
|
|
||||||
|
chmod 600 "$CERT_DIR/server.key"
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
sudo systemctl start stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Let's Encrypt with Certbot
|
||||||
|
|
||||||
|
For publicly accessible servers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install certbot
|
||||||
|
sudo apt install certbot
|
||||||
|
|
||||||
|
# Get certificate (standalone mode)
|
||||||
|
sudo certbot certonly --standalone -d yourdomain.com
|
||||||
|
|
||||||
|
# Copy to Stegasoo
|
||||||
|
sudo cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /opt/stegasoo/frontends/web/certs/server.crt
|
||||||
|
sudo cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /opt/stegasoo/frontends/web/certs/server.key
|
||||||
|
sudo chown $(whoami):$(whoami) /opt/stegasoo/frontends/web/certs/*
|
||||||
|
sudo chmod 600 /opt/stegasoo/frontends/web/certs/server.key
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
sudo systemctl restart stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Set up a cron job or systemd timer to copy renewed certificates and restart Stegasoo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Check Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CLI version
|
||||||
|
stegasoo --version
|
||||||
|
|
||||||
|
# Python import
|
||||||
|
python -c "import stegasoo; print(stegasoo.__version__)"
|
||||||
|
|
||||||
|
# Check Python version (must be 3.10-3.12)
|
||||||
|
python -V
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check All Features
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify Stegasoo installation."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def check_feature(name, check_fn):
|
||||||
|
try:
|
||||||
|
result = check_fn()
|
||||||
|
status = "✓" if result else "✗"
|
||||||
|
print(f" {status} {name}: {result}")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ {name}: Error - {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("Stegasoo Installation Check")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
# Python version check
|
||||||
|
py_version = sys.version_info
|
||||||
|
print(f"\nPython: {py_version.major}.{py_version.minor}.{py_version.micro}")
|
||||||
|
if py_version >= (3, 13):
|
||||||
|
print(" ⚠️ WARNING: Python 3.13+ not supported!")
|
||||||
|
print(" jpegio will not work. Use Python 3.12.")
|
||||||
|
elif py_version >= (3, 10):
|
||||||
|
print(" ✓ Python version OK")
|
||||||
|
else:
|
||||||
|
print(" ✗ Python 3.10+ required")
|
||||||
|
|
||||||
|
# Core
|
||||||
|
import stegasoo
|
||||||
|
print(f"\nStegasoo Version: {stegasoo.__version__}")
|
||||||
|
|
||||||
|
print("\nCore Features:")
|
||||||
|
check_feature("Argon2", lambda: stegasoo.has_argon2())
|
||||||
|
check_feature("Pillow", lambda: True) # Required, would fail import
|
||||||
|
|
||||||
|
print("\nOptional Features:")
|
||||||
|
check_feature("DCT (scipy)", stegasoo.has_dct_support)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from stegasoo.dct_steganography import has_jpegio_support
|
||||||
|
check_feature("JPEG native (jpegio)", has_jpegio_support)
|
||||||
|
except ImportError:
|
||||||
|
print(" ✗ JPEG native (jpegio): Not installed")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import lz4
|
||||||
|
check_feature("Compression (lz4)", lambda: True)
|
||||||
|
except ImportError:
|
||||||
|
print(" - Compression (lz4): Not installed (optional)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyzbar
|
||||||
|
check_feature("QR codes (pyzbar)", lambda: True)
|
||||||
|
except ImportError:
|
||||||
|
print(" - QR codes (pyzbar): Not installed (optional)")
|
||||||
|
|
||||||
|
print("\nInterfaces:")
|
||||||
|
try:
|
||||||
|
import click
|
||||||
|
check_feature("CLI", lambda: True)
|
||||||
|
except ImportError:
|
||||||
|
print(" ✗ CLI: Not installed")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import flask
|
||||||
|
check_feature("Web UI", lambda: True)
|
||||||
|
except ImportError:
|
||||||
|
print(" - Web UI: Not installed")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import fastapi
|
||||||
|
check_feature("REST API", lambda: True)
|
||||||
|
except ImportError:
|
||||||
|
print(" - REST API: Not installed")
|
||||||
|
|
||||||
|
print("\n" + "=" * 40)
|
||||||
|
print("Installation check complete!")
|
||||||
|
```
|
||||||
|
|
||||||
|
Save as `check_install.py` and run:
|
||||||
|
```bash
|
||||||
|
python check_install.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Encoding/Decoding
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick test with CLI
|
||||||
|
stegasoo generate --pin --words 4 --json > /tmp/creds.json
|
||||||
|
|
||||||
|
# Create test image
|
||||||
|
python -c "
|
||||||
|
from PIL import Image
|
||||||
|
img = Image.new('RGB', (256, 256), 'blue')
|
||||||
|
img.save('/tmp/test_carrier.png')
|
||||||
|
img.save('/tmp/test_ref.jpg')
|
||||||
|
"
|
||||||
|
|
||||||
|
# Encode
|
||||||
|
stegasoo encode \
|
||||||
|
-r /tmp/test_ref.jpg \
|
||||||
|
-c /tmp/test_carrier.png \
|
||||||
|
-p "test phrase words here" \
|
||||||
|
--pin 123456 \
|
||||||
|
-m "Hello, Stegasoo!" \
|
||||||
|
-o /tmp/test_stego.png
|
||||||
|
|
||||||
|
# Decode
|
||||||
|
stegasoo decode \
|
||||||
|
-r /tmp/test_ref.jpg \
|
||||||
|
-s /tmp/test_stego.png \
|
||||||
|
-p "test phrase words here" \
|
||||||
|
--pin 123456
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### "jpegio crashes" / "free(): invalid size" / Core dump
|
||||||
|
|
||||||
|
**This is the #1 issue!** You're using Python 3.13.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check your Python version
|
||||||
|
python -V
|
||||||
|
|
||||||
|
# If it shows 3.13, you need to use 3.12
|
||||||
|
# Option 1: Use pyenv
|
||||||
|
pyenv install 3.12
|
||||||
|
pyenv local 3.12
|
||||||
|
|
||||||
|
# Option 2: Use system Python 3.12
|
||||||
|
python3.12 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -e ".[all]"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "No module named 'stegasoo'"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure you're in the right environment
|
||||||
|
which python
|
||||||
|
pip list | grep stegasoo
|
||||||
|
|
||||||
|
# Reinstall
|
||||||
|
pip install -e ".[all]"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "Argon2 not available"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install argon2-cffi
|
||||||
|
pip install argon2-cffi
|
||||||
|
|
||||||
|
# On Linux, may need:
|
||||||
|
sudo apt-get install libffi-dev
|
||||||
|
pip install --force-reinstall argon2-cffi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "jpegio not available" (not crash, just missing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install build dependencies first
|
||||||
|
sudo apt-get install libjpeg-dev # Linux
|
||||||
|
brew install jpeg # macOS
|
||||||
|
|
||||||
|
# Then install jpegio
|
||||||
|
pip install cython numpy
|
||||||
|
pip install jpegio
|
||||||
|
|
||||||
|
# If still fails, build from source
|
||||||
|
git clone https://github.com/dwgoon/jpegio.git
|
||||||
|
cd jpegio
|
||||||
|
python setup.py install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "libzbar not found" (QR codes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
sudo apt-get install libzbar0
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
brew install zbar
|
||||||
|
|
||||||
|
# Then reinstall pyzbar
|
||||||
|
pip install --force-reinstall pyzbar
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker: "Cannot allocate memory"
|
||||||
|
|
||||||
|
Argon2 needs 256MB per operation. Increase container memory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker run
|
||||||
|
docker run --memory=768m ...
|
||||||
|
|
||||||
|
# Docker Compose - edit docker/docker-compose.yml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 768M
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Slow performance
|
||||||
|
|
||||||
|
- **Argon2 is intentionally slow** - This is a security feature
|
||||||
|
- Expected encode/decode time: 2-5 seconds
|
||||||
|
- DCT mode adds ~1-2 seconds for transforms
|
||||||
|
- Large images (10MB+) may take 15-30 seconds
|
||||||
|
|
||||||
|
#### "Carrier image too small"
|
||||||
|
|
||||||
|
- LSB needs ~3 bits per pixel
|
||||||
|
- DCT needs ~0.25 bits per pixel
|
||||||
|
- For 50KB message: LSB needs ~136K pixels, DCT needs ~1.6M pixels
|
||||||
|
- Use larger carrier images or shorter messages
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
1. Check the documentation:
|
||||||
|
- [README.md](README.md)
|
||||||
|
- [CLI.md](CLI.md)
|
||||||
|
- [API.md](API.md)
|
||||||
|
- [WEB_UI.md](WEB_UI.md)
|
||||||
|
|
||||||
|
2. Check existing issues on GitHub
|
||||||
|
|
||||||
|
3. Open a new issue with:
|
||||||
|
- Python version (`python --version`)
|
||||||
|
- OS and version
|
||||||
|
- Installation method
|
||||||
|
- Full error message
|
||||||
|
- Steps to reproduce
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After installation:
|
||||||
|
|
||||||
|
1. **Generate credentials**: `stegasoo generate --pin --words 4`
|
||||||
|
2. **Read the CLI docs**: [CLI.md](CLI.md)
|
||||||
|
3. **Try the Web UI**: `cd frontends/web && python app.py`
|
||||||
|
4. **Explore the API**: `cd frontends/api && python main.py`
|
||||||
|
|
||||||
|
Happy steganography! 🦕
|
||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024-2025 Aaron D. Lee
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
97
PLAN-4.1.6.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Stegasoo v4.1.6 Planning
|
||||||
|
|
||||||
|
## UI Tweaks
|
||||||
|
|
||||||
|
### 1. Revamp Tron Lines Animation (Carrier/Stego Image)
|
||||||
|
**Current state:**
|
||||||
|
- 6-8 snake paths, each with 3-5 segments (~24-40 total lines)
|
||||||
|
- 2px thick lines
|
||||||
|
- 30-60px length per segment
|
||||||
|
- Starting points spread across 80% of image area
|
||||||
|
- Colors: yellow, cyan, purple, blue with glow
|
||||||
|
|
||||||
|
**Target improvements:**
|
||||||
|
- [x] Thinner lines (1px instead of 2px)
|
||||||
|
- [x] More numerous (20-40 paths via 5x4 grid, ~60-200 segments total)
|
||||||
|
- [x] Better distribution across entire image (grid-based seeding)
|
||||||
|
- [x] Shorter segments (12-30px) for denser "circuit board" look
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `frontends/web/static/style.css` (~881-979) - `.embed-trace` styling
|
||||||
|
- `frontends/web/static/js/stegasoo.js` (~333-390) - `generateEmbedTraces()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools Page Expansion
|
||||||
|
|
||||||
|
### Analysis Tools
|
||||||
|
- [x] **JPEG Compression Tester** - Preview image at different quality levels (10-100%), show file size delta. Useful for understanding stego survivability.
|
||||||
|
- [ ] **LSB Plane Viewer** - Visualize least significant bit plane(s) of RGB channels. Classic stego analysis tool.
|
||||||
|
- [ ] **Histogram Viewer** - Color distribution graph per channel. Anomalies can indicate hidden data.
|
||||||
|
- [ ] **Image Diff** - Compare two images side-by-side with pixel difference highlighting. Great for original vs stego comparison.
|
||||||
|
- [ ] **Noise Analysis** - Chi-square or similar statistical analysis for detecting LSB embedding.
|
||||||
|
|
||||||
|
### Transform Tools
|
||||||
|
- [x] **Rotate/Flip** - 90°/180°/270° rotation, horizontal/vertical flip
|
||||||
|
- [ ] **Resize** - Scale with aspect ratio lock, common presets (50%, 25%, etc.)
|
||||||
|
- [ ] **Crop** - Basic rectangular crop with preview
|
||||||
|
- [x] **Format Convert** - PNG ↔ JPEG ↔ WebP with quality slider
|
||||||
|
|
||||||
|
### Existing Tools (already done)
|
||||||
|
- [x] Capacity Calculator
|
||||||
|
- [x] EXIF Viewer
|
||||||
|
- [x] EXIF Strip
|
||||||
|
- [x] Image Peek (header analysis)
|
||||||
|
|
||||||
|
### Tools UI/UX Overhaul
|
||||||
|
|
||||||
|
**Final Layout: Office-style Ribbon + Two-Panel**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 📏 📋 👁️ 📊 ┃ ✂️ 🔄 📐 🔀 Image Tools │ ← Icon toolbar
|
||||||
|
├────────────────────────────────────────┬────────────────────┤
|
||||||
|
│ [Format: PNG ▼] [Quality: 85] │ │
|
||||||
|
├────────────────────────────────────────┤ Capacity │
|
||||||
|
│ │ Calculator │
|
||||||
|
│ ┌────────────────────────────┐ │ ────────────── │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ Drop image here │ │ Dimensions: │
|
||||||
|
│ │ or click │ │ 1920 × 1080 │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └────────────────────────────┘ │ LSB Capacity: │
|
||||||
|
│ │ 245 KB │
|
||||||
|
│ [image.jpg] │ │
|
||||||
|
│ │ ────────────── │
|
||||||
|
│ │ [Clear] [Export] │
|
||||||
|
└────────────────────────────────────────┴────────────────────┘
|
||||||
|
Options + dropzone/preview Results sidebar
|
||||||
|
```
|
||||||
|
|
||||||
|
- Top ribbon: Icon buttons grouped by category (Analyze | Transform)
|
||||||
|
- Left panel: Tool options + dropzone/preview (INPUT)
|
||||||
|
- Right panel: Tool name + results/metadata + actions (OUTPUT)
|
||||||
|
- Flow: Left → Right (input → output)
|
||||||
|
|
||||||
|
**Implementation Tasks:**
|
||||||
|
- [x] Move inline CSS to style.css
|
||||||
|
- [x] Build icon toolbar ribbon
|
||||||
|
- [x] Build two-panel layout structure
|
||||||
|
- [x] Migrate existing tools (Capacity, EXIF, Strip)
|
||||||
|
- [x] Add new tools (Rotate, Compress, Convert)
|
||||||
|
- [ ] Loading spinner on all async operations
|
||||||
|
- [ ] Toast notifications instead of alerts
|
||||||
|
- [ ] Consistent color coding (green=analysis, amber=transform)
|
||||||
|
- [ ] Mobile: stack panels vertically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Improvements
|
||||||
|
|
||||||
|
### (Add items here)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other UI Tweaks
|
||||||
|
|
||||||
|
### (Add items here)
|
||||||
|
|
||||||
285
README.md
@@ -2,225 +2,156 @@
|
|||||||
|
|
||||||
A secure steganography system for hiding encrypted messages in images using hybrid authentication.
|
A secure steganography system for hiding encrypted messages in images using hybrid authentication.
|
||||||
|
|
||||||

|
[](https://github.com/adlee-was-taken/stegasoo/actions/workflows/test.yml)
|
||||||

|
[](https://github.com/adlee-was-taken/stegasoo/actions/workflows/lint.yml)
|
||||||
|

|
||||||
|
[](LICENSE)
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🔐 **AES-256-GCM** authenticated encryption
|
- **AES-256-GCM** authenticated encryption
|
||||||
- 🧠 **Argon2id** memory-hard key derivation (256MB RAM requirement)
|
- **Argon2id** memory-hard key derivation (256MB RAM requirement)
|
||||||
- 🎲 **Pseudo-random pixel selection** defeats steganalysis
|
- **Pseudo-random pixel selection** defeats steganalysis
|
||||||
- 📅 **Daily key rotation** with BIP-39 passphrases
|
- **Multi-factor authentication**: Reference photo + passphrase + PIN/RSA key
|
||||||
- 🔑 **Multi-factor authentication**: PIN, RSA key, or both
|
- **Multiple interfaces**: CLI, Web UI, REST API
|
||||||
- 🖼️ **Reference photo** as "something you have"
|
- **File embedding**: Hide any file type (PDF, ZIP, documents)
|
||||||
- 🌐 **Multiple interfaces**: CLI, Web UI, REST API
|
- **DCT steganography**: JPEG-resilient embedding for social media
|
||||||
- 📁 **File embedding** - Hide any file type (PDF, ZIP, documents)
|
- **Channel keys**: Private group communication channels
|
||||||
- 📱 **QR code support** - Encode/decode RSA keys via QR codes
|
|
||||||
|
|
||||||
|
## Embedding Modes
|
||||||
|
|
||||||
## WebUI Preview
|
| Mode | Capacity (1080p) | JPEG Resilient | Best For |
|
||||||
|
|------|------------------|----------------|----------|
|
||||||
|
| **DCT** (default) | ~150 KB | Yes | Social media, messaging apps |
|
||||||
|
| **LSB** | ~750 KB | No | Email, direct file transfer |
|
||||||
|
|
||||||
Front Page | Encode | Decode | Generate |
|
## Web UI
|
||||||
:-------------------------:|:-------------------------:|:------------------------:|:--------:|
|
|
||||||
 |  |  | 
|
|
||||||
|
|
||||||
|
| Home | Encode | Decode | Generate |
|
||||||
|
|:----:|:------:|:------:|:--------:|
|
||||||
|
|  |  |  |  |
|
||||||
|
|
||||||
## Installation
|
## Quick Start
|
||||||
|
|
||||||
### From Source
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Install (Python 3.10-3.12)
|
||||||
git clone https://github.com/adlee-was-taken/stegasoo.git
|
|
||||||
cd stegasoo
|
|
||||||
|
|
||||||
# Install core library
|
|
||||||
pip install -e .
|
|
||||||
|
|
||||||
# Install with CLI
|
|
||||||
pip install -e ".[cli]"
|
|
||||||
|
|
||||||
# Install with Web UI
|
|
||||||
pip install -e ".[web]"
|
|
||||||
|
|
||||||
# Install with REST API
|
|
||||||
pip install -e ".[api]"
|
|
||||||
|
|
||||||
# Install everything
|
|
||||||
pip install -e ".[all]"
|
pip install -e ".[all]"
|
||||||
```
|
|
||||||
|
|
||||||
### CLI Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate credentials
|
# Generate credentials
|
||||||
stegasoo generate --pin --words 3
|
stegasoo generate --pin --words 4
|
||||||
|
|
||||||
# With RSA key
|
# Encode a message
|
||||||
stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "secretpassword"
|
|
||||||
|
|
||||||
# Encode
|
|
||||||
stegasoo encode \
|
stegasoo encode \
|
||||||
--ref photo.jpg \
|
--ref my_photo.jpg \
|
||||||
--carrier meme.png \
|
--carrier meme.jpg \
|
||||||
--phrase "apple forest thunder" \
|
--passphrase "apple forest thunder mountain" \
|
||||||
--pin 123456 \
|
--pin 123456 \
|
||||||
--message "Secret message"
|
--message "Secret message"
|
||||||
|
|
||||||
# Decode
|
# Decode
|
||||||
stegasoo decode \
|
stegasoo decode \
|
||||||
--ref photo.jpg \
|
--ref my_photo.jpg \
|
||||||
--stego stego.png \
|
--stego stego_image.png \
|
||||||
--phrase "apple forest thunder" \
|
--passphrase "apple forest thunder mountain" \
|
||||||
--pin 123456
|
--pin 123456
|
||||||
|
|
||||||
# Pipe-friendly
|
|
||||||
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 > stego.png
|
|
||||||
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Web UI
|
## Interfaces
|
||||||
|
|
||||||
```bash
|
| Interface | Start Command | Documentation |
|
||||||
# Development
|
|-----------|---------------|---------------|
|
||||||
cd frontends/web
|
| **CLI** | `stegasoo --help` | [CLI.md](CLI.md) |
|
||||||
python app.py
|
| **Web UI** | `cd frontends/web && python app.py` | [WEB_UI.md](WEB_UI.md) |
|
||||||
|
| **REST API** | `cd frontends/api && uvicorn main:app` | [API.md](API.md) |
|
||||||
# Production
|
|
||||||
gunicorn --bind 0.0.0.0:5000 app:app
|
|
||||||
```
|
|
||||||
|
|
||||||
Visit http://localhost:5000
|
|
||||||
|
|
||||||
### REST API
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
cd frontends/api
|
|
||||||
python main.py
|
|
||||||
|
|
||||||
# Production
|
|
||||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
API docs at http://localhost:8000/docs
|
|
||||||
|
|
||||||
#### Example API Calls
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate credentials
|
|
||||||
curl -X POST http://localhost:8000/generate \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"use_pin": true, "use_rsa": false}'
|
|
||||||
|
|
||||||
# Encode (multipart)
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
-F "message=Secret" \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "pin=123456" \
|
|
||||||
-F "reference_photo=@photo.jpg" \
|
|
||||||
-F "carrier=@meme.png" \
|
|
||||||
--output stego.png
|
|
||||||
|
|
||||||
# Decode (multipart)
|
|
||||||
curl -X POST http://localhost:8000/decode/multipart \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "pin=123456" \
|
|
||||||
-F "reference_photo=@photo.jpg" \
|
|
||||||
-F "stego_image=@stego.png"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Model
|
## Security Model
|
||||||
|
|
||||||
| Component | Entropy | Purpose |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| Reference Photo | ~80-256 bits | Something you have |
|
|
||||||
| Day Phrase (3-12 words) | ~33-100+ bits | Something you know (rotates daily) |
|
|
||||||
| PIN (6-9 digits) | ~20+ bits | Something you know (static) |
|
|
||||||
| RSA Key (2048-bit) | ~128 bits | Something you have |
|
|
||||||
| **Combined** | **~133-400+ bits** | **Beyond brute force** |
|
|
||||||
|
|
||||||
### Attack Resistance
|
|
||||||
|
|
||||||
| Attack | Protection |
|
|
||||||
|--------|------------|
|
|
||||||
| Brute force | 2^133+ combinations |
|
|
||||||
| Rainbow tables | Random salt per message |
|
|
||||||
| Steganalysis | Random pixel selection |
|
|
||||||
| GPU cracking | Argon2id requires 256MB RAM per attempt |
|
|
||||||
| Side-channel | Constant-time operations in crypto |
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
```
|
||||||
stegasoo/
|
Reference Photo ──┐
|
||||||
├── src/stegasoo/ # Core library
|
(~80-256 bits) │
|
||||||
│ ├── __init__.py # Public API
|
├──► Argon2id KDF ──► AES-256-GCM Key
|
||||||
│ ├── constants.py # Configuration
|
Passphrase ───────┤ (256MB RAM)
|
||||||
│ ├── crypto.py # Encryption/decryption
|
(~43-132 bits) │
|
||||||
│ ├── steganography.py # Image embedding
|
│
|
||||||
│ ├── keygen.py # Credential generation
|
PIN ──────────────┤
|
||||||
│ ├── validation.py # Input validation
|
(~20-30 bits) │
|
||||||
│ ├── models.py # Data classes
|
│
|
||||||
│ ├── exceptions.py # Custom exceptions
|
RSA Key ──────────┘
|
||||||
│ └── utils.py # Utilities
|
(optional)
|
||||||
│
|
|
||||||
├── frontends/
|
|
||||||
│ ├── web/ # Flask web UI
|
|
||||||
│ ├── cli/ # Command-line interface
|
|
||||||
│ └── api/ # FastAPI REST API
|
|
||||||
│
|
|
||||||
├── data/
|
|
||||||
│ └── bip39-words.txt # BIP-39 wordlist
|
|
||||||
│
|
|
||||||
├── pyproject.toml # Package configuration
|
|
||||||
├── Dockerfile # Multi-stage Docker build
|
|
||||||
└── docker-compose.yml # Container orchestration
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
| Configuration | Entropy | Use Case |
|
||||||
|
|--------------|---------|----------|
|
||||||
|
| 4-word passphrase + 6-digit PIN | ~153 bits | Standard security |
|
||||||
|
| 4-word passphrase + PIN + RSA | ~280+ bits | Maximum security |
|
||||||
|
|
||||||
### Environment Variables
|
## Requirements
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Requirement | Version |
|
||||||
|----------|---------|-------------|
|
|-------------|---------|
|
||||||
| `FLASK_ENV` | production | Flask environment |
|
| Python | 3.10-3.12 |
|
||||||
| `PYTHONPATH` | - | Include src/ for development |
|
| RAM | 512 MB+ |
|
||||||
|
|
||||||
### Limits
|
|
||||||
|
|
||||||
| Limit | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| Max image size | 4 megapixels |
|
|
||||||
| Max message size | 50 KB |
|
|
||||||
| Max file upload | 5 MB |
|
|
||||||
| PIN length | 6-9 digits |
|
|
||||||
| Phrase length | 3-12 words |
|
|
||||||
| RSA key sizes | 2048, 3072, 4096 bits |
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dev dependencies
|
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
# Run tests
|
|
||||||
pytest
|
pytest
|
||||||
|
black src/ tests/ frontends/
|
||||||
# Format code
|
ruff check src/ tests/ frontends/
|
||||||
black src/ frontends/
|
|
||||||
ruff check src/ frontends/
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
mypy src/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick start (HTTPS enabled by default)
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
|
||||||
|
# Access
|
||||||
|
# Web UI: https://localhost:5000 (self-signed cert)
|
||||||
|
# REST API: http://localhost:8000
|
||||||
|
|
||||||
|
# Disable HTTPS if needed:
|
||||||
|
STEGASOO_HTTPS_ENABLED=false docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
See [DOCKER.md](DOCKER.md) and [docs/DOCKER_QUICKSTART.md](docs/DOCKER_QUICKSTART.md) for full documentation.
|
||||||
|
|
||||||
|
## Raspberry Pi
|
||||||
|
|
||||||
|
Pre-built SD card images available for Pi 4/5:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Flash image (download from GitHub Releases)
|
||||||
|
zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress
|
||||||
|
|
||||||
|
# First boot runs interactive setup wizard:
|
||||||
|
# - WiFi configuration
|
||||||
|
# - HTTPS with port 443
|
||||||
|
# - Channel key generation
|
||||||
|
# - Optional overclocking
|
||||||
|
```
|
||||||
|
|
||||||
|
See [rpi/README.md](rpi/README.md) for manual installation.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [INSTALL.md](INSTALL.md) - Installation guide
|
||||||
|
- [DOCKER.md](DOCKER.md) - Docker deployment
|
||||||
|
- [CLI.md](CLI.md) - Command-line reference
|
||||||
|
- [API.md](API.md) - REST API documentation
|
||||||
|
- [WEB_UI.md](WEB_UI.md) - Web interface guide
|
||||||
|
- [SECURITY.md](SECURITY.md) - Security model details
|
||||||
|
- [UNDER_THE_HOOD.md](UNDER_THE_HOOD.md) - Technical deep-dive
|
||||||
|
- [CHANGELOG.md](CHANGELOG.md) - Version history
|
||||||
|
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contributor guide
|
||||||
|
- `man stegasoo` - Man page (install: `sudo cp docs/stegasoo.1 /usr/local/share/man/man1/ && sudo mandb`)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - Use responsibly.
|
MIT License - see [LICENSE](LICENSE). Use responsibly.
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
---
|
||||||
|
|
||||||
This tool is for educational and legitimate privacy purposes only. Users are responsible for complying with applicable laws in their jurisdiction.
|
|
||||||
|
|
||||||
|
*This tool is for educational and legitimate privacy purposes. Users are responsible for complying with applicable laws.*
|
||||||
|
|||||||
44
RELEASE_CHECKLIST.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Stegasoo Release Checklist
|
||||||
|
|
||||||
|
Pre-release validation checklist. Complete all items before tagging a release.
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
- [ ] All tests pass: `./venv/bin/pytest tests/ -v`
|
||||||
|
- [ ] No lint errors: `./venv/bin/ruff check src/`
|
||||||
|
- [ ] Version bumped in `pyproject.toml`
|
||||||
|
- [ ] CHANGELOG.md updated
|
||||||
|
|
||||||
|
## Pi Image Validation
|
||||||
|
|
||||||
|
- [ ] Fresh Pi OS install with setup.sh works
|
||||||
|
- [ ] First-boot wizard completes successfully
|
||||||
|
- [ ] MOTD shows correct URL on SSH login
|
||||||
|
- [ ] Smoke test passes: `./rpi/smoke-test.sh --443 <PI_IP>`
|
||||||
|
- [ ] Encode/decode works on large image (10MB+)
|
||||||
|
- [ ] Sanitize script runs cleanly
|
||||||
|
- [ ] Image created and compressed
|
||||||
|
|
||||||
|
## Docker Validation
|
||||||
|
|
||||||
|
- [ ] Base image builds: `docker build -f docker/Dockerfile.base -t stegasoo-base:latest .`
|
||||||
|
- [ ] Web image builds: `docker-compose -f docker/docker-compose.yml build web`
|
||||||
|
- [ ] Container starts: `docker-compose -f docker/docker-compose.yml up -d web`
|
||||||
|
- [ ] Web UI accessible at http://localhost:5000
|
||||||
|
- [ ] Encode/decode works in container
|
||||||
|
- [ ] Container stops cleanly: `docker-compose -f docker/docker-compose.yml down`
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
- [ ] Merge feature branch to main
|
||||||
|
- [ ] Create annotated tag: `git tag -a vX.Y.Z -m "message"`
|
||||||
|
- [ ] Push tag: `git push origin vX.Y.Z`
|
||||||
|
- [ ] Create GitHub Release with release notes
|
||||||
|
- [ ] Upload Pi image (.img.zst.zip)
|
||||||
|
- [ ] Verify download links work
|
||||||
|
|
||||||
|
## Post-Release
|
||||||
|
|
||||||
|
- [ ] Delete old/obsolete releases if needed
|
||||||
|
- [ ] Update any external documentation
|
||||||
|
- [ ] Announce release (if applicable)
|
||||||
131
RELEASE_NOTES.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
## Stegasoo v4.2.1
|
||||||
|
|
||||||
|
### API Security
|
||||||
|
|
||||||
|
**API Key Authentication**
|
||||||
|
- All protected endpoints require `X-API-Key` header
|
||||||
|
- Keys stored hashed (SHA-256) in `~/.stegasoo/api_keys.json`
|
||||||
|
- Auth disabled when no keys configured (easy onboarding)
|
||||||
|
|
||||||
|
**TLS Support**
|
||||||
|
- Self-signed certificates auto-generated on first run
|
||||||
|
- Certs valid for localhost, all local IPs, hostname.local
|
||||||
|
- CLI: `stegasoo api tls generate` to pre-generate
|
||||||
|
|
||||||
|
### CLI Improvements
|
||||||
|
|
||||||
|
**New API Management Commands**
|
||||||
|
```bash
|
||||||
|
stegasoo api keys create NAME # Create new key
|
||||||
|
stegasoo api keys list # List API keys
|
||||||
|
stegasoo api tls generate # Generate TLS cert
|
||||||
|
stegasoo api serve # Start server with TLS
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Image Tools**
|
||||||
|
```bash
|
||||||
|
stegasoo tools compress IMG -q 75 # JPEG compression
|
||||||
|
stegasoo tools rotate IMG -r 90 # Lossless rotation
|
||||||
|
stegasoo tools convert IMG -f png # Format conversion
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **DCT rotation**: Portrait photos no longer export rotated 90°
|
||||||
|
- **jpegtran**: Removed `-trim` flag that destroyed DCT stego data
|
||||||
|
- **CLI encode**: Now outputs JPEG when carrier is JPEG (was always PNG)
|
||||||
|
- **Import paths**: Fixed for installed packages (AUR/pip)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
**AUR (Arch Linux)**
|
||||||
|
```bash
|
||||||
|
yay -S stegasoo-git # Full (Web + API + CLI)
|
||||||
|
yay -S stegasoo-cli-git # CLI only
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Raspberry Pi**
|
||||||
|
Flash `stegasoo-rpi-4.2.1.img.zst.zip` to SD card.
|
||||||
|
Default login: `admin` / `stegasoo`
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.11 - 3.14 (dropped 3.10 support)
|
||||||
|
|
||||||
|
### Release Assets
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `stegasoo-rpi-4.2.1.img.zst.zip` | Raspberry Pi SD card image |
|
||||||
|
| `stegasoo-docker-base-4.2.1.tar.zst` | Docker base image |
|
||||||
|
| Source code (zip/tar.gz) | Auto-generated |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stegasoo v4.2.0
|
||||||
|
|
||||||
|
### Performance Optimizations
|
||||||
|
|
||||||
|
Major performance improvements for Raspberry Pi and resource-constrained deployments.
|
||||||
|
|
||||||
|
#### DCT Vectorization (~14x faster)
|
||||||
|
- Batch DCT processing using `scipy.fft.dctn` with `axes=(1,2)`
|
||||||
|
- Processes 500 blocks at once instead of one-by-one
|
||||||
|
- Decode time reduced from ~2.6s to ~0.8s on 1MB images
|
||||||
|
|
||||||
|
#### Memory Optimization (50% reduction)
|
||||||
|
- Switched from `float64` to `float32` for all DCT operations
|
||||||
|
- Peak RAM: 211 MB → 107 MB for encode, 104 MB → 52 MB for decode
|
||||||
|
- Critical for Pi 3/4 avoiding swap thrashing
|
||||||
|
|
||||||
|
#### Progress Callbacks for Decode
|
||||||
|
- `progress_file` parameter added to `decode()` and extraction functions
|
||||||
|
- UI can now show decode progress (phases: loading, extracting, decoding, complete)
|
||||||
|
- JSON format: `{"current": 80, "total": 100, "percent": 80.0, "phase": "decoding"}`
|
||||||
|
|
||||||
|
#### Async API Endpoints
|
||||||
|
- Encode/decode operations now run in thread pool via `asyncio.to_thread()`
|
||||||
|
- API server can handle concurrent requests without blocking
|
||||||
|
- Essential for multi-user Pi deployments
|
||||||
|
|
||||||
|
### Compression
|
||||||
|
|
||||||
|
#### Zstd Default Compression
|
||||||
|
- `zstandard` is now a core dependency (always installed)
|
||||||
|
- Better compression ratio than zlib for QR code RSA keys
|
||||||
|
- New `STEGASOO-ZS:` prefix for zstd, backward compatible with `STEGASOO-Z:` (zlib)
|
||||||
|
|
||||||
|
### QR Code Generation
|
||||||
|
|
||||||
|
#### CLI Support
|
||||||
|
- `stegasoo generate --rsa --qr key.png` - save RSA key as QR image (PNG/JPG)
|
||||||
|
- `stegasoo generate --rsa --qr-ascii` - print ASCII QR to terminal
|
||||||
|
|
||||||
|
#### API Support
|
||||||
|
- `POST /generate-key-qr` - generate QR from RSA key
|
||||||
|
- Supports `png`, `jpg`, and `ascii` output formats
|
||||||
|
- Uses zstd compression by default
|
||||||
|
|
||||||
|
### Other Changes
|
||||||
|
|
||||||
|
- RSA key size capped at 3072 bits (4096 too large for QR codes)
|
||||||
|
- File auto-expire increased to 10 minutes
|
||||||
|
- Progress bar "candy cane" animation during Argon2 key derivation
|
||||||
|
- Optional API service in Pi setup (with security warning)
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Metric | v4.1.7 | v4.2.0 | Improvement |
|
||||||
|
|--------|--------|--------|-------------|
|
||||||
|
| Decode (1MB) | ~2.6s | ~0.8s | **70% faster** |
|
||||||
|
| Peak RAM | 211 MB | 107 MB | **50% less** |
|
||||||
|
| Concurrent API | No | Yes | check |
|
||||||
|
| QR Compression | zlib | zstd | **~15% smaller** |
|
||||||
|
|
||||||
|
### Full Changelog
|
||||||
|
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||||
125
SECURITY.md
@@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported | Notes |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ | ----- |
|
||||||
| 2.x.x | :white_check_mark: |
|
| 4.1.x | Current Version | What you SHOULD be using. |
|
||||||
| 1.x.x | :x: |
|
| 4.x.x | ⚠️ Security fixes only | Upgrade (EOL soon) |
|
||||||
|
| <= 3.x.x | ❌ End of life | |
|
||||||
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||||
|
|
||||||
Instead, please email: **security@example.com** (replace with your email)
|
Instead, please email: **adlee-was-taken@proton.me**
|
||||||
|
|
||||||
Include:
|
Include:
|
||||||
- Description of the vulnerability
|
- Description of the vulnerability
|
||||||
@@ -34,7 +36,7 @@ Stegasoo is designed to hide the **existence** of a secret message within an ord
|
|||||||
| Goal | How It's Achieved |
|
| Goal | How It's Achieved |
|
||||||
|------|-------------------|
|
|------|-------------------|
|
||||||
| **Confidentiality** | AES-256-GCM encryption with Argon2id key derivation |
|
| **Confidentiality** | AES-256-GCM encryption with Argon2id key derivation |
|
||||||
| **Steganography** | LSB embedding with pseudo-random pixel selection |
|
| **Steganography** | LSB/DCT embedding with pseudo-random pixel/coefficient selection |
|
||||||
| **Authentication** | Multi-factor: reference photo + passphrase + PIN (or RSA key) |
|
| **Authentication** | Multi-factor: reference photo + passphrase + PIN (or RSA key) |
|
||||||
| **Integrity** | GCM authentication tag detects tampering |
|
| **Integrity** | GCM authentication tag detects tampering |
|
||||||
|
|
||||||
@@ -43,20 +45,43 @@ Stegasoo is designed to hide the **existence** of a secret message within an ord
|
|||||||
Stegasoo combines multiple authentication factors:
|
Stegasoo combines multiple authentication factors:
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
│ Key Derivation │
|
│ Key Derivation │
|
||||||
│ │
|
│ │
|
||||||
│ Reference Photo ─────┐ │
|
│ Reference Photo ───────┐ │
|
||||||
│ (something you have) │ │
|
│ (something you have) │ │
|
||||||
│ ├──► Argon2id ──► AES-256 Key │
|
│ ├──► Argon2id ──► AES-256 Key │
|
||||||
│ Day Passphrase ──────┤ (256MB RAM) │
|
│ Passphrase ────────────┤ (256MB RAM) │
|
||||||
│ (something you know) │ │
|
│ (something you know) │ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ PIN or RSA Key ──────┘ │
|
│ PIN or RSA Key ────────┘ │
|
||||||
│ (second factor) │
|
│ (second factor) │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Changes in v4.0
|
||||||
|
|
||||||
|
### Removed: Date-Based Key Rotation
|
||||||
|
|
||||||
|
**Previous versions (v3.x and earlier):**
|
||||||
|
- Required a date parameter for encode/decode
|
||||||
|
- Keys rotated daily based on "day phrase"
|
||||||
|
- Users had to remember which date they used
|
||||||
|
|
||||||
|
**Version 4.0:**
|
||||||
|
- No date dependency
|
||||||
|
- Single passphrase (no rotation)
|
||||||
|
- Simpler but slightly reduced entropy per-message
|
||||||
|
|
||||||
|
**Security Impact:**
|
||||||
|
- Minimal - the date only added ~10 bits of entropy
|
||||||
|
- Passphrase default increased from 3 to 4 words to compensate (+11 bits)
|
||||||
|
- Overall entropy remains similar or higher with 4-word default
|
||||||
|
|
||||||
|
### Renamed: day_phrase → passphrase
|
||||||
|
|
||||||
|
Terminology change only. No security impact.
|
||||||
|
|
||||||
## What Stegasoo Does NOT Protect Against
|
## What Stegasoo Does NOT Protect Against
|
||||||
|
|
||||||
### 1. Statistical Steganalysis
|
### 1. Statistical Steganalysis
|
||||||
@@ -68,7 +93,9 @@ Stegasoo combines multiple authentication factors:
|
|||||||
- RS analysis
|
- RS analysis
|
||||||
- Machine learning classifiers
|
- Machine learning classifiers
|
||||||
|
|
||||||
**Mitigation:** Stegasoo uses pseudo-random pixel selection (not sequential), which helps but doesn't eliminate detectability.
|
**DCT mode is more resilient** but not undetectable.
|
||||||
|
|
||||||
|
**Mitigation:** Stegasoo uses pseudo-random pixel/coefficient selection, which helps but doesn't eliminate detectability.
|
||||||
|
|
||||||
**Recommendation:** Don't rely on Stegasoo if your adversary has:
|
**Recommendation:** Don't rely on Stegasoo if your adversary has:
|
||||||
- Access to the original carrier image
|
- Access to the original carrier image
|
||||||
@@ -113,24 +140,28 @@ Stegasoo combines multiple authentication factors:
|
|||||||
|
|
||||||
**Recommendation:**
|
**Recommendation:**
|
||||||
- Use 8+ digit PINs
|
- Use 8+ digit PINs
|
||||||
- Use 4+ word passphrases
|
- Use 4+ word passphrases (v4.0 default)
|
||||||
- Consider RSA keys for high-security use cases
|
- Consider RSA keys for high-security use cases
|
||||||
|
|
||||||
### 5. Image Modification
|
### 5. Image Modification
|
||||||
|
|
||||||
**Risk:** Lossy compression destroys hidden data.
|
**Risk:** Lossy compression destroys hidden data.
|
||||||
|
|
||||||
**Data is destroyed by:**
|
**LSB mode - data is destroyed by:**
|
||||||
- JPEG compression
|
- JPEG compression
|
||||||
- Resizing
|
- Resizing
|
||||||
- Filters/effects
|
- Filters/effects
|
||||||
- Screenshots
|
- Screenshots
|
||||||
- Social media upload (Instagram, Twitter, etc.)
|
- Social media upload
|
||||||
|
|
||||||
|
**DCT mode - more resilient but not immune:**
|
||||||
|
- Survives moderate JPEG recompression
|
||||||
|
- May fail with aggressive compression (quality < 70)
|
||||||
|
- Still destroyed by resizing, filters, screenshots
|
||||||
|
|
||||||
**Recommendation:**
|
**Recommendation:**
|
||||||
- Always use lossless formats (PNG, BMP)
|
- LSB: Always use lossless formats (PNG, BMP), direct transfer
|
||||||
- Transfer files directly (email, Signal, USB)
|
- DCT: Use for social media, but test with your specific platform
|
||||||
- Never upload stego images to social media
|
|
||||||
|
|
||||||
### 6. Metadata Leakage
|
### 6. Metadata Leakage
|
||||||
|
|
||||||
@@ -165,49 +196,52 @@ Stegasoo combines multiple authentication factors:
|
|||||||
| Encryption | AES-256-GCM | 12-byte IV, 16-byte tag |
|
| Encryption | AES-256-GCM | 12-byte IV, 16-byte tag |
|
||||||
| Photo Hash | SHA-256 | Full image bytes |
|
| Photo Hash | SHA-256 | Full image bytes |
|
||||||
|
|
||||||
### Pixel Selection
|
### Pixel/Coefficient Selection
|
||||||
|
|
||||||
Pixels are selected pseudo-randomly using a key derived from:
|
Selection key is derived from:
|
||||||
```
|
```
|
||||||
pixel_key = SHA256(photo_hash || passphrase || date || pin/rsa_signature)
|
selection_key = SHA256(photo_hash || passphrase || pin/rsa_signature)
|
||||||
```
|
```
|
||||||
|
|
||||||
This prevents:
|
This prevents:
|
||||||
- Sequential embedding patterns
|
- Sequential embedding patterns
|
||||||
- Statistical detection of modified regions
|
- Statistical detection of modified regions
|
||||||
|
|
||||||
### Format
|
### Message Format (v4.0)
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
│ Magic (4B) │ Version (1B) │ Date (10B) │ Salt (32B) │ IV (12B) │
|
│ Magic (4B) │ Version (1B) │ Salt (32B) │ IV (12B) │
|
||||||
├──────────────────────────────────────────────────────────────┤
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
│ Encrypted Payload (AES-256-GCM) │
|
│ Encrypted Payload (AES-256-GCM) │
|
||||||
│ ├── Type (1B): 0x01=text, 0x02=file │
|
│ ├── Type (1B): 0x01=text, 0x02=file │
|
||||||
│ ├── Length (4B) │
|
│ ├── Length (4B) │
|
||||||
│ ├── Data (variable) │
|
│ ├── Data (variable) │
|
||||||
│ └── [Filename if file] (variable) │
|
│ └── [Filename if file] (variable) │
|
||||||
├──────────────────────────────────────────────────────────────┤
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
│ GCM Auth Tag (16B) │
|
│ GCM Auth Tag (16B) │
|
||||||
└──────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** v4.0 removed the date field from the header, reducing overhead by 10 bytes.
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
### For Maximum Security
|
### For Maximum Security
|
||||||
|
|
||||||
1. **Use RSA keys** instead of PINs for authentication
|
1. **Use RSA keys** instead of PINs for authentication
|
||||||
2. **Use unique reference photos** not available online
|
2. **Use unique reference photos** not available online
|
||||||
3. **Use long passphrases** (4+ random words)
|
3. **Use long passphrases** (4+ random words, recommend 6+)
|
||||||
4. **Transfer via secure channels** (Signal, encrypted email)
|
4. **Transfer via secure channels** (Signal, encrypted email)
|
||||||
5. **Delete stego images** after message is read
|
5. **Delete stego images** after message is read
|
||||||
6. **Keep software updated** for security fixes
|
6. **Keep software updated** for security fixes
|
||||||
|
7. **Use DCT mode** for social media sharing
|
||||||
|
|
||||||
### For Casual Privacy
|
### For Casual Privacy
|
||||||
|
|
||||||
1. **6-digit PIN** is sufficient for non-adversarial use
|
1. **6-digit PIN** is sufficient for non-adversarial use
|
||||||
2. **3-word passphrase** provides reasonable security
|
2. **4-word passphrase** provides reasonable security (v4.0 default)
|
||||||
3. **PNG format** always for output
|
3. **PNG format** for LSB mode output
|
||||||
4. **Direct file transfer** (email attachment, AirDrop)
|
4. **Direct file transfer** (email attachment, AirDrop)
|
||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
@@ -216,8 +250,8 @@ This prevents:
|
|||||||
|------------|--------|--------|
|
|------------|--------|--------|
|
||||||
| LSB is detectable | Statistical analysis can detect hidden data | By design (tradeoff for capacity) |
|
| LSB is detectable | Statistical analysis can detect hidden data | By design (tradeoff for capacity) |
|
||||||
| No forward secrecy | Compromised key decrypts all messages | Use different keys per message for high security |
|
| No forward secrecy | Compromised key decrypts all messages | Use different keys per message for high security |
|
||||||
| Date in header | Reveals when message was encoded | By design (enables day-specific passphrases) |
|
|
||||||
| No deniability | Single password = single message | Future: plausible deniability layers |
|
| No deniability | Single password = single message | Future: plausible deniability layers |
|
||||||
|
| Python 3.13 incompatible | jpegio C extension crashes | Use Python 3.12 or earlier |
|
||||||
|
|
||||||
## Security Audit Status
|
## Security Audit Status
|
||||||
|
|
||||||
@@ -231,6 +265,9 @@ If you're a security researcher interested in auditing Stegasoo, please reach ou
|
|||||||
|
|
||||||
| Version | Security Changes |
|
| Version | Security Changes |
|
||||||
|---------|------------------|
|
|---------|------------------|
|
||||||
|
| 4.0.0 | Removed date dependency, increased default passphrase to 4 words, added JPEG normalization |
|
||||||
|
| 3.2.0 | DCT color mode added |
|
||||||
|
| 3.0.0 | Added DCT steganography mode |
|
||||||
| 2.2.0 | Added compression (no security impact) |
|
| 2.2.0 | Added compression (no security impact) |
|
||||||
| 2.1.0 | Upgraded to Argon2id, increased iterations |
|
| 2.1.0 | Upgraded to Argon2id, increased iterations |
|
||||||
| 2.0.0 | Added RSA key support |
|
| 2.0.0 | Added RSA key support |
|
||||||
|
|||||||
284
SECURITY_AUDIT_PLAN.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# Stegasoo Security Audit Plan
|
||||||
|
|
||||||
|
> **Target Audience**: Developers, security reviewers, and deployment administrators
|
||||||
|
> **Scope**: Web UI, REST API, CLI, and cryptographic core
|
||||||
|
> **Deployment Model**: Air-gapped / private LAN (primary), Internet-facing (secondary)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Stegasoo is a steganography tool designed for **air-gapped deployments** on private networks. While the primary threat model assumes a trusted local network, this audit plan covers security best practices for both isolated and potentially exposed deployments.
|
||||||
|
|
||||||
|
### Known Limitations (By Design)
|
||||||
|
|
||||||
|
- **Self-signed certificates**: HTTPS uses self-signed certs; users must add exceptions or deploy their own CA
|
||||||
|
- **No rate limiting**: Assumes trusted users on private network
|
||||||
|
- **Single-node**: No distributed session store; sessions are per-instance
|
||||||
|
- **Air-gap focus**: External security (firewalls, network isolation) is user's responsibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Authentication & Authorization
|
||||||
|
|
||||||
|
### 1.1 Password Security
|
||||||
|
- [ ] Passwords hashed with Argon2id (preferred) or PBKDF2 fallback
|
||||||
|
- [ ] Minimum password length enforced (8+ characters)
|
||||||
|
- [ ] Password not logged or exposed in error messages
|
||||||
|
- [ ] Password change requires current password verification
|
||||||
|
- [ ] Admin re-authentication required for sensitive operations (channel key export)
|
||||||
|
|
||||||
|
### 1.2 Session Management
|
||||||
|
- [ ] Session tokens are cryptographically random
|
||||||
|
- [ ] Session cookies have `HttpOnly` flag
|
||||||
|
- [ ] Session cookies have `Secure` flag (when HTTPS enabled)
|
||||||
|
- [ ] Session cookies have `SameSite` attribute
|
||||||
|
- [ ] Sessions invalidated on logout
|
||||||
|
- [ ] Sessions invalidated on password change
|
||||||
|
- [ ] Session timeout configured appropriately
|
||||||
|
|
||||||
|
### 1.3 Authorization
|
||||||
|
- [ ] Admin-only routes protected by `@admin_required` decorator
|
||||||
|
- [ ] User-only routes protected by `@login_required` decorator
|
||||||
|
- [ ] Users cannot access other users' saved channel keys
|
||||||
|
- [ ] Users cannot modify other users' accounts
|
||||||
|
- [ ] Role escalation not possible through API manipulation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Cryptographic Implementation
|
||||||
|
|
||||||
|
### 2.1 Key Derivation
|
||||||
|
- [ ] KDF uses Argon2id with appropriate parameters (memory, iterations, parallelism)
|
||||||
|
- [ ] PBKDF2 fallback uses sufficient iterations (600,000+)
|
||||||
|
- [ ] Salt is cryptographically random and unique per operation
|
||||||
|
- [ ] PIN/passphrase combined securely before KDF
|
||||||
|
|
||||||
|
### 2.2 Encryption
|
||||||
|
- [ ] AES-256-GCM used for payload encryption
|
||||||
|
- [ ] Nonce/IV is unique per encryption operation
|
||||||
|
- [ ] Authentication tag verified before decryption
|
||||||
|
- [ ] No padding oracle vulnerabilities
|
||||||
|
|
||||||
|
### 2.3 Channel Keys
|
||||||
|
- [ ] Channel keys are 128-bit (32 hex chars)
|
||||||
|
- [ ] Channel key derivation uses HKDF or similar
|
||||||
|
- [ ] Channel isolation prevents cross-channel decryption
|
||||||
|
- [ ] Fingerprint reveals no information about full key
|
||||||
|
|
||||||
|
### 2.4 Random Number Generation
|
||||||
|
- [ ] All random values use `secrets` module or OS CSPRNG
|
||||||
|
- [ ] No use of `random` module for security-sensitive operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Input Validation & Injection Prevention
|
||||||
|
|
||||||
|
### 3.1 Web UI
|
||||||
|
- [ ] All user input sanitized before rendering (XSS prevention)
|
||||||
|
- [ ] Jinja2 auto-escaping enabled
|
||||||
|
- [ ] No `| safe` filter on user-controlled content
|
||||||
|
- [ ] Content-Security-Policy header configured
|
||||||
|
- [ ] X-Content-Type-Options: nosniff
|
||||||
|
|
||||||
|
### 3.2 File Uploads
|
||||||
|
- [ ] File size limits enforced server-side
|
||||||
|
- [ ] File type validation (magic bytes, not just extension)
|
||||||
|
- [ ] Uploaded files not executed
|
||||||
|
- [ ] Filenames sanitized (path traversal prevention)
|
||||||
|
- [ ] Temporary files cleaned up after processing
|
||||||
|
|
||||||
|
### 3.3 API Inputs
|
||||||
|
- [ ] JSON schema validation on API endpoints
|
||||||
|
- [ ] Integer overflow checks on size parameters
|
||||||
|
- [ ] No SQL injection (parameterized queries only)
|
||||||
|
- [ ] No command injection (no shell=True with user input)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Steganography-Specific Security
|
||||||
|
|
||||||
|
### 4.1 Carrier Image Handling
|
||||||
|
- [ ] Malformed images don't crash the server (PIL/jpegio hardening)
|
||||||
|
- [ ] DCT mode subprocess isolation for crash protection
|
||||||
|
- [ ] Memory limits on image processing
|
||||||
|
- [ ] No arbitrary code execution from image metadata
|
||||||
|
|
||||||
|
### 4.2 Payload Security
|
||||||
|
- [ ] Payload size limits enforced
|
||||||
|
- [ ] Encrypted payload indistinguishable from random noise
|
||||||
|
- [ ] No metadata leakage in output images
|
||||||
|
- [ ] Reference photo required (prevents dictionary attacks)
|
||||||
|
|
||||||
|
### 4.3 Capacity Reporting
|
||||||
|
- [ ] Capacity calculation doesn't leak information about encoding method
|
||||||
|
- [ ] Failed decodes don't reveal why (wrong key vs no data vs corrupted)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Network & Transport Security
|
||||||
|
|
||||||
|
### 5.1 HTTPS Configuration
|
||||||
|
- [ ] TLS 1.2+ only (no SSLv3, TLS 1.0/1.1)
|
||||||
|
- [ ] Strong cipher suites configured
|
||||||
|
- [ ] Certificate generation uses 2048+ bit RSA or P-256 EC
|
||||||
|
- [ ] Private key file permissions restricted (600)
|
||||||
|
|
||||||
|
### 5.2 Headers
|
||||||
|
- [ ] X-Frame-Options: DENY (clickjacking prevention)
|
||||||
|
- [ ] X-Content-Type-Options: nosniff
|
||||||
|
- [ ] Referrer-Policy: same-origin
|
||||||
|
- [ ] Permissions-Policy configured
|
||||||
|
|
||||||
|
### 5.3 CORS (if applicable)
|
||||||
|
- [ ] CORS not enabled (or restricted to specific origins)
|
||||||
|
- [ ] Credentials not allowed cross-origin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Error Handling & Logging
|
||||||
|
|
||||||
|
### 6.1 Error Messages
|
||||||
|
- [ ] Stack traces not exposed to users in production
|
||||||
|
- [ ] Error messages don't reveal sensitive paths or config
|
||||||
|
- [ ] Failed login doesn't reveal if username exists
|
||||||
|
|
||||||
|
### 6.2 Logging
|
||||||
|
- [ ] Passwords never logged
|
||||||
|
- [ ] Channel keys never logged
|
||||||
|
- [ ] Passphrases never logged
|
||||||
|
- [ ] Log files have appropriate permissions
|
||||||
|
- [ ] Sensitive operations logged for audit trail (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Dependency Security
|
||||||
|
|
||||||
|
### 7.1 Python Dependencies
|
||||||
|
- [ ] All dependencies pinned to specific versions
|
||||||
|
- [ ] No known vulnerabilities in dependencies (run `pip-audit` or `safety`)
|
||||||
|
- [ ] Dependencies from trusted sources only (PyPI)
|
||||||
|
|
||||||
|
### 7.2 Frontend Dependencies
|
||||||
|
- [ ] All JS/CSS served locally (air-gap ready)
|
||||||
|
- [ ] No CDN dependencies
|
||||||
|
- [ ] Bootstrap and libraries are official releases
|
||||||
|
- [ ] Subresource integrity considered for any external loads
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Deployment Security
|
||||||
|
|
||||||
|
### 8.1 File Permissions
|
||||||
|
- [ ] Database file not world-readable (600 or 640)
|
||||||
|
- [ ] SSL certificates/keys not world-readable
|
||||||
|
- [ ] Config files with secrets protected
|
||||||
|
- [ ] Instance directory not in web root
|
||||||
|
|
||||||
|
### 8.2 Docker Deployment
|
||||||
|
- [ ] Container runs as non-root user
|
||||||
|
- [ ] No unnecessary capabilities
|
||||||
|
- [ ] Resource limits configured
|
||||||
|
- [ ] Health checks don't expose sensitive info
|
||||||
|
|
||||||
|
### 8.3 Raspberry Pi Deployment
|
||||||
|
- [ ] Default passwords changed
|
||||||
|
- [ ] SSH key-only authentication (recommended)
|
||||||
|
- [ ] Unnecessary services disabled
|
||||||
|
- [ ] Firewall configured (UFW/iptables)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Air-Gap Specific Considerations
|
||||||
|
|
||||||
|
### 9.1 Network Isolation
|
||||||
|
- [ ] Document expected network topology
|
||||||
|
- [ ] No phone-home or telemetry
|
||||||
|
- [ ] No external API calls
|
||||||
|
- [ ] Works fully offline after deployment
|
||||||
|
|
||||||
|
### 9.2 Key Distribution
|
||||||
|
- [ ] QR code export for channel keys (offline transfer)
|
||||||
|
- [ ] Print sheet for physical key backup
|
||||||
|
- [ ] No cloud sync or external key servers
|
||||||
|
|
||||||
|
### 9.3 Updates
|
||||||
|
- [ ] Document offline update procedure
|
||||||
|
- [ ] Signed releases (future consideration)
|
||||||
|
- [ ] Checksum verification for downloads
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Penetration Testing Checklist
|
||||||
|
|
||||||
|
### 10.1 Authentication Attacks
|
||||||
|
- [ ] Brute force login (note: no rate limiting by design)
|
||||||
|
- [ ] Session fixation
|
||||||
|
- [ ] Session hijacking
|
||||||
|
- [ ] Password reset flow abuse
|
||||||
|
|
||||||
|
### 10.2 Injection Attacks
|
||||||
|
- [ ] SQL injection on all inputs
|
||||||
|
- [ ] XSS (stored, reflected, DOM-based)
|
||||||
|
- [ ] Command injection
|
||||||
|
- [ ] Path traversal
|
||||||
|
- [ ] SSTI (Server-Side Template Injection)
|
||||||
|
|
||||||
|
### 10.3 Business Logic
|
||||||
|
- [ ] Access control bypass
|
||||||
|
- [ ] IDOR (Insecure Direct Object Reference)
|
||||||
|
- [ ] Race conditions
|
||||||
|
- [ ] Integer overflow in capacity calculations
|
||||||
|
|
||||||
|
### 10.4 Cryptographic Attacks
|
||||||
|
- [ ] Known-plaintext attacks on stego output
|
||||||
|
- [ ] Timing attacks on password verification
|
||||||
|
- [ ] Padding oracle attacks
|
||||||
|
- [ ] Key reuse vulnerabilities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools for Automated Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dependency vulnerability scan
|
||||||
|
pip-audit
|
||||||
|
safety check
|
||||||
|
|
||||||
|
# Static analysis
|
||||||
|
bandit -r stegasoo/ frontends/
|
||||||
|
|
||||||
|
# Web security scan (if exposed)
|
||||||
|
nikto -h https://localhost:5000
|
||||||
|
OWASP ZAP (manual)
|
||||||
|
|
||||||
|
# SSL/TLS configuration
|
||||||
|
testssl.sh https://localhost:5000
|
||||||
|
|
||||||
|
# Python code quality
|
||||||
|
ruff check .
|
||||||
|
mypy stegasoo/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Schedule
|
||||||
|
|
||||||
|
| Phase | Focus Area | Priority |
|
||||||
|
|-------|-----------|----------|
|
||||||
|
| Pre-release | Crypto implementation, auth flow | Critical |
|
||||||
|
| Post-release | Dependency scan, static analysis | High |
|
||||||
|
| Quarterly | Full penetration test | Medium |
|
||||||
|
| Ongoing | CVE monitoring for dependencies | High |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This plan assumes **trusted users on a private network** as the primary deployment model
|
||||||
|
- Internet-facing deployments should add rate limiting, fail2ban, and reverse proxy hardening
|
||||||
|
- For high-security deployments, consider external security audit by professionals
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 2026-01-07*
|
||||||
54
TODO-4.2.1.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Stegasoo 4.2.1 Plan
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
- [x] Fix EXIF viewer panel not loading metadata in Web UI
|
||||||
|
- Redesigned with card-based grid layout and categories
|
||||||
|
- Compact styling for better space usage
|
||||||
|
- [x] DCT mode: portrait photos export rotated 90° (EXIF orientation not handled)
|
||||||
|
- Added `_apply_exif_orientation()` to apply EXIF rotation before embedding
|
||||||
|
- [x] DCT mode: add rotation fallback (try as-is, rotate 90°, retry on failure)
|
||||||
|
- Added rotation fallback in `extract_from_dct()` with quick header validation
|
||||||
|
- [x] Rotate tool: use jpegtran for lossless JPEG rotation (preserves DCT stego!)
|
||||||
|
- Web UI rotate tool now uses jpegtran for JPEGs
|
||||||
|
- DCT decode rotation fallback now uses jpegtran for JPEGs
|
||||||
|
- Dynamic UI shows "DCT Safe" for JPEGs, warning for other formats
|
||||||
|
|
||||||
|
## Tools Audit
|
||||||
|
- [x] Web UI tools - full shakedown and fixes
|
||||||
|
- Compress, Rotate, Strip, EXIF viewer all working
|
||||||
|
- Rotate uses jpegtran for lossless JPEG rotation
|
||||||
|
- Compact UI styling
|
||||||
|
- [x] CLI tools - full shakedown and fixes
|
||||||
|
- Fixed encode to output JPEG when carrier is JPEG (was always PNG)
|
||||||
|
- Fixed jpegtran -trim flag destroying DCT stego data
|
||||||
|
- Added compress, rotate, convert tools (matching Web UI)
|
||||||
|
- Rotate uses jpegtran for JPEGs, supports flip-only operations
|
||||||
|
|
||||||
|
## AUR Packages
|
||||||
|
- [x] `stegasoo-cli` - standalone CLI package (no web dependencies)
|
||||||
|
- Created aur-cli/PKGBUILD with [cli,dct,compression] extras only
|
||||||
|
- No flask/gunicorn/fastapi/uvicorn/pyzbar deps
|
||||||
|
- 68MB vs 79MB for full package
|
||||||
|
- [x] `stegasoo-api` - REST API package
|
||||||
|
- Created aur-api/PKGBUILD with [api,cli,compression] extras
|
||||||
|
- Has fastapi/uvicorn, no flask/gunicorn
|
||||||
|
- 74MB package size
|
||||||
|
- Includes systemd service with TLS
|
||||||
|
|
||||||
|
## API Auth Work
|
||||||
|
- [x] API key authentication (simpler than OAuth2 for personal use)
|
||||||
|
- `frontends/api/auth.py` - key generation, hashing, validation
|
||||||
|
- Keys stored in `~/.stegasoo/api_keys.json` (hashed)
|
||||||
|
- `X-API-Key` header for authentication
|
||||||
|
- Auth disabled when no keys configured
|
||||||
|
- [x] TLS with self-signed certificates
|
||||||
|
- Auto-generates certs on first run
|
||||||
|
- CLI: `stegasoo api tls generate`
|
||||||
|
- Certs stored in `~/.stegasoo/certs/`
|
||||||
|
- [x] CLI commands for API management
|
||||||
|
- `stegasoo api keys list/create/delete`
|
||||||
|
- `stegasoo api tls generate/info`
|
||||||
|
- `stegasoo api serve` (starts with TLS by default)
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
- [ ] Postman collection (with environment templates)
|
||||||
818
UNDER_THE_HOOD.md
Normal file
@@ -0,0 +1,818 @@
|
|||||||
|
# Stegasoo Technical Deep Dive: Encoding & Decoding
|
||||||
|
|
||||||
|
A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work under the hood.
|
||||||
|
|
||||||
|
**Version 4.1** - Updated for channel keys and deployment isolation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [High-Level Overview](#high-level-overview)
|
||||||
|
2. [The Encoding Pipeline](#the-encoding-pipeline)
|
||||||
|
3. [The Decoding Pipeline](#the-decoding-pipeline)
|
||||||
|
4. [LSB Mode Deep Dive](#lsb-mode-deep-dive)
|
||||||
|
5. [DCT Mode Deep Dive](#dct-mode-deep-dive)
|
||||||
|
6. [Comparison Table](#comparison-table)
|
||||||
|
7. [Security Considerations](#security-considerations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High-Level Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ STEGASOO ARCHITECTURE (v4.1) │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ INPUTS PROCESSING OUTPUT │
|
||||||
|
│ ─────── ────────── ────── │
|
||||||
|
│ │
|
||||||
|
│ Reference Photo ─┐ │
|
||||||
|
│ Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key │
|
||||||
|
│ PIN/RSA Key ─────┤ │ │
|
||||||
|
│ Channel Key ─────┘ (v4.1) ▼ │
|
||||||
|
│ Message/File ────────────────────────► AES-256-GCM ──► Ciphertext │
|
||||||
|
│ Encryption │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ Carrier Image ───────────────────────────────────────► Embedding ─► Stego │
|
||||||
|
│ (LSB/DCT) Image │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### v4.0 Changes
|
||||||
|
|
||||||
|
| Change | v3.x | v4.0 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Authentication | day_phrase + date | passphrase (no date) |
|
||||||
|
| Default words | 3 | 4 |
|
||||||
|
| Header size | 75 bytes | 65 bytes (no date field) |
|
||||||
|
| Python support | 3.10+ | 3.10-3.12 only |
|
||||||
|
|
||||||
|
### v4.1 Changes
|
||||||
|
|
||||||
|
| Change | v4.0 | v4.1 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Channel keys | None | 32-byte deployment isolation |
|
||||||
|
| Key derivation | passphrase + ref + pin | passphrase + ref + pin + channel |
|
||||||
|
| Web auth | Session-based | Session + admin/user roles |
|
||||||
|
| Raspberry Pi | Manual setup | First-boot wizard with gum |
|
||||||
|
| Docker | Basic | Production-ready compose |
|
||||||
|
|
||||||
|
**Channel Keys** provide deployment isolation - messages encoded on one Stegasoo instance cannot be decoded by another instance with a different channel key, even with the same passphrase/PIN/reference photo.
|
||||||
|
|
||||||
|
### Module Responsibilities
|
||||||
|
|
||||||
|
| Module | File | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| **Crypto** | `crypto.py` | Key derivation (Argon2id), AES-256-GCM encryption/decryption |
|
||||||
|
| **Channel** | `channel.py` | Channel key management, deployment isolation (v4.1) |
|
||||||
|
| **Steganography** | `steganography.py` | LSB pixel manipulation, capacity calculation |
|
||||||
|
| **DCT Steganography** | `dct_steganography.py` | Frequency-domain embedding, jpegio integration |
|
||||||
|
| **Compression** | `compression.py` | Optional LZ4 compression of payload |
|
||||||
|
| **Validation** | `validation.py` | Input validation, size limits |
|
||||||
|
| **Utils** | `utils.py` | Image hashing, format detection |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Encoding Pipeline
|
||||||
|
|
||||||
|
### Step 1: Input Collection & Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# validation.py
|
||||||
|
def validate_encode_inputs(reference_photo, carrier, message, passphrase, pin, rsa_key):
|
||||||
|
# Check image dimensions (max 24 megapixels)
|
||||||
|
# Validate PIN format (6-9 digits)
|
||||||
|
# Validate passphrase (3-12 words from BIP-39)
|
||||||
|
# Check payload size vs carrier capacity
|
||||||
|
# Ensure reference != carrier (security)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Reference Photo Processing
|
||||||
|
|
||||||
|
```python
|
||||||
|
# utils.py
|
||||||
|
def get_image_hash(image_bytes: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Generate deterministic hash from reference photo.
|
||||||
|
This is the 'something you have' factor.
|
||||||
|
"""
|
||||||
|
# Resize to 256x256 (normalize different resolutions)
|
||||||
|
# Convert to grayscale (normalize color variations)
|
||||||
|
# Apply slight blur (reduce JPEG artifact sensitivity)
|
||||||
|
# SHA-256 hash of processed pixels
|
||||||
|
return hashlib.sha256(processed_pixels).digest() # 32 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why process the image?** Minor variations (JPEG recompression, slight crops) in the reference photo between sender and receiver would produce different hashes, breaking decryption. The preprocessing makes the hash more resilient.
|
||||||
|
|
||||||
|
### Step 3: Key Derivation (Argon2id)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# crypto.py
|
||||||
|
def derive_key(reference_hash: bytes, passphrase: str, pin: str,
|
||||||
|
rsa_signature: bytes = None) -> bytes:
|
||||||
|
"""
|
||||||
|
Combine all authentication factors into one AES key.
|
||||||
|
v4.0: No date parameter - simplified authentication.
|
||||||
|
"""
|
||||||
|
# Concatenate all factors
|
||||||
|
key_material = reference_hash + passphrase.encode() + pin.encode()
|
||||||
|
|
||||||
|
if rsa_signature:
|
||||||
|
key_material += rsa_signature
|
||||||
|
|
||||||
|
# Argon2id parameters (memory-hard to resist GPU attacks)
|
||||||
|
# - Memory: 256 MB
|
||||||
|
# - Iterations: 4
|
||||||
|
# - Parallelism: 4
|
||||||
|
# - Output: 32 bytes (256 bits)
|
||||||
|
|
||||||
|
key = argon2.hash_password_raw(
|
||||||
|
password=key_material,
|
||||||
|
salt=random_salt, # 16 bytes, stored with ciphertext
|
||||||
|
time_cost=4,
|
||||||
|
memory_cost=262144, # 256 MB
|
||||||
|
parallelism=4,
|
||||||
|
hash_len=32,
|
||||||
|
type=argon2.Type.ID
|
||||||
|
)
|
||||||
|
return key # 32-byte AES-256 key
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why Argon2id?**
|
||||||
|
- **Memory-hard**: Requires 256MB RAM per attempt, defeating GPU/ASIC attacks
|
||||||
|
- **Time-hard**: ~2-3 seconds per derivation
|
||||||
|
- **Side-channel resistant**: ID variant protects against timing attacks
|
||||||
|
|
||||||
|
### Step 4: Payload Preparation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# compression.py (optional)
|
||||||
|
def prepare_payload(data: bytes, filename: str = None) -> bytes:
|
||||||
|
"""
|
||||||
|
Prepare the payload with metadata header.
|
||||||
|
"""
|
||||||
|
# Header format (variable length):
|
||||||
|
# [1 byte] - Flags (compression, file mode, etc.)
|
||||||
|
# [4 bytes] - Original data length (big-endian)
|
||||||
|
# [2 bytes] - Filename length (if file mode)
|
||||||
|
# [N bytes] - Filename (if file mode)
|
||||||
|
# [N bytes] - Data (possibly compressed)
|
||||||
|
|
||||||
|
header = struct.pack('>BI', flags, len(data))
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
header += struct.pack('>H', len(filename)) + filename.encode()
|
||||||
|
|
||||||
|
# Optional LZ4 compression
|
||||||
|
if should_compress(data):
|
||||||
|
data = lz4.frame.compress(data)
|
||||||
|
flags |= FLAG_COMPRESSED
|
||||||
|
|
||||||
|
return header + data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: AES-256-GCM Encryption
|
||||||
|
|
||||||
|
```python
|
||||||
|
# crypto.py
|
||||||
|
def encrypt(plaintext: bytes, key: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Encrypt payload with AES-256-GCM.
|
||||||
|
Returns: salt + nonce + ciphertext + tag
|
||||||
|
"""
|
||||||
|
salt = os.urandom(16) # Random salt for key derivation
|
||||||
|
nonce = os.urandom(12) # Random nonce for GCM
|
||||||
|
|
||||||
|
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
||||||
|
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
|
||||||
|
|
||||||
|
# Final encrypted blob:
|
||||||
|
# [16 bytes] Salt
|
||||||
|
# [12 bytes] Nonce
|
||||||
|
# [16 bytes] Auth Tag
|
||||||
|
# [N bytes] Ciphertext
|
||||||
|
|
||||||
|
return salt + nonce + tag + ciphertext
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why GCM?**
|
||||||
|
- **Authenticated encryption**: Detects tampering
|
||||||
|
- **No padding oracle**: Stream cipher mode
|
||||||
|
- **Built-in integrity**: 128-bit authentication tag
|
||||||
|
|
||||||
|
### Step 6: Stego Header Construction
|
||||||
|
|
||||||
|
```python
|
||||||
|
# steganography.py / dct_steganography.py
|
||||||
|
def build_stego_header(encrypted_data: bytes, mode: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Build the header that precedes embedded data.
|
||||||
|
v4.0: Simplified header (no date field)
|
||||||
|
"""
|
||||||
|
# Header format:
|
||||||
|
# [4 bytes] - Magic number: "STGO" (v4)
|
||||||
|
# [1 byte] - Version (0x04)
|
||||||
|
# [1 byte] - Mode (0x01=LSB, 0x02=DCT)
|
||||||
|
# [4 bytes] - Payload length
|
||||||
|
# [N bytes] - Encrypted payload
|
||||||
|
|
||||||
|
if mode == 'lsb':
|
||||||
|
magic = b'STGO\x04\x01' # v4, mode 1 (LSB)
|
||||||
|
else:
|
||||||
|
magic = b'STGO\x04\x02' # v4, mode 2 (DCT)
|
||||||
|
|
||||||
|
length = struct.pack('>I', len(encrypted_data))
|
||||||
|
|
||||||
|
return magic + length + encrypted_data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Embedding (Mode-Specific)
|
||||||
|
|
||||||
|
This is where LSB and DCT diverge. See detailed sections below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Decoding Pipeline
|
||||||
|
|
||||||
|
### Step 1: Mode Detection
|
||||||
|
|
||||||
|
```python
|
||||||
|
def detect_mode(stego_image: bytes) -> str:
|
||||||
|
"""
|
||||||
|
Detect which embedding mode was used.
|
||||||
|
Checks format and magic bytes.
|
||||||
|
"""
|
||||||
|
img = Image.open(io.BytesIO(stego_image))
|
||||||
|
|
||||||
|
# JPEG images with JPGS magic = DCT mode with jpegio
|
||||||
|
if img.format == 'JPEG':
|
||||||
|
# Check for jpegio magic
|
||||||
|
return 'dct'
|
||||||
|
|
||||||
|
# PNG/BMP: Read first few bytes from LSB
|
||||||
|
# Check for STGO or DCTS magic
|
||||||
|
magic = extract_header_lsb(stego_image, 6)
|
||||||
|
|
||||||
|
if magic.startswith(b'STGO'):
|
||||||
|
mode_byte = magic[5]
|
||||||
|
return 'lsb' if mode_byte == 0x01 else 'dct'
|
||||||
|
elif magic.startswith(b'DCTS'):
|
||||||
|
return 'dct'
|
||||||
|
|
||||||
|
return 'lsb' # Default fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Key Re-derivation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Same process as encoding
|
||||||
|
def derive_key_for_decode(reference_hash, passphrase, pin, rsa_signature=None):
|
||||||
|
# Must use SAME parameters as encoding
|
||||||
|
# No date parameter in v4.0
|
||||||
|
return derive_key(reference_hash, passphrase, pin, rsa_signature)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Data Extraction
|
||||||
|
|
||||||
|
```python
|
||||||
|
def extract_data(stego_image: bytes, mode: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Extract raw bytes from stego image.
|
||||||
|
Mode-specific extraction.
|
||||||
|
"""
|
||||||
|
if mode == 'dct':
|
||||||
|
return extract_from_dct(stego_image, pixel_key)
|
||||||
|
else:
|
||||||
|
return extract_from_lsb(stego_image, pixel_key)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Decryption & Payload Recovery
|
||||||
|
|
||||||
|
```python
|
||||||
|
def decrypt_and_recover(encrypted_data: bytes, key: bytes) -> Union[str, bytes]:
|
||||||
|
"""
|
||||||
|
Decrypt and extract original message/file.
|
||||||
|
"""
|
||||||
|
# Parse header
|
||||||
|
salt = encrypted_data[:16]
|
||||||
|
nonce = encrypted_data[16:28]
|
||||||
|
tag = encrypted_data[28:44]
|
||||||
|
ciphertext = encrypted_data[44:]
|
||||||
|
|
||||||
|
# Decrypt
|
||||||
|
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
||||||
|
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
|
||||||
|
|
||||||
|
# Decompress if needed
|
||||||
|
if plaintext[0] & FLAG_COMPRESSED:
|
||||||
|
plaintext = lz4.frame.decompress(plaintext[5:])
|
||||||
|
|
||||||
|
# Extract payload
|
||||||
|
return parse_payload(plaintext)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LSB Mode Deep Dive
|
||||||
|
|
||||||
|
### How LSB Embedding Works
|
||||||
|
|
||||||
|
LSB (Least Significant Bit) embedding modifies the lowest bit of each color channel in selected pixels.
|
||||||
|
|
||||||
|
```
|
||||||
|
Original Pixel (RGB):
|
||||||
|
R: 11010110 G: 01101001 B: 10110100
|
||||||
|
↓ ↓ ↓
|
||||||
|
└─────────┴─────────┘
|
||||||
|
3 bits available
|
||||||
|
|
||||||
|
After embedding "101":
|
||||||
|
R: 1101011[1] G: 0110100[0] B: 1011010[1]
|
||||||
|
↑ ↑ ↑
|
||||||
|
modified modified modified
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pixel Selection Algorithm
|
||||||
|
|
||||||
|
```python
|
||||||
|
def select_pixels(carrier_shape, num_bits, seed: bytes) -> List[Tuple[int, int, int]]:
|
||||||
|
"""
|
||||||
|
Generate pseudo-random pixel coordinates.
|
||||||
|
Distributes modifications across entire image.
|
||||||
|
"""
|
||||||
|
height, width, channels = carrier_shape
|
||||||
|
total_positions = height * width * 3 # RGB channels
|
||||||
|
|
||||||
|
# Use seed to generate reproducible random order
|
||||||
|
rng = np.random.RandomState(int.from_bytes(seed[:4], 'big'))
|
||||||
|
all_positions = np.arange(total_positions)
|
||||||
|
rng.shuffle(all_positions)
|
||||||
|
|
||||||
|
# Convert flat indices to (y, x, channel)
|
||||||
|
selected = []
|
||||||
|
for idx in all_positions[:num_bits]:
|
||||||
|
y = idx // (width * 3)
|
||||||
|
x = (idx % (width * 3)) // 3
|
||||||
|
c = idx % 3
|
||||||
|
selected.append((y, x, c))
|
||||||
|
|
||||||
|
return selected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Embedding Process
|
||||||
|
|
||||||
|
```python
|
||||||
|
def embed_lsb(carrier: np.ndarray, data: bytes, seed: bytes) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Embed data using LSB substitution.
|
||||||
|
"""
|
||||||
|
bits = bytes_to_bits(data)
|
||||||
|
positions = select_pixels(carrier.shape, len(bits), seed)
|
||||||
|
|
||||||
|
stego = carrier.copy()
|
||||||
|
for i, (y, x, c) in enumerate(positions):
|
||||||
|
# Clear LSB and set to our bit
|
||||||
|
stego[y, x, c] = (stego[y, x, c] & 0xFE) | bits[i]
|
||||||
|
|
||||||
|
return stego
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capacity Calculation
|
||||||
|
|
||||||
|
```python
|
||||||
|
def calculate_lsb_capacity(width: int, height: int) -> int:
|
||||||
|
"""
|
||||||
|
Calculate maximum payload size for LSB mode.
|
||||||
|
"""
|
||||||
|
total_bits = width * height * 3 # 3 bits per pixel (RGB)
|
||||||
|
header_bits = 10 * 8 # 10-byte stego header
|
||||||
|
available_bits = total_bits - header_bits
|
||||||
|
|
||||||
|
return available_bits // 8 # Convert to bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example capacities:**
|
||||||
|
- 1920×1080: ~770 KB
|
||||||
|
- 4000×3000: ~4.5 MB
|
||||||
|
- 800×600: ~180 KB
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DCT Mode Deep Dive
|
||||||
|
|
||||||
|
### How DCT Embedding Works
|
||||||
|
|
||||||
|
DCT (Discrete Cosine Transform) mode embeds data in the frequency-domain coefficients, making it resilient to JPEG compression.
|
||||||
|
|
||||||
|
```
|
||||||
|
Image Block (8×8 pixels)
|
||||||
|
↓
|
||||||
|
DCT Transform
|
||||||
|
↓
|
||||||
|
DCT Coefficients (8×8)
|
||||||
|
┌────────────────────┐
|
||||||
|
│ DC AC₁ AC₂ AC₃ ...│ ← Lower frequencies (top-left)
|
||||||
|
│ AC₄ AC₅ AC₆ ... │
|
||||||
|
│ ... ... │ ← Mid frequencies (embed here)
|
||||||
|
│ ... ... │
|
||||||
|
│ AC₆₃ ────│ ← Higher frequencies (bottom-right)
|
||||||
|
└────────────────────┘
|
||||||
|
↓
|
||||||
|
Modify select ACs
|
||||||
|
↓
|
||||||
|
IDCT Transform
|
||||||
|
↓
|
||||||
|
Modified Image Block
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coefficient Selection
|
||||||
|
|
||||||
|
```python
|
||||||
|
# dct_steganography.py
|
||||||
|
EMBED_POSITIONS = [
|
||||||
|
(0, 1), (1, 0), (2, 0), (1, 1), (0, 2), (0, 3), (1, 2), (2, 1), (3, 0),
|
||||||
|
(4, 0), (3, 1), (2, 2), (1, 3), (0, 4), (0, 5), (1, 4), (2, 3), (3, 2),
|
||||||
|
(4, 1), (5, 0), (5, 1), (4, 2), (3, 3), (2, 4), (1, 5), (0, 6), (0, 7),
|
||||||
|
(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Use positions 4-20 (mid-frequency, good balance)
|
||||||
|
DEFAULT_EMBED_POSITIONS = EMBED_POSITIONS[4:20] # 16 positions per block
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why mid-frequency?**
|
||||||
|
- DC coefficient (0,0): Too visible, contains brightness
|
||||||
|
- Low AC: Visible changes, but survives compression
|
||||||
|
- Mid AC: Best balance of invisibility + resilience
|
||||||
|
- High AC: Invisible but destroyed by compression
|
||||||
|
|
||||||
|
### Block Processing
|
||||||
|
|
||||||
|
```python
|
||||||
|
def embed_in_block(block: np.ndarray, bits: List[int]) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Embed bits in a single 8×8 block.
|
||||||
|
"""
|
||||||
|
# Forward DCT
|
||||||
|
dct_block = dct_2d(block)
|
||||||
|
|
||||||
|
# Embed using quantization
|
||||||
|
for i, pos in enumerate(DEFAULT_EMBED_POSITIONS):
|
||||||
|
if i >= len(bits):
|
||||||
|
break
|
||||||
|
|
||||||
|
coef = dct_block[pos[0], pos[1]]
|
||||||
|
# Quantize and modify LSB
|
||||||
|
quantized = round(coef / QUANT_STEP)
|
||||||
|
if (quantized % 2) != bits[i]:
|
||||||
|
quantized += 1 if coef > 0 else -1
|
||||||
|
dct_block[pos[0], pos[1]] = quantized * QUANT_STEP
|
||||||
|
|
||||||
|
# Inverse DCT
|
||||||
|
return idct_2d(dct_block)
|
||||||
|
```
|
||||||
|
|
||||||
|
### jpegio Integration (Native JPEG Output)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def embed_jpegio(data: bytes, carrier_jpeg: bytes, seed: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Embed directly in JPEG DCT coefficients using jpegio.
|
||||||
|
Preserves JPEG structure perfectly.
|
||||||
|
|
||||||
|
Note: Requires Python 3.12 or earlier (jpegio incompatible with 3.13)
|
||||||
|
"""
|
||||||
|
import jpegio as jio
|
||||||
|
|
||||||
|
# Normalize problematic JPEGs (quality=100 causes crashes)
|
||||||
|
carrier_jpeg = normalize_jpeg_for_jpegio(carrier_jpeg)
|
||||||
|
|
||||||
|
# Read existing JPEG coefficients
|
||||||
|
jpeg = jio.read(temp_file_from_bytes(carrier_jpeg))
|
||||||
|
coef_array = jpeg.coef_arrays[0] # Y channel
|
||||||
|
|
||||||
|
# Find usable coefficients (magnitude >= 2, non-DC)
|
||||||
|
positions = get_usable_positions(coef_array)
|
||||||
|
order = generate_order(len(positions), seed)
|
||||||
|
|
||||||
|
# Embed by modifying coefficient LSBs
|
||||||
|
bits = bytes_to_bits(data)
|
||||||
|
for i, pos_idx in enumerate(order[:len(bits)]):
|
||||||
|
row, col = positions[pos_idx]
|
||||||
|
coef = coef_array[row, col]
|
||||||
|
|
||||||
|
if (coef & 1) != bits[i]:
|
||||||
|
# Flip LSB while preserving sign
|
||||||
|
if coef > 0:
|
||||||
|
coef_array[row, col] = coef - 1 if (coef & 1) else coef + 1
|
||||||
|
else:
|
||||||
|
coef_array[row, col] = coef + 1 if (coef & 1) else coef - 1
|
||||||
|
|
||||||
|
# Write modified JPEG
|
||||||
|
jio.write(jpeg, output_path)
|
||||||
|
return read_bytes(output_path)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JPEG Normalization (v4.0)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def normalize_jpeg_for_jpegio(image_data: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Normalize problematic JPEGs before jpegio processing.
|
||||||
|
|
||||||
|
JPEGs with quality=100 have quantization tables with all values=1,
|
||||||
|
which causes jpegio to crash. Re-save at quality 95.
|
||||||
|
"""
|
||||||
|
img = Image.open(io.BytesIO(image_data))
|
||||||
|
|
||||||
|
if img.format != 'JPEG':
|
||||||
|
return image_data
|
||||||
|
|
||||||
|
# Check if any quantization table has all values <= 1
|
||||||
|
needs_normalization = False
|
||||||
|
if hasattr(img, 'quantization'):
|
||||||
|
for table in img.quantization.values():
|
||||||
|
if max(table) <= 1:
|
||||||
|
needs_normalization = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not needs_normalization:
|
||||||
|
return image_data
|
||||||
|
|
||||||
|
# Re-save at safe quality
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
img.save(buffer, format='JPEG', quality=95, subsampling=0)
|
||||||
|
return buffer.getvalue()
|
||||||
|
```
|
||||||
|
|
||||||
|
### DCT Capacity Calculation
|
||||||
|
|
||||||
|
```python
|
||||||
|
def calculate_dct_capacity(width: int, height: int) -> int:
|
||||||
|
"""
|
||||||
|
Calculate maximum payload for DCT mode.
|
||||||
|
"""
|
||||||
|
blocks_x = width // 8
|
||||||
|
blocks_y = height // 8
|
||||||
|
total_blocks = blocks_x * blocks_y
|
||||||
|
|
||||||
|
bits_per_block = len(DEFAULT_EMBED_POSITIONS) # 16
|
||||||
|
total_bits = total_blocks * bits_per_block
|
||||||
|
|
||||||
|
header_bits = 10 * 8 # Stego header
|
||||||
|
available_bits = total_bits - header_bits
|
||||||
|
|
||||||
|
return available_bits // 8
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example capacities:**
|
||||||
|
- 1920×1080: ~64 KB
|
||||||
|
- 4000×3000: ~375 KB
|
||||||
|
- 800×600: ~14 KB
|
||||||
|
|
||||||
|
### Why DCT Survives JPEG Compression
|
||||||
|
|
||||||
|
```
|
||||||
|
Original JPEG: Stego JPEG: Re-compressed:
|
||||||
|
|
||||||
|
DCT coefficients Modified DCT Coefficients
|
||||||
|
preserved in coefficients re-quantized
|
||||||
|
file format still valid
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
[DCT] ──────► [Modified] ──────► [Still
|
||||||
|
[coefs] [DCT coefs] Modified!]
|
||||||
|
|
||||||
|
LSB changes survive because they're embedded in
|
||||||
|
the frequency domain, not spatial pixel values.
|
||||||
|
```
|
||||||
|
|
||||||
|
### DCT Advantages
|
||||||
|
|
||||||
|
| Advantage | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| **JPEG resilient** | Survives social media upload |
|
||||||
|
| **Better steganalysis resistance** | Harder to detect statistically |
|
||||||
|
| **Natural-looking output** | JPEG artifacts expected |
|
||||||
|
|
||||||
|
### DCT Limitations
|
||||||
|
|
||||||
|
| Limitation | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| **Lower capacity** | ~10% of LSB capacity |
|
||||||
|
| **Slower processing** | DCT transforms are compute-intensive |
|
||||||
|
| **Requires scipy/jpegio** | Additional dependencies |
|
||||||
|
| **Quality-dependent** | Heavy recompression still degrades data |
|
||||||
|
| **Python version** | jpegio requires Python 3.12 or earlier |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison Table
|
||||||
|
|
||||||
|
| Aspect | LSB Mode | DCT Mode |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| **Capacity (1080p)** | ~770 KB | ~50 KB |
|
||||||
|
| **Output Format** | PNG only | PNG or JPEG |
|
||||||
|
| **Survives JPEG** | ❌ No | ✅ Yes |
|
||||||
|
| **Social Media** | ❌ Broken | ✅ Works |
|
||||||
|
| **Processing Speed** | Fast (~0.5s) | Slower (~2s) |
|
||||||
|
| **Dependencies** | Pillow, NumPy | + scipy, jpegio |
|
||||||
|
| **Color Support** | Full color | Color or Grayscale |
|
||||||
|
| **Detection Resistance** | Moderate | Better |
|
||||||
|
| **Best For** | Email, cloud storage | Social media, messaging |
|
||||||
|
| **Max Tested Image** | 14MB+ | 14MB+ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### What Makes Stegasoo Secure?
|
||||||
|
|
||||||
|
```
|
||||||
|
MULTI-FACTOR AUTHENTICATION (v4.0)
|
||||||
|
──────────────────────────────────
|
||||||
|
Factor 1: Reference Photo ─┐
|
||||||
|
• 80-256 bits entropy │
|
||||||
|
• "Something you have" │
|
||||||
|
├──► Combined entropy: 133-400+ bits
|
||||||
|
Factor 2: Passphrase │ (Beyond brute force)
|
||||||
|
• 43-132 bits entropy │
|
||||||
|
• "Something you know" │
|
||||||
|
• 4 words default (v4.0) │
|
||||||
|
│
|
||||||
|
Factor 3: PIN │
|
||||||
|
• 20-30 bits entropy │
|
||||||
|
• "Something you know" │
|
||||||
|
│
|
||||||
|
Factor 4: RSA Key (optional) ─┘
|
||||||
|
• 112-128 bits entropy
|
||||||
|
• "Something you have"
|
||||||
|
|
||||||
|
MEMORY-HARD KDF (Argon2id)
|
||||||
|
──────────────────────────
|
||||||
|
• 256 MB RAM per attempt
|
||||||
|
• ~3 seconds per attempt
|
||||||
|
• Defeats GPU/ASIC attacks
|
||||||
|
• 10 attempts = 30 seconds, not 0.00001 seconds
|
||||||
|
|
||||||
|
AUTHENTICATED ENCRYPTION (AES-256-GCM)
|
||||||
|
──────────────────────────────────────
|
||||||
|
• 256-bit key (unbreakable)
|
||||||
|
• Built-in integrity check
|
||||||
|
• Detects tampering
|
||||||
|
• No padding oracle attacks
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attack Surface Analysis
|
||||||
|
|
||||||
|
| Attack | LSB Protection | DCT Protection |
|
||||||
|
|--------|----------------|----------------|
|
||||||
|
| Visual inspection | ✅ Imperceptible | ✅ Imperceptible |
|
||||||
|
| File size analysis | ⚠️ PNG larger | ✅ JPEG natural |
|
||||||
|
| Histogram analysis | ⚠️ Slight anomalies | ✅ Normal JPEG |
|
||||||
|
| Chi-square attack | ⚠️ Detectable at scale | ✅ Resistant |
|
||||||
|
| RS steganalysis | ⚠️ Detectable | ✅ Resistant |
|
||||||
|
| JPEG recompression | ❌ Destroyed | ✅ Survives |
|
||||||
|
|
||||||
|
### Threat Model
|
||||||
|
|
||||||
|
**Stegasoo protects against:**
|
||||||
|
- ✅ Passive eavesdropping
|
||||||
|
- ✅ Casual inspection of images
|
||||||
|
- ✅ Basic forensic analysis
|
||||||
|
- ✅ Brute force key guessing
|
||||||
|
- ✅ JPEG recompression (DCT mode)
|
||||||
|
|
||||||
|
**Stegasoo does NOT protect against:**
|
||||||
|
- ⚠️ Targeted forensic analysis with original carrier
|
||||||
|
- ⚠️ Nation-state level steganalysis
|
||||||
|
- ⚠️ Rubber hose cryptanalysis (coercion)
|
||||||
|
- ⚠️ Compromise of reference photo or credentials
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Flow Diagrams
|
||||||
|
|
||||||
|
### Complete Encode Flow (v4.0)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ENCODE FLOW (v4.0) │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
User Inputs Processing Output
|
||||||
|
─────────── ────────── ──────
|
||||||
|
|
||||||
|
Reference Photo ──────┐
|
||||||
|
├──► get_image_hash() ──► ref_hash (32 bytes)
|
||||||
|
│ │
|
||||||
|
Passphrase ───────────┤ ▼
|
||||||
|
├──► derive_key() ──────► aes_key (32 bytes)
|
||||||
|
PIN ──────────────────┤ (Argon2id) │
|
||||||
|
│ │
|
||||||
|
RSA Key (optional) ───┘ │
|
||||||
|
▼
|
||||||
|
Message/File ──────────► prepare_payload() ──► encrypt() ──► ciphertext
|
||||||
|
(compress, header) (AES-GCM) │
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
build_stego_header()
|
||||||
|
(magic + length)
|
||||||
|
│
|
||||||
|
Carrier Image ─────────────────────────────────────────► embed()
|
||||||
|
│ │
|
||||||
|
┌───────────┴─────┴────────────┐
|
||||||
|
│ │
|
||||||
|
LSB Mode DCT Mode
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
embed_lsb() embed_in_dct()
|
||||||
|
(pixel LSBs) (DCT coefficients)
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
PNG Output PNG or JPEG
|
||||||
|
│ │
|
||||||
|
└──────────┬───────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Stego Image
|
||||||
|
(downloadable)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Decode Flow (v4.0)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DECODE FLOW (v4.0) │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
User Inputs Processing Output
|
||||||
|
─────────── ────────── ──────
|
||||||
|
|
||||||
|
Reference Photo ──────┐
|
||||||
|
├──► get_image_hash() ──► ref_hash (32 bytes)
|
||||||
|
│ │
|
||||||
|
Passphrase ───────────┤ ▼
|
||||||
|
├──► derive_key() ──────► aes_key (32 bytes)
|
||||||
|
PIN ──────────────────┤ (Argon2id) │
|
||||||
|
│ (MUST MATCH!) │
|
||||||
|
RSA Key (optional) ───┘ │
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Stego Image ──────────► detect_mode() ──────► extract()
|
||||||
|
(read magic) │ │
|
||||||
|
│ ┌─────────┴─────┴──────────┐
|
||||||
|
│ │ │
|
||||||
|
│ LSB Mode DCT Mode
|
||||||
|
│ │ │
|
||||||
|
│ ▼ ▼
|
||||||
|
│ extract_lsb() extract_from_dct()
|
||||||
|
│ │ │
|
||||||
|
│ └────────┬─────────────────┘
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ parse_stego_header()
|
||||||
|
│ (magic, length)
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
└────────► decrypt()
|
||||||
|
(AES-GCM)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
decompress()
|
||||||
|
(if compressed)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
extract_payload()
|
||||||
|
(handle file/text)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Original Message
|
||||||
|
or File
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**LSB Mode** is simpler, faster, and higher capacity - perfect for controlled channels where images won't be modified.
|
||||||
|
|
||||||
|
**DCT Mode** is more complex but survives real-world image processing - essential for social media and messaging apps.
|
||||||
|
|
||||||
|
Both modes share the same cryptographic foundation (Argon2id + AES-256-GCM) and multi-factor authentication, ensuring security regardless of embedding method.
|
||||||
|
|
||||||
|
The choice comes down to your use case:
|
||||||
|
- **Public platform?** → DCT (maximum compatibility)
|
||||||
|
- **Private channel?** → LSB (maximum capacity)
|
||||||
|
|
||||||
|
### v4.0 Simplifications
|
||||||
|
|
||||||
|
- **No more date tracking** - encode/decode anytime without remembering dates
|
||||||
|
- **Single passphrase** - no daily rotation to manage
|
||||||
|
- **Default 4 words** - better security out of the box
|
||||||
|
- **JPEG normalization** - handles quality=100 images automatically
|
||||||
|
- **Large image support** - tested with 14MB+ images
|
||||||
42
WISHLIST-4.2.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Stegasoo v4.2 Wishlist
|
||||||
|
|
||||||
|
Blue sky ideas for future development. No timeline - just capturing thoughts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### GPU-Accelerated DCT Encoding/Decoding
|
||||||
|
- **Idea**: Leverage GPU for JPEG DCT coefficient manipulation
|
||||||
|
- **Potential Approaches**:
|
||||||
|
- OpenCL/CUDA for parallel DCT operations
|
||||||
|
- Raspberry Pi VideoCore IV/VI GPU compute
|
||||||
|
- WebGPU for browser-based acceleration
|
||||||
|
- **Challenges**:
|
||||||
|
- jpegio library is CPU-bound (C extension)
|
||||||
|
- Would need custom DCT implementation
|
||||||
|
- Memory transfer overhead may negate gains for small images
|
||||||
|
- **Research**:
|
||||||
|
- libjpeg-turbo uses SIMD but not GPU
|
||||||
|
- nvJPEG (NVIDIA) does GPU-accelerated JPEG
|
||||||
|
- Could potentially use GPU for the embedding math, not JPEG decode
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
(Add ideas here)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
(Add ideas here)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This is a living document - add ideas anytime
|
||||||
|
- Not all ideas will be implemented
|
||||||
|
- Feasibility research needed before committing to roadmap
|
||||||
23
aur-api/.SRCINFO
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
pkgbase = stegasoo-api-git
|
||||||
|
pkgdesc = Stegasoo REST API with TLS and API key authentication
|
||||||
|
pkgver = 4.2.1
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://github.com/adlee-was-taken/stegasoo
|
||||||
|
install = stegasoo-api-git.install
|
||||||
|
arch = x86_64
|
||||||
|
license = MIT
|
||||||
|
makedepends = git
|
||||||
|
makedepends = python
|
||||||
|
makedepends = python-build
|
||||||
|
makedepends = python-hatchling
|
||||||
|
depends = python>=3.11
|
||||||
|
depends = zbar
|
||||||
|
optdepends = libjpeg-turbo: jpegtran for lossless JPEG rotation (DCT-safe)
|
||||||
|
provides = stegasoo-api
|
||||||
|
conflicts = stegasoo-api
|
||||||
|
conflicts = stegasoo
|
||||||
|
conflicts = stegasoo-git
|
||||||
|
source = stegasoo-api-git::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main
|
||||||
|
sha256sums = SKIP
|
||||||
|
|
||||||
|
pkgname = stegasoo-api-git
|
||||||
109
aur-api/PKGBUILD
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# Maintainer: Aaron D. Lee <your-email@example.com>
|
||||||
|
pkgname=stegasoo-api-git
|
||||||
|
pkgver=4.2.1
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Stegasoo REST API with TLS and API key authentication"
|
||||||
|
arch=('x86_64')
|
||||||
|
url="https://github.com/adlee-was-taken/stegasoo"
|
||||||
|
license=('MIT')
|
||||||
|
|
||||||
|
# Python 3.11-3.14 supported
|
||||||
|
depends=(
|
||||||
|
'python>=3.11'
|
||||||
|
'zbar' # QR code reading for RSA key extraction
|
||||||
|
)
|
||||||
|
makedepends=(
|
||||||
|
'git'
|
||||||
|
'python'
|
||||||
|
'python-build'
|
||||||
|
'python-hatchling'
|
||||||
|
)
|
||||||
|
optdepends=(
|
||||||
|
'libjpeg-turbo: jpegtran for lossless JPEG rotation (DCT-safe)'
|
||||||
|
)
|
||||||
|
provides=('stegasoo-api')
|
||||||
|
conflicts=('stegasoo-api' 'stegasoo' 'stegasoo-git')
|
||||||
|
install=stegasoo-api-git.install
|
||||||
|
source=("${pkgname}::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main")
|
||||||
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
|
pkgver() {
|
||||||
|
cd "$pkgname"
|
||||||
|
git describe --long --tags 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || \
|
||||||
|
printf "%s.r%s.g%s" "4.2.1" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$pkgname"
|
||||||
|
python -m build --wheel --no-isolation
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$pkgname"
|
||||||
|
|
||||||
|
# Detect Python version for site-packages path
|
||||||
|
local pyver=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||||
|
|
||||||
|
# Install to /opt/stegasoo-api with dedicated venv
|
||||||
|
install -dm755 "$pkgdir/opt/stegasoo-api"
|
||||||
|
|
||||||
|
# Create fresh venv in package
|
||||||
|
python -m venv "$pkgdir/opt/stegasoo-api/venv"
|
||||||
|
|
||||||
|
# Install the wheel with API + CLI + compression extras
|
||||||
|
local wheel=$(ls dist/*.whl | head -1)
|
||||||
|
"$pkgdir/opt/stegasoo-api/venv/bin/pip" install --no-cache-dir "${wheel}[api,cli,compression]"
|
||||||
|
|
||||||
|
# Install API frontend (not included in wheel by default)
|
||||||
|
local site_packages="$pkgdir/opt/stegasoo-api/venv/lib/python${pyver}/site-packages"
|
||||||
|
install -dm755 "$site_packages/frontends/api"
|
||||||
|
cp -r frontends/api/*.py "$site_packages/frontends/api/"
|
||||||
|
cp -r frontends/__init__.py "$site_packages/frontends/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create temp directory for API
|
||||||
|
install -dm755 "$site_packages/frontends/api/temp_files"
|
||||||
|
|
||||||
|
# Create config directories
|
||||||
|
install -dm755 "$pkgdir/opt/stegasoo-api/config"
|
||||||
|
install -dm700 "$pkgdir/opt/stegasoo-api/certs"
|
||||||
|
|
||||||
|
# Fix shebangs - replace build-time paths with installed paths
|
||||||
|
find "$pkgdir/opt/stegasoo-api/venv/bin" -type f -exec \
|
||||||
|
sed -i "s|$pkgdir/opt/stegasoo-api/venv|/opt/stegasoo-api/venv|g" {} \;
|
||||||
|
|
||||||
|
# Fix pyvenv.cfg
|
||||||
|
sed -i "s|$pkgdir||g" "$pkgdir/opt/stegasoo-api/venv/pyvenv.cfg"
|
||||||
|
|
||||||
|
# Create symlink to /usr/bin
|
||||||
|
install -dm755 "$pkgdir/usr/bin"
|
||||||
|
ln -s /opt/stegasoo-api/venv/bin/stegasoo "$pkgdir/usr/bin/stegasoo"
|
||||||
|
|
||||||
|
# Install license
|
||||||
|
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
|
||||||
|
# Install docs
|
||||||
|
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||||
|
|
||||||
|
# Install systemd service
|
||||||
|
install -Dm644 /dev/stdin "$pkgdir/usr/lib/systemd/system/stegasoo-api.service" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Stegasoo REST API (HTTPS)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=stegasoo
|
||||||
|
WorkingDirectory=/opt/stegasoo-api/venv/lib/python${pyver}/site-packages/frontends/api
|
||||||
|
Environment="PATH=/opt/stegasoo-api/venv/bin"
|
||||||
|
Environment="HOME=/opt/stegasoo-api"
|
||||||
|
# TLS enabled by default - certs auto-generated on first run
|
||||||
|
# Use: stegasoo api tls generate (to pre-generate certs)
|
||||||
|
# Use: stegasoo api keys create <name> (to create API keys)
|
||||||
|
ExecStart=/opt/stegasoo-api/venv/bin/stegasoo api serve --host 127.0.0.1 --port 8000
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
}
|
||||||
102
aur-api/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Stegasoo API AUR Package
|
||||||
|
|
||||||
|
REST API server package for programmatic steganography operations. Includes HTTPS support and API key authentication.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From AUR (once published)
|
||||||
|
```bash
|
||||||
|
yay -S stegasoo-api-git
|
||||||
|
# or
|
||||||
|
paru -S stegasoo-api-git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual build
|
||||||
|
```bash
|
||||||
|
git clone https://aur.archlinux.org/stegasoo-api-git.git
|
||||||
|
cd stegasoo-api-git
|
||||||
|
makepkg -si
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Gets Installed
|
||||||
|
|
||||||
|
- `/opt/stegasoo-api/venv/` - Self-contained Python venv with API dependencies
|
||||||
|
- `/opt/stegasoo-api/config/` - API key storage
|
||||||
|
- `/opt/stegasoo-api/certs/` - TLS certificates
|
||||||
|
- `/usr/bin/stegasoo` - CLI executable
|
||||||
|
- `/usr/lib/systemd/system/stegasoo-api.service` - Systemd service
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create an API key
|
||||||
|
sudo -u stegasoo stegasoo api keys create mykey
|
||||||
|
|
||||||
|
# 2. Start the service
|
||||||
|
sudo systemctl enable --now stegasoo-api
|
||||||
|
|
||||||
|
# 3. Test the API
|
||||||
|
curl -k -H "X-API-Key: YOUR_KEY" https://localhost:8000/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Details
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Port | 8000 |
|
||||||
|
| Protocol | HTTPS (self-signed cert auto-generated) |
|
||||||
|
| API Docs | https://localhost:8000/docs |
|
||||||
|
| OpenAPI | https://localhost:8000/openapi.json |
|
||||||
|
|
||||||
|
## API Key Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all keys
|
||||||
|
stegasoo api keys list
|
||||||
|
|
||||||
|
# Create a new key
|
||||||
|
sudo -u stegasoo stegasoo api keys create <name>
|
||||||
|
|
||||||
|
# Revoke a key
|
||||||
|
sudo -u stegasoo stegasoo api keys revoke <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## TLS Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View current certificate info
|
||||||
|
stegasoo api tls info
|
||||||
|
|
||||||
|
# Generate new self-signed certificate
|
||||||
|
sudo -u stegasoo stegasoo api tls generate
|
||||||
|
|
||||||
|
# Use custom certificates (edit service)
|
||||||
|
sudo systemctl edit stegasoo-api
|
||||||
|
# Add:
|
||||||
|
# [Service]
|
||||||
|
# ExecStart=
|
||||||
|
# ExecStart=/opt/stegasoo-api/venv/bin/stegasoo api serve \
|
||||||
|
# --host 0.0.0.0 --port 8000 \
|
||||||
|
# --cert /path/to/cert.pem --key /path/to/key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Run (without systemd)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development mode (auto-reload)
|
||||||
|
/opt/stegasoo-api/venv/bin/stegasoo api serve --reload
|
||||||
|
|
||||||
|
# Production mode
|
||||||
|
/opt/stegasoo-api/venv/bin/stegasoo api serve --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## For Web UI
|
||||||
|
|
||||||
|
Install the full package instead:
|
||||||
|
```bash
|
||||||
|
yay -S stegasoo-git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintainer
|
||||||
|
|
||||||
|
Aaron D. Lee
|
||||||
63
aur-api/stegasoo-api-git.install
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
post_install() {
|
||||||
|
# Create stegasoo system user if it doesn't exist
|
||||||
|
if ! getent passwd stegasoo >/dev/null; then
|
||||||
|
useradd -r -s /usr/bin/nologin -d /opt/stegasoo-api stegasoo
|
||||||
|
echo "Created system user 'stegasoo'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set ownership of directories
|
||||||
|
chown -R stegasoo:stegasoo /opt/stegasoo-api/config 2>/dev/null || true
|
||||||
|
chown -R stegasoo:stegasoo /opt/stegasoo-api/certs 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==============================================="
|
||||||
|
echo " Stegasoo API installed successfully!"
|
||||||
|
echo "==============================================="
|
||||||
|
echo ""
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " Quick Start"
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " 1. Create an API key:"
|
||||||
|
echo " sudo -u stegasoo stegasoo api keys create mykey"
|
||||||
|
echo ""
|
||||||
|
echo " 2. Start the API server:"
|
||||||
|
echo " sudo systemctl start stegasoo-api"
|
||||||
|
echo " sudo systemctl enable stegasoo-api # auto-start"
|
||||||
|
echo ""
|
||||||
|
echo " 3. Access the API:"
|
||||||
|
echo " curl -k -H 'X-API-Key: YOUR_KEY' https://localhost:8000/"
|
||||||
|
echo ""
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " Service Details"
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " Port: 8000 (HTTPS by default)"
|
||||||
|
echo " Docs: https://localhost:8000/docs"
|
||||||
|
echo " Status: sudo systemctl status stegasoo-api"
|
||||||
|
echo ""
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " Management Commands"
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " stegasoo api keys list # List API keys"
|
||||||
|
echo " stegasoo api keys create X # Create new key"
|
||||||
|
echo " stegasoo api tls generate # Generate TLS certs"
|
||||||
|
echo " stegasoo api tls info # Show certificate info"
|
||||||
|
echo " stegasoo api serve --help # Server options"
|
||||||
|
echo ""
|
||||||
|
echo "==============================================="
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
post_upgrade() {
|
||||||
|
post_install
|
||||||
|
}
|
||||||
|
|
||||||
|
pre_remove() {
|
||||||
|
# Stop service if running
|
||||||
|
systemctl stop stegasoo-api 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
post_remove() {
|
||||||
|
echo "Stegasoo API removed."
|
||||||
|
echo "User 'stegasoo' and config in /opt/stegasoo-api were not removed."
|
||||||
|
echo "To remove: userdel stegasoo && rm -rf /opt/stegasoo-api"
|
||||||
|
}
|
||||||
22
aur-api/test-build.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test build the AUR API package locally
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "=== Cleaning previous builds ==="
|
||||||
|
rm -rf stegasoo-api-git pkg src *.pkg.tar.zst *.whl 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "=== Generating .SRCINFO ==="
|
||||||
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
|
|
||||||
|
echo "=== Building package ==="
|
||||||
|
makepkg -sf
|
||||||
|
|
||||||
|
echo "=== Package built ==="
|
||||||
|
ls -la *.pkg.tar.zst
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "To install: sudo pacman -U stegasoo-api-git-*.pkg.tar.zst"
|
||||||
|
echo "To test: makepkg -si"
|
||||||
22
aur-cli/.SRCINFO
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
pkgbase = stegasoo-cli-git
|
||||||
|
pkgdesc = Secure steganography CLI with hybrid photo + passphrase + PIN authentication
|
||||||
|
pkgver = 4.2.1
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://github.com/adlee-was-taken/stegasoo
|
||||||
|
install = stegasoo-cli-git.install
|
||||||
|
arch = x86_64
|
||||||
|
license = MIT
|
||||||
|
makedepends = git
|
||||||
|
makedepends = python
|
||||||
|
makedepends = python-build
|
||||||
|
makedepends = python-hatchling
|
||||||
|
depends = python>=3.11
|
||||||
|
optdepends = libjpeg-turbo: jpegtran for lossless JPEG rotation (DCT-safe)
|
||||||
|
provides = stegasoo-cli
|
||||||
|
conflicts = stegasoo-cli
|
||||||
|
conflicts = stegasoo
|
||||||
|
conflicts = stegasoo-git
|
||||||
|
source = stegasoo-cli-git::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main
|
||||||
|
sha256sums = SKIP
|
||||||
|
|
||||||
|
pkgname = stegasoo-cli-git
|
||||||
69
aur-cli/PKGBUILD
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Maintainer: Aaron D. Lee <your-email@example.com>
|
||||||
|
pkgname=stegasoo-cli-git
|
||||||
|
pkgver=4.2.1
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Secure steganography CLI with hybrid photo + passphrase + PIN authentication"
|
||||||
|
arch=('x86_64')
|
||||||
|
url="https://github.com/adlee-was-taken/stegasoo"
|
||||||
|
license=('MIT')
|
||||||
|
|
||||||
|
# Python 3.11-3.14 supported (uses jpeglib for modern Python compatibility)
|
||||||
|
depends=(
|
||||||
|
'python>=3.11'
|
||||||
|
)
|
||||||
|
makedepends=(
|
||||||
|
'git'
|
||||||
|
'python'
|
||||||
|
'python-build'
|
||||||
|
'python-hatchling'
|
||||||
|
)
|
||||||
|
optdepends=(
|
||||||
|
'libjpeg-turbo: jpegtran for lossless JPEG rotation (DCT-safe)'
|
||||||
|
)
|
||||||
|
provides=('stegasoo-cli')
|
||||||
|
conflicts=('stegasoo-cli' 'stegasoo' 'stegasoo-git')
|
||||||
|
install=stegasoo-cli-git.install
|
||||||
|
source=("${pkgname}::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main")
|
||||||
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
|
pkgver() {
|
||||||
|
cd "$pkgname"
|
||||||
|
git describe --long --tags 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || \
|
||||||
|
printf "%s.r%s.g%s" "4.2.1" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$pkgname"
|
||||||
|
python -m build --wheel --no-isolation
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$pkgname"
|
||||||
|
|
||||||
|
# Install to /opt/stegasoo-cli with dedicated venv
|
||||||
|
install -dm755 "$pkgdir/opt/stegasoo-cli"
|
||||||
|
|
||||||
|
# Create fresh venv in package
|
||||||
|
python -m venv "$pkgdir/opt/stegasoo-cli/venv"
|
||||||
|
|
||||||
|
# Install the wheel with CLI + DCT + compression extras (no web/api)
|
||||||
|
local wheel=$(ls dist/*.whl | head -1)
|
||||||
|
"$pkgdir/opt/stegasoo-cli/venv/bin/pip" install --no-cache-dir "${wheel}[cli,dct,compression]"
|
||||||
|
|
||||||
|
# Fix shebangs - replace build-time paths with installed paths
|
||||||
|
find "$pkgdir/opt/stegasoo-cli/venv/bin" -type f -exec \
|
||||||
|
sed -i "s|$pkgdir/opt/stegasoo-cli/venv|/opt/stegasoo-cli/venv|g" {} \;
|
||||||
|
|
||||||
|
# Fix pyvenv.cfg
|
||||||
|
sed -i "s|$pkgdir||g" "$pkgdir/opt/stegasoo-cli/venv/pyvenv.cfg"
|
||||||
|
|
||||||
|
# Create symlink to /usr/bin
|
||||||
|
install -dm755 "$pkgdir/usr/bin"
|
||||||
|
ln -s /opt/stegasoo-cli/venv/bin/stegasoo "$pkgdir/usr/bin/stegasoo"
|
||||||
|
|
||||||
|
# Install license
|
||||||
|
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
|
||||||
|
# Install docs
|
||||||
|
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||||
|
}
|
||||||
62
aur-cli/README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Stegasoo CLI AUR Package
|
||||||
|
|
||||||
|
Lightweight CLI-only package for steganography operations. No web UI or API server.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From AUR (once published)
|
||||||
|
```bash
|
||||||
|
yay -S stegasoo-cli-git
|
||||||
|
# or
|
||||||
|
paru -S stegasoo-cli-git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual build
|
||||||
|
```bash
|
||||||
|
git clone https://aur.archlinux.org/stegasoo-cli-git.git
|
||||||
|
cd stegasoo-cli-git
|
||||||
|
makepkg -si
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Gets Installed
|
||||||
|
|
||||||
|
- `/opt/stegasoo-cli/venv/` - Self-contained Python venv with CLI dependencies only
|
||||||
|
- `/usr/bin/stegasoo` - CLI executable
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show all commands
|
||||||
|
stegasoo --help
|
||||||
|
|
||||||
|
# Generate credentials (passphrase + PIN)
|
||||||
|
stegasoo generate
|
||||||
|
stegasoo generate --words 5 --pin-length 8
|
||||||
|
|
||||||
|
# Generate with RSA keys and QR codes
|
||||||
|
stegasoo generate --rsa --qr-ascii
|
||||||
|
|
||||||
|
# Encode a message
|
||||||
|
stegasoo encode -i carrier.jpg -r reference.jpg -m "secret message" \
|
||||||
|
-P "word1 word2 word3 word4" -p 123456
|
||||||
|
|
||||||
|
# Decode a message
|
||||||
|
stegasoo decode -i encoded.png -r reference.jpg \
|
||||||
|
-P "word1 word2 word3 word4" -p 123456
|
||||||
|
|
||||||
|
# Image tools
|
||||||
|
stegasoo tools --help
|
||||||
|
stegasoo tools compress image.png
|
||||||
|
stegasoo tools rotate image.jpg 90
|
||||||
|
```
|
||||||
|
|
||||||
|
## For Web UI or REST API
|
||||||
|
|
||||||
|
Install the full package instead:
|
||||||
|
```bash
|
||||||
|
yay -S stegasoo-git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintainer
|
||||||
|
|
||||||
|
Aaron D. Lee
|
||||||
20
aur-cli/stegasoo-cli-git.install
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
post_install() {
|
||||||
|
echo ""
|
||||||
|
echo "==============================================="
|
||||||
|
echo " Stegasoo CLI installed successfully!"
|
||||||
|
echo "==============================================="
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
echo " stegasoo --help # Show all commands"
|
||||||
|
echo " stegasoo generate # Generate passphrase + PIN"
|
||||||
|
echo " stegasoo encode ... # Hide data in an image"
|
||||||
|
echo " stegasoo decode ... # Extract hidden data"
|
||||||
|
echo " stegasoo tools --help # Image tools (compress, etc.)"
|
||||||
|
echo ""
|
||||||
|
echo "For web UI or REST API, install stegasoo-git instead."
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
post_upgrade() {
|
||||||
|
post_install
|
||||||
|
}
|
||||||
22
aur-cli/test-build.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test build the AUR CLI package locally
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "=== Cleaning previous builds ==="
|
||||||
|
rm -rf stegasoo-cli-git pkg src *.pkg.tar.zst *.whl 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "=== Generating .SRCINFO ==="
|
||||||
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
|
|
||||||
|
echo "=== Building package ==="
|
||||||
|
makepkg -sf
|
||||||
|
|
||||||
|
echo "=== Package built ==="
|
||||||
|
ls -la *.pkg.tar.zst
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "To install: sudo pacman -U stegasoo-cli-git-*.pkg.tar.zst"
|
||||||
|
echo "To test: makepkg -si"
|
||||||
120
aur/PKGBUILD
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Maintainer: Aaron D. Lee <your-email@example.com>
|
||||||
|
pkgname=stegasoo-git
|
||||||
|
pkgver=4.2.1
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Secure steganography with hybrid photo + passphrase + PIN authentication"
|
||||||
|
arch=('x86_64')
|
||||||
|
url="https://github.com/adlee-was-taken/stegasoo"
|
||||||
|
license=('MIT')
|
||||||
|
|
||||||
|
# Python 3.11-3.14 supported (uses jpeglib for modern Python compatibility)
|
||||||
|
depends=(
|
||||||
|
'python>=3.11'
|
||||||
|
'zbar' # QR code reading for Web UI
|
||||||
|
)
|
||||||
|
makedepends=(
|
||||||
|
'git'
|
||||||
|
'python'
|
||||||
|
'python-build'
|
||||||
|
'python-hatchling'
|
||||||
|
)
|
||||||
|
provides=('stegasoo')
|
||||||
|
conflicts=('stegasoo')
|
||||||
|
install=stegasoo-git.install
|
||||||
|
source=("${pkgname}::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main")
|
||||||
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
|
pkgver() {
|
||||||
|
cd "$pkgname"
|
||||||
|
git describe --long --tags 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || \
|
||||||
|
printf "%s.r%s.g%s" "4.2.1" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$pkgname"
|
||||||
|
python -m build --wheel --no-isolation
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$pkgname"
|
||||||
|
|
||||||
|
# Detect Python version for site-packages path
|
||||||
|
local pyver=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||||
|
|
||||||
|
# Install to /opt/stegasoo with dedicated venv
|
||||||
|
install -dm755 "$pkgdir/opt/stegasoo"
|
||||||
|
|
||||||
|
# Create fresh venv in package
|
||||||
|
python -m venv "$pkgdir/opt/stegasoo/venv"
|
||||||
|
|
||||||
|
# Install the wheel with all extras
|
||||||
|
local wheel=$(ls dist/*.whl | head -1)
|
||||||
|
"$pkgdir/opt/stegasoo/venv/bin/pip" install --no-cache-dir "${wheel}[all]"
|
||||||
|
|
||||||
|
# Install frontends (not included in wheel)
|
||||||
|
local site_packages="$pkgdir/opt/stegasoo/venv/lib/python${pyver}/site-packages"
|
||||||
|
cp -r frontends "$site_packages/"
|
||||||
|
|
||||||
|
# Create writable directories for stegasoo user
|
||||||
|
install -dm755 "$pkgdir/opt/stegasoo/venv/var/app-instance"
|
||||||
|
install -dm755 "$site_packages/frontends/web/temp_files"
|
||||||
|
install -dm755 "$site_packages/frontends/api/temp_files"
|
||||||
|
|
||||||
|
# Fix shebangs - replace build-time paths with installed paths
|
||||||
|
find "$pkgdir/opt/stegasoo/venv/bin" -type f -exec \
|
||||||
|
sed -i "s|$pkgdir/opt/stegasoo/venv|/opt/stegasoo/venv|g" {} \;
|
||||||
|
|
||||||
|
# Fix pyvenv.cfg
|
||||||
|
sed -i "s|$pkgdir||g" "$pkgdir/opt/stegasoo/venv/pyvenv.cfg"
|
||||||
|
|
||||||
|
# Create symlinks to /usr/bin
|
||||||
|
install -dm755 "$pkgdir/usr/bin"
|
||||||
|
ln -s /opt/stegasoo/venv/bin/stegasoo "$pkgdir/usr/bin/stegasoo"
|
||||||
|
|
||||||
|
# Install license
|
||||||
|
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
|
||||||
|
# Install docs
|
||||||
|
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||||
|
|
||||||
|
# Install systemd service files
|
||||||
|
install -Dm644 /dev/stdin "$pkgdir/usr/lib/systemd/system/stegasoo-web.service" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Stegasoo Web UI
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=stegasoo
|
||||||
|
WorkingDirectory=/opt/stegasoo/venv/lib/python${pyver}/site-packages/frontends/web
|
||||||
|
Environment="PATH=/opt/stegasoo/venv/bin"
|
||||||
|
ExecStart=/opt/stegasoo/venv/bin/gunicorn -b 127.0.0.1:5000 app:app
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
install -Dm644 /dev/stdin "$pkgdir/usr/lib/systemd/system/stegasoo-api.service" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Stegasoo REST API (HTTPS)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=stegasoo
|
||||||
|
WorkingDirectory=/opt/stegasoo/venv/lib/python${pyver}/site-packages/frontends/api
|
||||||
|
Environment="PATH=/opt/stegasoo/venv/bin"
|
||||||
|
Environment="HOME=/opt/stegasoo"
|
||||||
|
# TLS enabled by default - certs auto-generated on first run
|
||||||
|
# Use stegasoo api tls generate to pre-generate certs
|
||||||
|
# Use stegasoo api keys create <name> to create API keys
|
||||||
|
ExecStart=/opt/stegasoo/venv/bin/stegasoo api serve --host 127.0.0.1 --port 8000
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
}
|
||||||
90
aur/README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Stegasoo AUR Package
|
||||||
|
|
||||||
|
Full package with CLI, Web UI, and REST API. Supports Python 3.11-3.14.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From AUR (once published)
|
||||||
|
```bash
|
||||||
|
yay -S stegasoo-git
|
||||||
|
# or
|
||||||
|
paru -S stegasoo-git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual build
|
||||||
|
```bash
|
||||||
|
git clone https://aur.archlinux.org/stegasoo-git.git
|
||||||
|
cd stegasoo-git
|
||||||
|
makepkg -si
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Gets Installed
|
||||||
|
|
||||||
|
- `/opt/stegasoo/venv/` - Self-contained Python venv with all dependencies
|
||||||
|
- `/usr/bin/stegasoo` - CLI symlink
|
||||||
|
- `/usr/lib/systemd/system/stegasoo-web.service` - Web UI service (port 5000)
|
||||||
|
- `/usr/lib/systemd/system/stegasoo-api.service` - REST API service (port 8000, HTTPS)
|
||||||
|
|
||||||
|
## Optional Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# QR code reading from webcam/images (recommended)
|
||||||
|
sudo pacman -S zbar
|
||||||
|
```
|
||||||
|
|
||||||
|
All other dependencies are bundled in the venv.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
```bash
|
||||||
|
stegasoo --help
|
||||||
|
stegasoo generate # Generate passphrase + PIN
|
||||||
|
stegasoo generate --rsa --qr-ascii # With RSA keys and QR codes
|
||||||
|
stegasoo encode -i carrier.jpg -r reference.jpg -m "secret" -P "word1 word2 word3 word4" -p 123456
|
||||||
|
stegasoo decode -i encoded.png -r reference.jpg -P "word1 word2 word3 word4" -p 123456
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web UI
|
||||||
|
```bash
|
||||||
|
# Start service (user created automatically on install)
|
||||||
|
sudo systemctl enable --now stegasoo-web
|
||||||
|
|
||||||
|
# Access at http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
```bash
|
||||||
|
# Create an API key first
|
||||||
|
sudo -u stegasoo stegasoo api keys create mykey
|
||||||
|
|
||||||
|
# Start service (HTTPS with auto-generated self-signed cert)
|
||||||
|
sudo systemctl enable --now stegasoo-api
|
||||||
|
|
||||||
|
# Access docs at https://localhost:8000/docs
|
||||||
|
curl -k -H "X-API-Key: YOUR_KEY" https://localhost:8000/
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTPS Configuration
|
||||||
|
|
||||||
|
The API uses HTTPS by default with auto-generated self-signed certificates.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View certificate info
|
||||||
|
stegasoo api tls info
|
||||||
|
|
||||||
|
# Generate new self-signed cert
|
||||||
|
sudo -u stegasoo stegasoo api tls generate
|
||||||
|
|
||||||
|
# Use custom certs (edit service file)
|
||||||
|
sudo systemctl edit stegasoo-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative Packages
|
||||||
|
|
||||||
|
- `stegasoo-cli-git` - CLI only, minimal dependencies
|
||||||
|
- `stegasoo-api-git` - CLI + REST API, no web UI
|
||||||
|
|
||||||
|
## Maintainer
|
||||||
|
|
||||||
|
Aaron D. Lee
|
||||||
75
aur/stegasoo-git.install
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
post_install() {
|
||||||
|
# Create stegasoo system user if it doesn't exist
|
||||||
|
if ! getent passwd stegasoo >/dev/null; then
|
||||||
|
useradd -r -s /usr/bin/nologin -d /opt/stegasoo stegasoo
|
||||||
|
echo "Created system user 'stegasoo'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set ownership of instance directory for Flask
|
||||||
|
chown -R stegasoo:stegasoo /opt/stegasoo/venv/var/app-instance 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==============================================="
|
||||||
|
echo " Stegasoo installed successfully!"
|
||||||
|
echo "==============================================="
|
||||||
|
echo ""
|
||||||
|
echo "CLI usage:"
|
||||||
|
echo " stegasoo --help"
|
||||||
|
echo " stegasoo generate # Generate credentials"
|
||||||
|
echo " stegasoo encode # Encode a message"
|
||||||
|
echo " stegasoo decode # Decode a message"
|
||||||
|
echo ""
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " Web UI Service"
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " Port: 5000 (HTTP)"
|
||||||
|
echo " Start: sudo systemctl start stegasoo-web"
|
||||||
|
echo " Enable: sudo systemctl enable stegasoo-web"
|
||||||
|
echo " Status: sudo systemctl status stegasoo-web"
|
||||||
|
echo " Access: http://localhost:5000"
|
||||||
|
echo ""
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " REST API Service"
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " Port: 8000 (HTTPS by default)"
|
||||||
|
echo " Start: sudo systemctl start stegasoo-api"
|
||||||
|
echo " Enable: sudo systemctl enable stegasoo-api"
|
||||||
|
echo " Status: sudo systemctl status stegasoo-api"
|
||||||
|
echo " Access: https://localhost:8000"
|
||||||
|
echo ""
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " HTTPS Configuration"
|
||||||
|
echo "-----------------------------------------------"
|
||||||
|
echo " The API generates self-signed certs on first run."
|
||||||
|
echo " To pre-generate or use custom certificates:"
|
||||||
|
echo ""
|
||||||
|
echo " # Generate self-signed certs"
|
||||||
|
echo " sudo -u stegasoo stegasoo api tls generate"
|
||||||
|
echo ""
|
||||||
|
echo " # Use custom certs (edit the service file)"
|
||||||
|
echo " sudo systemctl edit stegasoo-api"
|
||||||
|
echo " # Add: ExecStart= with --cert and --key flags"
|
||||||
|
echo ""
|
||||||
|
echo " # Create API keys for authentication"
|
||||||
|
echo " sudo -u stegasoo stegasoo api keys create <name>"
|
||||||
|
echo ""
|
||||||
|
echo "==============================================="
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
post_upgrade() {
|
||||||
|
post_install
|
||||||
|
}
|
||||||
|
|
||||||
|
pre_remove() {
|
||||||
|
# Stop services if running
|
||||||
|
systemctl stop stegasoo-web 2>/dev/null || true
|
||||||
|
systemctl stop stegasoo-api 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
post_remove() {
|
||||||
|
# Optionally remove the stegasoo user
|
||||||
|
# userdel stegasoo 2>/dev/null || true
|
||||||
|
echo "Stegasoo removed. User 'stegasoo' was not removed."
|
||||||
|
echo "To remove: userdel stegasoo"
|
||||||
|
}
|
||||||
22
aur/test-build.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test build the AUR package locally
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "=== Cleaning previous builds ==="
|
||||||
|
rm -rf stegasoo-git pkg src *.pkg.tar.zst *.whl 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "=== Generating .SRCINFO ==="
|
||||||
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
|
|
||||||
|
echo "=== Building package ==="
|
||||||
|
makepkg -sf
|
||||||
|
|
||||||
|
echo "=== Package built ==="
|
||||||
|
ls -la *.pkg.tar.zst
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "To install: sudo pacman -U stegasoo-git-*.pkg.tar.zst"
|
||||||
|
echo "To test: makepkg -si"
|
||||||
BIN
data/WebUI.png
|
Before Width: | Height: | Size: 324 KiB |
BIN
data/WebUI.webp
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 17 KiB |
BIN
data/WebUI_About.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
data/WebUI_Account.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 16 KiB |
BIN
data/WebUI_Login.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
data/WebUI_Recover.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
data/WebUI_Setup.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
data/WebUI_Tools.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -1,62 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# ============================================================================
|
|
||||||
# Web UI (Flask)
|
|
||||||
# ============================================================================
|
|
||||||
web:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
target: web
|
|
||||||
container_name: stegasoo-web
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
environment:
|
|
||||||
- FLASK_ENV=production
|
|
||||||
restart: unless-stopped
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 512M # Argon2 needs 256MB per operation
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# REST API (FastAPI)
|
|
||||||
# ============================================================================
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
target: api
|
|
||||||
container_name: stegasoo-api
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
restart: unless-stopped
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 512M
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Nginx Reverse Proxy (optional, for production)
|
|
||||||
# ============================================================================
|
|
||||||
# nginx:
|
|
||||||
# image: nginx:alpine
|
|
||||||
# container_name: stegasoo-nginx
|
|
||||||
# ports:
|
|
||||||
# - "80:80"
|
|
||||||
# - "443:443"
|
|
||||||
# volumes:
|
|
||||||
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
# - ./certs:/etc/nginx/certs:ro
|
|
||||||
# depends_on:
|
|
||||||
# - web
|
|
||||||
# - api
|
|
||||||
# restart: unless-stopped
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Development overrides
|
|
||||||
# ============================================================================
|
|
||||||
# Use: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
|
|
||||||
152
docker/Dockerfile
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# Stegasoo Docker Image
|
||||||
|
# Uses pre-built base image for fast rebuilds
|
||||||
|
#
|
||||||
|
# First time setup:
|
||||||
|
# docker build -f Dockerfile.base -t stegasoo-base:latest .
|
||||||
|
#
|
||||||
|
# Then build normally (fast!):
|
||||||
|
# docker-compose build
|
||||||
|
#
|
||||||
|
# Or if you don't have the base image, this falls back to building deps
|
||||||
|
# (slow, but works)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ARG to switch between base image and full build
|
||||||
|
# ============================================================================
|
||||||
|
ARG USE_BASE_IMAGE=true
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Base stage - use pre-built image if available
|
||||||
|
# ============================================================================
|
||||||
|
FROM stegasoo-base:latest AS base-prebuilt
|
||||||
|
|
||||||
|
FROM python:3.12-slim AS base-full
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
libc-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libzbar0 \
|
||||||
|
libjpeg-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
curl \
|
||||||
|
openssl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install ALL dependencies (slow path)
|
||||||
|
RUN pip install --no-cache-dir \
|
||||||
|
cython numpy scipy>=1.10.0 jpegio>=0.2.0 \
|
||||||
|
argon2-cffi>=23.0.0 pillow>=10.0.0 cryptography>=41.0.0 \
|
||||||
|
reedsolo>=1.7.0 \
|
||||||
|
flask>=3.0.0 gunicorn>=21.0.0 \
|
||||||
|
fastapi>=0.100.0 "uvicorn[standard]>=0.20.0" python-multipart>=0.0.6 \
|
||||||
|
qrcode>=7.3.0 pyzbar>=0.1.9 click>=8.0.0 lz4>=4.0.0
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Select which base to use (default: prebuilt)
|
||||||
|
# ============================================================================
|
||||||
|
FROM base-prebuilt AS base
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Production stage - Web UI
|
||||||
|
# ============================================================================
|
||||||
|
FROM base AS web
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies (curl for healthcheck, openssl for cert generation)
|
||||||
|
USER root
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl openssl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy application files (this is all that rebuilds normally!)
|
||||||
|
COPY src/ src/
|
||||||
|
COPY data/ data/
|
||||||
|
COPY frontends/web/ frontends/web/
|
||||||
|
|
||||||
|
# Create upload directory and instance directories (for volumes)
|
||||||
|
# temp_files is for multi-worker temp file sharing
|
||||||
|
RUN mkdir -p /tmp/stego_uploads /app/frontends/web/instance /app/frontends/web/certs /app/frontends/web/temp_files
|
||||||
|
|
||||||
|
# Copy and set up entrypoint (before switching to non-root user)
|
||||||
|
COPY frontends/web/docker-entrypoint.sh /app/frontends/web/
|
||||||
|
RUN chmod +x /app/frontends/web/docker-entrypoint.sh
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd -m -u 1000 stego && chown -R stego:stego /app /tmp/stego_uploads
|
||||||
|
USER stego
|
||||||
|
|
||||||
|
# Set Python path
|
||||||
|
ENV PYTHONPATH=/app/src
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -fsk https://localhost:5000/ || curl -fs http://localhost:5000/ || exit 1
|
||||||
|
|
||||||
|
# Run with entrypoint (handles HTTPS/HTTP mode)
|
||||||
|
WORKDIR /app/frontends/web
|
||||||
|
ENTRYPOINT ["/app/frontends/web/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# API stage - REST API
|
||||||
|
# ============================================================================
|
||||||
|
FROM base AS api
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY src/ src/
|
||||||
|
COPY data/ data/
|
||||||
|
COPY frontends/api/ frontends/api/
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd -m -u 1000 stego && chown -R stego:stego /app
|
||||||
|
USER stego
|
||||||
|
|
||||||
|
# Set Python path
|
||||||
|
ENV PYTHONPATH=/app/src
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/')" || exit 1
|
||||||
|
|
||||||
|
# Run with uvicorn
|
||||||
|
WORKDIR /app/frontends/api
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CLI stage - Command line tool
|
||||||
|
# ============================================================================
|
||||||
|
FROM base AS cli
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY src/ src/
|
||||||
|
COPY data/ data/
|
||||||
|
COPY frontends/cli/ frontends/cli/
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd -m -u 1000 stego && chown -R stego:stego /app
|
||||||
|
USER stego
|
||||||
|
|
||||||
|
# Set Python path
|
||||||
|
ENV PYTHONPATH=/app/src
|
||||||
|
|
||||||
|
# Default to help
|
||||||
|
WORKDIR /app/frontends/cli
|
||||||
|
ENTRYPOINT ["python", "main.py"]
|
||||||
|
CMD ["--help"]
|
||||||
57
docker/Dockerfile.base
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Stegasoo Base Image
|
||||||
|
# Contains all slow-to-compile dependencies (jpegio, scipy, argon2)
|
||||||
|
# Build once: docker build -f Dockerfile.base -t stegasoo-base:latest .
|
||||||
|
# Push to registry for team use: docker push yourregistry/stegasoo-base:latest
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
# NOTE: g++ is required for jpegio C++ compilation
|
||||||
|
# NOTE: libjpeg-dev is required for jpegio
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
libc-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libzbar0 \
|
||||||
|
libjpeg-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install the slow-to-compile packages
|
||||||
|
# These rarely change, so they get cached in this base image
|
||||||
|
RUN pip install --no-cache-dir \
|
||||||
|
cython \
|
||||||
|
numpy \
|
||||||
|
scipy>=1.10.0 \
|
||||||
|
jpegio>=0.2.0 \
|
||||||
|
argon2-cffi>=23.0.0 \
|
||||||
|
pillow>=10.0.0 \
|
||||||
|
cryptography>=41.0.0 \
|
||||||
|
reedsolo>=1.7.0 \
|
||||||
|
zstandard>=0.22.0
|
||||||
|
|
||||||
|
# Install web/api framework packages (also stable)
|
||||||
|
RUN pip install --no-cache-dir \
|
||||||
|
flask>=3.0.0 \
|
||||||
|
gunicorn>=21.0.0 \
|
||||||
|
fastapi>=0.100.0 \
|
||||||
|
"uvicorn[standard]>=0.20.0" \
|
||||||
|
python-multipart>=0.0.6 \
|
||||||
|
qrcode>=7.3.0 \
|
||||||
|
pyzbar>=0.1.9 \
|
||||||
|
click>=8.0.0 \
|
||||||
|
lz4>=4.0.0
|
||||||
|
|
||||||
|
# Verify key packages work
|
||||||
|
RUN python -c "import jpegio; import scipy; import numpy; import zstandard; print('jpegio + scipy + numpy + zstd OK')"
|
||||||
|
|
||||||
|
# Label for tracking
|
||||||
|
LABEL org.opencontainers.image.title="Stegasoo Base"
|
||||||
|
LABEL org.opencontainers.image.description="Pre-compiled dependencies for Stegasoo"
|
||||||
|
LABEL org.opencontainers.image.version="4.2.1"
|
||||||
64
docker/docker-compose.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Shared environment variables
|
||||||
|
x-common-env: &common-env
|
||||||
|
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ============================================================================
|
||||||
|
# Web UI (Flask)
|
||||||
|
# ============================================================================
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
target: web
|
||||||
|
container_name: stegasoo-web
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
FLASK_ENV: production
|
||||||
|
# Authentication (v4.0.2)
|
||||||
|
STEGASOO_AUTH_ENABLED: ${STEGASOO_AUTH_ENABLED:-true}
|
||||||
|
# HTTPS enabled by default - generates self-signed cert if none provided
|
||||||
|
# To disable: STEGASOO_HTTPS_ENABLED=false docker-compose up
|
||||||
|
STEGASOO_HTTPS_ENABLED: ${STEGASOO_HTTPS_ENABLED:-true}
|
||||||
|
STEGASOO_HOSTNAME: ${STEGASOO_HOSTNAME:-localhost}
|
||||||
|
volumes:
|
||||||
|
# Persist auth database and SSL certs (v4.0.2)
|
||||||
|
- stegasoo-web-data:/app/frontends/web/instance
|
||||||
|
- stegasoo-web-certs:/app/frontends/web/certs
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2048M
|
||||||
|
reservations:
|
||||||
|
memory: 1024M
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# REST API (FastAPI)
|
||||||
|
# ============================================================================
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
target: api
|
||||||
|
container_name: stegasoo-api
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2048M
|
||||||
|
reservations:
|
||||||
|
memory: 1024M
|
||||||
|
|
||||||
|
# Named volumes for persistent data
|
||||||
|
volumes:
|
||||||
|
stegasoo-web-data:
|
||||||
|
driver: local
|
||||||
|
stegasoo-web-certs:
|
||||||
|
driver: local
|
||||||
162
docs/DOCKER_QUICKSTART.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Docker Quickstart
|
||||||
|
|
||||||
|
Get Stegasoo running in Docker in under 5 minutes.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From project root:
|
||||||
|
|
||||||
|
# Build web UI image
|
||||||
|
sudo docker build -t stegasoo-web --target web -f docker/Dockerfile .
|
||||||
|
|
||||||
|
# Or build all targets
|
||||||
|
sudo docker build -t stegasoo-api --target api -f docker/Dockerfile .
|
||||||
|
sudo docker build -t stegasoo-cli --target cli -f docker/Dockerfile .
|
||||||
|
|
||||||
|
# Or use docker-compose
|
||||||
|
sudo docker-compose -f docker/docker-compose.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run (Basic)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTP only, no auth
|
||||||
|
sudo docker run -d \
|
||||||
|
-p 5000:5000 \
|
||||||
|
-e STEGASOO_AUTH_ENABLED=false \
|
||||||
|
--name stegasoo \
|
||||||
|
stegasoo-web
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit http://localhost:5000
|
||||||
|
|
||||||
|
## Run (Production)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTPS + Auth + Channel Key
|
||||||
|
sudo docker run -d \
|
||||||
|
-p 5000:5000 \
|
||||||
|
-e STEGASOO_AUTH_ENABLED=true \
|
||||||
|
-e STEGASOO_HTTPS_ENABLED=true \
|
||||||
|
-e STEGASOO_HOSTNAME=stegasoo.local \
|
||||||
|
-e STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 \
|
||||||
|
-v stegasoo-data:/opt/stegasoo/frontends/web/instance \
|
||||||
|
-v stegasoo-certs:/opt/stegasoo/frontends/web/certs \
|
||||||
|
--name stegasoo \
|
||||||
|
stegasoo-web
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit https://localhost:5000 (accept self-signed cert warning)
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `STEGASOO_AUTH_ENABLED` | `true` | Require login |
|
||||||
|
| `STEGASOO_HTTPS_ENABLED` | `false` | Enable HTTPS |
|
||||||
|
| `STEGASOO_HOSTNAME` | `localhost` | Hostname for SSL cert |
|
||||||
|
| `STEGASOO_CHANNEL_KEY` | *(none)* | Shared channel key (32 alphanumeric chars with dashes) |
|
||||||
|
|
||||||
|
## Docker Compose
|
||||||
|
|
||||||
|
Create `.env` file in project root:
|
||||||
|
```bash
|
||||||
|
STEGASOO_AUTH_ENABLED=true
|
||||||
|
STEGASOO_HTTPS_ENABLED=true
|
||||||
|
STEGASOO_HOSTNAME=stegasoo.local
|
||||||
|
STEGASOO_CHANNEL_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
sudo docker-compose -f docker/docker-compose.yml up -d web
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom SSL Certificates
|
||||||
|
|
||||||
|
### Use Your Own Certs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop container
|
||||||
|
sudo docker stop stegasoo
|
||||||
|
|
||||||
|
# Copy certs to volume
|
||||||
|
sudo docker run --rm -v stegasoo-certs:/certs -v $(pwd):/src alpine \
|
||||||
|
sh -c "cp /src/your-cert.crt /certs/server.crt && cp /src/your-key.key /certs/server.key && chmod 600 /certs/server.key"
|
||||||
|
|
||||||
|
# Start container
|
||||||
|
sudo docker start stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use mkcert (Local Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install mkcert
|
||||||
|
brew install mkcert # macOS
|
||||||
|
# or: sudo apt install mkcert # Debian/Ubuntu
|
||||||
|
|
||||||
|
# Create local CA and certs
|
||||||
|
mkcert -install
|
||||||
|
mkcert -cert-file server.crt -key-file server.key localhost 127.0.0.1 stegasoo.local
|
||||||
|
|
||||||
|
# Copy to Docker volume (see above)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Let's Encrypt (Public Server)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get cert
|
||||||
|
sudo certbot certonly --standalone -d yourdomain.com
|
||||||
|
|
||||||
|
# Copy to Docker volume
|
||||||
|
sudo docker run --rm -v stegasoo-certs:/certs alpine \
|
||||||
|
sh -c "cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /certs/server.crt && \
|
||||||
|
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /certs/server.key && \
|
||||||
|
chmod 600 /certs/server.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
| Volume | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `stegasoo-data` | User database, settings |
|
||||||
|
| `stegasoo-certs` | SSL certificates |
|
||||||
|
|
||||||
|
## Smoke Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container logs
|
||||||
|
sudo docker logs stegasoo
|
||||||
|
|
||||||
|
# Test HTTP endpoint
|
||||||
|
curl -k https://localhost:5000/health
|
||||||
|
|
||||||
|
# Expected: {"status":"ok","version":"4.1.7",...}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Container won't start:**
|
||||||
|
```bash
|
||||||
|
sudo docker logs stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Out of memory:**
|
||||||
|
```bash
|
||||||
|
# Argon2 needs 256MB+ per operation
|
||||||
|
sudo docker run --memory=768m ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Certificate errors:**
|
||||||
|
```bash
|
||||||
|
# Regenerate self-signed cert
|
||||||
|
sudo docker exec stegasoo rm -rf /opt/stegasoo/frontends/web/certs/*
|
||||||
|
sudo docker restart stegasoo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reset everything:**
|
||||||
|
```bash
|
||||||
|
sudo docker stop stegasoo && sudo docker rm stegasoo
|
||||||
|
sudo docker volume rm stegasoo-data stegasoo-certs
|
||||||
|
```
|
||||||
361
docs/TEMPLATES.md
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
# Stegasoo Web Templates Specification
|
||||||
|
|
||||||
|
Quick reference for all Jinja2 templates in `frontends/web/templates/`.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Layout](#layout)
|
||||||
|
- [Auth & Setup](#auth--setup)
|
||||||
|
- [Core Features](#core-features)
|
||||||
|
- [Tools & Account](#tools--account)
|
||||||
|
- [Admin](#admin)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
### `base.html`
|
||||||
|
**Purpose:** Master layout template - all pages extend this.
|
||||||
|
|
||||||
|
| Block | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `{% block title %}` | Page title |
|
||||||
|
| `{% block content %}` | Main page content |
|
||||||
|
| `{% block scripts %}` | Page-specific JS |
|
||||||
|
|
||||||
|
**Key Elements:**
|
||||||
|
- `nav.navbar` - Bootstrap 5 navbar with logo, links, auth buttons
|
||||||
|
- `div.toast-container` - Flash message toasts (10s auto-dismiss)
|
||||||
|
- `main.container` - Content wrapper
|
||||||
|
- `footer` - Copyright + version
|
||||||
|
|
||||||
|
**Variables:** `is_authenticated`, `username`, `is_admin`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth & Setup
|
||||||
|
|
||||||
|
### `login.html`
|
||||||
|
**Route:** `/login`
|
||||||
|
|
||||||
|
**Form:** `POST /login`
|
||||||
|
- `username` - text input
|
||||||
|
- `password` - password input
|
||||||
|
- "Forgot password?" link to `/recover`
|
||||||
|
|
||||||
|
**JS:** `static/js/auth.js` - password toggle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `setup.html`
|
||||||
|
**Route:** `/setup` (first-run only)
|
||||||
|
|
||||||
|
**Form:** `POST /setup`
|
||||||
|
- `username` - admin username
|
||||||
|
- `password` - password (min 8 chars)
|
||||||
|
- `password_confirm` - confirmation
|
||||||
|
|
||||||
|
**JS:** Password confirmation validation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `setup_recovery.html`
|
||||||
|
**Route:** `/setup/recovery`
|
||||||
|
|
||||||
|
**Form:** `POST /setup/recovery`
|
||||||
|
- `recovery_key` - hidden, pre-generated
|
||||||
|
- `action` - "save" or "skip"
|
||||||
|
- Checkbox confirmation required for save
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Recovery key display (readonly input)
|
||||||
|
- Copy to clipboard button
|
||||||
|
- QR code image (if available)
|
||||||
|
- Download options: text file, QR image
|
||||||
|
- Stego backup upload form
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `recover.html`
|
||||||
|
**Route:** `/recover`
|
||||||
|
|
||||||
|
**Form:** `POST /recover`
|
||||||
|
- `recovery_key` - textarea for key input
|
||||||
|
- `new_password` - new password
|
||||||
|
- `new_password_confirm` - confirmation
|
||||||
|
|
||||||
|
**Accordion:** "Extract from stego backup"
|
||||||
|
- `POST /recover/stego` with `stego_image` + `reference_image`
|
||||||
|
- Pre-fills recovery key on success
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `regenerate_recovery.html`
|
||||||
|
**Route:** `/account/recovery/regenerate` (admin only)
|
||||||
|
|
||||||
|
**Form:** `POST /account/recovery/regenerate`
|
||||||
|
- `recovery_key` - hidden field
|
||||||
|
- `action` - "save" or "cancel"
|
||||||
|
- Confirmation checkbox
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- New key display
|
||||||
|
- QR code (obfuscated)
|
||||||
|
- Download: text, QR, stego backup
|
||||||
|
- Warning if replacing existing key
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### `index.html`
|
||||||
|
**Route:** `/`
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
- Hero section with tagline
|
||||||
|
- 3 action cards: Encode, Decode, Generate
|
||||||
|
- "How It Works" explainer section
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `generate.html`
|
||||||
|
**Route:** `/generate`
|
||||||
|
|
||||||
|
**Form:** `POST /generate`
|
||||||
|
- `words` - passphrase word count (3-12)
|
||||||
|
- `use_pin` - checkbox
|
||||||
|
- `pin_length` - PIN digits (6-9)
|
||||||
|
- `use_rsa` - checkbox
|
||||||
|
- `rsa_bits` - key size (2048/3072)
|
||||||
|
|
||||||
|
**Output panels:**
|
||||||
|
- Passphrase display
|
||||||
|
- PIN display (if enabled)
|
||||||
|
- RSA key + QR (if enabled)
|
||||||
|
- Entropy calculator
|
||||||
|
|
||||||
|
**JS:** `static/js/generate.js`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `encode.html`
|
||||||
|
**Route:** `/encode`
|
||||||
|
|
||||||
|
**Form:** `POST /encode` (multipart)
|
||||||
|
- `reference_photo` - file upload (drag-drop zone)
|
||||||
|
- `carrier_image` - file upload (drag-drop zone)
|
||||||
|
- `mode` - radio: DCT (default) / LSB
|
||||||
|
- `dct_format` - PNG / JPEG
|
||||||
|
- `dct_color` - Color / Grayscale
|
||||||
|
- `payload_type` - radio: Text / File
|
||||||
|
- `message` - textarea (if text)
|
||||||
|
- `embed_file` - file input (if file)
|
||||||
|
- `passphrase` - text input
|
||||||
|
- `pin` - text input
|
||||||
|
- `rsa_key` / `rsa_key_qr` - file inputs
|
||||||
|
- `rsa_key_password` - password
|
||||||
|
- `channel_key` - select (saved keys) or manual input
|
||||||
|
|
||||||
|
**Panels:**
|
||||||
|
- Reference preview with "Hash Acquired" status
|
||||||
|
- Carrier preview with capacity info
|
||||||
|
- Character counter for message
|
||||||
|
|
||||||
|
**JS:** `static/js/encode.js`, `static/js/stegasoo.js`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `encode_result.html`
|
||||||
|
**Route:** `/encode/result/<file_id>`
|
||||||
|
|
||||||
|
**Elements:**
|
||||||
|
- Success message
|
||||||
|
- Stego image preview
|
||||||
|
- Download button
|
||||||
|
- Share button (Web Share API)
|
||||||
|
- Mode/capacity info
|
||||||
|
- "Encode Another" link
|
||||||
|
|
||||||
|
**Variables:** `file_id`, `filename`, `mode`, `capacity_used`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `decode.html`
|
||||||
|
**Route:** `/decode`
|
||||||
|
|
||||||
|
**Form:** `POST /decode` (multipart)
|
||||||
|
- `reference_photo` - file upload
|
||||||
|
- `stego_image` - file upload
|
||||||
|
- `passphrase` - text input
|
||||||
|
- `pin` - text input
|
||||||
|
- `rsa_key` / `rsa_key_qr` - file inputs
|
||||||
|
- `rsa_key_password` - password
|
||||||
|
- `channel_key` - select or manual
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
- Decoded message display
|
||||||
|
- File download (if file payload)
|
||||||
|
|
||||||
|
**JS:** `static/js/decode.js`, `static/js/stegasoo.js`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools & Account
|
||||||
|
|
||||||
|
### `tools.html`
|
||||||
|
**Route:** `/tools`
|
||||||
|
|
||||||
|
**Tabbed interface:**
|
||||||
|
|
||||||
|
| Tab | Endpoint | Description |
|
||||||
|
|-----|----------|-------------|
|
||||||
|
| Capacity | `POST /api/tools/capacity` | Image capacity analysis |
|
||||||
|
| Peek | `POST /api/tools/peek` | Check for Stegasoo header |
|
||||||
|
| Strip | `POST /api/tools/strip` | Remove hidden data |
|
||||||
|
| EXIF | `POST /api/tools/exif/*` | Metadata viewer/editor |
|
||||||
|
|
||||||
|
**EXIF Editor features:**
|
||||||
|
- Upload image → view all EXIF fields
|
||||||
|
- Inline editing (click field to edit)
|
||||||
|
- "Clear All" button
|
||||||
|
- "Save" / "Download" buttons
|
||||||
|
|
||||||
|
**JS:** `static/js/tools.js`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `account.html`
|
||||||
|
**Route:** `/account`
|
||||||
|
|
||||||
|
**Sections:**
|
||||||
|
|
||||||
|
1. **User Info** - Username, role badge, logout link
|
||||||
|
|
||||||
|
2. **Recovery Key** (admin only)
|
||||||
|
- Status: Configured / Not Set
|
||||||
|
- Generate/Regenerate button
|
||||||
|
- Disable button
|
||||||
|
|
||||||
|
3. **Password Change**
|
||||||
|
- `current_password`
|
||||||
|
- `new_password`
|
||||||
|
- `new_password_confirm`
|
||||||
|
|
||||||
|
4. **Saved Channel Keys**
|
||||||
|
- List of saved keys with edit/delete
|
||||||
|
- "Add Key" form (name + key)
|
||||||
|
- Max 10 keys per user
|
||||||
|
|
||||||
|
**Variables:** `username`, `is_admin`, `has_recovery`, `channel_keys`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `about.html`
|
||||||
|
**Route:** `/about`
|
||||||
|
|
||||||
|
**Sections:**
|
||||||
|
- Version info + feature badges
|
||||||
|
- Security model explanation
|
||||||
|
- Channel key QR (if configured)
|
||||||
|
- Dependency status table
|
||||||
|
- Credits + links
|
||||||
|
|
||||||
|
**Variables:** `version`, `has_dct`, `has_qr_write`, `has_qr_read`, `channel_key`, `channel_qr`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin
|
||||||
|
|
||||||
|
### `admin/users.html`
|
||||||
|
**Route:** `/admin/users`
|
||||||
|
|
||||||
|
**Table columns:** Username | Role | Created | Actions
|
||||||
|
|
||||||
|
**Actions per user:**
|
||||||
|
- Reset Password button
|
||||||
|
- Delete button (disabled for self)
|
||||||
|
|
||||||
|
**Header:**
|
||||||
|
- User count: "X of 16 users"
|
||||||
|
- "Add User" button (modal trigger)
|
||||||
|
|
||||||
|
**Modal:** Add User form
|
||||||
|
- `username` input
|
||||||
|
- `role` select (admin/user)
|
||||||
|
- Auto-generated temp password display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `admin/user_new.html`
|
||||||
|
**Route:** `/admin/users/new`
|
||||||
|
|
||||||
|
**Form:** `POST /admin/users/new`
|
||||||
|
- `username` - text input
|
||||||
|
- `role` - select (user/admin)
|
||||||
|
|
||||||
|
Redirects to `user_created.html` on success.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `admin/user_created.html`
|
||||||
|
**Route:** `/admin/users/created`
|
||||||
|
|
||||||
|
**Display:**
|
||||||
|
- Success message
|
||||||
|
- Username
|
||||||
|
- Temporary password (copy button)
|
||||||
|
- "User must change password on first login" notice
|
||||||
|
- Back to users link
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `admin/password_reset.html`
|
||||||
|
**Route:** `/admin/users/<id>/password-reset`
|
||||||
|
|
||||||
|
**Display:**
|
||||||
|
- Success message
|
||||||
|
- New temporary password
|
||||||
|
- Copy button
|
||||||
|
- Back link
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Drag-Drop Upload Zones
|
||||||
|
```html
|
||||||
|
<div class="upload-zone" id="referenceZone">
|
||||||
|
<input type="file" name="reference_photo" accept="image/*">
|
||||||
|
<div class="preview"></div>
|
||||||
|
<div class="status"></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Password Toggle
|
||||||
|
```html
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" id="passwordInput">
|
||||||
|
<button onclick="togglePassword('passwordInput', this)">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toast Flash Messages
|
||||||
|
Rendered in `base.html`, auto-dismiss after 10 seconds:
|
||||||
|
- `success` → green
|
||||||
|
- `warning` → yellow
|
||||||
|
- `error` → red
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## External JS Files
|
||||||
|
|
||||||
|
| File | Used By |
|
||||||
|
|------|---------|
|
||||||
|
| `static/js/stegasoo.js` | encode, decode, about |
|
||||||
|
| `static/js/auth.js` | login, setup, recover, account |
|
||||||
|
| `static/js/generate.js` | generate |
|
||||||
|
| `static/js/encode.js` | encode |
|
||||||
|
| `static/js/decode.js` | decode |
|
||||||
|
| `static/js/tools.js` | tools |
|
||||||
340
docs/stegasoo.1
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
.\" Stegasoo man page
|
||||||
|
.\" Generate with: groff -man -Tascii stegasoo.1
|
||||||
|
.TH STEGASOO 1 "January 2026" "Stegasoo 4.1.7" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
stegasoo \- steganography with hybrid authentication
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B stegasoo
|
||||||
|
[\fB\-v\fR|\fB\-\-version\fR]
|
||||||
|
[\fB\-\-json\fR]
|
||||||
|
[\fB\-h\fR|\fB\-\-help\fR]
|
||||||
|
.I command
|
||||||
|
[\fIargs\fR]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B stegasoo
|
||||||
|
hides messages and files in images using PIN + passphrase security.
|
||||||
|
It uses LSB (Least Significant Bit) steganography with optional DCT
|
||||||
|
(Discrete Cosine Transform) encoding for JPEG resilience.
|
||||||
|
.PP
|
||||||
|
Messages are encrypted using a hybrid authentication scheme that combines
|
||||||
|
a reference photo (shared secret), passphrase, and PIN code.
|
||||||
|
.SH GLOBAL OPTIONS
|
||||||
|
.TP
|
||||||
|
.BR \-v ", " \-\-version
|
||||||
|
Show version and exit.
|
||||||
|
.TP
|
||||||
|
.B \-\-json
|
||||||
|
Output results as JSON (where supported).
|
||||||
|
.TP
|
||||||
|
.BR \-h ", " \-\-help
|
||||||
|
Show help message and exit.
|
||||||
|
.SH COMMANDS
|
||||||
|
.SS encode
|
||||||
|
Encode a message or file into an image.
|
||||||
|
.PP
|
||||||
|
.B stegasoo encode
|
||||||
|
.I carrier
|
||||||
|
.B \-r
|
||||||
|
.I reference
|
||||||
|
[\fB\-m\fR \fImessage\fR | \fB\-f\fR \fIfile\fR]
|
||||||
|
[\fIoptions\fR]
|
||||||
|
.TP
|
||||||
|
.BR \-r ", " \-\-reference " " \fIPATH\fR
|
||||||
|
Reference photo (shared secret). Required.
|
||||||
|
.TP
|
||||||
|
.BR \-m ", " \-\-message " " \fITEXT\fR
|
||||||
|
Message to encode.
|
||||||
|
.TP
|
||||||
|
.BR \-f ", " \-\-file " " \fIPATH\fR
|
||||||
|
File to embed instead of message.
|
||||||
|
.TP
|
||||||
|
.BR \-o ", " \-\-output " " \fIPATH\fR
|
||||||
|
Output image path.
|
||||||
|
.TP
|
||||||
|
.B \-\-passphrase " " \fITEXT\fR
|
||||||
|
Passphrase (recommend 4+ words). Prompts if not provided.
|
||||||
|
.TP
|
||||||
|
.B \-\-pin " " \fITEXT\fR
|
||||||
|
PIN code. Prompts if not provided.
|
||||||
|
.TP
|
||||||
|
.B \-\-dry\-run
|
||||||
|
Show capacity usage without encoding.
|
||||||
|
.PP
|
||||||
|
.B Examples:
|
||||||
|
.nf
|
||||||
|
stegasoo encode photo.png -r ref.jpg -m "Secret" --passphrase --pin
|
||||||
|
stegasoo encode photo.png -r ref.jpg -f doc.pdf -o encoded.png
|
||||||
|
.fi
|
||||||
|
.SS decode
|
||||||
|
Decode a message or file from an image.
|
||||||
|
.PP
|
||||||
|
.B stegasoo decode
|
||||||
|
.I image
|
||||||
|
.B \-r
|
||||||
|
.I reference
|
||||||
|
[\fIoptions\fR]
|
||||||
|
.TP
|
||||||
|
.BR \-r ", " \-\-reference " " \fIPATH\fR
|
||||||
|
Reference photo (shared secret). Required.
|
||||||
|
.TP
|
||||||
|
.B \-\-passphrase " " \fITEXT\fR
|
||||||
|
Passphrase. Prompts if not provided.
|
||||||
|
.TP
|
||||||
|
.B \-\-pin " " \fITEXT\fR
|
||||||
|
PIN code. Prompts if not provided.
|
||||||
|
.TP
|
||||||
|
.BR \-o ", " \-\-output " " \fIPATH\fR
|
||||||
|
Output path for file payloads.
|
||||||
|
.PP
|
||||||
|
.B Examples:
|
||||||
|
.nf
|
||||||
|
stegasoo decode encoded.png -r ref.jpg --passphrase --pin
|
||||||
|
stegasoo decode encoded.png -r ref.jpg -o ./extracted/
|
||||||
|
.fi
|
||||||
|
.SS generate
|
||||||
|
Generate random credentials (passphrase + PIN + optional channel key).
|
||||||
|
.PP
|
||||||
|
.B stegasoo generate
|
||||||
|
[\fIoptions\fR]
|
||||||
|
.TP
|
||||||
|
.B \-\-words " " \fIINTEGER\fR
|
||||||
|
Number of words in passphrase (default: 4).
|
||||||
|
.TP
|
||||||
|
.B \-\-pin\-length " " \fIINTEGER\fR
|
||||||
|
PIN length (default: 6).
|
||||||
|
.TP
|
||||||
|
.B \-\-channel\-key
|
||||||
|
Also generate a 256-bit channel key.
|
||||||
|
.PP
|
||||||
|
.B Examples:
|
||||||
|
.nf
|
||||||
|
stegasoo generate
|
||||||
|
stegasoo generate --words 6 --pin-length 8
|
||||||
|
stegasoo generate --channel-key
|
||||||
|
.fi
|
||||||
|
.SS info
|
||||||
|
Show version, features, and system information.
|
||||||
|
.PP
|
||||||
|
.B stegasoo info
|
||||||
|
[\fB\-\-full\fR]
|
||||||
|
.TP
|
||||||
|
.B \-\-full
|
||||||
|
Show full system information (CPU, temperature, disk on Pi).
|
||||||
|
.SS batch
|
||||||
|
Batch operations on multiple images.
|
||||||
|
.PP
|
||||||
|
.B stegasoo batch
|
||||||
|
.I subcommand
|
||||||
|
[\fIargs\fR]
|
||||||
|
.TP
|
||||||
|
.B batch encode
|
||||||
|
Encode message into multiple images.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
.B stegasoo batch encode
|
||||||
|
.I images...
|
||||||
|
[\fB\-m\fR \fImessage\fR | \fB\-f\fR \fIfile\fR]
|
||||||
|
[\fIoptions\fR]
|
||||||
|
.PP
|
||||||
|
Options: \fB\-m\fR, \fB\-f\fR, \fB\-o\fR/\fB\-\-output\-dir\fR, \fB\-\-suffix\fR, \fB\-\-passphrase\fR, \fB\-\-pin\fR,
|
||||||
|
\fB\-r\fR/\fB\-\-recursive\fR, \fB\-j\fR/\fB\-\-jobs\fR, \fB\-v\fR/\fB\-\-verbose\fR.
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B batch decode
|
||||||
|
Decode messages from multiple images.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
.B stegasoo batch decode
|
||||||
|
.I images...
|
||||||
|
[\fIoptions\fR]
|
||||||
|
.PP
|
||||||
|
Options: \fB\-o\fR/\fB\-\-output\-dir\fR, \fB\-\-passphrase\fR, \fB\-\-pin\fR, \fB\-r\fR/\fB\-\-recursive\fR,
|
||||||
|
\fB\-j\fR/\fB\-\-jobs\fR, \fB\-v\fR/\fB\-\-verbose\fR.
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B batch check
|
||||||
|
Check capacity of multiple images.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
.B stegasoo batch check
|
||||||
|
.I images...
|
||||||
|
[\fB\-r\fR/\fB\-\-recursive\fR]
|
||||||
|
.RE
|
||||||
|
.SS channel
|
||||||
|
Manage channel keys for deployment isolation.
|
||||||
|
.PP
|
||||||
|
Channel keys bind encode/decode operations to a specific group or deployment.
|
||||||
|
Messages encoded with one channel key can only be decoded by systems with
|
||||||
|
the same channel key.
|
||||||
|
.PP
|
||||||
|
.B stegasoo channel
|
||||||
|
.I subcommand
|
||||||
|
[\fIargs\fR]
|
||||||
|
.TP
|
||||||
|
.B channel generate
|
||||||
|
Generate a new random channel key.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
Options: \fB\-\-save\fR (project config), \fB\-\-save\-user\fR (user config).
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B channel show
|
||||||
|
Show the current channel key.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
Options: \fB\-\-key\fR \fITEXT\fR (show specific key instead).
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B channel qr
|
||||||
|
Display channel key as QR code.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
Options: \fB\-\-key\fR \fITEXT\fR, \fB\-\-format\fR [\fIascii\fR|\fIpng\fR], \fB\-o\fR/\fB\-\-output\fR \fIPATH\fR.
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B channel status
|
||||||
|
Show channel key status and configuration.
|
||||||
|
.TP
|
||||||
|
.B channel clear
|
||||||
|
Remove channel key configuration.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
Options: \fB\-\-project\fR, \fB\-\-user\fR.
|
||||||
|
.RE
|
||||||
|
.SS admin
|
||||||
|
Web UI administration commands.
|
||||||
|
.PP
|
||||||
|
.B stegasoo admin
|
||||||
|
.I subcommand
|
||||||
|
[\fIargs\fR]
|
||||||
|
.TP
|
||||||
|
.B admin generate\-key
|
||||||
|
Generate a new recovery key (for reference only).
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
Options: \fB\-\-qr\fR (show QR code in terminal).
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B admin recover
|
||||||
|
Reset admin password using recovery key.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
Options: \fB\-\-db\fR \fIPATH\fR (path to stegasoo.db), \fB\-\-password\fR \fITEXT\fR.
|
||||||
|
.RE
|
||||||
|
.SS tools
|
||||||
|
Image security tools.
|
||||||
|
.PP
|
||||||
|
.B stegasoo tools
|
||||||
|
.I subcommand
|
||||||
|
[\fIargs\fR]
|
||||||
|
.TP
|
||||||
|
.B tools capacity
|
||||||
|
Show steganography capacity for an image.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
.B stegasoo tools capacity
|
||||||
|
.I image
|
||||||
|
[\fB\-\-json\fR]
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B tools exif
|
||||||
|
View or edit EXIF metadata.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
.B stegasoo tools exif
|
||||||
|
.I image
|
||||||
|
[\fB\-\-clear\fR] [\fB\-\-set\fR \fIFIELD=VALUE\fR] [\fB\-o\fR \fIPATH\fR] [\fB\-\-json\fR]
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B tools peek
|
||||||
|
Check if image contains Stegasoo hidden data.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
.B stegasoo tools peek
|
||||||
|
.I image
|
||||||
|
[\fB\-\-json\fR]
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B tools strip
|
||||||
|
Strip EXIF/metadata from an image.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
.B stegasoo tools strip
|
||||||
|
.I image
|
||||||
|
[\fB\-o\fR \fIPATH\fR] [\fB\-\-format\fR [\fIpng\fR|\fIbmp\fR]]
|
||||||
|
.RE
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B STEGASOO_CHANNEL_KEY
|
||||||
|
Channel key for encode/decode operations. Overrides config file settings.
|
||||||
|
.TP
|
||||||
|
.B STEGASOO_HTTPS_ENABLED
|
||||||
|
Enable HTTPS for web UI (Docker/service mode).
|
||||||
|
.TP
|
||||||
|
.B STEGASOO_HOSTNAME
|
||||||
|
Hostname for SSL certificate generation.
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I ~/.stegasoo/channel.key
|
||||||
|
User's channel key configuration (encrypted).
|
||||||
|
.TP
|
||||||
|
.I .stegasoo.toml
|
||||||
|
Project-level configuration file.
|
||||||
|
.TP
|
||||||
|
.I frontends/web/instance/stegasoo.db
|
||||||
|
Web UI SQLite database (accounts, settings).
|
||||||
|
.SH EXAMPLES
|
||||||
|
.SS Basic encode/decode workflow
|
||||||
|
.nf
|
||||||
|
# Generate credentials
|
||||||
|
stegasoo generate
|
||||||
|
|
||||||
|
# Encode a secret message
|
||||||
|
stegasoo encode vacation.png -r selfie.jpg -m "Meet at noon"
|
||||||
|
|
||||||
|
# Decode the message (on another system with same reference photo)
|
||||||
|
stegasoo decode vacation_steg.png -r selfie.jpg
|
||||||
|
.fi
|
||||||
|
.SS Using channel keys for team isolation
|
||||||
|
.nf
|
||||||
|
# Generate and save a channel key
|
||||||
|
stegasoo channel generate --save-user
|
||||||
|
|
||||||
|
# Share the key with your team
|
||||||
|
stegasoo channel qr -o team-key.png
|
||||||
|
|
||||||
|
# Now all encode/decode operations use this channel
|
||||||
|
stegasoo encode photo.png -r ref.jpg -m "Team secret"
|
||||||
|
.fi
|
||||||
|
.SS Batch processing
|
||||||
|
.nf
|
||||||
|
# Check capacity of all PNGs in a directory
|
||||||
|
stegasoo batch check ./photos/*.png
|
||||||
|
|
||||||
|
# Encode same message into multiple images
|
||||||
|
stegasoo batch encode ./photos/ -r ref.jpg -m "Secret" -o ./encoded/
|
||||||
|
.fi
|
||||||
|
.SH SECURITY
|
||||||
|
Stegasoo uses multiple layers of security:
|
||||||
|
.IP \(bu 2
|
||||||
|
Reference photo provides a visual shared secret
|
||||||
|
.IP \(bu 2
|
||||||
|
Passphrase (recommend 4+ words) for strong encryption
|
||||||
|
.IP \(bu 2
|
||||||
|
PIN code adds additional entropy
|
||||||
|
.IP \(bu 2
|
||||||
|
Channel keys isolate different deployments
|
||||||
|
.IP \(bu 2
|
||||||
|
AES-256 encryption for payload data
|
||||||
|
.PP
|
||||||
|
For maximum security, share the reference photo out-of-band (in person,
|
||||||
|
secure messenger) and use a strong passphrase.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR openssl (1),
|
||||||
|
.BR qrencode (1)
|
||||||
|
.SH BUGS
|
||||||
|
Report bugs at: https://github.com/adlee-was-taken/stegasoo/issues
|
||||||
|
.SH AUTHOR
|
||||||
|
Written by the Stegasoo contributors.
|
||||||
|
.SH COPYRIGHT
|
||||||
|
Copyright \(co 2024-2026. MIT License.
|
||||||
48
examples/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Stegasoo Examples
|
||||||
|
|
||||||
|
This directory contains example scripts demonstrating how to use Stegasoo.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Install Stegasoo first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install stegasoo
|
||||||
|
# Or for development:
|
||||||
|
pip install -e ".[all]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### basic_usage.py
|
||||||
|
|
||||||
|
Basic encode/decode workflow with a text message.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python basic_usage.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### embed_file.py
|
||||||
|
|
||||||
|
Embed and extract files (documents, images, etc.) inside carrier images.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python embed_file.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### channel_keys.py
|
||||||
|
|
||||||
|
Use channel keys to create private communication channels for groups.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python channel_keys.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Images
|
||||||
|
|
||||||
|
You'll need to provide your own images:
|
||||||
|
|
||||||
|
- `my_secret_photo.png` - Your reference photo (keep this secret!)
|
||||||
|
- `carrier.png` - The image that will carry your hidden message
|
||||||
|
|
||||||
|
For testing, you can use any PNG or BMP image. JPEG carriers are supported with DCT mode.
|
||||||
59
examples/basic_usage.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Basic Stegasoo Usage Example
|
||||||
|
|
||||||
|
This example demonstrates how to encode and decode a secret message
|
||||||
|
using the Stegasoo library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import stegasoo
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Load your images
|
||||||
|
# The reference photo is your "key" - keep it secret!
|
||||||
|
reference_photo = Path("my_secret_photo.png").read_bytes()
|
||||||
|
carrier_image = Path("carrier.png").read_bytes()
|
||||||
|
|
||||||
|
# Your secret message
|
||||||
|
message = "This is my secret message!"
|
||||||
|
|
||||||
|
# Your credentials
|
||||||
|
passphrase = "correct horse battery staple" # Use 4+ words
|
||||||
|
pin = "123456" # 6-9 digits
|
||||||
|
|
||||||
|
# === ENCODE ===
|
||||||
|
print("Encoding message...")
|
||||||
|
result = stegasoo.encode(
|
||||||
|
message=message,
|
||||||
|
reference_photo=reference_photo,
|
||||||
|
carrier_image=carrier_image,
|
||||||
|
passphrase=passphrase,
|
||||||
|
pin=pin,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save the stego image
|
||||||
|
output_path = Path(f"secret_{result.suggested_filename}")
|
||||||
|
output_path.write_bytes(result.stego_image)
|
||||||
|
print(f"Saved to: {output_path}")
|
||||||
|
print(f"Capacity used: {result.capacity_used_percent:.1f}%")
|
||||||
|
|
||||||
|
# === DECODE ===
|
||||||
|
print("\nDecoding message...")
|
||||||
|
stego_image = output_path.read_bytes()
|
||||||
|
|
||||||
|
decoded = stegasoo.decode(
|
||||||
|
stego_image=stego_image,
|
||||||
|
reference_photo=reference_photo,
|
||||||
|
passphrase=passphrase,
|
||||||
|
pin=pin,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Decoded message: {decoded.message}")
|
||||||
|
print(f"Message type: {decoded.payload_type}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
72
examples/channel_keys.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Channel Keys Example
|
||||||
|
|
||||||
|
Channel keys allow you to create private communication channels.
|
||||||
|
Only people with the same channel key can decode messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import stegasoo
|
||||||
|
from stegasoo.channel import generate_channel_key, get_channel_fingerprint
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Generate a channel key for your group
|
||||||
|
channel_key = generate_channel_key()
|
||||||
|
fingerprint = get_channel_fingerprint(channel_key)
|
||||||
|
|
||||||
|
print("=== Channel Key Generated ===")
|
||||||
|
print(f"Key: {channel_key}")
|
||||||
|
print(f"Fingerprint: {fingerprint}")
|
||||||
|
print("\nShare this key securely with your group members!")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
# Load images
|
||||||
|
reference_photo = Path("my_secret_photo.png").read_bytes()
|
||||||
|
carrier_image = Path("carrier.png").read_bytes()
|
||||||
|
|
||||||
|
# Encode with channel key
|
||||||
|
print("\nEncoding message with channel key...")
|
||||||
|
result = stegasoo.encode(
|
||||||
|
message="Secret group message!",
|
||||||
|
reference_photo=reference_photo,
|
||||||
|
carrier_image=carrier_image,
|
||||||
|
passphrase="correct horse battery staple",
|
||||||
|
pin="123456",
|
||||||
|
channel_key=channel_key, # Add the channel key
|
||||||
|
)
|
||||||
|
|
||||||
|
stego_data = result.stego_image
|
||||||
|
print(f"Encoded successfully!")
|
||||||
|
|
||||||
|
# Decode with correct channel key
|
||||||
|
print("\nDecoding with correct channel key...")
|
||||||
|
decoded = stegasoo.decode(
|
||||||
|
stego_image=stego_data,
|
||||||
|
reference_photo=reference_photo,
|
||||||
|
passphrase="correct horse battery staple",
|
||||||
|
pin="123456",
|
||||||
|
channel_key=channel_key, # Same channel key
|
||||||
|
)
|
||||||
|
print(f"Message: {decoded.message}")
|
||||||
|
|
||||||
|
# Try to decode with wrong channel key
|
||||||
|
print("\nTrying to decode with wrong channel key...")
|
||||||
|
wrong_key = generate_channel_key()
|
||||||
|
try:
|
||||||
|
stegasoo.decode(
|
||||||
|
stego_image=stego_data,
|
||||||
|
reference_photo=reference_photo,
|
||||||
|
passphrase="correct horse battery staple",
|
||||||
|
pin="123456",
|
||||||
|
channel_key=wrong_key, # Different channel key
|
||||||
|
)
|
||||||
|
print("ERROR: Should have failed!")
|
||||||
|
except (stegasoo.DecryptionError, stegasoo.ExtractionError):
|
||||||
|
print("Correctly rejected - wrong channel key!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
78
examples/embed_file.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
File Embedding Example
|
||||||
|
|
||||||
|
This example demonstrates how to embed a file (like a document or image)
|
||||||
|
inside a carrier image using Stegasoo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import stegasoo
|
||||||
|
from stegasoo.models import FilePayload
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Load images
|
||||||
|
reference_photo = Path("my_secret_photo.png").read_bytes()
|
||||||
|
carrier_image = Path("carrier.png").read_bytes()
|
||||||
|
|
||||||
|
# Load the file to embed
|
||||||
|
secret_file = Path("secret_document.pdf")
|
||||||
|
file_data = secret_file.read_bytes()
|
||||||
|
|
||||||
|
# Create a FilePayload
|
||||||
|
payload = FilePayload(
|
||||||
|
filename=secret_file.name,
|
||||||
|
data=file_data,
|
||||||
|
mime_type="application/pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Credentials
|
||||||
|
passphrase = "correct horse battery staple"
|
||||||
|
pin = "123456"
|
||||||
|
|
||||||
|
# Check capacity first
|
||||||
|
capacity = stegasoo.calculate_capacity(carrier_image)
|
||||||
|
print(f"Carrier capacity: {capacity['capacity_bytes']:,} bytes")
|
||||||
|
print(f"File size: {len(file_data):,} bytes")
|
||||||
|
|
||||||
|
if len(file_data) > capacity["capacity_bytes"]:
|
||||||
|
print("Error: File too large for this carrier!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Encode the file
|
||||||
|
print("\nEmbedding file...")
|
||||||
|
result = stegasoo.encode(
|
||||||
|
file_payload=payload,
|
||||||
|
reference_photo=reference_photo,
|
||||||
|
carrier_image=carrier_image,
|
||||||
|
passphrase=passphrase,
|
||||||
|
pin=pin,
|
||||||
|
)
|
||||||
|
|
||||||
|
output_path = Path(f"contains_file_{result.suggested_filename}")
|
||||||
|
output_path.write_bytes(result.stego_image)
|
||||||
|
print(f"Saved to: {output_path}")
|
||||||
|
|
||||||
|
# Decode and extract the file
|
||||||
|
print("\nExtracting file...")
|
||||||
|
decoded = stegasoo.decode(
|
||||||
|
stego_image=output_path.read_bytes(),
|
||||||
|
reference_photo=reference_photo,
|
||||||
|
passphrase=passphrase,
|
||||||
|
pin=pin,
|
||||||
|
)
|
||||||
|
|
||||||
|
if decoded.payload_type == "file":
|
||||||
|
extracted_path = Path(f"extracted_{decoded.filename}")
|
||||||
|
extracted_path.write_bytes(decoded.file_data)
|
||||||
|
print(f"Extracted: {extracted_path}")
|
||||||
|
print(f"Original filename: {decoded.filename}")
|
||||||
|
print(f"MIME type: {decoded.mime_type}")
|
||||||
|
else:
|
||||||
|
print(f"Unexpected payload type: {decoded.payload_type}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
952
frontends/API.md
@@ -1,952 +0,0 @@
|
|||||||
# Stegasoo REST API Documentation
|
|
||||||
|
|
||||||
Complete REST API reference for Stegasoo steganography operations.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Authentication](#authentication)
|
|
||||||
- [Base URL](#base-url)
|
|
||||||
- [Endpoints](#endpoints)
|
|
||||||
- [GET /](#get--status)
|
|
||||||
- [POST /generate](#post-generate)
|
|
||||||
- [POST /encode](#post-encode-json)
|
|
||||||
- [POST /encode/multipart](#post-encodemultipart)
|
|
||||||
- [POST /decode](#post-decode-json)
|
|
||||||
- [POST /decode/multipart](#post-decodemultipart)
|
|
||||||
- [POST /image/info](#post-imageinfo)
|
|
||||||
- [Data Models](#data-models)
|
|
||||||
- [Error Handling](#error-handling)
|
|
||||||
- [Code Examples](#code-examples)
|
|
||||||
- [Rate Limiting](#rate-limiting)
|
|
||||||
- [Security Considerations](#security-considerations)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Stegasoo REST API provides programmatic access to all steganography operations:
|
|
||||||
|
|
||||||
- **Generate** credentials (phrases, PINs, RSA keys)
|
|
||||||
- **Encode** messages into images
|
|
||||||
- **Decode** messages from images
|
|
||||||
- **Analyze** image capacity
|
|
||||||
|
|
||||||
The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### From PyPI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install stegasoo[api]
|
|
||||||
```
|
|
||||||
|
|
||||||
### From Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/example/stegasoo.git
|
|
||||||
cd stegasoo
|
|
||||||
pip install -e ".[api]"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running the Server
|
|
||||||
|
|
||||||
**Development:**
|
|
||||||
```bash
|
|
||||||
cd frontends/api
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Production:**
|
|
||||||
```bash
|
|
||||||
cd frontends/api
|
|
||||||
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
|
||||||
```
|
|
||||||
|
|
||||||
**Docker:**
|
|
||||||
```bash
|
|
||||||
docker-compose up api
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
The API currently operates without authentication. For production deployments, implement authentication at the reverse proxy level (nginx, Caddy) or add API key middleware.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Base URL
|
|
||||||
|
|
||||||
| Environment | URL |
|
|
||||||
|-------------|-----|
|
|
||||||
| Local Development | `http://localhost:8000` |
|
|
||||||
| Docker | `http://localhost:8000` |
|
|
||||||
| Production | Configure as needed |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
### GET / (Status)
|
|
||||||
|
|
||||||
Check API status and configuration.
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET / HTTP/1.1
|
|
||||||
Host: localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "2.0.1",
|
|
||||||
"has_argon2": true,
|
|
||||||
"day_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Response Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `version` | string | Stegasoo library version |
|
|
||||||
| `has_argon2` | boolean | Whether Argon2id is available |
|
|
||||||
| `day_names` | array | Day names for phrase mapping |
|
|
||||||
|
|
||||||
#### cURL Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### POST /generate
|
|
||||||
|
|
||||||
Generate credentials for encoding/decoding.
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /generate HTTP/1.1
|
|
||||||
Host: localhost:8000
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Request Body
|
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
|
||||||
|-------|------|---------|-------------|
|
|
||||||
| `use_pin` | boolean | `true` | Generate a PIN |
|
|
||||||
| `use_rsa` | boolean | `false` | Generate an RSA key |
|
|
||||||
| `pin_length` | integer | `6` | PIN length (6-9) |
|
|
||||||
| `rsa_bits` | integer | `2048` | RSA key size (2048, 3072, 4096) |
|
|
||||||
| `words_per_phrase` | integer | `3` | Words per phrase (3-12) |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"phrases": {
|
|
||||||
"Monday": "abandon ability able",
|
|
||||||
"Tuesday": "actor actress actual",
|
|
||||||
"Wednesday": "advice aerobic affair",
|
|
||||||
"Thursday": "afraid again age",
|
|
||||||
"Friday": "agree ahead aim",
|
|
||||||
"Saturday": "airport aisle alarm",
|
|
||||||
"Sunday": "album alcohol alert"
|
|
||||||
},
|
|
||||||
"pin": "847293",
|
|
||||||
"rsa_key_pem": null,
|
|
||||||
"entropy": {
|
|
||||||
"phrase": 33,
|
|
||||||
"pin": 19,
|
|
||||||
"rsa": 0,
|
|
||||||
"total": 52
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Response Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `phrases` | object | Day-to-phrase mapping |
|
|
||||||
| `pin` | string\|null | Generated PIN (if requested) |
|
|
||||||
| `rsa_key_pem` | string\|null | PEM-encoded RSA key (if requested) |
|
|
||||||
| `entropy.phrase` | integer | Entropy from phrases (bits) |
|
|
||||||
| `entropy.pin` | integer | Entropy from PIN (bits) |
|
|
||||||
| `entropy.rsa` | integer | Entropy from RSA key (bits) |
|
|
||||||
| `entropy.total` | integer | Combined entropy (bits) |
|
|
||||||
|
|
||||||
#### cURL Examples
|
|
||||||
|
|
||||||
**PIN only:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/generate \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"use_pin": true, "use_rsa": false}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**RSA only:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/generate \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"use_pin": false, "use_rsa": true, "rsa_bits": 4096}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Both with custom settings:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/generate \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"use_pin": true,
|
|
||||||
"use_rsa": true,
|
|
||||||
"pin_length": 9,
|
|
||||||
"rsa_bits": 4096,
|
|
||||||
"words_per_phrase": 6
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### POST /encode (JSON)
|
|
||||||
|
|
||||||
Encode a message using base64-encoded images.
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /encode HTTP/1.1
|
|
||||||
Host: localhost:8000
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Request Body
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| `message` | string | ✓ | Message to encode |
|
|
||||||
| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo |
|
|
||||||
| `carrier_image_base64` | string | ✓ | Base64-encoded carrier image |
|
|
||||||
| `day_phrase` | string | ✓ | Today's passphrase |
|
|
||||||
| `pin` | string | * | Static PIN (6-9 digits) |
|
|
||||||
| `rsa_key_base64` | string | * | Base64-encoded RSA key PEM |
|
|
||||||
| `rsa_password` | string | | Password for RSA key |
|
|
||||||
| `date_str` | string | | Date override (YYYY-MM-DD) |
|
|
||||||
|
|
||||||
\* At least one of `pin` or `rsa_key_base64` required.
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"stego_image_base64": "iVBORw0KGgo...",
|
|
||||||
"filename": "a1b2c3d4_20251227.png",
|
|
||||||
"capacity_used_percent": 12.4,
|
|
||||||
"date_used": "2025-12-27",
|
|
||||||
"day_of_week": "Saturday"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Response Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `stego_image_base64` | string | Base64-encoded stego PNG |
|
|
||||||
| `filename` | string | Suggested filename |
|
|
||||||
| `capacity_used_percent` | float | Percentage of capacity used |
|
|
||||||
| `date_used` | string | Date embedded in image (YYYY-MM-DD) |
|
|
||||||
| `day_of_week` | string | Day name for passphrase rotation |
|
|
||||||
|
|
||||||
#### cURL Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Prepare base64-encoded images
|
|
||||||
REF_B64=$(base64 -w0 reference.jpg)
|
|
||||||
CARRIER_B64=$(base64 -w0 carrier.png)
|
|
||||||
|
|
||||||
curl -X POST http://localhost:8000/encode \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"message\": \"Secret message\",
|
|
||||||
\"reference_photo_base64\": \"$REF_B64\",
|
|
||||||
\"carrier_image_base64\": \"$CARRIER_B64\",
|
|
||||||
\"day_phrase\": \"apple forest thunder\",
|
|
||||||
\"pin\": \"123456\"
|
|
||||||
}" | jq -r '.stego_image_base64' | base64 -d > stego.png
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### POST /encode/multipart
|
|
||||||
|
|
||||||
Encode a message using direct file uploads. Returns the stego image directly.
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /encode/multipart HTTP/1.1
|
|
||||||
Host: localhost:8000
|
|
||||||
Content-Type: multipart/form-data; boundary=----FormBoundary
|
|
||||||
|
|
||||||
------FormBoundary
|
|
||||||
Content-Disposition: form-data; name="message"
|
|
||||||
|
|
||||||
Secret message here
|
|
||||||
------FormBoundary
|
|
||||||
Content-Disposition: form-data; name="day_phrase"
|
|
||||||
|
|
||||||
apple forest thunder
|
|
||||||
------FormBoundary
|
|
||||||
Content-Disposition: form-data; name="pin"
|
|
||||||
|
|
||||||
123456
|
|
||||||
------FormBoundary
|
|
||||||
Content-Disposition: form-data; name="reference_photo"; filename="ref.jpg"
|
|
||||||
Content-Type: image/jpeg
|
|
||||||
|
|
||||||
<binary image data>
|
|
||||||
------FormBoundary
|
|
||||||
Content-Disposition: form-data; name="carrier"; filename="carrier.png"
|
|
||||||
Content-Type: image/png
|
|
||||||
|
|
||||||
<binary image data>
|
|
||||||
------FormBoundary--
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Form Fields
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| `message` | string | ✓ | Message to encode |
|
|
||||||
| `reference_photo` | file | ✓ | Reference photo file |
|
|
||||||
| `carrier` | file | ✓ | Carrier image file |
|
|
||||||
| `day_phrase` | string | ✓ | Today's passphrase |
|
|
||||||
| `pin` | string | * | Static PIN |
|
|
||||||
| `rsa_key` | file | * | RSA key file (.pem) |
|
|
||||||
| `rsa_password` | string | | Password for RSA key |
|
|
||||||
| `date_str` | string | | Date override (YYYY-MM-DD) |
|
|
||||||
|
|
||||||
\* At least one of `pin` or `rsa_key` required.
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
Returns the PNG image directly with headers:
|
|
||||||
- `Content-Type: image/png`
|
|
||||||
- `Content-Disposition: attachment; filename=<generated_filename>.png`
|
|
||||||
- `X-Stegasoo-Date: 2025-12-27` (date used for encoding)
|
|
||||||
- `X-Stegasoo-Day: Saturday` (day of week for passphrase rotation)
|
|
||||||
- `X-Stegasoo-Capacity-Percent: 12.4` (capacity used)
|
|
||||||
|
|
||||||
#### cURL Examples
|
|
||||||
|
|
||||||
**With PIN:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
-F "message=Secret message" \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "pin=123456" \
|
|
||||||
-F "reference_photo=@reference.jpg" \
|
|
||||||
-F "carrier=@carrier.png" \
|
|
||||||
--output stego.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**With RSA key:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
-F "message=Secret message" \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "rsa_key=@mykey.pem" \
|
|
||||||
-F "rsa_password=keypassword" \
|
|
||||||
-F "reference_photo=@reference.jpg" \
|
|
||||||
-F "carrier=@carrier.png" \
|
|
||||||
--output stego.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**With both PIN and RSA:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
-F "message=Maximum security message" \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "pin=123456" \
|
|
||||||
-F "rsa_key=@mykey.pem" \
|
|
||||||
-F "rsa_password=keypassword" \
|
|
||||||
-F "reference_photo=@reference.jpg" \
|
|
||||||
-F "carrier=@carrier.png" \
|
|
||||||
--output stego.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**With custom date:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/encode/multipart \
|
|
||||||
-F "message=Backdated message" \
|
|
||||||
-F "day_phrase=monday phrase here" \
|
|
||||||
-F "pin=123456" \
|
|
||||||
-F "date_str=2025-12-29" \
|
|
||||||
-F "reference_photo=@reference.jpg" \
|
|
||||||
-F "carrier=@carrier.png" \
|
|
||||||
--output stego.png
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### POST /decode (JSON)
|
|
||||||
|
|
||||||
Decode a message using base64-encoded images.
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /decode HTTP/1.1
|
|
||||||
Host: localhost:8000
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Request Body
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| `stego_image_base64` | string | ✓ | Base64-encoded stego image |
|
|
||||||
| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo |
|
|
||||||
| `day_phrase` | string | ✓ | Passphrase for encoding day |
|
|
||||||
| `pin` | string | * | Static PIN |
|
|
||||||
| `rsa_key_base64` | string | * | Base64-encoded RSA key |
|
|
||||||
| `rsa_password` | string | | Password for RSA key |
|
|
||||||
|
|
||||||
\* Must match the security factors used during encoding.
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Secret message here"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### cURL Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Prepare base64-encoded images
|
|
||||||
STEGO_B64=$(base64 -w0 stego.png)
|
|
||||||
REF_B64=$(base64 -w0 reference.jpg)
|
|
||||||
|
|
||||||
curl -X POST http://localhost:8000/decode \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"stego_image_base64\": \"$STEGO_B64\",
|
|
||||||
\"reference_photo_base64\": \"$REF_B64\",
|
|
||||||
\"day_phrase\": \"apple forest thunder\",
|
|
||||||
\"pin\": \"123456\"
|
|
||||||
}"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### POST /decode/multipart
|
|
||||||
|
|
||||||
Decode a message using direct file uploads.
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /decode/multipart HTTP/1.1
|
|
||||||
Host: localhost:8000
|
|
||||||
Content-Type: multipart/form-data
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Form Fields
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| `stego_image` | file | ✓ | Stego image file |
|
|
||||||
| `reference_photo` | file | ✓ | Reference photo file |
|
|
||||||
| `day_phrase` | string | ✓ | Passphrase for encoding day |
|
|
||||||
| `pin` | string | * | Static PIN |
|
|
||||||
| `rsa_key` | file | * | RSA key file |
|
|
||||||
| `rsa_password` | string | | Password for RSA key |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Secret message here"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### cURL Examples
|
|
||||||
|
|
||||||
**With PIN:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/decode/multipart \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "pin=123456" \
|
|
||||||
-F "reference_photo=@reference.jpg" \
|
|
||||||
-F "stego_image=@stego.png"
|
|
||||||
```
|
|
||||||
|
|
||||||
**With RSA key:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/decode/multipart \
|
|
||||||
-F "day_phrase=apple forest thunder" \
|
|
||||||
-F "rsa_key=@mykey.pem" \
|
|
||||||
-F "rsa_password=keypassword" \
|
|
||||||
-F "reference_photo=@reference.jpg" \
|
|
||||||
-F "stego_image=@stego.png"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### POST /image/info
|
|
||||||
|
|
||||||
Get information about an image's capacity.
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /image/info HTTP/1.1
|
|
||||||
Host: localhost:8000
|
|
||||||
Content-Type: multipart/form-data
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Form Fields
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| `image` | file | ✓ | Image file to analyze |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"width": 1920,
|
|
||||||
"height": 1080,
|
|
||||||
"pixels": 2073600,
|
|
||||||
"capacity_bytes": 776970,
|
|
||||||
"capacity_kb": 758
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Response Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `width` | integer | Image width in pixels |
|
|
||||||
| `height` | integer | Image height in pixels |
|
|
||||||
| `pixels` | integer | Total pixel count |
|
|
||||||
| `capacity_bytes` | integer | Maximum message capacity (bytes) |
|
|
||||||
| `capacity_kb` | integer | Maximum message capacity (KB) |
|
|
||||||
|
|
||||||
#### cURL Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/image/info \
|
|
||||||
-F "image=@myimage.png"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Models
|
|
||||||
|
|
||||||
### GenerateRequest
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"use_pin": true,
|
|
||||||
"use_rsa": false,
|
|
||||||
"pin_length": 6,
|
|
||||||
"rsa_bits": 2048,
|
|
||||||
"words_per_phrase": 3
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GenerateResponse
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"phrases": {"Monday": "...", "Tuesday": "...", ...},
|
|
||||||
"pin": "123456",
|
|
||||||
"rsa_key_pem": "-----BEGIN PRIVATE KEY-----...",
|
|
||||||
"entropy": {"phrase": 33, "pin": 19, "rsa": 0, "total": 52}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### EncodeRequest
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "string",
|
|
||||||
"reference_photo_base64": "string",
|
|
||||||
"carrier_image_base64": "string",
|
|
||||||
"day_phrase": "string",
|
|
||||||
"pin": "string",
|
|
||||||
"rsa_key_base64": "string",
|
|
||||||
"rsa_password": "string",
|
|
||||||
"date_str": "YYYY-MM-DD"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### EncodeResponse
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"stego_image_base64": "string",
|
|
||||||
"filename": "string",
|
|
||||||
"capacity_used_percent": 12.4,
|
|
||||||
"date_used": "YYYY-MM-DD",
|
|
||||||
"day_of_week": "Saturday"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### DecodeRequest
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"stego_image_base64": "string",
|
|
||||||
"reference_photo_base64": "string",
|
|
||||||
"day_phrase": "string",
|
|
||||||
"pin": "string",
|
|
||||||
"rsa_key_base64": "string",
|
|
||||||
"rsa_password": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### DecodeResponse
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ImageInfoResponse
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"width": 1920,
|
|
||||||
"height": 1080,
|
|
||||||
"pixels": 2073600,
|
|
||||||
"capacity_bytes": 776970,
|
|
||||||
"capacity_kb": 758
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ErrorResponse
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "ErrorType",
|
|
||||||
"detail": "Error description"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### HTTP Status Codes
|
|
||||||
|
|
||||||
| Code | Meaning | Use Case |
|
|
||||||
|------|---------|----------|
|
|
||||||
| 200 | OK | Successful operation |
|
|
||||||
| 400 | Bad Request | Invalid input, capacity error |
|
|
||||||
| 401 | Unauthorized | Decryption failed (wrong credentials) |
|
|
||||||
| 500 | Internal Error | Unexpected server error |
|
|
||||||
|
|
||||||
### Error Response Format
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"detail": "Error message describing the problem"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Common Errors
|
|
||||||
|
|
||||||
| Status | Error | Solution |
|
|
||||||
|--------|-------|----------|
|
|
||||||
| 400 | "Must enable at least one of use_pin or use_rsa" | Set `use_pin` or `use_rsa` to true |
|
|
||||||
| 400 | "rsa_bits must be one of [2048, 3072, 4096]" | Use valid RSA key size |
|
|
||||||
| 400 | "Carrier image too small" | Use larger carrier image |
|
|
||||||
| 400 | "PIN must be 6-9 digits" | Fix PIN format |
|
|
||||||
| 401 | "Decryption failed. Check credentials." | Verify phrase, PIN, ref photo |
|
|
||||||
| 400 | "Message too long" | Reduce message size or use larger carrier |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Examples
|
|
||||||
|
|
||||||
### Python with requests
|
|
||||||
|
|
||||||
```python
|
|
||||||
import base64
|
|
||||||
import requests
|
|
||||||
|
|
||||||
BASE_URL = "http://localhost:8000"
|
|
||||||
|
|
||||||
# Generate credentials
|
|
||||||
response = requests.post(f"{BASE_URL}/generate", json={
|
|
||||||
"use_pin": True,
|
|
||||||
"use_rsa": False,
|
|
||||||
"words_per_phrase": 3
|
|
||||||
})
|
|
||||||
creds = response.json()
|
|
||||||
print(f"PIN: {creds['pin']}")
|
|
||||||
print(f"Monday phrase: {creds['phrases']['Monday']}")
|
|
||||||
|
|
||||||
# Encode using multipart
|
|
||||||
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
|
|
||||||
response = requests.post(f"{BASE_URL}/encode/multipart", files={
|
|
||||||
"reference_photo": ref,
|
|
||||||
"carrier": carrier,
|
|
||||||
}, data={
|
|
||||||
"message": "Secret message",
|
|
||||||
"day_phrase": "apple forest thunder",
|
|
||||||
"pin": "123456"
|
|
||||||
})
|
|
||||||
|
|
||||||
with open("stego.png", "wb") as f:
|
|
||||||
f.write(response.content)
|
|
||||||
|
|
||||||
# Decode using multipart
|
|
||||||
with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego:
|
|
||||||
response = requests.post(f"{BASE_URL}/decode/multipart", files={
|
|
||||||
"reference_photo": ref,
|
|
||||||
"stego_image": stego,
|
|
||||||
}, data={
|
|
||||||
"day_phrase": "apple forest thunder",
|
|
||||||
"pin": "123456"
|
|
||||||
})
|
|
||||||
|
|
||||||
print(f"Decoded: {response.json()['message']}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript/Node.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const FormData = require('form-data');
|
|
||||||
const fs = require('fs');
|
|
||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:8000';
|
|
||||||
|
|
||||||
async function encode() {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('message', 'Secret message');
|
|
||||||
form.append('day_phrase', 'apple forest thunder');
|
|
||||||
form.append('pin', '123456');
|
|
||||||
form.append('reference_photo', fs.createReadStream('reference.jpg'));
|
|
||||||
form.append('carrier', fs.createReadStream('carrier.png'));
|
|
||||||
|
|
||||||
const response = await axios.post(`${BASE_URL}/encode/multipart`, form, {
|
|
||||||
headers: form.getHeaders(),
|
|
||||||
responseType: 'arraybuffer'
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync('stego.png', response.data);
|
|
||||||
console.log('Encoded successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function decode() {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('day_phrase', 'apple forest thunder');
|
|
||||||
form.append('pin', '123456');
|
|
||||||
form.append('reference_photo', fs.createReadStream('reference.jpg'));
|
|
||||||
form.append('stego_image', fs.createReadStream('stego.png'));
|
|
||||||
|
|
||||||
const response = await axios.post(`${BASE_URL}/decode/multipart`, form, {
|
|
||||||
headers: form.getHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Decoded:', response.data.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
encode().then(decode);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Encode
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
|
|
||||||
writer.WriteField("message", "Secret message")
|
|
||||||
writer.WriteField("day_phrase", "apple forest thunder")
|
|
||||||
writer.WriteField("pin", "123456")
|
|
||||||
|
|
||||||
ref, _ := os.Open("reference.jpg")
|
|
||||||
refPart, _ := writer.CreateFormFile("reference_photo", "reference.jpg")
|
|
||||||
io.Copy(refPart, ref)
|
|
||||||
ref.Close()
|
|
||||||
|
|
||||||
carrier, _ := os.Open("carrier.png")
|
|
||||||
carrierPart, _ := writer.CreateFormFile("carrier", "carrier.png")
|
|
||||||
io.Copy(carrierPart, carrier)
|
|
||||||
carrier.Close()
|
|
||||||
|
|
||||||
writer.Close()
|
|
||||||
|
|
||||||
resp, _ := http.Post(
|
|
||||||
"http://localhost:8000/encode/multipart",
|
|
||||||
writer.FormDataContentType(),
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
|
|
||||||
stego, _ := os.Create("stego.png")
|
|
||||||
io.Copy(stego, resp.Body)
|
|
||||||
stego.Close()
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
fmt.Println("Encoded successfully")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Shell Script (Bash)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
BASE_URL="http://localhost:8000"
|
|
||||||
REF_PHOTO="reference.jpg"
|
|
||||||
CARRIER="carrier.png"
|
|
||||||
PHRASE="apple forest thunder"
|
|
||||||
PIN="123456"
|
|
||||||
MESSAGE="Secret message"
|
|
||||||
|
|
||||||
# Encode
|
|
||||||
echo "Encoding..."
|
|
||||||
curl -s -X POST "$BASE_URL/encode/multipart" \
|
|
||||||
-F "message=$MESSAGE" \
|
|
||||||
-F "day_phrase=$PHRASE" \
|
|
||||||
-F "pin=$PIN" \
|
|
||||||
-F "reference_photo=@$REF_PHOTO" \
|
|
||||||
-F "carrier=@$CARRIER" \
|
|
||||||
--output stego.png
|
|
||||||
|
|
||||||
echo "Encoded to stego.png"
|
|
||||||
|
|
||||||
# Decode
|
|
||||||
echo "Decoding..."
|
|
||||||
DECODED=$(curl -s -X POST "$BASE_URL/decode/multipart" \
|
|
||||||
-F "day_phrase=$PHRASE" \
|
|
||||||
-F "pin=$PIN" \
|
|
||||||
-F "reference_photo=@$REF_PHOTO" \
|
|
||||||
-F "stego_image=@stego.png" | jq -r '.message')
|
|
||||||
|
|
||||||
echo "Decoded message: $DECODED"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rate Limiting
|
|
||||||
|
|
||||||
The API does not implement rate limiting by default. For production:
|
|
||||||
|
|
||||||
1. **Reverse Proxy**: Use nginx or Caddy rate limiting
|
|
||||||
2. **Application Level**: Add FastAPI middleware
|
|
||||||
|
|
||||||
Example nginx rate limiting:
|
|
||||||
```nginx
|
|
||||||
limit_req_zone $binary_remote_addr zone=stegasoo:10m rate=10r/s;
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
limit_req zone=stegasoo burst=20 nodelay;
|
|
||||||
proxy_pass http://localhost:8000/;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### In Transit
|
|
||||||
|
|
||||||
- Use HTTPS in production
|
|
||||||
- Configure TLS at reverse proxy level
|
|
||||||
|
|
||||||
### Memory Usage
|
|
||||||
|
|
||||||
- Argon2id requires 256MB RAM per operation
|
|
||||||
- Concurrent requests can exhaust memory
|
|
||||||
- Limit workers based on available RAM
|
|
||||||
|
|
||||||
### Input Validation
|
|
||||||
|
|
||||||
The API validates:
|
|
||||||
- PIN format (6-9 digits, no leading zero)
|
|
||||||
- Message size (max 50KB)
|
|
||||||
- Image size (max 5MB file, ~4MP dimensions)
|
|
||||||
- RSA key validity
|
|
||||||
|
|
||||||
### Credential Handling
|
|
||||||
|
|
||||||
- Credentials are never logged
|
|
||||||
- No persistent storage of secrets
|
|
||||||
- Memory cleared after operations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Interactive Documentation
|
|
||||||
|
|
||||||
When the API is running, visit:
|
|
||||||
|
|
||||||
- **Swagger UI**: http://localhost:8000/docs
|
|
||||||
- **ReDoc**: http://localhost:8000/redoc
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [CLI Documentation](CLI.md) - Command-line interface
|
|
||||||
- [Web UI Documentation](WEB_UI.md) - Browser interface
|
|
||||||
- [README](README.md) - Project overview
|
|
||||||
- Image size (max 5MB file, ~4MP dimensions)
|
|
||||||
- RSA key validity
|
|
||||||
|
|
||||||
### Credential Handling
|
|
||||||
|
|
||||||
- Credentials are never logged
|
|
||||||
- No persistent storage of secrets
|
|
||||||
- Memory cleared after operations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Interactive Documentation
|
|
||||||
|
|
||||||
When the API is running, visit:
|
|
||||||
|
|
||||||
- **Swagger UI**: http://localhost:8000/docs
|
|
||||||
- **ReDoc**: http://localhost:8000/redoc
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [CLI Documentation](CLI.md) - Command-line interface
|
|
||||||
- [Web UI Documentation](WEB_UI.md) - Browser interface
|
|
||||||
- [README](README.md) - Project overview
|
|
||||||
634
frontends/CLI.md
@@ -1,634 +0,0 @@
|
|||||||
# Stegasoo CLI Documentation
|
|
||||||
|
|
||||||
Complete command-line interface reference for Stegasoo steganography operations.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Quick Start](#quick-start)
|
|
||||||
- [Commands](#commands)
|
|
||||||
- [generate](#generate-command)
|
|
||||||
- [encode](#encode-command)
|
|
||||||
- [decode](#decode-command)
|
|
||||||
- [info](#info-command)
|
|
||||||
- [Security Factors](#security-factors)
|
|
||||||
- [Workflow Examples](#workflow-examples)
|
|
||||||
- [Piping & Scripting](#piping--scripting)
|
|
||||||
- [Error Handling](#error-handling)
|
|
||||||
- [Exit Codes](#exit-codes)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### From PyPI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# CLI only
|
|
||||||
pip install stegasoo[cli]
|
|
||||||
|
|
||||||
# With all extras
|
|
||||||
pip install stegasoo[all]
|
|
||||||
```
|
|
||||||
|
|
||||||
### From Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/example/stegasoo.git
|
|
||||||
cd stegasoo
|
|
||||||
pip install -e ".[cli]"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verify Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
stegasoo --version
|
|
||||||
stegasoo --help
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Generate credentials (do this once, memorize results)
|
|
||||||
stegasoo generate --pin --words 3
|
|
||||||
|
|
||||||
# 2. Encode a message
|
|
||||||
stegasoo encode \
|
|
||||||
--ref secret_photo.jpg \
|
|
||||||
--carrier meme.png \
|
|
||||||
--phrase "apple forest thunder" \
|
|
||||||
--pin 123456 \
|
|
||||||
--message "Meet at midnight"
|
|
||||||
|
|
||||||
# 3. Decode a message
|
|
||||||
stegasoo decode \
|
|
||||||
--ref secret_photo.jpg \
|
|
||||||
--stego stego_abc123_20251227.png \
|
|
||||||
--phrase "apple forest thunder" \
|
|
||||||
--pin 123456
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### Generate Command
|
|
||||||
|
|
||||||
Generate credentials for encoding/decoding operations. Creates daily passphrases and optionally a PIN and/or RSA key.
|
|
||||||
|
|
||||||
#### Synopsis
|
|
||||||
|
|
||||||
```bash
|
|
||||||
stegasoo generate [OPTIONS]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Options
|
|
||||||
|
|
||||||
| Option | Short | Type | Default | Description |
|
|
||||||
|--------|-------|------|---------|-------------|
|
|
||||||
| `--pin/--no-pin` | | flag | `--pin` | Generate a PIN |
|
|
||||||
| `--rsa/--no-rsa` | | flag | `--no-rsa` | Generate an RSA key |
|
|
||||||
| `--pin-length` | | 6-9 | 6 | PIN length in digits |
|
|
||||||
| `--rsa-bits` | | choice | 2048 | RSA key size (2048, 3072, 4096) |
|
|
||||||
| `--words` | | 3-12 | 3 | Words per daily phrase |
|
|
||||||
| `--output` | `-o` | path | | Save RSA key to file |
|
|
||||||
| `--password` | `-p` | string | | Password for RSA key file |
|
|
||||||
| `--json` | | flag | | Output as JSON |
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
**Basic generation with PIN (default):**
|
|
||||||
```bash
|
|
||||||
stegasoo generate
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
════════════════════════════════════════════════════════════
|
|
||||||
STEGASOO CREDENTIALS
|
|
||||||
════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW
|
|
||||||
Do not screenshot or save to file!
|
|
||||||
|
|
||||||
─── STATIC PIN ───
|
|
||||||
847293
|
|
||||||
|
|
||||||
─── DAILY PHRASES ───
|
|
||||||
Monday │ abandon ability able
|
|
||||||
Tuesday │ actor actress actual
|
|
||||||
Wednesday │ advice aerobic affair
|
|
||||||
Thursday │ afraid again age
|
|
||||||
Friday │ agree ahead aim
|
|
||||||
Saturday │ airport aisle alarm
|
|
||||||
Sunday │ album alcohol alert
|
|
||||||
|
|
||||||
─── SECURITY ───
|
|
||||||
Phrase entropy: 33 bits
|
|
||||||
PIN entropy: 19 bits
|
|
||||||
Combined: 52 bits
|
|
||||||
+ photo entropy: 80-256 bits
|
|
||||||
```
|
|
||||||
|
|
||||||
**Generate with RSA key:**
|
|
||||||
```bash
|
|
||||||
stegasoo generate --rsa --rsa-bits 4096
|
|
||||||
```
|
|
||||||
|
|
||||||
**Save RSA key to encrypted file:**
|
|
||||||
```bash
|
|
||||||
stegasoo generate --rsa -o mykey.pem -p "mysecretpassword"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Maximum security (longer phrases + both factors):**
|
|
||||||
```bash
|
|
||||||
stegasoo generate --pin --rsa --words 6 --pin-length 9
|
|
||||||
```
|
|
||||||
|
|
||||||
**JSON output for scripting:**
|
|
||||||
```bash
|
|
||||||
stegasoo generate --json | jq '.phrases.Monday'
|
|
||||||
```
|
|
||||||
|
|
||||||
**RSA only (no PIN):**
|
|
||||||
```bash
|
|
||||||
stegasoo generate --no-pin --rsa -o key.pem -p "password123"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Encode Command
|
|
||||||
|
|
||||||
Encode a secret message into an image using steganography.
|
|
||||||
|
|
||||||
#### Synopsis
|
|
||||||
|
|
||||||
```bash
|
|
||||||
stegasoo encode [OPTIONS]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Options
|
|
||||||
|
|
||||||
| Option | Short | Type | Required | Description |
|
|
||||||
|--------|-------|------|----------|-------------|
|
|
||||||
| `--ref` | `-r` | path | ✓ | Reference photo (shared secret) |
|
|
||||||
| `--carrier` | `-c` | path | ✓ | Carrier image to hide message in |
|
|
||||||
| `--phrase` | `-p` | string | ✓ | Today's passphrase |
|
|
||||||
| `--message` | `-m` | string | | Message to encode |
|
|
||||||
| `--message-file` | `-f` | path | | Read message from file |
|
|
||||||
| `--pin` | | string | * | Static PIN (6-9 digits) |
|
|
||||||
| `--key` | `-k` | path | * | RSA key file |
|
|
||||||
| `--key-password` | | string | | Password for RSA key |
|
|
||||||
| `--output` | `-o` | path | | Output filename |
|
|
||||||
| `--date` | | YYYY-MM-DD | | Date override |
|
|
||||||
| `--quiet` | `-q` | flag | | Suppress output |
|
|
||||||
|
|
||||||
\* At least one of `--pin` or `--key` is required.
|
|
||||||
|
|
||||||
#### Message Input Methods
|
|
||||||
|
|
||||||
1. **Command line argument:**
|
|
||||||
```bash
|
|
||||||
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "Secret message"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **From file:**
|
|
||||||
```bash
|
|
||||||
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -f message.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **From stdin (pipe):**
|
|
||||||
```bash
|
|
||||||
echo "Secret message" | stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
**Basic encoding with PIN:**
|
|
||||||
```bash
|
|
||||||
stegasoo encode \
|
|
||||||
--ref photos/vacation.jpg \
|
|
||||||
--carrier memes/funny_cat.png \
|
|
||||||
--phrase "correct horse battery" \
|
|
||||||
--pin 847293 \
|
|
||||||
--message "The package arrives Tuesday"
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
✓ Encoded successfully!
|
|
||||||
Output: a1b2c3d4_20251227.png
|
|
||||||
Size: 245,832 bytes
|
|
||||||
Capacity used: 12.4%
|
|
||||||
Date: 2025-12-27
|
|
||||||
```
|
|
||||||
|
|
||||||
**With RSA key:**
|
|
||||||
```bash
|
|
||||||
stegasoo encode \
|
|
||||||
-r reference.jpg \
|
|
||||||
-c carrier.png \
|
|
||||||
-p "apple forest thunder" \
|
|
||||||
-k mykey.pem \
|
|
||||||
--key-password "secretpassword" \
|
|
||||||
-m "Encrypted with RSA"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Both PIN and RSA (maximum security):**
|
|
||||||
```bash
|
|
||||||
stegasoo encode \
|
|
||||||
-r ref.jpg \
|
|
||||||
-c carrier.png \
|
|
||||||
-p "word1 word2 word3" \
|
|
||||||
--pin 123456 \
|
|
||||||
-k mykey.pem \
|
|
||||||
--key-password "pass" \
|
|
||||||
-m "Double-locked message"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Custom output filename:**
|
|
||||||
```bash
|
|
||||||
stegasoo encode \
|
|
||||||
-r ref.jpg \
|
|
||||||
-c carrier.png \
|
|
||||||
-p "phrase words here" \
|
|
||||||
--pin 123456 \
|
|
||||||
-m "Message" \
|
|
||||||
-o holiday_photo.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**Encoding with specific date (for testing):**
|
|
||||||
```bash
|
|
||||||
stegasoo encode \
|
|
||||||
-r ref.jpg \
|
|
||||||
-c carrier.png \
|
|
||||||
-p "monday phrase here" \
|
|
||||||
--pin 123456 \
|
|
||||||
-m "Message" \
|
|
||||||
--date 2025-12-29
|
|
||||||
```
|
|
||||||
|
|
||||||
**Long message from file:**
|
|
||||||
```bash
|
|
||||||
stegasoo encode \
|
|
||||||
-r ref.jpg \
|
|
||||||
-c large_image.png \
|
|
||||||
-p "phrase" \
|
|
||||||
--pin 123456 \
|
|
||||||
-f secret_document.txt \
|
|
||||||
-o output.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**Quiet mode for scripting:**
|
|
||||||
```bash
|
|
||||||
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "msg" -q -o out.png
|
|
||||||
# No output, just creates the file
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Decode Command
|
|
||||||
|
|
||||||
Decode a secret message from a stego image.
|
|
||||||
|
|
||||||
#### Synopsis
|
|
||||||
|
|
||||||
```bash
|
|
||||||
stegasoo decode [OPTIONS]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Options
|
|
||||||
|
|
||||||
| Option | Short | Type | Required | Description |
|
|
||||||
|--------|-------|------|----------|-------------|
|
|
||||||
| `--ref` | `-r` | path | ✓ | Reference photo (same as encoding) |
|
|
||||||
| `--stego` | `-s` | path | ✓ | Stego image to decode |
|
|
||||||
| `--phrase` | `-p` | string | ✓ | Passphrase for the encoding day |
|
|
||||||
| `--pin` | | string | * | Static PIN |
|
|
||||||
| `--key` | `-k` | path | * | RSA key file |
|
|
||||||
| `--key-password` | | string | | Password for RSA key |
|
|
||||||
| `--output` | `-o` | path | | Save message to file |
|
|
||||||
| `--quiet` | `-q` | flag | | Output only the message |
|
|
||||||
|
|
||||||
\* Must provide the same security factors used during encoding.
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
**Basic decoding with PIN:**
|
|
||||||
```bash
|
|
||||||
stegasoo decode \
|
|
||||||
--ref photos/vacation.jpg \
|
|
||||||
--stego received_image.png \
|
|
||||||
--phrase "correct horse battery" \
|
|
||||||
--pin 847293
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
✓ Decoded successfully!
|
|
||||||
|
|
||||||
The package arrives Tuesday
|
|
||||||
```
|
|
||||||
|
|
||||||
**With RSA key:**
|
|
||||||
```bash
|
|
||||||
stegasoo decode \
|
|
||||||
-r reference.jpg \
|
|
||||||
-s stego_image.png \
|
|
||||||
-p "apple forest thunder" \
|
|
||||||
-k mykey.pem \
|
|
||||||
--key-password "secretpassword"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Save decoded message to file:**
|
|
||||||
```bash
|
|
||||||
stegasoo decode \
|
|
||||||
-r ref.jpg \
|
|
||||||
-s stego.png \
|
|
||||||
-p "phrase" \
|
|
||||||
--pin 123456 \
|
|
||||||
-o decoded_message.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
✓ Decoded successfully!
|
|
||||||
Saved to: decoded_message.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
**Quiet mode (message only):**
|
|
||||||
```bash
|
|
||||||
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
The package arrives Tuesday
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pipe to another command:**
|
|
||||||
```bash
|
|
||||||
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | gpg --decrypt
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Info Command
|
|
||||||
|
|
||||||
Display information about an image's capacity and embedded date.
|
|
||||||
|
|
||||||
#### Synopsis
|
|
||||||
|
|
||||||
```bash
|
|
||||||
stegasoo info IMAGE
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Arguments
|
|
||||||
|
|
||||||
| Argument | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `IMAGE` | path | Path to image file |
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
**Check carrier image capacity:**
|
|
||||||
```bash
|
|
||||||
stegasoo info vacation_photo.png
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
Image: vacation_photo.png
|
|
||||||
Dimensions: 1920 × 1080
|
|
||||||
Pixels: 2,073,600
|
|
||||||
Mode: RGB
|
|
||||||
Format: PNG
|
|
||||||
Capacity: ~776,970 bytes (758 KB)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check stego image (shows encoding date):**
|
|
||||||
```bash
|
|
||||||
stegasoo info stego_a1b2c3d4_20251227.png
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
```
|
|
||||||
Image: stego_a1b2c3d4_20251227.png
|
|
||||||
Dimensions: 1920 × 1080
|
|
||||||
Pixels: 2,073,600
|
|
||||||
Mode: RGB
|
|
||||||
Format: PNG
|
|
||||||
Capacity: ~776,970 bytes (758 KB)
|
|
||||||
Embed date: 2025-12-27 (Saturday)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Factors
|
|
||||||
|
|
||||||
Stegasoo uses multiple authentication factors:
|
|
||||||
|
|
||||||
| Factor | Description | Entropy |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| Reference Photo | A photo both parties have | ~80-256 bits |
|
|
||||||
| Day Phrase | Changes daily (e.g., 3 BIP-39 words) | ~33 bits (3 words) |
|
|
||||||
| Static PIN | Same every day (6-9 digits) | ~20 bits (6 digits) |
|
|
||||||
| RSA Key | Shared key file | ~128 bits effective |
|
|
||||||
|
|
||||||
### Minimum Requirements
|
|
||||||
|
|
||||||
- At least one of PIN or RSA key must be provided
|
|
||||||
- Reference photo is always required
|
|
||||||
- Day phrase is always required
|
|
||||||
|
|
||||||
### Security Configurations
|
|
||||||
|
|
||||||
| Configuration | Entropy (excl. photo) | Use Case |
|
|
||||||
|--------------|----------------------|----------|
|
|
||||||
| 3-word phrase + 6-digit PIN | ~53 bits | Casual use |
|
|
||||||
| 6-word phrase + 9-digit PIN | ~96 bits | Standard security |
|
|
||||||
| 3-word phrase + RSA 2048 | ~161 bits | File-based auth |
|
|
||||||
| 6-word phrase + PIN + RSA | ~224 bits | Maximum security |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Workflow Examples
|
|
||||||
|
|
||||||
### Daily Secure Communication
|
|
||||||
|
|
||||||
**Setup (once):**
|
|
||||||
```bash
|
|
||||||
# Both parties generate same credentials
|
|
||||||
stegasoo generate --pin --words 3
|
|
||||||
|
|
||||||
# Or share RSA key securely
|
|
||||||
stegasoo generate --rsa -o shared_key.pem -p "agreedpassword"
|
|
||||||
# Securely transfer shared_key.pem to recipient
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sender (daily):**
|
|
||||||
```bash
|
|
||||||
# Get today's phrase from your memorized list
|
|
||||||
TODAY_PHRASE="monday phrase words"
|
|
||||||
|
|
||||||
# Encode message
|
|
||||||
stegasoo encode \
|
|
||||||
-r our_shared_photo.jpg \
|
|
||||||
-c random_meme.png \
|
|
||||||
-p "$TODAY_PHRASE" \
|
|
||||||
--pin 847293 \
|
|
||||||
-m "Meeting moved to 3pm"
|
|
||||||
|
|
||||||
# Share output image via normal channels (email, chat, etc.)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Recipient (daily):**
|
|
||||||
```bash
|
|
||||||
# Use the phrase for the day the message was SENT
|
|
||||||
stegasoo decode \
|
|
||||||
-r our_shared_photo.jpg \
|
|
||||||
-s received_image.png \
|
|
||||||
-p "monday phrase words" \
|
|
||||||
--pin 847293
|
|
||||||
```
|
|
||||||
|
|
||||||
### Batch Processing
|
|
||||||
|
|
||||||
**Encode multiple messages:**
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
PHRASE="apple forest thunder"
|
|
||||||
PIN="123456"
|
|
||||||
REF="reference.jpg"
|
|
||||||
|
|
||||||
for file in messages/*.txt; do
|
|
||||||
name=$(basename "$file" .txt)
|
|
||||||
stegasoo encode \
|
|
||||||
-r "$REF" \
|
|
||||||
-c "carriers/${name}.png" \
|
|
||||||
-p "$PHRASE" \
|
|
||||||
--pin "$PIN" \
|
|
||||||
-f "$file" \
|
|
||||||
-o "output/${name}_stego.png" \
|
|
||||||
-q
|
|
||||||
echo "Encoded: $name"
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
### Archive with Date Preservation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Encode with specific date for archival
|
|
||||||
stegasoo encode \
|
|
||||||
-r ref.jpg \
|
|
||||||
-c carrier.png \
|
|
||||||
-p "archive phrase words" \
|
|
||||||
--pin 123456 \
|
|
||||||
-m "Historical record" \
|
|
||||||
--date 2025-01-15 \
|
|
||||||
-o archive_2025-01-15.png
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Piping & Scripting
|
|
||||||
|
|
||||||
### Stdin/Stdout Support
|
|
||||||
|
|
||||||
**Encode from pipe:**
|
|
||||||
```bash
|
|
||||||
cat secret.txt | stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -o out.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**Decode to pipe:**
|
|
||||||
```bash
|
|
||||||
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | less
|
|
||||||
```
|
|
||||||
|
|
||||||
**Chain with encryption:**
|
|
||||||
```bash
|
|
||||||
# Encode GPG-encrypted content
|
|
||||||
gpg -e -r recipient@email.com secret.txt
|
|
||||||
cat secret.txt.gpg | base64 | stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456
|
|
||||||
|
|
||||||
# Decode and decrypt
|
|
||||||
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | base64 -d | gpg -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON Output for Scripts
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get credentials as JSON
|
|
||||||
creds=$(stegasoo generate --json)
|
|
||||||
|
|
||||||
# Extract specific fields
|
|
||||||
pin=$(echo "$creds" | jq -r '.pin')
|
|
||||||
monday=$(echo "$creds" | jq -r '.phrases.Monday')
|
|
||||||
entropy=$(echo "$creds" | jq -r '.entropy.total')
|
|
||||||
|
|
||||||
echo "PIN: $pin"
|
|
||||||
echo "Monday phrase: $monday"
|
|
||||||
echo "Total entropy: $entropy bits"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling in Scripts
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if ! stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q 2>/dev/null; then
|
|
||||||
echo "Decryption failed - check credentials"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Common Errors
|
|
||||||
|
|
||||||
| Error | Cause | Solution |
|
|
||||||
|-------|-------|----------|
|
|
||||||
| "Must provide --pin or --key" | No security factor given | Add `--pin` or `--key` option |
|
|
||||||
| "PIN must be 6-9 digits" | Invalid PIN format | Use numeric PIN, 6-9 chars |
|
|
||||||
| "PIN cannot start with zero" | Leading zero in PIN | Use PIN starting with 1-9 |
|
|
||||||
| "Carrier image too small" | Message exceeds capacity | Use larger carrier image |
|
|
||||||
| "Decryption failed" | Wrong credentials | Verify phrase, PIN, ref photo |
|
|
||||||
| "RSA key is password-protected" | Missing key password | Add `--key-password` option |
|
|
||||||
|
|
||||||
### Troubleshooting Decryption Failures
|
|
||||||
|
|
||||||
1. **Check the encoding date:** The filename often contains the date (e.g., `_20251227`)
|
|
||||||
2. **Use correct phrase:** The phrase must match the day the message was encoded, not today
|
|
||||||
3. **Verify reference photo:** Must be the exact same file, not a resized copy
|
|
||||||
4. **Check stego image:** Ensure it wasn't resized, recompressed, or converted
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Exit Codes
|
|
||||||
|
|
||||||
| Code | Meaning |
|
|
||||||
|------|---------|
|
|
||||||
| 0 | Success |
|
|
||||||
| 1 | General error |
|
|
||||||
| 2 | Invalid arguments/options |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `PYTHONPATH` | Include `src/` for development |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [API Documentation](API.md) - REST API reference
|
|
||||||
- [Web UI Documentation](WEB_UI.md) - Browser interface guide
|
|
||||||
- [README](README.md) - Project overview and security model
|
|
||||||
@@ -1,739 +0,0 @@
|
|||||||
# Stegasoo Web UI Documentation
|
|
||||||
|
|
||||||
Complete guide for the Stegasoo web-based steganography interface.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Installation & Setup](#installation--setup)
|
|
||||||
- [Pages & Features](#pages--features)
|
|
||||||
- [Home Page](#home-page)
|
|
||||||
- [Generate Credentials](#generate-credentials)
|
|
||||||
- [Encode Message](#encode-message)
|
|
||||||
- [Decode Message](#decode-message)
|
|
||||||
- [About Page](#about-page)
|
|
||||||
- [User Interface Guide](#user-interface-guide)
|
|
||||||
- [Workflow Examples](#workflow-examples)
|
|
||||||
- [Security Features](#security-features)
|
|
||||||
- [Configuration](#configuration)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
- [Mobile Support](#mobile-support)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Stegasoo Web UI provides a user-friendly browser-based interface for:
|
|
||||||
|
|
||||||
- **Generating** secure credentials (phrases, PINs, RSA keys)
|
|
||||||
- **Encoding** secret messages into images
|
|
||||||
- **Decoding** hidden messages from images
|
|
||||||
- **Learning** about the security model
|
|
||||||
|
|
||||||
Built with Flask, Bootstrap 5, and a modern dark theme.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- ✅ Drag-and-drop file uploads
|
|
||||||
- ✅ Image previews
|
|
||||||
- ✅ Client-side date detection
|
|
||||||
- ✅ Native sharing (Web Share API)
|
|
||||||
- ✅ Responsive design (mobile-friendly)
|
|
||||||
- ✅ Password-protected RSA key downloads
|
|
||||||
- ✅ Real-time entropy calculations
|
|
||||||
- ✅ Automatic file cleanup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation & Setup
|
|
||||||
|
|
||||||
### From PyPI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install stegasoo[web]
|
|
||||||
```
|
|
||||||
|
|
||||||
### From Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/example/stegasoo.git
|
|
||||||
cd stegasoo
|
|
||||||
pip install -e ".[web]"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running the Server
|
|
||||||
|
|
||||||
**Development:**
|
|
||||||
```bash
|
|
||||||
cd frontends/web
|
|
||||||
python app.py
|
|
||||||
```
|
|
||||||
Server starts at http://localhost:5000
|
|
||||||
|
|
||||||
**Production with Gunicorn:**
|
|
||||||
```bash
|
|
||||||
cd frontends/web
|
|
||||||
gunicorn --bind 0.0.0.0:5000 --workers 2 --threads 4 --timeout 60 app:app
|
|
||||||
```
|
|
||||||
|
|
||||||
**Docker:**
|
|
||||||
```bash
|
|
||||||
docker-compose up web
|
|
||||||
```
|
|
||||||
|
|
||||||
### First-Time Setup
|
|
||||||
|
|
||||||
1. Navigate to http://localhost:5000
|
|
||||||
2. Click "Generate" to create your credentials
|
|
||||||
3. **Memorize** your phrases and PIN
|
|
||||||
4. Share credentials securely with your communication partner
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pages & Features
|
|
||||||
|
|
||||||
### Home Page
|
|
||||||
|
|
||||||
**URL:** `/`
|
|
||||||
|
|
||||||
The landing page introduces Stegasoo and provides quick access to all features.
|
|
||||||
|
|
||||||
#### Main Actions
|
|
||||||
|
|
||||||
| Card | Description | Link |
|
|
||||||
|------|-------------|------|
|
|
||||||
| **Encode Message** | Hide a secret in an image | `/encode` |
|
|
||||||
| **Decode Message** | Extract a hidden message | `/decode` |
|
|
||||||
| **Generate Keys** | Create new credentials | `/generate` |
|
|
||||||
|
|
||||||
#### "How It Works" Section
|
|
||||||
|
|
||||||
Explains the three key components:
|
|
||||||
1. **Reference Photo** - Shared secret image
|
|
||||||
2. **Day Phrase** - Changes daily
|
|
||||||
3. **Static PIN** - Same every day
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Generate Credentials
|
|
||||||
|
|
||||||
**URL:** `/generate`
|
|
||||||
|
|
||||||
Create a new set of credentials for steganography operations.
|
|
||||||
|
|
||||||
#### Configuration Options
|
|
||||||
|
|
||||||
| Option | Range | Default | Description |
|
|
||||||
|--------|-------|---------|-------------|
|
|
||||||
| Words per phrase | 3-12 | 3 | BIP-39 words per daily phrase |
|
|
||||||
| Use PIN | on/off | on | Generate a numeric PIN |
|
|
||||||
| PIN length | 6-9 | 6 | Digits in the PIN |
|
|
||||||
| Use RSA Key | on/off | off | Generate an RSA key pair |
|
|
||||||
| RSA key size | 2048/3072/4096 | 2048 | Key size in bits |
|
|
||||||
|
|
||||||
#### Entropy Calculator
|
|
||||||
|
|
||||||
The UI displays real-time entropy calculations:
|
|
||||||
|
|
||||||
```
|
|
||||||
Estimated entropy: ~53 bits
|
|
||||||
[==========> ] Good for most use cases
|
|
||||||
• Reference photo adds ~80-256 bits more
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Generated Output
|
|
||||||
|
|
||||||
After clicking "Generate Credentials":
|
|
||||||
|
|
||||||
**Static PIN** (if enabled):
|
|
||||||
```
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ 8 4 7 2 9 3 │
|
|
||||||
└─────────────────────┘
|
|
||||||
Use this 6-digit PIN every day
|
|
||||||
```
|
|
||||||
|
|
||||||
**Daily Phrases:**
|
|
||||||
```
|
|
||||||
Day │ Phrase
|
|
||||||
─────────────────────────────────────────
|
|
||||||
Monday │ abandon ability able
|
|
||||||
Tuesday │ actor actress actual
|
|
||||||
Wednesday │ advice aerobic affair
|
|
||||||
Thursday │ afraid again age
|
|
||||||
Friday │ agree ahead aim
|
|
||||||
Saturday │ airport aisle alarm
|
|
||||||
Sunday │ album alcohol alert
|
|
||||||
```
|
|
||||||
|
|
||||||
**RSA Key** (if enabled):
|
|
||||||
- Copy to clipboard button
|
|
||||||
- Download as password-protected .pem file
|
|
||||||
|
|
||||||
**Security Summary:**
|
|
||||||
```
|
|
||||||
Phrase entropy: 33 bits/phrase
|
|
||||||
PIN entropy: 19 bits/PIN
|
|
||||||
RSA entropy: 128 bits/RSA
|
|
||||||
─────────────────────────────
|
|
||||||
Total: 180 bits
|
|
||||||
+ reference photo (~80-256 bits) = 260+ bits combined
|
|
||||||
```
|
|
||||||
|
|
||||||
#### RSA Key Download
|
|
||||||
|
|
||||||
1. Click "Download as .pem"
|
|
||||||
2. Enter a password (minimum 8 characters)
|
|
||||||
3. Click "Download Protected Key"
|
|
||||||
4. Save the file securely
|
|
||||||
5. Share with your communication partner through a secure channel
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Encode Message
|
|
||||||
|
|
||||||
**URL:** `/encode`
|
|
||||||
|
|
||||||
Hide a secret message inside an image.
|
|
||||||
|
|
||||||
#### Input Fields
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| Reference Photo | Image file | ✓ | Your shared secret photo |
|
|
||||||
| Carrier Image | Image file | ✓ | Image to hide message in |
|
|
||||||
| Secret Message | Text | ✓ | Message to hide (max 50KB) |
|
|
||||||
| Day Phrase | Text | ✓ | Today's passphrase |
|
|
||||||
| PIN | Number | * | Your static PIN |
|
|
||||||
| RSA Key | .pem file | * | Your shared RSA key |
|
|
||||||
| RSA Key Password | Password | | Password for encrypted key |
|
|
||||||
|
|
||||||
\* At least one security factor (PIN or RSA Key) required.
|
|
||||||
|
|
||||||
#### Drag-and-Drop Upload
|
|
||||||
|
|
||||||
Both image upload zones support:
|
|
||||||
- Click to browse
|
|
||||||
- Drag and drop files
|
|
||||||
- Instant image preview
|
|
||||||
- File name display
|
|
||||||
|
|
||||||
#### Character Counter
|
|
||||||
|
|
||||||
```
|
|
||||||
Message: [ ]
|
|
||||||
1,234 / 50,000 characters 2%
|
|
||||||
```
|
|
||||||
|
|
||||||
Shows warning at 80% capacity.
|
|
||||||
|
|
||||||
#### Day Detection
|
|
||||||
|
|
||||||
The page automatically detects your local day of week and updates the label:
|
|
||||||
```
|
|
||||||
Saturday's Phrase: [ ]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Encoding Process
|
|
||||||
|
|
||||||
1. Fill in all required fields
|
|
||||||
2. Click "Encode Message"
|
|
||||||
3. Wait for processing (shows spinner)
|
|
||||||
4. Redirected to result page
|
|
||||||
|
|
||||||
#### Result Page
|
|
||||||
|
|
||||||
**URL:** `/encode/result/<file_id>`
|
|
||||||
|
|
||||||
After successful encoding:
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────────────────────────────────┐
|
|
||||||
│ ✓ Message Encoded Successfully! │
|
|
||||||
│ │
|
|
||||||
│ 📄 a1b2c3d4_20251227.png │
|
|
||||||
│ Your secret message is hidden │
|
|
||||||
│ in this image │
|
|
||||||
│ │
|
|
||||||
│ [ Download Image ] │
|
|
||||||
│ [ Share Image ] │
|
|
||||||
│ │
|
|
||||||
│ ⚠️ File expires in 5 minutes. │
|
|
||||||
│ Download or share now. │
|
|
||||||
│ │
|
|
||||||
│ [ Encode Another Message ] │
|
|
||||||
└────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Share Options:**
|
|
||||||
|
|
||||||
1. **Native Share** (mobile/supported browsers):
|
|
||||||
- Uses Web Share API
|
|
||||||
- Opens system share sheet
|
|
||||||
- Can share directly to apps
|
|
||||||
|
|
||||||
2. **Fallback Share** (desktop):
|
|
||||||
- Email link
|
|
||||||
- Telegram link
|
|
||||||
- WhatsApp link
|
|
||||||
- Copy link to clipboard
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Decode Message
|
|
||||||
|
|
||||||
**URL:** `/decode`
|
|
||||||
|
|
||||||
Extract a hidden message from a stego image.
|
|
||||||
|
|
||||||
#### Input Fields
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| Reference Photo | Image file | ✓ | Same photo used for encoding |
|
|
||||||
| Stego Image | Image file | ✓ | Image containing hidden message |
|
|
||||||
| Day Phrase | Text | ✓ | Phrase for the **encoding** day |
|
|
||||||
| PIN | Number | * | Same PIN used for encoding |
|
|
||||||
| RSA Key | .pem file | * | Same RSA key used for encoding |
|
|
||||||
| RSA Key Password | Password | | Password for encrypted key |
|
|
||||||
|
|
||||||
\* Must match security factors used during encoding.
|
|
||||||
|
|
||||||
#### Date Detection from Filename
|
|
||||||
|
|
||||||
When you upload a stego image with a date in the filename (e.g., `stego_20251227.png`), the UI:
|
|
||||||
1. Extracts the date
|
|
||||||
2. Determines the day of week
|
|
||||||
3. Updates the phrase label: "Saturday's Phrase"
|
|
||||||
|
|
||||||
This helps you use the correct daily phrase.
|
|
||||||
|
|
||||||
#### Decoding Process
|
|
||||||
|
|
||||||
1. Fill in all required fields
|
|
||||||
2. Click "Decode Message"
|
|
||||||
3. Wait for processing
|
|
||||||
4. View decoded message on same page
|
|
||||||
|
|
||||||
#### Successful Decode
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────────────────────────────────┐
|
|
||||||
│ ✓ Message Decrypted Successfully! │
|
|
||||||
│ │
|
|
||||||
│ Decoded Message: │
|
|
||||||
│ ┌──────────────────────────────────┐ │
|
|
||||||
│ │ Meet at midnight. The package │ │
|
|
||||||
│ │ will be under the bridge. │ │
|
|
||||||
│ └──────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
│ [ Decode Another Message ] │
|
|
||||||
└────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Troubleshooting Tips
|
|
||||||
|
|
||||||
The page includes built-in troubleshooting guidance:
|
|
||||||
|
|
||||||
- ✓ Use the **exact same reference photo** file
|
|
||||||
- ✓ Use the phrase for the **encoding day**, not today
|
|
||||||
- ✓ Provide the **same security factors** used during encoding
|
|
||||||
- ✓ Ensure the stego image hasn't been **resized or recompressed**
|
|
||||||
- ✓ If using RSA key, verify the **password is correct**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### About Page
|
|
||||||
|
|
||||||
**URL:** `/about`
|
|
||||||
|
|
||||||
Learn about Stegasoo's security model and best practices.
|
|
||||||
|
|
||||||
#### Sections
|
|
||||||
|
|
||||||
**System Status:**
|
|
||||||
- Argon2id availability (vs PBKDF2 fallback)
|
|
||||||
- AES-256-GCM encryption status
|
|
||||||
|
|
||||||
**Security Model Table:**
|
|
||||||
|
|
||||||
| Component | Entropy | Purpose |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| Reference Photo | ~80-256 bits | Something you have |
|
|
||||||
| 3-Word Phrase | ~33 bits | Something you know (daily) |
|
|
||||||
| 6-Digit PIN | ~20 bits | Something you know (static) |
|
|
||||||
| Date | N/A | Automatic key rotation |
|
|
||||||
| **Combined** | **133+ bits** | **Beyond brute force** |
|
|
||||||
|
|
||||||
**Attack Resistance:**
|
|
||||||
|
|
||||||
What attackers can't do:
|
|
||||||
- Brute force (2^133 combinations)
|
|
||||||
- Use rainbow tables (random salt)
|
|
||||||
- Detect hidden data (random pixels)
|
|
||||||
- Use GPU farms (256MB RAM per attempt)
|
|
||||||
|
|
||||||
Real threats:
|
|
||||||
- Social engineering
|
|
||||||
- Physical device access
|
|
||||||
- Malware/keyloggers
|
|
||||||
- Shoulder surfing
|
|
||||||
|
|
||||||
**Best Practices:**
|
|
||||||
|
|
||||||
Do:
|
|
||||||
- Memorize phrases and PIN
|
|
||||||
- Use reference photo both parties have
|
|
||||||
- Use different carrier images each time
|
|
||||||
- Share stego images through normal channels
|
|
||||||
|
|
||||||
Don't:
|
|
||||||
- Transmit the reference photo
|
|
||||||
- Reuse carrier images
|
|
||||||
- Store credentials digitally
|
|
||||||
- Resize/recompress stego images
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Interface Guide
|
|
||||||
|
|
||||||
### Navigation
|
|
||||||
|
|
||||||
The navbar provides quick access to all pages:
|
|
||||||
|
|
||||||
```
|
|
||||||
[Logo] Stegasoo Home | Encode | Decode | Generate | About
|
|
||||||
```
|
|
||||||
|
|
||||||
### Color Scheme
|
|
||||||
|
|
||||||
| Element | Color | Purpose |
|
|
||||||
|---------|-------|---------|
|
|
||||||
| Background | Dark gradient | Reduce eye strain |
|
|
||||||
| Cards | Semi-transparent | Visual hierarchy |
|
|
||||||
| Headers | Purple gradient | Brand identity |
|
|
||||||
| Success | Green | Positive actions |
|
|
||||||
| Warning | Yellow | Caution messages |
|
|
||||||
| Error | Red | Error states |
|
|
||||||
|
|
||||||
### Form Validation
|
|
||||||
|
|
||||||
- Real-time validation feedback
|
|
||||||
- Clear error messages in alerts
|
|
||||||
- Required field indicators
|
|
||||||
- Input constraints (max length, format)
|
|
||||||
|
|
||||||
### Loading States
|
|
||||||
|
|
||||||
During long operations:
|
|
||||||
- Button shows spinner
|
|
||||||
- Button text changes (e.g., "Encoding...")
|
|
||||||
- Button is disabled to prevent double-submit
|
|
||||||
|
|
||||||
### Flash Messages
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────────────────────┐
|
|
||||||
│ ✓ Credentials Generated! [×] │
|
|
||||||
└──────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
Types:
|
|
||||||
- Success (green) - Operation completed
|
|
||||||
- Error (red) - Operation failed
|
|
||||||
- Warning (yellow) - Caution needed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Workflow Examples
|
|
||||||
|
|
||||||
### First-Time Setup (Both Parties)
|
|
||||||
|
|
||||||
**Party A:**
|
|
||||||
1. Go to `/generate`
|
|
||||||
2. Configure: PIN ✓, 3 words, 6 digits
|
|
||||||
3. Click "Generate Credentials"
|
|
||||||
4. **Write down** phrases and PIN on paper
|
|
||||||
5. **Memorize** over the next few days
|
|
||||||
6. Destroy the paper
|
|
||||||
|
|
||||||
**Share with Party B (in person or secure channel):**
|
|
||||||
- The 7 daily phrases
|
|
||||||
- The PIN
|
|
||||||
- The reference photo file (if not already shared)
|
|
||||||
|
|
||||||
### Sending a Secret Message
|
|
||||||
|
|
||||||
1. Go to `/encode`
|
|
||||||
2. Upload your shared reference photo
|
|
||||||
3. Upload any carrier image (meme, vacation photo, etc.)
|
|
||||||
4. Type your secret message
|
|
||||||
5. Enter today's phrase (check your memory!)
|
|
||||||
6. Enter your PIN
|
|
||||||
7. Click "Encode Message"
|
|
||||||
8. Download or share the resulting image
|
|
||||||
9. Send via any channel (email, social media, chat)
|
|
||||||
|
|
||||||
### Receiving a Secret Message
|
|
||||||
|
|
||||||
1. Receive the stego image through any channel
|
|
||||||
2. Go to `/decode`
|
|
||||||
3. Upload the same reference photo
|
|
||||||
4. Upload the received stego image
|
|
||||||
5. Note the date in the filename (e.g., `_20251227`)
|
|
||||||
6. Enter the phrase for **that day** (not today!)
|
|
||||||
7. Enter the PIN
|
|
||||||
8. Click "Decode Message"
|
|
||||||
9. Read the secret message
|
|
||||||
|
|
||||||
### Changing Credentials
|
|
||||||
|
|
||||||
To rotate to new credentials:
|
|
||||||
1. Both parties generate new credentials together
|
|
||||||
2. Agree on a cutover date
|
|
||||||
3. Messages encoded before cutover use old credentials
|
|
||||||
4. Messages encoded after cutover use new credentials
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Features
|
|
||||||
|
|
||||||
### Client-Side Security
|
|
||||||
|
|
||||||
| Feature | Implementation |
|
|
||||||
|---------|----------------|
|
|
||||||
| Local date detection | JavaScript `Date()` object |
|
|
||||||
| No credential storage | Nothing saved in browser |
|
|
||||||
| Automatic cleanup | Files deleted after 5 minutes |
|
|
||||||
| HTTPS support | Configure at server level |
|
|
||||||
|
|
||||||
### Server-Side Security
|
|
||||||
|
|
||||||
| Feature | Implementation |
|
|
||||||
|---------|----------------|
|
|
||||||
| Memory-hard KDF | Argon2id (256MB RAM) |
|
|
||||||
| Authenticated encryption | AES-256-GCM |
|
|
||||||
| Random salt | Per-message salt |
|
|
||||||
| Temporary storage | In-memory, auto-expiring |
|
|
||||||
| Input validation | All inputs validated |
|
|
||||||
| File size limits | 5MB max upload |
|
|
||||||
|
|
||||||
### File Security
|
|
||||||
|
|
||||||
| Aspect | Protection |
|
|
||||||
|--------|------------|
|
|
||||||
| Upload location | `/tmp/stego_uploads` (Docker) |
|
|
||||||
| Storage duration | 5 minutes maximum |
|
|
||||||
| Access control | Random 16-byte file ID |
|
|
||||||
| Cleanup | Automatic + manual |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `FLASK_ENV` | production | Flask environment |
|
|
||||||
| `PYTHONPATH` | - | Include `src/` for development |
|
|
||||||
|
|
||||||
### Application Limits
|
|
||||||
|
|
||||||
| Limit | Value | Config Location |
|
|
||||||
|-------|-------|-----------------|
|
|
||||||
| Max file upload | 5 MB | `app.config['MAX_CONTENT_LENGTH']` |
|
|
||||||
| File expiry | 5 minutes | `TEMP_FILE_EXPIRY` |
|
|
||||||
| Max image pixels | 4 MP | `stegasoo.constants` |
|
|
||||||
| Max message size | 50 KB | `stegasoo.constants` |
|
|
||||||
| PIN length | 6-9 digits | `stegasoo.constants` |
|
|
||||||
|
|
||||||
### Production Deployment
|
|
||||||
|
|
||||||
**With Gunicorn:**
|
|
||||||
```bash
|
|
||||||
gunicorn \
|
|
||||||
--bind 0.0.0.0:5000 \
|
|
||||||
--workers 2 \
|
|
||||||
--threads 4 \
|
|
||||||
--timeout 60 \
|
|
||||||
app:app
|
|
||||||
```
|
|
||||||
|
|
||||||
**Worker Calculation:**
|
|
||||||
- Each encode/decode uses ~256MB RAM (Argon2)
|
|
||||||
- Formula: `workers = (available_RAM - 512MB) / 256MB`
|
|
||||||
|
|
||||||
**With Nginx (reverse proxy):**
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name stegasoo.example.com;
|
|
||||||
|
|
||||||
client_max_body_size 10M;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_read_timeout 120s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**With Docker Compose:**
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
target: web
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 512M
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
#### "Decryption failed"
|
|
||||||
|
|
||||||
**Causes:**
|
|
||||||
- Wrong day phrase
|
|
||||||
- Wrong PIN
|
|
||||||
- Different reference photo
|
|
||||||
- Stego image was modified
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
1. Check the date in the stego filename
|
|
||||||
2. Use the phrase for that specific day
|
|
||||||
3. Verify you're using the original reference photo
|
|
||||||
4. Ensure the stego image wasn't resized/recompressed
|
|
||||||
|
|
||||||
#### "Carrier image too small"
|
|
||||||
|
|
||||||
**Cause:** Message too large for carrier capacity
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
1. Use a larger carrier image (more pixels)
|
|
||||||
2. Shorten the message
|
|
||||||
3. Check capacity with `/info` command (CLI)
|
|
||||||
|
|
||||||
#### "You must provide at least a PIN or RSA Key"
|
|
||||||
|
|
||||||
**Cause:** No security factor selected
|
|
||||||
|
|
||||||
**Solution:** Enter a PIN and/or upload an RSA key
|
|
||||||
|
|
||||||
#### Upload fails silently
|
|
||||||
|
|
||||||
**Causes:**
|
|
||||||
- File too large (>5MB)
|
|
||||||
- Invalid file type
|
|
||||||
- Browser issue
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
1. Reduce file size
|
|
||||||
2. Use PNG, JPG, or BMP formats
|
|
||||||
3. Try a different browser
|
|
||||||
|
|
||||||
#### RSA key password error
|
|
||||||
|
|
||||||
**Causes:**
|
|
||||||
- Wrong password
|
|
||||||
- Unencrypted key with password provided
|
|
||||||
- Corrupted key file
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
1. Verify the correct password
|
|
||||||
2. If key is unencrypted, leave password blank
|
|
||||||
3. Re-download or regenerate the key
|
|
||||||
|
|
||||||
### Browser Compatibility
|
|
||||||
|
|
||||||
| Browser | Status | Notes |
|
|
||||||
|---------|--------|-------|
|
|
||||||
| Chrome 80+ | ✓ Full | Web Share API supported |
|
|
||||||
| Firefox 80+ | ✓ Full | Limited Web Share |
|
|
||||||
| Safari 14+ | ✓ Full | Web Share on iOS |
|
|
||||||
| Edge 80+ | ✓ Full | Web Share API supported |
|
|
||||||
| IE 11 | ✗ None | Not supported |
|
|
||||||
|
|
||||||
### Performance Issues
|
|
||||||
|
|
||||||
**Slow encoding/decoding:**
|
|
||||||
- Normal: Argon2 is intentionally slow (security feature)
|
|
||||||
- Expected time: 2-5 seconds per operation
|
|
||||||
|
|
||||||
**High memory usage:**
|
|
||||||
- Normal: Argon2 requires 256MB RAM
|
|
||||||
- Configure worker count based on available RAM
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mobile Support
|
|
||||||
|
|
||||||
### Responsive Design
|
|
||||||
|
|
||||||
The UI adapts to mobile screens:
|
|
||||||
- Single-column layout on small screens
|
|
||||||
- Touch-friendly buttons (48px minimum)
|
|
||||||
- Readable text without zooming
|
|
||||||
- Scrollable tables
|
|
||||||
|
|
||||||
### Mobile-Specific Features
|
|
||||||
|
|
||||||
**Native Sharing:**
|
|
||||||
On supported mobile browsers, the "Share Image" button opens the native share sheet, allowing you to share directly to:
|
|
||||||
- Messaging apps (iMessage, WhatsApp, Telegram)
|
|
||||||
- Social media (Instagram, Twitter)
|
|
||||||
- Email
|
|
||||||
- Other installed apps
|
|
||||||
|
|
||||||
**Camera Upload:**
|
|
||||||
File input accepts camera capture:
|
|
||||||
- Take a new photo as reference
|
|
||||||
- Capture carrier image directly
|
|
||||||
|
|
||||||
### PWA Support (Future)
|
|
||||||
|
|
||||||
The web app can be added to home screen on mobile devices for quick access.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
|
||||||
|
|
||||||
| Shortcut | Action |
|
|
||||||
|----------|--------|
|
|
||||||
| `Tab` | Navigate between fields |
|
|
||||||
| `Enter` | Submit form (when focused) |
|
|
||||||
| `Esc` | Close modal/alert |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Accessibility
|
|
||||||
|
|
||||||
| Feature | Implementation |
|
|
||||||
|---------|----------------|
|
|
||||||
| Screen readers | ARIA labels on interactive elements |
|
|
||||||
| Keyboard navigation | Full tab support |
|
|
||||||
| Color contrast | WCAG AA compliant |
|
|
||||||
| Focus indicators | Visible focus rings |
|
|
||||||
| Form labels | All inputs labeled |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [CLI Documentation](CLI.md) - Command-line interface
|
|
||||||
- [API Documentation](API.md) - REST API reference
|
|
||||||
- [README](README.md) - Project overview
|
|
||||||
0
frontends/api/__init__.py
Normal file
256
frontends/api/auth.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
"""
|
||||||
|
API Key Authentication for Stegasoo REST API.
|
||||||
|
|
||||||
|
Provides simple API key authentication with hashed key storage.
|
||||||
|
Keys can be stored in user config (~/.stegasoo/) or project config (./config/).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from .auth import require_api_key, get_api_key_status
|
||||||
|
|
||||||
|
@app.get("/protected")
|
||||||
|
async def protected_endpoint(api_key: str = Depends(require_api_key)):
|
||||||
|
return {"status": "authenticated"}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, Security
|
||||||
|
from fastapi.security import APIKeyHeader
|
||||||
|
|
||||||
|
# API key header name
|
||||||
|
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||||
|
|
||||||
|
# Config locations
|
||||||
|
USER_CONFIG_DIR = Path.home() / ".stegasoo"
|
||||||
|
PROJECT_CONFIG_DIR = Path("./config")
|
||||||
|
|
||||||
|
# Key file name
|
||||||
|
API_KEYS_FILE = "api_keys.json"
|
||||||
|
|
||||||
|
# Environment variable for API key (alternative to file)
|
||||||
|
API_KEY_ENV_VAR = "STEGASOO_API_KEY"
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_key(key: str) -> str:
|
||||||
|
"""Hash an API key for storage."""
|
||||||
|
return hashlib.sha256(key.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_keys_file(location: str = "user") -> Path:
|
||||||
|
"""Get path to API keys file."""
|
||||||
|
if location == "project":
|
||||||
|
return PROJECT_CONFIG_DIR / API_KEYS_FILE
|
||||||
|
return USER_CONFIG_DIR / API_KEYS_FILE
|
||||||
|
|
||||||
|
|
||||||
|
def _load_keys(location: str = "user") -> dict:
|
||||||
|
"""Load API keys from config file."""
|
||||||
|
keys_file = _get_keys_file(location)
|
||||||
|
if keys_file.exists():
|
||||||
|
try:
|
||||||
|
with open(keys_file) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
return {"keys": [], "enabled": True}
|
||||||
|
return {"keys": [], "enabled": True}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_keys(data: dict, location: str = "user") -> None:
|
||||||
|
"""Save API keys to config file."""
|
||||||
|
keys_file = _get_keys_file(location)
|
||||||
|
keys_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(keys_file, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
# Secure permissions (owner read/write only)
|
||||||
|
os.chmod(keys_file, 0o600)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_api_key() -> str:
|
||||||
|
"""Generate a new API key."""
|
||||||
|
# Format: stegasoo_XXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
# 32 bytes = 256 bits of entropy
|
||||||
|
random_part = secrets.token_hex(16)
|
||||||
|
return f"stegasoo_{random_part[:4]}_{random_part[4:]}"
|
||||||
|
|
||||||
|
|
||||||
|
def add_api_key(name: str, location: str = "user") -> str:
|
||||||
|
"""
|
||||||
|
Generate and store a new API key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Descriptive name for the key (e.g., "laptop", "automation")
|
||||||
|
location: "user" or "project"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The generated API key (only shown once!)
|
||||||
|
"""
|
||||||
|
key = generate_api_key()
|
||||||
|
key_hash = _hash_key(key)
|
||||||
|
|
||||||
|
data = _load_keys(location)
|
||||||
|
|
||||||
|
# Check for duplicate name
|
||||||
|
for existing in data["keys"]:
|
||||||
|
if existing["name"] == name:
|
||||||
|
raise ValueError(f"Key with name '{name}' already exists")
|
||||||
|
|
||||||
|
data["keys"].append({
|
||||||
|
"name": name,
|
||||||
|
"hash": key_hash,
|
||||||
|
"created": __import__("datetime").datetime.now().isoformat(),
|
||||||
|
})
|
||||||
|
|
||||||
|
_save_keys(data, location)
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def remove_api_key(name: str, location: str = "user") -> bool:
|
||||||
|
"""
|
||||||
|
Remove an API key by name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if key was found and removed, False otherwise
|
||||||
|
"""
|
||||||
|
data = _load_keys(location)
|
||||||
|
original_count = len(data["keys"])
|
||||||
|
|
||||||
|
data["keys"] = [k for k in data["keys"] if k["name"] != name]
|
||||||
|
|
||||||
|
if len(data["keys"]) < original_count:
|
||||||
|
_save_keys(data, location)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def list_api_keys(location: str = "user") -> list[dict]:
|
||||||
|
"""
|
||||||
|
List all API keys (names and creation dates, not actual keys).
|
||||||
|
"""
|
||||||
|
data = _load_keys(location)
|
||||||
|
return [{"name": k["name"], "created": k.get("created", "unknown")} for k in data["keys"]]
|
||||||
|
|
||||||
|
|
||||||
|
def set_auth_enabled(enabled: bool, location: str = "user") -> None:
|
||||||
|
"""Enable or disable API key authentication."""
|
||||||
|
data = _load_keys(location)
|
||||||
|
data["enabled"] = enabled
|
||||||
|
_save_keys(data, location)
|
||||||
|
|
||||||
|
|
||||||
|
def is_auth_enabled() -> bool:
|
||||||
|
"""Check if API key authentication is enabled."""
|
||||||
|
# Check project config first, then user config
|
||||||
|
for location in ["project", "user"]:
|
||||||
|
data = _load_keys(location)
|
||||||
|
if "enabled" in data:
|
||||||
|
return data["enabled"]
|
||||||
|
|
||||||
|
# Default: enabled if any keys exist
|
||||||
|
return bool(get_all_key_hashes())
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_key_hashes() -> set[str]:
|
||||||
|
"""Get all valid API key hashes from all sources."""
|
||||||
|
hashes = set()
|
||||||
|
|
||||||
|
# Check environment variable first
|
||||||
|
env_key = os.environ.get(API_KEY_ENV_VAR)
|
||||||
|
if env_key:
|
||||||
|
hashes.add(_hash_key(env_key))
|
||||||
|
|
||||||
|
# Check project and user configs
|
||||||
|
for location in ["project", "user"]:
|
||||||
|
data = _load_keys(location)
|
||||||
|
for key_entry in data.get("keys", []):
|
||||||
|
if "hash" in key_entry:
|
||||||
|
hashes.add(key_entry["hash"])
|
||||||
|
|
||||||
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
|
def validate_api_key(key: str) -> bool:
|
||||||
|
"""Validate an API key against stored hashes."""
|
||||||
|
if not key:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key_hash = _hash_key(key)
|
||||||
|
valid_hashes = get_all_key_hashes()
|
||||||
|
|
||||||
|
return key_hash in valid_hashes
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_key_status() -> dict:
|
||||||
|
"""Get current API key authentication status."""
|
||||||
|
user_keys = list_api_keys("user")
|
||||||
|
project_keys = list_api_keys("project")
|
||||||
|
env_configured = bool(os.environ.get(API_KEY_ENV_VAR))
|
||||||
|
|
||||||
|
total_keys = len(user_keys) + len(project_keys) + (1 if env_configured else 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"enabled": is_auth_enabled(),
|
||||||
|
"total_keys": total_keys,
|
||||||
|
"user_keys": len(user_keys),
|
||||||
|
"project_keys": len(project_keys),
|
||||||
|
"env_configured": env_configured,
|
||||||
|
"keys": {
|
||||||
|
"user": user_keys,
|
||||||
|
"project": project_keys,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# FastAPI dependency for API key authentication
|
||||||
|
async def require_api_key(api_key: Optional[str] = Security(API_KEY_HEADER)) -> str:
|
||||||
|
"""
|
||||||
|
FastAPI dependency that requires a valid API key.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@app.get("/protected")
|
||||||
|
async def endpoint(key: str = Depends(require_api_key)):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
# Check if auth is enabled
|
||||||
|
if not is_auth_enabled():
|
||||||
|
return "auth_disabled"
|
||||||
|
|
||||||
|
# No keys configured = auth disabled
|
||||||
|
if not get_all_key_hashes():
|
||||||
|
return "no_keys_configured"
|
||||||
|
|
||||||
|
# Validate the provided key
|
||||||
|
if not api_key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="API key required. Provide X-API-Key header.",
|
||||||
|
headers={"WWW-Authenticate": "ApiKey"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not validate_api_key(api_key):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Invalid API key.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
|
||||||
|
async def optional_api_key(api_key: Optional[str] = Security(API_KEY_HEADER)) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
FastAPI dependency that optionally validates API key.
|
||||||
|
|
||||||
|
Returns the key if valid, None if not provided or invalid.
|
||||||
|
Doesn't raise exceptions - useful for endpoints that work
|
||||||
|
with or without auth.
|
||||||
|
"""
|
||||||
|
if api_key and validate_api_key(api_key):
|
||||||
|
return api_key
|
||||||
|
return None
|
||||||
0
frontends/cli/__init__.py
Normal file
16
frontends/web/.env.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Stegasoo Web UI Configuration
|
||||||
|
# Copy this file to .env and customize
|
||||||
|
|
||||||
|
# Authentication (v4.0.2+)
|
||||||
|
STEGASOO_AUTH_ENABLED=true
|
||||||
|
STEGASOO_HTTPS_ENABLED=false
|
||||||
|
STEGASOO_HOSTNAME=localhost
|
||||||
|
STEGASOO_PORT=5000
|
||||||
|
|
||||||
|
# Channel Key (format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX)
|
||||||
|
# Generate with: stegasoo generate --channel-key
|
||||||
|
# Leave empty for public mode
|
||||||
|
STEGASOO_CHANNEL_KEY=
|
||||||
|
|
||||||
|
# Flask settings
|
||||||
|
FLASK_ENV=production
|
||||||
62
frontends/web/README_subprocess.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Subprocess Isolation for Stegasoo WebUI
|
||||||
|
|
||||||
|
This update runs encode/decode/compare operations in isolated subprocesses
|
||||||
|
to prevent jpegio/scipy crashes from taking down the Flask server.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **app.py** - Updated Flask app using subprocess isolation
|
||||||
|
- **subprocess_stego.py** - Flask-side wrapper with clean API
|
||||||
|
- **stego_worker.py** - Subprocess script that does actual stegasoo operations
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Place all three files in your `webui/` directory (same level as templates/)
|
||||||
|
|
||||||
|
2. Make sure stego_worker.py is executable (optional):
|
||||||
|
```bash
|
||||||
|
chmod +x stego_worker.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the Flask app:
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Instead of calling stegasoo functions directly in the Flask process:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# OLD (crashes could kill Flask)
|
||||||
|
result = encode(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
We now run them in subprocesses:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# NEW (crashes only kill the subprocess)
|
||||||
|
result = subprocess_stego.encode(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
If jpegio or scipy crashes due to memory corruption, only the subprocess
|
||||||
|
dies. Flask logs the error and continues running. The next request spawns
|
||||||
|
a fresh subprocess.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
In `app.py`, you can adjust the timeout:
|
||||||
|
|
||||||
|
```python
|
||||||
|
subprocess_stego = SubprocessStego(timeout=180) # 3 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
Larger images may need longer timeouts.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If you see "Worker script not found" errors, make sure `stego_worker.py`
|
||||||
|
is in the same directory as `app.py`.
|
||||||
|
|
||||||
|
If subprocess operations fail, check the Flask logs for error details.
|
||||||
|
The subprocess wrapper captures both stdout and stderr from the worker.
|
||||||
0
frontends/web/__init__.py
Normal file
3066
frontends/web/app.py
@@ -1,766 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Stegasoo Web Frontend
|
|
||||||
|
|
||||||
Flask-based web UI for steganography operations.
|
|
||||||
Supports both text messages and file embedding.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import io
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import secrets
|
|
||||||
import mimetypes
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from flask import (
|
|
||||||
Flask, render_template, request, send_file,
|
|
||||||
jsonify, flash, redirect, url_for
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add parent to path for development
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
|
||||||
|
|
||||||
import stegasoo
|
|
||||||
from stegasoo import (
|
|
||||||
encode, decode, generate_credentials,
|
|
||||||
export_rsa_key_pem, load_rsa_key,
|
|
||||||
validate_pin, validate_message, validate_image,
|
|
||||||
validate_rsa_key, validate_security_factors,
|
|
||||||
validate_file_payload,
|
|
||||||
get_today_day, generate_filename,
|
|
||||||
DAY_NAMES, __version__,
|
|
||||||
StegasooError, DecryptionError, CapacityError,
|
|
||||||
has_argon2,
|
|
||||||
FilePayload,
|
|
||||||
MAX_FILE_PAYLOAD_SIZE,
|
|
||||||
)
|
|
||||||
from stegasoo.constants import (
|
|
||||||
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
|
||||||
VALID_RSA_SIZES, MAX_FILE_SIZE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# QR Code support
|
|
||||||
try:
|
|
||||||
import qrcode
|
|
||||||
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
|
|
||||||
HAS_QRCODE = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_QRCODE = False
|
|
||||||
|
|
||||||
# QR Code reading
|
|
||||||
try:
|
|
||||||
from pyzbar.pyzbar import decode as pyzbar_decode
|
|
||||||
HAS_QRCODE_READ = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_QRCODE_READ = False
|
|
||||||
|
|
||||||
import zlib
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Import QR utilities
|
|
||||||
from stegasoo.qr_utils import (
|
|
||||||
compress_data, decompress_data, auto_decompress,
|
|
||||||
is_compressed, can_fit_in_qr, needs_compression,
|
|
||||||
generate_qr_code, read_qr_code, extract_key_from_qr,
|
|
||||||
has_qr_write, has_qr_read,
|
|
||||||
QR_MAX_BINARY, COMPRESSION_PREFIX
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FLASK APP CONFIGURATION
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.secret_key = secrets.token_hex(32)
|
|
||||||
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 20MB max upload
|
|
||||||
|
|
||||||
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
|
|
||||||
TEMP_FILES: dict[str, dict] = {}
|
|
||||||
THUMBNAIL_FILES: dict[str, bytes] = {}
|
|
||||||
TEMP_FILE_EXPIRY = 300 # 5 minutes
|
|
||||||
THUMBNAIL_SIZE = (250, 250) # Maximum dimensions for thumbnail
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# CONFIGURATION
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Override stegasoo limits for larger files
|
|
||||||
# Note: You might need to modify the stegasoo library itself
|
|
||||||
# to actually increase these limits in its internal calculations
|
|
||||||
|
|
||||||
# Flask upload limit (30MB)
|
|
||||||
MAX_UPLOAD_SIZE = 30 * 1024 * 1024
|
|
||||||
|
|
||||||
# Try to import and override stegasoo constants if possible
|
|
||||||
try:
|
|
||||||
# Check current limits
|
|
||||||
print(f"Current MAX_FILE_SIZE from constants: {MAX_FILE_SIZE}")
|
|
||||||
print(f"Current MAX_FILE_PAYLOAD_SIZE: {MAX_FILE_PAYLOAD_SIZE}")
|
|
||||||
|
|
||||||
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
|
|
||||||
|
|
||||||
# Note: You might need to patch the stegasoo module
|
|
||||||
# if MAX_FILE_PAYLOAD_SIZE is used internally
|
|
||||||
import stegasoo
|
|
||||||
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
|
|
||||||
print(f"Overriding MAX_FILE_PAYLOAD_SIZE to {DESIRED_PAYLOAD_SIZE}")
|
|
||||||
stegasoo.MAX_FILE_PAYLOAD_SIZE = DESIRED_PAYLOAD_SIZE
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not override stegasoo limits: {e}")
|
|
||||||
|
|
||||||
def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
|
|
||||||
"""Generate thumbnail from image data."""
|
|
||||||
try:
|
|
||||||
with Image.open(io.BytesIO(image_data)) as img:
|
|
||||||
# Convert to RGB if necessary
|
|
||||||
if img.mode in ('RGBA', 'LA', 'P'):
|
|
||||||
# Create white background for transparent images
|
|
||||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
||||||
if img.mode == 'P':
|
|
||||||
img = img.convert('RGBA')
|
|
||||||
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
|
||||||
img = background
|
|
||||||
elif img.mode != 'RGB':
|
|
||||||
img = img.convert('RGB')
|
|
||||||
|
|
||||||
# Create thumbnail
|
|
||||||
img.thumbnail(size, Image.Resampling.LANCZOS)
|
|
||||||
|
|
||||||
# Save to bytes
|
|
||||||
buffer = io.BytesIO()
|
|
||||||
img.save(buffer, format='JPEG', quality=85, optimize=True)
|
|
||||||
return buffer.getvalue()
|
|
||||||
except Exception as e:
|
|
||||||
# Log error but don't crash
|
|
||||||
print(f"Thumbnail generation error: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_temp_files():
|
|
||||||
"""Remove expired temporary files."""
|
|
||||||
now = time.time()
|
|
||||||
expired = [fid for fid, info in TEMP_FILES.items() if now - info['timestamp'] > TEMP_FILE_EXPIRY]
|
|
||||||
|
|
||||||
for fid in expired:
|
|
||||||
TEMP_FILES.pop(fid, None)
|
|
||||||
# Also clean up corresponding thumbnail
|
|
||||||
thumb_id = f"{fid}_thumb"
|
|
||||||
THUMBNAIL_FILES.pop(thumb_id, None)
|
|
||||||
|
|
||||||
|
|
||||||
def allowed_image(filename: str) -> bool:
|
|
||||||
"""Check if file has allowed image extension."""
|
|
||||||
if not filename or '.' not in filename:
|
|
||||||
return False
|
|
||||||
ext = filename.rsplit('.', 1)[1].lower()
|
|
||||||
return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
|
|
||||||
|
|
||||||
|
|
||||||
def format_size(size_bytes: int) -> str:
|
|
||||||
"""Format file size for display."""
|
|
||||||
if size_bytes < 1024:
|
|
||||||
return f"{size_bytes} B"
|
|
||||||
elif size_bytes < 1024 * 1024:
|
|
||||||
return f"{size_bytes / 1024:.1f} KB"
|
|
||||||
else:
|
|
||||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# ROUTES
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template('index.html')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate', methods=['GET', 'POST'])
|
|
||||||
def generate():
|
|
||||||
if request.method == 'POST':
|
|
||||||
words_per_phrase = int(request.form.get('words_per_phrase', 3))
|
|
||||||
use_pin = request.form.get('use_pin') == 'on'
|
|
||||||
use_rsa = request.form.get('use_rsa') == 'on'
|
|
||||||
|
|
||||||
if not use_pin and not use_rsa:
|
|
||||||
flash('You must select at least one security factor (PIN or RSA Key)', 'error')
|
|
||||||
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
|
|
||||||
|
|
||||||
pin_length = int(request.form.get('pin_length', 6))
|
|
||||||
rsa_bits = int(request.form.get('rsa_bits', 2048))
|
|
||||||
|
|
||||||
# Clamp values
|
|
||||||
words_per_phrase = max(3, min(12, words_per_phrase))
|
|
||||||
pin_length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, pin_length))
|
|
||||||
if rsa_bits not in VALID_RSA_SIZES:
|
|
||||||
rsa_bits = 2048
|
|
||||||
|
|
||||||
try:
|
|
||||||
creds = generate_credentials(
|
|
||||||
use_pin=use_pin,
|
|
||||||
use_rsa=use_rsa,
|
|
||||||
pin_length=pin_length,
|
|
||||||
rsa_bits=rsa_bits,
|
|
||||||
words_per_phrase=words_per_phrase
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store RSA key temporarily for QR generation
|
|
||||||
qr_token = None
|
|
||||||
qr_needs_compression = False
|
|
||||||
qr_too_large = False
|
|
||||||
|
|
||||||
if creds.rsa_key_pem and HAS_QRCODE:
|
|
||||||
# Check if key fits in QR code
|
|
||||||
if can_fit_in_qr(creds.rsa_key_pem, compress=True):
|
|
||||||
qr_needs_compression = True
|
|
||||||
else:
|
|
||||||
qr_too_large = True
|
|
||||||
|
|
||||||
if not qr_too_large:
|
|
||||||
qr_token = secrets.token_urlsafe(16)
|
|
||||||
cleanup_temp_files()
|
|
||||||
TEMP_FILES[qr_token] = {
|
|
||||||
'data': creds.rsa_key_pem.encode(),
|
|
||||||
'filename': 'rsa_key.pem',
|
|
||||||
'timestamp': time.time(),
|
|
||||||
'type': 'rsa_key',
|
|
||||||
'compress': qr_needs_compression
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template('generate.html',
|
|
||||||
phrases=creds.phrases,
|
|
||||||
pin=creds.pin,
|
|
||||||
days=DAY_NAMES,
|
|
||||||
generated=True,
|
|
||||||
words_per_phrase=words_per_phrase,
|
|
||||||
pin_length=pin_length if use_pin else None,
|
|
||||||
use_pin=use_pin,
|
|
||||||
use_rsa=use_rsa,
|
|
||||||
rsa_bits=rsa_bits,
|
|
||||||
rsa_key_pem=creds.rsa_key_pem,
|
|
||||||
phrase_entropy=creds.phrase_entropy,
|
|
||||||
pin_entropy=creds.pin_entropy,
|
|
||||||
rsa_entropy=creds.rsa_entropy,
|
|
||||||
total_entropy=creds.total_entropy,
|
|
||||||
has_qrcode=HAS_QRCODE,
|
|
||||||
qr_token=qr_token,
|
|
||||||
qr_needs_compression=qr_needs_compression,
|
|
||||||
qr_too_large=qr_too_large
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
flash(f'Error generating credentials: {e}', 'error')
|
|
||||||
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
|
|
||||||
|
|
||||||
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate/qr/<token>')
|
|
||||||
def generate_qr(token):
|
|
||||||
"""Generate QR code for RSA key."""
|
|
||||||
if not HAS_QRCODE:
|
|
||||||
return "QR code support not available", 501
|
|
||||||
|
|
||||||
if token not in TEMP_FILES:
|
|
||||||
return "Token expired or invalid", 404
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[token]
|
|
||||||
if file_info.get('type') != 'rsa_key':
|
|
||||||
return "Invalid token type", 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
key_pem = file_info['data'].decode('utf-8')
|
|
||||||
compress = file_info.get('compress', False)
|
|
||||||
qr_png = generate_qr_code(key_pem, compress=compress)
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(qr_png),
|
|
||||||
mimetype='image/png',
|
|
||||||
as_attachment=False
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error generating QR code: {e}", 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate/qr-download/<token>')
|
|
||||||
def generate_qr_download(token):
|
|
||||||
"""Download QR code as PNG file."""
|
|
||||||
if not HAS_QRCODE:
|
|
||||||
return "QR code support not available", 501
|
|
||||||
|
|
||||||
if token not in TEMP_FILES:
|
|
||||||
return "Token expired or invalid", 404
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[token]
|
|
||||||
if file_info.get('type') != 'rsa_key':
|
|
||||||
return "Invalid token type", 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
key_pem = file_info['data'].decode('utf-8')
|
|
||||||
compress = file_info.get('compress', False)
|
|
||||||
qr_png = generate_qr_code(key_pem, compress=compress)
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(qr_png),
|
|
||||||
mimetype='image/png',
|
|
||||||
as_attachment=True,
|
|
||||||
download_name='stegasoo_rsa_key_qr.png'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error generating QR code: {e}", 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate/download-key', methods=['POST'])
|
|
||||||
def download_key():
|
|
||||||
"""Download RSA key as password-protected PEM file."""
|
|
||||||
key_pem = request.form.get('key_pem', '')
|
|
||||||
password = request.form.get('key_password', '')
|
|
||||||
|
|
||||||
if not key_pem:
|
|
||||||
flash('No key to download', 'error')
|
|
||||||
return redirect(url_for('generate'))
|
|
||||||
|
|
||||||
if not password or len(password) < 8:
|
|
||||||
flash('Password must be at least 8 characters', 'error')
|
|
||||||
return redirect(url_for('generate'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
private_key = load_rsa_key(key_pem.encode('utf-8'))
|
|
||||||
encrypted_pem = export_rsa_key_pem(private_key, password=password)
|
|
||||||
|
|
||||||
key_id = secrets.token_hex(4)
|
|
||||||
filename = f'stegasoo_key_{private_key.key_size}_{key_id}.pem'
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(encrypted_pem),
|
|
||||||
mimetype='application/x-pem-file',
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=filename
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
flash(f'Error creating key file: {e}', 'error')
|
|
||||||
return redirect(url_for('generate'))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/extract-key-from-qr', methods=['POST'])
|
|
||||||
def extract_key_from_qr_route():
|
|
||||||
"""
|
|
||||||
Extract RSA key from uploaded QR code image.
|
|
||||||
Returns JSON with the extracted key or error.
|
|
||||||
"""
|
|
||||||
if not HAS_QRCODE_READ:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'QR code reading not available. Install pyzbar and libzbar.'
|
|
||||||
}), 501
|
|
||||||
|
|
||||||
qr_image = request.files.get('qr_image')
|
|
||||||
if not qr_image:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'No QR image provided'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
image_data = qr_image.read()
|
|
||||||
key_pem = extract_key_from_qr(image_data)
|
|
||||||
|
|
||||||
if key_pem:
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'key_pem': key_pem
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'No valid RSA key found in QR code'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode', methods=['GET', 'POST'])
|
|
||||||
def encode_page():
|
|
||||||
day_of_week = get_today_day()
|
|
||||||
max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
try:
|
|
||||||
# Get files
|
|
||||||
ref_photo = request.files.get('reference_photo')
|
|
||||||
carrier = request.files.get('carrier')
|
|
||||||
rsa_key_file = request.files.get('rsa_key')
|
|
||||||
payload_file = request.files.get('payload_file')
|
|
||||||
|
|
||||||
if not ref_photo or not carrier:
|
|
||||||
flash('Both reference photo and carrier image are required', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
|
|
||||||
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Get form data
|
|
||||||
message = request.form.get('message', '')
|
|
||||||
day_phrase = request.form.get('day_phrase', '')
|
|
||||||
pin = request.form.get('pin', '').strip()
|
|
||||||
rsa_password = request.form.get('rsa_password', '')
|
|
||||||
payload_type = request.form.get('payload_type', 'text')
|
|
||||||
|
|
||||||
# Determine payload
|
|
||||||
if payload_type == 'file' and payload_file and payload_file.filename:
|
|
||||||
# File payload
|
|
||||||
file_data = payload_file.read()
|
|
||||||
|
|
||||||
result = validate_file_payload(file_data, payload_file.filename)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
mime_type, _ = mimetypes.guess_type(payload_file.filename)
|
|
||||||
payload = FilePayload(
|
|
||||||
data=file_data,
|
|
||||||
filename=payload_file.filename,
|
|
||||||
mime_type=mime_type
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Text message
|
|
||||||
result = validate_message(message)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
payload = message
|
|
||||||
|
|
||||||
if not day_phrase:
|
|
||||||
flash('Day phrase is required', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Read files
|
|
||||||
ref_data = ref_photo.read()
|
|
||||||
carrier_data = carrier.read()
|
|
||||||
|
|
||||||
# Handle RSA key - can come from .pem file or QR code image
|
|
||||||
rsa_key_data = None
|
|
||||||
rsa_key_qr = request.files.get('rsa_key_qr')
|
|
||||||
rsa_key_from_qr = False # Track source for password handling
|
|
||||||
|
|
||||||
if rsa_key_file and rsa_key_file.filename:
|
|
||||||
# RSA key from .pem file
|
|
||||||
rsa_key_data = rsa_key_file.read()
|
|
||||||
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
|
|
||||||
# RSA key from QR code image
|
|
||||||
qr_image_data = rsa_key_qr.read()
|
|
||||||
key_pem = extract_key_from_qr(qr_image_data)
|
|
||||||
if key_pem:
|
|
||||||
rsa_key_data = key_pem.encode('utf-8')
|
|
||||||
rsa_key_from_qr = True # QR keys are never password-protected
|
|
||||||
else:
|
|
||||||
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate security factors
|
|
||||||
result = validate_security_factors(pin, rsa_key_data)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate PIN if provided
|
|
||||||
if pin:
|
|
||||||
result = validate_pin(pin)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Determine key password - QR code keys are never password-protected
|
|
||||||
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
|
||||||
|
|
||||||
# Validate RSA key if provided
|
|
||||||
if rsa_key_data:
|
|
||||||
result = validate_rsa_key(rsa_key_data, key_password)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate carrier image
|
|
||||||
result = validate_image(carrier_data, "Carrier image")
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Get date
|
|
||||||
client_date = request.form.get('client_date', '').strip()
|
|
||||||
if client_date and len(client_date) == 10 and client_date[4] == '-' and client_date[7] == '-':
|
|
||||||
date_str = client_date
|
|
||||||
else:
|
|
||||||
date_str = datetime.now().strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# Encode
|
|
||||||
encode_result = encode(
|
|
||||||
message=payload,
|
|
||||||
reference_photo=ref_data,
|
|
||||||
carrier_image=carrier_data,
|
|
||||||
day_phrase=day_phrase,
|
|
||||||
pin=pin,
|
|
||||||
rsa_key_data=rsa_key_data,
|
|
||||||
rsa_password=key_password,
|
|
||||||
date_str=date_str
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store temporarily
|
|
||||||
file_id = secrets.token_urlsafe(16)
|
|
||||||
cleanup_temp_files()
|
|
||||||
TEMP_FILES[file_id] = {
|
|
||||||
'data': encode_result.stego_image,
|
|
||||||
'filename': encode_result.filename,
|
|
||||||
'timestamp': time.time()
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(url_for('encode_result', file_id=file_id))
|
|
||||||
|
|
||||||
except CapacityError as e:
|
|
||||||
flash(str(e), 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
except StegasooError as e:
|
|
||||||
flash(str(e), 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
except Exception as e:
|
|
||||||
flash(f'Error: {e}', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/result/<file_id>')
|
|
||||||
def encode_result(file_id):
|
|
||||||
if file_id not in TEMP_FILES:
|
|
||||||
flash('File expired or not found. Please encode again.', 'error')
|
|
||||||
return redirect(url_for('encode_page'))
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
|
|
||||||
# Generate thumbnail
|
|
||||||
thumbnail_data = generate_thumbnail(file_info['data'])
|
|
||||||
thumbnail_id = None
|
|
||||||
|
|
||||||
if thumbnail_data:
|
|
||||||
thumbnail_id = f"{file_id}_thumb"
|
|
||||||
THUMBNAIL_FILES[thumbnail_id] = thumbnail_data
|
|
||||||
|
|
||||||
return render_template('encode_result.html',
|
|
||||||
file_id=file_id,
|
|
||||||
filename=file_info['filename'],
|
|
||||||
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/thumbnail/<thumb_id>')
|
|
||||||
def encode_thumbnail(thumb_id):
|
|
||||||
"""Serve thumbnail image."""
|
|
||||||
if thumb_id not in THUMBNAIL_FILES:
|
|
||||||
return "Thumbnail not found", 404
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(THUMBNAIL_FILES[thumb_id]),
|
|
||||||
mimetype='image/jpeg',
|
|
||||||
as_attachment=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/download/<file_id>')
|
|
||||||
def encode_download(file_id):
|
|
||||||
if file_id not in TEMP_FILES:
|
|
||||||
flash('File expired or not found.', 'error')
|
|
||||||
return redirect(url_for('encode_page'))
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(file_info['data']),
|
|
||||||
mimetype='image/png',
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=file_info['filename']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/file/<file_id>')
|
|
||||||
def encode_file_route(file_id):
|
|
||||||
"""Serve file for Web Share API."""
|
|
||||||
if file_id not in TEMP_FILES:
|
|
||||||
return "Not found", 404
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(file_info['data']),
|
|
||||||
mimetype='image/png',
|
|
||||||
as_attachment=False,
|
|
||||||
download_name=file_info['filename']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/cleanup/<file_id>', methods=['POST'])
|
|
||||||
def encode_cleanup(file_id):
|
|
||||||
"""Manually cleanup a file after sharing."""
|
|
||||||
TEMP_FILES.pop(file_id, None)
|
|
||||||
|
|
||||||
# Also cleanup thumbnail if exists
|
|
||||||
thumb_id = f"{file_id}_thumb"
|
|
||||||
THUMBNAIL_FILES.pop(thumb_id, None)
|
|
||||||
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/decode', methods=['GET', 'POST'])
|
|
||||||
def decode_page():
|
|
||||||
if request.method == 'POST':
|
|
||||||
try:
|
|
||||||
# Get files
|
|
||||||
ref_photo = request.files.get('reference_photo')
|
|
||||||
stego_image = request.files.get('stego_image')
|
|
||||||
rsa_key_file = request.files.get('rsa_key')
|
|
||||||
|
|
||||||
if not ref_photo or not stego_image:
|
|
||||||
flash('Both reference photo and stego image are required', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Get form data
|
|
||||||
day_phrase = request.form.get('day_phrase', '')
|
|
||||||
pin = request.form.get('pin', '').strip()
|
|
||||||
rsa_password = request.form.get('rsa_password', '')
|
|
||||||
|
|
||||||
if not day_phrase:
|
|
||||||
flash('Day phrase is required', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Read files
|
|
||||||
ref_data = ref_photo.read()
|
|
||||||
stego_data = stego_image.read()
|
|
||||||
|
|
||||||
# Handle RSA key - can come from .pem file or QR code image
|
|
||||||
rsa_key_data = None
|
|
||||||
rsa_key_qr = request.files.get('rsa_key_qr')
|
|
||||||
rsa_key_from_qr = False # Track source for password handling
|
|
||||||
|
|
||||||
if rsa_key_file and rsa_key_file.filename:
|
|
||||||
# RSA key from .pem file
|
|
||||||
rsa_key_data = rsa_key_file.read()
|
|
||||||
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
|
|
||||||
# RSA key from QR code image
|
|
||||||
qr_image_data = rsa_key_qr.read()
|
|
||||||
key_pem = extract_key_from_qr(qr_image_data)
|
|
||||||
if key_pem:
|
|
||||||
rsa_key_data = key_pem.encode('utf-8')
|
|
||||||
rsa_key_from_qr = True # QR keys are never password-protected
|
|
||||||
else:
|
|
||||||
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate security factors
|
|
||||||
result = validate_security_factors(pin, rsa_key_data)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate PIN if provided
|
|
||||||
if pin:
|
|
||||||
result = validate_pin(pin)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Determine key password - QR code keys are never password-protected
|
|
||||||
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
|
||||||
|
|
||||||
# Validate RSA key if provided
|
|
||||||
if rsa_key_data:
|
|
||||||
result = validate_rsa_key(rsa_key_data, key_password)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Decode
|
|
||||||
decode_result = decode(
|
|
||||||
stego_image=stego_data,
|
|
||||||
reference_photo=ref_data,
|
|
||||||
day_phrase=day_phrase,
|
|
||||||
pin=pin,
|
|
||||||
rsa_key_data=rsa_key_data,
|
|
||||||
rsa_password=key_password
|
|
||||||
)
|
|
||||||
|
|
||||||
if decode_result.is_file:
|
|
||||||
# File content - store temporarily for download
|
|
||||||
file_id = secrets.token_urlsafe(16)
|
|
||||||
cleanup_temp_files()
|
|
||||||
|
|
||||||
filename = decode_result.filename or 'decoded_file'
|
|
||||||
TEMP_FILES[file_id] = {
|
|
||||||
'data': decode_result.file_data,
|
|
||||||
'filename': filename,
|
|
||||||
'mime_type': decode_result.mime_type,
|
|
||||||
'timestamp': time.time()
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template('decode.html',
|
|
||||||
decoded_file=True,
|
|
||||||
file_id=file_id,
|
|
||||||
filename=filename,
|
|
||||||
file_size=format_size(len(decode_result.file_data)),
|
|
||||||
mime_type=decode_result.mime_type
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Text content
|
|
||||||
return render_template('decode.html', decoded_message=decode_result.message)
|
|
||||||
|
|
||||||
except DecryptionError:
|
|
||||||
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
except StegasooError as e:
|
|
||||||
flash(str(e), 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
except Exception as e:
|
|
||||||
flash(f'Error: {e}', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/decode/download/<file_id>')
|
|
||||||
def decode_download(file_id):
|
|
||||||
"""Download decoded file."""
|
|
||||||
if file_id not in TEMP_FILES:
|
|
||||||
flash('File expired or not found.', 'error')
|
|
||||||
return redirect(url_for('decode_page'))
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
mime_type = file_info.get('mime_type', 'application/octet-stream')
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(file_info['data']),
|
|
||||||
mimetype=mime_type,
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=file_info['filename']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/about')
|
|
||||||
def about():
|
|
||||||
return render_template('about.html',
|
|
||||||
has_argon2=has_argon2(),
|
|
||||||
has_qrcode_read=HAS_QRCODE_READ,
|
|
||||||
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# MAIN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
|
||||||
@@ -1,781 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Stegasoo Web Frontend
|
|
||||||
|
|
||||||
Flask-based web UI for steganography operations.
|
|
||||||
Supports both text messages and file embedding.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import io
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import secrets
|
|
||||||
import mimetypes
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from flask import (
|
|
||||||
Flask, render_template, request, send_file,
|
|
||||||
jsonify, flash, redirect, url_for
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add parent to path for development
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
|
||||||
|
|
||||||
import stegasoo
|
|
||||||
from stegasoo import (
|
|
||||||
encode, decode, generate_credentials,
|
|
||||||
export_rsa_key_pem, load_rsa_key,
|
|
||||||
validate_pin, validate_message, validate_image,
|
|
||||||
validate_rsa_key, validate_security_factors,
|
|
||||||
validate_file_payload,
|
|
||||||
get_today_day, generate_filename,
|
|
||||||
DAY_NAMES, __version__,
|
|
||||||
StegasooError, DecryptionError, CapacityError,
|
|
||||||
has_argon2,
|
|
||||||
FilePayload,
|
|
||||||
MAX_FILE_PAYLOAD_SIZE,
|
|
||||||
)
|
|
||||||
from stegasoo.constants import (
|
|
||||||
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
|
||||||
VALID_RSA_SIZES, MAX_FILE_SIZE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# QR Code support
|
|
||||||
try:
|
|
||||||
import qrcode
|
|
||||||
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
|
|
||||||
HAS_QRCODE = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_QRCODE = False
|
|
||||||
|
|
||||||
# QR Code reading
|
|
||||||
try:
|
|
||||||
from pyzbar.pyzbar import decode as pyzbar_decode
|
|
||||||
HAS_QRCODE_READ = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_QRCODE_READ = False
|
|
||||||
|
|
||||||
import zlib
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Import QR utilities
|
|
||||||
from stegasoo.qr_utils import (
|
|
||||||
compress_data, decompress_data, auto_decompress,
|
|
||||||
is_compressed, can_fit_in_qr, needs_compression,
|
|
||||||
generate_qr_code, read_qr_code, extract_key_from_qr,
|
|
||||||
has_qr_write, has_qr_read,
|
|
||||||
QR_MAX_BINARY, COMPRESSION_PREFIX
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FLASK APP CONFIGURATION
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.secret_key = secrets.token_hex(32)
|
|
||||||
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 20MB max upload
|
|
||||||
|
|
||||||
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
|
|
||||||
TEMP_FILES: dict[str, dict] = {}
|
|
||||||
THUMBNAIL_FILES: dict[str, bytes] = {}
|
|
||||||
TEMP_FILE_EXPIRY = 300 # 5 minutes
|
|
||||||
THUMBNAIL_SIZE = (250, 250) # Maximum dimensions for thumbnail
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# CONFIGURATION
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Override stegasoo limits for larger files
|
|
||||||
# Note: You might need to modify the stegasoo library itself
|
|
||||||
# to actually increase these limits in its internal calculations
|
|
||||||
|
|
||||||
# Flask upload limit (30MB)
|
|
||||||
MAX_UPLOAD_SIZE = 30 * 1024 * 1024
|
|
||||||
|
|
||||||
# Try to import and override stegasoo constants if possible
|
|
||||||
try:
|
|
||||||
# Check current limits
|
|
||||||
print(f"Current MAX_FILE_SIZE from constants: {MAX_FILE_SIZE}")
|
|
||||||
print(f"Current MAX_FILE_PAYLOAD_SIZE: {MAX_FILE_PAYLOAD_SIZE}")
|
|
||||||
|
|
||||||
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
|
|
||||||
|
|
||||||
# Note: You might need to patch the stegasoo module
|
|
||||||
# if MAX_FILE_PAYLOAD_SIZE is used internally
|
|
||||||
import stegasoo
|
|
||||||
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
|
|
||||||
print(f"Overriding MAX_FILE_PAYLOAD_SIZE to {DESIRED_PAYLOAD_SIZE}")
|
|
||||||
stegasoo.MAX_FILE_PAYLOAD_SIZE = DESIRED_PAYLOAD_SIZE
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not override stegasoo limits: {e}")
|
|
||||||
|
|
||||||
def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
|
|
||||||
"""Generate thumbnail from image data."""
|
|
||||||
try:
|
|
||||||
with Image.open(io.BytesIO(image_data)) as img:
|
|
||||||
# Convert to RGB if necessary
|
|
||||||
if img.mode in ('RGBA', 'LA', 'P'):
|
|
||||||
# Create white background for transparent images
|
|
||||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
||||||
if img.mode == 'P':
|
|
||||||
img = img.convert('RGBA')
|
|
||||||
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
|
||||||
img = background
|
|
||||||
elif img.mode != 'RGB':
|
|
||||||
img = img.convert('RGB')
|
|
||||||
|
|
||||||
# Create thumbnail
|
|
||||||
img.thumbnail(size, Image.Resampling.LANCZOS)
|
|
||||||
|
|
||||||
# Save to bytes
|
|
||||||
buffer = io.BytesIO()
|
|
||||||
img.save(buffer, format='JPEG', quality=85, optimize=True)
|
|
||||||
return buffer.getvalue()
|
|
||||||
except Exception as e:
|
|
||||||
# Log error but don't crash
|
|
||||||
print(f"Thumbnail generation error: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_temp_files():
|
|
||||||
"""Remove expired temporary files."""
|
|
||||||
now = time.time()
|
|
||||||
expired = [fid for fid, info in TEMP_FILES.items() if now - info['timestamp'] > TEMP_FILE_EXPIRY]
|
|
||||||
|
|
||||||
for fid in expired:
|
|
||||||
TEMP_FILES.pop(fid, None)
|
|
||||||
# Also clean up corresponding thumbnail
|
|
||||||
thumb_id = f"{fid}_thumb"
|
|
||||||
THUMBNAIL_FILES.pop(thumb_id, None)
|
|
||||||
|
|
||||||
|
|
||||||
def allowed_image(filename: str) -> bool:
|
|
||||||
"""Check if file has allowed image extension."""
|
|
||||||
if not filename or '.' not in filename:
|
|
||||||
return False
|
|
||||||
ext = filename.rsplit('.', 1)[1].lower()
|
|
||||||
return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
|
|
||||||
|
|
||||||
|
|
||||||
def format_size(size_bytes: int) -> str:
|
|
||||||
"""Format file size for display."""
|
|
||||||
if size_bytes < 1024:
|
|
||||||
return f"{size_bytes} B"
|
|
||||||
elif size_bytes < 1024 * 1024:
|
|
||||||
return f"{size_bytes / 1024:.1f} KB"
|
|
||||||
else:
|
|
||||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# ROUTES
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template('index.html')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate', methods=['GET', 'POST'])
|
|
||||||
def generate():
|
|
||||||
if request.method == 'POST':
|
|
||||||
words_per_phrase = int(request.form.get('words_per_phrase', 3))
|
|
||||||
use_pin = request.form.get('use_pin') == 'on'
|
|
||||||
use_rsa = request.form.get('use_rsa') == 'on'
|
|
||||||
|
|
||||||
if not use_pin and not use_rsa:
|
|
||||||
flash('You must select at least one security factor (PIN or RSA Key)', 'error')
|
|
||||||
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
|
|
||||||
|
|
||||||
pin_length = int(request.form.get('pin_length', 6))
|
|
||||||
rsa_bits = int(request.form.get('rsa_bits', 2048))
|
|
||||||
|
|
||||||
# Clamp values
|
|
||||||
words_per_phrase = max(3, min(12, words_per_phrase))
|
|
||||||
pin_length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, pin_length))
|
|
||||||
if rsa_bits not in VALID_RSA_SIZES:
|
|
||||||
rsa_bits = 2048
|
|
||||||
|
|
||||||
try:
|
|
||||||
creds = generate_credentials(
|
|
||||||
use_pin=use_pin,
|
|
||||||
use_rsa=use_rsa,
|
|
||||||
pin_length=pin_length,
|
|
||||||
rsa_bits=rsa_bits,
|
|
||||||
words_per_phrase=words_per_phrase
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store RSA key temporarily for QR generation
|
|
||||||
qr_token = None
|
|
||||||
qr_needs_compression = False
|
|
||||||
qr_too_large = False
|
|
||||||
|
|
||||||
if creds.rsa_key_pem and HAS_QRCODE:
|
|
||||||
# Check if key fits in QR code
|
|
||||||
if can_fit_in_qr(creds.rsa_key_pem, compress=True):
|
|
||||||
qr_needs_compression = True
|
|
||||||
else:
|
|
||||||
qr_too_large = True
|
|
||||||
|
|
||||||
if not qr_too_large:
|
|
||||||
qr_token = secrets.token_urlsafe(16)
|
|
||||||
cleanup_temp_files()
|
|
||||||
TEMP_FILES[qr_token] = {
|
|
||||||
'data': creds.rsa_key_pem.encode(),
|
|
||||||
'filename': 'rsa_key.pem',
|
|
||||||
'timestamp': time.time(),
|
|
||||||
'type': 'rsa_key',
|
|
||||||
'compress': qr_needs_compression
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template('generate.html',
|
|
||||||
phrases=creds.phrases,
|
|
||||||
pin=creds.pin,
|
|
||||||
days=DAY_NAMES,
|
|
||||||
generated=True,
|
|
||||||
words_per_phrase=words_per_phrase,
|
|
||||||
pin_length=pin_length if use_pin else None,
|
|
||||||
use_pin=use_pin,
|
|
||||||
use_rsa=use_rsa,
|
|
||||||
rsa_bits=rsa_bits,
|
|
||||||
rsa_key_pem=creds.rsa_key_pem,
|
|
||||||
phrase_entropy=creds.phrase_entropy,
|
|
||||||
pin_entropy=creds.pin_entropy,
|
|
||||||
rsa_entropy=creds.rsa_entropy,
|
|
||||||
total_entropy=creds.total_entropy,
|
|
||||||
has_qrcode=HAS_QRCODE,
|
|
||||||
qr_token=qr_token,
|
|
||||||
qr_needs_compression=qr_needs_compression,
|
|
||||||
qr_too_large=qr_too_large
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
flash(f'Error generating credentials: {e}', 'error')
|
|
||||||
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
|
|
||||||
|
|
||||||
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate/qr/<token>')
|
|
||||||
def generate_qr(token):
|
|
||||||
"""Generate QR code for RSA key."""
|
|
||||||
if not HAS_QRCODE:
|
|
||||||
return "QR code support not available", 501
|
|
||||||
|
|
||||||
if token not in TEMP_FILES:
|
|
||||||
return "Token expired or invalid", 404
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[token]
|
|
||||||
if file_info.get('type') != 'rsa_key':
|
|
||||||
return "Invalid token type", 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
key_pem = file_info['data'].decode('utf-8')
|
|
||||||
compress = file_info.get('compress', False)
|
|
||||||
qr_png = generate_qr_code(key_pem, compress=compress)
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(qr_png),
|
|
||||||
mimetype='image/png',
|
|
||||||
as_attachment=False
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error generating QR code: {e}", 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate/qr-download/<token>')
|
|
||||||
def generate_qr_download(token):
|
|
||||||
"""Download QR code as PNG file."""
|
|
||||||
if not HAS_QRCODE:
|
|
||||||
return "QR code support not available", 501
|
|
||||||
|
|
||||||
if token not in TEMP_FILES:
|
|
||||||
return "Token expired or invalid", 404
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[token]
|
|
||||||
if file_info.get('type') != 'rsa_key':
|
|
||||||
return "Invalid token type", 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
key_pem = file_info['data'].decode('utf-8')
|
|
||||||
compress = file_info.get('compress', False)
|
|
||||||
qr_png = generate_qr_code(key_pem, compress=compress)
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(qr_png),
|
|
||||||
mimetype='image/png',
|
|
||||||
as_attachment=True,
|
|
||||||
download_name='stegasoo_rsa_key_qr.png'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error generating QR code: {e}", 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate/download-key', methods=['POST'])
|
|
||||||
def download_key():
|
|
||||||
"""Download RSA key as password-protected PEM file."""
|
|
||||||
key_pem = request.form.get('key_pem', '')
|
|
||||||
password = request.form.get('key_password', '')
|
|
||||||
|
|
||||||
if not key_pem:
|
|
||||||
flash('No key to download', 'error')
|
|
||||||
return redirect(url_for('generate'))
|
|
||||||
|
|
||||||
if not password or len(password) < 8:
|
|
||||||
flash('Password must be at least 8 characters', 'error')
|
|
||||||
return redirect(url_for('generate'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
private_key = load_rsa_key(key_pem.encode('utf-8'))
|
|
||||||
encrypted_pem = export_rsa_key_pem(private_key, password=password)
|
|
||||||
|
|
||||||
key_id = secrets.token_hex(4)
|
|
||||||
filename = f'stegasoo_key_{private_key.key_size}_{key_id}.pem'
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(encrypted_pem),
|
|
||||||
mimetype='application/x-pem-file',
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=filename
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
flash(f'Error creating key file: {e}', 'error')
|
|
||||||
return redirect(url_for('generate'))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/extract-key-from-qr', methods=['POST'])
|
|
||||||
def extract_key_from_qr_route():
|
|
||||||
"""
|
|
||||||
Extract RSA key from uploaded QR code image.
|
|
||||||
Returns JSON with the extracted key or error.
|
|
||||||
"""
|
|
||||||
if not HAS_QRCODE_READ:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'QR code reading not available. Install pyzbar and libzbar.'
|
|
||||||
}), 501
|
|
||||||
|
|
||||||
qr_image = request.files.get('qr_image')
|
|
||||||
if not qr_image:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'No QR image provided'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
image_data = qr_image.read()
|
|
||||||
key_pem = extract_key_from_qr(image_data)
|
|
||||||
|
|
||||||
if key_pem:
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'key_pem': key_pem
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'No valid RSA key found in QR code'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode', methods=['GET', 'POST'])
|
|
||||||
def encode_page():
|
|
||||||
day_of_week = get_today_day()
|
|
||||||
max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
try:
|
|
||||||
# Get files
|
|
||||||
ref_photo = request.files.get('reference_photo')
|
|
||||||
carrier = request.files.get('carrier')
|
|
||||||
rsa_key_file = request.files.get('rsa_key')
|
|
||||||
payload_file = request.files.get('payload_file')
|
|
||||||
|
|
||||||
if not ref_photo or not carrier:
|
|
||||||
flash('Both reference photo and carrier image are required', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
|
|
||||||
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Get form data
|
|
||||||
message = request.form.get('message', '')
|
|
||||||
day_phrase = request.form.get('day_phrase', '')
|
|
||||||
pin = request.form.get('pin', '').strip()
|
|
||||||
rsa_password = request.form.get('rsa_password', '')
|
|
||||||
payload_type = request.form.get('payload_type', 'text')
|
|
||||||
|
|
||||||
# Determine payload
|
|
||||||
if payload_type == 'file' and payload_file and payload_file.filename:
|
|
||||||
# File payload
|
|
||||||
file_data = payload_file.read()
|
|
||||||
|
|
||||||
result = validate_file_payload(file_data, payload_file.filename)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
mime_type, _ = mimetypes.guess_type(payload_file.filename)
|
|
||||||
payload = FilePayload(
|
|
||||||
data=file_data,
|
|
||||||
filename=payload_file.filename,
|
|
||||||
mime_type=mime_type
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Text message
|
|
||||||
result = validate_message(message)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
payload = message
|
|
||||||
|
|
||||||
if not day_phrase:
|
|
||||||
flash('Day phrase is required', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Read files
|
|
||||||
ref_data = ref_photo.read()
|
|
||||||
carrier_data = carrier.read()
|
|
||||||
|
|
||||||
# Handle RSA key - can come from .pem file or QR code image
|
|
||||||
rsa_key_data = None
|
|
||||||
rsa_key_qr = request.files.get('rsa_key_qr')
|
|
||||||
rsa_key_from_qr = False # Track source for password handling
|
|
||||||
|
|
||||||
if rsa_key_file and rsa_key_file.filename:
|
|
||||||
# RSA key from .pem file
|
|
||||||
rsa_key_data = rsa_key_file.read()
|
|
||||||
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
|
|
||||||
# RSA key from QR code image
|
|
||||||
qr_image_data = rsa_key_qr.read()
|
|
||||||
key_pem = extract_key_from_qr(qr_image_data)
|
|
||||||
if key_pem:
|
|
||||||
rsa_key_data = key_pem.encode('utf-8')
|
|
||||||
rsa_key_from_qr = True # QR keys are never password-protected
|
|
||||||
else:
|
|
||||||
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate security factors
|
|
||||||
result = validate_security_factors(pin, rsa_key_data)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate PIN if provided
|
|
||||||
if pin:
|
|
||||||
result = validate_pin(pin)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Determine key password - QR code keys are never password-protected
|
|
||||||
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
|
||||||
|
|
||||||
# Validate RSA key if provided
|
|
||||||
if rsa_key_data:
|
|
||||||
result = validate_rsa_key(rsa_key_data, key_password)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate carrier image
|
|
||||||
result = validate_image(carrier_data, "Carrier image")
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Get date
|
|
||||||
client_date = request.form.get('client_date', '').strip()
|
|
||||||
if client_date and len(client_date) == 10 and client_date[4] == '-' and client_date[7] == '-':
|
|
||||||
date_str = client_date
|
|
||||||
else:
|
|
||||||
date_str = datetime.now().strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# Encode
|
|
||||||
encode_result = encode(
|
|
||||||
message=payload,
|
|
||||||
reference_photo=ref_data,
|
|
||||||
carrier_image=carrier_data,
|
|
||||||
day_phrase=day_phrase,
|
|
||||||
pin=pin,
|
|
||||||
rsa_key_data=rsa_key_data,
|
|
||||||
rsa_password=key_password,
|
|
||||||
date_str=date_str
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store temporarily
|
|
||||||
file_id = secrets.token_urlsafe(16)
|
|
||||||
cleanup_temp_files()
|
|
||||||
TEMP_FILES[file_id] = {
|
|
||||||
'data': encode_result.stego_image,
|
|
||||||
'filename': encode_result.filename,
|
|
||||||
'timestamp': time.time()
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(url_for('encode_result', file_id=file_id))
|
|
||||||
|
|
||||||
except CapacityError as e:
|
|
||||||
flash(str(e), 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
except StegasooError as e:
|
|
||||||
flash(str(e), 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
except Exception as e:
|
|
||||||
flash(f'Error: {e}', 'error')
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/result/<file_id>')
|
|
||||||
def encode_result(file_id):
|
|
||||||
if file_id not in TEMP_FILES:
|
|
||||||
flash('File expired or not found. Please encode again.', 'error')
|
|
||||||
return redirect(url_for('encode_page'))
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
|
|
||||||
# Generate thumbnail
|
|
||||||
thumbnail_data = generate_thumbnail(file_info['data'])
|
|
||||||
thumbnail_id = None
|
|
||||||
|
|
||||||
if thumbnail_data:
|
|
||||||
thumbnail_id = f"{file_id}_thumb"
|
|
||||||
THUMBNAIL_FILES[thumbnail_id] = thumbnail_data
|
|
||||||
|
|
||||||
return render_template('encode_result.html',
|
|
||||||
file_id=file_id,
|
|
||||||
filename=file_info['filename'],
|
|
||||||
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/thumbnail/<thumb_id>')
|
|
||||||
def encode_thumbnail(thumb_id):
|
|
||||||
"""Serve thumbnail image."""
|
|
||||||
if thumb_id not in THUMBNAIL_FILES:
|
|
||||||
return "Thumbnail not found", 404
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(THUMBNAIL_FILES[thumb_id]),
|
|
||||||
mimetype='image/jpeg',
|
|
||||||
as_attachment=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/download/<file_id>')
|
|
||||||
def encode_download(file_id):
|
|
||||||
if file_id not in TEMP_FILES:
|
|
||||||
flash('File expired or not found.', 'error')
|
|
||||||
return redirect(url_for('encode_page'))
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(file_info['data']),
|
|
||||||
mimetype='image/png',
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=file_info['filename']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/file/<file_id>')
|
|
||||||
def encode_file_route(file_id):
|
|
||||||
"""Serve file for Web Share API."""
|
|
||||||
if file_id not in TEMP_FILES:
|
|
||||||
return "Not found", 404
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(file_info['data']),
|
|
||||||
mimetype='image/png',
|
|
||||||
as_attachment=False,
|
|
||||||
download_name=file_info['filename']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/encode/cleanup/<file_id>', methods=['POST'])
|
|
||||||
def encode_cleanup(file_id):
|
|
||||||
"""Manually cleanup a file after sharing."""
|
|
||||||
TEMP_FILES.pop(file_id, None)
|
|
||||||
|
|
||||||
# Also cleanup thumbnail if exists
|
|
||||||
thumb_id = f"{file_id}_thumb"
|
|
||||||
THUMBNAIL_FILES.pop(thumb_id, None)
|
|
||||||
|
|
||||||
return jsonify({'status': 'ok'})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/decode', methods=['GET', 'POST'])
|
|
||||||
def decode_page():
|
|
||||||
if request.method == 'POST':
|
|
||||||
try:
|
|
||||||
# Get files
|
|
||||||
ref_photo = request.files.get('reference_photo')
|
|
||||||
stego_image = request.files.get('stego_image')
|
|
||||||
rsa_key_file = request.files.get('rsa_key')
|
|
||||||
|
|
||||||
if not ref_photo or not stego_image:
|
|
||||||
flash('Both reference photo and stego image are required', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Get form data
|
|
||||||
day_phrase = request.form.get('day_phrase', '')
|
|
||||||
pin = request.form.get('pin', '').strip()
|
|
||||||
rsa_password = request.form.get('rsa_password', '')
|
|
||||||
|
|
||||||
# Get encoding date from form (detected from filename in JS)
|
|
||||||
stego_date = request.form.get('stego_date', '').strip()
|
|
||||||
|
|
||||||
if not day_phrase:
|
|
||||||
flash('Day phrase is required', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Read files
|
|
||||||
ref_data = ref_photo.read()
|
|
||||||
stego_data = stego_image.read()
|
|
||||||
|
|
||||||
# Handle RSA key - can come from .pem file or QR code image
|
|
||||||
rsa_key_data = None
|
|
||||||
rsa_key_qr = request.files.get('rsa_key_qr')
|
|
||||||
rsa_key_from_qr = False # Track source for password handling
|
|
||||||
|
|
||||||
if rsa_key_file and rsa_key_file.filename:
|
|
||||||
# RSA key from .pem file
|
|
||||||
rsa_key_data = rsa_key_file.read()
|
|
||||||
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
|
|
||||||
# RSA key from QR code image
|
|
||||||
qr_image_data = rsa_key_qr.read()
|
|
||||||
key_pem = extract_key_from_qr(qr_image_data)
|
|
||||||
if key_pem:
|
|
||||||
rsa_key_data = key_pem.encode('utf-8')
|
|
||||||
rsa_key_from_qr = True # QR keys are never password-protected
|
|
||||||
else:
|
|
||||||
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate security factors
|
|
||||||
result = validate_security_factors(pin, rsa_key_data)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Validate PIN if provided
|
|
||||||
if pin:
|
|
||||||
result = validate_pin(pin)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
# Determine key password - QR code keys are never password-protected
|
|
||||||
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
|
|
||||||
|
|
||||||
# Validate RSA key if provided
|
|
||||||
if rsa_key_data:
|
|
||||||
result = validate_rsa_key(rsa_key_data, key_password)
|
|
||||||
if not result.is_valid:
|
|
||||||
flash(result.error_message, 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
with open('/tmp/debug_stego.png', 'wb') as f:
|
|
||||||
f.write(stego_data)
|
|
||||||
with open('/tmp/debug_ref.png', 'wb') as f:
|
|
||||||
f.write(ref_data)
|
|
||||||
with open('/tmp/debug_params.txt', 'w') as f:
|
|
||||||
f.write(f"day_phrase: {day_phrase}\n")
|
|
||||||
f.write(f"pin: {pin}\n")
|
|
||||||
f.write(f"date_str: {stego_date}\n")
|
|
||||||
f.write(f"rsa_key: {len(rsa_key_data) if rsa_key_data else None}\n")
|
|
||||||
|
|
||||||
print(f"DEBUG: Saved inputs to /tmp/debug_*")
|
|
||||||
# Decode
|
|
||||||
decode_result = decode(
|
|
||||||
stego_image=stego_data,
|
|
||||||
reference_photo=ref_data,
|
|
||||||
day_phrase=day_phrase,
|
|
||||||
pin=pin,
|
|
||||||
rsa_key_data=rsa_key_data,
|
|
||||||
rsa_password=key_password,
|
|
||||||
date_str=stego_date if stego_date else None
|
|
||||||
)
|
|
||||||
|
|
||||||
if decode_result.is_file:
|
|
||||||
# File content - store temporarily for download
|
|
||||||
file_id = secrets.token_urlsafe(16)
|
|
||||||
cleanup_temp_files()
|
|
||||||
|
|
||||||
filename = decode_result.filename or 'decoded_file'
|
|
||||||
TEMP_FILES[file_id] = {
|
|
||||||
'data': decode_result.file_data,
|
|
||||||
'filename': filename,
|
|
||||||
'mime_type': decode_result.mime_type,
|
|
||||||
'timestamp': time.time()
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template('decode.html',
|
|
||||||
decoded_file=True,
|
|
||||||
file_id=file_id,
|
|
||||||
filename=filename,
|
|
||||||
file_size=format_size(len(decode_result.file_data)),
|
|
||||||
mime_type=decode_result.mime_type
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Text content
|
|
||||||
return render_template('decode.html', decoded_message=decode_result.message)
|
|
||||||
|
|
||||||
except DecryptionError:
|
|
||||||
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
except StegasooError as e:
|
|
||||||
flash(str(e), 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
except Exception as e:
|
|
||||||
flash(f'Error: {e}', 'error')
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/decode/download/<file_id>')
|
|
||||||
def decode_download(file_id):
|
|
||||||
"""Download decoded file."""
|
|
||||||
if file_id not in TEMP_FILES:
|
|
||||||
flash('File expired or not found.', 'error')
|
|
||||||
return redirect(url_for('decode_page'))
|
|
||||||
|
|
||||||
file_info = TEMP_FILES[file_id]
|
|
||||||
mime_type = file_info.get('mime_type', 'application/octet-stream')
|
|
||||||
|
|
||||||
return send_file(
|
|
||||||
io.BytesIO(file_info['data']),
|
|
||||||
mimetype=mime_type,
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=file_info['filename']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/about')
|
|
||||||
def about():
|
|
||||||
return render_template('about.html',
|
|
||||||
has_argon2=has_argon2(),
|
|
||||||
has_qrcode_read=HAS_QRCODE_READ,
|
|
||||||
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# MAIN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
|
||||||
979
frontends/web/auth.py
Normal file
@@ -0,0 +1,979 @@
|
|||||||
|
"""
|
||||||
|
Stegasoo Authentication Module (v4.1.0)
|
||||||
|
|
||||||
|
Multi-user authentication with role-based access control.
|
||||||
|
- Admin user created at first-run setup
|
||||||
|
- Admin can create up to 16 additional users
|
||||||
|
- Uses Argon2id password hashing
|
||||||
|
- Flask sessions for authentication state
|
||||||
|
- SQLite3 for user storage
|
||||||
|
"""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import secrets
|
||||||
|
import sqlite3
|
||||||
|
import string
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from argon2 import PasswordHasher
|
||||||
|
from argon2.exceptions import VerifyMismatchError
|
||||||
|
from flask import current_app, flash, g, redirect, session, url_for
|
||||||
|
|
||||||
|
# Argon2 password hasher (lighter than stegasoo's 256MB for faster login)
|
||||||
|
ph = PasswordHasher(
|
||||||
|
time_cost=3,
|
||||||
|
memory_cost=65536, # 64MB
|
||||||
|
parallelism=4,
|
||||||
|
hash_len=32,
|
||||||
|
salt_len=16,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
MAX_USERS = 16 # Plus 1 admin = 17 total
|
||||||
|
MAX_CHANNEL_KEYS = 10 # Per user
|
||||||
|
ROLE_ADMIN = "admin"
|
||||||
|
ROLE_USER = "user"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class User:
|
||||||
|
"""User data class."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
role: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin(self) -> bool:
|
||||||
|
return self.role == ROLE_ADMIN
|
||||||
|
|
||||||
|
|
||||||
|
def get_db_path() -> Path:
|
||||||
|
"""Get database path in Flask instance folder."""
|
||||||
|
instance_path = Path(current_app.instance_path)
|
||||||
|
instance_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
return instance_path / "stegasoo.db"
|
||||||
|
|
||||||
|
|
||||||
|
def get_db() -> sqlite3.Connection:
|
||||||
|
"""Get database connection, cached on Flask g object."""
|
||||||
|
if "db" not in g:
|
||||||
|
g.db = sqlite3.connect(get_db_path())
|
||||||
|
g.db.row_factory = sqlite3.Row
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
|
||||||
|
def close_db(e=None):
|
||||||
|
"""Close database connection at end of request."""
|
||||||
|
db = g.pop("db", None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
"""Initialize database schema with migration support."""
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
|
# Check if we need to migrate from old single-user schema
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='admin_user'"
|
||||||
|
)
|
||||||
|
has_old_table = cursor.fetchone() is not None
|
||||||
|
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='users'"
|
||||||
|
)
|
||||||
|
has_new_table = cursor.fetchone() is not None
|
||||||
|
|
||||||
|
if has_old_table and not has_new_table:
|
||||||
|
# Migrate from old schema
|
||||||
|
_migrate_from_single_user(db)
|
||||||
|
elif not has_new_table:
|
||||||
|
# Fresh install - create new schema
|
||||||
|
_create_schema(db)
|
||||||
|
else:
|
||||||
|
# Existing install - check for new tables (migrations)
|
||||||
|
_ensure_channel_keys_table(db)
|
||||||
|
_ensure_app_settings_table(db)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_schema(db: sqlite3.Connection):
|
||||||
|
"""Create the multi-user schema."""
|
||||||
|
db.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL DEFAULT 'user',
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_channel_keys (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
channel_key TEXT NOT NULL,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at TEXT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(user_id, channel_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_channel_keys_user ON user_channel_keys(user_id);
|
||||||
|
|
||||||
|
-- App-level settings (v4.1.0)
|
||||||
|
-- Stores recovery key hash and other instance-wide settings
|
||||||
|
CREATE TABLE IF NOT EXISTS app_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_from_single_user(db: sqlite3.Connection):
|
||||||
|
"""Migrate from old single-user admin_user table to multi-user users table."""
|
||||||
|
# Create new table
|
||||||
|
_create_schema(db)
|
||||||
|
|
||||||
|
# Copy admin user from old table
|
||||||
|
old_user = db.execute(
|
||||||
|
"SELECT username, password_hash, created_at FROM admin_user WHERE id = 1"
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if old_user:
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (username, password_hash, role, created_at)
|
||||||
|
VALUES (?, ?, 'admin', ?)
|
||||||
|
""",
|
||||||
|
(old_user["username"], old_user["password_hash"], old_user["created_at"]),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Drop old table
|
||||||
|
db.execute("DROP TABLE admin_user")
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_channel_keys_table(db: sqlite3.Connection):
|
||||||
|
"""Ensure user_channel_keys table exists (migration for existing installs)."""
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='user_channel_keys'"
|
||||||
|
)
|
||||||
|
if cursor.fetchone() is None:
|
||||||
|
db.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_channel_keys (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
channel_key TEXT NOT NULL,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at TEXT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(user_id, channel_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_channel_keys_user ON user_channel_keys(user_id);
|
||||||
|
""")
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_app_settings_table(db: sqlite3.Connection):
|
||||||
|
"""Ensure app_settings table exists (v4.1.0 migration)."""
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='app_settings'"
|
||||||
|
)
|
||||||
|
if cursor.fetchone() is None:
|
||||||
|
db.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS app_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# App Settings (v4.1.0)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_setting(key: str) -> str | None:
|
||||||
|
"""Get an app-level setting value."""
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT value FROM app_settings WHERE key = ?", (key,)
|
||||||
|
).fetchone()
|
||||||
|
return row["value"] if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def set_app_setting(key: str, value: str) -> None:
|
||||||
|
"""Set an app-level setting value."""
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO app_settings (key, value)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
""",
|
||||||
|
(key, value, value),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_app_setting(key: str) -> bool:
|
||||||
|
"""Delete an app-level setting. Returns True if deleted."""
|
||||||
|
db = get_db()
|
||||||
|
cursor = db.execute("DELETE FROM app_settings WHERE key = ?", (key,))
|
||||||
|
db.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Recovery Key Management (v4.1.0)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# Setting key for recovery hash
|
||||||
|
RECOVERY_KEY_SETTING = "recovery_key_hash"
|
||||||
|
|
||||||
|
|
||||||
|
def has_recovery_key() -> bool:
|
||||||
|
"""Check if a recovery key has been configured."""
|
||||||
|
return get_app_setting(RECOVERY_KEY_SETTING) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def get_recovery_key_hash() -> str | None:
|
||||||
|
"""Get the stored recovery key hash."""
|
||||||
|
return get_app_setting(RECOVERY_KEY_SETTING)
|
||||||
|
|
||||||
|
|
||||||
|
def set_recovery_key_hash(key_hash: str) -> None:
|
||||||
|
"""Store a recovery key hash."""
|
||||||
|
set_app_setting(RECOVERY_KEY_SETTING, key_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_recovery_key() -> bool:
|
||||||
|
"""Remove the recovery key. Returns True if removed."""
|
||||||
|
return delete_app_setting(RECOVERY_KEY_SETTING)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_and_reset_admin_password(recovery_key: str, new_password: str) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Verify recovery key and reset the first admin's password.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recovery_key: User-provided recovery key
|
||||||
|
new_password: New password to set
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(success, message) tuple
|
||||||
|
"""
|
||||||
|
from stegasoo.recovery import verify_recovery_key
|
||||||
|
|
||||||
|
stored_hash = get_recovery_key_hash()
|
||||||
|
if not stored_hash:
|
||||||
|
return False, "No recovery key configured for this instance"
|
||||||
|
|
||||||
|
if not verify_recovery_key(recovery_key, stored_hash):
|
||||||
|
return False, "Invalid recovery key"
|
||||||
|
|
||||||
|
# Find first admin user
|
||||||
|
db = get_db()
|
||||||
|
admin = db.execute(
|
||||||
|
"SELECT id, username FROM users WHERE role = 'admin' ORDER BY id LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not admin:
|
||||||
|
return False, "No admin user found"
|
||||||
|
|
||||||
|
# Reset password
|
||||||
|
new_hash = ph.hash(new_password)
|
||||||
|
db.execute(
|
||||||
|
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||||
|
(new_hash, admin["id"]),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Invalidate all sessions for this user
|
||||||
|
invalidate_user_sessions(admin["id"])
|
||||||
|
|
||||||
|
return True, f"Password reset for '{admin['username']}'"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# User Queries
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def any_users_exist() -> bool:
|
||||||
|
"""Check if any users have been created (for first-run detection)."""
|
||||||
|
db = get_db()
|
||||||
|
result = db.execute("SELECT 1 FROM users LIMIT 1").fetchone()
|
||||||
|
return result is not None
|
||||||
|
|
||||||
|
|
||||||
|
def user_exists() -> bool:
|
||||||
|
"""Alias for any_users_exist() for backwards compatibility."""
|
||||||
|
return any_users_exist()
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_count() -> int:
|
||||||
|
"""Get total number of users."""
|
||||||
|
db = get_db()
|
||||||
|
result = db.execute("SELECT COUNT(*) FROM users").fetchone()
|
||||||
|
return result[0] if result else 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_non_admin_count() -> int:
|
||||||
|
"""Get number of non-admin users."""
|
||||||
|
db = get_db()
|
||||||
|
result = db.execute("SELECT COUNT(*) FROM users WHERE role != 'admin'").fetchone()
|
||||||
|
return result[0] if result else 0
|
||||||
|
|
||||||
|
|
||||||
|
def can_create_user() -> bool:
|
||||||
|
"""Check if we can create more users (within limit)."""
|
||||||
|
return get_non_admin_count() < MAX_USERS
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_by_id(user_id: int) -> User | None:
|
||||||
|
"""Get user by ID."""
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT id, username, role, created_at FROM users WHERE id = ?", (user_id,)
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
return User(
|
||||||
|
id=row["id"],
|
||||||
|
username=row["username"],
|
||||||
|
role=row["role"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_by_username(username: str) -> User | None:
|
||||||
|
"""Get user by username."""
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT id, username, role, created_at FROM users WHERE username = ?",
|
||||||
|
(username,),
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
return User(
|
||||||
|
id=row["id"],
|
||||||
|
username=row["username"],
|
||||||
|
role=row["role"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_users() -> list[User]:
|
||||||
|
"""Get all users, admins first, then by creation date."""
|
||||||
|
db = get_db()
|
||||||
|
rows = db.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, username, role, created_at FROM users
|
||||||
|
ORDER BY role = 'admin' DESC, created_at ASC
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
return [
|
||||||
|
User(
|
||||||
|
id=row["id"],
|
||||||
|
username=row["username"],
|
||||||
|
role=row["role"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user() -> User | None:
|
||||||
|
"""Get the currently logged-in user from session."""
|
||||||
|
user_id = session.get("user_id")
|
||||||
|
if user_id:
|
||||||
|
return get_user_by_id(user_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_username() -> str:
|
||||||
|
"""Get current user's username (backwards compatibility)."""
|
||||||
|
user = get_current_user()
|
||||||
|
return user.username if user else "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Authentication
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def verify_user_password(username: str, password: str) -> User | None:
|
||||||
|
"""
|
||||||
|
Verify password for a user.
|
||||||
|
|
||||||
|
Returns User if valid, None if invalid.
|
||||||
|
Also rehashes password if needed.
|
||||||
|
"""
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT id, username, role, created_at, password_hash FROM users WHERE username = ?",
|
||||||
|
(username,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ph.verify(row["password_hash"], password)
|
||||||
|
|
||||||
|
# Rehash if parameters changed
|
||||||
|
if ph.check_needs_rehash(row["password_hash"]):
|
||||||
|
new_hash = ph.hash(password)
|
||||||
|
db.execute(
|
||||||
|
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||||
|
(new_hash, row["id"]),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return User(
|
||||||
|
id=row["id"],
|
||||||
|
username=row["username"],
|
||||||
|
role=row["role"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
)
|
||||||
|
except VerifyMismatchError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password: str) -> bool:
|
||||||
|
"""Verify password for current user (backwards compatibility)."""
|
||||||
|
user = get_current_user()
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
result = verify_user_password(user.username, password)
|
||||||
|
return result is not None
|
||||||
|
|
||||||
|
|
||||||
|
def is_authenticated() -> bool:
|
||||||
|
"""Check if current session is authenticated."""
|
||||||
|
return session.get("user_id") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def is_admin() -> bool:
|
||||||
|
"""Check if current user is an admin."""
|
||||||
|
user = get_current_user()
|
||||||
|
return user.is_admin if user else False
|
||||||
|
|
||||||
|
|
||||||
|
def login_user(user: User):
|
||||||
|
"""Set up session for logged-in user."""
|
||||||
|
session["user_id"] = user.id
|
||||||
|
session["username"] = user.username
|
||||||
|
session["role"] = user.role
|
||||||
|
# Legacy compatibility
|
||||||
|
session["authenticated"] = True
|
||||||
|
|
||||||
|
|
||||||
|
def logout_user():
|
||||||
|
"""Clear session for logout."""
|
||||||
|
session.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# User Management
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def generate_temp_password(length: int = 8) -> str:
|
||||||
|
"""Generate a random temporary password."""
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_username(username: str) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Validate username format.
|
||||||
|
|
||||||
|
Rules: 3-80 chars, alphanumeric + underscore/hyphen + @/. for email-style
|
||||||
|
"""
|
||||||
|
if not username:
|
||||||
|
return False, "Username is required"
|
||||||
|
|
||||||
|
if len(username) < 3:
|
||||||
|
return False, "Username must be at least 3 characters"
|
||||||
|
|
||||||
|
if len(username) > 80:
|
||||||
|
return False, "Username must be at most 80 characters"
|
||||||
|
|
||||||
|
# Allow: alphanumeric, underscore, hyphen, @, . (for email-style)
|
||||||
|
allowed = set(string.ascii_letters + string.digits + "_-@.")
|
||||||
|
if not all(c in allowed for c in username):
|
||||||
|
return False, "Username can only contain letters, numbers, underscore, hyphen, @ and ."
|
||||||
|
|
||||||
|
# Must start with letter or number
|
||||||
|
if username[0] not in string.ascii_letters + string.digits:
|
||||||
|
return False, "Username must start with a letter or number"
|
||||||
|
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password(password: str) -> tuple[bool, str]:
|
||||||
|
"""Validate password requirements."""
|
||||||
|
if not password:
|
||||||
|
return False, "Password is required"
|
||||||
|
|
||||||
|
if len(password) < 8:
|
||||||
|
return False, "Password must be at least 8 characters"
|
||||||
|
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(
|
||||||
|
username: str, password: str, role: str = ROLE_USER
|
||||||
|
) -> tuple[bool, str, User | None]:
|
||||||
|
"""
|
||||||
|
Create a new user.
|
||||||
|
|
||||||
|
Returns (success, message, user).
|
||||||
|
"""
|
||||||
|
# Validate username
|
||||||
|
valid, msg = validate_username(username)
|
||||||
|
if not valid:
|
||||||
|
return False, msg, None
|
||||||
|
|
||||||
|
# Validate password
|
||||||
|
valid, msg = validate_password(password)
|
||||||
|
if not valid:
|
||||||
|
return False, msg, None
|
||||||
|
|
||||||
|
# Check if username already exists
|
||||||
|
if get_user_by_username(username):
|
||||||
|
return False, "Username already exists", None
|
||||||
|
|
||||||
|
# Check user limit (only for non-admin users)
|
||||||
|
if role != ROLE_ADMIN and not can_create_user():
|
||||||
|
return False, f"Maximum of {MAX_USERS} users reached", None
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
password_hash = ph.hash(password)
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (username, password_hash, role)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(username, password_hash, role),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
user = get_user_by_id(cursor.lastrowid)
|
||||||
|
return True, "User created successfully", user
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
return False, "Username already exists", None
|
||||||
|
|
||||||
|
|
||||||
|
def create_admin_user(username: str, password: str) -> tuple[bool, str]:
|
||||||
|
"""Create the initial admin user (first-run setup)."""
|
||||||
|
if any_users_exist():
|
||||||
|
return False, "Admin user already exists"
|
||||||
|
|
||||||
|
success, msg, _ = create_user(username, password, ROLE_ADMIN)
|
||||||
|
return success, msg
|
||||||
|
|
||||||
|
|
||||||
|
def change_password(
|
||||||
|
user_id: int, current_password: str, new_password: str
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
"""Change a user's password (requires current password)."""
|
||||||
|
user = get_user_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
return False, "User not found"
|
||||||
|
|
||||||
|
# Verify current password
|
||||||
|
if not verify_user_password(user.username, current_password):
|
||||||
|
return False, "Current password is incorrect"
|
||||||
|
|
||||||
|
# Validate new password
|
||||||
|
valid, msg = validate_password(new_password)
|
||||||
|
if not valid:
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
# Update password
|
||||||
|
new_hash = ph.hash(new_password)
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||||
|
(new_hash, user_id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True, "Password changed successfully"
|
||||||
|
|
||||||
|
|
||||||
|
def reset_user_password(user_id: int, new_password: str) -> tuple[bool, str]:
|
||||||
|
"""Reset a user's password (admin function, no current password required)."""
|
||||||
|
user = get_user_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
return False, "User not found"
|
||||||
|
|
||||||
|
# Validate new password
|
||||||
|
valid, msg = validate_password(new_password)
|
||||||
|
if not valid:
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
# Update password
|
||||||
|
new_hash = ph.hash(new_password)
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||||
|
(new_hash, user_id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Invalidate user's sessions
|
||||||
|
invalidate_user_sessions(user_id)
|
||||||
|
|
||||||
|
return True, "Password reset successfully"
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user(user_id: int, current_user_id: int) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Delete a user.
|
||||||
|
|
||||||
|
Cannot delete yourself or the last admin.
|
||||||
|
"""
|
||||||
|
if user_id == current_user_id:
|
||||||
|
return False, "Cannot delete yourself"
|
||||||
|
|
||||||
|
user = get_user_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
return False, "User not found"
|
||||||
|
|
||||||
|
# Check if this is the last admin
|
||||||
|
if user.role == ROLE_ADMIN:
|
||||||
|
db = get_db()
|
||||||
|
admin_count = db.execute(
|
||||||
|
"SELECT COUNT(*) FROM users WHERE role = 'admin'"
|
||||||
|
).fetchone()[0]
|
||||||
|
if admin_count <= 1:
|
||||||
|
return False, "Cannot delete the last admin"
|
||||||
|
|
||||||
|
# Invalidate user's sessions before deletion
|
||||||
|
invalidate_user_sessions(user_id)
|
||||||
|
|
||||||
|
# Delete user
|
||||||
|
db = get_db()
|
||||||
|
db.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return True, f"User '{user.username}' deleted"
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_user_sessions(user_id: int):
|
||||||
|
"""
|
||||||
|
Invalidate all sessions for a user.
|
||||||
|
|
||||||
|
This is called when a user is deleted or their password is reset.
|
||||||
|
Since we use server-side sessions, we increment a "session version"
|
||||||
|
that's checked on each request.
|
||||||
|
"""
|
||||||
|
# For Flask's default session (client-side), we can't truly invalidate.
|
||||||
|
# But we can add a check - store a "valid_from" timestamp in the DB
|
||||||
|
# and compare against session creation time.
|
||||||
|
#
|
||||||
|
# For now, we'll use a simpler approach: store invalidated user IDs
|
||||||
|
# in app config (memory) which gets checked by login_required.
|
||||||
|
#
|
||||||
|
# This works for single-process deployments (like RPi).
|
||||||
|
# For multi-process, would need Redis or DB-backed sessions.
|
||||||
|
|
||||||
|
if "invalidated_users" not in current_app.config:
|
||||||
|
current_app.config["invalidated_users"] = set()
|
||||||
|
|
||||||
|
current_app.config["invalidated_users"].add(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def is_session_valid() -> bool:
|
||||||
|
"""Check if current session is still valid (user not deleted/invalidated)."""
|
||||||
|
user_id = session.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if user was invalidated
|
||||||
|
invalidated = current_app.config.get("invalidated_users", set())
|
||||||
|
if user_id in invalidated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if user still exists
|
||||||
|
if not get_user_by_id(user_id):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Channel Keys
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChannelKey:
|
||||||
|
"""Saved channel key data class."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
user_id: int
|
||||||
|
name: str
|
||||||
|
channel_key: str
|
||||||
|
created_at: str
|
||||||
|
last_used_at: str | None
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_channel_keys(user_id: int) -> list[ChannelKey]:
|
||||||
|
"""Get all saved channel keys for a user, most recently used first."""
|
||||||
|
db = get_db()
|
||||||
|
rows = db.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, user_id, name, channel_key, created_at, last_used_at
|
||||||
|
FROM user_channel_keys
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY last_used_at DESC NULLS LAST, created_at DESC
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
).fetchall()
|
||||||
|
return [
|
||||||
|
ChannelKey(
|
||||||
|
id=row["id"],
|
||||||
|
user_id=row["user_id"],
|
||||||
|
name=row["name"],
|
||||||
|
channel_key=row["channel_key"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
last_used_at=row["last_used_at"],
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_key_by_id(key_id: int, user_id: int) -> ChannelKey | None:
|
||||||
|
"""Get a specific channel key (ensures user owns it)."""
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, user_id, name, channel_key, created_at, last_used_at
|
||||||
|
FROM user_channel_keys
|
||||||
|
WHERE id = ? AND user_id = ?
|
||||||
|
""",
|
||||||
|
(key_id, user_id),
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
return ChannelKey(
|
||||||
|
id=row["id"],
|
||||||
|
user_id=row["user_id"],
|
||||||
|
name=row["name"],
|
||||||
|
channel_key=row["channel_key"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
last_used_at=row["last_used_at"],
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_key_count(user_id: int) -> int:
|
||||||
|
"""Get count of saved channel keys for a user."""
|
||||||
|
db = get_db()
|
||||||
|
result = db.execute(
|
||||||
|
"SELECT COUNT(*) FROM user_channel_keys WHERE user_id = ?", (user_id,)
|
||||||
|
).fetchone()
|
||||||
|
return result[0] if result else 0
|
||||||
|
|
||||||
|
|
||||||
|
def can_save_channel_key(user_id: int) -> bool:
|
||||||
|
"""Check if user can save more channel keys (within limit)."""
|
||||||
|
return get_channel_key_count(user_id) < MAX_CHANNEL_KEYS
|
||||||
|
|
||||||
|
|
||||||
|
def save_channel_key(
|
||||||
|
user_id: int, name: str, channel_key: str
|
||||||
|
) -> tuple[bool, str, ChannelKey | None]:
|
||||||
|
"""
|
||||||
|
Save a channel key for a user.
|
||||||
|
|
||||||
|
Returns (success, message, key).
|
||||||
|
"""
|
||||||
|
# Validate name
|
||||||
|
name = name.strip()
|
||||||
|
if not name:
|
||||||
|
return False, "Key name is required", None
|
||||||
|
if len(name) > 50:
|
||||||
|
return False, "Key name must be at most 50 characters", None
|
||||||
|
|
||||||
|
# Validate channel key format (hex string)
|
||||||
|
channel_key = channel_key.strip().lower()
|
||||||
|
if not channel_key:
|
||||||
|
return False, "Channel key is required", None
|
||||||
|
if not all(c in "0123456789abcdef" for c in channel_key):
|
||||||
|
return False, "Invalid channel key format", None
|
||||||
|
|
||||||
|
# Check limit
|
||||||
|
if not can_save_channel_key(user_id):
|
||||||
|
return False, f"Maximum of {MAX_CHANNEL_KEYS} saved keys reached", None
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO user_channel_keys (user_id, name, channel_key)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(user_id, name, channel_key),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
key = get_channel_key_by_id(cursor.lastrowid, user_id)
|
||||||
|
return True, "Channel key saved", key
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
return False, "This channel key is already saved", None
|
||||||
|
|
||||||
|
|
||||||
|
def update_channel_key_name(
|
||||||
|
key_id: int, user_id: int, new_name: str
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
"""Update the name of a saved channel key."""
|
||||||
|
new_name = new_name.strip()
|
||||||
|
if not new_name:
|
||||||
|
return False, "Key name is required"
|
||||||
|
if len(new_name) > 50:
|
||||||
|
return False, "Key name must be at most 50 characters"
|
||||||
|
|
||||||
|
key = get_channel_key_by_id(key_id, user_id)
|
||||||
|
if not key:
|
||||||
|
return False, "Channel key not found"
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"UPDATE user_channel_keys SET name = ? WHERE id = ? AND user_id = ?",
|
||||||
|
(new_name, key_id, user_id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return True, "Key name updated"
|
||||||
|
|
||||||
|
|
||||||
|
def update_channel_key_last_used(key_id: int, user_id: int):
|
||||||
|
"""Update the last_used_at timestamp for a channel key."""
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE user_channel_keys
|
||||||
|
SET last_used_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ? AND user_id = ?
|
||||||
|
""",
|
||||||
|
(key_id, user_id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_channel_key(key_id: int, user_id: int) -> tuple[bool, str]:
|
||||||
|
"""Delete a saved channel key."""
|
||||||
|
key = get_channel_key_by_id(key_id, user_id)
|
||||||
|
if not key:
|
||||||
|
return False, "Channel key not found"
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"DELETE FROM user_channel_keys WHERE id = ? AND user_id = ?",
|
||||||
|
(key_id, user_id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return True, f"Key '{key.name}' deleted"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Decorators
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(f):
|
||||||
|
"""Decorator to require login for a route."""
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
# Check if auth is enabled
|
||||||
|
if not current_app.config.get("AUTH_ENABLED", True):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
# Check for first-run setup
|
||||||
|
if not any_users_exist():
|
||||||
|
return redirect(url_for("setup"))
|
||||||
|
|
||||||
|
# Check authentication
|
||||||
|
if not is_authenticated():
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
# Check if session is still valid (user not deleted)
|
||||||
|
if not is_session_valid():
|
||||||
|
logout_user()
|
||||||
|
flash("Your session has expired. Please log in again.", "warning")
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(f):
|
||||||
|
"""Decorator to require admin role for a route."""
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
# Check if auth is enabled
|
||||||
|
if not current_app.config.get("AUTH_ENABLED", True):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
# Check for first-run setup
|
||||||
|
if not any_users_exist():
|
||||||
|
return redirect(url_for("setup"))
|
||||||
|
|
||||||
|
# Check authentication
|
||||||
|
if not is_authenticated():
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
# Check if session is still valid
|
||||||
|
if not is_session_valid():
|
||||||
|
logout_user()
|
||||||
|
flash("Your session has expired. Please log in again.", "warning")
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
# Check admin role
|
||||||
|
if not is_admin():
|
||||||
|
flash("Admin access required", "error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# App Initialization
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
"""Initialize auth module with Flask app."""
|
||||||
|
app.teardown_appcontext(close_db)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
init_db()
|
||||||
52
frontends/web/dev_run.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Stegasoo Web Frontend - Development Runner
|
||||||
|
# Press 'r' to restart, 'q' to quit (single keypress, no Enter needed)
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
PID=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo -e "\n\033[33mShutting down...\033[0m"
|
||||||
|
[[ -n "$PID" ]] && kill "$PID" 2>/dev/null
|
||||||
|
stty sane 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup SIGINT SIGTERM EXIT
|
||||||
|
|
||||||
|
start_server() {
|
||||||
|
clear
|
||||||
|
echo -e "\033[36m┌──────────────────────────────────────┐\033[0m"
|
||||||
|
echo -e "\033[36m│ Stegasoo Dev Server │\033[0m"
|
||||||
|
echo -e "\033[36m│ \033[0m[r] restart [q] quit\033[36m │\033[0m"
|
||||||
|
echo -e "\033[36m└──────────────────────────────────────┘\033[0m"
|
||||||
|
|
||||||
|
pkill -f "python app.py" 2>/dev/null
|
||||||
|
sleep 0.3
|
||||||
|
|
||||||
|
python app.py 2>&1 &
|
||||||
|
PID=$!
|
||||||
|
echo -e "\033[32m✓ Running on http://localhost:5000 (PID: $PID)\033[0m\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_server
|
||||||
|
|
||||||
|
# Single keypress mode
|
||||||
|
stty -echo -icanon time 0 min 0
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
key=$(dd bs=1 count=1 2>/dev/null)
|
||||||
|
case "$key" in
|
||||||
|
r|R) start_server ;;
|
||||||
|
q|Q) cleanup ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if crashed
|
||||||
|
if [[ -n "$PID" ]] && ! kill -0 "$PID" 2>/dev/null; then
|
||||||
|
echo -e "\033[31m✗ Crashed! Press 'r' to restart\033[0m"
|
||||||
|
PID=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
75
frontends/web/docker-entrypoint.sh
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Docker entrypoint for Stegasoo Web UI
|
||||||
|
# Handles SSL certificate generation and gunicorn startup
|
||||||
|
#
|
||||||
|
# Supports mkcert for browser-trusted certificates (no warning screen)
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CERT_DIR="/app/frontends/web/certs"
|
||||||
|
CERT_FILE="$CERT_DIR/cert.pem"
|
||||||
|
KEY_FILE="$CERT_DIR/key.pem"
|
||||||
|
HOSTNAME="${STEGASOO_HOSTNAME:-localhost}"
|
||||||
|
|
||||||
|
# Generate SSL certificates
|
||||||
|
# Priority: 1) Existing certs, 2) mkcert (trusted), 3) openssl (self-signed)
|
||||||
|
generate_certs() {
|
||||||
|
if [ -f "$CERT_FILE" ] && [ -f "$KEY_FILE" ]; then
|
||||||
|
echo "Using existing SSL certificates."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$CERT_DIR"
|
||||||
|
|
||||||
|
# Try mkcert first (creates browser-trusted certs)
|
||||||
|
if command -v mkcert &> /dev/null; then
|
||||||
|
echo "Generating trusted certificate with mkcert for $HOSTNAME..."
|
||||||
|
cd "$CERT_DIR"
|
||||||
|
mkcert -key-file key.pem -cert-file cert.pem "$HOSTNAME" localhost 127.0.0.1 ::1
|
||||||
|
echo "Trusted certificate generated."
|
||||||
|
echo ""
|
||||||
|
echo " To trust on other devices, install the CA cert from:"
|
||||||
|
echo " $(mkcert -CAROOT)/rootCA.pem"
|
||||||
|
echo ""
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback to self-signed (shows browser warning)
|
||||||
|
echo "Generating self-signed SSL certificate for $HOSTNAME..."
|
||||||
|
echo "(Install mkcert for browser-trusted certs without warnings)"
|
||||||
|
|
||||||
|
openssl req -x509 -newkey rsa:2048 \
|
||||||
|
-keyout "$KEY_FILE" \
|
||||||
|
-out "$CERT_FILE" \
|
||||||
|
-sha256 -days 365 -nodes \
|
||||||
|
-subj "/CN=$HOSTNAME" \
|
||||||
|
-addext "subjectAltName=DNS:$HOSTNAME,DNS:localhost,IP:127.0.0.1" \
|
||||||
|
2>/dev/null
|
||||||
|
|
||||||
|
echo "Self-signed certificate generated."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start gunicorn with appropriate settings
|
||||||
|
if [ "${STEGASOO_HTTPS_ENABLED:-false}" = "true" ]; then
|
||||||
|
echo "HTTPS mode enabled"
|
||||||
|
generate_certs
|
||||||
|
|
||||||
|
exec gunicorn \
|
||||||
|
--bind 0.0.0.0:5000 \
|
||||||
|
--workers 2 \
|
||||||
|
--threads 4 \
|
||||||
|
--timeout 120 \
|
||||||
|
--certfile "$CERT_FILE" \
|
||||||
|
--keyfile "$KEY_FILE" \
|
||||||
|
app:app
|
||||||
|
else
|
||||||
|
echo "HTTP mode (HTTPS disabled)"
|
||||||
|
exec gunicorn \
|
||||||
|
--bind 0.0.0.0:5000 \
|
||||||
|
--workers 2 \
|
||||||
|
--threads 4 \
|
||||||
|
--timeout 120 \
|
||||||
|
app:app
|
||||||
|
fi
|
||||||
153
frontends/web/ssl_utils.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
SSL Certificate Utilities
|
||||||
|
|
||||||
|
Auto-generates self-signed certificates for HTTPS.
|
||||||
|
Uses cryptography library (already a dependency).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import ipaddress
|
||||||
|
import socket
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
|
||||||
|
|
||||||
|
def _get_local_ips() -> list[str]:
|
||||||
|
"""Get local IP addresses for this machine."""
|
||||||
|
ips = []
|
||||||
|
try:
|
||||||
|
# Get hostname and resolve to IP
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
for addr_info in socket.getaddrinfo(hostname, None, socket.AF_INET):
|
||||||
|
ip = addr_info[4][0]
|
||||||
|
if ip not in ips and not ip.startswith("127."):
|
||||||
|
ips.append(ip)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Also try connecting to external to get primary interface IP
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
if ip not in ips:
|
||||||
|
ips.append(ip)
|
||||||
|
s.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ips
|
||||||
|
|
||||||
|
|
||||||
|
def get_cert_paths(base_dir: Path) -> tuple[Path, Path]:
|
||||||
|
"""Get paths for cert and key files."""
|
||||||
|
cert_dir = base_dir / "certs"
|
||||||
|
cert_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return cert_dir / "server.crt", cert_dir / "server.key"
|
||||||
|
|
||||||
|
|
||||||
|
def certs_exist(base_dir: Path) -> bool:
|
||||||
|
"""Check if both cert files exist."""
|
||||||
|
cert_path, key_path = get_cert_paths(base_dir)
|
||||||
|
return cert_path.exists() and key_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_self_signed_cert(
|
||||||
|
base_dir: Path,
|
||||||
|
hostname: str = "localhost",
|
||||||
|
days_valid: int = 365,
|
||||||
|
) -> tuple[Path, Path]:
|
||||||
|
"""
|
||||||
|
Generate self-signed SSL certificate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_dir: Base directory for certs folder
|
||||||
|
hostname: Server hostname for certificate
|
||||||
|
days_valid: Certificate validity in days
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (cert_path, key_path)
|
||||||
|
"""
|
||||||
|
cert_path, key_path = get_cert_paths(base_dir)
|
||||||
|
|
||||||
|
# Generate RSA key
|
||||||
|
key = rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=2048,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create certificate
|
||||||
|
subject = issuer = x509.Name([
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Stegasoo"),
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Subject Alternative Names
|
||||||
|
san_list = [
|
||||||
|
x509.DNSName(hostname),
|
||||||
|
x509.DNSName("localhost"),
|
||||||
|
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add hostname.local for mDNS access
|
||||||
|
if not hostname.endswith(".local"):
|
||||||
|
san_list.append(x509.DNSName(f"{hostname}.local"))
|
||||||
|
|
||||||
|
# Add the hostname as IP if it looks like one
|
||||||
|
try:
|
||||||
|
san_list.append(x509.IPAddress(ipaddress.IPv4Address(hostname)))
|
||||||
|
except ipaddress.AddressValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add local network IPs
|
||||||
|
for local_ip in _get_local_ips():
|
||||||
|
try:
|
||||||
|
ip_addr = ipaddress.IPv4Address(local_ip)
|
||||||
|
if x509.IPAddress(ip_addr) not in san_list:
|
||||||
|
san_list.append(x509.IPAddress(ip_addr))
|
||||||
|
except (ipaddress.AddressValueError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(subject)
|
||||||
|
.issuer_name(issuer)
|
||||||
|
.public_key(key.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(now)
|
||||||
|
.not_valid_after(now + datetime.timedelta(days=days_valid))
|
||||||
|
.add_extension(
|
||||||
|
x509.SubjectAlternativeName(san_list),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.sign(key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write key file (chmod 600)
|
||||||
|
key_path.write_bytes(
|
||||||
|
key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
key_path.chmod(0o600)
|
||||||
|
|
||||||
|
# Write cert file
|
||||||
|
cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
|
||||||
|
|
||||||
|
return cert_path, key_path
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_certs(base_dir: Path, hostname: str = "localhost") -> tuple[Path, Path]:
|
||||||
|
"""Ensure certificates exist, generating if needed."""
|
||||||
|
if certs_exist(base_dir):
|
||||||
|
return get_cert_paths(base_dir)
|
||||||
|
|
||||||
|
print(f"Generating self-signed SSL certificate for {hostname}...")
|
||||||
|
return generate_self_signed_cert(base_dir, hostname)
|
||||||
142
frontends/web/static/js/auth.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Stegasoo Authentication Pages JavaScript
|
||||||
|
* Handles login, setup, account, and admin user management pages
|
||||||
|
*/
|
||||||
|
|
||||||
|
const StegasooAuth = {
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// PASSWORD VISIBILITY TOGGLE
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle password field visibility
|
||||||
|
* @param {string} inputId - ID of the password input
|
||||||
|
* @param {HTMLElement} btn - The toggle button element
|
||||||
|
*/
|
||||||
|
togglePassword(inputId, btn) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
icon?.classList.replace('bi-eye', 'bi-eye-slash');
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
icon?.classList.replace('bi-eye-slash', 'bi-eye');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// PASSWORD CONFIRMATION VALIDATION
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize password confirmation validation on a form
|
||||||
|
* @param {string} formId - ID of the form
|
||||||
|
* @param {string} passwordId - ID of the password field
|
||||||
|
* @param {string} confirmId - ID of the confirmation field
|
||||||
|
*/
|
||||||
|
initPasswordConfirmation(formId, passwordId, confirmId) {
|
||||||
|
const form = document.getElementById(formId);
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
const password = document.getElementById(passwordId)?.value;
|
||||||
|
const confirm = document.getElementById(confirmId)?.value;
|
||||||
|
|
||||||
|
if (password !== confirm) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Passwords do not match');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// COPY TO CLIPBOARD
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy field value to clipboard with visual feedback
|
||||||
|
* @param {string} fieldId - ID of the input field to copy
|
||||||
|
*/
|
||||||
|
copyField(fieldId) {
|
||||||
|
const field = document.getElementById(fieldId);
|
||||||
|
if (!field) return;
|
||||||
|
|
||||||
|
field.select();
|
||||||
|
navigator.clipboard.writeText(field.value).then(() => {
|
||||||
|
const btn = field.nextElementSibling;
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const originalHTML = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i class="bi bi-check"></i>';
|
||||||
|
setTimeout(() => btn.innerHTML = originalHTML, 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// PASSWORD GENERATION
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random password
|
||||||
|
* @param {number} length - Password length (default 8)
|
||||||
|
* @returns {string} Generated password
|
||||||
|
*/
|
||||||
|
generatePassword(length = 8) {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let password = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return password;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate password and update input field
|
||||||
|
* @param {string} inputId - ID of the password input
|
||||||
|
* @param {number} length - Password length
|
||||||
|
*/
|
||||||
|
regeneratePassword(inputId = 'passwordInput', length = 8) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
if (input) {
|
||||||
|
input.value = this.generatePassword(length);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// DELETE CONFIRMATION
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm deletion with a prompt
|
||||||
|
* @param {string} itemName - Name of item being deleted
|
||||||
|
* @param {string} formId - ID of the form to submit if confirmed
|
||||||
|
* @returns {boolean} True if confirmed
|
||||||
|
*/
|
||||||
|
confirmDelete(itemName, formId = null) {
|
||||||
|
const confirmed = confirm(`Are you sure you want to delete "${itemName}"? This cannot be undone.`);
|
||||||
|
if (confirmed && formId) {
|
||||||
|
const form = document.getElementById(formId);
|
||||||
|
form?.submit();
|
||||||
|
}
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make togglePassword available globally for onclick handlers
|
||||||
|
function togglePassword(inputId, btn) {
|
||||||
|
StegasooAuth.togglePassword(inputId, btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make copyField available globally for onclick handlers
|
||||||
|
function copyField(fieldId) {
|
||||||
|
StegasooAuth.copyField(fieldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make regeneratePassword available globally for onclick handlers
|
||||||
|
function regeneratePassword() {
|
||||||
|
StegasooAuth.regeneratePassword();
|
||||||
|
}
|
||||||
279
frontends/web/static/js/generate.js
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
/**
|
||||||
|
* Stegasoo Generate Page JavaScript
|
||||||
|
* Handles credential generation form and display
|
||||||
|
*/
|
||||||
|
|
||||||
|
const StegasooGenerate = {
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// FORM CONTROLS
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the words range slider
|
||||||
|
*/
|
||||||
|
initWordsSlider() {
|
||||||
|
const wordsRange = document.getElementById('wordsRange');
|
||||||
|
const wordsValue = document.getElementById('wordsValue');
|
||||||
|
|
||||||
|
wordsRange?.addEventListener('input', function() {
|
||||||
|
const bits = this.value * 11;
|
||||||
|
wordsValue.textContent = `${this.value} words (~${bits} bits)`;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize PIN/RSA option toggles
|
||||||
|
*/
|
||||||
|
initOptionToggles() {
|
||||||
|
const usePinCheck = document.getElementById('usePinCheck');
|
||||||
|
const useRsaCheck = document.getElementById('useRsaCheck');
|
||||||
|
const pinOptions = document.getElementById('pinOptions');
|
||||||
|
const rsaOptions = document.getElementById('rsaOptions');
|
||||||
|
const rsaQrWarning = document.getElementById('rsaQrWarning');
|
||||||
|
const rsaBitsSelect = document.getElementById('rsaBitsSelect');
|
||||||
|
|
||||||
|
usePinCheck?.addEventListener('change', function() {
|
||||||
|
pinOptions?.classList.toggle('d-none', !this.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
useRsaCheck?.addEventListener('change', function() {
|
||||||
|
rsaOptions?.classList.toggle('d-none', !this.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// RSA key size QR warning (>3072 bits)
|
||||||
|
rsaBitsSelect?.addEventListener('change', function() {
|
||||||
|
rsaQrWarning?.classList.toggle('d-none', parseInt(this.value) <= 3072);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// CREDENTIAL VISIBILITY
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
pinHidden: false,
|
||||||
|
passphraseHidden: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle PIN visibility
|
||||||
|
*/
|
||||||
|
togglePinVisibility() {
|
||||||
|
const pinDigits = document.getElementById('pinDigits');
|
||||||
|
const icon = document.getElementById('pinToggleIcon');
|
||||||
|
const text = document.getElementById('pinToggleText');
|
||||||
|
|
||||||
|
this.pinHidden = !this.pinHidden;
|
||||||
|
pinDigits?.classList.toggle('blurred', this.pinHidden);
|
||||||
|
|
||||||
|
if (icon) icon.className = this.pinHidden ? 'bi bi-eye' : 'bi bi-eye-slash';
|
||||||
|
if (text) text.textContent = this.pinHidden ? 'Show' : 'Hide';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle passphrase visibility
|
||||||
|
*/
|
||||||
|
togglePassphraseVisibility() {
|
||||||
|
const display = document.getElementById('passphraseDisplay');
|
||||||
|
const icon = document.getElementById('passphraseToggleIcon');
|
||||||
|
const text = document.getElementById('passphraseToggleText');
|
||||||
|
|
||||||
|
this.passphraseHidden = !this.passphraseHidden;
|
||||||
|
display?.classList.toggle('blurred', this.passphraseHidden);
|
||||||
|
|
||||||
|
if (icon) icon.className = this.passphraseHidden ? 'bi bi-eye' : 'bi bi-eye-slash';
|
||||||
|
if (text) text.textContent = this.passphraseHidden ? 'Show' : 'Hide';
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// MEMORY AID STORY GENERATION
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
currentStoryTemplate: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Story templates organized by word count (3-12 words supported)
|
||||||
|
*/
|
||||||
|
storyTemplates: {
|
||||||
|
3: [
|
||||||
|
w => `The ${w[0]} ${w[1]} ${w[2]}.`,
|
||||||
|
w => `${w[0]} loves ${w[1]} and ${w[2]}.`,
|
||||||
|
w => `A ${w[0]} found a ${w[1]} near the ${w[2]}.`,
|
||||||
|
w => `${w[0]}, ${w[1]}, ${w[2]} — never forget.`,
|
||||||
|
w => `The ${w[0]} hid the ${w[1]} under the ${w[2]}.`,
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
w => `${w[0]} and ${w[1]} discovered a ${w[2]} made of ${w[3]}.`,
|
||||||
|
w => `The ${w[0]} ${w[1]} ate ${w[2]} for ${w[3]}.`,
|
||||||
|
w => `In the ${w[0]}, a ${w[1]} met a ${w[2]} carrying ${w[3]}.`,
|
||||||
|
w => `${w[0]} said "${w[1]}" while holding a ${w[2]} ${w[3]}.`,
|
||||||
|
w => `The secret: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}.`,
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
w => `${w[0]} traveled to ${w[1]} seeking the ${w[2]} of ${w[3]} and ${w[4]}.`,
|
||||||
|
w => `The ${w[0]} ${w[1]} lived in a ${w[2]} house with ${w[3]} ${w[4]}.`,
|
||||||
|
w => `"${w[0]}!" shouted ${w[1]} as the ${w[2]} ${w[3]} flew toward ${w[4]}.`,
|
||||||
|
w => `Captain ${w[0]} sailed the ${w[1]} ${w[2]} searching for ${w[3]} ${w[4]}.`,
|
||||||
|
w => `In ${w[0]} kingdom, ${w[1]} guards protected the ${w[2]} ${w[3]} ${w[4]}.`,
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
w => `${w[0]} met ${w[1]} at the ${w[2]}. Together they found ${w[3]}, ${w[4]}, and ${w[5]}.`,
|
||||||
|
w => `The ${w[0]} ${w[1]} wore a ${w[2]} hat while eating ${w[3]} ${w[4]} ${w[5]}.`,
|
||||||
|
w => `Detective ${w[0]} found ${w[1]} ${w[2]} near the ${w[3]} ${w[4]} ${w[5]}.`,
|
||||||
|
w => `In the ${w[0]} ${w[1]}, a ${w[2]} ${w[3]} sang about ${w[4]} ${w[5]}.`,
|
||||||
|
w => `Chef ${w[0]} combined ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, and ${w[5]}.`,
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
w => `${w[0]} and ${w[1]} walked through the ${w[2]} ${w[3]} to find the ${w[4]} ${w[5]} ${w[6]}.`,
|
||||||
|
w => `The ${w[0]} professor studied ${w[1]} ${w[2]} while drinking ${w[3]} ${w[4]} with ${w[5]} ${w[6]}.`,
|
||||||
|
w => `"${w[0]} ${w[1]}!" yelled ${w[2]} as ${w[3]} ${w[4]} attacked the ${w[5]} ${w[6]}.`,
|
||||||
|
w => `In ${w[0]}, King ${w[1]} decreed that ${w[2]} ${w[3]} must honor ${w[4]} ${w[5]} ${w[6]}.`,
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
w => `${w[0]} ${w[1]} and ${w[2]} ${w[3]} met at the ${w[4]} ${w[5]} to discuss ${w[6]} ${w[7]}.`,
|
||||||
|
w => `The ${w[0]} ${w[1]} ${w[2]} traveled from ${w[3]} to ${w[4]} carrying ${w[5]} ${w[6]} ${w[7]}.`,
|
||||||
|
w => `${w[0]} discovered that ${w[1]} ${w[2]} plus ${w[3]} ${w[4]} equals ${w[5]} ${w[6]} ${w[7]}.`,
|
||||||
|
],
|
||||||
|
9: [
|
||||||
|
w => `${w[0]} ${w[1]} ${w[2]} watched as ${w[3]} ${w[4]} ${w[5]} danced with ${w[6]} ${w[7]} ${w[8]}.`,
|
||||||
|
w => `In the ${w[0]} ${w[1]} ${w[2]}, three friends — ${w[3]}, ${w[4]}, ${w[5]} — found ${w[6]} ${w[7]} ${w[8]}.`,
|
||||||
|
w => `The recipe: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, ${w[5]}, ${w[6]}, ${w[7]}, ${w[8]}.`,
|
||||||
|
],
|
||||||
|
10: [
|
||||||
|
w => `${w[0]} ${w[1]} told ${w[2]} ${w[3]} about the ${w[4]} ${w[5]} ${w[6]} hidden in ${w[7]} ${w[8]} ${w[9]}.`,
|
||||||
|
w => `The ${w[0]} ${w[1]} ${w[2]} ${w[3]} ${w[4]} lived beside ${w[5]} ${w[6]} ${w[7]} ${w[8]} ${w[9]}.`,
|
||||||
|
],
|
||||||
|
11: [
|
||||||
|
w => `${w[0]} ${w[1]} ${w[2]} and ${w[3]} ${w[4]} ${w[5]} discovered ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]}.`,
|
||||||
|
w => `In ${w[0]} ${w[1]}, the ${w[2]} ${w[3]} ${w[4]} sang of ${w[5]} ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]}.`,
|
||||||
|
],
|
||||||
|
12: [
|
||||||
|
w => `${w[0]} ${w[1]} ${w[2]} met ${w[3]} ${w[4]} ${w[5]} at the ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]} ${w[11]}.`,
|
||||||
|
w => `The twelve treasures: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, ${w[5]}, ${w[6]}, ${w[7]}, ${w[8]}, ${w[9]}, ${w[10]}, ${w[11]}.`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap word in highlight span
|
||||||
|
*/
|
||||||
|
hl(word) {
|
||||||
|
return `<span class="passphrase-word">${word}</span>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a memory story for given words
|
||||||
|
* @param {string[]} words - Array of passphrase words
|
||||||
|
* @param {number|null} idx - Template index (null for current)
|
||||||
|
* @returns {string} HTML story
|
||||||
|
*/
|
||||||
|
generateStory(words, idx = null) {
|
||||||
|
const count = words.length;
|
||||||
|
if (count === 0) return '';
|
||||||
|
|
||||||
|
// Clamp to supported range (3-12)
|
||||||
|
const templateKey = Math.max(3, Math.min(12, count));
|
||||||
|
const templates = this.storyTemplates[templateKey];
|
||||||
|
|
||||||
|
if (!templates || templates.length === 0) {
|
||||||
|
// Fallback: just list the words
|
||||||
|
return words.map(w => this.hl(w)).join(' — ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateIdx = (idx ?? this.currentStoryTemplate) % templates.length;
|
||||||
|
// Apply highlighting to words
|
||||||
|
const highlighted = words.map(w => this.hl(w));
|
||||||
|
return templates[templateIdx](highlighted);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle memory aid visibility
|
||||||
|
* @param {string[]} words - Passphrase words array
|
||||||
|
*/
|
||||||
|
toggleMemoryAid(words) {
|
||||||
|
const container = document.getElementById('memoryAidContainer');
|
||||||
|
const icon = document.getElementById('memoryAidIcon');
|
||||||
|
const text = document.getElementById('memoryAidText');
|
||||||
|
|
||||||
|
const isHidden = container?.classList.contains('d-none');
|
||||||
|
container?.classList.toggle('d-none', !isHidden);
|
||||||
|
|
||||||
|
if (icon) icon.className = isHidden ? 'bi bi-lightbulb-fill' : 'bi bi-lightbulb';
|
||||||
|
if (text) text.textContent = isHidden ? 'Hide Aid' : 'Memory Aid';
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
document.getElementById('memoryStory').innerHTML = this.generateStory(words);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate story with next template
|
||||||
|
* @param {string[]} words - Passphrase words array
|
||||||
|
*/
|
||||||
|
regenerateStory(words) {
|
||||||
|
const count = words.length;
|
||||||
|
const templateKey = Math.max(3, Math.min(12, count));
|
||||||
|
const templates = this.storyTemplates[templateKey] || [];
|
||||||
|
this.currentStoryTemplate = (this.currentStoryTemplate + 1) % Math.max(1, templates.length);
|
||||||
|
document.getElementById('memoryStory').innerHTML = this.generateStory(words, this.currentStoryTemplate);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// QR CODE PRINTING
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print QR code in new window
|
||||||
|
*/
|
||||||
|
printQrCode() {
|
||||||
|
const qrImg = document.getElementById('qrCodeImage');
|
||||||
|
if (!qrImg) return;
|
||||||
|
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
printWindow.document.write(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>QR Code</title>
|
||||||
|
<style>
|
||||||
|
body { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
||||||
|
img { max-width: 400px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img src="${qrImg.src}" alt="QR Code">
|
||||||
|
<script>window.onload = function() { window.print(); }<\/script>
|
||||||
|
</body>
|
||||||
|
</html>`);
|
||||||
|
printWindow.document.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// INITIALIZATION
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize generate form page
|
||||||
|
*/
|
||||||
|
initForm() {
|
||||||
|
this.initWordsSlider();
|
||||||
|
this.initOptionToggles();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global function wrappers for onclick handlers
|
||||||
|
function togglePinVisibility() {
|
||||||
|
StegasooGenerate.togglePinVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePassphraseVisibility() {
|
||||||
|
StegasooGenerate.togglePassphraseVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
function printQrCode() {
|
||||||
|
StegasooGenerate.printQrCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-init form controls
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (document.querySelector('[data-page="generate"]')) {
|
||||||
|
StegasooGenerate.initForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
6
frontends/web/static/js/qrcode.min.js
vendored
Normal file
1699
frontends/web/static/js/stegasoo.js
Normal file
5
frontends/web/static/vendor/css/bootstrap-icons.min.css
vendored
Normal file
6
frontends/web/static/vendor/css/bootstrap.min.css
vendored
Normal file
BIN
frontends/web/static/vendor/css/fonts/bootstrap-icons.woff
vendored
Normal file
BIN
frontends/web/static/vendor/css/fonts/bootstrap-icons.woff2
vendored
Normal file
7
frontends/web/static/vendor/js/bootstrap.bundle.min.js
vendored
Normal file
1
frontends/web/static/vendor/js/html5-qrcode.min.js
vendored
Normal file
0
frontends/web/stegasoo_users.db
Normal file
295
frontends/web/stego_worker.py
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Stegasoo Subprocess Worker (v4.0.0)
|
||||||
|
|
||||||
|
This script runs in a subprocess and handles encode/decode operations.
|
||||||
|
If it crashes due to jpeglib/scipy issues, the parent Flask process survives.
|
||||||
|
|
||||||
|
CHANGES in v4.0.0:
|
||||||
|
- Added channel_key support for encode/decode operations
|
||||||
|
- New channel_status operation
|
||||||
|
|
||||||
|
Communication is via JSON over stdin/stdout:
|
||||||
|
- Input: JSON object with operation parameters
|
||||||
|
- Output: JSON object with results or error
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
echo '{"operation": "encode", ...}' | python stego_worker.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Ensure stegasoo is importable
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_channel_key(channel_key_param):
|
||||||
|
"""
|
||||||
|
Resolve channel_key parameter to value for stegasoo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel_key_param: 'auto', 'none', explicit key, or None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (auto), "" (public), or explicit key string
|
||||||
|
"""
|
||||||
|
if channel_key_param is None or channel_key_param == "auto":
|
||||||
|
return None # Auto mode - use server config
|
||||||
|
elif channel_key_param == "none":
|
||||||
|
return "" # Public mode
|
||||||
|
else:
|
||||||
|
return channel_key_param # Explicit key
|
||||||
|
|
||||||
|
|
||||||
|
def _get_channel_info(resolved_key):
|
||||||
|
"""
|
||||||
|
Get channel mode and fingerprint for response.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(mode, fingerprint) tuple
|
||||||
|
"""
|
||||||
|
from stegasoo import get_channel_status, has_channel_key
|
||||||
|
|
||||||
|
if resolved_key == "":
|
||||||
|
return "public", None
|
||||||
|
|
||||||
|
if resolved_key is not None:
|
||||||
|
# Explicit key
|
||||||
|
fingerprint = f"{resolved_key[:4]}-••••-••••-••••-••••-••••-••••-{resolved_key[-4:]}"
|
||||||
|
return "private", fingerprint
|
||||||
|
|
||||||
|
# Auto mode - check server config
|
||||||
|
if has_channel_key():
|
||||||
|
status = get_channel_status()
|
||||||
|
return "private", status.get("fingerprint")
|
||||||
|
|
||||||
|
return "public", None
|
||||||
|
|
||||||
|
|
||||||
|
def encode_operation(params: dict) -> dict:
|
||||||
|
"""Handle encode operation."""
|
||||||
|
from stegasoo import FilePayload, encode
|
||||||
|
|
||||||
|
# Decode base64 inputs
|
||||||
|
carrier_data = base64.b64decode(params["carrier_b64"])
|
||||||
|
reference_data = base64.b64decode(params["reference_b64"])
|
||||||
|
|
||||||
|
# Optional RSA key
|
||||||
|
rsa_key_data = None
|
||||||
|
if params.get("rsa_key_b64"):
|
||||||
|
rsa_key_data = base64.b64decode(params["rsa_key_b64"])
|
||||||
|
|
||||||
|
# Determine payload type
|
||||||
|
if params.get("file_b64"):
|
||||||
|
file_data = base64.b64decode(params["file_b64"])
|
||||||
|
payload = FilePayload(
|
||||||
|
data=file_data,
|
||||||
|
filename=params.get("file_name", "file"),
|
||||||
|
mime_type=params.get("file_mime", "application/octet-stream"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
payload = params.get("message", "")
|
||||||
|
|
||||||
|
# Resolve channel key (v4.0.0)
|
||||||
|
resolved_channel_key = _resolve_channel_key(params.get("channel_key", "auto"))
|
||||||
|
|
||||||
|
# Call encode with correct parameter names
|
||||||
|
result = encode(
|
||||||
|
message=payload,
|
||||||
|
reference_photo=reference_data,
|
||||||
|
carrier_image=carrier_data,
|
||||||
|
passphrase=params.get("passphrase", ""),
|
||||||
|
pin=params.get("pin"),
|
||||||
|
rsa_key_data=rsa_key_data,
|
||||||
|
rsa_password=params.get("rsa_password"),
|
||||||
|
embed_mode=params.get("embed_mode", "lsb"),
|
||||||
|
dct_output_format=params.get("dct_output_format", "png"),
|
||||||
|
dct_color_mode=params.get("dct_color_mode", "color"),
|
||||||
|
channel_key=resolved_channel_key, # v4.0.0
|
||||||
|
progress_file=params.get("progress_file"), # v4.1.2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build stats dict if available
|
||||||
|
stats = None
|
||||||
|
if hasattr(result, "stats") and result.stats:
|
||||||
|
stats = {
|
||||||
|
"pixels_modified": getattr(result.stats, "pixels_modified", 0),
|
||||||
|
"capacity_used": getattr(result.stats, "capacity_used", 0),
|
||||||
|
"bytes_embedded": getattr(result.stats, "bytes_embedded", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get channel info for response (v4.0.0)
|
||||||
|
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"stego_b64": base64.b64encode(result.stego_image).decode("ascii"),
|
||||||
|
"filename": getattr(result, "filename", None),
|
||||||
|
"stats": stats,
|
||||||
|
"channel_mode": channel_mode,
|
||||||
|
"channel_fingerprint": channel_fingerprint,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _write_decode_progress(progress_file: str | None, percent: int, phase: str) -> None:
|
||||||
|
"""Write decode progress to file."""
|
||||||
|
if not progress_file:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
with open(progress_file, "w") as f:
|
||||||
|
json.dump({"percent": percent, "phase": phase}, f)
|
||||||
|
except Exception:
|
||||||
|
pass # Best effort
|
||||||
|
|
||||||
|
|
||||||
|
def decode_operation(params: dict) -> dict:
|
||||||
|
"""Handle decode operation."""
|
||||||
|
from stegasoo import decode
|
||||||
|
|
||||||
|
progress_file = params.get("progress_file")
|
||||||
|
|
||||||
|
# Progress: starting
|
||||||
|
_write_decode_progress(progress_file, 5, "reading")
|
||||||
|
|
||||||
|
# Decode base64 inputs
|
||||||
|
stego_data = base64.b64decode(params["stego_b64"])
|
||||||
|
reference_data = base64.b64decode(params["reference_b64"])
|
||||||
|
|
||||||
|
_write_decode_progress(progress_file, 15, "reading")
|
||||||
|
|
||||||
|
# Optional RSA key
|
||||||
|
rsa_key_data = None
|
||||||
|
if params.get("rsa_key_b64"):
|
||||||
|
rsa_key_data = base64.b64decode(params["rsa_key_b64"])
|
||||||
|
|
||||||
|
# Resolve channel key (v4.0.0)
|
||||||
|
resolved_channel_key = _resolve_channel_key(params.get("channel_key", "auto"))
|
||||||
|
|
||||||
|
# Library handles progress internally via progress_file parameter
|
||||||
|
# Call decode with correct parameter names
|
||||||
|
result = decode(
|
||||||
|
stego_image=stego_data,
|
||||||
|
reference_photo=reference_data,
|
||||||
|
passphrase=params.get("passphrase", ""),
|
||||||
|
pin=params.get("pin"),
|
||||||
|
rsa_key_data=rsa_key_data,
|
||||||
|
rsa_password=params.get("rsa_password"),
|
||||||
|
embed_mode=params.get("embed_mode", "auto"),
|
||||||
|
channel_key=resolved_channel_key, # v4.0.0
|
||||||
|
progress_file=progress_file, # v4.2.0: pass through for real-time progress
|
||||||
|
)
|
||||||
|
# Library writes 100% "complete" - no need for worker to write again
|
||||||
|
|
||||||
|
if result.is_file:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"is_file": True,
|
||||||
|
"file_b64": base64.b64encode(result.file_data).decode("ascii"),
|
||||||
|
"filename": result.filename,
|
||||||
|
"mime_type": result.mime_type,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"is_file": False,
|
||||||
|
"message": result.message,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compare_operation(params: dict) -> dict:
|
||||||
|
"""Handle compare_modes operation."""
|
||||||
|
from stegasoo import compare_modes
|
||||||
|
|
||||||
|
carrier_data = base64.b64decode(params["carrier_b64"])
|
||||||
|
result = compare_modes(carrier_data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"comparison": result,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def capacity_check_operation(params: dict) -> dict:
|
||||||
|
"""Handle will_fit_by_mode operation."""
|
||||||
|
from stegasoo import will_fit_by_mode
|
||||||
|
|
||||||
|
carrier_data = base64.b64decode(params["carrier_b64"])
|
||||||
|
|
||||||
|
result = will_fit_by_mode(
|
||||||
|
payload=params["payload_size"],
|
||||||
|
carrier_image=carrier_data,
|
||||||
|
embed_mode=params.get("embed_mode", "lsb"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"result": result,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def channel_status_operation(params: dict) -> dict:
|
||||||
|
"""Handle channel status check (v4.0.0)."""
|
||||||
|
from stegasoo import get_channel_status
|
||||||
|
|
||||||
|
status = get_channel_status()
|
||||||
|
reveal = params.get("reveal", False)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"status": {
|
||||||
|
"mode": status["mode"],
|
||||||
|
"configured": status["configured"],
|
||||||
|
"fingerprint": status.get("fingerprint"),
|
||||||
|
"source": status.get("source"),
|
||||||
|
"key": status.get("key") if reveal and status["configured"] else None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point - read JSON from stdin, write JSON to stdout."""
|
||||||
|
try:
|
||||||
|
# Read all input
|
||||||
|
input_text = sys.stdin.read()
|
||||||
|
|
||||||
|
if not input_text.strip():
|
||||||
|
output = {"success": False, "error": "No input provided"}
|
||||||
|
else:
|
||||||
|
params = json.loads(input_text)
|
||||||
|
operation = params.get("operation")
|
||||||
|
|
||||||
|
if operation == "encode":
|
||||||
|
output = encode_operation(params)
|
||||||
|
elif operation == "decode":
|
||||||
|
output = decode_operation(params)
|
||||||
|
elif operation == "compare":
|
||||||
|
output = compare_operation(params)
|
||||||
|
elif operation == "capacity":
|
||||||
|
output = capacity_check_operation(params)
|
||||||
|
elif operation == "channel_status":
|
||||||
|
output = channel_status_operation(params)
|
||||||
|
else:
|
||||||
|
output = {"success": False, "error": f"Unknown operation: {operation}"}
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
output = {"success": False, "error": f"Invalid JSON: {e}"}
|
||||||
|
except Exception as e:
|
||||||
|
output = {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"error_type": type(e).__name__,
|
||||||
|
"traceback": traceback.format_exc(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write output as JSON
|
||||||
|
print(json.dumps(output), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
546
frontends/web/subprocess_stego.py
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
"""
|
||||||
|
Subprocess Steganography Wrapper (v4.0.0)
|
||||||
|
|
||||||
|
Runs stegasoo operations in isolated subprocesses to prevent crashes
|
||||||
|
from taking down the Flask server.
|
||||||
|
|
||||||
|
CHANGES in v4.0.0:
|
||||||
|
- Added channel_key parameter to encode() and decode() methods
|
||||||
|
- Channel keys enable deployment/group isolation
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from subprocess_stego import SubprocessStego
|
||||||
|
|
||||||
|
stego = SubprocessStego()
|
||||||
|
|
||||||
|
# Encode with channel key
|
||||||
|
result = stego.encode(
|
||||||
|
carrier_data=carrier_bytes,
|
||||||
|
reference_data=ref_bytes,
|
||||||
|
message="secret message",
|
||||||
|
passphrase="my passphrase",
|
||||||
|
pin="123456",
|
||||||
|
embed_mode="dct",
|
||||||
|
channel_key="auto", # or "none", or explicit key
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
stego_bytes = result.stego_data
|
||||||
|
extension = result.extension
|
||||||
|
else:
|
||||||
|
error_message = result.error
|
||||||
|
|
||||||
|
# Decode
|
||||||
|
result = stego.decode(
|
||||||
|
stego_data=stego_bytes,
|
||||||
|
reference_data=ref_bytes,
|
||||||
|
passphrase="my passphrase",
|
||||||
|
pin="123456",
|
||||||
|
channel_key="auto",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compare modes (capacity)
|
||||||
|
result = stego.compare_modes(carrier_bytes)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Default timeout for operations (seconds)
|
||||||
|
DEFAULT_TIMEOUT = 120
|
||||||
|
|
||||||
|
# Path to worker script - adjust if needed
|
||||||
|
WORKER_SCRIPT = Path(__file__).parent / "stego_worker.py"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EncodeResult:
|
||||||
|
"""Result from encode operation."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
stego_data: bytes | None = None
|
||||||
|
filename: str | None = None
|
||||||
|
stats: dict[str, Any] | None = None
|
||||||
|
# Channel info (v4.0.0)
|
||||||
|
channel_mode: str | None = None
|
||||||
|
channel_fingerprint: str | None = None
|
||||||
|
error: str | None = None
|
||||||
|
error_type: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DecodeResult:
|
||||||
|
"""Result from decode operation."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
is_file: bool = False
|
||||||
|
message: str | None = None
|
||||||
|
file_data: bytes | None = None
|
||||||
|
filename: str | None = None
|
||||||
|
mime_type: str | None = None
|
||||||
|
error: str | None = None
|
||||||
|
error_type: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CompareResult:
|
||||||
|
"""Result from compare_modes operation."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
width: int = 0
|
||||||
|
height: int = 0
|
||||||
|
lsb: dict[str, Any] | None = None
|
||||||
|
dct: dict[str, Any] | None = None
|
||||||
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CapacityResult:
|
||||||
|
"""Result from capacity check operation."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
fits: bool = False
|
||||||
|
payload_size: int = 0
|
||||||
|
capacity: int = 0
|
||||||
|
usage_percent: float = 0.0
|
||||||
|
headroom: int = 0
|
||||||
|
mode: str = ""
|
||||||
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChannelStatusResult:
|
||||||
|
"""Result from channel status check (v4.0.0)."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
mode: str = "public"
|
||||||
|
configured: bool = False
|
||||||
|
fingerprint: str | None = None
|
||||||
|
source: str | None = None
|
||||||
|
key: str | None = None
|
||||||
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class SubprocessStego:
|
||||||
|
"""
|
||||||
|
Subprocess-isolated steganography operations.
|
||||||
|
|
||||||
|
All operations run in a separate Python process. If jpeglib or scipy
|
||||||
|
crashes, only the subprocess dies - Flask keeps running.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
worker_path: Path | None = None,
|
||||||
|
python_executable: str | None = None,
|
||||||
|
timeout: int = DEFAULT_TIMEOUT,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize subprocess wrapper.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
worker_path: Path to stego_worker.py (default: same directory)
|
||||||
|
python_executable: Python interpreter to use (default: same as current)
|
||||||
|
timeout: Default timeout in seconds
|
||||||
|
"""
|
||||||
|
self.worker_path = worker_path or WORKER_SCRIPT
|
||||||
|
self.python = python_executable or sys.executable
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
if not self.worker_path.exists():
|
||||||
|
raise FileNotFoundError(f"Worker script not found: {self.worker_path}")
|
||||||
|
|
||||||
|
def _run_worker(self, params: dict[str, Any], timeout: int | None = None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Run the worker subprocess with given parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: Dictionary of parameters (will be JSON-encoded)
|
||||||
|
timeout: Operation timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with results from worker
|
||||||
|
"""
|
||||||
|
timeout = timeout or self.timeout
|
||||||
|
input_json = json.dumps(params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[self.python, str(self.worker_path)],
|
||||||
|
input=input_json,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
cwd=str(self.worker_path.parent),
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
# Worker crashed
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Worker crashed (exit code {result.returncode})",
|
||||||
|
"stderr": result.stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not result.stdout.strip():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Worker returned empty output",
|
||||||
|
"stderr": result.stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Operation timed out after {timeout} seconds",
|
||||||
|
"error_type": "TimeoutError",
|
||||||
|
}
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Invalid JSON from worker: {e}",
|
||||||
|
"raw_output": result.stdout if "result" in dir() else None,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"error_type": type(e).__name__,
|
||||||
|
}
|
||||||
|
|
||||||
|
def encode(
|
||||||
|
self,
|
||||||
|
carrier_data: bytes,
|
||||||
|
reference_data: bytes,
|
||||||
|
message: str | None = None,
|
||||||
|
file_data: bytes | None = None,
|
||||||
|
file_name: str | None = None,
|
||||||
|
file_mime: str | None = None,
|
||||||
|
passphrase: str = "",
|
||||||
|
pin: str | None = None,
|
||||||
|
rsa_key_data: bytes | None = None,
|
||||||
|
rsa_password: str | None = None,
|
||||||
|
embed_mode: str = "lsb",
|
||||||
|
dct_output_format: str = "png",
|
||||||
|
dct_color_mode: str = "color",
|
||||||
|
# Channel key (v4.0.0)
|
||||||
|
channel_key: str | None = "auto",
|
||||||
|
timeout: int | None = None,
|
||||||
|
# Progress file (v4.1.2)
|
||||||
|
progress_file: str | None = None,
|
||||||
|
) -> EncodeResult:
|
||||||
|
"""
|
||||||
|
Encode a message or file into an image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
carrier_data: Carrier image bytes
|
||||||
|
reference_data: Reference photo bytes
|
||||||
|
message: Text message to encode (if not file)
|
||||||
|
file_data: File bytes to encode (if not message)
|
||||||
|
file_name: Original filename (for file payload)
|
||||||
|
file_mime: MIME type (for file payload)
|
||||||
|
passphrase: Encryption passphrase
|
||||||
|
pin: Optional PIN
|
||||||
|
rsa_key_data: Optional RSA key PEM bytes
|
||||||
|
rsa_password: RSA key password if encrypted
|
||||||
|
embed_mode: 'lsb' or 'dct'
|
||||||
|
dct_output_format: 'png' or 'jpeg' (for DCT mode)
|
||||||
|
dct_color_mode: 'grayscale' or 'color' (for DCT mode)
|
||||||
|
channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
|
||||||
|
timeout: Operation timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EncodeResult with stego_data and extension on success
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"operation": "encode",
|
||||||
|
"carrier_b64": base64.b64encode(carrier_data).decode("ascii"),
|
||||||
|
"reference_b64": base64.b64encode(reference_data).decode("ascii"),
|
||||||
|
"message": message,
|
||||||
|
"passphrase": passphrase,
|
||||||
|
"pin": pin,
|
||||||
|
"embed_mode": embed_mode,
|
||||||
|
"dct_output_format": dct_output_format,
|
||||||
|
"dct_color_mode": dct_color_mode,
|
||||||
|
"channel_key": channel_key, # v4.0.0
|
||||||
|
"progress_file": progress_file, # v4.1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_data:
|
||||||
|
params["file_b64"] = base64.b64encode(file_data).decode("ascii")
|
||||||
|
params["file_name"] = file_name
|
||||||
|
params["file_mime"] = file_mime
|
||||||
|
|
||||||
|
if rsa_key_data:
|
||||||
|
params["rsa_key_b64"] = base64.b64encode(rsa_key_data).decode("ascii")
|
||||||
|
params["rsa_password"] = rsa_password
|
||||||
|
|
||||||
|
result = self._run_worker(params, timeout)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
return EncodeResult(
|
||||||
|
success=True,
|
||||||
|
stego_data=base64.b64decode(result["stego_b64"]),
|
||||||
|
filename=result.get("filename"),
|
||||||
|
stats=result.get("stats"),
|
||||||
|
channel_mode=result.get("channel_mode"),
|
||||||
|
channel_fingerprint=result.get("channel_fingerprint"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return EncodeResult(
|
||||||
|
success=False,
|
||||||
|
error=result.get("error", "Unknown error"),
|
||||||
|
error_type=result.get("error_type"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def decode(
|
||||||
|
self,
|
||||||
|
stego_data: bytes,
|
||||||
|
reference_data: bytes,
|
||||||
|
passphrase: str = "",
|
||||||
|
pin: str | None = None,
|
||||||
|
rsa_key_data: bytes | None = None,
|
||||||
|
rsa_password: str | None = None,
|
||||||
|
embed_mode: str = "auto",
|
||||||
|
# Channel key (v4.0.0)
|
||||||
|
channel_key: str | None = "auto",
|
||||||
|
timeout: int | None = None,
|
||||||
|
# Progress tracking (v4.1.5)
|
||||||
|
progress_file: str | None = None,
|
||||||
|
) -> DecodeResult:
|
||||||
|
"""
|
||||||
|
Decode a message or file from a stego image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stego_data: Stego image bytes
|
||||||
|
reference_data: Reference photo bytes
|
||||||
|
passphrase: Decryption passphrase
|
||||||
|
pin: Optional PIN
|
||||||
|
rsa_key_data: Optional RSA key PEM bytes
|
||||||
|
rsa_password: RSA key password if encrypted
|
||||||
|
embed_mode: 'auto', 'lsb', or 'dct'
|
||||||
|
channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
|
||||||
|
timeout: Operation timeout in seconds
|
||||||
|
progress_file: Path to write progress updates (v4.1.5)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DecodeResult with message or file_data on success
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"operation": "decode",
|
||||||
|
"stego_b64": base64.b64encode(stego_data).decode("ascii"),
|
||||||
|
"reference_b64": base64.b64encode(reference_data).decode("ascii"),
|
||||||
|
"passphrase": passphrase,
|
||||||
|
"pin": pin,
|
||||||
|
"embed_mode": embed_mode,
|
||||||
|
"channel_key": channel_key, # v4.0.0
|
||||||
|
"progress_file": progress_file, # v4.1.5
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsa_key_data:
|
||||||
|
params["rsa_key_b64"] = base64.b64encode(rsa_key_data).decode("ascii")
|
||||||
|
params["rsa_password"] = rsa_password
|
||||||
|
|
||||||
|
result = self._run_worker(params, timeout)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
if result.get("is_file"):
|
||||||
|
return DecodeResult(
|
||||||
|
success=True,
|
||||||
|
is_file=True,
|
||||||
|
file_data=base64.b64decode(result["file_b64"]),
|
||||||
|
filename=result.get("filename"),
|
||||||
|
mime_type=result.get("mime_type"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return DecodeResult(
|
||||||
|
success=True,
|
||||||
|
is_file=False,
|
||||||
|
message=result.get("message"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return DecodeResult(
|
||||||
|
success=False,
|
||||||
|
error=result.get("error", "Unknown error"),
|
||||||
|
error_type=result.get("error_type"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def compare_modes(
|
||||||
|
self,
|
||||||
|
carrier_data: bytes,
|
||||||
|
timeout: int | None = None,
|
||||||
|
) -> CompareResult:
|
||||||
|
"""
|
||||||
|
Compare LSB and DCT capacity for a carrier image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
carrier_data: Carrier image bytes
|
||||||
|
timeout: Operation timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CompareResult with capacity information
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"operation": "compare",
|
||||||
|
"carrier_b64": base64.b64encode(carrier_data).decode("ascii"),
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._run_worker(params, timeout)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
comparison = result.get("comparison", {})
|
||||||
|
return CompareResult(
|
||||||
|
success=True,
|
||||||
|
width=comparison.get("width", 0),
|
||||||
|
height=comparison.get("height", 0),
|
||||||
|
lsb=comparison.get("lsb"),
|
||||||
|
dct=comparison.get("dct"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return CompareResult(
|
||||||
|
success=False,
|
||||||
|
error=result.get("error", "Unknown error"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_capacity(
|
||||||
|
self,
|
||||||
|
carrier_data: bytes,
|
||||||
|
payload_size: int,
|
||||||
|
embed_mode: str = "lsb",
|
||||||
|
timeout: int | None = None,
|
||||||
|
) -> CapacityResult:
|
||||||
|
"""
|
||||||
|
Check if a payload will fit in the carrier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
carrier_data: Carrier image bytes
|
||||||
|
payload_size: Size of payload in bytes
|
||||||
|
embed_mode: 'lsb' or 'dct'
|
||||||
|
timeout: Operation timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CapacityResult with fit information
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"operation": "capacity",
|
||||||
|
"carrier_b64": base64.b64encode(carrier_data).decode("ascii"),
|
||||||
|
"payload_size": payload_size,
|
||||||
|
"embed_mode": embed_mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._run_worker(params, timeout)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
r = result.get("result", {})
|
||||||
|
return CapacityResult(
|
||||||
|
success=True,
|
||||||
|
fits=r.get("fits", False),
|
||||||
|
payload_size=r.get("payload_size", 0),
|
||||||
|
capacity=r.get("capacity", 0),
|
||||||
|
usage_percent=r.get("usage_percent", 0.0),
|
||||||
|
headroom=r.get("headroom", 0),
|
||||||
|
mode=r.get("mode", embed_mode),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return CapacityResult(
|
||||||
|
success=False,
|
||||||
|
error=result.get("error", "Unknown error"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_channel_status(
|
||||||
|
self,
|
||||||
|
reveal: bool = False,
|
||||||
|
timeout: int | None = None,
|
||||||
|
) -> ChannelStatusResult:
|
||||||
|
"""
|
||||||
|
Get current channel key status (v4.0.0).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reveal: Include full key in response
|
||||||
|
timeout: Operation timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ChannelStatusResult with channel info
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"operation": "channel_status",
|
||||||
|
"reveal": reveal,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._run_worker(params, timeout)
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
|
status = result.get("status", {})
|
||||||
|
return ChannelStatusResult(
|
||||||
|
success=True,
|
||||||
|
mode=status.get("mode", "public"),
|
||||||
|
configured=status.get("configured", False),
|
||||||
|
fingerprint=status.get("fingerprint"),
|
||||||
|
source=status.get("source"),
|
||||||
|
key=status.get("key") if reveal else None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ChannelStatusResult(
|
||||||
|
success=False,
|
||||||
|
error=result.get("error", "Unknown error"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience function for quick usage
|
||||||
|
_default_stego: SubprocessStego | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_subprocess_stego() -> SubprocessStego:
|
||||||
|
"""Get or create default SubprocessStego instance."""
|
||||||
|
global _default_stego
|
||||||
|
if _default_stego is None:
|
||||||
|
_default_stego = SubprocessStego()
|
||||||
|
return _default_stego
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Progress File Utilities (v4.1.2)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def generate_job_id() -> str:
|
||||||
|
"""Generate a unique job ID for tracking encode/decode operations."""
|
||||||
|
return str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
|
||||||
|
def get_progress_file_path(job_id: str) -> str:
|
||||||
|
"""Get the progress file path for a job ID."""
|
||||||
|
return str(Path(tempfile.gettempdir()) / f"stegasoo_progress_{job_id}.json")
|
||||||
|
|
||||||
|
|
||||||
|
def read_progress(job_id: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Read progress from file for a job ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Progress dict with current, total, percent, phase, or None if not found
|
||||||
|
"""
|
||||||
|
progress_file = get_progress_file_path(job_id)
|
||||||
|
try:
|
||||||
|
with open(progress_file) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_progress_file(job_id: str) -> None:
|
||||||
|
"""Remove progress file for a completed job."""
|
||||||
|
progress_file = get_progress_file_path(job_id)
|
||||||
|
try:
|
||||||
|
Path(progress_file).unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
212
frontends/web/temp_storage.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
"""
|
||||||
|
File-based Temporary Storage
|
||||||
|
|
||||||
|
Stores temp files on disk instead of in-memory dict.
|
||||||
|
This allows multiple Gunicorn workers to share temp files
|
||||||
|
and survives service restarts within the expiry window.
|
||||||
|
|
||||||
|
Files are stored in a temp directory with:
|
||||||
|
- {file_id}.data - The actual file data
|
||||||
|
- {file_id}.json - Metadata (filename, timestamp, mime_type, etc.)
|
||||||
|
|
||||||
|
IMPORTANT: This module ONLY manages files in the temp_files/ directory.
|
||||||
|
It does NOT touch instance/ (auth database) or any other directories.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
# Default temp directory (can be overridden)
|
||||||
|
DEFAULT_TEMP_DIR = Path(__file__).parent / "temp_files"
|
||||||
|
|
||||||
|
# Lock for thread-safe operations
|
||||||
|
_lock = Lock()
|
||||||
|
|
||||||
|
# Module-level temp directory (set on init)
|
||||||
|
_temp_dir: Path = DEFAULT_TEMP_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def init(temp_dir: Path | str | None = None):
|
||||||
|
"""Initialize temp storage with optional custom directory."""
|
||||||
|
global _temp_dir
|
||||||
|
_temp_dir = Path(temp_dir) if temp_dir else DEFAULT_TEMP_DIR
|
||||||
|
_temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _data_path(file_id: str) -> Path:
|
||||||
|
"""Get path for file data."""
|
||||||
|
return _temp_dir / f"{file_id}.data"
|
||||||
|
|
||||||
|
|
||||||
|
def _meta_path(file_id: str) -> Path:
|
||||||
|
"""Get path for file metadata."""
|
||||||
|
return _temp_dir / f"{file_id}.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _thumb_path(thumb_id: str) -> Path:
|
||||||
|
"""Get path for thumbnail data."""
|
||||||
|
return _temp_dir / f"{thumb_id}.thumb"
|
||||||
|
|
||||||
|
|
||||||
|
def save_temp_file(file_id: str, data: bytes, metadata: dict) -> None:
|
||||||
|
"""
|
||||||
|
Save a temp file with its metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_id: Unique identifier for the file
|
||||||
|
data: File contents as bytes
|
||||||
|
metadata: Dict with filename, mime_type, timestamp, etc.
|
||||||
|
"""
|
||||||
|
init() # Ensure directory exists
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
# Add timestamp if not present
|
||||||
|
if "timestamp" not in metadata:
|
||||||
|
metadata["timestamp"] = time.time()
|
||||||
|
|
||||||
|
# Write data file
|
||||||
|
_data_path(file_id).write_bytes(data)
|
||||||
|
|
||||||
|
# Write metadata
|
||||||
|
_meta_path(file_id).write_text(json.dumps(metadata))
|
||||||
|
|
||||||
|
|
||||||
|
def get_temp_file(file_id: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Get a temp file and its metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with 'data' (bytes) and all metadata fields, or None if not found.
|
||||||
|
"""
|
||||||
|
init()
|
||||||
|
|
||||||
|
data_file = _data_path(file_id)
|
||||||
|
meta_file = _meta_path(file_id)
|
||||||
|
|
||||||
|
if not data_file.exists() or not meta_file.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = data_file.read_bytes()
|
||||||
|
metadata = json.loads(meta_file.read_text())
|
||||||
|
return {"data": data, **metadata}
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def has_temp_file(file_id: str) -> bool:
|
||||||
|
"""Check if a temp file exists."""
|
||||||
|
init()
|
||||||
|
return _data_path(file_id).exists() and _meta_path(file_id).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_temp_file(file_id: str) -> None:
|
||||||
|
"""Delete a temp file and its metadata."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_data_path(file_id).unlink(missing_ok=True)
|
||||||
|
_meta_path(file_id).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def save_thumbnail(thumb_id: str, data: bytes) -> None:
|
||||||
|
"""Save a thumbnail."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_thumb_path(thumb_id).write_bytes(data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_thumbnail(thumb_id: str) -> bytes | None:
|
||||||
|
"""Get thumbnail data."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
thumb_file = _thumb_path(thumb_id)
|
||||||
|
if not thumb_file.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return thumb_file.read_bytes()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_thumbnail(thumb_id: str) -> None:
|
||||||
|
"""Delete a thumbnail."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_thumb_path(thumb_id).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_expired(max_age_seconds: float) -> int:
|
||||||
|
"""
|
||||||
|
Delete expired temp files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_age_seconds: Maximum age in seconds before expiry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of files deleted
|
||||||
|
"""
|
||||||
|
init()
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
deleted = 0
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
# Find all metadata files
|
||||||
|
for meta_file in _temp_dir.glob("*.json"):
|
||||||
|
try:
|
||||||
|
metadata = json.loads(meta_file.read_text())
|
||||||
|
timestamp = metadata.get("timestamp", 0)
|
||||||
|
|
||||||
|
if now - timestamp > max_age_seconds:
|
||||||
|
file_id = meta_file.stem
|
||||||
|
_data_path(file_id).unlink(missing_ok=True)
|
||||||
|
meta_file.unlink(missing_ok=True)
|
||||||
|
# Also delete thumbnail if exists
|
||||||
|
_thumb_path(f"{file_id}_thumb").unlink(missing_ok=True)
|
||||||
|
deleted += 1
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
# Remove corrupted files
|
||||||
|
meta_file.unlink(missing_ok=True)
|
||||||
|
deleted += 1
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_all() -> int:
|
||||||
|
"""
|
||||||
|
Delete all temp files. Call on service start/stop.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of files deleted
|
||||||
|
"""
|
||||||
|
init()
|
||||||
|
|
||||||
|
deleted = 0
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
for f in _temp_dir.iterdir():
|
||||||
|
if f.is_file():
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
deleted += 1
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
|
def get_stats() -> dict:
|
||||||
|
"""Get temp storage statistics."""
|
||||||
|
init()
|
||||||
|
|
||||||
|
files = list(_temp_dir.glob("*.data"))
|
||||||
|
total_size = sum(f.stat().st_size for f in files if f.exists())
|
||||||
|
|
||||||
|
return {
|
||||||
|
"file_count": len(files),
|
||||||
|
"total_size_bytes": total_size,
|
||||||
|
"temp_dir": str(_temp_dir),
|
||||||
|
}
|
||||||
@@ -11,33 +11,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
Stegasoo is a secure steganography tool that hides encrypted messages and files
|
Stegasoo hides encrypted messages and files inside images using multi-factor authentication.
|
||||||
inside ordinary images using multi-factor authentication.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h6 class="text-primary mt-4 mb-3"><i class="bi bi-stars me-2"></i>Key Features</h6>
|
<h6 class="text-primary mt-4 mb-3">Features</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<i class="bi bi-check-circle text-success me-2"></i>
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
<strong>Text & File Embedding</strong>
|
<strong>Text & File Embedding</strong>
|
||||||
<br/>Hide messages or any file type (PDF, ZIP, documents)
|
<br><small class="text-muted">Any file type: PDF, ZIP, documents</small>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<i class="bi bi-check-circle text-success me-2"></i>
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
<strong>Multi-Factor Security</strong>
|
<strong>Multi-Factor Security</strong>
|
||||||
<br/>Combines photo + phrase + PIN/RSA key
|
<br><small class="text-muted">Photo + passphrase + PIN/RSA key</small>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<i class="bi bi-check-circle text-success me-2"></i>
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
<strong>AES-256-GCM Encryption</strong>
|
<strong>AES-256-GCM Encryption</strong>
|
||||||
<br/>Military-grade authenticated encryption
|
<br><small class="text-muted">Authenticated encryption with integrity check</small>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<i class="bi bi-check-circle text-success me-2"></i>
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
<strong>Daily Rotating Phrases</strong>
|
<strong>DCT & LSB Modes</strong>
|
||||||
<br/>Different passphrase each day of the week
|
<br><small class="text-muted">JPEG resilience (DCT) or high capacity (LSB)</small>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,22 +45,28 @@
|
|||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<i class="bi bi-check-circle text-success me-2"></i>
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
<strong>Random Pixel Embedding</strong>
|
<strong>Random Pixel Embedding</strong>
|
||||||
<br/>Defeats statistical steganalysis
|
<br><small class="text-muted">Defeats statistical analysis</small>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<i class="bi bi-check-circle text-success me-2"></i>
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
<strong>Format Preservation</strong>
|
<strong>Large Image Support</strong>
|
||||||
<br/>Maintains PNG/BMP lossless formats
|
<br><small class="text-muted">Up to {{ max_payload_kb }} KB, tested with 14MB+ images</small>
|
||||||
</li>
|
|
||||||
<li class="mb-2">
|
|
||||||
<i class="bi bi-check-circle text-success me-2"></i>
|
|
||||||
<strong>Large Capacity</strong>
|
|
||||||
<br/>Up to {{ max_payload_kb }} KB payload, 24MP images
|
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<i class="bi bi-check-circle text-success me-2"></i>
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
<strong>Zero Server Storage</strong>
|
<strong>Zero Server Storage</strong>
|
||||||
<br/>Nothing saved, files auto-expire and are scrubbed from disk.
|
<br><small class="text-muted">Nothing saved, files auto-expire</small>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
|
<strong>QR Code Keys</strong>
|
||||||
|
<br><small class="text-muted">Import/export RSA keys via QR</small>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="bi bi-check-circle text-success me-2"></i>
|
||||||
|
<strong>Channel Keys</strong>
|
||||||
|
<span class="badge bg-info ms-1">v4.1</span>
|
||||||
|
<br><small class="text-muted">Group/deployment isolation</small>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,259 +74,334 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Embedding Modes -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Embedding Modes</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Two modes optimized for different use cases.</p>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<!-- DCT Mode -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card bg-dark h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-soundwave text-warning me-2"></i>
|
||||||
|
<strong>DCT Mode</strong>
|
||||||
|
<span class="badge bg-success ms-2">Default</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="small">
|
||||||
|
<strong>DCT (Discrete Cosine Transform)</strong> embeds data in frequency coefficients. Survives JPEG recompression.
|
||||||
|
</p>
|
||||||
|
<ul class="small mb-0">
|
||||||
|
<li><strong>Capacity:</strong> ~75 KB/MP</li>
|
||||||
|
<li><strong>Output:</strong> JPEG or PNG</li>
|
||||||
|
<li><strong>Color:</strong> Color or grayscale</li>
|
||||||
|
<li><strong>Speed:</strong> ~2s</li>
|
||||||
|
<li><strong>Error Correction:</strong> Reed-Solomon</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<div class="small">
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i> Instagram, Facebook<br>
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i> WhatsApp, Signal, Telegram<br>
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i> Twitter/X<br>
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i> Any recompressing platform
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LSB Mode -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card bg-dark h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-grid-3x3-gap text-primary me-2"></i>
|
||||||
|
<strong>LSB Mode</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="small">
|
||||||
|
<strong>LSB (Least Significant Bit)</strong> embeds data in the lowest bit of each color channel. Imperceptible to the eye.
|
||||||
|
</p>
|
||||||
|
<ul class="small mb-0">
|
||||||
|
<li><strong>Capacity:</strong> ~375 KB/MP</li>
|
||||||
|
<li><strong>Output:</strong> PNG (lossless)</li>
|
||||||
|
<li><strong>Color:</strong> Full color</li>
|
||||||
|
<li><strong>Speed:</strong> ~0.5s</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<div class="small">
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i> Email attachments<br>
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i> Cloud storage<br>
|
||||||
|
<i class="bi bi-check-circle text-success me-1"></i> Direct file transfer<br>
|
||||||
|
<i class="bi bi-x-circle text-danger me-1"></i> Social media
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mode Comparison Table -->
|
||||||
|
<h6 class="mt-3"><i class="bi bi-table me-2"></i>Comparison</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-sm small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Aspect</th>
|
||||||
|
<th>DCT Mode <span class="badge bg-success ms-1">Default</span></th>
|
||||||
|
<th>LSB Mode</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Capacity (1080p)</td>
|
||||||
|
<td class="text-warning">~50 KB</td>
|
||||||
|
<td class="text-success">~770 KB</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Survives JPEG</td>
|
||||||
|
<td class="text-success">✅ Yes</td>
|
||||||
|
<td class="text-danger">❌ No</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Social Media</td>
|
||||||
|
<td class="text-success">✅ Works</td>
|
||||||
|
<td class="text-danger">❌ Broken</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Detection Resistance</td>
|
||||||
|
<td>Better</td>
|
||||||
|
<td>Moderate</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info small mt-3 mb-0">
|
||||||
|
<i class="bi bi-lightbulb me-2"></i>
|
||||||
|
<strong>Auto-Detection:</strong> Mode is detected automatically when decoding.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>How Security Works</h5>
|
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>How Security Works</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>Stegasoo uses <strong>hybrid multi-factor authentication</strong> to derive encryption keys:</p>
|
<p>Multi-factor authentication derives encryption keys:</p>
|
||||||
|
|
||||||
<div class="row text-center my-4">
|
<div class="row text-center my-4">
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-6 col-lg-3 mb-3">
|
||||||
<div class="p-3 bg-dark rounded">
|
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
|
||||||
<i class="bi bi-image text-info fs-2 d-block mb-2"></i>
|
<i class="bi bi-image text-info fs-2 d-block mb-2"></i>
|
||||||
<strong>Reference Photo</strong>
|
<strong>Reference Photo</strong>
|
||||||
<div class="small text-muted mt-1">Something you have</div>
|
<div class="small text-muted mt-1">Something you have</div>
|
||||||
<div class="small text-success">~80-256 bits</div>
|
<div class="small text-success mt-auto pt-2">~80-256 bits</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-6 col-lg-3 mb-3">
|
||||||
<div class="p-3 bg-dark rounded">
|
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
|
||||||
<i class="bi bi-chat-quote text-warning fs-2 d-block mb-2"></i>
|
<i class="bi bi-chat-quote text-warning fs-2 d-block mb-2"></i>
|
||||||
<strong>Daily Phrase</strong>
|
<strong>Passphrase</strong>
|
||||||
<div class="small text-muted mt-1">Something you know (rotates)</div>
|
<div class="small text-muted mt-1">Something you know</div>
|
||||||
<div class="small text-success">~33 bits (3 words)</div>
|
<div class="small text-success mt-auto pt-2">~44 bits (4 words)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-6 col-lg-3 mb-3">
|
||||||
<div class="p-3 bg-dark rounded">
|
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
|
||||||
<i class="bi bi-123 text-danger fs-2 d-block mb-2"></i>
|
<i class="bi bi-123 text-danger fs-2 d-block mb-2"></i>
|
||||||
<strong>Static PIN</strong>
|
<strong>Static PIN</strong>
|
||||||
<div class="small text-muted mt-1">Something you know (fixed)</div>
|
<div class="small text-muted mt-1">Something you know</div>
|
||||||
<div class="small text-success">~20 bits (6 digits)</div>
|
<div class="small text-success mt-auto pt-2">~20 bits (6 digits)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-6 col-lg-3 mb-3">
|
||||||
<div class="p-3 bg-dark rounded">
|
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
|
||||||
<i class="bi bi-key text-primary fs-2 d-block mb-2"></i>
|
<i class="bi bi-key text-primary fs-2 d-block mb-2"></i>
|
||||||
<strong>RSA Key</strong>
|
<strong>RSA Key</strong>
|
||||||
<div class="small text-muted mt-1">Something you have (optional)</div>
|
<div class="small text-muted mt-1">Optional</div>
|
||||||
<div class="small text-success">~128 bits (2048-bit)</div>
|
<div class="small text-success mt-auto pt-2">~128 bits</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-secondary">
|
<div class="alert alert-secondary">
|
||||||
<i class="bi bi-calculator me-2"></i>
|
<i class="bi bi-calculator me-2"></i>
|
||||||
<strong>Combined entropy:</strong> 130-400+ bits depending on configuration.
|
<strong>Combined entropy:</strong> 144-424+ bits. 128 bits is infeasible to brute force.
|
||||||
For reference, 128 bits is considered computationally infeasible to brute force.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h6 class="mt-4">Key Derivation</h6>
|
<h6 class="mt-4">Key Derivation</h6>
|
||||||
<p>
|
<p>
|
||||||
{% if has_argon2 %}
|
{% if has_argon2 %}
|
||||||
<span class="badge bg-success me-1"><i class="bi bi-check"></i> Argon2id Available</span>
|
<span class="badge bg-success me-1"><i class="bi bi-check"></i> Argon2id</span>
|
||||||
Using <strong>Argon2id</strong> with 256MB memory cost — the winner of the Password Hashing Competition
|
256MB memory cost. Memory-hard KDF defeats GPU/ASIC attacks.
|
||||||
and current best practice for key derivation.
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-warning text-dark me-1"><i class="bi bi-exclamation-triangle"></i> Argon2 Not Available</span>
|
<span class="badge bg-warning text-dark me-1"><i class="bi bi-exclamation-triangle"></i> Argon2 Not Available</span>
|
||||||
Falling back to <strong>PBKDF2-SHA512</strong> with 600,000 iterations.
|
Using PBKDF2-SHA512 with 600k iterations.
|
||||||
Install <code>argon2-cffi</code> for stronger security.
|
Install <code>argon2-cffi</code> for stronger security.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h6 class="mt-4">Steganography Technique</h6>
|
|
||||||
<p>
|
|
||||||
Uses <strong>LSB (Least Significant Bit)</strong> embedding with pseudo-random pixel selection.
|
|
||||||
The pixel locations are determined by a key derived from your credentials, making the
|
|
||||||
hidden data's location unpredictable without the correct inputs.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mb-4">
|
<!-- Channel Keys (v4.0.0) -->
|
||||||
|
<div class="card mb-4" id="channel-keys">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0"><i class="bi bi-file-earmark-binary me-2"></i>File Embedding</h5>
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-broadcast me-2"></i>Channel Keys
|
||||||
|
<span class="badge bg-info ms-2">v4.1</span>
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
<span class="badge bg-info me-1">New in v2.1</span>
|
Channel keys provide <strong>deployment/group isolation</strong>. Messages encoded with one channel key
|
||||||
Stegasoo now supports embedding <strong>any file type</strong>, not just text messages.
|
cannot be decoded with a different key, even if all other credentials match.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row mt-4">
|
||||||
<div class="col-md-6">
|
<!-- Auto Mode -->
|
||||||
<h6><i class="bi bi-check2-square text-success me-2"></i>Supported</h6>
|
<div class="col-md-4 mb-3">
|
||||||
<ul class="small">
|
<div class="card bg-dark h-100">
|
||||||
<li>PDF documents</li>
|
<div class="card-header text-center">
|
||||||
<li>ZIP/RAR archives</li>
|
<i class="bi bi-gear-fill text-success fs-2 d-block mb-2"></i>
|
||||||
<li>Office documents (DOCX, XLSX, PPTX)</li>
|
<strong>Auto</strong>
|
||||||
<li>Source code files</li>
|
</div>
|
||||||
<li>Any binary file up to {{ max_payload_kb }} KB</li>
|
<div class="card-body">
|
||||||
</ul>
|
<p class="small mb-2">Uses server-configured key if available, otherwise public mode.</p>
|
||||||
|
<ul class="small mb-0">
|
||||||
|
<li>Server admin configures the shared key</li>
|
||||||
|
<li>All users share the same channel</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<h6><i class="bi bi-info-circle text-info me-2"></i>How It Works</h6>
|
<!-- Public Mode -->
|
||||||
<ul class="small">
|
<div class="col-md-4 mb-3">
|
||||||
<li>Original filename is preserved</li>
|
<div class="card bg-dark h-100">
|
||||||
<li>MIME type is stored for proper handling</li>
|
<div class="card-header text-center">
|
||||||
<li>File is encrypted identically to text</li>
|
<i class="bi bi-globe text-info fs-2 d-block mb-2"></i>
|
||||||
<li>Decoding auto-detects text vs. file</li>
|
<strong>Public</strong>
|
||||||
</ul>
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="small mb-2">No channel key. Compatible with other public installations.</p>
|
||||||
|
<ul class="small mb-0">
|
||||||
|
<li>Default if no server key configured</li>
|
||||||
|
<li>Anyone can decode (with credentials)</li>
|
||||||
|
<li>Interoperable between deployments</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Mode -->
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card bg-dark h-100">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<i class="bi bi-key-fill text-warning fs-2 d-block mb-2"></i>
|
||||||
|
<strong>Custom</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="small mb-2">Your own group key. Share with recipients.</p>
|
||||||
|
<ul class="small mb-0">
|
||||||
|
<li>Format: <code>XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX</code></li>
|
||||||
|
<li>32 chars (128 bits entropy)</li>
|
||||||
|
<li>Private group communication</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info small mt-3">
|
{% if channel_configured %}
|
||||||
<i class="bi bi-lightbulb me-2"></i>
|
<div class="alert alert-success mt-3 mb-0">
|
||||||
<strong>Tip:</strong> For larger files, compress them first (ZIP) to maximize capacity.
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
A 16MP carrier image can hold approximately 6MB of raw data, but we limit payloads
|
<strong>This server has a channel key configured:</strong>
|
||||||
to {{ max_payload_kb }} KB for reasonable processing times.
|
<code class="ms-2">{{ channel_fingerprint }}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
</div>
|
<div class="alert alert-info mt-3 mb-0">
|
||||||
|
|
||||||
<!-- REST API Card - UPDATED BASED ON CURRENT IMPLEMENTATION -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>REST API</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>
|
|
||||||
<span class="badge bg-success me-1"><i class="bi bi-check-circle"></i> FastAPI</span>
|
|
||||||
Stegasoo includes a complete REST API built with FastAPI, featuring automatic documentation,
|
|
||||||
type validation, and comprehensive error handling.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h6 class="mt-4"><i class="bi bi-layers me-2"></i>API Endpoints</h6>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<ul class="small">
|
|
||||||
<li><code>POST /generate</code> – Generate credentials</li>
|
|
||||||
<li><code>POST /encode</code> – Encode text message (JSON)</li>
|
|
||||||
<li><code>POST /encode/file</code> – Encode binary file (JSON)</li>
|
|
||||||
<li><code>POST /encode/multipart</code> – Encode with file uploads</li>
|
|
||||||
<li><code>POST /decode</code> – Decode message (JSON)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<ul class="small">
|
|
||||||
<li><code>POST /decode/multipart</code> – Decode with file uploads</li>
|
|
||||||
<li><code>POST /extract-key-from-qr</code> – Extract RSA key from QR</li>
|
|
||||||
<li><code>POST /image/info</code> – Get image capacity</li>
|
|
||||||
<li><code>GET /</code> – API status and capabilities</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-info small mt-3">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
<strong>Note:</strong> The <code>/encode/multipart</code> endpoint returns the PNG image directly
|
This server is running in <strong>public mode</strong>.
|
||||||
(with headers indicating metadata), while <code>/decode/multipart</code> returns JSON.
|
Set <code>STEGASOO_CHANNEL_KEY</code> to enable server-wide channel isolation.
|
||||||
Use <code>--output</code> flag to save responses to files.
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<h6 class="mt-4"><i class="bi bi-file-earmark-code me-2"></i>JSON API Examples</h6>
|
</div>
|
||||||
<pre class="bg-dark p-3 rounded"><code>// Generate credentials
|
</div>
|
||||||
curl -X POST "http://localhost:8000/generate" \
|
|
||||||
-H "Content-Type: application/json" \
|
<!-- Version History -->
|
||||||
-d '{"use_pin": true, "use_rsa": false, "pin_length": 6, "words_per_phrase": 3}'
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
// Encode text message (images must be base64 encoded first)
|
<h5 class="mb-0"><i class="bi bi-clock-history me-2"></i>Version History</h5>
|
||||||
// First encode images: base64 -w0 photo.jpg > photo.b64
|
</div>
|
||||||
curl -X POST "http://localhost:8000/encode" \
|
<div class="card-body">
|
||||||
-H "Content-Type: application/json" \
|
<!-- Current Version - Prominent -->
|
||||||
-d '{
|
<div class="alert alert-success mb-4">
|
||||||
"message": "secret message",
|
<div class="d-flex align-items-center">
|
||||||
"reference_photo_base64": "'"$(cat photo.b64)"'",
|
<span class="badge bg-success fs-6 me-3">v4.2.1</span>
|
||||||
"carrier_image_base64": "'"$(cat carrier.b64)"'",
|
<div>
|
||||||
"day_phrase": "apple forest thunder",
|
<strong>Security & API improvements:</strong>
|
||||||
"pin": "123456"
|
API key authentication,
|
||||||
}'
|
TLS with self-signed certs,
|
||||||
|
CLI tools (compress, rotate, convert),
|
||||||
// Encode file (base64) - encode file first: base64 -w0 document.pdf > doc.b64
|
jpegtran lossless JPEG rotation
|
||||||
curl -X POST "http://localhost:8000/encode/file" \
|
</div>
|
||||||
-H "Content-Type: application/json" \
|
</div>
|
||||||
-d '{
|
</div>
|
||||||
"file_data_base64": "'"$(cat doc.b64)"'",
|
|
||||||
"filename": "document.pdf",
|
<!-- Previous Versions - Accordion -->
|
||||||
"reference_photo_base64": "'"$(cat photo.b64)"'",
|
<div class="accordion" id="versionAccordion">
|
||||||
"carrier_image_base64": "'"$(cat carrier.b64)"'",
|
<div class="accordion-item bg-dark">
|
||||||
"day_phrase": "apple forest thunder",
|
<h2 class="accordion-header">
|
||||||
"pin": "123456"
|
<button class="accordion-button collapsed bg-dark text-light py-2" type="button"
|
||||||
}'</code></pre>
|
data-bs-toggle="collapse" data-bs-target="#olderVersions">
|
||||||
|
<i class="bi bi-archive me-2"></i>Previous Versions
|
||||||
<h6 class="mt-4"><i class="bi bi-upload me-2"></i>Multipart API Examples</h6>
|
</button>
|
||||||
<pre class="bg-dark p-3 rounded"><code># Encode text with file uploads
|
</h2>
|
||||||
curl -X POST "http://localhost:8000/encode/multipart" \
|
<div id="olderVersions" class="accordion-collapse collapse" data-bs-parent="#versionAccordion">
|
||||||
-F "day_phrase=apple forest thunder" \
|
<div class="accordion-body p-0">
|
||||||
-F "pin=123456" \
|
<table class="table table-dark table-sm small mb-0">
|
||||||
-F "reference_photo=@photo.jpg" \
|
<tbody>
|
||||||
-F "carrier=@carrier.png" \
|
<tr>
|
||||||
-F "message=secret" \
|
<td width="80"><strong>4.1.7</strong></td>
|
||||||
--output stego.png
|
<td>Progress bars for encode, mobile polish, release validation</td>
|
||||||
|
</tr>
|
||||||
# Encode file (no message field when using payload_file)
|
<tr>
|
||||||
curl -X POST "http://localhost:8000/encode/multipart" \
|
<td width="80"><strong>4.1.1</strong></td>
|
||||||
-F "day_phrase=apple forest thunder" \
|
<td>DCT RS format stability, Docker cleanup, first-boot wizard</td>
|
||||||
-F "pin=123456" \
|
</tr>
|
||||||
-F "reference_photo=@photo.jpg" \
|
<tr>
|
||||||
-F "carrier=@carrier.png" \
|
<td><strong>4.1.0</strong></td>
|
||||||
-F "payload_file=@document.pdf" \
|
<td>Reed-Solomon error correction for DCT, majority voting headers</td>
|
||||||
--output stego.png
|
</tr>
|
||||||
|
<tr>
|
||||||
# Encode with RSA key from QR code (optional)
|
<td><strong>4.0.0</strong></td>
|
||||||
curl -X POST "http://localhost:8000/encode/multipart" \
|
<td>Channel keys, DCT default, subprocess isolation</td>
|
||||||
-F "day_phrase=apple forest thunder" \
|
</tr>
|
||||||
-F "pin=123456" \
|
<tr>
|
||||||
-F "reference_photo=@photo.jpg" \
|
<td>3.2.0</td>
|
||||||
-F "carrier=@carrier.png" \
|
<td>Single passphrase, more default words</td>
|
||||||
-F "message=secret" \
|
</tr>
|
||||||
-F "rsa_key_qr=@keyqr.png" \
|
<tr>
|
||||||
--output stego.png
|
<td>3.0.0</td>
|
||||||
|
<td>DCT mode, JPEG output, color preservation</td>
|
||||||
# Decode with file uploads (returns JSON)
|
</tr>
|
||||||
curl -X POST "http://localhost:8000/decode/multipart" \
|
<tr>
|
||||||
-F "day_phrase=apple forest thunder" \
|
<td>2.x</td>
|
||||||
-F "pin=123456" \
|
<td>Web UI, REST API, RSA keys, QR codes, file embedding</td>
|
||||||
-F "reference_photo=@photo.jpg" \
|
</tr>
|
||||||
-F "stego_image=@stego.png" \
|
<tr>
|
||||||
--output result.json</code></pre>
|
<td>1.0.0</td>
|
||||||
|
<td>Initial release, CLI only, LSB mode</td>
|
||||||
<h6 class="mt-4"><i class="bi bi-qr-code me-2"></i>QR Code Support</h6>
|
</tr>
|
||||||
<p class="small">
|
</tbody>
|
||||||
The API can extract RSA keys from QR code images. QR code reading requires
|
</table>
|
||||||
<code>pyzbar</code> and <code>libzbar</code> system library.
|
</div>
|
||||||
</p>
|
</div>
|
||||||
<pre class="bg-dark p-3 rounded"><code># Extract key from QR code (returns JSON)
|
</div>
|
||||||
curl -X POST "http://localhost:8000/extract-key-from-qr" \
|
|
||||||
-F "qr_image=@keyqr.png"</code></pre>
|
|
||||||
|
|
||||||
<div class="alert alert-info small mt-3">
|
|
||||||
<i class="bi bi-journal-text me-2"></i>
|
|
||||||
<strong>Interactive Documentation:</strong> When running the API server, visit
|
|
||||||
<code>/docs</code> for Swagger UI or <code>/redoc</code> for ReDoc documentation.
|
|
||||||
All endpoints include detailed schemas and example requests.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h6 class="mt-4"><i class="bi bi-terminal me-2"></i>Command Line Interface</h6>
|
|
||||||
<p class="small">
|
|
||||||
Stegasoo also includes a full-featured CLI. Install with <code>pip install stegasoo[cli]</code>
|
|
||||||
or see the <a href="/cli">CLI documentation</a> for complete usage.
|
|
||||||
</p>
|
|
||||||
<pre class="bg-dark p-3 rounded"><code># CLI Examples
|
|
||||||
stegasoo generate --pin --words 3
|
|
||||||
stegasoo encode -r photo.jpg -c meme.png -p "phrase" --pin 123456 -m "secret"
|
|
||||||
stegasoo decode -r photo.jpg -s stego.png -p "phrase" --pin 123456
|
|
||||||
stegasoo info image.png</code></pre>
|
|
||||||
|
|
||||||
<p class="small text-muted mt-3 mb-0">
|
|
||||||
<span class="badge bg-{% if has_argon2 %}success{% else %}warning{% endif %} me-1">
|
|
||||||
{% if has_argon2 %}Argon2 Available{% else %}PBKDF2 Fallback{% endif %}
|
|
||||||
</span>
|
|
||||||
<span class="badge bg-{% if has_qrcode_read %}success{% else %}secondary{% endif %}">
|
|
||||||
{% if has_qrcode_read %}QR Reading Available{% else %}QR Reading Not Available{% endif %}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -341,11 +421,11 @@ stegasoo info image.png</code></pre>
|
|||||||
<div id="setup" class="accordion-collapse collapse show" data-bs-parent="#usageAccordion">
|
<div id="setup" class="accordion-collapse collapse show" data-bs-parent="#usageAccordion">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<ol>
|
<ol>
|
||||||
<li>Both parties agree on a <strong>reference photo</strong> (shared secretly, never transmitted)</li>
|
<li>Agree on a <strong>reference photo</strong> (never transmitted)</li>
|
||||||
<li>Go to <a href="/generate">Generate</a> and create credentials</li>
|
<li>Go to <a href="/generate">Generate</a> to create credentials</li>
|
||||||
<li><strong>Memorize</strong> the 7 daily phrases and PIN</li>
|
<li>Memorize passphrase and PIN</li>
|
||||||
<li>If using RSA, download and securely store the key file</li>
|
<li>If using RSA, store the key file securely</li>
|
||||||
<li>Share credentials with your contact through a secure channel</li>
|
<li>Share credentials via secure channel</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -355,20 +435,23 @@ stegasoo info image.png</code></pre>
|
|||||||
<h2 class="accordion-header">
|
<h2 class="accordion-header">
|
||||||
<button class="accordion-button collapsed bg-dark text-light" type="button"
|
<button class="accordion-button collapsed bg-dark text-light" type="button"
|
||||||
data-bs-toggle="collapse" data-bs-target="#encoding">
|
data-bs-toggle="collapse" data-bs-target="#encoding">
|
||||||
<i class="bi bi-2-circle me-2"></i>Encoding a Message or File
|
<i class="bi bi-2-circle me-2"></i>Encoding
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="encoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
|
<div id="encoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<ol>
|
<ol>
|
||||||
<li>Go to <a href="/encode">Encode</a></li>
|
<li>Go to <a href="/encode">Encode</a></li>
|
||||||
<li>Upload your <strong>reference photo</strong></li>
|
<li>Upload <strong>reference photo</strong> and <strong>carrier image</strong></li>
|
||||||
<li>Upload a <strong>carrier image</strong> (the image to hide data in)</li>
|
<li>Choose mode:
|
||||||
<li>Choose <strong>Text</strong> or <strong>File</strong> mode</li>
|
<ul>
|
||||||
<li>Enter your message or select a file to embed</li>
|
<li><strong>DCT</strong> (default): social media</li>
|
||||||
<li>Enter <strong>today's phrase</strong> and your PIN/key</li>
|
<li><strong>LSB</strong>: email, cloud, direct transfer</li>
|
||||||
<li>Download the resulting stego image</li>
|
</ul>
|
||||||
<li>Send the stego image through any channel (email, social media, etc.)</li>
|
</li>
|
||||||
|
<li>Enter message or select file</li>
|
||||||
|
<li>Enter passphrase and PIN/key</li>
|
||||||
|
<li>Download stego image</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,23 +461,21 @@ stegasoo info image.png</code></pre>
|
|||||||
<h2 class="accordion-header">
|
<h2 class="accordion-header">
|
||||||
<button class="accordion-button collapsed bg-dark text-light" type="button"
|
<button class="accordion-button collapsed bg-dark text-light" type="button"
|
||||||
data-bs-toggle="collapse" data-bs-target="#decoding">
|
data-bs-toggle="collapse" data-bs-target="#decoding">
|
||||||
<i class="bi bi-3-circle me-2"></i>Decoding a Message or File
|
<i class="bi bi-3-circle me-2"></i>Decoding
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="decoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
|
<div id="decoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<ol>
|
<ol>
|
||||||
<li>Go to <a href="/decode">Decode</a></li>
|
<li>Go to <a href="/decode">Decode</a></li>
|
||||||
<li>Upload your <strong>reference photo</strong> (same one used for encoding)</li>
|
<li>Upload <strong>reference photo</strong></li>
|
||||||
<li>Upload the <strong>stego image</strong> you received</li>
|
<li>Upload <strong>stego image</strong></li>
|
||||||
<li>Enter the phrase for <strong>the day it was encoded</strong> (check the filename for date)</li>
|
<li>Enter passphrase and PIN/key</li>
|
||||||
<li>Enter your PIN and/or RSA key</li>
|
<li>View message or download file</li>
|
||||||
<li>View the decoded message or download the extracted file</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
<div class="alert alert-warning small mt-3 mb-0">
|
<div class="alert alert-info small mt-3 mb-0">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-magic me-2"></i>
|
||||||
The stego image filename contains the encoding date (e.g., <code>abc123_20251228.png</code>).
|
Mode is auto-detected.
|
||||||
Use this to determine which day's phrase to use!
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,65 +484,119 @@ stegasoo info image.png</code></pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits & Specifications</h5>
|
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits & Specs</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-dark table-striped">
|
<!-- Key Specs - Always Visible -->
|
||||||
<tbody>
|
<div class="row text-center mb-4">
|
||||||
<tr>
|
<div class="col-6 col-md-4 col-lg-2 mb-3">
|
||||||
<td><i class="bi bi-file-text me-2"></i>Max text message</td>
|
<div class="p-3 bg-dark rounded h-100">
|
||||||
<td><strong>2 million characters</strong> (~2 MB)</td>
|
<i class="bi bi-file-earmark text-primary fs-3 d-block mb-2"></i>
|
||||||
</tr>
|
<div class="small text-muted">Max Payload</div>
|
||||||
<tr>
|
<strong>{{ max_payload_kb }} KB</strong>
|
||||||
<td><i class="bi bi-file-earmark me-2"></i>Max file payload</td>
|
</div>
|
||||||
<td><strong>{{ max_payload_kb }} KB</strong></td>
|
</div>
|
||||||
</tr>
|
<div class="col-6 col-md-4 col-lg-2 mb-3">
|
||||||
<tr>
|
<div class="p-3 bg-dark rounded h-100">
|
||||||
<td><i class="bi bi-image me-2"></i>Max carrier image</td>
|
<i class="bi bi-image text-info fs-3 d-block mb-2"></i>
|
||||||
<td><strong>24 megapixels</strong> (~6000×4000)</td>
|
<div class="small text-muted">Max Carrier</div>
|
||||||
</tr>
|
<strong>24 MP</strong>
|
||||||
<tr>
|
</div>
|
||||||
<td><i class="bi bi-upload me-2"></i>Max upload size</td>
|
</div>
|
||||||
<td><strong>30 MB</strong></td>
|
<div class="col-6 col-md-4 col-lg-2 mb-3">
|
||||||
</tr>
|
<div class="p-3 bg-dark rounded h-100">
|
||||||
<tr>
|
<i class="bi bi-soundwave text-warning fs-3 d-block mb-2"></i>
|
||||||
<td><i class="bi bi-clock me-2"></i>Temp file expiry</td>
|
<div class="small text-muted">DCT Capacity</div>
|
||||||
<td><strong>5 minutes</strong></td>
|
<strong>~75 KB/MP</strong>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
</div>
|
||||||
<td><i class="bi bi-key me-2"></i>PIN length</td>
|
<div class="col-6 col-md-4 col-lg-2 mb-3">
|
||||||
<td><strong>6-9 digits</strong></td>
|
<div class="p-3 bg-dark rounded h-100">
|
||||||
</tr>
|
<i class="bi bi-grid-3x3 text-success fs-3 d-block mb-2"></i>
|
||||||
<tr>
|
<div class="small text-muted">LSB Capacity</div>
|
||||||
<td><i class="bi bi-shield me-2"></i>RSA key sizes</td>
|
<strong>~375 KB/MP</strong>
|
||||||
<td><strong>2048, 3072, 4096 bits</strong></td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
<div class="col-6 col-md-4 col-lg-2 mb-3">
|
||||||
<td><i class="bi bi-chat-quote me-2"></i>Phrase length</td>
|
<div class="p-3 bg-dark rounded h-100">
|
||||||
<td><strong>3-12 words</strong> (BIP-39 wordlist)</td>
|
<i class="bi bi-shield-check text-danger fs-3 d-block mb-2"></i>
|
||||||
</tr>
|
<div class="small text-muted">Encryption</div>
|
||||||
<tr>
|
<strong>AES-256</strong>
|
||||||
<td><i class="bi bi-cpu me-2"></i>API documentation</td>
|
</div>
|
||||||
<td><strong>/docs (Swagger)</strong> and <strong>/redoc</strong></td>
|
</div>
|
||||||
</tr>
|
<div class="col-6 col-md-4 col-lg-2 mb-3">
|
||||||
<tr>
|
<div class="p-3 bg-dark rounded h-100">
|
||||||
<td><i class="bi bi-qr-code me-2"></i>QR code support</td>
|
<i class="bi bi-bandaid text-info fs-3 d-block mb-2"></i>
|
||||||
<td><strong>RSA key encoding/extraction </strong>(up to 3072 bit keys)</td>
|
<div class="small text-muted">DCT ECC</div>
|
||||||
</tr>
|
<strong>RS Code</strong>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Correction Detail -->
|
||||||
|
<div class="alert alert-info small mb-4">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
<strong>Reed-Solomon Error Correction:</strong> DCT mode corrects up to 16 byte errors per 223-byte chunk.
|
||||||
|
Handles problematic carrier images with uniform areas that cause unstable DCT coefficients.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- More Specs - Accordion -->
|
||||||
|
<div class="accordion" id="specsAccordion">
|
||||||
|
<div class="accordion-item bg-dark">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed bg-dark text-light py-2" type="button"
|
||||||
|
data-bs-toggle="collapse" data-bs-target="#moreSpecs">
|
||||||
|
<i class="bi bi-list-ul me-2"></i>More Specifications
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="moreSpecs" class="accordion-collapse collapse" data-bs-parent="#specsAccordion">
|
||||||
|
<div class="accordion-body p-0">
|
||||||
|
<table class="table table-dark table-striped small mb-0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><i class="bi bi-file-text me-2"></i>Max text</td>
|
||||||
|
<td><strong>2M characters</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="bi bi-upload me-2"></i>Max upload</td>
|
||||||
|
<td><strong>30 MB</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="bi bi-clock me-2"></i>File expiry</td>
|
||||||
|
<td><strong>10 min</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="bi bi-key me-2"></i>PIN</td>
|
||||||
|
<td><strong>6-9 digits</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="bi bi-shield me-2"></i>RSA keys</td>
|
||||||
|
<td><strong>2048, 3072 bit</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="bi bi-chat-quote me-2"></i>Passphrase</td>
|
||||||
|
<td><strong>3-12 words</strong> (BIP-39)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="bi bi-code me-2"></i>Python Version</td>
|
||||||
|
<td><strong>3.10-3.12</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="bi bi-box me-2"></i>Built with</td>
|
||||||
|
<td>Flask, Pillow, NumPy, SciPy, jpegio, reedsolo, cryptography, argon2-cffi</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center mt-4 text-muted small">
|
|
||||||
<p>
|
|
||||||
Stegasoo v{{ version }} •
|
|
||||||
<i class="bi bi-github me-1"></i>Open Source •
|
|
||||||
Built with Python, FastAPI, and cryptography
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
440
frontends/web/templates/account.html
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Account - Stegasoo{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-person-gear me-2"></i>Account Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Logged in as <strong>{{ username }}</strong>
|
||||||
|
{% if is_admin %}
|
||||||
|
<span class="badge bg-warning text-dark ms-2">
|
||||||
|
<i class="bi bi-shield-check me-1"></i>Admin
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if is_admin %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<a href="{{ url_for('admin_users') }}" class="btn btn-outline-primary w-100">
|
||||||
|
<i class="bi bi-people me-2"></i>Manage Users
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recovery Key Management (Admin only) -->
|
||||||
|
<div class="card bg-dark mb-4">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
|
<strong>Recovery Key</strong>
|
||||||
|
{% if has_recovery %}
|
||||||
|
<span class="badge bg-success ms-2">Configured</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary ms-2">Not Set</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="{{ url_for('regenerate_recovery') }}" class="btn btn-outline-warning"
|
||||||
|
onclick="return confirm('Generate a new recovery key? This will invalidate any existing key.')">
|
||||||
|
<i class="bi bi-arrow-repeat me-1"></i>
|
||||||
|
{{ 'Regenerate' if has_recovery else 'Generate' }}
|
||||||
|
</a>
|
||||||
|
{% if has_recovery %}
|
||||||
|
<form method="POST" action="{{ url_for('disable_recovery') }}" style="display:inline;">
|
||||||
|
<button type="submit" class="btn btn-outline-danger"
|
||||||
|
onclick="return confirm('Disable recovery? If you forget your password, you will NOT be able to recover your account.')">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
{% if has_recovery %}
|
||||||
|
Allows password reset if you're locked out.
|
||||||
|
{% else %}
|
||||||
|
No recovery option - most secure, but no password reset possible.
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h6 class="text-muted mb-3">Change Password</h6>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('account') }}" id="accountForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">
|
||||||
|
<i class="bi bi-key me-1"></i> Current Password
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" name="current_password" class="form-control"
|
||||||
|
id="currentPasswordInput" required>
|
||||||
|
<button class="btn btn-outline-secondary" type="button"
|
||||||
|
onclick="togglePassword('currentPasswordInput', this)">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">
|
||||||
|
<i class="bi bi-key-fill me-1"></i> New Password
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" name="new_password" class="form-control"
|
||||||
|
id="newPasswordInput" required minlength="8">
|
||||||
|
<button class="btn btn-outline-secondary" type="button"
|
||||||
|
onclick="togglePassword('newPasswordInput', this)">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Minimum 8 characters</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">
|
||||||
|
<i class="bi bi-key-fill me-1"></i> Confirm New Password
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" name="new_password_confirm" class="form-control"
|
||||||
|
id="newPasswordConfirmInput" required minlength="8">
|
||||||
|
<button class="btn btn-outline-secondary" type="button"
|
||||||
|
onclick="togglePassword('newPasswordConfirmInput', this)">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="bi bi-check-lg me-2"></i>Update Password
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Saved Channel Keys Section -->
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-key-fill me-2"></i>Saved Channel Keys</h5>
|
||||||
|
<span class="badge bg-secondary">{{ channel_keys|length }} / {{ max_channel_keys }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if channel_keys %}
|
||||||
|
<div class="list-group list-group-flush mb-3">
|
||||||
|
{% for key in channel_keys %}
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
|
<div>
|
||||||
|
<strong>{{ key.name }}</strong>
|
||||||
|
<br>
|
||||||
|
<code class="small text-muted">{{ key.channel_key[:4] }}...{{ key.channel_key[-4:] }}</code>
|
||||||
|
{% if key.last_used_at %}
|
||||||
|
<span class="text-muted small ms-2">Last used: {{ key.last_used_at[:10] }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
{% if is_admin %}
|
||||||
|
<button type="button" class="btn btn-outline-info"
|
||||||
|
onclick="showKeyQr('{{ key.channel_key }}', '{{ key.name }}')"
|
||||||
|
title="Show QR Code">
|
||||||
|
<i class="bi bi-qr-code"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
onclick="renameKey({{ key.id }}, '{{ key.name }}')"
|
||||||
|
title="Rename">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<form method="POST" action="{{ url_for('account_delete_key', key_id=key.id) }}"
|
||||||
|
style="display:inline;"
|
||||||
|
onsubmit="return confirm('Delete key "{{ key.name }}"?')">
|
||||||
|
<button type="submit" class="btn btn-outline-danger" title="Delete">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-3">No saved channel keys. Save keys for quick access on encode/decode pages.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if can_save_key %}
|
||||||
|
<hr>
|
||||||
|
<h6 class="text-muted mb-3">Add New Key</h6>
|
||||||
|
<form method="POST" action="{{ url_for('account_save_key') }}">
|
||||||
|
<div class="row g-2 mb-2">
|
||||||
|
<div class="col-5">
|
||||||
|
<input type="text" name="key_name" class="form-control form-control-sm"
|
||||||
|
placeholder="Key name" required maxlength="50">
|
||||||
|
</div>
|
||||||
|
<div class="col-7">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" name="channel_key" id="channelKeyInput"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
placeholder="XXXX-XXXX-..." required
|
||||||
|
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="scanChannelKeyBtn"
|
||||||
|
title="Scan QR code with camera">
|
||||||
|
<i class="bi bi-camera"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Save Key
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info mb-0 small">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
Maximum of {{ max_channel_keys }} keys reached. Delete a key to add more.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logout -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="{{ url_for('logout') }}" class="btn btn-outline-danger w-100">
|
||||||
|
<i class="bi bi-box-arrow-left me-2"></i>Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rename Modal -->
|
||||||
|
<div class="modal fade" id="renameModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="POST" id="renameForm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title">Rename Key</h6>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="text" name="new_name" class="form-control" id="renameInput"
|
||||||
|
required maxlength="50">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">Rename</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if is_admin %}
|
||||||
|
<!-- QR Code Modal (Admin only) -->
|
||||||
|
<div class="modal fade" id="qrModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h6 class="modal-title"><i class="bi bi-qr-code me-2"></i><span id="qrKeyName">Channel Key</span></h6>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<canvas id="qrCanvas" class="bg-white p-2 rounded"></canvas>
|
||||||
|
<div class="mt-2">
|
||||||
|
<code class="small" id="qrKeyDisplay"></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="qrDownload">
|
||||||
|
<i class="bi bi-download me-1"></i>Download
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="qrPrint">
|
||||||
|
<i class="bi bi-printer me-1"></i>Print Sheet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
|
||||||
|
{% if is_admin %}
|
||||||
|
<script src="{{ url_for('static', filename='js/qrcode.min.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
|
<script>
|
||||||
|
StegasooAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput');
|
||||||
|
|
||||||
|
// Webcam QR scanning for channel key input (v4.1.5)
|
||||||
|
document.getElementById('scanChannelKeyBtn')?.addEventListener('click', function() {
|
||||||
|
Stegasoo.showQrScanner((text) => {
|
||||||
|
const input = document.getElementById('channelKeyInput');
|
||||||
|
if (input) {
|
||||||
|
// Clean and format the key
|
||||||
|
const clean = text.replace(/[^A-Za-z0-9]/g, '').toUpperCase();
|
||||||
|
if (clean.length === 32) {
|
||||||
|
input.value = clean.match(/.{4}/g).join('-');
|
||||||
|
} else {
|
||||||
|
input.value = text.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 'Scan Channel Key');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format channel key input as user types
|
||||||
|
document.getElementById('channelKeyInput')?.addEventListener('input', function() {
|
||||||
|
Stegasoo.formatChannelKeyInput(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
function renameKey(keyId, currentName) {
|
||||||
|
document.getElementById('renameInput').value = currentName;
|
||||||
|
document.getElementById('renameForm').action = '/account/keys/' + keyId + '/rename';
|
||||||
|
new bootstrap.Modal(document.getElementById('renameModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
{% if is_admin %}
|
||||||
|
function showKeyQr(channelKey, keyName) {
|
||||||
|
// Format key with dashes if not already
|
||||||
|
const clean = channelKey.replace(/-/g, '').toUpperCase();
|
||||||
|
const formatted = clean.match(/.{4}/g)?.join('-') || clean;
|
||||||
|
|
||||||
|
// Update modal content
|
||||||
|
document.getElementById('qrKeyName').textContent = keyName;
|
||||||
|
document.getElementById('qrKeyDisplay').textContent = formatted;
|
||||||
|
|
||||||
|
// Generate QR code using QRious
|
||||||
|
const canvas = document.getElementById('qrCanvas');
|
||||||
|
if (typeof QRious !== 'undefined' && canvas) {
|
||||||
|
try {
|
||||||
|
new QRious({
|
||||||
|
element: canvas,
|
||||||
|
value: formatted,
|
||||||
|
size: 200,
|
||||||
|
level: 'M'
|
||||||
|
});
|
||||||
|
new bootstrap.Modal(document.getElementById('qrModal')).show();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('QR generation error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download QR as PNG
|
||||||
|
document.getElementById('qrDownload')?.addEventListener('click', function() {
|
||||||
|
const canvas = document.getElementById('qrCanvas');
|
||||||
|
const keyName = document.getElementById('qrKeyName').textContent;
|
||||||
|
if (canvas) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = 'stegasoo-channel-key-' + keyName.toLowerCase().replace(/\s+/g, '-') + '.png';
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Print tiled QR sheet (US Letter)
|
||||||
|
document.getElementById('qrPrint')?.addEventListener('click', function() {
|
||||||
|
const canvas = document.getElementById('qrCanvas');
|
||||||
|
const keyText = document.getElementById('qrKeyDisplay').textContent;
|
||||||
|
const keyName = document.getElementById('qrKeyName').textContent;
|
||||||
|
if (canvas && keyText) {
|
||||||
|
printQrSheet(canvas, keyText, keyName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Print QR codes tiled on US Letter paper (8.5" x 11")
|
||||||
|
function printQrSheet(canvas, keyText, title) {
|
||||||
|
const qrDataUrl = canvas.toDataURL('image/png');
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
if (!printWindow) {
|
||||||
|
alert('Please allow popups to print');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// US Letter: 8.5" x 11" - create 4x5 grid of QR codes
|
||||||
|
const cols = 4;
|
||||||
|
const rows = 5;
|
||||||
|
|
||||||
|
// Split key into two lines (4 groups each)
|
||||||
|
const keyParts = keyText.split('-');
|
||||||
|
const keyLine1 = keyParts.slice(0, 4).join('-');
|
||||||
|
const keyLine2 = keyParts.slice(4).join('-');
|
||||||
|
|
||||||
|
let qrGrid = '';
|
||||||
|
for (let i = 0; i < rows * cols; i++) {
|
||||||
|
qrGrid += `
|
||||||
|
<div class="qr-tile">
|
||||||
|
<div class="key-text">${keyLine1}</div>
|
||||||
|
<img src="${qrDataUrl}" alt="QR">
|
||||||
|
<div class="key-text">${keyLine2}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
printWindow.document.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: letter;
|
||||||
|
margin: 0.2in;
|
||||||
|
margin-top: 0.1in;
|
||||||
|
margin-bottom: 0.1in;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
@page { margin: 0.15in; }
|
||||||
|
html, body { margin: 0; padding: 0; }
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(${cols}, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
margin-top: 0.09in;
|
||||||
|
}
|
||||||
|
.qr-tile {
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
padding: 0.04in;
|
||||||
|
text-align: center;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
.qr-tile img {
|
||||||
|
width: 1.6in;
|
||||||
|
height: 1.6in;
|
||||||
|
}
|
||||||
|
.key-text {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="grid">${qrGrid}</div>
|
||||||
|
<div class="footer">Cut along dashed lines</div>
|
||||||
|
<script>
|
||||||
|
window.onload = function() { window.print(); };
|
||||||
|
<\/script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
printWindow.document.close();
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
50
frontends/web/templates/admin/password_reset.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Password Reset - Stegasoo{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<i class="bi bi-key fs-4 me-2"></i>
|
||||||
|
<span class="fs-5">Password Reset</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
<strong>Important:</strong> This password will only be shown once.
|
||||||
|
Make sure to share it with <strong>{{ username }}</strong> securely.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
The user's sessions have been invalidated. They will need to log in
|
||||||
|
again with the new password.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label text-muted small">New Password for {{ username }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control form-control-lg font-monospace"
|
||||||
|
value="{{ password }}" readonly id="passwordField">
|
||||||
|
<button class="btn btn-outline-secondary" type="button"
|
||||||
|
onclick="copyField('passwordField')" title="Copy password">
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('admin_users') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>Back to Users
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||