mirror of
https://github.com/cedricbonhomme/Stegano.git
synced 2025-05-17 10:58:32 +02:00
Compare commits
No commits in common. "master" and "v0.10.1" have entirely different histories.
47 changed files with 1717 additions and 1759 deletions
42
.builds/debian.yml
Normal file
42
.builds/debian.yml
Normal file
|
@ -0,0 +1,42 @@
|
|||
image: debian/bullseye
|
||||
sources:
|
||||
- https://git.sr.ht/~cedric/stegano
|
||||
packages:
|
||||
- python3
|
||||
- python3-dev
|
||||
- python3-pip
|
||||
- python3-setuptools
|
||||
- python3-venv
|
||||
- rustc
|
||||
- cargo
|
||||
- libffi-dev
|
||||
- libssl-dev
|
||||
- libjpeg-dev
|
||||
- libpng-dev
|
||||
- libfreetype6-dev
|
||||
- ffmpeg
|
||||
- libsm6
|
||||
- libxext6
|
||||
environment:
|
||||
project: stegano
|
||||
tasks:
|
||||
- dependencies: |
|
||||
pip3 install --user poetry
|
||||
export PATH="$PATH:/home/build/.local/bin"
|
||||
cd ${project}
|
||||
poetry install
|
||||
- lint: |
|
||||
export PATH="$PATH:/home/build/.local/bin"
|
||||
cd ${project}
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings.
|
||||
poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- test: |
|
||||
export PATH="$PATH:/home/build/.local/bin"
|
||||
cd ${project}
|
||||
poetry run nose2 -v --pretty-assert
|
||||
- typecheck: |
|
||||
export PATH="$PATH:/home/build/.local/bin"
|
||||
cd ${project}
|
||||
poetry run mypy stegano
|
42
.github/workflows/pythonapp.yaml
vendored
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=15 --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
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
|
|
@ -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"]
|
||||
- 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.7.3
|
||||
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
|
200
CHANGELOG.md
200
CHANGELOG.md
|
@ -1,251 +1,199 @@
|
|||
## Release History
|
||||
|
||||
### 1.0.1 (2025-05-03)
|
||||
|
||||
- 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.
|
||||
* 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
|
||||
* 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.
|
||||
* Stegano now uses Pillow 8.3.0.
|
||||
|
||||
|
||||
### 0.9.8 (2019-12-20)
|
||||
|
||||
- Stegano is now using poetry;
|
||||
- minor improvements to the command line.
|
||||
* Stegano is now using poetry;
|
||||
* minor improvements to the command line.
|
||||
|
||||
|
||||
### 0.9.7 (2019-10-27)
|
||||
|
||||
- fixed markdown of the previous release.
|
||||
* fixed markdown of the previous release.
|
||||
|
||||
|
||||
### 0.9.6 (2019-10-27)
|
||||
|
||||
- fixed markdown of the previous release;
|
||||
* 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
|
||||
* updated dependencies;
|
||||
* home page of the project is now: https://git.sr.ht/~cedric/Stegano
|
||||
|
||||
|
||||
### 0.9.4 (2019-06-05)
|
||||
|
||||
- new: Implemented LFSR generator (with tests and CLI)
|
||||
* new: Implemented LFSR generator (with tests and CLI)
|
||||
([PR #27](https://github.com/cedricbonhomme/Stegano/pull/27))
|
||||
- new: Implemented Ackermann generators CLI interface
|
||||
* new: Implemented Ackermann generators CLI interface
|
||||
([PR #26](https://github.com/cedricbonhomme/Stegano/pull/26))
|
||||
- new: The Ackermann functions are not actual generators
|
||||
* new: The Ackermann functions are not actual generators
|
||||
([#24](https://github.com/cedricbonhomme/Stegano/issues/24))
|
||||
- new: add a shift parameter for the lsbmodule
|
||||
* new: add a shift parameter for the lsbmodule
|
||||
([#25](https://github.com/cedricbonhomme/Stegano/issues/25))
|
||||
- fix: lsbset.hide cause .png transparent area lost
|
||||
* 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
|
||||
* 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.
|
||||
* 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
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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
|
||||
* 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.
|
||||
* 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.
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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
|
||||
* 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).
|
||||
* 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),
|
||||
* 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.
|
||||
* 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
|
||||
* 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.
|
||||
|
@ -253,66 +201,66 @@
|
|||
|
||||
### 0.5.5 (2016-08-03)
|
||||
|
||||
- bugfix: Incorrect padding size in `base642string` in tools.base642binary().
|
||||
* 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
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* minor improvements and bug fixes;
|
||||
* added unit tests for the slsb and slsbset modules.
|
||||
|
||||
|
||||
### 0.5 (2016-03-18)
|
||||
|
||||
- management of greyscale images.
|
||||
* management of greyscale images.
|
||||
|
||||
|
||||
### 0.4.6 (2016-03-12)
|
||||
|
||||
- bugfix when the length of the message to hide is not divisible by 3,
|
||||
* 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.
|
||||
* bugfix.
|
||||
|
||||
|
||||
### 0.4.4 (2015-12-23)
|
||||
|
||||
- new project home page;
|
||||
- minor updated to the documentation.
|
||||
* 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*.
|
||||
* bug fixes for Python 3;
|
||||
* bug fixes in the scripts in *./bin*.
|
||||
|
||||
|
||||
### 0.4.2 (2015-10-05)
|
||||
|
||||
- first stable release on PypI.
|
||||
* first stable release on PypI.
|
||||
|
||||
|
||||
### 0.4 (2012-01-02)
|
||||
|
@ -326,16 +274,16 @@ 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;
|
||||
* 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ô.
|
||||
* new documentation (reStructuredText) comes with Stéganô.
|
||||
|
||||
|
||||
### 0.2 (2011-03-24)
|
||||
|
||||
- this release introduces some bugfixes and a major speed improvement of the
|
||||
* 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
|
||||
* a new technique for hiding/revealing a message in a JPEG picture by using the
|
||||
description field of the image is provided.
|
||||
|
|
|
@ -9,13 +9,11 @@
|
|||
|
||||
- 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
|
||||
- Nejdet Çağdaş Yücesoy <nejdetyucesoy@gmail.com>
|
||||
- panni <panni@fragstore.net>
|
||||
- Peter Justin <peter@peterjustin.me>
|
||||
- Christophe Goessen - https://github.com/cgoessen
|
||||
- thundersparkf - https://github.com/thundersparkf
|
||||
- Mickaël Schoentgen <mschoentgen@nuxeo.com>
|
||||
|
||||
And thank you to the testers!
|
||||
|
|
24
README.md
24
README.md
|
@ -1,8 +1,9 @@
|
|||
# Stegano
|
||||
|
||||
[](https://github.com/cedricbonhomme/Stegano/actions?query=workflow%3A%22Python+application%22)
|
||||
[](https://builds.sr.ht/~cedric/stegano)
|
||||
|
||||
[Stegano](https://github.com/cedricbonhomme/Stegano), a pure Python Steganography
|
||||
|
||||
[Stegano](https://sr.ht/~cedric/stegano), a pure Python Steganography
|
||||
module.
|
||||
|
||||
Steganography is the art and science of writing hidden messages in such a way
|
||||
|
@ -11,6 +12,9 @@ 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.
|
||||
|
||||
For reporting issues, visit the tracker here:
|
||||
https://todo.sr.ht/~cedric/stegano
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -64,7 +68,7 @@ Secret Message
|
|||
### Hide the message with the Sieve of Eratosthenes
|
||||
|
||||
```bash
|
||||
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m 'Secret Message' --generator eratosthenes -o Lena2.png
|
||||
$ stegano-lsb-set 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
|
||||
|
@ -93,23 +97,11 @@ Contributions are welcome. If you want to contribute to Stegano I highly
|
|||
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)
|
||||
Copyright (C) 2010-2021 [Cédric Bonhomme](https://www.cedricbonhomme.org)
|
||||
|
||||
For more information, [the list of authors and contributors](CONTRIBUTORS.md) is available.
|
||||
|
|
1
bin/__init__.py
Normal file
1
bin/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
140
bin/lsb.py
Executable file
140
bin/lsb.py
Executable file
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2021 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.8 $"
|
||||
__date__ = "$Date: 2016/08/04 $"
|
||||
__revision__ = "$Date: 2019/06/01 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import argparse
|
||||
|
||||
try:
|
||||
from stegano import lsb
|
||||
except:
|
||||
print("Install Stegano: pipx install Stegano")
|
||||
|
||||
from stegano import tools
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog="stegano-lsb")
|
||||
subparsers = parser.add_subparsers(help="sub-command help", dest="command")
|
||||
|
||||
# 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.",
|
||||
)
|
||||
|
||||
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.",
|
||||
)
|
||||
|
||||
# Shift the message to hide
|
||||
parser_hide.add_argument(
|
||||
"-s", "--shift", dest="shift", default=0, help="Shift for the message to hide"
|
||||
)
|
||||
|
||||
# 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.",
|
||||
)
|
||||
parser_reveal.add_argument(
|
||||
"-o",
|
||||
dest="secret_binary",
|
||||
help="Output for the binary secret (Text or any binary file).",
|
||||
)
|
||||
# Shift the message to reveal
|
||||
parser_reveal.add_argument(
|
||||
"-s", "--shift", dest="shift", default=0, help="Shift for the reveal"
|
||||
)
|
||||
|
||||
arguments = parser.parse_args()
|
||||
|
||||
if arguments.command == "hide":
|
||||
if arguments.secret_message != None:
|
||||
secret = arguments.secret_message
|
||||
elif arguments.secret_file != None:
|
||||
secret = tools.binary2base64(arguments.secret_file)
|
||||
|
||||
img_encoded = lsb.hide(
|
||||
arguments.input_image_file, secret, arguments.encoding, int(arguments.shift)
|
||||
)
|
||||
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":
|
||||
secret = lsb.reveal(
|
||||
arguments.input_image_file, arguments.encoding, int(arguments.shift)
|
||||
)
|
||||
if arguments.secret_binary != None:
|
||||
data = tools.base642binary(secret)
|
||||
with open(arguments.secret_binary, "wb") as f:
|
||||
f.write(data)
|
||||
else:
|
||||
print(secret)
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
#
|
||||
|
@ -24,19 +26,18 @@ __revision__ = "$Date: 2019/06/04 $"
|
|||
__license__ = "GPLv3"
|
||||
|
||||
import inspect
|
||||
|
||||
import crayons
|
||||
|
||||
try:
|
||||
from stegano import lsb
|
||||
from stegano.lsb import generators
|
||||
except Exception:
|
||||
from stegano import lsbset
|
||||
from stegano.lsbset import generators
|
||||
except:
|
||||
print("Install stegano: pipx install Stegano")
|
||||
|
||||
import argparse
|
||||
|
||||
from stegano import tools
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
class ValidateGenerator(argparse.Action):
|
||||
def __call__(self, parser, args, values, option_string=None):
|
||||
|
@ -53,10 +54,8 @@ class ValidateGenerator(argparse.Action):
|
|||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog="stegano-lsb")
|
||||
subparsers = parser.add_subparsers(
|
||||
help="sub-command help", dest="command", required=True
|
||||
)
|
||||
parser = argparse.ArgumentParser(prog="stegano-lsb-set")
|
||||
subparsers = parser.add_subparsers(help="sub-command help", dest="command")
|
||||
|
||||
# Subparser: Hide
|
||||
parser_hide = subparsers.add_parser("hide", help="hide help")
|
||||
|
@ -75,7 +74,7 @@ def main():
|
|||
choices=tools.ENCODINGS.keys(),
|
||||
default="UTF-8",
|
||||
help="Specify the encoding of the message to hide."
|
||||
" UTF-8 (default) or UTF-32LE.",
|
||||
+ " UTF-8 (default) or UTF-32LE.",
|
||||
)
|
||||
|
||||
# Generator
|
||||
|
@ -85,12 +84,9 @@ def main():
|
|||
dest="generator_function",
|
||||
action=ValidateGenerator,
|
||||
nargs="*",
|
||||
required=False,
|
||||
default=None,
|
||||
required=True,
|
||||
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"
|
||||
)
|
||||
|
@ -130,21 +126,17 @@ def main():
|
|||
choices=tools.ENCODINGS.keys(),
|
||||
default="UTF-8",
|
||||
help="Specify the encoding of the message to reveal."
|
||||
" UTF-8 (default) or UTF-32LE.",
|
||||
+ " UTF-8 (default) or UTF-32LE.",
|
||||
)
|
||||
|
||||
# Generator
|
||||
parser_reveal.add_argument(
|
||||
"-g",
|
||||
"--generator",
|
||||
dest="generator_function",
|
||||
action=ValidateGenerator,
|
||||
nargs="*",
|
||||
required=False,
|
||||
required=True,
|
||||
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"
|
||||
)
|
||||
|
@ -155,14 +147,20 @@ def main():
|
|||
)
|
||||
|
||||
# Subparser: List generators
|
||||
subparsers.add_parser("list-generators", help="list-generators help")
|
||||
parser_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:
|
||||
arguments.generator_function[0]
|
||||
except AttributeError:
|
||||
print("You must specify the name of a generator.")
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
if arguments.generator_function[0] == "LFSR":
|
||||
# Compute the size of the image for use by the LFSR generator if needed
|
||||
|
@ -177,22 +175,18 @@ def main():
|
|||
else:
|
||||
generator = getattr(generators, arguments.generator_function[0])()
|
||||
|
||||
except AttributeError:
|
||||
print(f"Unknown generator: {arguments.generator_function}")
|
||||
except AttributeError as e:
|
||||
print("Unknown generator: {}".format(arguments.generator_function))
|
||||
exit(1)
|
||||
|
||||
if arguments.command == "hide":
|
||||
if arguments.secret_message is not None:
|
||||
if arguments.secret_message != 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,
|
||||
img_encoded = lsbset.hide(
|
||||
arguments.input_image_file, secret, generator, int(arguments.shift)
|
||||
)
|
||||
try:
|
||||
img_encoded.save(arguments.output_image_file)
|
||||
|
@ -202,18 +196,15 @@ def main():
|
|||
|
||||
elif arguments.command == "reveal":
|
||||
try:
|
||||
secret = lsb.reveal(
|
||||
encoded_image=arguments.input_image_file,
|
||||
generator=generator,
|
||||
shift=int(arguments.shift),
|
||||
encoding=arguments.encoding,
|
||||
secret = lsbset.reveal(
|
||||
arguments.input_image_file, generator, int(arguments.shift)
|
||||
)
|
||||
except IndexError:
|
||||
print("Impossible to detect message.")
|
||||
exit(0)
|
||||
if arguments.secret_binary is not None:
|
||||
if arguments.secret_binary != None:
|
||||
data = tools.base642binary(secret)
|
||||
with open(arguments.secret_binary, "wb") as f:
|
||||
with open(arguments.secret_binary, "w") as f:
|
||||
f.write(data)
|
||||
else:
|
||||
print(secret)
|
||||
|
@ -222,6 +213,6 @@ def main():
|
|||
all_generators = inspect.getmembers(generators, inspect.isfunction)
|
||||
for generator in all_generators:
|
||||
print("Generator id:")
|
||||
print(f" {crayons.green(generator[0], bold=True)}")
|
||||
print(" {}".format(crayons.green(generator[0], bold=True)))
|
||||
print("Desciption:")
|
||||
print(f" {generator[1].__doc__}")
|
||||
print(" {}".format(generator[1].__doc__))
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
#
|
||||
|
@ -22,13 +24,13 @@ __version__ = "$Revision: 0.7 $"
|
|||
__date__ = "$Date: 2016/08/25 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import argparse
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import argparse
|
||||
|
||||
try:
|
||||
from stegano.steganalysis import parity
|
||||
except Exception:
|
||||
except:
|
||||
print("Install Stegano: pipx install Stegano")
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
#
|
||||
|
@ -26,7 +28,7 @@ import argparse
|
|||
|
||||
try:
|
||||
from stegano import red
|
||||
except Exception:
|
||||
except:
|
||||
print("Install stegano: sudo pip install Stegano")
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
#
|
||||
|
@ -23,13 +25,13 @@ __date__ = "$Date: 2016/08/26 $"
|
|||
__revision__ = "$Date: 2016/08/26 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import argparse
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import argparse
|
||||
|
||||
try:
|
||||
from stegano.steganalysis import statistics
|
||||
except Exception:
|
||||
except:
|
||||
print("Install Stegano: sudo pip install Stegano")
|
||||
|
||||
|
19
docs/conf.py
19
docs/conf.py
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Stéganô documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed Jul 25 13:33:39 2012.
|
||||
|
@ -9,13 +10,19 @@
|
|||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
|
||||
# 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
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = []
|
||||
|
@ -33,8 +40,8 @@ source_suffix = ".rst"
|
|||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = "Stegano"
|
||||
copyright = "2010-2025, Cédric Bonhomme"
|
||||
project = u"Stegano"
|
||||
copyright = u"2010-2021, Cédric Bonhomme"
|
||||
author = "Cédric Bonhomme <cedric@cedricbonhomme.org>"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
@ -42,9 +49,9 @@ author = "Cédric Bonhomme <cedric@cedricbonhomme.org>"
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "0.11"
|
||||
version = "0.9"
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "0.11.0"
|
||||
release = "0.9.4"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -159,12 +166,12 @@ latex_engine = "pdflatex"
|
|||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
("index", "Stgan.tex", "Stegano Documentation", "Cédric Bonhomme", "howto"),
|
||||
("index", "Stgan.tex", u"Stegano Documentation", u"Cédric Bonhomme", "howto"),
|
||||
]
|
||||
|
||||
latex_show_urls = True
|
||||
latex_show_pagerefs = True
|
||||
|
||||
ADDITIONAL_PREAMBLE = r"""
|
||||
ADDITIONAL_PREAMBLE = """
|
||||
\setcounter{tocdepth}{3}
|
||||
"""
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
Presentation
|
||||
============
|
||||
|
||||
.. image:: https://builds.sr.ht/~cedric/stegano.svg
|
||||
:target: https://builds.sr.ht/~cedric/stegano
|
||||
|
||||
|
||||
Stegano_ is a pure Python steganography_ module.
|
||||
|
||||
Steganography is the art and science of writing hidden messages in such a way
|
||||
|
@ -26,7 +30,7 @@ Moreover some methods of steganalysis_ are provided:
|
|||
- steganalysis of LSB encoding in color images;
|
||||
- statistical steganalysis.
|
||||
|
||||
You can also use Stegano through a `Web service <https://github.com/cedricbonhomme/stegano-web>`_.
|
||||
You can also use Stegano through this `Web service <https://stegano-web.herokuapp.com>`_.
|
||||
Not all functionalities of Stegano are covered.
|
||||
|
||||
Requirements
|
||||
|
@ -49,7 +53,7 @@ Tutorial
|
|||
steganalysis
|
||||
|
||||
You can have a look at the
|
||||
`unit tests <https://github.com/cedricbonhomme/Stegano/tree/master/tests>`_.
|
||||
`unit tests <https://git.sr.ht/~cedric/stegano/tree/master/tests>`_.
|
||||
|
||||
|
||||
License
|
||||
|
@ -73,7 +77,7 @@ Contact
|
|||
|
||||
|
||||
.. _Python: https://www.python.org
|
||||
.. _Stegano: https://github.com/cedricbonhomme/Stegano
|
||||
.. _Stegano: https://sr.ht/~cedric/stegano
|
||||
.. _`Pillow`: https://pypi.python.org/pypi/Pillow
|
||||
.. _`piexif`: https://pypi.python.org/pypi/piexif
|
||||
.. _steganography: http://en.wikipedia.org/wiki/Steganography
|
||||
|
|
|
@ -12,4 +12,7 @@ If you want to retrieve the source code (with the unit tests):
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/cedricbonhomme/Stegano
|
||||
$ git clone https://git.sr.ht/~cedric/stegano
|
||||
|
||||
.. image:: https://builds.sr.ht/~cedric/stegano.svg
|
||||
:target: https://builds.sr.ht/~cedric/stegano
|
||||
|
|
|
@ -2,14 +2,14 @@ Using Stegano as a Python module
|
|||
================================
|
||||
|
||||
You can find more examples in the
|
||||
`unit tests directory <https://github.com/cedricbonhomme/Stegano/tree/master/tests>`_.
|
||||
`unit tests directory <https://git.sr.ht/~cedric/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
|
||||
Python 3.10.0 (default, Oct 17 2021, 09:02:57) [GCC 11.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!")
|
||||
|
@ -26,33 +26,28 @@ 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
|
||||
Python 3.10.0 (default, Oct 17 2021, 09:02:57) [GCC 11.2.0] on linux
|
||||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> from stegano import lsb
|
||||
>>> from stegano.lsb import generators
|
||||
>>> from stegano import lsbset
|
||||
>>> from stegano.lsbset 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 = lsbset.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())
|
||||
>>> message = lsbset.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
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "/home/cedric/projects/Stegano/stegano/lsbset/lsbset.py", line 111, in reveal
|
||||
for color in img_list[generated_number]:
|
||||
IndexError: list index out of range
|
||||
|
||||
# Decode with Eratosthenes
|
||||
>>> message = lsb.reveal("./image.png", generators.eratosthenes())
|
||||
>>> message = lsbset.reveal("./image.png", generators.eratosthenes())
|
||||
>>> message
|
||||
'Hello World!'
|
||||
|
||||
|
@ -112,7 +107,7 @@ 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
|
||||
Python 3.10.0 (default, Oct 17 2021, 09:02:57) [GCC 11.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",
|
||||
|
|
|
@ -12,34 +12,31 @@ Display help
|
|||
.. code-block:: bash
|
||||
|
||||
$ stegano-lsb --help
|
||||
usage: stegano-lsb [-h] {hide,reveal,list-generators} ...
|
||||
usage: stegano-lsb [-h] {hide,reveal} ...
|
||||
|
||||
positional arguments:
|
||||
{hide,reveal,list-generators}
|
||||
sub-command help
|
||||
{hide,reveal} sub-command help
|
||||
hide hide help
|
||||
reveal reveal help
|
||||
list-generators list-generators help
|
||||
|
||||
options:
|
||||
optional arguments:
|
||||
-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
|
||||
usage: stegano-lsb hide [-h] -i INPUT_IMAGE_FILE [-e {UTF-8,UTF-32LE}]
|
||||
(-m SECRET_MESSAGE | -f SECRET_FILE) -o
|
||||
OUTPUT_IMAGE_FILE
|
||||
|
||||
options:
|
||||
optional arguments:
|
||||
-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
|
||||
Specify the encoding of the message to hide. UTF-8
|
||||
(default) or UTF-32LE.
|
||||
-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
|
||||
|
@ -49,19 +46,18 @@ Display help
|
|||
.. 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]
|
||||
usage: stegano-lsb reveal [-h] -i INPUT_IMAGE_FILE [-e {UTF-8,UTF-32LE}]
|
||||
[-o SECRET_BINARY]
|
||||
|
||||
options:
|
||||
optional arguments:
|
||||
-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).
|
||||
Specify the encoding of the message to reveal. UTF-8
|
||||
(default) or UTF-32LE.
|
||||
-o SECRET_BINARY Output for the binary secret (Text or any binary
|
||||
file).
|
||||
|
||||
|
||||
Hide and reveal a text message
|
||||
|
@ -96,40 +92,63 @@ Hide and reveal a binary file
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
The command ``stegano-lsb-set``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Sets are used in order to select the pixels where the message will be hidden.
|
||||
|
||||
Hide and reveal a text message with set
|
||||
---------------------------------------
|
||||
Hide and reveal a text message
|
||||
------------------------------
|
||||
|
||||
.. 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
|
||||
$ stegano-lsb-set 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
|
||||
$ stegano-lsb-set reveal --generator mersenne -i ./surprise.png
|
||||
|
||||
# Try to reveal with fermat numbers
|
||||
$ stegano-lsb reveal --generator fermat -i ./surprise.png
|
||||
$ stegano-lsb-set reveal --generator fermat -i ./surprise.png
|
||||
|
||||
# Try to reveal with carmichael numbers
|
||||
$ stegano-lsb reveal --generator carmichael -i ./surprise.png
|
||||
$ stegano-lsb-set reveal --generator carmichael -i ./surprise.png
|
||||
|
||||
# Try to reveal with Sieve of Eratosthenes
|
||||
$ stegano-lsb reveal --generator eratosthenes -i ./surprise.png
|
||||
$ stegano-lsb-set reveal --generator eratosthenes -i ./surprise.png
|
||||
|
||||
An other example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Hide the message - LSB with a set defined by the identity function (f(x) = x).
|
||||
stegano-lsb-set hide -i ./tests/sample-files/Montenach.png --generator identity -m 'I like steganography.' -o ./enc-identity.png
|
||||
|
||||
# Hide the message - LSB only.
|
||||
stegano-lsb hide -i ./tests/sample-files/Montenach.png -m 'I like steganography.' -o ./enc.png
|
||||
|
||||
# Check if the two generated files are the same.
|
||||
sha1sum ./enc-identity.png ./enc.png
|
||||
|
||||
# The output of lsb is given to lsb-set.
|
||||
stegano-lsb-set reveal -i ./enc.png --generator identity
|
||||
|
||||
# The output of lsb-set is given to lsb.
|
||||
stegano-lsb reveal -i ./enc-identity.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``:
|
||||
In this case, simply use the optional arguments ``--shift``:
|
||||
|
||||
|
||||
.. 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
|
||||
stegano-lsb-set reveal -i ./tests/sample-files/Lenna.png --generator fibonacci --shift 7
|
||||
|
||||
|
||||
List all available generators
|
||||
|
@ -137,7 +156,7 @@ List all available generators
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ stegano-lsb list-generators
|
||||
$ stegano-lsb-set list-generators
|
||||
Generator id:
|
||||
ackermann
|
||||
Desciption:
|
||||
|
|
|
@ -7,7 +7,7 @@ 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.'
|
||||
stegano-lsb-set 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
|
||||
|
@ -16,4 +16,4 @@ Parity
|
|||
stegano-steganalysis-parity -i ./surprise.png -o ./surprise_st_secret.png
|
||||
|
||||
# Reveal with Sieve of Eratosthenes
|
||||
stegano-lsb reveal -i ./surprise.png --generator eratosthenes
|
||||
stegano-lsb-set reveal -i ./surprise.png --generator eratosthenes
|
||||
|
|
13
mypy.ini
Normal file
13
mypy.ini
Normal file
|
@ -0,0 +1,13 @@
|
|||
[mypy]
|
||||
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
|
1446
poetry.lock
generated
1446
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,93 +1,61 @@
|
|||
[build-system]
|
||||
requires = ["poetry-core>=2.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
|
||||
[project]
|
||||
[tool.poetry]
|
||||
name = "stegano"
|
||||
version = "1.0.1"
|
||||
version = "0.10.1"
|
||||
description = "A pure Python Steganography module."
|
||||
authors = [
|
||||
{name = "Cédric Bonhomme", email= "cedric@cedricbonhomme.org"}
|
||||
"Cédric Bonhomme <cedric@cedricbonhomme.org>"
|
||||
]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
readme = "README.md"
|
||||
keywords = ["Steganography", "Security", "Stegano"]
|
||||
|
||||
dynamic = ["classifiers"]
|
||||
homepage = "https://sr.ht/~cedric/stegano"
|
||||
repository = "https://git.sr.ht/~cedric/stegano"
|
||||
documentation = "https://stegano.readthedocs.io"
|
||||
|
||||
requires-python = ">=3.10,<4.0"
|
||||
dependencies = [
|
||||
"pillow (>=9.5,<11.0)",
|
||||
"piexif (>=1.1.3)",
|
||||
"crayons (>=0.4.0)",
|
||||
"opencv-python (>=4.8.1.78)"
|
||||
]
|
||||
keywords = ["steganography", "security", "stegano"]
|
||||
|
||||
[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",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"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/**/*",
|
||||
"bin/*"
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
stegano-lsb = "bin.lsb:main"
|
||||
stegano-lsb-set = "bin.lsbset:main"
|
||||
stegano-red = "bin.red:main"
|
||||
stegano-steganalysis-parity = "bin.parity:main"
|
||||
stegano-steganalysis-statistics = "bin.statistics:main"
|
||||
|
||||
[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.dependencies]
|
||||
python = ">=3.8,<3.11"
|
||||
pillow = "^8.2.0"
|
||||
piexif = "^1.1.3"
|
||||
crayons = "^0.4.0"
|
||||
opencv-python = "^4.5.4"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
mypy = "^0.910"
|
||||
flake8 = "^4.0.1"
|
||||
nose2 = "^0.10.0"
|
||||
Sphinx = "^4.2.0"
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
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|stegano.egg-info"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import exifHeader, lsb, red, steganalysis
|
||||
from . import red
|
||||
from . import exifHeader
|
||||
from . import lsb
|
||||
from . import lsbset
|
||||
|
||||
__all__ = ["red", "exifHeader", "lsb", "steganalysis"]
|
||||
from . import steganalysis
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .exifHeader import hide, reveal
|
||||
|
||||
__all__ = ["hide", "reveal"]
|
||||
from .exifHeader import *
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -24,7 +26,6 @@ __revision__ = "$Date: 2017/01/18 $"
|
|||
__license__ = "GPLv3"
|
||||
|
||||
import piexif
|
||||
|
||||
from stegano import tools
|
||||
|
||||
|
||||
|
@ -36,11 +37,11 @@ def hide(
|
|||
img_format=None,
|
||||
):
|
||||
"""Hide a message (string) in an image."""
|
||||
from base64 import b64encode
|
||||
from zlib import compress
|
||||
from base64 import b64encode
|
||||
|
||||
if secret_file is not None:
|
||||
with open(secret_file) as f:
|
||||
with open(secret_file, "r") as f:
|
||||
secret_message = f.read()
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .lsb import hide, reveal
|
||||
|
||||
__all__ = ["hide", "reveal"]
|
||||
from .lsb import *
|
||||
|
|
132
stegano/lsb/lsb.py
Normal file → Executable file
132
stegano/lsb/lsb.py
Normal file → Executable file
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -18,73 +20,111 @@
|
|||
# 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 $"
|
||||
__version__ = "$Revision: 0.4 $"
|
||||
__date__ = "$Date: 2016/08/04 $"
|
||||
__revision__ = "$Date: 2019/06/01 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
from typing import IO, Iterator, Union
|
||||
from typing import IO, Union
|
||||
|
||||
from stegano import tools
|
||||
|
||||
from .generators import identity
|
||||
|
||||
|
||||
def hide(
|
||||
image: Union[str, IO[bytes]],
|
||||
input_image: Union[str, IO[bytes]],
|
||||
message: str,
|
||||
generator: Union[None, Iterator[int]] = None,
|
||||
shift: int = 0,
|
||||
encoding: str = "UTF-8",
|
||||
shift: int = 0,
|
||||
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
|
||||
message_length = len(message)
|
||||
assert message_length != 0, "message length is zero"
|
||||
|
||||
if not generator:
|
||||
generator = identity()
|
||||
img = tools.open_image(input_image)
|
||||
|
||||
while shift != 0:
|
||||
next(generator)
|
||||
if img.mode not in ["RGB", "RGBA"]:
|
||||
if not auto_convert_rgb:
|
||||
print("The mode of the image is not RGB. Mode is {}".format(img.mode))
|
||||
answer = input("Convert the image to RGB ? [Y / n]\n") or "Y"
|
||||
if answer.lower() == "n":
|
||||
raise Exception("Not a RGB image.")
|
||||
img = img.convert("RGB")
|
||||
|
||||
encoded = img.copy()
|
||||
width, height = img.size
|
||||
index = 0
|
||||
|
||||
message = str(message_length) + ":" + str(message)
|
||||
message_bits = "".join(tools.a2bits_list(message, encoding))
|
||||
message_bits += "0" * ((3 - (len(message_bits) % 3)) % 3)
|
||||
|
||||
npixels = width * height
|
||||
len_message_bits = len(message_bits)
|
||||
if len_message_bits > npixels * 3:
|
||||
raise Exception(
|
||||
"The message you want to hide is too long: {}".format(message_length)
|
||||
)
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
if shift != 0:
|
||||
shift -= 1
|
||||
continue
|
||||
if index + 3 <= len_message_bits:
|
||||
|
||||
while hider.encode_another_pixel():
|
||||
generated_number = next(generator)
|
||||
# Get the colour component.
|
||||
pixel = img.getpixel((col, row))
|
||||
r = pixel[0]
|
||||
g = pixel[1]
|
||||
b = pixel[2]
|
||||
|
||||
col = generated_number % width
|
||||
row = int(generated_number / width)
|
||||
# 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])
|
||||
|
||||
hider.encode_pixel((col, row))
|
||||
# Save the new pixel
|
||||
if img.mode == "RGBA":
|
||||
encoded.putpixel((col, row), (r, g, b, pixel[3]))
|
||||
else:
|
||||
encoded.putpixel((col, row), (r, g, b))
|
||||
|
||||
return hider.encoded_image
|
||||
index += 3
|
||||
else:
|
||||
img.close()
|
||||
return encoded
|
||||
|
||||
|
||||
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,
|
||||
):
|
||||
def reveal(input_image: Union[str, IO[bytes]], encoding: str = "UTF-8", shift: int = 0):
|
||||
"""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)
|
||||
img = tools.open_image(input_image)
|
||||
width, height = img.size
|
||||
buff, count = 0, 0
|
||||
bitab = []
|
||||
limit = None
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
if shift != 0:
|
||||
shift -= 1
|
||||
continue
|
||||
# pixel = [r, g, b] or [r,g,b,a]
|
||||
pixel = img.getpixel((col, row))
|
||||
if img.mode == "RGBA":
|
||||
pixel = pixel[:3] # ignore the alpha
|
||||
for color in pixel:
|
||||
buff += (color & 1) << (tools.ENCODINGS[encoding] - 1 - count)
|
||||
count += 1
|
||||
if count == tools.ENCODINGS[encoding]:
|
||||
bitab.append(chr(buff))
|
||||
buff, count = 0, 0
|
||||
if bitab[-1] == ":" and limit is None:
|
||||
try:
|
||||
limit = int("".join(bitab[:-1]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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
|
||||
if len(bitab) - len(str(limit)) - 1 == limit:
|
||||
img.close()
|
||||
return "".join(bitab)[len(str(limit)) + 1 :]
|
||||
|
|
4
stegano/lsbset/__init__.py
Normal file
4
stegano/lsbset/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .lsbset import *
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -24,15 +26,15 @@ __revision__ = "$Date: 2021/11/29 $"
|
|||
__license__ = "GPLv3"
|
||||
|
||||
import itertools
|
||||
import math
|
||||
from typing import Any, Dict, Iterator, List
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import math
|
||||
from typing import Dict, Iterator, List, Any
|
||||
|
||||
|
||||
def identity() -> Iterator[int]:
|
||||
"""f(x) = x"""
|
||||
"""f(x) = x
|
||||
"""
|
||||
n = 0
|
||||
while True:
|
||||
yield n
|
||||
|
@ -72,7 +74,7 @@ def eratosthenes() -> Iterator[int]:
|
|||
"""Generate the prime numbers with the sieve of Eratosthenes.
|
||||
https://oeis.org/A000040
|
||||
"""
|
||||
d: Dict[int, List[int]] = {}
|
||||
d = {} # type: Dict[int, List[int]]
|
||||
for i in itertools.count(2):
|
||||
if i in d:
|
||||
for j in d[i]:
|
||||
|
@ -89,7 +91,8 @@ def composite() -> Iterator[int]:
|
|||
"""
|
||||
p1 = 3
|
||||
for p2 in eratosthenes():
|
||||
yield from range(p1 + 1, p2)
|
||||
for n in range(p1 + 1, p2):
|
||||
yield n
|
||||
p1 = p2
|
||||
|
||||
|
||||
|
@ -107,7 +110,8 @@ def carmichael() -> Iterator[int]:
|
|||
|
||||
|
||||
def ackermann_slow(m: int, n: int) -> int:
|
||||
"""Ackermann number."""
|
||||
"""Ackermann number.
|
||||
"""
|
||||
if m == 0:
|
||||
return n + 1
|
||||
elif n == 0:
|
||||
|
@ -117,7 +121,8 @@ def ackermann_slow(m: int, n: int) -> int:
|
|||
|
||||
|
||||
def ackermann_naive(m: int) -> Iterator[int]:
|
||||
"""Naive Ackermann encapsulated in a generator."""
|
||||
"""Naive Ackermann encapsulated in a generator.
|
||||
"""
|
||||
n = 0
|
||||
while True:
|
||||
yield ackermann_slow(m, n)
|
||||
|
@ -125,7 +130,8 @@ def ackermann_naive(m: int) -> Iterator[int]:
|
|||
|
||||
|
||||
def ackermann_fast(m: int, n: int) -> int:
|
||||
"""Ackermann number."""
|
||||
"""Ackermann number.
|
||||
"""
|
||||
while m >= 4:
|
||||
if n == 0:
|
||||
n = 1
|
||||
|
@ -143,7 +149,8 @@ def ackermann_fast(m: int, n: int) -> int:
|
|||
|
||||
|
||||
def ackermann(m: int) -> Iterator[int]:
|
||||
"""Ackermann encapsulated in a generator."""
|
||||
"""Ackermann encapsulated in a generator.
|
||||
"""
|
||||
n = 0
|
||||
while True:
|
||||
yield ackermann_fast(m, n)
|
||||
|
@ -161,7 +168,8 @@ def fibonacci() -> Iterator[int]:
|
|||
|
||||
|
||||
def log_gen() -> Iterator[int]:
|
||||
"""Logarithmic generator."""
|
||||
"""Logarithmic generator.
|
||||
"""
|
||||
y = 1
|
||||
while True:
|
||||
adder = max(1, math.pow(10, int(math.log10(y))))
|
||||
|
@ -223,7 +231,7 @@ def LFSR(m: int) -> Iterator[int]:
|
|||
# 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))
|
||||
out = sum([e * (2 ** i) for i, e in enumerate(state)])
|
||||
yield out
|
||||
|
||||
|
||||
|
@ -238,15 +246,13 @@ def shi_tomashi(
|
|||
"""
|
||||
image = cv2.imread(image_path)
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
corners: np.ndarray = cv2.goodFeaturesToTrack(
|
||||
corners: np.signedinteger[Any] = cv2.goodFeaturesToTrack(
|
||||
gray, max_corners, quality, min_distance
|
||||
)
|
||||
corners_int: np.ndarray[Any, np.dtype[np.signedinteger[Any]]] = np.array(
|
||||
np.intp(corners)
|
||||
)
|
||||
corners = np.int0(corners)
|
||||
i = 0
|
||||
while True:
|
||||
x, y = corners_int[i].ravel()
|
||||
x, y = corners[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
|
136
stegano/lsbset/lsbset.py
Normal file
136
stegano/lsbset/lsbset.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://git.sr.ht/~cedric/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 PIL import Image
|
||||
|
||||
from stegano import tools
|
||||
|
||||
|
||||
def hide(
|
||||
input_image: Union[str, IO[bytes]],
|
||||
message: str,
|
||||
generator: Iterator[int],
|
||||
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.
|
||||
"""
|
||||
message_length = len(message)
|
||||
assert message_length != 0, "message length is zero"
|
||||
|
||||
img = tools.open_image(input_image)
|
||||
|
||||
if img.mode not in ["RGB", "RGBA"]:
|
||||
if not auto_convert_rgb:
|
||||
print("The mode of the image is not RGB. Mode is {}".format(img.mode))
|
||||
answer = input("Convert the image to RGB ? [Y / n]\n") or "Y"
|
||||
if answer.lower() == "n":
|
||||
raise Exception("Not a RGB image.")
|
||||
img = img.convert("RGB")
|
||||
|
||||
img_list = list(img.getdata())
|
||||
width, height = img.size
|
||||
index = 0
|
||||
|
||||
message = str(message_length) + ":" + str(message)
|
||||
message_bits = "".join(tools.a2bits_list(message, encoding))
|
||||
message_bits += "0" * ((3 - (len(message_bits) % 3)) % 3)
|
||||
|
||||
npixels = width * height
|
||||
len_message_bits = len(message_bits)
|
||||
if len_message_bits > npixels * 3:
|
||||
raise Exception(
|
||||
"The message you want to hide is too long: {}".format(message_length)
|
||||
)
|
||||
while shift != 0:
|
||||
next(generator)
|
||||
shift -= 1
|
||||
|
||||
while index + 3 <= len_message_bits:
|
||||
generated_number = next(generator)
|
||||
r, g, b, *a = 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
|
||||
if img.mode == "RGBA":
|
||||
img_list[generated_number] = (r, g, b, *a)
|
||||
else:
|
||||
img_list[generated_number] = (r, g, b)
|
||||
|
||||
index += 3
|
||||
|
||||
# create empty new image of appropriate format
|
||||
encoded = Image.new(img.mode, (img.size))
|
||||
|
||||
# insert saved data into the image
|
||||
encoded.putdata(img_list)
|
||||
|
||||
return encoded
|
||||
|
||||
|
||||
def reveal(
|
||||
input_image: Union[str, IO[bytes]],
|
||||
generator: Iterator[int],
|
||||
shift: int = 0,
|
||||
encoding: str = "UTF-8",
|
||||
):
|
||||
"""Find a message in an image (with the LSB technique)."""
|
||||
img = tools.open_image(input_image)
|
||||
img_list = list(img.getdata())
|
||||
width, height = img.size
|
||||
buff, count = 0, 0
|
||||
bitab = []
|
||||
limit = None
|
||||
|
||||
while shift != 0:
|
||||
next(generator)
|
||||
shift -= 1
|
||||
|
||||
while True:
|
||||
generated_number = next(generator)
|
||||
# color = [r, g, b]
|
||||
for color in img_list[generated_number][:3]: # ignore the alpha
|
||||
buff += (color & 1) << (tools.ENCODINGS[encoding] - 1 - count)
|
||||
count += 1
|
||||
if count == tools.ENCODINGS[encoding]:
|
||||
bitab.append(chr(buff))
|
||||
buff, count = 0, 0
|
||||
if bitab[-1] == ":" and limit is None:
|
||||
if "".join(bitab[:-1]).isdigit():
|
||||
limit = int("".join(bitab[:-1]))
|
||||
else:
|
||||
raise IndexError("Impossible to detect message.")
|
||||
if len(bitab) - len(str(limit)) - 1 == limit:
|
||||
return "".join(bitab)[len(str(limit)) + 1 :]
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .red import hide, reveal
|
||||
|
||||
__all__ = ["hide", "reveal"]
|
||||
from .red import *
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stéganô is a basic Python Steganography module.
|
||||
# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .parity import *
|
||||
from .statistics import *
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -34,10 +36,7 @@ def steganalyse(img: Image.Image) -> Image.Image:
|
|||
width, height = img.size
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
if pixel := img.getpixel((col, row)):
|
||||
r, g, b = pixel[0:3]
|
||||
else:
|
||||
raise Exception("Error during steganlysis.")
|
||||
r, g, b = img.getpixel((col, row))[0:3]
|
||||
if r % 2 == 0:
|
||||
r = 0
|
||||
else:
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -24,7 +26,9 @@ __revision__ = "$Date: 2021/11/01 $"
|
|||
__license__ = "GPLv3"
|
||||
|
||||
import typing
|
||||
from collections import Counter, OrderedDict
|
||||
|
||||
from collections import Counter
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def steganalyse(img):
|
||||
|
@ -32,7 +36,7 @@ def steganalyse(img):
|
|||
Steganlysis of the LSB technique.
|
||||
"""
|
||||
width, height = img.size
|
||||
colours_counter: typing.Counter[int] = Counter()
|
||||
colours_counter = Counter() # type: typing.Counter[int]
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
r, g, b = img.getpixel((col, row))
|
||||
|
@ -43,7 +47,7 @@ def steganalyse(img):
|
|||
sorted(list(colours_counter.items()), key=lambda t: t[1])
|
||||
)
|
||||
|
||||
colours: float = 0
|
||||
colours = 0 # type: float
|
||||
for colour in list(dict_colours.keys()):
|
||||
colours += colour
|
||||
colours = colours / len(dict_colours)
|
||||
|
|
122
stegano/tools.py
122
stegano/tools.py
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -26,7 +28,7 @@ __license__ = "GPLv3"
|
|||
import base64
|
||||
import itertools
|
||||
from functools import reduce
|
||||
from typing import IO, List, Union
|
||||
from typing import IO, Iterator, List, Tuple, Union
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
@ -74,7 +76,9 @@ def setlsb(component: int, bit: str) -> int:
|
|||
return component & ~1 | int(bit)
|
||||
|
||||
|
||||
def n_at_a_time(items: List[int], n: int, fillvalue: str):
|
||||
def n_at_a_time(
|
||||
items: List[int], n: int, fillvalue: str
|
||||
) -> Iterator[Tuple[Union[int, str]]]:
|
||||
"""Returns an iterator which groups n items at a time.
|
||||
Any final partial tuple will be padded with the fillvalue
|
||||
|
||||
|
@ -111,113 +115,3 @@ def open_image(fname_or_instance: Union[str, IO[bytes]]):
|
|||
return fname_or_instance
|
||||
|
||||
return Image.open(fname_or_instance)
|
||||
|
||||
|
||||
class Hider:
|
||||
def __init__(
|
||||
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):
|
||||
# Get the colour component.
|
||||
r, g, b, *a = 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):
|
||||
# pixel = [r, g, b] or [r,g,b,a]
|
||||
pixel = 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 : # noqa: E203
|
||||
]
|
||||
if self.close_file:
|
||||
self.encoded_image.close()
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -23,9 +25,9 @@ __date__ = "$Date: 2016/05/17 $"
|
|||
__revision__ = "$Date: 2017/01/18 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import io
|
||||
import os
|
||||
import unittest
|
||||
import io
|
||||
|
||||
from stegano import exifHeader
|
||||
|
||||
|
@ -33,9 +35,10 @@ from stegano import exifHeader
|
|||
class TestEXIFHeader(unittest.TestCase):
|
||||
def test_hide_empty_message(self):
|
||||
"""Test hiding the empty string."""
|
||||
exifHeader.hide(
|
||||
secret = exifHeader.hide(
|
||||
"./tests/sample-files/20160505T130442.jpg", "./image.jpg", secret_message=""
|
||||
)
|
||||
# secret.save(""./image.png"")
|
||||
|
||||
clear_message = exifHeader.reveal("./image.jpg")
|
||||
|
||||
|
@ -45,7 +48,7 @@ class TestEXIFHeader(unittest.TestCase):
|
|||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
||||
|
||||
for message in messages_to_hide:
|
||||
exifHeader.hide(
|
||||
secret = exifHeader.hide(
|
||||
"./tests/sample-files/20160505T130442.jpg",
|
||||
"./image.jpg",
|
||||
secret_message=message,
|
||||
|
@ -53,12 +56,13 @@ class TestEXIFHeader(unittest.TestCase):
|
|||
|
||||
clear_message = exifHeader.reveal("./image.jpg")
|
||||
|
||||
self.assertEqual(message, clear_message.decode())
|
||||
self.assertEqual(message, message)
|
||||
|
||||
def test_with_image_without_exif_data(self):
|
||||
exifHeader.hide(
|
||||
secret = exifHeader.hide(
|
||||
"./tests/sample-files/Lenna.jpg", "./image.jpg", secret_message=""
|
||||
)
|
||||
# secret.save(""./image.png"")
|
||||
|
||||
clear_message = exifHeader.reveal("./image.jpg")
|
||||
|
||||
|
@ -68,7 +72,7 @@ class TestEXIFHeader(unittest.TestCase):
|
|||
text_file_to_hide = "./tests/sample-files/lorem_ipsum.txt"
|
||||
with open(text_file_to_hide, "rb") as f:
|
||||
message = f.read()
|
||||
exifHeader.hide(
|
||||
secret = exifHeader.hide(
|
||||
"./tests/sample-files/20160505T130442.jpg",
|
||||
img_enc="./image.jpg",
|
||||
secret_file=text_file_to_hide,
|
||||
|
@ -78,12 +82,13 @@ class TestEXIFHeader(unittest.TestCase):
|
|||
self.assertEqual(message, clear_message)
|
||||
|
||||
def test_with_png_image(self):
|
||||
exifHeader.hide(
|
||||
secret = exifHeader.hide(
|
||||
"./tests/sample-files/Lenna.png", "./image.png", secret_message="Secret"
|
||||
)
|
||||
# secret.save(""./image.png"")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
exifHeader.reveal("./image.png")
|
||||
clear_message = exifHeader.reveal("./image.png")
|
||||
|
||||
def test_with_bytes(self):
|
||||
outputBytes = io.BytesIO()
|
||||
|
@ -97,11 +102,11 @@ class TestEXIFHeader(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
try:
|
||||
os.unlink("./image.jpg")
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.unlink("./image.png")
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -23,13 +25,12 @@ __date__ = "$Date: 2017/03/01 $"
|
|||
__revision__ = "$Date: 2017/03/01 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import itertools
|
||||
import unittest
|
||||
|
||||
import itertools
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from stegano.lsb import generators
|
||||
from stegano.lsbset import generators
|
||||
|
||||
|
||||
class TestGenerators(unittest.TestCase):
|
||||
|
@ -70,7 +71,7 @@ class TestGenerators(unittest.TestCase):
|
|||
|
||||
def test_eratosthenes(self):
|
||||
"""Test the Eratosthenes sieve."""
|
||||
with open("./tests/expected-results/eratosthenes") as f:
|
||||
with open("./tests/expected-results/eratosthenes", "r") as f:
|
||||
self.assertEqual(
|
||||
tuple(itertools.islice(generators.eratosthenes(), 168)),
|
||||
tuple(int(line) for line in f),
|
||||
|
@ -78,7 +79,7 @@ class TestGenerators(unittest.TestCase):
|
|||
|
||||
def test_composite(self):
|
||||
"""Test the composite sieve."""
|
||||
with open("./tests/expected-results/composite") as f:
|
||||
with open("./tests/expected-results/composite", "r") as f:
|
||||
self.assertEqual(
|
||||
tuple(itertools.islice(generators.composite(), 114)),
|
||||
tuple(int(line) for line in f),
|
||||
|
@ -86,7 +87,7 @@ class TestGenerators(unittest.TestCase):
|
|||
|
||||
def test_fermat(self):
|
||||
"""Test the Fermat generator."""
|
||||
with open("./tests/expected-results/fermat") as f:
|
||||
with open("./tests/expected-results/fermat", "r") as f:
|
||||
self.assertEqual(
|
||||
tuple(itertools.islice(generators.fermat(), 9)),
|
||||
tuple(int(line) for line in f),
|
||||
|
@ -94,7 +95,7 @@ class TestGenerators(unittest.TestCase):
|
|||
|
||||
def test_triangular_numbers(self):
|
||||
"""Test the Triangular numbers generator."""
|
||||
with open("./tests/expected-results/triangular_numbers") as f:
|
||||
with open("./tests/expected-results/triangular_numbers", "r") as f:
|
||||
self.assertEqual(
|
||||
tuple(itertools.islice(generators.triangular_numbers(), 54)),
|
||||
tuple(int(line) for line in f),
|
||||
|
@ -102,7 +103,7 @@ class TestGenerators(unittest.TestCase):
|
|||
|
||||
def test_mersenne(self):
|
||||
"""Test the Mersenne generator."""
|
||||
with open("./tests/expected-results/mersenne") as f:
|
||||
with open("./tests/expected-results/mersenne", "r") as f:
|
||||
self.assertEqual(
|
||||
tuple(itertools.islice(generators.mersenne(), 20)),
|
||||
tuple(int(line) for line in f),
|
||||
|
@ -110,7 +111,7 @@ class TestGenerators(unittest.TestCase):
|
|||
|
||||
def test_carmichael(self):
|
||||
"""Test the Carmichael generator."""
|
||||
with open("./tests/expected-results/carmichael") as f:
|
||||
with open("./tests/expected-results/carmichael", "r") as f:
|
||||
self.assertEqual(
|
||||
tuple(itertools.islice(generators.carmichael(), 33)),
|
||||
tuple(int(line) for line in f),
|
||||
|
@ -118,7 +119,7 @@ class TestGenerators(unittest.TestCase):
|
|||
|
||||
def test_ackermann_slow(self):
|
||||
"""Test the Ackermann set."""
|
||||
with open("./tests/expected-results/ackermann") as f:
|
||||
with open("./tests/expected-results/ackermann", "r") as f:
|
||||
self.assertEqual(generators.ackermann_slow(3, 1), int(f.readline()))
|
||||
self.assertEqual(generators.ackermann_slow(3, 2), int(f.readline()))
|
||||
|
||||
|
@ -126,30 +127,31 @@ class TestGenerators(unittest.TestCase):
|
|||
"""Test the Naive Ackermann generator"""
|
||||
gen = generators.ackermann_naive(3)
|
||||
next(gen)
|
||||
with open("./tests/expected-results/ackermann") as f:
|
||||
with open("./tests/expected-results/ackermann", "r") 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:
|
||||
with open("./tests/expected-results/ackermann", "r") 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()))
|
||||
self.assertEqual(generators.ackermann_fast(4, 2), 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:
|
||||
with open("./tests/expected-results/ackermann", "r") 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:
|
||||
with open("./tests/expected-results/LFSR", "r") as f:
|
||||
self.assertEqual(
|
||||
tuple(itertools.islice(generators.LFSR(2**8), 256)),
|
||||
tuple(itertools.islice(generators.LFSR(2 ** 8), 256)),
|
||||
tuple(int(line) for line in f),
|
||||
)
|
||||
|
||||
|
@ -174,13 +176,14 @@ class TestGenerators(unittest.TestCase):
|
|||
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)
|
||||
self.assertIsNone(
|
||||
np.testing.assert_allclose(corners, test_file_reshaped, rtol=1e-0, atol=0)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def shi_tomashi_reconfigure(
|
||||
file_name: str,
|
||||
max_corners: int = 1000,
|
||||
corners: int = 1000,
|
||||
quality: float = 0.001,
|
||||
min_distance: int = 10,
|
||||
):
|
||||
|
@ -189,10 +192,8 @@ class TestGenerators(unittest.TestCase):
|
|||
"""
|
||||
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 = cv2.goodFeaturesToTrack(gray, corners, quality, min_distance)
|
||||
corners = np.int0(corners)
|
||||
corners = corners.reshape(corners.shape[0], -1)
|
||||
np.savetxt("tests/expected-results/shi_tomashi.txt", corners)
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -18,18 +20,18 @@
|
|||
# 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 $"
|
||||
__version__ = "$Revision: 0.3 $"
|
||||
__date__ = "$Date: 2016/04/12 $"
|
||||
__revision__ = "$Date: 2017/05/04 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import io
|
||||
import os
|
||||
import base64
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from stegano import lsb
|
||||
from stegano.lsb import generators
|
||||
|
||||
|
||||
class TestLSB(unittest.TestCase):
|
||||
|
@ -38,205 +40,133 @@ class TestLSB(unittest.TestCase):
|
|||
Test hiding the empty string.
|
||||
"""
|
||||
with self.assertRaises(AssertionError):
|
||||
lsb.hide("./tests/sample-files/Lenna.png", "", generators.eratosthenes())
|
||||
lsb.hide("./tests/sample-files/Lenna.png", "")
|
||||
|
||||
def test_hide_and_reveal_without_generator(self):
|
||||
def test_hide_and_reveal(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",
|
||||
"./tests/sample-files/Lenna.png", messages_to_hide, encoding="UTF-32LE"
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsb.reveal(
|
||||
"./image.png", generators.eratosthenes(), encoding="UTF-32LE"
|
||||
)
|
||||
clear_message = lsb.reveal("./image.png", encoding="UTF-32LE")
|
||||
self.assertEqual(messages_to_hide, clear_message)
|
||||
|
||||
def test_with_transparent_png(self):
|
||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
||||
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(),
|
||||
"./tests/sample-files/transparent.png", message, encoding="UTF-32LE"
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsb.reveal("./image.png", generators.eratosthenes())
|
||||
clear_message = lsb.reveal("./image.png", encoding="UTF-32LE")
|
||||
|
||||
self.assertEqual(message, clear_message)
|
||||
|
||||
@patch("builtins.input", return_value="y")
|
||||
def test_manual_convert_rgb(self, input):
|
||||
message_to_hide = "Hello World!"
|
||||
message_to_hide = "I love 🍕 and 🍫!"
|
||||
lsb.hide(
|
||||
"./tests/sample-files/Lenna-grayscale.png",
|
||||
message_to_hide,
|
||||
generators.eratosthenes(),
|
||||
encoding="UTF-32LE",
|
||||
)
|
||||
|
||||
@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."):
|
||||
message_to_hide = "I love 🍕 and 🍫!"
|
||||
with self.assertRaises(Exception):
|
||||
lsb.hide(
|
||||
"./tests/sample-files/Lenna-grayscale.png",
|
||||
message_to_hide,
|
||||
generators.eratosthenes(),
|
||||
encoding="UTF-32LE",
|
||||
)
|
||||
|
||||
def test_auto_convert_rgb(self):
|
||||
message_to_hide = "I love 🍕 and 🍫!"
|
||||
lsb.hide(
|
||||
"./tests/sample-files/Lenna-grayscale.png",
|
||||
message_to_hide,
|
||||
encoding="UTF-32LE",
|
||||
auto_convert_rgb=True,
|
||||
)
|
||||
|
||||
def test_with_text_file(self):
|
||||
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)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsb.reveal("./image.png")
|
||||
self.assertEqual(message, clear_message)
|
||||
|
||||
def test_with_binary_file(self):
|
||||
binary_file_to_hide = "./tests/sample-files/free-software-song.ogg"
|
||||
with open(binary_file_to_hide, "rb") as bin_file:
|
||||
encoded_string = base64.b64encode(bin_file.read())
|
||||
message = encoded_string.decode()
|
||||
secret = lsb.hide("./tests/sample-files/Montenach.png", message)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsb.reveal("./image.png")
|
||||
clear_message += "==="
|
||||
clear_message = base64.b64decode(clear_message)
|
||||
with open("file1", "wb") as f:
|
||||
f.write(clear_message)
|
||||
with open("file1", "rb") as bin_file:
|
||||
encoded_string = base64.b64encode(bin_file.read())
|
||||
message1 = encoded_string.decode()
|
||||
self.assertEqual(message, message1)
|
||||
try:
|
||||
os.unlink("./file1")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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.assertRaises(Exception):
|
||||
lsb.hide("./tests/sample-files/Lenna.png", message)
|
||||
|
||||
def test_with_bytes(self):
|
||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
||||
|
||||
for message in messages_to_hide:
|
||||
outputBytes = io.BytesIO()
|
||||
with open("./tests/sample-files/20160505T130442.jpg", "rb") as f:
|
||||
bytes_image = lsb.hide(f, message)
|
||||
bytes_image.save(outputBytes, "PNG")
|
||||
outputBytes.seek(0)
|
||||
|
||||
clear_message = lsb.reveal(outputBytes)
|
||||
|
||||
self.assertEqual(message, clear_message)
|
||||
|
||||
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 = lsb.hide("./tests/sample-files/20160505T130442.jpg", message)
|
||||
bytes_image.save(outputBytes, "PNG")
|
||||
outputBytes.seek(0)
|
||||
|
||||
clear_message = lsb.reveal(outputBytes, generators.identity())
|
||||
clear_message = lsb.reveal(outputBytes)
|
||||
|
||||
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")
|
||||
|
|
207
tests/test_lsbset.py
Normal file
207
tests/test_lsbset.py
Normal file
|
@ -0,0 +1,207 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://git.sr.ht/~cedric/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.5 $"
|
||||
__date__ = "$Date: 2016/04/13 $"
|
||||
__revision__ = "$Date: 2021/11/29 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import io
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from stegano import lsbset
|
||||
from stegano.lsbset import generators
|
||||
|
||||
|
||||
class TestLSBSet(unittest.TestCase):
|
||||
def test_hide_empty_message(self):
|
||||
"""
|
||||
Test hiding the empty string.
|
||||
"""
|
||||
with self.assertRaises(AssertionError):
|
||||
lsbset.hide("./tests/sample-files/Lenna.png", "", generators.eratosthenes())
|
||||
|
||||
def test_hide_and_reveal(self):
|
||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
||||
for message in messages_to_hide:
|
||||
secret = lsbset.hide(
|
||||
"./tests/sample-files/Lenna.png", message, generators.eratosthenes()
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsbset.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 = lsbset.hide(
|
||||
"./tests/sample-files/Lenna.png", message, generators.ackermann(m=3)
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsbset.reveal("./image.png", generators.ackermann(m=3))
|
||||
|
||||
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 = lsbset.hide(
|
||||
"./tests/sample-files/Lenna.png",
|
||||
message,
|
||||
generators.shi_tomashi("./tests/sample-files/Lenna.png"),
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsbset.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 = lsbset.hide(
|
||||
"./tests/sample-files/Lenna.png", message, generators.eratosthenes(), 4
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsbset.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 = lsbset.hide(
|
||||
"./tests/sample-files/Lenna.png",
|
||||
messages_to_hide,
|
||||
generators.eratosthenes(),
|
||||
encoding="UTF-32LE",
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsbset.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 = lsbset.hide(
|
||||
"./tests/sample-files/transparent.png",
|
||||
message,
|
||||
generators.eratosthenes(),
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
clear_message = lsbset.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!"
|
||||
lsbset.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!"
|
||||
with self.assertRaises(Exception):
|
||||
lsbset.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 = lsbset.hide(
|
||||
"./tests/sample-files/20160505T130442.jpg",
|
||||
message,
|
||||
generators.identity(),
|
||||
)
|
||||
bytes_image.save(outputBytes, "PNG")
|
||||
outputBytes.seek(0)
|
||||
|
||||
clear_message = lsbset.reveal(outputBytes, generators.identity())
|
||||
|
||||
self.assertEqual(message, clear_message)
|
||||
|
||||
def test_auto_convert_rgb(self):
|
||||
message_to_hide = "Hello World!"
|
||||
lsbset.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.assertRaises(Exception):
|
||||
lsbset.hide(
|
||||
"./tests/sample-files/Lenna.png", message, generators.identity()
|
||||
)
|
||||
|
||||
def test_hide_and_reveal_with_bad_generator(self):
|
||||
message_to_hide = "Hello World!"
|
||||
secret = lsbset.hide(
|
||||
"./tests/sample-files/Lenna.png", message_to_hide, generators.eratosthenes()
|
||||
)
|
||||
secret.save("./image.png")
|
||||
|
||||
with self.assertRaises(IndexError):
|
||||
lsbset.reveal("./image.png", generators.identity())
|
||||
|
||||
def test_with_unknown_generator(self):
|
||||
message_to_hide = "Hello World!"
|
||||
with self.assertRaises(AttributeError):
|
||||
lsbset.hide(
|
||||
"./tests/sample-files/Lenna.png",
|
||||
message_to_hide,
|
||||
generators.eratosthene(),
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
os.unlink("./image.png")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -34,7 +36,7 @@ class TestRed(unittest.TestCase):
|
|||
Test hiding the empty string.
|
||||
"""
|
||||
with self.assertRaises(AssertionError):
|
||||
red.hide("./tests/sample-files/Lenna.png", "")
|
||||
secret = red.hide("./tests/sample-files/Lenna.png", "")
|
||||
|
||||
def test_hide_and_reveal(self):
|
||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
||||
|
@ -45,7 +47,7 @@ class TestRed(unittest.TestCase):
|
|||
|
||||
clear_message = red.reveal("./image.png")
|
||||
|
||||
self.assertEqual(message, clear_message)
|
||||
self.assertEqual(message, message)
|
||||
|
||||
def test_with_too_long_message(self):
|
||||
with open("./tests/sample-files/lorem_ipsum.txt") as f:
|
||||
|
@ -56,7 +58,7 @@ class TestRed(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
try:
|
||||
os.unlink("./image.png")
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -24,12 +25,15 @@ __date__ = "$Date: 2019/06/06 $"
|
|||
__revision__ = "$Date: 2019/06/06 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import io
|
||||
import os
|
||||
import base64
|
||||
import unittest
|
||||
|
||||
from PIL import Image, ImageChops
|
||||
from unittest.mock import patch
|
||||
|
||||
from stegano import lsb
|
||||
from stegano.steganalysis import parity, statistics
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
|
||||
class TestSteganalysis(unittest.TestCase):
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Stegano - Stegano is a pure Python steganography module.
|
||||
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
# Copyright (C) 2010-2021 Cédric Bonhomme - https://www.cedricbonhomme.org
|
||||
#
|
||||
# For more information : https://github.com/cedricbonhomme/Stegano
|
||||
# For more information : https://git.sr.ht/~cedric/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
|
||||
|
@ -23,7 +25,9 @@ __date__ = "$Date: 2017/02/22 $"
|
|||
__revision__ = "$Date: 2017/02/22 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import io
|
||||
|
||||
from stegano import tools
|
||||
|
||||
|
@ -81,7 +85,7 @@ class TestTools(unittest.TestCase):
|
|||
self.assertEqual(list(result), [(1, 2), (3, 4), (5, "X")])
|
||||
|
||||
def test_binary2base64(self):
|
||||
with open("./tests/expected-results/binary2base64") as f:
|
||||
with open("./tests/expected-results/binary2base64", "r") as f:
|
||||
expected_value = f.read()
|
||||
value = tools.binary2base64("tests/sample-files/free-software-song.ogg")
|
||||
self.assertEqual(expected_value, value)
|
||||
|
|
Loading…
Add table
Reference in a new issue