Compare commits
No commits in common. "master" and "v0.4.3" have entirely different histories.
|
@ -1,16 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
end_of_line = lf
|
|
||||||
charset = utf-8
|
|
||||||
max_line_length = 88
|
|
||||||
|
|
||||||
[*.{yml,yaml,json,js,css,html}]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.{md,rst}]
|
|
||||||
trim_trailing_whitespace = false
|
|
42
.github/workflows/pythonapp.yaml
vendored
|
@ -1,42 +0,0 @@
|
||||||
name: Python application
|
|
||||||
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
pip install poetry
|
|
||||||
poetry install --with dev
|
|
||||||
|
|
||||||
- name: Lint with flake8
|
|
||||||
run: |
|
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
|
||||||
poetry run flake8 stegano --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
||||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
|
||||||
poetry run flake8 stegano --count --max-complexity=18 --ignore=E203 --max-line-length=127 --statistics
|
|
||||||
|
|
||||||
- name: Test with pytest
|
|
||||||
run: |
|
|
||||||
poetry run nose2 -v --pretty-assert
|
|
||||||
env:
|
|
||||||
testing: actions
|
|
||||||
|
|
||||||
# - name: Type check with mypy
|
|
||||||
# run: |
|
|
||||||
# poetry run mypy .
|
|
27
.github/workflows/release.yml
vendored
|
@ -1,27 +0,0 @@
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
|
|
||||||
name: release
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pypi-publish:
|
|
||||||
name: Upload release to PyPI
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: pypi
|
|
||||||
url: https://pypi.org/p/Stegano
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install Poetry
|
|
||||||
run: python -m pip install --upgrade pip poetry
|
|
||||||
- name: Build artifacts
|
|
||||||
run: poetry build
|
|
||||||
- name: Publish package distributions to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
|
38
.gitignore
vendored
|
@ -1,38 +0,0 @@
|
||||||
# use glob syntax
|
|
||||||
syntax: glob
|
|
||||||
|
|
||||||
*.elc
|
|
||||||
*.pyc
|
|
||||||
*~
|
|
||||||
*.db
|
|
||||||
|
|
||||||
# Virtualenv
|
|
||||||
venv
|
|
||||||
build
|
|
||||||
|
|
||||||
# setuptools
|
|
||||||
build/*
|
|
||||||
stegano.egg-info/
|
|
||||||
dist/*
|
|
||||||
|
|
||||||
# tests
|
|
||||||
.coverage
|
|
||||||
.mypy_cache/
|
|
||||||
.cache/
|
|
||||||
|
|
||||||
# sphinx
|
|
||||||
docs/_build
|
|
||||||
|
|
||||||
# Emacs
|
|
||||||
eproject.cfg
|
|
||||||
|
|
||||||
# Temporary files (vim backups)
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Log files:
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Vagrant:
|
|
||||||
.vagrant/
|
|
16
.hgignore
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# use glob syntax
|
||||||
|
syntax: glob
|
||||||
|
|
||||||
|
*.elc
|
||||||
|
*.pyc
|
||||||
|
*~
|
||||||
|
*.png
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Temporary files (vim backups)
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
|
||||||
|
build/*
|
||||||
|
Stegano.egg-info/*
|
||||||
|
dist/*
|
|
@ -1,36 +0,0 @@
|
||||||
ci:
|
|
||||||
autoupdate_schedule: monthly
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
|
||||||
rev: v3.3.1
|
|
||||||
hooks:
|
|
||||||
- id: pyupgrade
|
|
||||||
args: ["--py37-plus"]
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
|
||||||
rev: 5.12.0
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: 22.3.0
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
|
||||||
rev: 6.1.0
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
additional_dependencies:
|
|
||||||
- flake8-bugbear
|
|
||||||
- flake8-implicit-str-concat
|
|
||||||
args: ["--max-line-length=125", "--ignore=E203"]
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v4.1.0
|
|
||||||
hooks:
|
|
||||||
- id: fix-byte-order-marker
|
|
||||||
- id: trailing-whitespace
|
|
||||||
exclude: .md
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
exclude: tests/.*
|
|
||||||
- repo: https://github.com/pypa/pip-audit
|
|
||||||
rev: v2.9.0
|
|
||||||
hooks:
|
|
||||||
- id: pip-audit
|
|
|
@ -1,22 +0,0 @@
|
||||||
# .readthedocs.yaml
|
|
||||||
# Read the Docs configuration file
|
|
||||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
||||||
|
|
||||||
# Required
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
# Set the version of Python and other tools you might need
|
|
||||||
build:
|
|
||||||
os: ubuntu-22.04
|
|
||||||
tools:
|
|
||||||
python: "3.11"
|
|
||||||
|
|
||||||
# Build documentation in the docs/ directory with Sphinx
|
|
||||||
sphinx:
|
|
||||||
configuration: docs/conf.py
|
|
||||||
|
|
||||||
# We recommend specifying your dependencies to enable reproducible builds:
|
|
||||||
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
|
||||||
python:
|
|
||||||
install:
|
|
||||||
- requirements: docs/requirements.txt
|
|
355
CHANGELOG.md
|
@ -1,349 +1,14 @@
|
||||||
## Release History
|
===============
|
||||||
|
Release History
|
||||||
|
===============
|
||||||
|
|
||||||
### 2.0.0 (2025-06-22)
|
0.4.3 (2015-10-06)
|
||||||
|
------------------
|
||||||
|
|
||||||
- Added functions for hiding/revealing messages in PCM encoded .wav files.
|
* bug fixes for Python 3;
|
||||||
([#54](https://github.com/cedricbonhomme/Stegano/pull/54))
|
* bug fixes in the scripts installed in */usr/local/bin*.
|
||||||
- Improved typing.
|
|
||||||
- Updated dependencies.
|
|
||||||
|
|
||||||
|
0.4.2 (2015-10-05)
|
||||||
|
------------------
|
||||||
|
|
||||||
### 1.0.1 (2025-05-03)
|
* first stable release on PypI.
|
||||||
|
|
||||||
- Improved the packaging configuration for the command line (stegano.console).
|
|
||||||
|
|
||||||
|
|
||||||
### 1.0.0 (2025-04-26)
|
|
||||||
|
|
||||||
- Updated dependencies.
|
|
||||||
- Improved the packaging configuration.
|
|
||||||
- Fixed typing errors.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.11.5 (2025-02-13)
|
|
||||||
|
|
||||||
- Updated dependencies.
|
|
||||||
- Aligned pyproject.toml with the standard specification.
|
|
||||||
- Publishing to PyPI using a Trusted Publisher.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.11.4 (2024-09-07)
|
|
||||||
|
|
||||||
- Added a parameter, close_file, to lsb.reveal in order to
|
|
||||||
specify if the file must be closed at the end of the processing.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.11.3 (2024-01-02)
|
|
||||||
|
|
||||||
- Stegano now supports Python 3.12. Support of Python 3.8 has been removed.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.11.2 (2023-05-23)
|
|
||||||
|
|
||||||
- improved typing of various functions;
|
|
||||||
- updated dependencies.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.11.1 (2022-11-20)
|
|
||||||
|
|
||||||
- Fixed a bug in the command line when no sub-command is specified.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.11.0 (2022-11-20)
|
|
||||||
|
|
||||||
- Reduced memory footprint and processing speed,
|
|
||||||
the modules ``lsb`` and ``lsbset`` have been merged
|
|
||||||
([PR #34](https://github.com/cedricbonhomme/Stegano/pull/34)).
|
|
||||||
|
|
||||||
|
|
||||||
### 0.10.2 (2022-01-13)
|
|
||||||
|
|
||||||
- Stegano now uses Pillow 9.0.0 (CVE-2022-22815).
|
|
||||||
|
|
||||||
|
|
||||||
### 0.10.1 (2021-11-30)
|
|
||||||
|
|
||||||
- Stegano now uses OpenCV Python 4.5.4 abd Numpy 1.21.4.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.10.0 (2021-11-29)
|
|
||||||
|
|
||||||
- new: Implemented Shi-Tomashi corner generator
|
|
||||||
([PR #32](https://github.com/cedricbonhomme/Stegano/pull/32)).
|
|
||||||
Implemented by thundersparkf (see CONTRIBUTORS.md file).
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.9 (2021-07-02)
|
|
||||||
|
|
||||||
- Stegano now uses Pillow 8.3.0.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.8 (2019-12-20)
|
|
||||||
|
|
||||||
- Stegano is now using poetry;
|
|
||||||
- minor improvements to the command line.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.7 (2019-10-27)
|
|
||||||
|
|
||||||
- fixed markdown of the previous release.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.6 (2019-10-27)
|
|
||||||
|
|
||||||
- fixed markdown of the previous release;
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.5 (2019-10-27)
|
|
||||||
|
|
||||||
- updated dependencies;
|
|
||||||
- home page of the project is now: https://github.com/cedricbonhomme/Stegano
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.4 (2019-06-05)
|
|
||||||
|
|
||||||
- new: Implemented LFSR generator (with tests and CLI)
|
|
||||||
([PR #27](https://github.com/cedricbonhomme/Stegano/pull/27))
|
|
||||||
- new: Implemented Ackermann generators CLI interface
|
|
||||||
([PR #26](https://github.com/cedricbonhomme/Stegano/pull/26))
|
|
||||||
- new: The Ackermann functions are not actual generators
|
|
||||||
([#24](https://github.com/cedricbonhomme/Stegano/issues/24))
|
|
||||||
- new: add a shift parameter for the lsbmodule
|
|
||||||
([#25](https://github.com/cedricbonhomme/Stegano/issues/25))
|
|
||||||
- fix: lsbset.hide cause .png transparent area lost
|
|
||||||
([#23](https://github.com/cedricbonhomme/Stegano/issues/23))
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.3 (2019-04-10)
|
|
||||||
|
|
||||||
- it is now possible to either pass the location of an image or directly pass
|
|
||||||
an already opened Image.Image to the hide and reveal methods;
|
|
||||||
- code re-formatted a bit with black.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.2 (2019-04-04)
|
|
||||||
|
|
||||||
- updated Pillow dependency to version 6.0.0 in order to fix a bug when opening
|
|
||||||
some PNG files (https://github.com/python-pillow/Pillow/issues/3557).
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.1 (2019-03-06)
|
|
||||||
|
|
||||||
- updated Pillow dependency in order to fix a bug when opening some PNG files.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.9.0 (2018-12-18)
|
|
||||||
|
|
||||||
- added the possibility to shift the encoded bits when using the lsbset module.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.8.6 (2018-11-05)
|
|
||||||
|
|
||||||
- fixed a potential security issue related to CVE-2018-18074.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.8.5 (2018-04-18)
|
|
||||||
|
|
||||||
- Fixed an encoding problem which occured on Windows during the installation
|
|
||||||
of the module.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.8.4 (2018-02-28)
|
|
||||||
|
|
||||||
- Stegano is ready for use with pipenv and pipsi.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.8.3 (2018-02-23)
|
|
||||||
|
|
||||||
- the recommended way to install Stegano is now to use pipenv.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.8.2 (2017-12-20)
|
|
||||||
|
|
||||||
- Fixed a bug with the new 'encoding' function when using Stegano as a command
|
|
||||||
line tool. No default value was set. Default value is UTF-8.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.8.1 (2017-05-16)
|
|
||||||
|
|
||||||
- it is now possible to specify the encoding (UTF-8 or UTF-32LE) of the message
|
|
||||||
to hide/reveal through the command line;
|
|
||||||
- the help of the command line now displays the available choices for the
|
|
||||||
arguments, if it is necessary (list of available encodings, list of available
|
|
||||||
generators);
|
|
||||||
- tests expected results lies now in a dedicated folder;
|
|
||||||
- a script has been added in order to get proper exit code check for mypy.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.8 (2017-05-06)
|
|
||||||
|
|
||||||
- updated command line. All commands are now prefixed with *stegano-*;
|
|
||||||
- improved type hints;
|
|
||||||
- it is possible to load and save images from and to file objects (BytesIO);
|
|
||||||
- improved checks when revealing a message with the lsbset module fails.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.7.1 (2017-05-05)
|
|
||||||
|
|
||||||
- improved generators for the lsb-set module;
|
|
||||||
- improved tests for the generators;
|
|
||||||
- improved type hints.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.7 (2017-05-04)
|
|
||||||
|
|
||||||
- unicode is now supported. By default UTF-8 encoding is used. UTF-32LE can also
|
|
||||||
be used to hide non-ASCII characters. UTF-8 (8 bits) is the default choice
|
|
||||||
since it is possible to hide longer messages with it.
|
|
||||||
- improved checks with type hints.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.9 (2017-03-10)
|
|
||||||
|
|
||||||
- introduces some type hints (PEP 484);
|
|
||||||
- more tests for the generators and for the tools module;
|
|
||||||
- updated descriptions of generators;
|
|
||||||
- fixed a bug with a generator that has been previously renamed.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.8 (2017-03-08)
|
|
||||||
|
|
||||||
- bugfix: fixed #12: Error when revealing a hidden binary file in an image.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.7 (2017-02-21)
|
|
||||||
|
|
||||||
- bugfix: added missing dependency in the setup.py file.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.6 (2017-02-20)
|
|
||||||
|
|
||||||
- improved docstrings for the desciption of the generators;
|
|
||||||
- improved the command which displays the list of generators.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.5 (2017-02-16)
|
|
||||||
|
|
||||||
- added a command to list all available generators for the lsb-set module;
|
|
||||||
- test when the data image is coming via byte stream, for the lsb module.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.4 (2017-02-06)
|
|
||||||
|
|
||||||
- a command line for the 'red' module has been added;
|
|
||||||
- bugfix: fixed a bug in the lsb-set command line when the generator wasn't
|
|
||||||
specified by the user.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.3 (2017-01-29)
|
|
||||||
|
|
||||||
- Support for transparent PNG images has been added (lsb and lsbset modules).
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.2 (2017-01-19)
|
|
||||||
|
|
||||||
- bugfix: solved a bug when the image data is coming via byte streams (ByteIO),
|
|
||||||
for the exifHeader hiding method.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6.1 (2016-08-25)
|
|
||||||
|
|
||||||
- reorganization of the steganalysis sub-module.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.6 (2016-08-04)
|
|
||||||
|
|
||||||
- improvements of the command line of Stéganô. The use of Stéganô through the
|
|
||||||
command line has slightly changed ('hide' and 'reveal' are now sub-parameters
|
|
||||||
of the command line). No changes if you use Stéganô as a module in your
|
|
||||||
software. The documentation has been updated accordingly.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.5.5 (2016-08-03)
|
|
||||||
|
|
||||||
- bugfix: Incorrect padding size in `base642string` in tools.base642binary().
|
|
||||||
|
|
||||||
|
|
||||||
### 0.5.4 (2016-05-22)
|
|
||||||
|
|
||||||
- the generator provided to the functions lsbset.hide() and lsbset.reveal() is
|
|
||||||
now a function. This is more convenient for a user who wants to use a custom
|
|
||||||
generator (not in the module lsbset.generators).
|
|
||||||
- performance improvements for the lsb and lsbset modules.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.5.3 (2016-05-19)
|
|
||||||
|
|
||||||
- reorganization of all modules. No impact for the users of Stegano.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.5.2 (2016-05-18)
|
|
||||||
|
|
||||||
- improvements and bug fixes for the exifHeader module;
|
|
||||||
- added unit tests for the exifHeader module;
|
|
||||||
- improvements of the documentation.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.5.1 (2016-04-16)
|
|
||||||
|
|
||||||
- minor improvements and bug fixes;
|
|
||||||
- added unit tests for the slsb and slsbset modules.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.5 (2016-03-18)
|
|
||||||
|
|
||||||
- management of greyscale images.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.4.6 (2016-03-12)
|
|
||||||
|
|
||||||
- bugfix when the length of the message to hide is not divisible by 3,
|
|
||||||
for the slsb and slsbset module.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.4.5 (2015-12-23)
|
|
||||||
|
|
||||||
- bugfix.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.4.4 (2015-12-23)
|
|
||||||
|
|
||||||
- new project home page;
|
|
||||||
- minor updated to the documentation.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.4.3 (2015-10-06)
|
|
||||||
|
|
||||||
- bug fixes for Python 3;
|
|
||||||
- bug fixes in the scripts in *./bin*.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.4.2 (2015-10-05)
|
|
||||||
|
|
||||||
- first stable release on PypI.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.4 (2012-01-02)
|
|
||||||
|
|
||||||
This release introduces a more advanced LSB (Least Significant Bit) method
|
|
||||||
based on integers sets. The sets generated with Python generators
|
|
||||||
(Sieve of Eratosthenes, Fermat, Carmichael numbers, etc.) are used to select
|
|
||||||
the pixels used to hide the information. You can use these new methods in your
|
|
||||||
Python codes as a Python module or as a program in your scripts.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.3 (2011-04-15)
|
|
||||||
|
|
||||||
- you can now use Stéganô as a library in your Python program;
|
|
||||||
(python setup.py install) or as a 'program' thanks to the scripts provided
|
|
||||||
in the bin directory;
|
|
||||||
- new documentation (reStructuredText) comes with Stéganô.
|
|
||||||
|
|
||||||
|
|
||||||
### 0.2 (2011-03-24)
|
|
||||||
|
|
||||||
- this release introduces some bugfixes and a major speed improvement of the
|
|
||||||
*reveal* function for the LSB method. Moreover it is now possible to hide a
|
|
||||||
binary file (ogg, executable, etc.);
|
|
||||||
- a new technique for hiding/revealing a message in a JPEG picture by using the
|
|
||||||
description field of the image is provided.
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
## Owner
|
|
||||||
|
|
||||||
|
|
||||||
- Cédric Bonhomme <cedric@cedricbonhomme.org>
|
|
||||||
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
|
|
||||||
- Alexander Treml - https://github.com/AlexanderTreml
|
|
||||||
- Adrien Cosson - https://cosson.io
|
|
||||||
- Andrew Roberts <andy.roberts.uk@gmail.com>
|
|
||||||
- Christophe Goessen - https://github.com/cgoessen
|
|
||||||
- Flavien Roux - https://github.com/FlavienRx
|
|
||||||
- Maxwell Gerber - https://github.com/maxwellgerber
|
|
||||||
- Mickaël Schoentgen <mschoentgen@nuxeo.com>
|
|
||||||
- Nejdet Çağdaş Yücesoy <nejdetyucesoy@gmail.com>
|
|
||||||
- panni <panni@fragstore.net>
|
|
||||||
- Peter Justin <peter@peterjustin.me>
|
|
||||||
- thundersparkf - https://github.com/thundersparkf
|
|
||||||
|
|
||||||
|
|
||||||
And thank you to the testers!
|
|
13
MANIFEST.in
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#documentation
|
||||||
|
recursive-include docs *
|
||||||
|
|
||||||
|
#example files
|
||||||
|
recursive-include examples *
|
||||||
|
|
||||||
|
# binary files
|
||||||
|
recursive-include bin *
|
||||||
|
|
||||||
|
#Misc
|
||||||
|
include COPYING
|
||||||
|
include README.md
|
||||||
|
include requirements.txt
|
127
README.md
|
@ -1,115 +1,54 @@
|
||||||
# Stegano
|
Stéganô
|
||||||
|
=======
|
||||||
|
|
||||||
[](https://github.com/cedricbonhomme/Stegano/actions?query=workflow%3A%22Python+application%22)
|
A Python Steganography module.
|
||||||
|
|
||||||
[Stegano](https://github.com/cedricbonhomme/Stegano), a pure Python Steganography
|
|
||||||
module.
|
|
||||||
|
|
||||||
Steganography is the art and science of writing hidden messages in such a way
|
|
||||||
that no one, apart from the sender and intended recipient, suspects the
|
|
||||||
existence of the message, a form of security through obscurity. Consequently,
|
|
||||||
functions provided by Stegano only hide messages, without encryption.
|
|
||||||
Steganography is often used with cryptography.
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
$ sudo pip install Stegano
|
||||||
|
|
||||||
|
|
||||||
```bash
|
Use Stéganô as a library in your Python program
|
||||||
$ poetry install stegano
|
-----------------------------------------------
|
||||||
```
|
|
||||||
|
|
||||||
You will be able to use Stegano in your Python programs.
|
If you want to use Stéganô in your Python program you just have to import the
|
||||||
|
|
||||||
If you only want to install Stegano as a command line tool:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ pipx install stegano
|
|
||||||
```
|
|
||||||
|
|
||||||
pipx installs scripts (system wide available) provided by Python packages into
|
|
||||||
separate virtualenvs to shield them from your system and each other.
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
A [tutorial](https://stegano.readthedocs.io) is available.
|
|
||||||
|
|
||||||
|
|
||||||
## Use Stegano as a library in your Python program
|
|
||||||
|
|
||||||
If you want to use Stegano in your Python program you just have to import the
|
|
||||||
appropriate steganography technique. For example:
|
appropriate steganography technique. For example:
|
||||||
|
|
||||||
```python
|
|
||||||
>>> from stegano import lsb
|
>>> from stegano import slsb
|
||||||
>>> secret = lsb.hide("./tests/sample-files/Lenna.png", "Hello World")
|
>>> secret = slsb.hide("./pictures/Lenna.png", "Hello Workd")
|
||||||
>>> secret.save("./Lenna-secret.png")
|
>>> secret.save("./Lenna-secret.png")
|
||||||
>>>
|
|
||||||
>>> clear_message = lsb.reveal("./Lenna-secret.png")
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Use Stegano as a command line tool
|
Use Stéganô as a program
|
||||||
|
------------------------
|
||||||
|
|
||||||
### Hide and reveal a message
|
In addition you can use Stéganô as a program.
|
||||||
|
|
||||||
```bash
|
Example:
|
||||||
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m "Secret Message" -o Lena1.png
|
|
||||||
$ stegano-lsb reveal -i Lena1.png
|
$ slsb --hide -i ../examples/pictures/Lenna.png -o Lena1.png -m "Secret Message"
|
||||||
Secret Message
|
|
||||||
```
|
Another example (hide the message with Sieve of Eratosthenes):
|
||||||
|
|
||||||
|
$ slsb-set --hide -i ../examples/pictures/Lenna.png -o Lena2.png --generator eratosthenes -m 'Secret Message'
|
||||||
|
|
||||||
|
|
||||||
### Hide the message with the Sieve of Eratosthenes
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
```bash
|
There are some examples in the folder *examples*.
|
||||||
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m 'Secret Message' --generator eratosthenes -o Lena2.png
|
|
||||||
```
|
|
||||||
|
|
||||||
The message will be scattered in the picture, following a set described by the
|
|
||||||
Sieve of Eratosthenes. Other sets are available. You can also use your own
|
|
||||||
generators.
|
|
||||||
|
|
||||||
This will make a steganalysis more complicated.
|
|
||||||
|
|
||||||
|
|
||||||
## Running the tests
|
Turorial
|
||||||
|
--------
|
||||||
|
|
||||||
```bash
|
A [tutorial](https://stegano.readthedocs.org/en/latest/tutorial) is available.
|
||||||
$ python -m unittest discover -v
|
|
||||||
```
|
|
||||||
|
|
||||||
Running the static type checker:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ mypy stegano
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Contributions
|
Contact
|
||||||
|
-------
|
||||||
|
|
||||||
Contributions are welcome. If you want to contribute to Stegano I highly
|
[My home page](https://www.cedricbonhomme.org).
|
||||||
recommend you to install it in a Python virtual environment with poetry.
|
|
||||||
|
|
||||||
|
|
||||||
## Donations
|
|
||||||
|
|
||||||
If you wish and if you like Stegano, you can donate via GitHub Sponsors:
|
|
||||||
|
|
||||||
[](https://github.com/sponsors/cedricbonhomme)
|
|
||||||
|
|
||||||
or with Bitcoin to this address:
|
|
||||||
bc1q56u6sj7cvlwu58v5lemljcvkh7v2gc3tv8mj0e
|
|
||||||
|
|
||||||
Thank you !
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This software is licensed under
|
|
||||||
[GNU General Public License version 3](https://www.gnu.org/licenses/gpl-3.0.html)
|
|
||||||
|
|
||||||
Copyright (C) 2010-2025 [Cédric Bonhomme](https://www.cedricbonhomme.org)
|
|
||||||
|
|
||||||
For more information, [the list of authors and contributors](CONTRIBUTORS.md) is available.
|
|
||||||
|
|
85
bin/slsb
Executable file
|
@ -0,0 +1,85 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2011 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.1 $"
|
||||||
|
__date__ = "$Date: 2011/04/06 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from stegano import slsb
|
||||||
|
except:
|
||||||
|
print("Install Stegano: sudo pip install Stegano")
|
||||||
|
|
||||||
|
from stegano import tools
|
||||||
|
|
||||||
|
from optparse import OptionParser
|
||||||
|
parser = OptionParser(version=__version__)
|
||||||
|
parser.add_option('--hide', action='store_true', default=False,
|
||||||
|
help="Hides a message in an image.")
|
||||||
|
parser.add_option('--reveal', action='store_true', default=False,
|
||||||
|
help="Reveals the message hided in an image.")
|
||||||
|
# Original image
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Input image file.")
|
||||||
|
# Image containing the secret
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Output image containing the secret.")
|
||||||
|
|
||||||
|
# Non binary secret message to hide
|
||||||
|
parser.add_option("-m", "--secret-message", dest="secret_message",
|
||||||
|
help="Your secret message to hide (non binary).")
|
||||||
|
|
||||||
|
# Binary secret to hide (OGG, executable, etc.)
|
||||||
|
parser.add_option("-f", "--secret-file", dest="secret_file",
|
||||||
|
help="Your secret to hide (Text or any binary file).")
|
||||||
|
# Output for the binary binary secret.
|
||||||
|
parser.add_option("-b", "--binary", dest="secret_binary",
|
||||||
|
help="Output for the binary secret (Text or any binary file).")
|
||||||
|
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Lenna.png',
|
||||||
|
output_image_file = './pictures/Lenna_enc.png',
|
||||||
|
secret_message = '', secret_file = '', secret_binary = "")
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if options.hide:
|
||||||
|
if options.secret_message != "" and options.secret_file == "":
|
||||||
|
secret = options.secret_message
|
||||||
|
elif options.secret_message == "" and options.secret_file != "":
|
||||||
|
secret = tools.binary2base64(options.secret_file)
|
||||||
|
|
||||||
|
img_encoded = slsb.hide(options.input_image_file, secret)
|
||||||
|
try:
|
||||||
|
img_encoded.save(options.output_image_file)
|
||||||
|
except Exception as e:
|
||||||
|
# If hide() returns an error (Too long message).
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
elif options.reveal:
|
||||||
|
secret = slsb.reveal(options.input_image_file)
|
||||||
|
if options.secret_binary != "":
|
||||||
|
data = tools.base642binary(secret)
|
||||||
|
with open(options.secret_binary, "w") as f:
|
||||||
|
f.write(data)
|
||||||
|
else:
|
||||||
|
print(secret)
|
95
bin/slsb-set
Executable file
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2011 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.1 $"
|
||||||
|
__date__ = "$Date: 2011/12/29 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from stegano import slsbset
|
||||||
|
except:
|
||||||
|
print("Install stegano: sudo pip install Stegano")
|
||||||
|
|
||||||
|
from stegano import tools
|
||||||
|
|
||||||
|
from optparse import OptionParser
|
||||||
|
parser = OptionParser(version=__version__)
|
||||||
|
parser.add_option('--hide', action='store_true', default=False,
|
||||||
|
help="Hides a message in an image.")
|
||||||
|
parser.add_option('--reveal', action='store_true', default=False,
|
||||||
|
help="Reveals the message hided in an image.")
|
||||||
|
# Original image
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Input image file.")
|
||||||
|
|
||||||
|
# Generator
|
||||||
|
parser.add_option("-g", "--generator", dest="generator_function",
|
||||||
|
help="Generator")
|
||||||
|
|
||||||
|
# Image containing the secret
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Output image containing the secret.")
|
||||||
|
|
||||||
|
# Non binary secret message to hide
|
||||||
|
parser.add_option("-m", "--secret-message", dest="secret_message",
|
||||||
|
help="Your secret message to hide (non binary).")
|
||||||
|
|
||||||
|
# Binary secret to hide (OGG, executable, etc.)
|
||||||
|
parser.add_option("-f", "--secret-file", dest="secret_file",
|
||||||
|
help="Your secret to hide (Text or any binary file).")
|
||||||
|
# Output for the binary binary secret.
|
||||||
|
parser.add_option("-b", "--binary", dest="secret_binary",
|
||||||
|
help="Output for the binary secret (Text or any binary file).")
|
||||||
|
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Lenna.png',
|
||||||
|
generator_function = 'fermat',
|
||||||
|
output_image_file = './pictures/Lenna_enc.png',
|
||||||
|
secret_message = '', secret_file = '', secret_binary = "")
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if options.hide:
|
||||||
|
if options.secret_message != "" and options.secret_file == "":
|
||||||
|
secret = options.secret_message
|
||||||
|
elif options.secret_message == "" and options.secret_file != "":
|
||||||
|
secret = tools.binary2base64(options.secret_file)
|
||||||
|
|
||||||
|
img_encoded = slsbset.hide(options.input_image_file, secret, options.generator_function)
|
||||||
|
try:
|
||||||
|
img_encoded.save(options.output_image_file)
|
||||||
|
except Exception as e:
|
||||||
|
# If hide() returns an error (Too long message).
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
elif options.reveal:
|
||||||
|
try:
|
||||||
|
secret = slsbset.reveal(options.input_image_file, options.generator_function)
|
||||||
|
except IndexError:
|
||||||
|
print("Impossible to detect message.")
|
||||||
|
exit(0)
|
||||||
|
if options.secret_binary != "":
|
||||||
|
data = tools.base642binary(secret)
|
||||||
|
with open(options.secret_binary, "w") as f:
|
||||||
|
f.write(data)
|
||||||
|
else:
|
||||||
|
print(secret)
|
46
bin/steganalysis-parity
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2011 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.1 $"
|
||||||
|
__date__ = "$Date: 2011/12/29 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from stegano import steganalysisParity
|
||||||
|
except:
|
||||||
|
print("Install Stegano: sudo pip install Stegano")
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from optparse import OptionParser
|
||||||
|
parser = OptionParser()
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Image file")
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Image file")
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Lenna.png',
|
||||||
|
output_image_file = './pictures/Lenna_steganalysed.png')
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
input_image_file = Image.open(options.input_image_file)
|
||||||
|
output_image = steganalysisParity.steganalyse(input_image_file)
|
||||||
|
output_image.save(options.output_image_file)
|
164
docs/conf.py
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Stéganô documentation build configuration file, created by
|
# Stéganô documentation build configuration file, created by
|
||||||
# sphinx-quickstart on Wed Jul 25 13:33:39 2012.
|
# sphinx-quickstart on Wed Jul 25 13:33:39 2012.
|
||||||
|
@ -9,162 +10,233 @@
|
||||||
#
|
#
|
||||||
# All configuration values have a default; values that are commented out
|
# All configuration values have a default; values that are commented out
|
||||||
# serve to show the default.
|
# serve to show the default.
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
#sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
# -- General configuration -----------------------------------------------------
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
# needs_sphinx = '1.0'
|
#needs_sphinx = '1.0'
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
extensions = []
|
extensions = []
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ["_templates"]
|
templates_path = ['_templates']
|
||||||
|
|
||||||
# The suffix of source filenames.
|
# The suffix of source filenames.
|
||||||
source_suffix = ".rst"
|
source_suffix = '.rst'
|
||||||
|
|
||||||
# The encoding of source files.
|
# The encoding of source files.
|
||||||
# source_encoding = 'utf-8-sig'
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = "index"
|
master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = "Stegano"
|
project = u'Stéganô'
|
||||||
copyright = "2010-2025, Cédric Bonhomme"
|
copyright = u'2012, Cédric Bonhomme'
|
||||||
author = "Cédric Bonhomme <cedric@cedricbonhomme.org>"
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = "0.11"
|
version = '0.4'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = "0.11.0"
|
release = '0.4'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
# language = None
|
#language = None
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
# non-false value, then it is used:
|
# non-false value, then it is used:
|
||||||
# today = ''
|
#today = ''
|
||||||
# Else, today_fmt is used as the format for a strftime call.
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
# today_fmt = '%B %d, %Y'
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
exclude_patterns = ["_build"]
|
exclude_patterns = ['_build']
|
||||||
|
|
||||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||||
# default_role = None
|
#default_role = None
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
# add_function_parentheses = True
|
#add_function_parentheses = True
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
# If true, the current module name will be prepended to all description
|
||||||
# unit titles (such as .. function::).
|
# unit titles (such as .. function::).
|
||||||
# add_module_names = True
|
#add_module_names = True
|
||||||
|
|
||||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
# output. They are ignored by default.
|
# output. They are ignored by default.
|
||||||
# show_authors = False
|
#show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
# A list of ignored prefixes for module index sorting.
|
||||||
# modindex_common_prefix = []
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = 'default'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
# html_theme_options = {}
|
#html_theme_options = {}
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
# html_theme_path = []
|
#html_theme_path = []
|
||||||
|
|
||||||
# The name for this set of Sphinx documents. If None, it defaults to
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
# "<project> v<release> documentation".
|
# "<project> v<release> documentation".
|
||||||
# html_title = None
|
#html_title = None
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
# html_short_title = None
|
#html_short_title = None
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
# of the sidebar.
|
# of the sidebar.
|
||||||
# html_logo = None
|
#html_logo = None
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
# pixels large.
|
# pixels large.
|
||||||
# html_favicon = None
|
#html_favicon = None
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ["_static"]
|
html_static_path = ['_static']
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
# using the given strftime format.
|
# using the given strftime format.
|
||||||
# html_last_updated_fmt = '%b %d, %Y'
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
# typographically correct entities.
|
# typographically correct entities.
|
||||||
# html_use_smartypants = True
|
#html_use_smartypants = True
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
# Custom sidebar templates, maps document names to template names.
|
||||||
# html_sidebars = {}
|
#html_sidebars = {}
|
||||||
|
|
||||||
# Additional templates that should be rendered to pages, maps page names to
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
# template names.
|
# template names.
|
||||||
# html_additional_pages = {}
|
#html_additional_pages = {}
|
||||||
|
|
||||||
# If false, no module index is generated.
|
# If false, no module index is generated.
|
||||||
# html_domain_indices = True
|
#html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#html_use_index = True
|
||||||
|
|
||||||
# If true, the index is split into individual pages for each letter.
|
# If true, the index is split into individual pages for each letter.
|
||||||
# html_split_index = False
|
#html_split_index = False
|
||||||
|
|
||||||
# If true, links to the reST sources are added to the pages.
|
# If true, links to the reST sources are added to the pages.
|
||||||
# html_show_sourcelink = True
|
#html_show_sourcelink = True
|
||||||
|
|
||||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
# html_show_sphinx = True
|
#html_show_sphinx = True
|
||||||
|
|
||||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
# html_show_copyright = True
|
#html_show_copyright = True
|
||||||
|
|
||||||
# If true, an OpenSearch description file will be output, and all pages will
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
# contain a <link> tag referring to it. The value of this option must be the
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
# base URL from which the finished HTML is served.
|
# base URL from which the finished HTML is served.
|
||||||
# html_use_opensearch = ''
|
#html_use_opensearch = ''
|
||||||
|
|
||||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
# html_file_suffix = None
|
#html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'Stgandoc'
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
latex_engine = "pdflatex"
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#'preamble': '',
|
||||||
|
}
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
("index", "Stgan.tex", "Stegano Documentation", "Cédric Bonhomme", "howto"),
|
('index', 'Stgan.tex', u'Stéganô Documentation',
|
||||||
|
u'Cédric Bonhomme', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
latex_show_urls = True
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
latex_show_pagerefs = True
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
ADDITIONAL_PREAMBLE = r"""
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
\setcounter{tocdepth}{3}
|
# not chapters.
|
||||||
"""
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output --------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
('index', 'stgan', u'Stéganô Documentation',
|
||||||
|
[u'Cédric Bonhomme'], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ------------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
('index', 'Stgan', u'Stéganô Documentation',
|
||||||
|
u'Cédric Bonhomme', 'Stgan', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
#texinfo_show_urls = 'footnote'
|
||||||
|
|
|
@ -3,78 +3,84 @@
|
||||||
You can adapt this file completely to your liking, but it should at least
|
You can adapt this file completely to your liking, but it should at least
|
||||||
contain the root `toctree` directive.
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
Presentation
|
Welcome to Stéganô's documentation!
|
||||||
|
===================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
Stéganô is a Python steganography_ module.
|
||||||
|
Steganography is the art and science of writing hidden messages in such a way that no one,
|
||||||
|
apart from the sender and intended recipient, suspects the existence of the message, a form
|
||||||
|
of security through obscurity. Consequently, functions provided by Stéganô only hide message,
|
||||||
|
without encryption. Indeed steganography is often used with cryptography.
|
||||||
|
|
||||||
|
The advantage of steganography, over cryptography alone, is that messages do not attract
|
||||||
|
attention to themselves. If you are interested in cryptography have a look at my project pySecret.
|
||||||
|
|
||||||
|
|
||||||
|
Download Stéganô
|
||||||
|
================
|
||||||
|
|
||||||
|
You can clone the source code of Stéganô_ :
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ hg clone https://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
|
||||||
|
More information about how to install Stéganô in the :doc:`tutorial </tutorial>`.
|
||||||
|
|
||||||
|
Requirements
|
||||||
============
|
============
|
||||||
|
|
||||||
Stegano_ is a pure Python steganography_ module.
|
- Python_ >= 3.2 (tested with Python 3.3.1);
|
||||||
|
- `Pillow`_ (friendly fork of Python Imaging Library).
|
||||||
|
|
||||||
Steganography is the art and science of writing hidden messages in such a way
|
Methods of hiding
|
||||||
that no one, apart from the sender and intended recipient, suspects the
|
=================
|
||||||
existence of the message, a form of security through obscurity.
|
|
||||||
Consequently, functions provided by Stegano only hide messages,
|
|
||||||
without encryption. Steganography is often used with cryptography.
|
|
||||||
|
|
||||||
Stegano implements these methods of hiding:
|
For the moment, Stéganô implements these methods of hiding:
|
||||||
|
|
||||||
- using the red portion of a pixel to hide ASCII messages;
|
- using the red portion of a pixel to hide ASCII messages;
|
||||||
- using the `Least Significant Bit <http://en.wikipedia.org/wiki/Least_significant_bit>`_ (LSB) technique;
|
- using the `Least Significant Bit <http://en.wikipedia.org/wiki/Least_significant_bit>`_ (LSB) technique;
|
||||||
- using the LSB technique with sets based on generators (Sieve for Eratosthenes, Fermat, Mersenne numbers, etc.);
|
- using the LSB technique with sets based on generators (Sieve for Eratosthenes, Fermat, Mersenne numbers, etc.);
|
||||||
- using the description field of the image (JPEG and TIFF).
|
- using the description field of the image (JPEG).
|
||||||
|
|
||||||
Moreover some methods of steganalysis_ are provided:
|
Moreover some methods of steganalysis_ are provided:
|
||||||
|
|
||||||
- steganalysis of LSB encoding in color images;
|
- steganalysis of LSB encoding in color images;
|
||||||
- statistical steganalysis.
|
- statistical steganalysis.
|
||||||
|
|
||||||
You can also use Stegano through a `Web service <https://github.com/cedricbonhomme/stegano-web>`_.
|
|
||||||
Not all functionalities of Stegano are covered.
|
|
||||||
|
|
||||||
Requirements
|
Turorial
|
||||||
============
|
|
||||||
|
|
||||||
- Python_ 3;
|
|
||||||
- `Pillow`_;
|
|
||||||
- `piexif`_.
|
|
||||||
|
|
||||||
|
|
||||||
Tutorial
|
|
||||||
========
|
========
|
||||||
|
|
||||||
.. toctree::
|
More information available at the :doc:`tutorial </tutorial>` page
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
installation
|
|
||||||
module
|
|
||||||
software
|
|
||||||
steganalysis
|
|
||||||
|
|
||||||
You can have a look at the
|
|
||||||
`unit tests <https://github.com/cedricbonhomme/Stegano/tree/master/tests>`_.
|
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Stegano_ is under GPL v3 license.
|
Stéganô is under GPL v3 license.
|
||||||
|
|
||||||
|
|
||||||
Donation
|
Donation
|
||||||
========
|
========
|
||||||
|
|
||||||
If you wish and if you like Stegano, you can
|
If you wish and if you like Stéganô, you can donate via bitcoin. My bitcoin address: `1GVmhR9fbBeEh7rP1qNq76jWArDdDQ3otZ <http://blockexplorer.com/address/1GVmhR9fbBeEh7rP1qNq76jWArDdDQ3otZ>`_
|
||||||
`donate <https://github.com/sponsors/cedricbonhomme>`_.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Contact
|
Contact
|
||||||
=======
|
=======
|
||||||
|
|
||||||
`My home page <https://www.cedricbonhomme.org>`_
|
`My home page <http://cedricbonhomme.org>`_
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
.. _Python: https://www.python.org
|
* :ref:`genindex`
|
||||||
.. _Stegano: https://github.com/cedricbonhomme/Stegano
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
|
.. _Python: http://python.org/
|
||||||
|
.. _Stéganô: https://bitbucket.org/cedricbonhomme/stegano/
|
||||||
.. _`Pillow`: https://pypi.python.org/pypi/Pillow
|
.. _`Pillow`: https://pypi.python.org/pypi/Pillow
|
||||||
.. _`piexif`: https://pypi.python.org/pypi/piexif
|
|
||||||
.. _steganography: http://en.wikipedia.org/wiki/Steganography
|
.. _steganography: http://en.wikipedia.org/wiki/Steganography
|
||||||
.. _steganalysis: http://en.wikipedia.org/wiki/Steganalysis
|
.. _steganalysis: http://en.wikipedia.org/wiki/Steganalysis
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ poetry install Stegano
|
|
||||||
|
|
||||||
You will be able to use Stegano in your Python programs
|
|
||||||
or as a command line tool.
|
|
||||||
|
|
||||||
If you want to retrieve the source code (with the unit tests):
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ git clone https://github.com/cedricbonhomme/Stegano
|
|
120
docs/module.rst
|
@ -1,120 +0,0 @@
|
||||||
Using Stegano as a Python module
|
|
||||||
================================
|
|
||||||
|
|
||||||
You can find more examples in the
|
|
||||||
`unit tests directory <https://github.com/cedricbonhomme/Stegano/tree/master/tests>`_.
|
|
||||||
|
|
||||||
LSB method
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
Python 3.11.0 (main, Oct 31 2022, 15:15:22) [GCC 12.2.0] on linux
|
|
||||||
Type "help", "copyright", "credits" or "license" for more information.
|
|
||||||
>>> from stegano import lsb
|
|
||||||
>>> secret = lsb.hide("./tests/sample-files/Lenna.png", "Hello world!")
|
|
||||||
>>> secret.save("./Lenna-secret.png")
|
|
||||||
>>> print(lsb.reveal("./Lenna-secret.png"))
|
|
||||||
Hello world!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
LSB method with sets
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
Sets are used in order to select the pixels where the message will be hidden.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
Python 3.11.0 (main, Oct 31 2022, 15:15:22) [GCC 12.2.0] on linux
|
|
||||||
Type "help", "copyright", "credits" or "license" for more information.
|
|
||||||
>>> from stegano import lsb
|
|
||||||
>>> from stegano.lsb import generators
|
|
||||||
|
|
||||||
# Hide a secret with the Sieve of Eratosthenes
|
|
||||||
>>> secret_message = "Hello World!"
|
|
||||||
>>> secret_image = lsb.hide("./tests/sample-files/Lenna.png", secret_message, generators.eratosthenes())
|
|
||||||
>>> secret_image.save("./image.png")
|
|
||||||
|
|
||||||
# Try to decode with another generator
|
|
||||||
>>> message = lsb.reveal("./image.png", generators.fibonacci())
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "/Users/flavien/.local/share/virtualenvs/Stegano-sY_cwr69/bin/stegano-lsb", line 6, in <module>
|
|
||||||
sys.exit(main())
|
|
||||||
File "/Users/flavien/Perso/dev/Stegano/bin/lsb.py", line 190, in main
|
|
||||||
img_encoded = lsb.hide(
|
|
||||||
File "/Users/flavien/Perso/dev/Stegano/stegano/lsb/lsb.py", line 63, in hide
|
|
||||||
hider.encode_pixel((col, row))
|
|
||||||
File "/Users/flavien/Perso/dev/Stegano/stegano/tools.py", line 165, in encode_pixel
|
|
||||||
r, g, b, *a = self.encoded_image.getpixel(coordinate)
|
|
||||||
File "/Users/flavien/.local/share/virtualenvs/Stegano-sY_cwr69/lib/python3.10/site-packages/PIL/Image.py", line 1481, in getpixel
|
|
||||||
return self.im.getpixel(xy)
|
|
||||||
IndexError: image index out of range
|
|
||||||
|
|
||||||
# Decode with Eratosthenes
|
|
||||||
>>> message = lsb.reveal("./image.png", generators.eratosthenes())
|
|
||||||
>>> message
|
|
||||||
'Hello World!'
|
|
||||||
|
|
||||||
>>> # Generators available
|
|
||||||
>>> import inspect
|
|
||||||
>>> all_generators = inspect.getmembers(generators, inspect.isfunction)
|
|
||||||
>>> for generator in all_generators:
|
|
||||||
... print(generator[0], generator[1].__doc__)
|
|
||||||
...
|
|
||||||
Dead_Man_Walking None
|
|
||||||
OEIS_A000217
|
|
||||||
http://oeis.org/A000217
|
|
||||||
Triangular numbers: a(n) = C(n+1,2) = n(n+1)/2 = 0+1+2+...+n.
|
|
||||||
|
|
||||||
ackermann
|
|
||||||
Ackermann number.
|
|
||||||
|
|
||||||
carmichael None
|
|
||||||
eratosthenes
|
|
||||||
Generate the prime numbers with the sieve of Eratosthenes.
|
|
||||||
|
|
||||||
eratosthenes_composite
|
|
||||||
Generate the composite numbers with the sieve of Eratosthenes.
|
|
||||||
|
|
||||||
fermat
|
|
||||||
Generate the n-th Fermat Number.
|
|
||||||
|
|
||||||
fibonacci
|
|
||||||
A generator for Fibonacci numbers, goes to next number in series on each call.
|
|
||||||
This generator start at 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, ...
|
|
||||||
See: http://oeis.org/A000045
|
|
||||||
|
|
||||||
identity
|
|
||||||
f(x) = x
|
|
||||||
|
|
||||||
log_gen
|
|
||||||
Logarithmic generator.
|
|
||||||
|
|
||||||
mersenne
|
|
||||||
Generate 2^n-1.
|
|
||||||
|
|
||||||
syracuse
|
|
||||||
Generate the sequence of Syracuse.
|
|
||||||
|
|
||||||
shi_tomashi Shi-Tomachi corner generator of the given points
|
|
||||||
https://docs.opencv.org/4.x/d4/d8c/tutorial_py_shi_tomasi.html
|
|
||||||
|
|
||||||
triangular_numbers Triangular numbers: a(n) = C(n+1,2) = n(n+1)/2 = 0+1+2+...+n.
|
|
||||||
http://oeis.org/A000217
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Description field of the image
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
For JPEG and TIFF images.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
Python 3.11.0 (main, Oct 31 2022, 15:15:22) [GCC 12.2.0] on linux
|
|
||||||
Type "help", "copyright", "credits" or "license" for more information.
|
|
||||||
>>> from stegano import exifHeader
|
|
||||||
>>> secret = exifHeader.hide("./tests/sample-files/20160505T130442.jpg",
|
|
||||||
"./image.jpg", secret_message="Hello world!")
|
|
||||||
>>> print(exifHeader.reveal("./image.jpg"))
|
|
|
@ -1,2 +0,0 @@
|
||||||
sphinx
|
|
||||||
sphinx_rtd_theme
|
|
|
@ -1,239 +0,0 @@
|
||||||
Using Stegano in command line
|
|
||||||
=============================
|
|
||||||
|
|
||||||
The command ``stegano-lsb``
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Hide and reveal a message with the LSB method.
|
|
||||||
|
|
||||||
Display help
|
|
||||||
------------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-lsb --help
|
|
||||||
usage: stegano-lsb [-h] {hide,reveal,list-generators} ...
|
|
||||||
|
|
||||||
positional arguments:
|
|
||||||
{hide,reveal,list-generators}
|
|
||||||
sub-command help
|
|
||||||
hide hide help
|
|
||||||
reveal reveal help
|
|
||||||
list-generators list-generators help
|
|
||||||
|
|
||||||
options:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-lsb hide --help
|
|
||||||
usage: stegano-lsb hide [-h] -i INPUT_IMAGE_FILE [-e {UTF-8,UTF-32LE}] [-g [GENERATOR_FUNCTION ...]] [-s SHIFT] (-m SECRET_MESSAGE | -f SECRET_FILE) -o OUTPUT_IMAGE_FILE
|
|
||||||
|
|
||||||
options:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
-i INPUT_IMAGE_FILE, --input INPUT_IMAGE_FILE
|
|
||||||
Input image file.
|
|
||||||
-e {UTF-8,UTF-32LE}, --encoding {UTF-8,UTF-32LE}
|
|
||||||
Specify the encoding of the message to hide. UTF-8 (default) or UTF-32LE.
|
|
||||||
-g [GENERATOR_FUNCTION ...], --generator [GENERATOR_FUNCTION ...]
|
|
||||||
Generator (with optional arguments)
|
|
||||||
-s SHIFT, --shift SHIFT
|
|
||||||
Shift for the generator
|
|
||||||
-m SECRET_MESSAGE Your secret message to hide (non binary).
|
|
||||||
-f SECRET_FILE Your secret to hide (Text or any binary file).
|
|
||||||
-o OUTPUT_IMAGE_FILE, --output OUTPUT_IMAGE_FILE
|
|
||||||
Output image containing the secret.
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-lsb reveal --help
|
|
||||||
usage: stegano-lsb reveal [-h] -i INPUT_IMAGE_FILE [-e {UTF-8,UTF-32LE}] [-g [GENERATOR_FUNCTION ...]] [-s SHIFT] [-o SECRET_BINARY]
|
|
||||||
|
|
||||||
options:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
-i INPUT_IMAGE_FILE, --input INPUT_IMAGE_FILE
|
|
||||||
Input image file.
|
|
||||||
-e {UTF-8,UTF-32LE}, --encoding {UTF-8,UTF-32LE}
|
|
||||||
Specify the encoding of the message to reveal. UTF-8 (default) or UTF-32LE.
|
|
||||||
-g [GENERATOR_FUNCTION ...], --generator [GENERATOR_FUNCTION ...]
|
|
||||||
Generator (with optional arguments)
|
|
||||||
-s SHIFT, --shift SHIFT
|
|
||||||
Shift for the generator
|
|
||||||
-o SECRET_BINARY Output for the binary secret (Text or any binary file).
|
|
||||||
|
|
||||||
|
|
||||||
Hide and reveal a text message
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m 'Hello World!' -o ./Lenna_enc.png
|
|
||||||
$ stegano-lsb reveal -i ./Lenna_enc.png
|
|
||||||
Hello World!
|
|
||||||
|
|
||||||
Specify an encoding
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m 'I love 🍕 and 🍫.' -e UTF-32LE -o ./Lenna_enc.png
|
|
||||||
$ stegano-lsb reveal -i ./Lenna_enc.png
|
|
||||||
I love 🍕 and 🍫.
|
|
||||||
|
|
||||||
The default encoding is UTF-8.
|
|
||||||
|
|
||||||
Hide and reveal a binary file
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wget http://www.gnu.org/music/free-software-song.ogg
|
|
||||||
$ stegano-lsb hide -i ./tests/sample-files/Montenach.png -f ./free-software-song.ogg -o ./Montenach_enc.png
|
|
||||||
$ rm free-software-song.ogg
|
|
||||||
$ stegano-lsb reveal -i ./Montenach_enc.png -o ./song.ogg
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sets are used in order to select the pixels where the message will be hidden.
|
|
||||||
|
|
||||||
Hide and reveal a text message with set
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# Hide the message with the Sieve of Eratosthenes
|
|
||||||
$ stegano-lsb hide -i ./tests/sample-files/Montenach.png --generator eratosthenes -m 'Joyeux Noël!' -o ./surprise.png
|
|
||||||
|
|
||||||
# Try to reveal with Mersenne numbers
|
|
||||||
$ stegano-lsb reveal --generator mersenne -i ./surprise.png
|
|
||||||
|
|
||||||
# Try to reveal with fermat numbers
|
|
||||||
$ stegano-lsb reveal --generator fermat -i ./surprise.png
|
|
||||||
|
|
||||||
# Try to reveal with carmichael numbers
|
|
||||||
$ stegano-lsb reveal --generator carmichael -i ./surprise.png
|
|
||||||
|
|
||||||
# Try to reveal with Sieve of Eratosthenes
|
|
||||||
$ stegano-lsb reveal --generator eratosthenes -i ./surprise.png
|
|
||||||
|
|
||||||
|
|
||||||
Sometimes it can be useful to skip the first values of a set. For example if you want
|
|
||||||
to hide several messages or because due to the selected generator
|
|
||||||
(Fibonacci starts with 0, 1, 1, etc.). Or maybe you just want to add more complexity.
|
|
||||||
In this case, simply use the optional arguments ``--shift`` or ``-s``:
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m 'Shifted secret message' -o ~/Lenna1.png --shift 7
|
|
||||||
$ stegano-lsb reveal -i ~/Lenna1.png --shift 7
|
|
||||||
Shifted secret message
|
|
||||||
|
|
||||||
|
|
||||||
List all available generators
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-lsb list-generators
|
|
||||||
Generator id:
|
|
||||||
ackermann
|
|
||||||
Desciption:
|
|
||||||
Ackermann number.
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
ackermann_naive
|
|
||||||
Desciption:
|
|
||||||
Ackermann number.
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
carmichael
|
|
||||||
Desciption:
|
|
||||||
Composite numbers n such that a^(n-1) == 1 (mod n) for every a coprime
|
|
||||||
to n.
|
|
||||||
https://oeis.org/A002997
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
composite
|
|
||||||
Desciption:
|
|
||||||
Generate the composite numbers using the sieve of Eratosthenes.
|
|
||||||
https://oeis.org/A002808
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
eratosthenes
|
|
||||||
Desciption:
|
|
||||||
Generate the prime numbers with the sieve of Eratosthenes.
|
|
||||||
https://oeis.org/A000040
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
fermat
|
|
||||||
Desciption:
|
|
||||||
Generate the n-th Fermat Number.
|
|
||||||
https://oeis.org/A000215
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
fibonacci
|
|
||||||
Desciption:
|
|
||||||
Generate the sequence of Fibonacci.
|
|
||||||
https://oeis.org/A000045
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
identity
|
|
||||||
Desciption:
|
|
||||||
f(x) = x
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
log_gen
|
|
||||||
Desciption:
|
|
||||||
Logarithmic generator.
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
mersenne
|
|
||||||
Desciption:
|
|
||||||
Generate 2^p - 1, where p is prime.
|
|
||||||
https://oeis.org/A001348
|
|
||||||
|
|
||||||
Generator id:
|
|
||||||
triangular_numbers
|
|
||||||
Desciption:
|
|
||||||
Triangular numbers: a(n) = C(n+1,2) = n(n+1)/2 = 0+1+2+...+n.
|
|
||||||
http://oeis.org/A000217
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The command ``stegano-red``
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Hide and reveal a text message with the red portion of a pixel.
|
|
||||||
|
|
||||||
Display help
|
|
||||||
------------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-red hide --help
|
|
||||||
usage: stegano-red hide [-h] [-i INPUT_IMAGE_FILE] [-m SECRET_MESSAGE]
|
|
||||||
[-o OUTPUT_IMAGE_FILE]
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
-i INPUT_IMAGE_FILE, --input INPUT_IMAGE_FILE
|
|
||||||
Image file
|
|
||||||
-m SECRET_MESSAGE Your secret message to hide (non binary).
|
|
||||||
-o OUTPUT_IMAGE_FILE, --output OUTPUT_IMAGE_FILE
|
|
||||||
Image file
|
|
||||||
|
|
||||||
Hide and reveal a text message
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ stegano-red hide -i ./tests/sample-files/Lenna.png -m 'Basic steganography technique.' -o ~/Lenna1.png
|
|
||||||
$ stegano-red reveal -i ~/Lenna1.png
|
|
||||||
Basic steganography technique.
|
|
|
@ -1,19 +0,0 @@
|
||||||
Steganalysis
|
|
||||||
============
|
|
||||||
|
|
||||||
Parity
|
|
||||||
------
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
# Hide the message with Sieve of Eratosthenes
|
|
||||||
stegano-lsb hide -i ./tests/sample-files/20160505T130442.jpg -o ./surprise.png --generator eratosthenes -m 'Very important message.'
|
|
||||||
|
|
||||||
# Steganalysis of the original photo
|
|
||||||
stegano-steganalysis-parity -i ./tests/sample-files/20160505T130442.jpg -o ./surprise_st_original.png
|
|
||||||
|
|
||||||
# Steganalysis of the secret photo
|
|
||||||
stegano-steganalysis-parity -i ./surprise.png -o ./surprise_st_secret.png
|
|
||||||
|
|
||||||
# Reveal with Sieve of Eratosthenes
|
|
||||||
stegano-lsb reveal -i ./surprise.png --generator eratosthenes
|
|
95
docs/tutorial.rst
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
Getting Stéganô
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ hg clone https://bitbucket.org/cedricbonhomme/stegano
|
||||||
|
$ cd stegano/
|
||||||
|
$ chmod u+x *.py # if you want to use Stéganô in command line
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ python setup.py install
|
||||||
|
|
||||||
|
Now you will be able to use Stéganô in your Python program.
|
||||||
|
|
||||||
|
Using Stéganô as a Python module
|
||||||
|
================================
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
Python 2.7 (r27:82500, Jul 5 2010, 10:14:47)
|
||||||
|
[GCC 4.3.2] on linux2
|
||||||
|
Type "help", "copyright", "credits" or "license" for more information.
|
||||||
|
>>> from stegano import slsb
|
||||||
|
>>> secret = slsb.hide("./pictures/Lenna.png", "Hello world!")
|
||||||
|
>>> secret.save("./Lenna-secret.png")
|
||||||
|
>>> slsb.reveal("./Lenna-secret.png")
|
||||||
|
Hello world!
|
||||||
|
|
||||||
|
Using Stéganô in command line for your scripts
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
Display help
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ./slsb.py --help
|
||||||
|
Usage: slsb.py [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version show program's version number and exit
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--hide Hides a message in an image.
|
||||||
|
--reveal Reveals the message hided in an image.
|
||||||
|
-i INPUT_IMAGE_FILE, --input=INPUT_IMAGE_FILE
|
||||||
|
Input image file.
|
||||||
|
-o OUTPUT_IMAGE_FILE, --output=OUTPUT_IMAGE_FILE
|
||||||
|
Output image containing the secret.
|
||||||
|
-m SECRET_MESSAGE, --secret-message=SECRET_MESSAGE
|
||||||
|
Your secret message to hide (non binary).
|
||||||
|
-f SECRET_FILE, --secret-file=SECRET_FILE
|
||||||
|
Your secret to hide (Text or any binary file).
|
||||||
|
-b SECRET_BINARY, --binary=SECRET_BINARY
|
||||||
|
Output for the binary secret (Text or any binary
|
||||||
|
file).
|
||||||
|
|
||||||
|
Hide and reveal a text message
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ./slsb.py --hide -i ./pictures/Lenna.png -o ./pictures/Lenna_enc.png -m HelloWorld!
|
||||||
|
$ ./slsb.py --reveal -i ./pictures/Lenna_enc.png
|
||||||
|
HelloWorld!
|
||||||
|
|
||||||
|
Hide and reveal a binary file
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ wget http://www.gnu.org/music/free-software-song.ogg
|
||||||
|
$ ./slsb.py --hide -i ./pictures/Montenach.png -o ./pictures/Montenach_enc.png -f ./free-software-song.ogg
|
||||||
|
$ rm free-software-song.ogg
|
||||||
|
$ ./slsb.py --reveal -i ./pictures/Montenach_enc.png -b ./song.ogg
|
||||||
|
|
||||||
|
Hide and reveal a message by using the description field of the image
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ./exif-header.py --hide -i ./Elisha-Cuthbert.jpg -o ./Elisha-Cuthbert_enc.jpg -f ./fileToHide.txt
|
||||||
|
$ ./exif-header.py --reveal -i ./Elisha-Cuthbert_enc.jpg
|
||||||
|
|
||||||
|
Steganalysis
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ./steganalysis-parity.py -i ./pictures./Lenna_enc.png -o ./pictures/Lenna_enc_st.png
|
||||||
|
|
||||||
|
|
4
examples/example-lsb.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from stegano import slsb
|
||||||
|
|
||||||
|
secret = slsb.hide("./pictures/Lenna.png", "Bonjour tout le monde")
|
||||||
|
secret.save("./Lenna-secret.png")
|
6
examples/example1.sh
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
wget http://www.gnu.org/music/free-software-song.ogg
|
||||||
|
slsb --hide -i ./pictures/Montenach.png -o ./pictures/Montenach_enc.png -f ./free-software-song.ogg
|
||||||
|
rm free-software-song.ogg
|
||||||
|
slsb --reveal -i ./pictures/Montenach_enc.png -b ./zik.ogg
|
26
examples/example2.sh
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test the LSB method with sets.
|
||||||
|
#
|
||||||
|
|
||||||
|
echo "We're going to test a little Stéganô..."
|
||||||
|
|
||||||
|
echo "Hide the message with the Sieve of Eratosthenes..."
|
||||||
|
slsb-set --hide -i ./pictures/Montenach.png -o ./surprise.png --generator eratosthenes -m 'Joyeux Noël!'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Try to reveal with Mersenne numbers..."
|
||||||
|
slsb-set --reveal --generator mersenne -i ./surprise.png
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Try to reveal with fermat numbers..."
|
||||||
|
slsb-set --reveal --generator fermat -i ./surprise.png
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Try to reveal with carmichael numbers..."
|
||||||
|
slsb-set --reveal --generator carmichael -i ./surprise.png
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Try to reveal with Sieve of Eratosthenes..."
|
||||||
|
slsb-set --reveal --generator eratosthenes -i ./surprise.png
|
22
examples/example3.sh
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Some tests of the LSB method which uses sets (slsb-set). Sets are used in order to select the pixels where the
|
||||||
|
# message will be hidden.
|
||||||
|
|
||||||
|
|
||||||
|
# Hide the message - LSB with a set defined by the identity function (f(x) = x).
|
||||||
|
slsb-set --hide -i examples/pictures/Montenach.png -o ~/enc-identity.png --generator identity -m 'I like steganography.'
|
||||||
|
|
||||||
|
# Hide the message - LSB only.
|
||||||
|
slsb --hide -i examples/pictures/Montenach.png -o ~/enc.png -m 'I like steganography.'
|
||||||
|
|
||||||
|
|
||||||
|
# Check if the two generated files are the same.
|
||||||
|
sha1sum ~/enc-identity.png ~/enc.png
|
||||||
|
|
||||||
|
|
||||||
|
# The output of slsb is given to slsb-set.
|
||||||
|
slsb-set --reveal -i ~/enc.png --generator identity
|
||||||
|
|
||||||
|
# The output of slsb-set is given to slsb.
|
||||||
|
slsb --reveal -i ~/enc-identity.png
|
21
examples/example4.sh
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test the LSB method with sets.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
echo "Hide the message with Sieve of Eratosthenes..."
|
||||||
|
slsb-set --hide -i ./pictures/Ginnifer-Goodwin.png -o ./surprise.png --generator eratosthenes -m 'Probably the most beautiful woman in the world.'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Steganalysis of the original photo..."
|
||||||
|
steganalysis-parity -i ./pictures/Ginnifer-Goodwin.png -o ./surprise_st_original.png
|
||||||
|
|
||||||
|
echo "Steganalysis of the secret photo..."
|
||||||
|
steganalysis-parity -i ./surprise.png -o ./surprise_st_secret.png
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Reveal with Sieve of Eratosthenes..."
|
||||||
|
echo "The secret is:"
|
||||||
|
slsb-set --reveal --generator eratosthenes -i ./surprise.png
|
BIN
examples/pictures/Elisha-Cuthbert.jpg
Normal file
After Width: | Height: | Size: 373 KiB |
BIN
examples/pictures/Ginnifer-Goodwin.png
Normal file
After Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 464 KiB After Width: | Height: | Size: 464 KiB |
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 3.8 MiB |
1173
poetry.lock
generated
|
@ -1,98 +0,0 @@
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core>=2.0"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "stegano"
|
|
||||||
version = "2.0.0"
|
|
||||||
description = "A pure Python Steganography module."
|
|
||||||
authors = [
|
|
||||||
{name = "Cédric Bonhomme", email= "cedric@cedricbonhomme.org"}
|
|
||||||
]
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
readme = "README.md"
|
|
||||||
keywords = ["Steganography", "Security", "Stegano"]
|
|
||||||
|
|
||||||
dynamic = ["classifiers"]
|
|
||||||
|
|
||||||
requires-python = ">=3.10,<4.0"
|
|
||||||
dependencies = [
|
|
||||||
"pillow (>=9.5,<12.0)",
|
|
||||||
"piexif (>=1.1.3)",
|
|
||||||
"crayons (>=0.4.0)",
|
|
||||||
"opencv-python (>=4.11.0.86)"
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.urls]
|
|
||||||
Homepage = "https://github.com/cedricbonhomme/Stegano"
|
|
||||||
Changelog = "https://github.com/cedricbonhomme/Stegano/blob/master/CHANGELOG.md"
|
|
||||||
Repository = "https://github.com/cedricbonhomme/Stegano"
|
|
||||||
Documentation = "https://stegano.readthedocs.io"
|
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
stegano-lsb = "stegano.console.lsb:main"
|
|
||||||
stegano-red = "stegano.console.red:main"
|
|
||||||
stegano-steganalysis-parity = "stegano.console.parity:main"
|
|
||||||
stegano-steganalysis-statistics = "stegano.console.statistics:main"
|
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry]
|
|
||||||
requires-poetry = ">=2.0"
|
|
||||||
classifiers = [
|
|
||||||
"Development Status :: 5 - Production/Stable",
|
|
||||||
"Environment :: Console",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"Intended Audience :: Science/Research",
|
|
||||||
"Topic :: Security",
|
|
||||||
"Operating System :: POSIX :: Linux",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
"Programming Language :: Python :: 3.12",
|
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"
|
|
||||||
]
|
|
||||||
include = [
|
|
||||||
"README.md",
|
|
||||||
"COPYING",
|
|
||||||
"CHANGELOG.md",
|
|
||||||
"docs/**/*",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
mypy = "^1.8.0"
|
|
||||||
flake8 = "^6.0.0"
|
|
||||||
nose2 = "^0.14.0"
|
|
||||||
Sphinx = "^6.2.1"
|
|
||||||
pre-commit = "^3.6.0"
|
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev]
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
python_version = "3.13"
|
|
||||||
check_untyped_defs = true
|
|
||||||
ignore_errors = false
|
|
||||||
ignore_missing_imports = true
|
|
||||||
strict_optional = true
|
|
||||||
no_implicit_optional = true
|
|
||||||
warn_unused_ignores = true
|
|
||||||
warn_redundant_casts = true
|
|
||||||
warn_unused_configs = true
|
|
||||||
warn_unreachable = true
|
|
||||||
|
|
||||||
show_error_context = true
|
|
||||||
pretty = true
|
|
||||||
|
|
||||||
exclude = "build|dist|docs"
|
|
||||||
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
|
|
||||||
|
|
||||||
[tool.flake8]
|
|
||||||
ignore = ["E203"]
|
|
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pillow
|
2
setup.cfg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[metadata]
|
||||||
|
description-file = README.md
|
49
setup.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
try:
|
||||||
|
from setuptools import setup
|
||||||
|
except ImportError:
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
'stegano',
|
||||||
|
'stegano.exif'
|
||||||
|
]
|
||||||
|
|
||||||
|
requires = ['pillow']
|
||||||
|
|
||||||
|
with open('README.md', 'r') as f:
|
||||||
|
readme = f.read()
|
||||||
|
with open('CHANGELOG.md', 'r') as f:
|
||||||
|
changelog = f.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='Stegano',
|
||||||
|
version='0.4.3',
|
||||||
|
author='Cédric Bonhomme',
|
||||||
|
author_email='cedric@cedricbonhomme.org',
|
||||||
|
packages=packages,
|
||||||
|
include_package_data=True,
|
||||||
|
scripts=['bin/slsb', 'bin/slsb-set', 'bin/steganalysis-parity'],
|
||||||
|
url='https://bitbucket.org/cedricbonhomme/stegano',
|
||||||
|
description='A Python Steganography module.',
|
||||||
|
long_description=readme + changelog,
|
||||||
|
platforms = ['Linux'],
|
||||||
|
license='GPLv3',
|
||||||
|
install_requires=requires,
|
||||||
|
zip_safe=False,
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Topic :: Utilities",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 2.7",
|
||||||
|
"Programming Language :: Python :: 3.4",
|
||||||
|
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"
|
||||||
|
]
|
||||||
|
)
|
|
@ -1,5 +1 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from . import exifHeader, lsb, red, steganalysis
|
|
||||||
|
|
||||||
__all__ = ["red", "exifHeader", "lsb", "steganalysis"]
|
|
||||||
|
|
108
stegano/basic.py
Executable file
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2013 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.1 $"
|
||||||
|
__date__ = "$Date: 2010/10/01 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
def hide(img, message):
|
||||||
|
"""
|
||||||
|
Hide a message (string) in an image.
|
||||||
|
|
||||||
|
Use the red portion of a pixel (r, g, b) tuple to
|
||||||
|
hide the message string characters as ASCII values.
|
||||||
|
The red value of the first pixel is used for length of string.
|
||||||
|
"""
|
||||||
|
length = len(message)
|
||||||
|
# Limit length of message to 255
|
||||||
|
if length > 255:
|
||||||
|
return False
|
||||||
|
# Use a copy of image to hide the text in
|
||||||
|
encoded = img.copy()
|
||||||
|
width, height = img.size
|
||||||
|
index = 0
|
||||||
|
for row in range(height):
|
||||||
|
for col in range(width):
|
||||||
|
(r, g, b) = img.getpixel((col, row))
|
||||||
|
# first value is length of message
|
||||||
|
if row == 0 and col == 0 and index < length:
|
||||||
|
asc = length
|
||||||
|
elif index <= length:
|
||||||
|
c = message[index -1]
|
||||||
|
asc = ord(c)
|
||||||
|
else:
|
||||||
|
asc = r
|
||||||
|
encoded.putpixel((col, row), (asc, g , b))
|
||||||
|
index += 1
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
def reveal(img):
|
||||||
|
"""
|
||||||
|
Find a message in an image.
|
||||||
|
|
||||||
|
Check the red portion of an pixel (r, g, b) tuple for
|
||||||
|
hidden message characters (ASCII values).
|
||||||
|
The red value of the first pixel is used for length of string.
|
||||||
|
"""
|
||||||
|
width, height = img.size
|
||||||
|
message = ""
|
||||||
|
index = 0
|
||||||
|
for row in range(height):
|
||||||
|
for col in range(width):
|
||||||
|
r, g, b = img.getpixel((col, row))
|
||||||
|
# First pixel r value is length of message
|
||||||
|
if row == 0 and col == 0:
|
||||||
|
length = r
|
||||||
|
elif index <= length:
|
||||||
|
message += chr(r)
|
||||||
|
index += 1
|
||||||
|
return message
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Point of entry in execution mode.
|
||||||
|
from optparse import OptionParser
|
||||||
|
usage = "usage: %prog hide|reveal [options]"
|
||||||
|
parser = OptionParser(usage)
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Image file.")
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Image file.")
|
||||||
|
parser.add_option("-s", "--secret", dest="secret",
|
||||||
|
help="Your secret (Message, Image, Music or any binary file).")
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Lenna.png',
|
||||||
|
output_image_file = './pictures/Lenna_enc.png',
|
||||||
|
secret = 'Hello World!')
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
if sys.argv[1] == "hide":
|
||||||
|
img = Image.open(options.input_image_file)
|
||||||
|
img_encoded = hide(img, options.secret)
|
||||||
|
img_encoded.save(options.output_image_file)
|
||||||
|
|
||||||
|
elif sys.argv[1] == "reveal":
|
||||||
|
img = Image.open(options.input_image_file)
|
||||||
|
print(reveal(img))
|
|
@ -1,227 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.7 $"
|
|
||||||
__date__ = "$Date: 2016/03/18 $"
|
|
||||||
__revision__ = "$Date: 2019/06/04 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
import crayons
|
|
||||||
|
|
||||||
try:
|
|
||||||
from stegano import lsb
|
|
||||||
from stegano.lsb import generators
|
|
||||||
except Exception:
|
|
||||||
print("Install stegano: pipx install Stegano")
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from stegano import tools
|
|
||||||
|
|
||||||
|
|
||||||
class ValidateGenerator(argparse.Action):
|
|
||||||
def __call__(self, parser, args, values, option_string=None):
|
|
||||||
valid_generators = [
|
|
||||||
generator[0]
|
|
||||||
for generator in inspect.getmembers(generators, inspect.isfunction)
|
|
||||||
]
|
|
||||||
# Verify that the generator is valid
|
|
||||||
generator = values[0]
|
|
||||||
if generator not in valid_generators:
|
|
||||||
raise ValueError("Unknown generator: %s" % generator)
|
|
||||||
# Set the generator_function arg of the parser
|
|
||||||
setattr(args, self.dest, values)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(prog="stegano-lsb")
|
|
||||||
subparsers = parser.add_subparsers(
|
|
||||||
help="sub-command help", dest="command", required=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Subparser: Hide
|
|
||||||
parser_hide = subparsers.add_parser("hide", help="hide help")
|
|
||||||
# Original image
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-i",
|
|
||||||
"--input",
|
|
||||||
dest="input_image_file",
|
|
||||||
required=True,
|
|
||||||
help="Input image file.",
|
|
||||||
)
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-e",
|
|
||||||
"--encoding",
|
|
||||||
dest="encoding",
|
|
||||||
choices=tools.ENCODINGS.keys(),
|
|
||||||
default="UTF-8",
|
|
||||||
help="Specify the encoding of the message to hide."
|
|
||||||
" UTF-8 (default) or UTF-32LE.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generator
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-g",
|
|
||||||
"--generator",
|
|
||||||
dest="generator_function",
|
|
||||||
action=ValidateGenerator,
|
|
||||||
nargs="*",
|
|
||||||
required=False,
|
|
||||||
default=None,
|
|
||||||
help="Generator (with optional arguments)",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Shift the message to hide
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-s", "--shift", dest="shift", default=0, help="Shift for the generator"
|
|
||||||
)
|
|
||||||
|
|
||||||
group_secret = parser_hide.add_mutually_exclusive_group(required=True)
|
|
||||||
# Non binary secret message to hide
|
|
||||||
group_secret.add_argument(
|
|
||||||
"-m", dest="secret_message", help="Your secret message to hide (non binary)."
|
|
||||||
)
|
|
||||||
# Binary secret message to hide
|
|
||||||
group_secret.add_argument(
|
|
||||||
"-f", dest="secret_file", help="Your secret to hide (Text or any binary file)."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Image containing the secret
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-o",
|
|
||||||
"--output",
|
|
||||||
dest="output_image_file",
|
|
||||||
required=True,
|
|
||||||
help="Output image containing the secret.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Subparser: Reveal
|
|
||||||
parser_reveal = subparsers.add_parser("reveal", help="reveal help")
|
|
||||||
parser_reveal.add_argument(
|
|
||||||
"-i",
|
|
||||||
"--input",
|
|
||||||
dest="input_image_file",
|
|
||||||
required=True,
|
|
||||||
help="Input image file.",
|
|
||||||
)
|
|
||||||
parser_reveal.add_argument(
|
|
||||||
"-e",
|
|
||||||
"--encoding",
|
|
||||||
dest="encoding",
|
|
||||||
choices=tools.ENCODINGS.keys(),
|
|
||||||
default="UTF-8",
|
|
||||||
help="Specify the encoding of the message to reveal."
|
|
||||||
" UTF-8 (default) or UTF-32LE.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generator
|
|
||||||
parser_reveal.add_argument(
|
|
||||||
"-g",
|
|
||||||
"--generator",
|
|
||||||
dest="generator_function",
|
|
||||||
action=ValidateGenerator,
|
|
||||||
nargs="*",
|
|
||||||
required=False,
|
|
||||||
help="Generator (with optional arguments)",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Shift the message to reveal
|
|
||||||
parser_reveal.add_argument(
|
|
||||||
"-s", "--shift", dest="shift", default=0, help="Shift for the generator"
|
|
||||||
)
|
|
||||||
parser_reveal.add_argument(
|
|
||||||
"-o",
|
|
||||||
dest="secret_binary",
|
|
||||||
help="Output for the binary secret (Text or any binary file).",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Subparser: List generators
|
|
||||||
subparsers.add_parser("list-generators", help="list-generators help")
|
|
||||||
|
|
||||||
arguments = parser.parse_args()
|
|
||||||
|
|
||||||
if arguments.command != "list-generators":
|
|
||||||
if not arguments.generator_function:
|
|
||||||
generator = None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
if arguments.generator_function[0] == "LFSR":
|
|
||||||
# Compute the size of the image for use by the LFSR generator if needed
|
|
||||||
tmp = tools.open_image(arguments.input_image_file)
|
|
||||||
size = tmp.width * tmp.height
|
|
||||||
tmp.close()
|
|
||||||
arguments.generator_function.append(size)
|
|
||||||
if len(arguments.generator_function) > 1:
|
|
||||||
generator = getattr(generators, arguments.generator_function[0])(
|
|
||||||
*[int(e) for e in arguments.generator_function[1:]]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
generator = getattr(generators, arguments.generator_function[0])()
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
print(f"Unknown generator: {arguments.generator_function}")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if arguments.command == "hide":
|
|
||||||
if arguments.secret_message is not None:
|
|
||||||
secret = arguments.secret_message
|
|
||||||
elif arguments.secret_file != "":
|
|
||||||
secret = tools.binary2base64(arguments.secret_file)
|
|
||||||
|
|
||||||
img_encoded = lsb.hide(
|
|
||||||
image=arguments.input_image_file,
|
|
||||||
message=secret,
|
|
||||||
generator=generator,
|
|
||||||
shift=int(arguments.shift),
|
|
||||||
encoding=arguments.encoding,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
img_encoded.save(arguments.output_image_file)
|
|
||||||
except Exception as e:
|
|
||||||
# If hide() returns an error (Too long message).
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
elif arguments.command == "reveal":
|
|
||||||
try:
|
|
||||||
secret = lsb.reveal(
|
|
||||||
encoded_image=arguments.input_image_file,
|
|
||||||
generator=generator,
|
|
||||||
shift=int(arguments.shift),
|
|
||||||
encoding=arguments.encoding,
|
|
||||||
)
|
|
||||||
except IndexError:
|
|
||||||
print("Impossible to detect message.")
|
|
||||||
exit(0)
|
|
||||||
if arguments.secret_binary is not None:
|
|
||||||
data = tools.base642binary(secret)
|
|
||||||
with open(arguments.secret_binary, "wb") as f:
|
|
||||||
f.write(data)
|
|
||||||
else:
|
|
||||||
print(secret)
|
|
||||||
|
|
||||||
elif arguments.command == "list-generators":
|
|
||||||
all_generators = inspect.getmembers(generators, inspect.isfunction)
|
|
||||||
for generator in all_generators:
|
|
||||||
print("Generator id:")
|
|
||||||
print(f" {crayons.green(generator[0], bold=True)}")
|
|
||||||
print("Desciption:")
|
|
||||||
print(f" {generator[1].__doc__}")
|
|
|
@ -1,55 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.7 $"
|
|
||||||
__date__ = "$Date: 2016/08/25 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
try:
|
|
||||||
from stegano.steganalysis import parity
|
|
||||||
except Exception:
|
|
||||||
print("Install Stegano: pipx install Stegano")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(prog="stegano-steganalysis-parity")
|
|
||||||
parser.add_argument(
|
|
||||||
"-i",
|
|
||||||
"--input",
|
|
||||||
dest="input_image_file",
|
|
||||||
required=True,
|
|
||||||
help="Input image file.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-o",
|
|
||||||
"--output",
|
|
||||||
dest="output_image_file",
|
|
||||||
required=True,
|
|
||||||
help="Output image file.",
|
|
||||||
)
|
|
||||||
arguments = parser.parse_args()
|
|
||||||
|
|
||||||
input_image_file = Image.open(arguments.input_image_file)
|
|
||||||
output_image = parity.steganalyse(input_image_file)
|
|
||||||
output_image.save(arguments.output_image_file)
|
|
|
@ -1,61 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.1 $"
|
|
||||||
__date__ = "$Date: 2017/02/06 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
try:
|
|
||||||
from stegano import red
|
|
||||||
except Exception:
|
|
||||||
print("Install stegano: sudo pip install Stegano")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(prog="stegano-red")
|
|
||||||
subparsers = parser.add_subparsers(help="sub-command help", dest="command")
|
|
||||||
|
|
||||||
parser_hide = subparsers.add_parser("hide", help="hide help")
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-i", "--input", dest="input_image_file", help="Image file"
|
|
||||||
)
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-m", dest="secret_message", help="Your secret message to hide (non binary)."
|
|
||||||
)
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-o", "--output", dest="output_image_file", help="Image file"
|
|
||||||
)
|
|
||||||
|
|
||||||
parser_reveal = subparsers.add_parser("reveal", help="reveal help")
|
|
||||||
parser_reveal.add_argument(
|
|
||||||
"-i", "--input", dest="input_image_file", help="Image file"
|
|
||||||
)
|
|
||||||
|
|
||||||
arguments = parser.parse_args()
|
|
||||||
|
|
||||||
if arguments.command == "hide":
|
|
||||||
secret = red.hide(arguments.input_image_file, arguments.secret_message)
|
|
||||||
secret.save(arguments.output_image_file)
|
|
||||||
|
|
||||||
elif arguments.command == "reveal":
|
|
||||||
secret = red.reveal(arguments.input_image_file)
|
|
||||||
print(secret)
|
|
|
@ -1,44 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cédric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.1 $"
|
|
||||||
__date__ = "$Date: 2016/08/26 $"
|
|
||||||
__revision__ = "$Date: 2016/08/26 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
try:
|
|
||||||
from stegano.steganalysis import statistics
|
|
||||||
except Exception:
|
|
||||||
print("Install Stegano: sudo pip install Stegano")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(prog="stegano-steganalysis-parity")
|
|
||||||
parser.add_argument("-i", "--input", dest="input_image_file", help="Image file")
|
|
||||||
parser.add_argument("-o", "--output", dest="output_image_file", help="Image file")
|
|
||||||
arguments = parser.parse_args()
|
|
||||||
|
|
||||||
input_image_file = Image.open(arguments.input_image_file)
|
|
||||||
output_image = statistics.steganalyse(input_image_file)
|
|
||||||
output_image.save(arguments.output_image_file)
|
|
|
@ -1,128 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.7 $"
|
|
||||||
__date__ = "$Date: 2016/03/18 $"
|
|
||||||
__revision__ = "$Date: 2019/06/04 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
try:
|
|
||||||
from stegano import wav
|
|
||||||
except Exception:
|
|
||||||
print("Install stegano: pipx install Stegano")
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from stegano import tools
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(prog="stegano-lsb")
|
|
||||||
subparsers = parser.add_subparsers(
|
|
||||||
help="sub-command help", dest="command", required=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Subparser: Hide
|
|
||||||
parser_hide = subparsers.add_parser("hide", help="hide help")
|
|
||||||
# Original audio
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-i",
|
|
||||||
"--input",
|
|
||||||
dest="input_audio_file",
|
|
||||||
required=True,
|
|
||||||
help="Input audio file.",
|
|
||||||
)
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-e",
|
|
||||||
"--encoding",
|
|
||||||
dest="encoding",
|
|
||||||
choices=tools.ENCODINGS.keys(),
|
|
||||||
default="UTF-8",
|
|
||||||
help="Specify the encoding of the message to hide."
|
|
||||||
" UTF-8 (default) or UTF-32LE.",
|
|
||||||
)
|
|
||||||
|
|
||||||
group_secret = parser_hide.add_mutually_exclusive_group(required=True)
|
|
||||||
# Non binary secret message to hide
|
|
||||||
group_secret.add_argument(
|
|
||||||
"-m", dest="secret_message", help="Your secret message to hide (non binary)."
|
|
||||||
)
|
|
||||||
# Binary secret message to hide
|
|
||||||
group_secret.add_argument(
|
|
||||||
"-f", dest="secret_file", help="Your secret to hide (Text or any binary file)."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Audio containing the secret
|
|
||||||
parser_hide.add_argument(
|
|
||||||
"-o",
|
|
||||||
"--output",
|
|
||||||
dest="output_audio_file",
|
|
||||||
required=True,
|
|
||||||
help="Output audio containing the secret.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Subparser: Reveal
|
|
||||||
parser_reveal = subparsers.add_parser("reveal", help="reveal help")
|
|
||||||
parser_reveal.add_argument(
|
|
||||||
"-i",
|
|
||||||
"--input",
|
|
||||||
dest="input_audio_file",
|
|
||||||
required=True,
|
|
||||||
help="Input audio file.",
|
|
||||||
)
|
|
||||||
parser_reveal.add_argument(
|
|
||||||
"-e",
|
|
||||||
"--encoding",
|
|
||||||
dest="encoding",
|
|
||||||
choices=tools.ENCODINGS.keys(),
|
|
||||||
default="UTF-8",
|
|
||||||
help="Specify the encoding of the message to reveal."
|
|
||||||
" UTF-8 (default) or UTF-32LE.",
|
|
||||||
)
|
|
||||||
|
|
||||||
arguments = parser.parse_args()
|
|
||||||
|
|
||||||
if arguments.command == "hide":
|
|
||||||
if arguments.secret_message is not None:
|
|
||||||
secret = arguments.secret_message
|
|
||||||
elif arguments.secret_file != "":
|
|
||||||
secret = tools.binary2base64(arguments.secret_file)
|
|
||||||
|
|
||||||
wav.hide(
|
|
||||||
input_file=arguments.input_audio_file,
|
|
||||||
message=secret,
|
|
||||||
encoding=arguments.encoding,
|
|
||||||
output_file=arguments.output_audio_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif arguments.command == "reveal":
|
|
||||||
try:
|
|
||||||
secret = wav.reveal(
|
|
||||||
input_file=arguments.input_audio_file, encoding=arguments.encoding
|
|
||||||
)
|
|
||||||
except IndexError:
|
|
||||||
print("Impossible to detect message.")
|
|
||||||
exit(0)
|
|
||||||
if arguments.secret_binary is not None:
|
|
||||||
data = tools.base642binary(secret)
|
|
||||||
with open(arguments.secret_binary, "wb") as f:
|
|
||||||
f.write(data)
|
|
||||||
else:
|
|
||||||
print(secret)
|
|
1
stegano/exif/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
197
stegano/exif/minimal_exif_reader.py
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
"""
|
||||||
|
This module offers one class, MinimalExifReader. Pass jpg filename
|
||||||
|
to the constructor. Will read minimal exif info from the file. Three
|
||||||
|
"public" functions available:
|
||||||
|
imageDescription()--returns Exif ImageDescription tag (0x010e) contents,
|
||||||
|
or '' if not found.
|
||||||
|
copyright()--returns Exif copyright tag (0x8298) contents, or '' if not
|
||||||
|
found.
|
||||||
|
dateTimeOriginal()--returns Exif DateTimeOriginal tag (0x9003) contents,
|
||||||
|
or '' if not found. If found, the trailing nul char
|
||||||
|
is stripped. This function also takes an optional
|
||||||
|
format string to apply time.strftime-style formatting
|
||||||
|
to the date time.
|
||||||
|
|
||||||
|
Brought to you by Megabyte Rodeo Software.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Written by Chris Stromberger, 10/2004. Public Domain.
|
||||||
|
# Much is owed to Thierry Bousch's exifdump.py:
|
||||||
|
# http://topo.math.u-psud.fr/~bousch/exifdump.py
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------
|
||||||
|
class ExifFormatException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------
|
||||||
|
class MinimalExifReader:
|
||||||
|
IMAGE_DESCRIPTION_TAG = 0x010e
|
||||||
|
COPYRIGHT_TAG = 0x8298
|
||||||
|
EXIF_SUBIFD_TAG = 0x8769
|
||||||
|
DATE_TIME_ORIGINAL_TAG = 0x9003
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def __init__(self, filename):
|
||||||
|
"""Pass in jpg exif file name to process. Will attempt to find tags
|
||||||
|
of interest."""
|
||||||
|
|
||||||
|
self.tagsToFind = {self.IMAGE_DESCRIPTION_TAG:'',
|
||||||
|
self.COPYRIGHT_TAG:'',
|
||||||
|
self.DATE_TIME_ORIGINAL_TAG:''}
|
||||||
|
|
||||||
|
# Read first bit of file to see if exif file.
|
||||||
|
f = open(filename, 'rb')
|
||||||
|
firstTwoBytes = f.read(2)
|
||||||
|
if firstTwoBytes != '\xff\xd8':
|
||||||
|
f.close()
|
||||||
|
raise ExifFormatException("Missing SOI marker")
|
||||||
|
|
||||||
|
appMarker = f.read(2)
|
||||||
|
# See if there's an APP0 section, which sometimes appears.
|
||||||
|
if appMarker == '\xff\xe0':
|
||||||
|
#print "Skipping app0"
|
||||||
|
# Yes, we have app0. Skip over it.
|
||||||
|
app0DataLength = ord(f.read(1)) * 256 + ord(f.read(1))
|
||||||
|
app0 = f.read(app0DataLength - 2)
|
||||||
|
appMarker = f.read(2)
|
||||||
|
|
||||||
|
if appMarker != '\xff\xe1':
|
||||||
|
raise ExifFormatException("Can't find APP1 marker")
|
||||||
|
|
||||||
|
exifHeader = f.read(8)
|
||||||
|
#import binascii
|
||||||
|
#print binascii.hexlify(exifHeader)
|
||||||
|
if (exifHeader[2:6] != 'Exif' or
|
||||||
|
exifHeader[6:8] != '\x00\x00'):
|
||||||
|
f.close()
|
||||||
|
raise ExifFormatException("Malformed APP1")
|
||||||
|
|
||||||
|
app1DataLength = ord(exifHeader[0]) * 256 + ord(exifHeader[1])
|
||||||
|
#print app1DataLength
|
||||||
|
|
||||||
|
# Read exif info starting at the beginning of the self.tiff section.
|
||||||
|
# This is 8 bytes into the app1 section, so subtract 8 from
|
||||||
|
# app1 length.
|
||||||
|
self.tiff = f.read(app1DataLength - 8)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
self.endian = self.tiff[0]
|
||||||
|
if self.endian not in ('I', 'M'):
|
||||||
|
raise ExifFormatException("Invalid endianess found: %s" % self.endian)
|
||||||
|
|
||||||
|
# Now navigate to the items of interest and get them.
|
||||||
|
ifdStart = self.getValueAtLocation(4, 4)
|
||||||
|
self.ifdSearch(ifdStart)
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def imageDescription(self):
|
||||||
|
"""Return image description tag contents or '' if not found."""
|
||||||
|
|
||||||
|
return self.tagsToFind[self.IMAGE_DESCRIPTION_TAG].strip('\x20\x00')
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def copyright(self):
|
||||||
|
"""Return copyright tag contents or '' if not found."""
|
||||||
|
|
||||||
|
return self.tagsToFind[self.COPYRIGHT_TAG].strip('\x20\x00')
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def dateTimeOriginal(self, formatString = None):
|
||||||
|
"""Pass in optional format string to get time.strftime style formatting,
|
||||||
|
else get default exif format for date time string (without trailing nul).
|
||||||
|
Returns '' if tag not found."""
|
||||||
|
|
||||||
|
# The datetime should end in nul, get rid of it.
|
||||||
|
if formatString is None or not self.tagsToFind[self.DATE_TIME_ORIGINAL_TAG]:
|
||||||
|
return self.tagsToFind[self.DATE_TIME_ORIGINAL_TAG].strip('\x20\x00')
|
||||||
|
else:
|
||||||
|
# This will only work if the datetime string is in the standard exif format (i.e., hasn't been altered).
|
||||||
|
try:
|
||||||
|
import time
|
||||||
|
return time.strftime(formatString, time.strptime(self.tagsToFind[self.DATE_TIME_ORIGINAL_TAG].strip('\x20\x00'), '%Y:%m:%d %H:%M:%S'))
|
||||||
|
except:
|
||||||
|
return self.tagsToFind[self.DATE_TIME_ORIGINAL_TAG].strip('\x20\x00')
|
||||||
|
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def ifdSearch(self, ifdStart):
|
||||||
|
numIfdEntries = self.getValueAtLocation(ifdStart, 2)
|
||||||
|
tagsStart = ifdStart + 2
|
||||||
|
for entryNum in range(numIfdEntries):
|
||||||
|
# For my purposes, all files will have either no tags, or
|
||||||
|
# only our tags of interest, so no need to waste time trying to
|
||||||
|
# break out of the loop early.
|
||||||
|
thisTagStart = tagsStart + 12 * entryNum
|
||||||
|
tagId = self.getValueAtLocation(thisTagStart, 2)
|
||||||
|
if tagId == self.EXIF_SUBIFD_TAG:
|
||||||
|
# This is a special tag that points to another ifd. Our
|
||||||
|
# date time original tag is in the sub ifd.
|
||||||
|
self.ifdSearch(self.getTagValue(thisTagStart))
|
||||||
|
elif tagId in self.tagsToFind:
|
||||||
|
assert(not self.tagsToFind[tagId])
|
||||||
|
self.tagsToFind[tagId] = self.getTagValue(thisTagStart)
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def getValueAtLocation(self, offset, length):
|
||||||
|
slice = self.tiff[offset:offset + length]
|
||||||
|
if self.endian == 'I':
|
||||||
|
val = self.s2n_intel(slice)
|
||||||
|
else:
|
||||||
|
val = self.s2n_motorola(slice)
|
||||||
|
return val
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def s2n_motorola(self, str):
|
||||||
|
x = 0
|
||||||
|
for c in str:
|
||||||
|
x = (x << 8) | ord(c)
|
||||||
|
return x
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def s2n_intel(self, str):
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
for c in str:
|
||||||
|
x = x | (ord(c) << y)
|
||||||
|
y = y + 8
|
||||||
|
return x
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def getTagValue(self, thisTagStart):
|
||||||
|
datatype = self.getValueAtLocation(thisTagStart + 2, 2)
|
||||||
|
numBytes = [ 1, 1, 2, 4, 8, 1, 1, 2, 4, 8 ] [datatype-1] * self.getValueAtLocation(thisTagStart + 4, 4)
|
||||||
|
if numBytes > 4:
|
||||||
|
offsetToValue = self.getValueAtLocation(thisTagStart + 8, 4)
|
||||||
|
return self.tiff[offsetToValue:offsetToValue + numBytes]
|
||||||
|
else:
|
||||||
|
if datatype == 2 or datatype == 1 or datatype == 7:
|
||||||
|
return self.tiff[thisTagStart + 8:thisTagStart + 8 + numBytes]
|
||||||
|
else:
|
||||||
|
return self.getValueAtLocation(thisTagStart + 8, numBytes)
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.tagsToFind)
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
print("Pass jpgs to process.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
for filename in sys.argv[1:]:
|
||||||
|
try:
|
||||||
|
f = MinimalExifReader(filename)
|
||||||
|
print(filename)
|
||||||
|
print("description: '%s'" % f.imageDescription())
|
||||||
|
print("copyright: '%s'" % f.copyright())
|
||||||
|
print("dateTimeOriginal: '%s'" % f.dateTimeOriginal())
|
||||||
|
print("dateTimeOriginal: '%s'" % f.dateTimeOriginal('%B %d, %Y %I:%M:%S %p'))
|
||||||
|
print()
|
||||||
|
except ExifFormatException as ex:
|
||||||
|
sys.stderr.write("Exif format error: %s\n" % ex)
|
||||||
|
except:
|
||||||
|
sys.stderr.write("Unable to process %s\n" % filename)
|
||||||
|
|
457
stegano/exif/minimal_exif_writer.py
Normal file
|
@ -0,0 +1,457 @@
|
||||||
|
"""
|
||||||
|
Offers one class, MinimalExifWriter, which takes a jpg filename
|
||||||
|
in the constructor. Allows you to: remove exif section, add
|
||||||
|
image description, add copyright. Typical usage:
|
||||||
|
|
||||||
|
f = MinimalExifWriter('xyz.jpg')
|
||||||
|
f.newImageDescription('This is a photo of something very interesting!')
|
||||||
|
f.newCopyright('Jose Blow, All Rights Reserved', addCopyrightYear = 1)
|
||||||
|
f.process()
|
||||||
|
|
||||||
|
Class methods:
|
||||||
|
newImageDescription(description)--will add Exif ImageDescription to file.
|
||||||
|
|
||||||
|
newCopyright(copyright, addSymbol = 0, addYear = 0)--will add Exif Copyright to file.
|
||||||
|
Will optionally prepend copyright symbol, or copyright symbol and current year.
|
||||||
|
|
||||||
|
removeExif()--will obliterate existing exif section.
|
||||||
|
|
||||||
|
process()--call after calling one or more of the above. Will remove existing exif
|
||||||
|
section, optionally saving some existing tags (see below), and insert a new exif
|
||||||
|
section with only three tags at most: description, copyright and date time original.
|
||||||
|
If removeExif() not called, existing description (or new description if newDescription()
|
||||||
|
called), existing copyright (or new copyright if newCopyright() called) and existing
|
||||||
|
"DateTimeOriginal" (date/time picture taken) tags will be rewritten to the new
|
||||||
|
minimal exif section.
|
||||||
|
|
||||||
|
Run at comand line with no args to see command line usage.
|
||||||
|
|
||||||
|
Does not work on unix due to differences in mmap. Not sure what's up there--
|
||||||
|
don't need it on unix!
|
||||||
|
|
||||||
|
Brought to you by Megabyte Rodeo Software.
|
||||||
|
http://www.fetidcascade.com/pyexif.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Written by Chris Stromberger, 10/2004. Public Domain.
|
||||||
|
# Last updated: 12/3/2004.
|
||||||
|
|
||||||
|
DUMP_TIFF = 0
|
||||||
|
VERBOSE = 0
|
||||||
|
if VERBOSE:
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
import mmap
|
||||||
|
import sys
|
||||||
|
from . import minimal_exif_reader
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------
|
||||||
|
class ExifFormatException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
class MinimalExifWriter:
|
||||||
|
SOI_MARKER = '\xff\xd8'
|
||||||
|
APP0_MARKER = '\xff\xe0'
|
||||||
|
APP1_MARKER = '\xff\xe1'
|
||||||
|
|
||||||
|
# Standard app0 segment that will work for all files. We hope.
|
||||||
|
# Based on http://www.funducode.com/freec/Fileformats/format3/format3b.htm.
|
||||||
|
APP0 = '\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00'
|
||||||
|
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.filename = filename
|
||||||
|
self.removeExifSection = 0
|
||||||
|
self.description = None
|
||||||
|
self.copyright = None
|
||||||
|
self.dateTimeOriginal = None
|
||||||
|
|
||||||
|
#---------------------------------------------
|
||||||
|
def newImageDescription(self, description):
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
#---------------------------------------------
|
||||||
|
def newCopyright(self, copyright, addSymbol = 0, addYear = 0):
|
||||||
|
if addYear:
|
||||||
|
import time
|
||||||
|
year = time.localtime()[0]
|
||||||
|
self.copyright = "\xa9 %s %s" % (year, copyright)
|
||||||
|
elif addSymbol:
|
||||||
|
self.copyright = "\xa9 %s" % copyright
|
||||||
|
else:
|
||||||
|
self.copyright = copyright
|
||||||
|
|
||||||
|
#---------------------------------------------
|
||||||
|
def removeExif(self):
|
||||||
|
self.removeExifSection = 1
|
||||||
|
|
||||||
|
#---------------------------------------------
|
||||||
|
def process(self):
|
||||||
|
if not self.removeExifSection:
|
||||||
|
self.getExistingExifInfo()
|
||||||
|
|
||||||
|
if VERBOSE:
|
||||||
|
print(self)
|
||||||
|
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
fd = os.open(self.filename, os.O_RDWR)
|
||||||
|
except:
|
||||||
|
sys.stderr.write('Unable to open "%s"\n' % filename)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.m = mmap.mmap(fd, 0)
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
# We only add app0 if all we're doing is removing the exif section.
|
||||||
|
justRemovingExif = self.description is None and self.copyright is None and self.removeExifSection
|
||||||
|
if VERBOSE: print('justRemovingExif=%s' % justRemovingExif)
|
||||||
|
self.removeExifInfo(addApp0 = justRemovingExif)
|
||||||
|
if justRemovingExif:
|
||||||
|
self.m.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get here means we are adding new description and/or copyright.
|
||||||
|
self.removeApp0()
|
||||||
|
|
||||||
|
totalTagsToBeAdded = len([_f for _f in (self.description, self.copyright, self.dateTimeOriginal) if _f])
|
||||||
|
assert(totalTagsToBeAdded > 0)
|
||||||
|
|
||||||
|
# Layout will be: firstifd|description|copyright|exififd|datetime.
|
||||||
|
# First ifd will have tags: desc|copyright|subifd tag.
|
||||||
|
ifd = [self.twoBytesHexIntel(totalTagsToBeAdded)]
|
||||||
|
ifdEnd = ['\x00\x00\x00\x00']
|
||||||
|
NUM_TAGS_LEN = 2
|
||||||
|
TAG_LEN = 12
|
||||||
|
NEXT_IFD_OFFSET_LEN = 4
|
||||||
|
TIFF_HEADER_LENGTH = 8
|
||||||
|
ifdLength = NUM_TAGS_LEN + TAG_LEN * totalTagsToBeAdded + NEXT_IFD_OFFSET_LEN
|
||||||
|
|
||||||
|
# Subifd only has one tag.
|
||||||
|
SUBIFD_LENGTH = NUM_TAGS_LEN + TAG_LEN + NEXT_IFD_OFFSET_LEN
|
||||||
|
|
||||||
|
offsetToEndOfData = ifdLength + TIFF_HEADER_LENGTH
|
||||||
|
|
||||||
|
if self.description:
|
||||||
|
ifd.append(self.descriptionTag(len(self.description), offsetToEndOfData))
|
||||||
|
ifdEnd.append(self.description)
|
||||||
|
offsetToEndOfData += len(self.description)
|
||||||
|
|
||||||
|
if self.copyright:
|
||||||
|
ifd.append(self.copyrightTag(len(self.copyright), offsetToEndOfData))
|
||||||
|
ifdEnd.append(self.copyright)
|
||||||
|
offsetToEndOfData += len(self.copyright)
|
||||||
|
|
||||||
|
if self.dateTimeOriginal:
|
||||||
|
ifd.append(self.subIfdTag(offsetToEndOfData))
|
||||||
|
offsetToEndOfData += SUBIFD_LENGTH
|
||||||
|
ifdEnd.append(self.buildSubIfd(len(self.dateTimeOriginal), offsetToEndOfData))
|
||||||
|
ifdEnd.append(self.dateTimeOriginal)
|
||||||
|
|
||||||
|
app1 = self.buildApp1Section(ifd, ifdEnd)
|
||||||
|
|
||||||
|
self.addApp1(app1)
|
||||||
|
|
||||||
|
self.m.close()
|
||||||
|
|
||||||
|
#---------------------------------------------
|
||||||
|
# Build exif subifd with one tag for datetime (0x9003).
|
||||||
|
# Type is ascii (0x0002).
|
||||||
|
def buildSubIfd(self, lenDateTime, offsetToEndOfData):
|
||||||
|
return '\x01\x00\x03\x90\x02\x00%s%s\x00\x00\x00\x00' % (self.fourBytesHexIntel(lenDateTime), self.fourBytesHexIntel(offsetToEndOfData))
|
||||||
|
|
||||||
|
#---------------------------------------------
|
||||||
|
def getExistingExifInfo(self):
|
||||||
|
# Save off the old stuff.
|
||||||
|
try:
|
||||||
|
f = minimal_exif_reader.MinimalExifReader(self.filename)
|
||||||
|
except:
|
||||||
|
# Assume no existing exif info in the file. We
|
||||||
|
# don't care.
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.description:
|
||||||
|
self.description = f.imageDescription()
|
||||||
|
|
||||||
|
if not self.copyright:
|
||||||
|
self.copyright = f.copyright()
|
||||||
|
|
||||||
|
self.dateTimeOriginal = f.dateTimeOriginal()
|
||||||
|
if self.dateTimeOriginal:
|
||||||
|
# Restore ending nul.
|
||||||
|
if self.dateTimeOriginal[-1] != '\x00':
|
||||||
|
self.dateTimeOriginal += '\x00'
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def removeExifInfo(self, addApp0 = 1):
|
||||||
|
"""Remove the app1 section of the jpg. This removes all exif info and the exif
|
||||||
|
thumbnail. addApp0 should be 1 to add a minimal app0 section right after soi
|
||||||
|
to make it a legitimate jpg, I think (various image programs can read the file
|
||||||
|
without app0, but I think the standard requires one).
|
||||||
|
"""
|
||||||
|
# Read first bit of file to see if exif file.
|
||||||
|
self.m.seek(0)
|
||||||
|
if self.m.read(2) != self.SOI_MARKER:
|
||||||
|
self.m.close()
|
||||||
|
raise ExifFormatException("Missing SOI marker")
|
||||||
|
|
||||||
|
app0DataLength = 0
|
||||||
|
appMarker = self.m.read(2)
|
||||||
|
# See if there's an APP0 section, which sometimes appears.
|
||||||
|
if appMarker == self.APP0_MARKER:
|
||||||
|
if VERBOSE: print('app0 found')
|
||||||
|
app0DataLength = ord(self.m.read(1)) * 256 + ord(self.m.read(1))
|
||||||
|
if VERBOSE: print('app0DataLength: %s' % app0DataLength)
|
||||||
|
# Back up 2 bytes to get the length bytes.
|
||||||
|
self.m.seek(-2, 1)
|
||||||
|
existingApp0 = self.m.read(app0DataLength)
|
||||||
|
appMarker = self.m.read(2)
|
||||||
|
|
||||||
|
if appMarker != self.APP1_MARKER:
|
||||||
|
# We don't care, we'll add our minimal app1 later.
|
||||||
|
return
|
||||||
|
|
||||||
|
exifHeader = self.m.read(8)
|
||||||
|
if VERBOSE: print('exif header: %s' % binascii.hexlify(exifHeader))
|
||||||
|
if (exifHeader[2:6] != 'Exif' or
|
||||||
|
exifHeader[6:8] != '\x00\x00'):
|
||||||
|
self.m.close()
|
||||||
|
raise ExifFormatException("Malformed APP1")
|
||||||
|
|
||||||
|
app1Length = ord(exifHeader[0]) * 256 + ord(exifHeader[1])
|
||||||
|
if VERBOSE: print('app1Length: %s' % app1Length)
|
||||||
|
|
||||||
|
originalFileSize = self.m.size()
|
||||||
|
|
||||||
|
# Shift stuff just past app1 to overwrite app1.
|
||||||
|
# Start at app1 length bytes in + other bytes not incl in app1 length.
|
||||||
|
src = app1Length + len(self.SOI_MARKER) + len(self.APP1_MARKER)
|
||||||
|
if app0DataLength:
|
||||||
|
src += app0DataLength + len(self.APP0_MARKER)
|
||||||
|
dest = len(self.SOI_MARKER)
|
||||||
|
if addApp0:
|
||||||
|
if app0DataLength != 0:
|
||||||
|
# We'll re-add the existing app0.
|
||||||
|
dest += app0DataLength + len(self.APP0_MARKER)
|
||||||
|
else:
|
||||||
|
# Add our generic app0.
|
||||||
|
dest += len(self.APP0)
|
||||||
|
count = originalFileSize - app1Length - len(self.SOI_MARKER) - len(self.APP1_MARKER)
|
||||||
|
if app0DataLength:
|
||||||
|
count -= app0DataLength + len(self.APP0_MARKER)
|
||||||
|
|
||||||
|
if VERBOSE: print('self.m.move(%s, %s, %s)' % (dest, src, count))
|
||||||
|
self.m.move(dest, src, count)
|
||||||
|
|
||||||
|
if addApp0:
|
||||||
|
if app0DataLength != 0:
|
||||||
|
self.m.resize(originalFileSize - app1Length - len(self.APP1_MARKER))
|
||||||
|
else:
|
||||||
|
self.m.seek(len(self.SOI_MARKER))
|
||||||
|
self.m.write(self.APP0)
|
||||||
|
self.m.resize(originalFileSize - app1Length - len(self.APP1_MARKER) + len(self.APP0))
|
||||||
|
else:
|
||||||
|
self.m.resize(originalFileSize - app1Length - len(self.APP1_MARKER))
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def removeApp0(self):
|
||||||
|
self.m.seek(0)
|
||||||
|
header = self.m.read(6)
|
||||||
|
if (header[0:2] != self.SOI_MARKER or
|
||||||
|
header[2:4] != self.APP0_MARKER):
|
||||||
|
if VERBOSE: print('no app0 found: %s' % binascii.hexlify(header))
|
||||||
|
return
|
||||||
|
|
||||||
|
originalFileSize = self.m.size()
|
||||||
|
|
||||||
|
app0Length = ord(header[4]) * 256 + ord(header[5])
|
||||||
|
if VERBOSE: print('app0Length:', app0Length)
|
||||||
|
|
||||||
|
# Shift stuff to overwrite app0.
|
||||||
|
# Start at app0 length bytes in + other bytes not incl in app0 length.
|
||||||
|
src = app0Length + len(self.SOI_MARKER) + len(self.APP0_MARKER)
|
||||||
|
dest = len(self.SOI_MARKER)
|
||||||
|
count = originalFileSize - app0Length - len(self.SOI_MARKER) - len(self.APP0_MARKER)
|
||||||
|
self.m.move(dest, src, count)
|
||||||
|
if VERBOSE: print('m.move(%s, %s, %s)' % (dest, src, count))
|
||||||
|
self.m.resize(originalFileSize - app0Length - len(self.APP0_MARKER))
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def addApp1(self, app1):
|
||||||
|
originalFileSize = self.m.size()
|
||||||
|
|
||||||
|
# Insert app1 section.
|
||||||
|
self.m.resize(originalFileSize + len(app1))
|
||||||
|
src = len(self.SOI_MARKER)
|
||||||
|
dest = len(app1) + len(self.SOI_MARKER)
|
||||||
|
count = originalFileSize - len(self.SOI_MARKER)
|
||||||
|
self.m.move(dest, src, count)
|
||||||
|
self.m.seek(len(self.SOI_MARKER))
|
||||||
|
self.m.write(app1)
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def fourBytesHexIntel(self, number):
|
||||||
|
return '%s%s%s%s' % (chr(number & 0x000000ff),
|
||||||
|
chr((number >> 8) & 0x000000ff),
|
||||||
|
chr((number >> 16) & 0x000000ff),
|
||||||
|
chr((number >> 24) & 0x000000ff))
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def twoBytesHexIntel(self, number):
|
||||||
|
return '%s%s' % (chr(number & 0x00ff),
|
||||||
|
chr((number >> 8) & 0x00ff))
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def descriptionTag(self, numChars, loc):
|
||||||
|
return self.asciiTag('\x0e\x01', numChars, loc)
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def copyrightTag(self, numChars, loc):
|
||||||
|
return self.asciiTag('\x98\x82', numChars, loc)
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def subIfdTag(self, loc):
|
||||||
|
return '\x69\x87\x04\x00\x01\x00\x00\x00%s' % self.fourBytesHexIntel(loc)
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def asciiTag(self, tag, numChars, loc):
|
||||||
|
"""Create ascii tag. Assumes description > 4 chars long."""
|
||||||
|
|
||||||
|
return '%s\x02\x00%s%s' % (tag, self.fourBytesHexIntel(numChars), self.fourBytesHexIntel(loc))
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def buildApp1Section(self, ifdPieces, ifdEndPieces):
|
||||||
|
"""Create the APP1 section of an exif jpg. Consists of exif header plus
|
||||||
|
tiff header + ifd and associated data."""
|
||||||
|
|
||||||
|
# Intel byte order, offset to first ifd will be 8.
|
||||||
|
tiff = 'II\x2a\x00\x08\x00\x00\x00%s%s' % (''.join(ifdPieces), ''.join(ifdEndPieces))
|
||||||
|
if DUMP_TIFF:
|
||||||
|
f = open('tiff.dump', 'wb')
|
||||||
|
f.write(tiff)
|
||||||
|
f.close()
|
||||||
|
app1Length = len(tiff) + 8
|
||||||
|
return '\xff\xe1%s%sExif\x00\x00%s' % (chr((app1Length >> 8) & 0x00ff), chr(app1Length & 0x00ff), tiff)
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def __str__(self):
|
||||||
|
return """filename: %(filename)s
|
||||||
|
removeExifSection: %(removeExifSection)s
|
||||||
|
description: %(description)s
|
||||||
|
copyright: %(copyright)s
|
||||||
|
dateTimeOriginal: %(dateTimeOriginal)s
|
||||||
|
""" % self.__dict__
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def usage(error = None):
|
||||||
|
"""Print command line usage and exit"""
|
||||||
|
|
||||||
|
if error:
|
||||||
|
print(error)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("""This program will remove exif info from an exif jpg, and can optionally
|
||||||
|
add the ImageDescription exif tag and/or the Copyright tag. But it will always remove
|
||||||
|
some or all existing exif info (depending on options--see below)!
|
||||||
|
So don't run this on your original images without a backup.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h: shows this message.
|
||||||
|
-f <file>: jpg to process (required).
|
||||||
|
-x: remove exif info (including thumbnail).
|
||||||
|
-d <description or file>: remove exif info (including thumbnail) and then add exif
|
||||||
|
ImageDescription. Will save the existing copyright tag if present,
|
||||||
|
as well as the date time original tag (date & time photo taken),
|
||||||
|
unless -x also passed (-x always means remove all exif info).
|
||||||
|
It will attempt to open whatever is passed on the
|
||||||
|
command line as a file; if successful, the contents of the file
|
||||||
|
are added as the description, else the literal text on the
|
||||||
|
command line is used as the description.
|
||||||
|
-c <copyright or file>: remove exif info (including thumbnail) and then add exif
|
||||||
|
Copyright tag. Will save the existing image description tag if present,
|
||||||
|
as well as the date time original tag (date & time photo taken),
|
||||||
|
unless -x also passed (-x always means remove all exif info).
|
||||||
|
It will attempt to open whatever is passed on the command line as a file;
|
||||||
|
if successful, the contents of the file are added as the copyright,
|
||||||
|
else the literal text on the command line is used as the copyright.
|
||||||
|
-s: prepend copyright symbol to copyright.
|
||||||
|
-y: prepend copyright symbol and current year to copyright.
|
||||||
|
|
||||||
|
The image description and copyright must be > 4 characters long.
|
||||||
|
|
||||||
|
This software courtesy of Megabyte Rodeo Software.""")
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
def parseArgs(args_):
|
||||||
|
import getopt
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(args_, "yshxd:f:c:")
|
||||||
|
except getopt.GetoptError:
|
||||||
|
usage()
|
||||||
|
|
||||||
|
filename = None
|
||||||
|
description = ''
|
||||||
|
copyright = ''
|
||||||
|
addCopyrightSymbol = 0
|
||||||
|
addCopyrightYear = 0
|
||||||
|
removeExif = 0
|
||||||
|
|
||||||
|
for o, a in opts:
|
||||||
|
if o == "-h":
|
||||||
|
usage()
|
||||||
|
if o == "-f":
|
||||||
|
filename = a
|
||||||
|
if o == "-d":
|
||||||
|
try:
|
||||||
|
f = open(a)
|
||||||
|
description = f.read()
|
||||||
|
f.close()
|
||||||
|
except:
|
||||||
|
description = a
|
||||||
|
if o == "-c":
|
||||||
|
try:
|
||||||
|
f = open(a)
|
||||||
|
copyright = f.read()
|
||||||
|
f.close()
|
||||||
|
except:
|
||||||
|
copyright = a
|
||||||
|
if o == '-x':
|
||||||
|
removeExif = 1
|
||||||
|
if o == '-s':
|
||||||
|
addCopyrightSymbol = 1
|
||||||
|
if o == '-y':
|
||||||
|
addCopyrightYear = 1
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
usage('Missing jpg filename')
|
||||||
|
if description and (len(description) <= 4 or len(description) > 60000):
|
||||||
|
usage('Description too short or too long')
|
||||||
|
if copyright and (len(copyright) <= 4 or len(copyright) > 60000):
|
||||||
|
usage('Copyright too short or too long')
|
||||||
|
if not description and not copyright and not removeExif:
|
||||||
|
usage('Nothing to do!')
|
||||||
|
|
||||||
|
return filename, description, copyright, removeExif, addCopyrightSymbol, addCopyrightYear
|
||||||
|
|
||||||
|
#---------------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
filename, description, copyright, removeExif, addCopyrightSymbol, addCopyrightYear = parseArgs(sys.argv[1:])
|
||||||
|
f = MinimalExifWriter(filename)
|
||||||
|
if description:
|
||||||
|
f.newImageDescription(description)
|
||||||
|
if copyright:
|
||||||
|
f.newCopyright(copyright, addCopyrightSymbol, addCopyrightYear)
|
||||||
|
if removeExif:
|
||||||
|
f.removeExif()
|
||||||
|
|
||||||
|
f.process()
|
||||||
|
except ExifFormatException as ex:
|
||||||
|
sys.stderr.write("Exif format error: %s\n" % ex)
|
||||||
|
except SystemExit:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
sys.stderr.write("Unable to process %s\n" % filename)
|
||||||
|
raise
|
118
stegano/exifHeader.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2013 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.1 $"
|
||||||
|
__date__ = "$Date: 2010/03/24 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
# Thanks to: http://www.julesberman.info/spec2img.htm
|
||||||
|
|
||||||
|
def hide(img, img_enc, copyright="http://bitbucket.org/cedricbonhomme/stegano", \
|
||||||
|
secret_message = None, secret_file = None):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
import datetime
|
||||||
|
from zlib import compress
|
||||||
|
from zlib import decompress
|
||||||
|
from base64 import b64encode
|
||||||
|
from .exif.minimal_exif_writer import MinimalExifWriter
|
||||||
|
|
||||||
|
if secret_file != None:
|
||||||
|
with open(secret_file, "r") as f:
|
||||||
|
secret_file_content = f.read()
|
||||||
|
text = "\nImage annotation date: "
|
||||||
|
text = text + str(datetime.date.today())
|
||||||
|
text = text + "\nImage description:\n"
|
||||||
|
if secret_file != None:
|
||||||
|
text = compress(b64encode(text + secret_file_content))
|
||||||
|
else:
|
||||||
|
text = compress(b64encode(text + secret_message))
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.copy(img, img_enc)
|
||||||
|
except Exception as e:
|
||||||
|
print(("Impossible to copy image:", e))
|
||||||
|
return
|
||||||
|
|
||||||
|
f = MinimalExifWriter(img_enc)
|
||||||
|
f.removeExif()
|
||||||
|
f.newImageDescription(text)
|
||||||
|
f.newCopyright(copyright, addYear = 1)
|
||||||
|
f.process()
|
||||||
|
|
||||||
|
|
||||||
|
def reveal(img):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
from base64 import b64decode
|
||||||
|
from zlib import decompress
|
||||||
|
from .exif.minimal_exif_reader import MinimalExifReader
|
||||||
|
try:
|
||||||
|
g = MinimalExifReader(img)
|
||||||
|
except:
|
||||||
|
print("Impossible to read description.")
|
||||||
|
return
|
||||||
|
print((b64decode(decompress(g.imageDescription()))))
|
||||||
|
print(("\nCopyright " + g.copyright()))
|
||||||
|
#print g.dateTimeOriginal()s
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Point of entry in execution mode.
|
||||||
|
from optparse import OptionParser
|
||||||
|
parser = OptionParser(version=__version__)
|
||||||
|
parser.add_option('--hide', action='store_true', default=False,
|
||||||
|
help="Hides a message in an image.")
|
||||||
|
parser.add_option('--reveal', action='store_true', default=False,
|
||||||
|
help="Reveals the message hided in an image.")
|
||||||
|
# Original image
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Input image file.")
|
||||||
|
# Image containing the secret
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Output image containing the secret.")
|
||||||
|
|
||||||
|
# Secret raw message to hide
|
||||||
|
parser.add_option("-m", "--secret-message", dest="secret_message",
|
||||||
|
help="Your raw secret message to hide.")
|
||||||
|
|
||||||
|
# Secret text file to hide.
|
||||||
|
parser.add_option("-f", "--secret-file", dest="secret_file",
|
||||||
|
help="Your secret textt file to hide.")
|
||||||
|
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Elisha-Cuthbert.jpg',
|
||||||
|
output_image_file = './pictures/Elisha-Cuthbert_enc.jpg',
|
||||||
|
secret_message = '', secret_file = '')
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
if options.hide:
|
||||||
|
if options.secret_message != "" and options.secret_file == "":
|
||||||
|
hide(img=options.input_image_file, img_enc=options.output_image_file, \
|
||||||
|
secret_message=options.secret_message)
|
||||||
|
elif options.secret_message == "" and options.secret_file != "":
|
||||||
|
hide(img=options.input_image_file, img_enc=options.output_image_file, \
|
||||||
|
secret_file=options.secret_file)
|
||||||
|
|
||||||
|
elif options.reveal:
|
||||||
|
reveal(img=options.input_image_file)
|
|
@ -1,5 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from .exifHeader import hide, reveal
|
|
||||||
|
|
||||||
__all__ = ["hide", "reveal"]
|
|
|
@ -1,161 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.2.2 $"
|
|
||||||
__date__ = "$Date: 2016/05/26 $"
|
|
||||||
__revision__ = "$Date: 2017/01/18 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import piexif
|
|
||||||
|
|
||||||
from stegano import tools
|
|
||||||
|
|
||||||
|
|
||||||
def hide(
|
|
||||||
input_image_file,
|
|
||||||
img_enc,
|
|
||||||
secret_message=None,
|
|
||||||
secret_file=None,
|
|
||||||
img_format=None,
|
|
||||||
):
|
|
||||||
"""Hide a message (string) in an image."""
|
|
||||||
from base64 import b64encode
|
|
||||||
from zlib import compress
|
|
||||||
|
|
||||||
if secret_file is not None:
|
|
||||||
with open(secret_file, "rb") as f:
|
|
||||||
secret_message = f.read()
|
|
||||||
|
|
||||||
try:
|
|
||||||
text = compress(b64encode(bytes(secret_message, "utf-8")))
|
|
||||||
except Exception:
|
|
||||||
text = compress(b64encode(secret_message))
|
|
||||||
|
|
||||||
img = tools.open_image(input_image_file)
|
|
||||||
|
|
||||||
if img_format is None:
|
|
||||||
img_format = img.format
|
|
||||||
|
|
||||||
if "exif" in img.info:
|
|
||||||
exif_dict = piexif.load(img.info["exif"])
|
|
||||||
else:
|
|
||||||
exif_dict = {}
|
|
||||||
exif_dict["0th"] = {}
|
|
||||||
exif_dict["0th"][piexif.ImageIFD.ImageDescription] = text
|
|
||||||
exif_bytes = piexif.dump(exif_dict)
|
|
||||||
img.save(img_enc, format=img_format, exif=exif_bytes)
|
|
||||||
img.close()
|
|
||||||
return img
|
|
||||||
|
|
||||||
|
|
||||||
def reveal(input_image_file):
|
|
||||||
"""Find a message in an image."""
|
|
||||||
from base64 import b64decode
|
|
||||||
from zlib import decompress
|
|
||||||
|
|
||||||
img = tools.open_image(input_image_file)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if img.format in ["JPEG", "TIFF"]:
|
|
||||||
if "exif" in img.info:
|
|
||||||
exif_dict = piexif.load(img.info.get("exif", b""))
|
|
||||||
description_key = piexif.ImageIFD.ImageDescription
|
|
||||||
encoded_message = exif_dict["0th"][description_key]
|
|
||||||
else:
|
|
||||||
encoded_message = b""
|
|
||||||
else:
|
|
||||||
raise ValueError("Given file is neither JPEG nor TIFF.")
|
|
||||||
finally:
|
|
||||||
img.close()
|
|
||||||
|
|
||||||
return b64decode(decompress(encoded_message))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Point of entry in execution mode.
|
|
||||||
# TODO: improve the management of arguments
|
|
||||||
from optparse import OptionParser
|
|
||||||
|
|
||||||
parser = OptionParser(version=__version__)
|
|
||||||
parser.add_option(
|
|
||||||
"--hide",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Hides a message in an image.",
|
|
||||||
)
|
|
||||||
parser.add_option(
|
|
||||||
"--reveal",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Reveals the message hided in an image.",
|
|
||||||
)
|
|
||||||
# Original image
|
|
||||||
parser.add_option(
|
|
||||||
"-i", "--input", dest="input_image_file", help="Input image file."
|
|
||||||
)
|
|
||||||
# Image containing the secret
|
|
||||||
parser.add_option(
|
|
||||||
"-o",
|
|
||||||
"--output",
|
|
||||||
dest="output_image_file",
|
|
||||||
help="Output image containing the secret.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Secret raw message to hide
|
|
||||||
parser.add_option(
|
|
||||||
"-m",
|
|
||||||
"--secret-message",
|
|
||||||
dest="secret_message",
|
|
||||||
help="Your raw secret message to hide.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Secret text file to hide.
|
|
||||||
parser.add_option(
|
|
||||||
"-f",
|
|
||||||
"--secret-file",
|
|
||||||
dest="secret_file",
|
|
||||||
help="Your secret text file to hide.",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.set_defaults(
|
|
||||||
input_image_file="./pictures/Elisha-Cuthbert.jpg",
|
|
||||||
output_image_file="./pictures/Elisha-Cuthbert_enc.jpg",
|
|
||||||
secret_message="",
|
|
||||||
secret_file="",
|
|
||||||
)
|
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
|
||||||
|
|
||||||
if options.hide:
|
|
||||||
if options.secret_message != "" and options.secret_file == "":
|
|
||||||
hide(
|
|
||||||
input_image_file=options.input_image_file,
|
|
||||||
img_enc=options.output_image_file,
|
|
||||||
secret_message=options.secret_message,
|
|
||||||
)
|
|
||||||
elif options.secret_message == "" and options.secret_file != "":
|
|
||||||
hide(
|
|
||||||
input_image_file=options.input_image_file,
|
|
||||||
img_enc=options.output_image_file,
|
|
||||||
secret_file=options.secret_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif options.reveal:
|
|
||||||
reveal(input_image_file=options.input_image_file)
|
|
155
stegano/generators.py
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2013 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.2 $"
|
||||||
|
__date__ = "$Date: 2011/12/28 $"
|
||||||
|
__revision__ = "$Date: 2012/12/14 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
import math
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
def identity():
|
||||||
|
"""
|
||||||
|
f(x) = x
|
||||||
|
"""
|
||||||
|
n = 0
|
||||||
|
while True:
|
||||||
|
yield n
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
def Dead_Man_Walking():
|
||||||
|
n = 0
|
||||||
|
while True:
|
||||||
|
yield n + 7
|
||||||
|
n += 2
|
||||||
|
|
||||||
|
def OEIS_A000217():
|
||||||
|
"""
|
||||||
|
http://oeis.org/A000217
|
||||||
|
Triangular numbers: a(n) = C(n+1,2) = n(n+1)/2 = 0+1+2+...+n.
|
||||||
|
"""
|
||||||
|
n = 0
|
||||||
|
while True:
|
||||||
|
yield (n*(n+1))//2
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
def fermat():
|
||||||
|
"""
|
||||||
|
Generate the n-th Fermat Number.
|
||||||
|
"""
|
||||||
|
y = 5
|
||||||
|
while True:
|
||||||
|
yield y
|
||||||
|
y = pow(y-1,2)+1
|
||||||
|
|
||||||
|
def mersenne():
|
||||||
|
"""
|
||||||
|
Generate 2^n-1.
|
||||||
|
"""
|
||||||
|
y = 1
|
||||||
|
while True:
|
||||||
|
yield y
|
||||||
|
y = 2*y + 1
|
||||||
|
|
||||||
|
def eratosthenes():
|
||||||
|
"""
|
||||||
|
Generate the prime numbers with the sieve of Eratosthenes.
|
||||||
|
"""
|
||||||
|
d = {}
|
||||||
|
for i in itertools.count(2):
|
||||||
|
if i in d:
|
||||||
|
for j in d[i]:
|
||||||
|
d[i + j] = d.get(i + j, []) + [j]
|
||||||
|
del d[i]
|
||||||
|
else:
|
||||||
|
d[i * i] = [i]
|
||||||
|
yield i
|
||||||
|
|
||||||
|
def eratosthenes_composite():
|
||||||
|
"""
|
||||||
|
Generate the composite numbers with the sieve of Eratosthenes.
|
||||||
|
"""
|
||||||
|
p1 = 3
|
||||||
|
for p2 in eratosthenes():
|
||||||
|
for n in range(p1 + 1, p2):
|
||||||
|
yield n
|
||||||
|
p1 = p2
|
||||||
|
|
||||||
|
def carmichael():
|
||||||
|
for m in eratosthenes_composite():
|
||||||
|
for a in range(2, m):
|
||||||
|
if pow(a,m,m) != a:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
yield m
|
||||||
|
|
||||||
|
def ackermann(m, n):
|
||||||
|
"""
|
||||||
|
Ackermann number.
|
||||||
|
"""
|
||||||
|
if m == 0:
|
||||||
|
return n + 1
|
||||||
|
elif n == 0:
|
||||||
|
return ackermann(m - 1, 1)
|
||||||
|
else:
|
||||||
|
return ackermann(m - 1, ackermann(m, n - 1))
|
||||||
|
|
||||||
|
def fibonacci():
|
||||||
|
"""
|
||||||
|
A generator for Fibonacci numbers, goes to next number in series on each call.
|
||||||
|
This generator start at 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, ...
|
||||||
|
See: http://oeis.org/A000045
|
||||||
|
"""
|
||||||
|
a, b = 1, 2
|
||||||
|
while True:
|
||||||
|
yield a
|
||||||
|
a, b = b, a + b
|
||||||
|
|
||||||
|
def syracuse(l=15):
|
||||||
|
"""
|
||||||
|
Generate the sequence of Syracuse.
|
||||||
|
"""
|
||||||
|
y = l
|
||||||
|
while True:
|
||||||
|
yield y
|
||||||
|
q,r = divmod(y,2)
|
||||||
|
if r == 0:
|
||||||
|
y = q
|
||||||
|
else:
|
||||||
|
y = 3*y + 1
|
||||||
|
|
||||||
|
def log_gen():
|
||||||
|
"""
|
||||||
|
Logarithmic generator.
|
||||||
|
"""
|
||||||
|
y = 1
|
||||||
|
while True:
|
||||||
|
adder = max(1, math.pow(10, int(math.log10(y))))
|
||||||
|
yield int(y)
|
||||||
|
y = y + adder
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Point of entry in execution mode.
|
||||||
|
f = fibonacci()
|
||||||
|
for x in range(13):
|
||||||
|
print(next(f), end=' ') # 0 1 1 2 3 5 8 13 21 34 55 89 144
|
|
@ -1,5 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from .lsb import hide, reveal
|
|
||||||
|
|
||||||
__all__ = ["hide", "reveal"]
|
|
|
@ -1,254 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.3 $"
|
|
||||||
__date__ = "$Date: 2011/12/28 $"
|
|
||||||
__revision__ = "$Date: 2021/11/29 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
import math
|
|
||||||
from typing import Any, Dict, Iterator, List
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
def identity() -> Iterator[int]:
|
|
||||||
"""f(x) = x"""
|
|
||||||
n = 0
|
|
||||||
while True:
|
|
||||||
yield n
|
|
||||||
n += 1
|
|
||||||
|
|
||||||
|
|
||||||
def triangular_numbers() -> Iterator[int]:
|
|
||||||
"""Triangular numbers: a(n) = C(n+1,2) = n(n+1)/2 = 0+1+2+...+n.
|
|
||||||
http://oeis.org/A000217
|
|
||||||
"""
|
|
||||||
n = 0
|
|
||||||
while True:
|
|
||||||
yield (n * (n + 1)) // 2
|
|
||||||
n += 1
|
|
||||||
|
|
||||||
|
|
||||||
def fermat() -> Iterator[int]:
|
|
||||||
"""Generate the n-th Fermat Number.
|
|
||||||
https://oeis.org/A000215
|
|
||||||
"""
|
|
||||||
y = 3
|
|
||||||
while True:
|
|
||||||
yield y
|
|
||||||
y = pow(y - 1, 2) + 1
|
|
||||||
|
|
||||||
|
|
||||||
def mersenne() -> Iterator[int]:
|
|
||||||
"""Generate 2^p - 1, where p is prime.
|
|
||||||
https://oeis.org/A001348
|
|
||||||
"""
|
|
||||||
prime_numbers = eratosthenes()
|
|
||||||
while True:
|
|
||||||
yield 2 ** next(prime_numbers) - 1
|
|
||||||
|
|
||||||
|
|
||||||
def eratosthenes() -> Iterator[int]:
|
|
||||||
"""Generate the prime numbers with the sieve of Eratosthenes.
|
|
||||||
https://oeis.org/A000040
|
|
||||||
"""
|
|
||||||
d: Dict[int, List[int]] = {}
|
|
||||||
for i in itertools.count(2):
|
|
||||||
if i in d:
|
|
||||||
for j in d[i]:
|
|
||||||
d[i + j] = d.get(i + j, []) + [j]
|
|
||||||
del d[i]
|
|
||||||
else:
|
|
||||||
d[i * i] = [i]
|
|
||||||
yield i
|
|
||||||
|
|
||||||
|
|
||||||
def composite() -> Iterator[int]:
|
|
||||||
"""Generate the composite numbers using the sieve of Eratosthenes.
|
|
||||||
https://oeis.org/A002808
|
|
||||||
"""
|
|
||||||
p1 = 3
|
|
||||||
for p2 in eratosthenes():
|
|
||||||
yield from range(p1 + 1, p2)
|
|
||||||
p1 = p2
|
|
||||||
|
|
||||||
|
|
||||||
def carmichael() -> Iterator[int]:
|
|
||||||
"""Composite numbers n such that a^(n-1) == 1 (mod n) for every a coprime
|
|
||||||
to n.
|
|
||||||
https://oeis.org/A002997
|
|
||||||
"""
|
|
||||||
for m in composite():
|
|
||||||
for a in range(2, m):
|
|
||||||
if pow(a, m, m) != a:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
yield m
|
|
||||||
|
|
||||||
|
|
||||||
def ackermann_slow(m: int, n: int) -> int:
|
|
||||||
"""Ackermann number."""
|
|
||||||
if m == 0:
|
|
||||||
return n + 1
|
|
||||||
elif n == 0:
|
|
||||||
return ackermann_slow(m - 1, 1)
|
|
||||||
else:
|
|
||||||
return ackermann_slow(m - 1, ackermann_slow(m, n - 1))
|
|
||||||
|
|
||||||
|
|
||||||
def ackermann_naive(m: int) -> Iterator[int]:
|
|
||||||
"""Naive Ackermann encapsulated in a generator."""
|
|
||||||
n = 0
|
|
||||||
while True:
|
|
||||||
yield ackermann_slow(m, n)
|
|
||||||
n += 1
|
|
||||||
|
|
||||||
|
|
||||||
def ackermann_fast(m: int, n: int) -> int:
|
|
||||||
"""Ackermann number."""
|
|
||||||
while m >= 4:
|
|
||||||
if n == 0:
|
|
||||||
n = 1
|
|
||||||
else:
|
|
||||||
n = ackermann_fast(m, n - 1)
|
|
||||||
m -= 1
|
|
||||||
if m == 3:
|
|
||||||
return (1 << n + 3) - 3
|
|
||||||
elif m == 2:
|
|
||||||
return (n << 1) + 3
|
|
||||||
elif m == 1:
|
|
||||||
return n + 2
|
|
||||||
else:
|
|
||||||
return n + 1
|
|
||||||
|
|
||||||
|
|
||||||
def ackermann(m: int) -> Iterator[int]:
|
|
||||||
"""Ackermann encapsulated in a generator."""
|
|
||||||
n = 0
|
|
||||||
while True:
|
|
||||||
yield ackermann_fast(m, n)
|
|
||||||
n += 1
|
|
||||||
|
|
||||||
|
|
||||||
def fibonacci() -> Iterator[int]:
|
|
||||||
"""Generate the sequence of Fibonacci.
|
|
||||||
https://oeis.org/A000045
|
|
||||||
"""
|
|
||||||
a, b = 1, 2
|
|
||||||
while True:
|
|
||||||
yield a
|
|
||||||
a, b = b, a + b
|
|
||||||
|
|
||||||
|
|
||||||
def log_gen() -> Iterator[int]:
|
|
||||||
"""Logarithmic generator."""
|
|
||||||
y = 1
|
|
||||||
while True:
|
|
||||||
adder = max(1, math.pow(10, int(math.log10(y))))
|
|
||||||
yield int(y)
|
|
||||||
y = y + int(adder)
|
|
||||||
|
|
||||||
|
|
||||||
polys = {
|
|
||||||
2: [2, 1],
|
|
||||||
3: [3, 1],
|
|
||||||
4: [4, 1],
|
|
||||||
5: [5, 2],
|
|
||||||
6: [6, 1],
|
|
||||||
7: [7, 1],
|
|
||||||
8: [8, 4, 3, 2],
|
|
||||||
9: [9, 4],
|
|
||||||
10: [10, 3],
|
|
||||||
11: [11, 2],
|
|
||||||
12: [12, 6, 4, 1],
|
|
||||||
13: [13, 4, 3, 1],
|
|
||||||
14: [14, 8, 6, 1],
|
|
||||||
15: [15, 1],
|
|
||||||
16: [16, 12, 3, 1],
|
|
||||||
17: [17, 3],
|
|
||||||
18: [18, 7],
|
|
||||||
19: [19, 5, 2, 1],
|
|
||||||
20: [20, 3],
|
|
||||||
21: [21, 2],
|
|
||||||
22: [22, 1],
|
|
||||||
23: [23, 5],
|
|
||||||
24: [24, 7, 2, 1],
|
|
||||||
25: [25, 3],
|
|
||||||
26: [26, 6, 2, 1],
|
|
||||||
27: [27, 5, 2, 1],
|
|
||||||
28: [28, 3],
|
|
||||||
29: [29, 2],
|
|
||||||
30: [30, 23, 2, 1],
|
|
||||||
31: [31, 3],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def LFSR(m: int) -> Iterator[int]:
|
|
||||||
"""LFSR generator of the given size
|
|
||||||
https://en.wikipedia.org/wiki/Linear-feedback_shift_register
|
|
||||||
"""
|
|
||||||
n: int = m.bit_length() - 1
|
|
||||||
# Set initial state to {1 0 0 ... 0}
|
|
||||||
state: List[int] = [0] * n
|
|
||||||
state[0] = 1
|
|
||||||
feedback: int = 0
|
|
||||||
poly: List[int] = polys[n]
|
|
||||||
while True:
|
|
||||||
# Compute the feedback bit
|
|
||||||
feedback = 0
|
|
||||||
for i in range(len(poly)):
|
|
||||||
feedback = feedback ^ state[poly[i] - 1]
|
|
||||||
# Roll the registers
|
|
||||||
state.pop()
|
|
||||||
# Add the feedback bit
|
|
||||||
state.insert(0, feedback)
|
|
||||||
# Convert the registers to an int
|
|
||||||
out = sum(e * (2**i) for i, e in enumerate(state))
|
|
||||||
yield out
|
|
||||||
|
|
||||||
|
|
||||||
def shi_tomashi(
|
|
||||||
image_path: str,
|
|
||||||
max_corners: int = 100,
|
|
||||||
quality: float = 0.01,
|
|
||||||
min_distance: float = 10.0,
|
|
||||||
) -> Iterator[int]:
|
|
||||||
"""Shi-Tomachi corner generator of the given points
|
|
||||||
https://docs.opencv.org/4.x/d4/d8c/tutorial_py_shi_tomasi.html
|
|
||||||
"""
|
|
||||||
image = cv2.imread(image_path)
|
|
||||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
||||||
corners: np.ndarray = cv2.goodFeaturesToTrack(
|
|
||||||
gray, max_corners, quality, min_distance
|
|
||||||
)
|
|
||||||
corners_int: np.ndarray[Any, np.dtype[np.signedinteger[Any]]] = np.array(
|
|
||||||
np.intp(corners)
|
|
||||||
)
|
|
||||||
i = 0
|
|
||||||
while True:
|
|
||||||
x, y = corners_int[i].ravel()
|
|
||||||
# Compute the pixel number with top left of image as origin
|
|
||||||
# using coordinates of the corner.
|
|
||||||
# (y * number of pixels a row) + pixels left in last row
|
|
||||||
yield (y * image.shape[1]) + x
|
|
||||||
i += 1
|
|
|
@ -1,90 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.7 $"
|
|
||||||
__date__ = "$Date: 2016/03/13 $"
|
|
||||||
__revision__ = "$Date: 2019/05/31 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
from typing import IO, Iterator, Union
|
|
||||||
|
|
||||||
from stegano import tools
|
|
||||||
|
|
||||||
from .generators import identity
|
|
||||||
|
|
||||||
|
|
||||||
def hide(
|
|
||||||
image: Union[str, IO[bytes]],
|
|
||||||
message: str,
|
|
||||||
generator: Union[None, Iterator[int]] = None,
|
|
||||||
shift: int = 0,
|
|
||||||
encoding: str = "UTF-8",
|
|
||||||
auto_convert_rgb: bool = False,
|
|
||||||
):
|
|
||||||
"""Hide a message (string) in an image with the
|
|
||||||
LSB (Least Significant Bit) technique.
|
|
||||||
"""
|
|
||||||
hider = tools.Hider(image, message, encoding, auto_convert_rgb)
|
|
||||||
width = hider.encoded_image.width
|
|
||||||
|
|
||||||
if not generator:
|
|
||||||
generator = identity()
|
|
||||||
|
|
||||||
while shift != 0:
|
|
||||||
next(generator)
|
|
||||||
shift -= 1
|
|
||||||
|
|
||||||
while hider.encode_another_pixel():
|
|
||||||
generated_number = next(generator)
|
|
||||||
|
|
||||||
col = generated_number % width
|
|
||||||
row = int(generated_number / width)
|
|
||||||
|
|
||||||
hider.encode_pixel((col, row))
|
|
||||||
|
|
||||||
return hider.encoded_image
|
|
||||||
|
|
||||||
|
|
||||||
def reveal(
|
|
||||||
encoded_image: Union[str, IO[bytes]],
|
|
||||||
generator: Union[None, Iterator[int]] = None,
|
|
||||||
shift: int = 0,
|
|
||||||
encoding: str = "UTF-8",
|
|
||||||
close_file: bool = True,
|
|
||||||
):
|
|
||||||
"""Find a message in an image (with the LSB technique)."""
|
|
||||||
revealer = tools.Revealer(encoded_image, encoding, close_file)
|
|
||||||
width = revealer.encoded_image.width
|
|
||||||
|
|
||||||
if not generator:
|
|
||||||
generator = identity()
|
|
||||||
|
|
||||||
while shift != 0:
|
|
||||||
next(generator)
|
|
||||||
shift -= 1
|
|
||||||
|
|
||||||
while True:
|
|
||||||
generated_number = next(generator)
|
|
||||||
|
|
||||||
col = generated_number % width
|
|
||||||
row = int(generated_number / width)
|
|
||||||
|
|
||||||
if revealer.decode_pixel((col, row)):
|
|
||||||
return revealer.secret_message
|
|
|
@ -1,5 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from .red import hide, reveal
|
|
||||||
|
|
||||||
__all__ = ["hide", "reveal"]
|
|
|
@ -1,94 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stéganô is a basic Python Steganography module.
|
|
||||||
# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.2 $"
|
|
||||||
__date__ = "$Date: 2010/10/01 $"
|
|
||||||
__revision__ = "$Date: 2017/02/06 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
from typing import IO, Union, cast
|
|
||||||
|
|
||||||
from stegano import tools
|
|
||||||
|
|
||||||
|
|
||||||
def hide(input_image: Union[str, IO[bytes]], message: str):
|
|
||||||
"""
|
|
||||||
Hide a message (string) in an image.
|
|
||||||
|
|
||||||
Use the red portion of a pixel (r, g, b) tuple to
|
|
||||||
hide the message string characters as ASCII values.
|
|
||||||
The red value of the first pixel is used for message_length of the string.
|
|
||||||
"""
|
|
||||||
message_length = len(message)
|
|
||||||
assert message_length != 0, "message message_length is zero"
|
|
||||||
assert message_length < 255, "message is too long"
|
|
||||||
img = tools.open_image(input_image)
|
|
||||||
# Ensure image mode is RGB
|
|
||||||
if img.mode != "RGB":
|
|
||||||
img = img.convert("RGB")
|
|
||||||
# Use a copy of image to hide the text in
|
|
||||||
encoded = img.copy()
|
|
||||||
width, height = img.size
|
|
||||||
index = 0
|
|
||||||
for row in range(height):
|
|
||||||
for col in range(width):
|
|
||||||
pixel = cast(tuple[int, int, int], img.getpixel((col, row)))
|
|
||||||
r, g, b = pixel
|
|
||||||
# first value is message_length of message
|
|
||||||
if row == 0 and col == 0 and index < message_length:
|
|
||||||
asc = message_length
|
|
||||||
elif index <= message_length:
|
|
||||||
c = message[index - 1]
|
|
||||||
asc = ord(c)
|
|
||||||
else:
|
|
||||||
asc = r
|
|
||||||
encoded.putpixel((col, row), (asc, g, b))
|
|
||||||
index += 1
|
|
||||||
img.close()
|
|
||||||
return encoded
|
|
||||||
|
|
||||||
|
|
||||||
def reveal(input_image: Union[str, IO[bytes]]):
|
|
||||||
"""
|
|
||||||
Find a message in an image.
|
|
||||||
|
|
||||||
Check the red portion of an pixel (r, g, b) tuple for
|
|
||||||
hidden message characters (ASCII values).
|
|
||||||
The red value of the first pixel is used for message_length of string.
|
|
||||||
"""
|
|
||||||
img = tools.open_image(input_image)
|
|
||||||
# Ensure image mode is RGB
|
|
||||||
if img.mode != "RGB":
|
|
||||||
img = img.convert("RGB")
|
|
||||||
width, height = img.size
|
|
||||||
message = ""
|
|
||||||
index = 0
|
|
||||||
for row in range(height):
|
|
||||||
for col in range(width):
|
|
||||||
pixel = cast(tuple[int, int, int], img.getpixel((col, row)))
|
|
||||||
r, g, b = pixel
|
|
||||||
# First pixel r value is length of message
|
|
||||||
if row == 0 and col == 0:
|
|
||||||
message_length = r
|
|
||||||
elif index <= message_length:
|
|
||||||
message += chr(r)
|
|
||||||
index += 1
|
|
||||||
img.close()
|
|
||||||
return message
|
|
163
stegano/slsb.py
Executable file
|
@ -0,0 +1,163 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2011 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.2 $"
|
||||||
|
__date__ = "$Date: 2010/03/24 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from . import tools
|
||||||
|
|
||||||
|
def hide(input_image_file, message):
|
||||||
|
"""
|
||||||
|
Hide a message (string) in an image with the
|
||||||
|
LSB (Least Significant Bit) technique.
|
||||||
|
"""
|
||||||
|
img = Image.open(input_image_file)
|
||||||
|
encoded = img.copy()
|
||||||
|
width, height = img.size
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
message = str(len(message)) + ":" + message
|
||||||
|
#message_bits = tools.a2bits(message)
|
||||||
|
message_bits = "".join(tools.a2bits_list(message))
|
||||||
|
|
||||||
|
npixels = width * height
|
||||||
|
if len(message_bits) > npixels * 3:
|
||||||
|
raise Exception("""The message you want to hide is too long (%s > %s).""" % (len(message_bits), npixels * 3))
|
||||||
|
|
||||||
|
for row in range(height):
|
||||||
|
for col in range(width):
|
||||||
|
|
||||||
|
if index + 3 <= len(message_bits) :
|
||||||
|
|
||||||
|
# Get the colour component.
|
||||||
|
(r, g, b) = img.getpixel((col, row))
|
||||||
|
|
||||||
|
# Change the Least Significant Bit of each colour component.
|
||||||
|
r = tools.setlsb(r, message_bits[index])
|
||||||
|
g = tools.setlsb(g, message_bits[index+1])
|
||||||
|
b = tools.setlsb(b, message_bits[index+2])
|
||||||
|
|
||||||
|
# Save the new pixel
|
||||||
|
encoded.putpixel((col, row), (r, g , b))
|
||||||
|
|
||||||
|
index += 3
|
||||||
|
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
def reveal(input_image_file):
|
||||||
|
"""
|
||||||
|
Find a message in an image
|
||||||
|
(with the LSB technique).
|
||||||
|
"""
|
||||||
|
img = Image.open(input_image_file)
|
||||||
|
width, height = img.size
|
||||||
|
buff, count = 0, 0
|
||||||
|
bitab = []
|
||||||
|
limit = None
|
||||||
|
for row in range(height):
|
||||||
|
for col in range(width):
|
||||||
|
|
||||||
|
# color = [r, g, b]
|
||||||
|
for color in img.getpixel((col, row)):
|
||||||
|
buff += (color&1)<<(7-count)
|
||||||
|
count += 1
|
||||||
|
if count == 8:
|
||||||
|
bitab.append(chr(buff))
|
||||||
|
buff, count = 0, 0
|
||||||
|
if bitab[-1] == ":" and limit == None:
|
||||||
|
try:
|
||||||
|
limit = int("".join(bitab[:-1]))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(bitab)-len(str(limit))-1 == limit :
|
||||||
|
return "".join(bitab)[len(str(limit))+1:]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def write(image, output_image_file):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
image.save(output_image_file)
|
||||||
|
except Exception as e:
|
||||||
|
# If hide() returns an error (Too long message).
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Point of entry in execution mode.
|
||||||
|
from optparse import OptionParser
|
||||||
|
parser = OptionParser(version=__version__)
|
||||||
|
parser.add_option('--hide', action='store_true', default=False,
|
||||||
|
help="Hides a message in an image.")
|
||||||
|
parser.add_option('--reveal', action='store_true', default=False,
|
||||||
|
help="Reveals the message hided in an image.")
|
||||||
|
# Original image
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Input image file.")
|
||||||
|
# Image containing the secret
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Output image containing the secret.")
|
||||||
|
|
||||||
|
# Non binary secret message to hide
|
||||||
|
parser.add_option("-m", "--secret-message", dest="secret_message",
|
||||||
|
help="Your secret message to hide (non binary).")
|
||||||
|
|
||||||
|
# Binary secret to hide (OGG, executable, etc.)
|
||||||
|
parser.add_option("-f", "--secret-file", dest="secret_file",
|
||||||
|
help="Your secret to hide (Text or any binary file).")
|
||||||
|
# Output for the binary binary secret.
|
||||||
|
parser.add_option("-b", "--binary", dest="secret_binary",
|
||||||
|
help="Output for the binary secret (Text or any binary file).")
|
||||||
|
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Lenna.png',
|
||||||
|
output_image_file = './pictures/Lenna_enc.png',
|
||||||
|
secret_message = '', secret_file = '', secret_binary = "")
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if options.hide:
|
||||||
|
if options.secret_message != "" and options.secret_file == "":
|
||||||
|
secret = options.secret_message
|
||||||
|
elif options.secret_message == "" and options.secret_file != "":
|
||||||
|
secret = tools.binary2base64(options.secret_file)
|
||||||
|
|
||||||
|
img_encoded = hide(options.input_image_file, secret)
|
||||||
|
try:
|
||||||
|
img_encoded.save(options.output_image_file)
|
||||||
|
except Exception as e:
|
||||||
|
# If hide() returns an error (Too long message).
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
elif options.reveal:
|
||||||
|
secret = reveal(options.input_image_file)
|
||||||
|
if options.secret_binary != "":
|
||||||
|
data = tools.base642binary(secret)
|
||||||
|
with open(options.secret_binary, "w") as f:
|
||||||
|
f.write(data)
|
||||||
|
else:
|
||||||
|
print(secret)
|
183
stegano/slsbset.py
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2013 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.4 $"
|
||||||
|
__date__ = "$Date: 2011/12/28 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from . import tools
|
||||||
|
from . import generators
|
||||||
|
|
||||||
|
def hide(input_image_file, message, generator_function):
|
||||||
|
"""
|
||||||
|
Hide a message (string) in an image with the
|
||||||
|
LSB (Least Significant Bit) technique.
|
||||||
|
"""
|
||||||
|
img = Image.open(input_image_file)
|
||||||
|
img_list = list(img.getdata())
|
||||||
|
width, height = img.size
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
message = str(len(message)) + ":" + message
|
||||||
|
#message_bits = tools.a2bits(message)
|
||||||
|
message_bits = "".join(tools.a2bits_list(message))
|
||||||
|
|
||||||
|
npixels = width * height
|
||||||
|
if len(message_bits) > npixels * 3:
|
||||||
|
raise Exception("""The message you want to hide is too long (%s > %s).""" % (len(message_bits), npixels * 3))
|
||||||
|
|
||||||
|
generator = getattr(generators, generator_function)()
|
||||||
|
|
||||||
|
while index + 3 <= len(message_bits) :
|
||||||
|
generated_number = next(generator)
|
||||||
|
(r, g, b) = img_list[generated_number]
|
||||||
|
|
||||||
|
# Change the Least Significant Bit of each colour component.
|
||||||
|
r = tools.setlsb(r, message_bits[index])
|
||||||
|
g = tools.setlsb(g, message_bits[index+1])
|
||||||
|
b = tools.setlsb(b, message_bits[index+2])
|
||||||
|
|
||||||
|
# Save the new pixel
|
||||||
|
img_list[generated_number] = (r, g , b)
|
||||||
|
|
||||||
|
index += 3
|
||||||
|
|
||||||
|
# create empty new image of appropriate format
|
||||||
|
encoded = Image.new('RGB', (img.size))
|
||||||
|
|
||||||
|
# insert saved data into the image
|
||||||
|
encoded.putdata(img_list)
|
||||||
|
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def reveal(input_image_file, generator_function):
|
||||||
|
"""
|
||||||
|
Find a message in an image
|
||||||
|
(with the LSB technique).
|
||||||
|
"""
|
||||||
|
img = Image.open(input_image_file)
|
||||||
|
img_list = list(img.getdata())
|
||||||
|
width, height = img.size
|
||||||
|
buff, count = 0, 0
|
||||||
|
bitab = []
|
||||||
|
limit = None
|
||||||
|
|
||||||
|
generator = getattr(generators, generator_function)()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
generated_number = next(generator)
|
||||||
|
# color = [r, g, b]
|
||||||
|
for color in img_list[generated_number]:
|
||||||
|
buff += (color&1)<<(7-count)
|
||||||
|
count += 1
|
||||||
|
if count == 8:
|
||||||
|
bitab.append(chr(buff))
|
||||||
|
buff, count = 0, 0
|
||||||
|
if bitab[-1] == ":" and limit == None:
|
||||||
|
try:
|
||||||
|
limit = int("".join(bitab[:-1]))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if len(bitab)-len(str(limit))-1 == limit :
|
||||||
|
return "".join(bitab)[len(str(limit))+1:]
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def write(image, output_image_file):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
image.save(output_image_file)
|
||||||
|
except Exception as e:
|
||||||
|
# If hide() returns an error (Too long message).
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Point of entry in execution mode.
|
||||||
|
from optparse import OptionParser
|
||||||
|
parser = OptionParser(version=__version__)
|
||||||
|
parser.add_option('--hide', action='store_true', default=False,
|
||||||
|
help="Hides a message in an image.")
|
||||||
|
parser.add_option('--reveal', action='store_true', default=False,
|
||||||
|
help="Reveals the message hided in an image.")
|
||||||
|
# Original image
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Input image file.")
|
||||||
|
|
||||||
|
# Generator
|
||||||
|
parser.add_option("-g", "--generator", dest="generator_function",
|
||||||
|
help="Generator")
|
||||||
|
|
||||||
|
# Image containing the secret
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Output image containing the secret.")
|
||||||
|
|
||||||
|
# Non binary secret message to hide
|
||||||
|
parser.add_option("-m", "--secret-message", dest="secret_message",
|
||||||
|
help="Your secret message to hide (non binary).")
|
||||||
|
|
||||||
|
# Binary secret to hide (OGG, executable, etc.)
|
||||||
|
parser.add_option("-f", "--secret-file", dest="secret_file",
|
||||||
|
help="Your secret to hide (Text or any binary file).")
|
||||||
|
# Output for the binary binary secret.
|
||||||
|
parser.add_option("-b", "--binary", dest="secret_binary",
|
||||||
|
help="Output for the binary secret (Text or any binary file).")
|
||||||
|
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Lenna.png',
|
||||||
|
generator_function = 'fermat',
|
||||||
|
output_image_file = './pictures/Lenna_enc.png',
|
||||||
|
secret_message = '', secret_file = '', secret_binary = "")
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if options.hide:
|
||||||
|
if options.secret_message != "" and options.secret_file == "":
|
||||||
|
secret = options.secret_message
|
||||||
|
elif options.secret_message == "" and options.secret_file != "":
|
||||||
|
secret = tools.binary2base64(options.secret_file)
|
||||||
|
|
||||||
|
img_encoded = hide(options.input_image_file, secret, options.generator_function)
|
||||||
|
try:
|
||||||
|
img_encoded.save(options.output_image_file)
|
||||||
|
except Exception as e:
|
||||||
|
# If hide() returns an error (Too long message).
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
elif options.reveal:
|
||||||
|
try:
|
||||||
|
secret = reveal(options.input_image_file, options.generator_function)
|
||||||
|
except IndexError:
|
||||||
|
print("Impossible to detect message.")
|
||||||
|
exit(0)
|
||||||
|
if options.secret_binary != "":
|
||||||
|
data = tools.base642binary(secret)
|
||||||
|
with open(options.secret_binary, "w") as f:
|
||||||
|
f.write(data)
|
||||||
|
else:
|
||||||
|
print(secret)
|
|
@ -1 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
|
@ -1,52 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.2 $"
|
|
||||||
__date__ = "$Date: 2010/10/01 $"
|
|
||||||
__revision__ = "$Date: 2021/11/01 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import typing
|
|
||||||
from collections import Counter, OrderedDict
|
|
||||||
|
|
||||||
|
|
||||||
def steganalyse(img):
|
|
||||||
"""
|
|
||||||
Steganlysis of the LSB technique.
|
|
||||||
"""
|
|
||||||
width, height = img.size
|
|
||||||
colours_counter: typing.Counter[int] = Counter()
|
|
||||||
for row in range(height):
|
|
||||||
for col in range(width):
|
|
||||||
r, g, b = img.getpixel((col, row))
|
|
||||||
colours_counter[r] += 1
|
|
||||||
|
|
||||||
most_common = colours_counter.most_common(10)
|
|
||||||
dict_colours = OrderedDict(
|
|
||||||
sorted(list(colours_counter.items()), key=lambda t: t[1])
|
|
||||||
)
|
|
||||||
|
|
||||||
colours: float = 0
|
|
||||||
for colour in list(dict_colours.keys()):
|
|
||||||
colours += colour
|
|
||||||
colours = colours / len(dict_colours)
|
|
||||||
|
|
||||||
# return colours.most_common(10)
|
|
||||||
return list(dict_colours.keys())[:30], most_common
|
|
|
@ -1,8 +1,10 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
#-*- coding: utf-8 -*-
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2013 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
#
|
#
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,28 +20,22 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
__author__ = "Cedric Bonhomme"
|
||||||
__version__ = "$Revision: 0.9.4 $"
|
__version__ = "$Revision: 0.1 $"
|
||||||
__date__ = "$Date: 2010/10/01 $"
|
__date__ = "$Date: 2010/10/01 $"
|
||||||
__revision__ = "$Date: 2019/06/06 $"
|
|
||||||
__license__ = "GPLv3"
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
def steganalyse(img):
|
||||||
def steganalyse(img: Image.Image) -> Image.Image:
|
|
||||||
"""
|
"""
|
||||||
Steganlysis of the LSB technique.
|
Steganlysis of the LSB technique.
|
||||||
"""
|
"""
|
||||||
encoded = Image.new(img.mode, (img.size))
|
encoded = img.copy()
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
|
bits = ""
|
||||||
for row in range(height):
|
for row in range(height):
|
||||||
for col in range(width):
|
for col in range(width):
|
||||||
if pixel := cast(tuple[int, int, int], img.getpixel((col, row))):
|
r, g, b = img.getpixel((col, row))
|
||||||
r, g, b = pixel[0:3]
|
|
||||||
else:
|
|
||||||
raise Exception("Error during steganlysis.")
|
|
||||||
if r % 2 == 0:
|
if r % 2 == 0:
|
||||||
r = 0
|
r = 0
|
||||||
else:
|
else:
|
||||||
|
@ -52,5 +48,21 @@ def steganalyse(img: Image.Image) -> Image.Image:
|
||||||
b = 0
|
b = 0
|
||||||
else:
|
else:
|
||||||
b = 255
|
b = 255
|
||||||
encoded.putpixel((col, row), (r, g, b))
|
encoded.putpixel((col, row), (r, g , b))
|
||||||
return encoded
|
return encoded
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Point of entry in execution mode.
|
||||||
|
from optparse import OptionParser
|
||||||
|
parser = OptionParser()
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Image file")
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Image file")
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Lenna.png',
|
||||||
|
output_image_file = './pictures/Lenna_steganalysed.png')
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
input_image_file = Image.open(options.input_image_file)
|
||||||
|
output_image = steganalyse(input_image_file)
|
||||||
|
output_image.save(options.output_image_file)
|
70
stegano/steganalysisStatistics.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2013 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
|
#
|
||||||
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
__author__ = "Cedric Bonhomme"
|
||||||
|
__version__ = "$Revision: 0.1 $"
|
||||||
|
__date__ = "$Date: 2010/10/01 $"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
from collections import Counter
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
def steganalyse(img):
|
||||||
|
"""
|
||||||
|
Steganlysis of the LSB technique.
|
||||||
|
"""
|
||||||
|
encoded = img.copy()
|
||||||
|
width, height = img.size
|
||||||
|
colours = Counter()
|
||||||
|
for row in range(height):
|
||||||
|
for col in range(width):
|
||||||
|
r, g, b = img.getpixel((col, row))
|
||||||
|
colours[r] += 1
|
||||||
|
|
||||||
|
most_common = colours.most_common(10)
|
||||||
|
dict_colours = OrderedDict(sorted(list(colours.items()), key=lambda t: t[1]))
|
||||||
|
|
||||||
|
colours = 0
|
||||||
|
for colour in list(dict_colours.keys()):
|
||||||
|
colours += colour
|
||||||
|
colours = colours / len(dict_colours)
|
||||||
|
|
||||||
|
#return colours.most_common(10)
|
||||||
|
return list(dict_colours.keys())[:30], most_common
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Point of entry in execution mode.
|
||||||
|
from optparse import OptionParser
|
||||||
|
parser = OptionParser()
|
||||||
|
parser.add_option("-i", "--input", dest="input_image_file",
|
||||||
|
help="Image file.")
|
||||||
|
parser.add_option("-o", "--output", dest="output_image_file",
|
||||||
|
help="Image file.")
|
||||||
|
parser.set_defaults(input_image_file = './pictures/Lenna.png',
|
||||||
|
output_image_file = './pictures/Lenna_steganalysed.png')
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
input_image_file = Image.open(options.input_image_file)
|
||||||
|
output_image = steganalyse(input_image_file)
|
||||||
|
soutput_image.save(options.output_image_file)
|
214
stegano/tools.py
|
@ -1,8 +1,10 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
|
# Stéganô - Stéganô is a basic Python Steganography module.
|
||||||
|
# Copyright (C) 2010-2013 Cédric Bonhomme - http://cedricbonhomme.org/
|
||||||
#
|
#
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,32 +20,25 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
__author__ = "Cedric Bonhomme"
|
||||||
__version__ = "$Revision: 0.3 $"
|
__version__ = "$Revision: 0.1 $"
|
||||||
__date__ = "$Date: 2010/10/01 $"
|
__date__ = "$Date: 2010/10/01 $"
|
||||||
__revision__ = "$Date: 2017/05/04 $"
|
|
||||||
__license__ = "GPLv3"
|
__license__ = "GPLv3"
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import itertools
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import IO, List, Union, cast
|
|
||||||
|
|
||||||
from PIL import Image
|
def a2bits(chars):
|
||||||
|
"""
|
||||||
ENCODINGS = {"UTF-8": 8, "UTF-32LE": 32}
|
Converts a string to its bits representation as a string of 0's and 1's.
|
||||||
|
|
||||||
|
|
||||||
def a2bits(chars: str) -> str:
|
|
||||||
"""Converts a string to its bits representation as a string of 0's and 1's.
|
|
||||||
|
|
||||||
>>> a2bits("Hello World!")
|
>>> a2bits("Hello World!")
|
||||||
'010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001'
|
'010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001'
|
||||||
"""
|
"""
|
||||||
return bin(reduce(lambda x, y: (x << 8) + y, (ord(c) for c in chars), 1))[3:]
|
return bin(reduce(lambda x, y : (x<<8)+y, (ord(c) for c in chars), 1))[3:]
|
||||||
|
|
||||||
|
def a2bits_list(chars):
|
||||||
def a2bits_list(chars: str, encoding: str = "UTF-8") -> List[str]:
|
"""
|
||||||
"""Convert a string to its bits representation as a list of 0's and 1's.
|
Convert a string to its bits representation as a list of 0's and 1's.
|
||||||
|
|
||||||
>>> a2bits_list("Hello World!")
|
>>> a2bits_list("Hello World!")
|
||||||
['01001000',
|
['01001000',
|
||||||
|
@ -61,170 +56,55 @@ def a2bits_list(chars: str, encoding: str = "UTF-8") -> List[str]:
|
||||||
>>> "".join(a2bits_list("Hello World!"))
|
>>> "".join(a2bits_list("Hello World!"))
|
||||||
'010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001'
|
'010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001'
|
||||||
"""
|
"""
|
||||||
return [bin(ord(x))[2:].rjust(ENCODINGS[encoding], "0") for x in chars]
|
return [bin(ord(x))[2:].rjust(8,"0") for x in chars]
|
||||||
|
|
||||||
|
def bs(s):
|
||||||
|
"""
|
||||||
|
Converts an int to its bits representation as a string of 0's and 1's.
|
||||||
|
"""
|
||||||
|
return str(s) if s<=1 else bs(s>>1) + str(s&1)
|
||||||
|
|
||||||
def bs(s: int) -> str:
|
def setlsb(component, bit):
|
||||||
"""Converts an int to its bits representation as a string of 0's and 1's."""
|
"""
|
||||||
return str(s) if s <= 1 else bs(s >> 1) + str(s & 1)
|
Set Least Significant Bit of a colour component.
|
||||||
|
"""
|
||||||
|
|
||||||
def setlsb(component: int, bit: str) -> int:
|
|
||||||
"""Set Least Significant Bit of a colour component."""
|
|
||||||
return component & ~1 | int(bit)
|
return component & ~1 | int(bit)
|
||||||
|
|
||||||
|
def n_at_a_time(items, n, fillvalue):
|
||||||
def n_at_a_time(items: List[int], n: int, fillvalue: str):
|
"""
|
||||||
"""Returns an iterator which groups n items at a time.
|
Returns an iterator which groups n items at a time.
|
||||||
Any final partial tuple will be padded with the fillvalue
|
Any final partial tuple will be padded with the fillvalue
|
||||||
|
|
||||||
>>> list(n_at_a_time([1, 2, 3, 4, 5], 2, 'X'))
|
>>> list(n_at_a_time([1, 2, 3, 4, 5], 2, 'X'))
|
||||||
[(1, 2), (3, 4), (5, 'X')]
|
[(1, 2), (3, 4), (5, 'X')]
|
||||||
"""
|
"""
|
||||||
it = iter(items)
|
it = iter(items)
|
||||||
return itertools.zip_longest(*[it] * n, fillvalue=fillvalue)
|
return its.izip_longest(*[it] * n, fillvalue=fillvalue)
|
||||||
|
|
||||||
|
def binary2base64(binary_file):
|
||||||
def binary2base64(binary_file: str) -> str:
|
"""
|
||||||
"""Convert a binary file (OGG, executable, etc.) to a
|
Convert a binary file (OGG, executable, etc.) to a
|
||||||
printable string.
|
printable string.
|
||||||
"""
|
"""
|
||||||
# Use mode = "rb" to read binary file
|
# Use mode = "rb" to read binary file
|
||||||
with open(binary_file, "rb") as bin_file:
|
fin = open(binary_file, "rb")
|
||||||
encoded_string = base64.b64encode(bin_file.read())
|
binary_data = fin.read()
|
||||||
return encoded_string.decode()
|
fin.close()
|
||||||
|
|
||||||
|
# Encode binary to base64 string (printable)
|
||||||
|
return base64.b64encode(binary_data)
|
||||||
|
|
||||||
def base642binary(b64_fname: str) -> bytes:
|
"""fout = open(output_file, "w")
|
||||||
"""Convert a printable string to a binary file."""
|
fout.write(b64_data)
|
||||||
b64_fname += "==="
|
fout.close"""
|
||||||
return base64.b64decode(b64_fname)
|
|
||||||
|
|
||||||
|
def base642binary(b64_fname):
|
||||||
def open_image(fname_or_instance: Union[str, IO[bytes], Image.Image]) -> Image.Image:
|
|
||||||
"""Opens an image and returns it.
|
|
||||||
|
|
||||||
:param fname_or_instance: Can be a path to the image (str),
|
|
||||||
a file-like object (IO[bytes]),
|
|
||||||
or a PIL Image instance.
|
|
||||||
"""
|
"""
|
||||||
if isinstance(fname_or_instance, Image.Image):
|
Convert a printable file to a binary file.
|
||||||
return fname_or_instance
|
"""
|
||||||
|
# Read base64 string
|
||||||
return Image.open(fname_or_instance)
|
#fin = open(b64_fname, "r")
|
||||||
|
#b64_str = fin.read()
|
||||||
|
#fin.close()
|
||||||
class Hider:
|
# Decode base64 string to original binary sound object
|
||||||
def __init__(
|
return base64.b64decode(b64_fname)
|
||||||
self,
|
|
||||||
input_image: Union[str, IO[bytes]],
|
|
||||||
message: str,
|
|
||||||
encoding: str = "UTF-8",
|
|
||||||
auto_convert_rgb: bool = False,
|
|
||||||
):
|
|
||||||
self._index = 0
|
|
||||||
|
|
||||||
message_length = len(message)
|
|
||||||
assert message_length != 0, "message length is zero"
|
|
||||||
|
|
||||||
image = open_image(input_image)
|
|
||||||
|
|
||||||
if image.mode not in ["RGB", "RGBA"]:
|
|
||||||
if not auto_convert_rgb:
|
|
||||||
print(f"The mode of the image is not RGB. Mode is {image.mode}")
|
|
||||||
answer = input("Convert the image to RGB ? [Y / n]\n") or "Y"
|
|
||||||
if answer.lower() == "n":
|
|
||||||
raise Exception("Not a RGB image.")
|
|
||||||
|
|
||||||
image = image.convert("RGB")
|
|
||||||
|
|
||||||
self.encoded_image = image.copy()
|
|
||||||
image.close()
|
|
||||||
|
|
||||||
message = str(message_length) + ":" + str(message)
|
|
||||||
self._message_bits = "".join(a2bits_list(message, encoding))
|
|
||||||
self._message_bits += "0" * ((3 - (len(self._message_bits) % 3)) % 3)
|
|
||||||
|
|
||||||
width, height = self.encoded_image.size
|
|
||||||
npixels = width * height
|
|
||||||
self._len_message_bits = len(self._message_bits)
|
|
||||||
|
|
||||||
if self._len_message_bits > npixels * 3:
|
|
||||||
raise Exception(
|
|
||||||
f"The message you want to hide is too long: {message_length}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def encode_another_pixel(self):
|
|
||||||
return True if self._index + 3 <= self._len_message_bits else False
|
|
||||||
|
|
||||||
def encode_pixel(self, coordinate: tuple):
|
|
||||||
# Determine expected pixel format based on mode
|
|
||||||
if self.encoded_image.mode == "RGBA":
|
|
||||||
r, g, b, *a = cast(
|
|
||||||
tuple[int, int, int, int], self.encoded_image.getpixel(coordinate)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
r, g, b, *a = cast(
|
|
||||||
tuple[int, int, int], self.encoded_image.getpixel(coordinate)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Change the Least Significant Bit of each colour component.
|
|
||||||
r = setlsb(r, self._message_bits[self._index])
|
|
||||||
g = setlsb(g, self._message_bits[self._index + 1])
|
|
||||||
b = setlsb(b, self._message_bits[self._index + 2])
|
|
||||||
|
|
||||||
# Save the new pixel
|
|
||||||
if self.encoded_image.mode == "RGBA":
|
|
||||||
self.encoded_image.putpixel(coordinate, (r, g, b, *a))
|
|
||||||
else:
|
|
||||||
self.encoded_image.putpixel(coordinate, (r, g, b))
|
|
||||||
|
|
||||||
self._index += 3
|
|
||||||
|
|
||||||
|
|
||||||
class Revealer:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
encoded_image: Union[str, IO[bytes]],
|
|
||||||
encoding: str = "UTF-8",
|
|
||||||
close_file: bool = True,
|
|
||||||
):
|
|
||||||
self.encoded_image = open_image(encoded_image)
|
|
||||||
self._encoding_length = ENCODINGS[encoding]
|
|
||||||
self._buff, self._count = 0, 0
|
|
||||||
self._bitab: List[str] = []
|
|
||||||
self._limit: Union[None, int] = None
|
|
||||||
self.secret_message = ""
|
|
||||||
self.close_file = close_file
|
|
||||||
|
|
||||||
def decode_pixel(self, coordinate: tuple):
|
|
||||||
# Tell mypy that this will be a 3- or 4-tuple of ints
|
|
||||||
pixel = cast(
|
|
||||||
tuple[int, int, int] | tuple[int, int, int, int],
|
|
||||||
self.encoded_image.getpixel(coordinate),
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.encoded_image.mode == "RGBA":
|
|
||||||
pixel = pixel[:3] # ignore the alpha
|
|
||||||
|
|
||||||
for color in pixel:
|
|
||||||
self._buff += (color & 1) << (self._encoding_length - 1 - self._count)
|
|
||||||
self._count += 1
|
|
||||||
|
|
||||||
if self._count == self._encoding_length:
|
|
||||||
self._bitab.append(chr(self._buff))
|
|
||||||
self._buff, self._count = 0, 0
|
|
||||||
|
|
||||||
if self._bitab[-1] == ":" and self._limit is None:
|
|
||||||
if "".join(self._bitab[:-1]).isdigit():
|
|
||||||
self._limit = int("".join(self._bitab[:-1]))
|
|
||||||
else:
|
|
||||||
raise IndexError("Impossible to detect message.")
|
|
||||||
|
|
||||||
if len(self._bitab) - len(str(self._limit)) - 1 == self._limit:
|
|
||||||
self.secret_message = "".join(self._bitab)[len(str(self._limit)) + 1 :]
|
|
||||||
if self.close_file:
|
|
||||||
self.encoded_image.close()
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
|
@ -1,5 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from .wav import hide, reveal
|
|
||||||
|
|
||||||
__all__ = ["hide", "reveal"]
|
|
|
@ -1,112 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stéganô is a basic Python Steganography module.
|
|
||||||
# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.2 $"
|
|
||||||
__date__ = "$Date: 2010/10/01 $"
|
|
||||||
__revision__ = "$Date: 2017/02/06 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import wave
|
|
||||||
from typing import IO, Union
|
|
||||||
|
|
||||||
from stegano import tools
|
|
||||||
|
|
||||||
|
|
||||||
def hide(
|
|
||||||
input_file: Union[str, IO[bytes]],
|
|
||||||
message: str,
|
|
||||||
output_file: Union[str, IO[bytes]],
|
|
||||||
encoding: str = "UTF-8",
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Hide a message (string) in a .wav audio file.
|
|
||||||
|
|
||||||
Use the lsb of each PCM encoded sample to hide the message string characters as ASCII values.
|
|
||||||
The first eight bits are used for message_length of the string.
|
|
||||||
"""
|
|
||||||
message_length = len(message)
|
|
||||||
assert message_length != 0, "message message_length is zero"
|
|
||||||
assert message_length < 255, "message is too long"
|
|
||||||
|
|
||||||
output = wave.open(output_file, "wb")
|
|
||||||
with wave.open(input_file, "rb") as input:
|
|
||||||
# get .wav params
|
|
||||||
nchannels, sampwidth, framerate, nframes, comptype, _ = input.getparams()
|
|
||||||
assert comptype == "NONE", "only uncompressed files are supported"
|
|
||||||
|
|
||||||
nsamples = nframes * nchannels
|
|
||||||
|
|
||||||
message_bits = f"{message_length:08b}" + "".join(
|
|
||||||
tools.a2bits_list(message, encoding)
|
|
||||||
)
|
|
||||||
assert len(message_bits) <= nsamples, "message is too long"
|
|
||||||
|
|
||||||
# copy over .wav params to output
|
|
||||||
output.setnchannels(nchannels)
|
|
||||||
output.setsampwidth(sampwidth)
|
|
||||||
output.setframerate(framerate)
|
|
||||||
|
|
||||||
# encode message in frames
|
|
||||||
frames = bytearray(input.readframes(nsamples))
|
|
||||||
for i in range(nsamples):
|
|
||||||
if i < len(message_bits):
|
|
||||||
if message_bits[i] == "0":
|
|
||||||
frames[i] = frames[i] & ~1
|
|
||||||
else:
|
|
||||||
frames[i] = frames[i] | 1
|
|
||||||
|
|
||||||
# write out
|
|
||||||
output.writeframes(frames)
|
|
||||||
|
|
||||||
|
|
||||||
def reveal(input_file: Union[str, IO[bytes]], encoding: str = "UTF-8"):
|
|
||||||
"""
|
|
||||||
Find a message in an image.
|
|
||||||
|
|
||||||
Check the lsb of each PCM encoded sample for hidden message characters (ASCII values).
|
|
||||||
The first eight bits are used for message_length of the string.
|
|
||||||
"""
|
|
||||||
message = ""
|
|
||||||
encoding_len = tools.ENCODINGS[encoding]
|
|
||||||
with wave.open(input_file, "rb") as input:
|
|
||||||
nchannels, _, _, nframes, comptype, _ = input.getparams()
|
|
||||||
assert comptype == "NONE", "only uncompressed files are supported"
|
|
||||||
|
|
||||||
nsamples = nframes * nchannels
|
|
||||||
frames = bytearray(input.readframes(nsamples))
|
|
||||||
|
|
||||||
# Read first 8 bits for message length
|
|
||||||
length_bits = ""
|
|
||||||
for i in range(8):
|
|
||||||
length_bits += str(frames[i] & 1)
|
|
||||||
message_length = int(length_bits, 2)
|
|
||||||
|
|
||||||
# Read message bits
|
|
||||||
message_bits = ""
|
|
||||||
for i in range(8, 8 + message_length * encoding_len):
|
|
||||||
message_bits += str(frames[i] & 1)
|
|
||||||
|
|
||||||
# Convert bits to string
|
|
||||||
chars = [
|
|
||||||
chr(int(message_bits[i : i + encoding_len], 2))
|
|
||||||
for i in range(0, len(message_bits), encoding_len)
|
|
||||||
]
|
|
||||||
message = "".join(chars)
|
|
||||||
return message
|
|
|
@ -1,256 +0,0 @@
|
||||||
2
|
|
||||||
5
|
|
||||||
11
|
|
||||||
22
|
|
||||||
44
|
|
||||||
88
|
|
||||||
177
|
|
||||||
99
|
|
||||||
199
|
|
||||||
143
|
|
||||||
30
|
|
||||||
61
|
|
||||||
122
|
|
||||||
244
|
|
||||||
232
|
|
||||||
208
|
|
||||||
161
|
|
||||||
67
|
|
||||||
135
|
|
||||||
15
|
|
||||||
31
|
|
||||||
63
|
|
||||||
127
|
|
||||||
255
|
|
||||||
254
|
|
||||||
252
|
|
||||||
249
|
|
||||||
242
|
|
||||||
228
|
|
||||||
200
|
|
||||||
144
|
|
||||||
33
|
|
||||||
66
|
|
||||||
133
|
|
||||||
10
|
|
||||||
20
|
|
||||||
41
|
|
||||||
83
|
|
||||||
167
|
|
||||||
79
|
|
||||||
159
|
|
||||||
62
|
|
||||||
125
|
|
||||||
250
|
|
||||||
245
|
|
||||||
234
|
|
||||||
213
|
|
||||||
170
|
|
||||||
85
|
|
||||||
171
|
|
||||||
87
|
|
||||||
174
|
|
||||||
92
|
|
||||||
184
|
|
||||||
112
|
|
||||||
224
|
|
||||||
193
|
|
||||||
131
|
|
||||||
6
|
|
||||||
12
|
|
||||||
24
|
|
||||||
49
|
|
||||||
98
|
|
||||||
197
|
|
||||||
138
|
|
||||||
21
|
|
||||||
43
|
|
||||||
86
|
|
||||||
172
|
|
||||||
89
|
|
||||||
179
|
|
||||||
102
|
|
||||||
204
|
|
||||||
153
|
|
||||||
50
|
|
||||||
101
|
|
||||||
203
|
|
||||||
151
|
|
||||||
47
|
|
||||||
95
|
|
||||||
191
|
|
||||||
126
|
|
||||||
253
|
|
||||||
251
|
|
||||||
247
|
|
||||||
239
|
|
||||||
222
|
|
||||||
188
|
|
||||||
121
|
|
||||||
243
|
|
||||||
230
|
|
||||||
205
|
|
||||||
155
|
|
||||||
55
|
|
||||||
110
|
|
||||||
221
|
|
||||||
187
|
|
||||||
119
|
|
||||||
238
|
|
||||||
220
|
|
||||||
185
|
|
||||||
114
|
|
||||||
229
|
|
||||||
202
|
|
||||||
149
|
|
||||||
42
|
|
||||||
84
|
|
||||||
169
|
|
||||||
82
|
|
||||||
165
|
|
||||||
74
|
|
||||||
148
|
|
||||||
40
|
|
||||||
81
|
|
||||||
162
|
|
||||||
68
|
|
||||||
137
|
|
||||||
18
|
|
||||||
37
|
|
||||||
75
|
|
||||||
150
|
|
||||||
45
|
|
||||||
90
|
|
||||||
180
|
|
||||||
104
|
|
||||||
209
|
|
||||||
163
|
|
||||||
70
|
|
||||||
140
|
|
||||||
25
|
|
||||||
51
|
|
||||||
103
|
|
||||||
206
|
|
||||||
156
|
|
||||||
57
|
|
||||||
115
|
|
||||||
231
|
|
||||||
207
|
|
||||||
158
|
|
||||||
60
|
|
||||||
120
|
|
||||||
241
|
|
||||||
227
|
|
||||||
198
|
|
||||||
141
|
|
||||||
27
|
|
||||||
54
|
|
||||||
108
|
|
||||||
216
|
|
||||||
176
|
|
||||||
97
|
|
||||||
194
|
|
||||||
132
|
|
||||||
8
|
|
||||||
17
|
|
||||||
34
|
|
||||||
69
|
|
||||||
139
|
|
||||||
23
|
|
||||||
46
|
|
||||||
93
|
|
||||||
186
|
|
||||||
117
|
|
||||||
235
|
|
||||||
215
|
|
||||||
175
|
|
||||||
94
|
|
||||||
189
|
|
||||||
123
|
|
||||||
246
|
|
||||||
237
|
|
||||||
219
|
|
||||||
183
|
|
||||||
111
|
|
||||||
223
|
|
||||||
190
|
|
||||||
124
|
|
||||||
248
|
|
||||||
240
|
|
||||||
225
|
|
||||||
195
|
|
||||||
134
|
|
||||||
13
|
|
||||||
26
|
|
||||||
52
|
|
||||||
105
|
|
||||||
211
|
|
||||||
166
|
|
||||||
77
|
|
||||||
154
|
|
||||||
53
|
|
||||||
107
|
|
||||||
214
|
|
||||||
173
|
|
||||||
91
|
|
||||||
182
|
|
||||||
109
|
|
||||||
218
|
|
||||||
181
|
|
||||||
106
|
|
||||||
212
|
|
||||||
168
|
|
||||||
80
|
|
||||||
160
|
|
||||||
65
|
|
||||||
130
|
|
||||||
4
|
|
||||||
9
|
|
||||||
19
|
|
||||||
39
|
|
||||||
78
|
|
||||||
157
|
|
||||||
59
|
|
||||||
118
|
|
||||||
236
|
|
||||||
217
|
|
||||||
178
|
|
||||||
100
|
|
||||||
201
|
|
||||||
146
|
|
||||||
36
|
|
||||||
73
|
|
||||||
147
|
|
||||||
38
|
|
||||||
76
|
|
||||||
152
|
|
||||||
48
|
|
||||||
96
|
|
||||||
192
|
|
||||||
129
|
|
||||||
3
|
|
||||||
7
|
|
||||||
14
|
|
||||||
29
|
|
||||||
58
|
|
||||||
116
|
|
||||||
233
|
|
||||||
210
|
|
||||||
164
|
|
||||||
72
|
|
||||||
145
|
|
||||||
35
|
|
||||||
71
|
|
||||||
142
|
|
||||||
28
|
|
||||||
56
|
|
||||||
113
|
|
||||||
226
|
|
||||||
196
|
|
||||||
136
|
|
||||||
16
|
|
||||||
32
|
|
||||||
64
|
|
||||||
128
|
|
||||||
1
|
|
||||||
2
|
|
|
@ -1,33 +0,0 @@
|
||||||
561
|
|
||||||
1105
|
|
||||||
1729
|
|
||||||
2465
|
|
||||||
2821
|
|
||||||
6601
|
|
||||||
8911
|
|
||||||
10585
|
|
||||||
15841
|
|
||||||
29341
|
|
||||||
41041
|
|
||||||
46657
|
|
||||||
52633
|
|
||||||
62745
|
|
||||||
63973
|
|
||||||
75361
|
|
||||||
101101
|
|
||||||
115921
|
|
||||||
126217
|
|
||||||
162401
|
|
||||||
172081
|
|
||||||
188461
|
|
||||||
252601
|
|
||||||
278545
|
|
||||||
294409
|
|
||||||
314821
|
|
||||||
334153
|
|
||||||
340561
|
|
||||||
399001
|
|
||||||
410041
|
|
||||||
449065
|
|
||||||
488881
|
|
||||||
512461
|
|
|
@ -1,114 +0,0 @@
|
||||||
4
|
|
||||||
6
|
|
||||||
8
|
|
||||||
9
|
|
||||||
10
|
|
||||||
12
|
|
||||||
14
|
|
||||||
15
|
|
||||||
16
|
|
||||||
18
|
|
||||||
20
|
|
||||||
21
|
|
||||||
22
|
|
||||||
24
|
|
||||||
25
|
|
||||||
26
|
|
||||||
27
|
|
||||||
28
|
|
||||||
30
|
|
||||||
32
|
|
||||||
33
|
|
||||||
34
|
|
||||||
35
|
|
||||||
36
|
|
||||||
38
|
|
||||||
39
|
|
||||||
40
|
|
||||||
42
|
|
||||||
44
|
|
||||||
45
|
|
||||||
46
|
|
||||||
48
|
|
||||||
49
|
|
||||||
50
|
|
||||||
51
|
|
||||||
52
|
|
||||||
54
|
|
||||||
55
|
|
||||||
56
|
|
||||||
57
|
|
||||||
58
|
|
||||||
60
|
|
||||||
62
|
|
||||||
63
|
|
||||||
64
|
|
||||||
65
|
|
||||||
66
|
|
||||||
68
|
|
||||||
69
|
|
||||||
70
|
|
||||||
72
|
|
||||||
74
|
|
||||||
75
|
|
||||||
76
|
|
||||||
77
|
|
||||||
78
|
|
||||||
80
|
|
||||||
81
|
|
||||||
82
|
|
||||||
84
|
|
||||||
85
|
|
||||||
86
|
|
||||||
87
|
|
||||||
88
|
|
||||||
90
|
|
||||||
91
|
|
||||||
92
|
|
||||||
93
|
|
||||||
94
|
|
||||||
95
|
|
||||||
96
|
|
||||||
98
|
|
||||||
99
|
|
||||||
100
|
|
||||||
102
|
|
||||||
104
|
|
||||||
105
|
|
||||||
106
|
|
||||||
108
|
|
||||||
110
|
|
||||||
111
|
|
||||||
112
|
|
||||||
114
|
|
||||||
115
|
|
||||||
116
|
|
||||||
117
|
|
||||||
118
|
|
||||||
119
|
|
||||||
120
|
|
||||||
121
|
|
||||||
122
|
|
||||||
123
|
|
||||||
124
|
|
||||||
125
|
|
||||||
126
|
|
||||||
128
|
|
||||||
129
|
|
||||||
130
|
|
||||||
132
|
|
||||||
133
|
|
||||||
134
|
|
||||||
135
|
|
||||||
136
|
|
||||||
138
|
|
||||||
140
|
|
||||||
141
|
|
||||||
142
|
|
||||||
143
|
|
||||||
144
|
|
||||||
145
|
|
||||||
146
|
|
||||||
147
|
|
||||||
148
|
|
||||||
150
|
|
|
@ -1,168 +0,0 @@
|
||||||
2
|
|
||||||
3
|
|
||||||
5
|
|
||||||
7
|
|
||||||
11
|
|
||||||
13
|
|
||||||
17
|
|
||||||
19
|
|
||||||
23
|
|
||||||
29
|
|
||||||
31
|
|
||||||
37
|
|
||||||
41
|
|
||||||
43
|
|
||||||
47
|
|
||||||
53
|
|
||||||
59
|
|
||||||
61
|
|
||||||
67
|
|
||||||
71
|
|
||||||
73
|
|
||||||
79
|
|
||||||
83
|
|
||||||
89
|
|
||||||
97
|
|
||||||
101
|
|
||||||
103
|
|
||||||
107
|
|
||||||
109
|
|
||||||
113
|
|
||||||
127
|
|
||||||
131
|
|
||||||
137
|
|
||||||
139
|
|
||||||
149
|
|
||||||
151
|
|
||||||
157
|
|
||||||
163
|
|
||||||
167
|
|
||||||
173
|
|
||||||
179
|
|
||||||
181
|
|
||||||
191
|
|
||||||
193
|
|
||||||
197
|
|
||||||
199
|
|
||||||
211
|
|
||||||
223
|
|
||||||
227
|
|
||||||
229
|
|
||||||
233
|
|
||||||
239
|
|
||||||
241
|
|
||||||
251
|
|
||||||
257
|
|
||||||
263
|
|
||||||
269
|
|
||||||
271
|
|
||||||
277
|
|
||||||
281
|
|
||||||
283
|
|
||||||
293
|
|
||||||
307
|
|
||||||
311
|
|
||||||
313
|
|
||||||
317
|
|
||||||
331
|
|
||||||
337
|
|
||||||
347
|
|
||||||
349
|
|
||||||
353
|
|
||||||
359
|
|
||||||
367
|
|
||||||
373
|
|
||||||
379
|
|
||||||
383
|
|
||||||
389
|
|
||||||
397
|
|
||||||
401
|
|
||||||
409
|
|
||||||
419
|
|
||||||
421
|
|
||||||
431
|
|
||||||
433
|
|
||||||
439
|
|
||||||
443
|
|
||||||
449
|
|
||||||
457
|
|
||||||
461
|
|
||||||
463
|
|
||||||
467
|
|
||||||
479
|
|
||||||
487
|
|
||||||
491
|
|
||||||
499
|
|
||||||
503
|
|
||||||
509
|
|
||||||
521
|
|
||||||
523
|
|
||||||
541
|
|
||||||
547
|
|
||||||
557
|
|
||||||
563
|
|
||||||
569
|
|
||||||
571
|
|
||||||
577
|
|
||||||
587
|
|
||||||
593
|
|
||||||
599
|
|
||||||
601
|
|
||||||
607
|
|
||||||
613
|
|
||||||
617
|
|
||||||
619
|
|
||||||
631
|
|
||||||
641
|
|
||||||
643
|
|
||||||
647
|
|
||||||
653
|
|
||||||
659
|
|
||||||
661
|
|
||||||
673
|
|
||||||
677
|
|
||||||
683
|
|
||||||
691
|
|
||||||
701
|
|
||||||
709
|
|
||||||
719
|
|
||||||
727
|
|
||||||
733
|
|
||||||
739
|
|
||||||
743
|
|
||||||
751
|
|
||||||
757
|
|
||||||
761
|
|
||||||
769
|
|
||||||
773
|
|
||||||
787
|
|
||||||
797
|
|
||||||
809
|
|
||||||
811
|
|
||||||
821
|
|
||||||
823
|
|
||||||
827
|
|
||||||
829
|
|
||||||
839
|
|
||||||
853
|
|
||||||
857
|
|
||||||
859
|
|
||||||
863
|
|
||||||
877
|
|
||||||
881
|
|
||||||
883
|
|
||||||
887
|
|
||||||
907
|
|
||||||
911
|
|
||||||
919
|
|
||||||
929
|
|
||||||
937
|
|
||||||
941
|
|
||||||
947
|
|
||||||
953
|
|
||||||
967
|
|
||||||
971
|
|
||||||
977
|
|
||||||
983
|
|
||||||
991
|
|
||||||
997
|
|
|
@ -1,9 +0,0 @@
|
||||||
3
|
|
||||||
5
|
|
||||||
17
|
|
||||||
257
|
|
||||||
65537
|
|
||||||
4294967297
|
|
||||||
18446744073709551617
|
|
||||||
340282366920938463463374607431768211457
|
|
||||||
115792089237316195423570985008687907853269984665640564039457584007913129639937
|
|
|
@ -1,20 +0,0 @@
|
||||||
3
|
|
||||||
7
|
|
||||||
31
|
|
||||||
127
|
|
||||||
2047
|
|
||||||
8191
|
|
||||||
131071
|
|
||||||
524287
|
|
||||||
8388607
|
|
||||||
536870911
|
|
||||||
2147483647
|
|
||||||
137438953471
|
|
||||||
2199023255551
|
|
||||||
8796093022207
|
|
||||||
140737488355327
|
|
||||||
9007199254740991
|
|
||||||
576460752303423487
|
|
||||||
2305843009213693951
|
|
||||||
147573952589676412927
|
|
||||||
2361183241434822606847
|
|
Before Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 2.4 KiB |
|
@ -1 +0,0 @@
|
||||||
([58, 56, 54, 57, 60, 59, 62, 61, 63, 65, 64, 66, 67, 254, 68, 69, 255, 70, 253, 71, 72, 74, 252, 73, 251, 250, 75, 249, 76, 77], [(224, 4327), (221, 4138), (223, 4057), (222, 3987), (225, 3713), (220, 3554), (209, 3516), (207, 3476), (208, 3424), (219, 3360)])
|
|
|
@ -1,54 +0,0 @@
|
||||||
0
|
|
||||||
1
|
|
||||||
3
|
|
||||||
6
|
|
||||||
10
|
|
||||||
15
|
|
||||||
21
|
|
||||||
28
|
|
||||||
36
|
|
||||||
45
|
|
||||||
55
|
|
||||||
66
|
|
||||||
78
|
|
||||||
91
|
|
||||||
105
|
|
||||||
120
|
|
||||||
136
|
|
||||||
153
|
|
||||||
171
|
|
||||||
190
|
|
||||||
210
|
|
||||||
231
|
|
||||||
253
|
|
||||||
276
|
|
||||||
300
|
|
||||||
325
|
|
||||||
351
|
|
||||||
378
|
|
||||||
406
|
|
||||||
435
|
|
||||||
465
|
|
||||||
496
|
|
||||||
528
|
|
||||||
561
|
|
||||||
595
|
|
||||||
630
|
|
||||||
666
|
|
||||||
703
|
|
||||||
741
|
|
||||||
780
|
|
||||||
820
|
|
||||||
861
|
|
||||||
903
|
|
||||||
946
|
|
||||||
990
|
|
||||||
1035
|
|
||||||
1081
|
|
||||||
1128
|
|
||||||
1176
|
|
||||||
1225
|
|
||||||
1275
|
|
||||||
1326
|
|
||||||
1378
|
|
||||||
1431
|
|
Before Width: | Height: | Size: 5.4 MiB |
Before Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,109 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.2 $"
|
|
||||||
__date__ = "$Date: 2016/05/17 $"
|
|
||||||
__revision__ = "$Date: 2017/01/18 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from stegano import exifHeader
|
|
||||||
|
|
||||||
|
|
||||||
class TestEXIFHeader(unittest.TestCase):
|
|
||||||
def test_hide_empty_message(self):
|
|
||||||
"""Test hiding the empty string."""
|
|
||||||
exifHeader.hide(
|
|
||||||
"./tests/sample-files/20160505T130442.jpg", "./image.jpg", secret_message=""
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_message = exifHeader.reveal("./image.jpg")
|
|
||||||
|
|
||||||
self.assertEqual(b"", clear_message)
|
|
||||||
|
|
||||||
def test_hide_and_reveal(self):
|
|
||||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
|
||||||
|
|
||||||
for message in messages_to_hide:
|
|
||||||
exifHeader.hide(
|
|
||||||
"./tests/sample-files/20160505T130442.jpg",
|
|
||||||
"./image.jpg",
|
|
||||||
secret_message=message,
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_message = exifHeader.reveal("./image.jpg")
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message.decode())
|
|
||||||
|
|
||||||
def test_with_image_without_exif_data(self):
|
|
||||||
exifHeader.hide(
|
|
||||||
"./tests/sample-files/Lenna.jpg", "./image.jpg", secret_message=""
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_message = exifHeader.reveal("./image.jpg")
|
|
||||||
|
|
||||||
self.assertEqual(b"", clear_message)
|
|
||||||
|
|
||||||
def test_with_text_file(self):
|
|
||||||
text_file_to_hide = "./tests/sample-files/lorem_ipsum.txt"
|
|
||||||
with open(text_file_to_hide, "rb") as f:
|
|
||||||
message = f.read()
|
|
||||||
exifHeader.hide(
|
|
||||||
"./tests/sample-files/20160505T130442.jpg",
|
|
||||||
img_enc="./image.jpg",
|
|
||||||
secret_file=text_file_to_hide,
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_message = exifHeader.reveal("./image.jpg")
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_with_png_image(self):
|
|
||||||
exifHeader.hide(
|
|
||||||
"./tests/sample-files/Lenna.png", "./image.png", secret_message="Secret"
|
|
||||||
)
|
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
exifHeader.reveal("./image.png")
|
|
||||||
|
|
||||||
def test_with_bytes(self):
|
|
||||||
outputBytes = io.BytesIO()
|
|
||||||
message = b"Secret"
|
|
||||||
with open("./tests/sample-files/20160505T130442.jpg", "rb") as f:
|
|
||||||
exifHeader.hide(f, outputBytes, secret_message=message)
|
|
||||||
|
|
||||||
clear_message = exifHeader.reveal(outputBytes)
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
try:
|
|
||||||
os.unlink("./image.jpg")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
os.unlink("./image.png")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
|
@ -1,201 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.1 $"
|
|
||||||
__date__ = "$Date: 2017/03/01 $"
|
|
||||||
__revision__ = "$Date: 2017/03/01 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from stegano.lsb import generators
|
|
||||||
|
|
||||||
|
|
||||||
class TestGenerators(unittest.TestCase):
|
|
||||||
def test_identity(self):
|
|
||||||
"""Test the identity generator."""
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.identity(), 15)),
|
|
||||||
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_fibonacci(self):
|
|
||||||
"""Test the Fibonacci generator."""
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.fibonacci(), 20)),
|
|
||||||
(
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
5,
|
|
||||||
8,
|
|
||||||
13,
|
|
||||||
21,
|
|
||||||
34,
|
|
||||||
55,
|
|
||||||
89,
|
|
||||||
144,
|
|
||||||
233,
|
|
||||||
377,
|
|
||||||
610,
|
|
||||||
987,
|
|
||||||
1597,
|
|
||||||
2584,
|
|
||||||
4181,
|
|
||||||
6765,
|
|
||||||
10946,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_eratosthenes(self):
|
|
||||||
"""Test the Eratosthenes sieve."""
|
|
||||||
with open("./tests/expected-results/eratosthenes") as f:
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.eratosthenes(), 168)),
|
|
||||||
tuple(int(line) for line in f),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_composite(self):
|
|
||||||
"""Test the composite sieve."""
|
|
||||||
with open("./tests/expected-results/composite") as f:
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.composite(), 114)),
|
|
||||||
tuple(int(line) for line in f),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_fermat(self):
|
|
||||||
"""Test the Fermat generator."""
|
|
||||||
with open("./tests/expected-results/fermat") as f:
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.fermat(), 9)),
|
|
||||||
tuple(int(line) for line in f),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_triangular_numbers(self):
|
|
||||||
"""Test the Triangular numbers generator."""
|
|
||||||
with open("./tests/expected-results/triangular_numbers") as f:
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.triangular_numbers(), 54)),
|
|
||||||
tuple(int(line) for line in f),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_mersenne(self):
|
|
||||||
"""Test the Mersenne generator."""
|
|
||||||
with open("./tests/expected-results/mersenne") as f:
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.mersenne(), 20)),
|
|
||||||
tuple(int(line) for line in f),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_carmichael(self):
|
|
||||||
"""Test the Carmichael generator."""
|
|
||||||
with open("./tests/expected-results/carmichael") as f:
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.carmichael(), 33)),
|
|
||||||
tuple(int(line) for line in f),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_ackermann_slow(self):
|
|
||||||
"""Test the Ackermann set."""
|
|
||||||
with open("./tests/expected-results/ackermann") as f:
|
|
||||||
self.assertEqual(generators.ackermann_slow(3, 1), int(f.readline()))
|
|
||||||
self.assertEqual(generators.ackermann_slow(3, 2), int(f.readline()))
|
|
||||||
|
|
||||||
def test_ackermann_naive(self):
|
|
||||||
"""Test the Naive Ackermann generator"""
|
|
||||||
gen = generators.ackermann_naive(3)
|
|
||||||
next(gen)
|
|
||||||
with open("./tests/expected-results/ackermann") as f:
|
|
||||||
self.assertEqual(next(gen), int(f.readline()))
|
|
||||||
self.assertEqual(next(gen), int(f.readline()))
|
|
||||||
|
|
||||||
def test_ackermann_fast(self):
|
|
||||||
"""Test the Ackermann set."""
|
|
||||||
with open("./tests/expected-results/ackermann") as f:
|
|
||||||
self.assertEqual(generators.ackermann_fast(3, 1), int(f.readline()))
|
|
||||||
self.assertEqual(generators.ackermann_fast(3, 2), int(f.readline()))
|
|
||||||
self.assertEqual(generators.ackermann_fast(4, 1), int(f.readline()))
|
|
||||||
|
|
||||||
def test_ackermann(self):
|
|
||||||
"""Test the Ackermann generator"""
|
|
||||||
gen = generators.ackermann(3)
|
|
||||||
next(gen)
|
|
||||||
with open("./tests/expected-results/ackermann") as f:
|
|
||||||
self.assertEqual(next(gen), int(f.readline()))
|
|
||||||
self.assertEqual(next(gen), int(f.readline()))
|
|
||||||
|
|
||||||
def test_LFSR(self):
|
|
||||||
"""Test the LFSR generator"""
|
|
||||||
with open("./tests/expected-results/LFSR") as f:
|
|
||||||
self.assertEqual(
|
|
||||||
tuple(itertools.islice(generators.LFSR(2**8), 256)),
|
|
||||||
tuple(int(line) for line in f),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_shi_tomashi(self):
|
|
||||||
"""Test the Shi Tomashi generator"""
|
|
||||||
|
|
||||||
# The expected results are only for tests/sample-files/Montenach.png file and
|
|
||||||
# the below mentioned shi-tomashi configuration.
|
|
||||||
# If the values below are changed,
|
|
||||||
# please ensure the tests/expected-results/shi_tomashi.txt
|
|
||||||
# is also appropriately modified
|
|
||||||
# Using the shi_tomashi_reconfigure static method
|
|
||||||
|
|
||||||
image = cv2.imread("tests/sample-files/Montenach.png")
|
|
||||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
||||||
corners = cv2.goodFeaturesToTrack(gray, 1000, 0.001, 10)
|
|
||||||
# Commented because min_distance argument of generators.shi_tomashi is now set
|
|
||||||
# to 10.0:
|
|
||||||
# corners = np.int0(corners)
|
|
||||||
corners = corners.reshape(corners.shape[0], -1)
|
|
||||||
test_file = np.loadtxt("tests/expected-results/shi_tomashi.txt")
|
|
||||||
test_file_reshaped = test_file.reshape(
|
|
||||||
int(test_file.shape[0]), int(test_file.shape[1])
|
|
||||||
)
|
|
||||||
res = np.testing.assert_allclose(corners, test_file_reshaped, rtol=1e-0, atol=0) # type: ignore
|
|
||||||
self.assertIsNone(res)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def shi_tomashi_reconfigure(
|
|
||||||
file_name: str,
|
|
||||||
max_corners: int = 1000,
|
|
||||||
quality: float = 0.001,
|
|
||||||
min_distance: int = 10,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Method to update/reconfigure Shi-Tomashi for various images and configuration
|
|
||||||
"""
|
|
||||||
image = cv2.imread(file_name)
|
|
||||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
||||||
corners = cv2.goodFeaturesToTrack(gray, max_corners, quality, min_distance)
|
|
||||||
# Commented because min_distance argument of generators.shi_tomashi is now set
|
|
||||||
# to 10.0:
|
|
||||||
# corners = np.int0(corners)
|
|
||||||
corners = corners.reshape(corners.shape[0], -1)
|
|
||||||
np.savetxt("tests/expected-results/shi_tomashi.txt", corners)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
|
@ -1,248 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.6 $"
|
|
||||||
__date__ = "$Date: 2016/04/13 $"
|
|
||||||
__revision__ = "$Date: 2022/01/04 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import unittest
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from stegano import lsb
|
|
||||||
from stegano.lsb import generators
|
|
||||||
|
|
||||||
|
|
||||||
class TestLSB(unittest.TestCase):
|
|
||||||
def test_hide_empty_message(self):
|
|
||||||
"""
|
|
||||||
Test hiding the empty string.
|
|
||||||
"""
|
|
||||||
with self.assertRaises(AssertionError):
|
|
||||||
lsb.hide("./tests/sample-files/Lenna.png", "", generators.eratosthenes())
|
|
||||||
|
|
||||||
def test_hide_and_reveal_without_generator(self):
|
|
||||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = lsb.hide("./tests/sample-files/Lenna.png", message)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal("./image.png")
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_hide_and_reveal_with_eratosthenes(self):
|
|
||||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna.png", message, generators.eratosthenes()
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal("./image.png", generators.eratosthenes())
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_hide_and_reveal_with_ackermann(self):
|
|
||||||
messages_to_hide = ["foo"]
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna.png", message, generators.ackermann(m=3)
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal("./image.png", generators.ackermann(m=3))
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_hide_and_reveal_with_ackermann_naive(self):
|
|
||||||
messages_to_hide = ["foo"]
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna.png",
|
|
||||||
message,
|
|
||||||
generators.ackermann_naive(m=2),
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal("./image.png", generators.ackermann_naive(m=2))
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_hide_and_reveal_with_mersenne(self):
|
|
||||||
messages_to_hide = ["f"]
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/Montenach.png",
|
|
||||||
message,
|
|
||||||
generators.mersenne(),
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal("./image.png", generators.mersenne())
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_hide_and_reveal_with_shi_tomashi(self):
|
|
||||||
messages_to_hide = ["foo bar"]
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna.png",
|
|
||||||
message,
|
|
||||||
generators.shi_tomashi("./tests/sample-files/Lenna.png"),
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal(
|
|
||||||
"./image.png", generators.shi_tomashi("./tests/sample-files/Lenna.png")
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_hide_and_reveal_with_shift(self):
|
|
||||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna.png", message, generators.eratosthenes(), 4
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal("./image.png", generators.eratosthenes(), 4)
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_hide_and_reveal_UTF32LE(self):
|
|
||||||
messages_to_hide = "I love 🍕 and 🍫!"
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna.png",
|
|
||||||
messages_to_hide,
|
|
||||||
generators.eratosthenes(),
|
|
||||||
encoding="UTF-32LE",
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal(
|
|
||||||
"./image.png", generators.eratosthenes(), encoding="UTF-32LE"
|
|
||||||
)
|
|
||||||
self.assertEqual(messages_to_hide, clear_message)
|
|
||||||
|
|
||||||
def test_with_transparent_png(self):
|
|
||||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/transparent.png",
|
|
||||||
message,
|
|
||||||
generators.eratosthenes(),
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = lsb.reveal("./image.png", generators.eratosthenes())
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
@patch("builtins.input", return_value="y")
|
|
||||||
def test_manual_convert_rgb(self, input):
|
|
||||||
message_to_hide = "Hello World!"
|
|
||||||
lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna-grayscale.png",
|
|
||||||
message_to_hide,
|
|
||||||
generators.eratosthenes(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch("builtins.input", return_value="n")
|
|
||||||
def test_refuse_convert_rgb(self, input):
|
|
||||||
message_to_hide = "Hello World!"
|
|
||||||
# lsb.hide(
|
|
||||||
# "./tests/sample-files/Lenna-grayscale.png",
|
|
||||||
# message_to_hide,
|
|
||||||
# generators.eratosthenes(),
|
|
||||||
# )
|
|
||||||
with self.assertRaisesRegex(Exception, "Not a RGB image."):
|
|
||||||
lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna-grayscale.png",
|
|
||||||
message_to_hide,
|
|
||||||
generators.eratosthenes(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_with_location_of_image_as_argument(self):
|
|
||||||
messages_to_hide = ["Hello World!"]
|
|
||||||
|
|
||||||
for message in messages_to_hide:
|
|
||||||
outputBytes = io.BytesIO()
|
|
||||||
bytes_image = lsb.hide(
|
|
||||||
"./tests/sample-files/20160505T130442.jpg",
|
|
||||||
message,
|
|
||||||
generators.identity(),
|
|
||||||
)
|
|
||||||
bytes_image.save(outputBytes, "PNG")
|
|
||||||
outputBytes.seek(0)
|
|
||||||
|
|
||||||
clear_message = lsb.reveal(outputBytes, generators.identity())
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_auto_convert_rgb(self):
|
|
||||||
message_to_hide = "Hello World!"
|
|
||||||
lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna-grayscale.png",
|
|
||||||
message_to_hide,
|
|
||||||
generators.eratosthenes(),
|
|
||||||
auto_convert_rgb=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_with_too_long_message(self):
|
|
||||||
with open("./tests/sample-files/lorem_ipsum.txt") as f:
|
|
||||||
message = f.read()
|
|
||||||
message += message * 2
|
|
||||||
with self.assertRaisesRegex(
|
|
||||||
Exception, "The message you want to hide is too long:"
|
|
||||||
):
|
|
||||||
lsb.hide("./tests/sample-files/Lenna.png", message, generators.identity())
|
|
||||||
|
|
||||||
def test_hide_and_reveal_with_bad_generator(self):
|
|
||||||
message_to_hide = "Hello World!"
|
|
||||||
secret = lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna.png", message_to_hide, generators.eratosthenes()
|
|
||||||
)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
with self.assertRaises(IndexError):
|
|
||||||
lsb.reveal("./image.png", generators.identity())
|
|
||||||
|
|
||||||
def test_with_unknown_generator(self):
|
|
||||||
message_to_hide = "Hello World!"
|
|
||||||
with self.assertRaises(AttributeError):
|
|
||||||
lsb.hide(
|
|
||||||
"./tests/sample-files/Lenna.png",
|
|
||||||
message_to_hide,
|
|
||||||
generators.unknown_generator(), # type: ignore
|
|
||||||
)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
try:
|
|
||||||
os.unlink("./image.png")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
|
@ -1,64 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.1 $"
|
|
||||||
__date__ = "$Date: 2016/05/19 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import os
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from stegano import red
|
|
||||||
|
|
||||||
|
|
||||||
class TestRed(unittest.TestCase):
|
|
||||||
def test_hide_empty_message(self):
|
|
||||||
"""
|
|
||||||
Test hiding the empty string.
|
|
||||||
"""
|
|
||||||
with self.assertRaises(AssertionError):
|
|
||||||
red.hide("./tests/sample-files/Lenna.png", "")
|
|
||||||
|
|
||||||
def test_hide_and_reveal(self):
|
|
||||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
|
||||||
|
|
||||||
for message in messages_to_hide:
|
|
||||||
secret = red.hide("./tests/sample-files/Lenna.png", message)
|
|
||||||
secret.save("./image.png")
|
|
||||||
|
|
||||||
clear_message = red.reveal("./image.png")
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_with_too_long_message(self):
|
|
||||||
with open("./tests/sample-files/lorem_ipsum.txt") as f:
|
|
||||||
message = f.read()
|
|
||||||
with self.assertRaises(AssertionError):
|
|
||||||
red.hide("./tests/sample-files/Lenna.png", message)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
try:
|
|
||||||
os.unlink("./image.png")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
|
@ -1,66 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.9.4 $"
|
|
||||||
__date__ = "$Date: 2019/06/06 $"
|
|
||||||
__revision__ = "$Date: 2019/06/06 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from PIL import Image, ImageChops
|
|
||||||
|
|
||||||
from stegano import lsb
|
|
||||||
from stegano.steganalysis import parity, statistics
|
|
||||||
|
|
||||||
|
|
||||||
class TestSteganalysis(unittest.TestCase):
|
|
||||||
def test_parity(self):
|
|
||||||
"""Test stegano.steganalysis.parity"""
|
|
||||||
text_file_to_hide = "./tests/sample-files/lorem_ipsum.txt"
|
|
||||||
with open(text_file_to_hide) as f:
|
|
||||||
message = f.read()
|
|
||||||
secret = lsb.hide("./tests/sample-files/Lenna.png", message)
|
|
||||||
analysis = parity.steganalyse(secret)
|
|
||||||
target = Image.open("./tests/expected-results/parity.png")
|
|
||||||
diff = ImageChops.difference(target, analysis).getbbox()
|
|
||||||
self.assertTrue(diff is None)
|
|
||||||
|
|
||||||
def test_parity_rgba(self):
|
|
||||||
"""Test that stegano.steganalysis.parity works with RGBA images"""
|
|
||||||
img = Image.open("./tests/sample-files/transparent.png")
|
|
||||||
analysis = parity.steganalyse(img)
|
|
||||||
target = Image.open("./tests/expected-results/parity_rgba.png")
|
|
||||||
diff = ImageChops.difference(target, analysis).getbbox()
|
|
||||||
self.assertTrue(diff is None)
|
|
||||||
|
|
||||||
def test_statistics(self):
|
|
||||||
"""Test stegano.steganalysis.statistics"""
|
|
||||||
image = Image.open("./tests/sample-files/Lenna.png")
|
|
||||||
stats = str(statistics.steganalyse(image)) + "\n"
|
|
||||||
file = open("./tests/expected-results/statistics")
|
|
||||||
target = file.read()
|
|
||||||
file.close()
|
|
||||||
self.assertEqual(stats, target)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
|
@ -1,87 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.1 $"
|
|
||||||
__date__ = "$Date: 2017/02/22 $"
|
|
||||||
__revision__ = "$Date: 2017/02/22 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from stegano import tools
|
|
||||||
|
|
||||||
|
|
||||||
class TestTools(unittest.TestCase):
|
|
||||||
def test_a2bits(self):
|
|
||||||
bits = tools.a2bits("Hello World!")
|
|
||||||
self.assertEqual(
|
|
||||||
bits,
|
|
||||||
"010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_a2bits_list_UTF8(self):
|
|
||||||
list_of_bits = tools.a2bits_list("Hello World!")
|
|
||||||
self.assertEqual(
|
|
||||||
list_of_bits,
|
|
||||||
[
|
|
||||||
"01001000",
|
|
||||||
"01100101",
|
|
||||||
"01101100",
|
|
||||||
"01101100",
|
|
||||||
"01101111",
|
|
||||||
"00100000",
|
|
||||||
"01010111",
|
|
||||||
"01101111",
|
|
||||||
"01110010",
|
|
||||||
"01101100",
|
|
||||||
"01100100",
|
|
||||||
"00100001",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_a2bits_list_UTF32LE(self):
|
|
||||||
list_of_bits = tools.a2bits_list("Hello World!", "UTF-32LE")
|
|
||||||
self.assertEqual(
|
|
||||||
list_of_bits,
|
|
||||||
[
|
|
||||||
"00000000000000000000000001001000",
|
|
||||||
"00000000000000000000000001100101",
|
|
||||||
"00000000000000000000000001101100",
|
|
||||||
"00000000000000000000000001101100",
|
|
||||||
"00000000000000000000000001101111",
|
|
||||||
"00000000000000000000000000100000",
|
|
||||||
"00000000000000000000000001010111",
|
|
||||||
"00000000000000000000000001101111",
|
|
||||||
"00000000000000000000000001110010",
|
|
||||||
"00000000000000000000000001101100",
|
|
||||||
"00000000000000000000000001100100",
|
|
||||||
"00000000000000000000000000100001",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_n_at_a_time(self):
|
|
||||||
result = tools.n_at_a_time([1, 2, 3, 4, 5], 2, "X")
|
|
||||||
self.assertEqual(list(result), [(1, 2), (3, 4), (5, "X")])
|
|
||||||
|
|
||||||
def test_binary2base64(self):
|
|
||||||
with open("./tests/expected-results/binary2base64") as f:
|
|
||||||
expected_value = f.read()
|
|
||||||
value = tools.binary2base64("tests/sample-files/free-software-song.ogg")
|
|
||||||
self.assertEqual(expected_value, value)
|
|
|
@ -1,62 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# Stegano - Stegano is a pure Python steganography module.
|
|
||||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
|
||||||
#
|
|
||||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
__author__ = "Cedric Bonhomme"
|
|
||||||
__version__ = "$Revision: 0.1 $"
|
|
||||||
__date__ = "$Date: 2016/05/19 $"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
|
|
||||||
import os
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from stegano import wav
|
|
||||||
|
|
||||||
|
|
||||||
class TestWav(unittest.TestCase):
|
|
||||||
def test_hide_empty_message(self):
|
|
||||||
"""
|
|
||||||
Test hiding the empty string.
|
|
||||||
"""
|
|
||||||
with self.assertRaises(AssertionError):
|
|
||||||
wav.hide("./tests/sample-files/free-software-song.wav", "", "./audio.wav")
|
|
||||||
|
|
||||||
def test_hide_and_reveal(self):
|
|
||||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
|
||||||
|
|
||||||
for message in messages_to_hide:
|
|
||||||
wav.hide("./tests/sample-files/free-software-song.wav", message, "./audio.wav")
|
|
||||||
clear_message = wav.reveal("./audio.wav")
|
|
||||||
|
|
||||||
self.assertEqual(message, clear_message)
|
|
||||||
|
|
||||||
def test_with_too_long_message(self):
|
|
||||||
with open("./tests/sample-files/lorem_ipsum.txt") as f:
|
|
||||||
message = f.read()
|
|
||||||
with self.assertRaises(AssertionError):
|
|
||||||
wav.hide("./tests/sample-files/free-software-song.wav", message, "./audio.wav")
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
try:
|
|
||||||
os.unlink("./audio.wav")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|