Compare commits

..

No commits in common. "master" and "v0.8.2" have entirely different histories.

65 changed files with 1938 additions and 4592 deletions

View file

@ -1,16 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
charset = utf-8
max_line_length = 88
[*.{yml,yaml,json,js,css,html}]
indent_size = 2
[*.{md,rst}]
trim_trailing_whitespace = false

View file

@ -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 .

View file

@ -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

2
.gitignore vendored
View file

@ -12,7 +12,7 @@ build
# setuptools # setuptools
build/* build/*
stegano.egg-info/ Stegano.egg-info/*
dist/* dist/*
# tests # tests

View file

@ -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

View file

@ -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

17
.travis.yml Normal file
View file

@ -0,0 +1,17 @@
language: python
python:
- 3.5
- 3.6
install:
- "pip install -r requirements.txt"
- "pip install -r requirements.dev.txt"
script:
# Run the test suit
- nosetests --with-coverage --cover-package=stegano
# Run the type checker
- python tools/run_mypy.py
after_success:
- coveralls

View file

@ -1,341 +0,0 @@
## 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.
### 0.10.0 (2021-11-29)
- new: Implemented Shi-Tomashi corner generator
([PR #32](https://github.com/cedricbonhomme/Stegano/pull/32)).
Implemented by thundersparkf (see CONTRIBUTORS.md file).
### 0.9.9 (2021-07-02)
- Stegano now uses Pillow 8.3.0.
### 0.9.8 (2019-12-20)
- Stegano is now using poetry;
- minor improvements to the command line.
### 0.9.7 (2019-10-27)
- fixed markdown of the previous release.
### 0.9.6 (2019-10-27)
- fixed markdown of the previous release;
### 0.9.5 (2019-10-27)
- updated dependencies;
- home page of the project is now: https://github.com/cedricbonhomme/Stegano
### 0.9.4 (2019-06-05)
- new: Implemented LFSR generator (with tests and CLI)
([PR #27](https://github.com/cedricbonhomme/Stegano/pull/27))
- new: Implemented Ackermann generators CLI interface
([PR #26](https://github.com/cedricbonhomme/Stegano/pull/26))
- new: The Ackermann functions are not actual generators
([#24](https://github.com/cedricbonhomme/Stegano/issues/24))
- new: add a shift parameter for the lsbmodule
([#25](https://github.com/cedricbonhomme/Stegano/issues/25))
- fix: lsbset.hide cause .png transparent area lost
([#23](https://github.com/cedricbonhomme/Stegano/issues/23))
### 0.9.3 (2019-04-10)
- it is now possible to either pass the location of an image or directly pass
an already opened Image.Image to the hide and reveal methods;
- code re-formatted a bit with black.
### 0.9.2 (2019-04-04)
- updated Pillow dependency to version 6.0.0 in order to fix a bug when opening
some PNG files (https://github.com/python-pillow/Pillow/issues/3557).
### 0.9.1 (2019-03-06)
- updated Pillow dependency in order to fix a bug when opening some PNG files.
### 0.9.0 (2018-12-18)
- added the possibility to shift the encoded bits when using the lsbset module.
### 0.8.6 (2018-11-05)
- fixed a potential security issue related to CVE-2018-18074.
### 0.8.5 (2018-04-18)
- Fixed an encoding problem which occured on Windows during the installation
of the module.
### 0.8.4 (2018-02-28)
- Stegano is ready for use with pipenv and pipsi.
### 0.8.3 (2018-02-23)
- the recommended way to install Stegano is now to use pipenv.
### 0.8.2 (2017-12-20)
- Fixed a bug with the new 'encoding' function when using Stegano as a command
line tool. No default value was set. Default value is UTF-8.
### 0.8.1 (2017-05-16)
- it is now possible to specify the encoding (UTF-8 or UTF-32LE) of the message
to hide/reveal through the command line;
- the help of the command line now displays the available choices for the
arguments, if it is necessary (list of available encodings, list of available
generators);
- tests expected results lies now in a dedicated folder;
- a script has been added in order to get proper exit code check for mypy.
### 0.8 (2017-05-06)
- updated command line. All commands are now prefixed with *stegano-*;
- improved type hints;
- it is possible to load and save images from and to file objects (BytesIO);
- improved checks when revealing a message with the lsbset module fails.
### 0.7.1 (2017-05-05)
- improved generators for the lsb-set module;
- improved tests for the generators;
- improved type hints.
### 0.7 (2017-05-04)
- unicode is now supported. By default UTF-8 encoding is used. UTF-32LE can also
be used to hide non-ASCII characters. UTF-8 (8 bits) is the default choice
since it is possible to hide longer messages with it.
- improved checks with type hints.
### 0.6.9 (2017-03-10)
- introduces some type hints (PEP 484);
- more tests for the generators and for the tools module;
- updated descriptions of generators;
- fixed a bug with a generator that has been previously renamed.
### 0.6.8 (2017-03-08)
- bugfix: fixed #12: Error when revealing a hidden binary file in an image.
### 0.6.7 (2017-02-21)
- bugfix: added missing dependency in the setup.py file.
### 0.6.6 (2017-02-20)
- improved docstrings for the desciption of the generators;
- improved the command which displays the list of generators.
### 0.6.5 (2017-02-16)
- added a command to list all available generators for the lsb-set module;
- test when the data image is coming via byte stream, for the lsb module.
### 0.6.4 (2017-02-06)
- a command line for the 'red' module has been added;
- bugfix: fixed a bug in the lsb-set command line when the generator wasn't
specified by the user.
### 0.6.3 (2017-01-29)
- Support for transparent PNG images has been added (lsb and lsbset modules).
### 0.6.2 (2017-01-19)
- bugfix: solved a bug when the image data is coming via byte streams (ByteIO),
for the exifHeader hiding method.
### 0.6.1 (2016-08-25)
- reorganization of the steganalysis sub-module.
### 0.6 (2016-08-04)
- improvements of the command line of Stéganô. The use of Stéganô through the
command line has slightly changed ('hide' and 'reveal' are now sub-parameters
of the command line). No changes if you use Stéganô as a module in your
software. The documentation has been updated accordingly.
### 0.5.5 (2016-08-03)
- bugfix: Incorrect padding size in `base642string` in tools.base642binary().
### 0.5.4 (2016-05-22)
- the generator provided to the functions lsbset.hide() and lsbset.reveal() is
now a function. This is more convenient for a user who wants to use a custom
generator (not in the module lsbset.generators).
- performance improvements for the lsb and lsbset modules.
### 0.5.3 (2016-05-19)
- reorganization of all modules. No impact for the users of Stegano.
### 0.5.2 (2016-05-18)
- improvements and bug fixes for the exifHeader module;
- added unit tests for the exifHeader module;
- improvements of the documentation.
### 0.5.1 (2016-04-16)
- minor improvements and bug fixes;
- added unit tests for the slsb and slsbset modules.
### 0.5 (2016-03-18)
- management of greyscale images.
### 0.4.6 (2016-03-12)
- bugfix when the length of the message to hide is not divisible by 3,
for the slsb and slsbset module.
### 0.4.5 (2015-12-23)
- bugfix.
### 0.4.4 (2015-12-23)
- new project home page;
- minor updated to the documentation.
### 0.4.3 (2015-10-06)
- bug fixes for Python 3;
- bug fixes in the scripts in *./bin*.
### 0.4.2 (2015-10-05)
- first stable release on PypI.
### 0.4 (2012-01-02)
This release introduces a more advanced LSB (Least Significant Bit) method
based on integers sets. The sets generated with Python generators
(Sieve of Eratosthenes, Fermat, Carmichael numbers, etc.) are used to select
the pixels used to hide the information. You can use these new methods in your
Python codes as a Python module or as a program in your scripts.
### 0.3 (2011-04-15)
- you can now use Stéganô as a library in your Python program;
(python setup.py install) or as a 'program' thanks to the scripts provided
in the bin directory;
- new documentation (reStructuredText) comes with Stéganô.
### 0.2 (2011-03-24)
- this release introduces some bugfixes and a major speed improvement of the
*reveal* function for the LSB method. Moreover it is now possible to hide a
binary file (ogg, executable, etc.);
- a new technique for hiding/revealing a message in a JPEG picture by using the
description field of the image is provided.

190
CHANGELOG.rst Normal file
View file

@ -0,0 +1,190 @@
Release History
===============
0.8.2 (2017-12-20)
------------------
* Fixed a bug with the new 'encoding' function when using Stegano as a command
line tool. No default value was set. Default value is UTF-8.
0.8.1 (2017-05-16)
------------------
* it is now possible to specify the encoding (UTF-8 or UTF-32LE) of the message
to hide/reveal through the command line;
* the help of the command line now displays the available choices for the
arguments, if it is necessary (list of available encodings, list of available
generators);
* tests expected results lies now in a dedicated folder;
* a script has been added in order to get proper exit code check for mypy.
0.8 (2017-05-06)
----------------
* updated command line. All commands are now prefixed with *stegano-*;
* improved type hints;
* it is possible to load and save images from and to file objects (BytesIO);
* improved checks when revealing a message with the lsbset module fails.
0.7.1 (2017-05-05)
------------------
* improved generators for the lsb-set module;
* improved tests for the generators;
* improved type hints.
0.7 (2017-05-04)
----------------
* unicode is now supported. By default UTF-8 encoding is used. UTF-32LE can also
be used to hide non-ASCII characters. UTF-8 (8 bits) is the default choice
since it is possible to hide longer messages with it.
* improved checks with type hints.
0.6.9 (2017-03-10)
------------------
* introduces some type hints (PEP 484);
* more tests for the generators and for the tools module;
* updated descriptions of generators;
* fixed a bug with a generator that has been previously renamed.
0.6.8 (2017-03-08)
------------------
* bugfix: fixed #12: Error when revealing a hidden binary file in an image.
0.6.7 (2017-02-21)
------------------
* bugfix: added missing dependency in the setup.py file.
0.6.6 (2017-02-20)
------------------
* improved docstrings for the desciption of the generators;
* improved the command which displays the list of generators.
0.6.5 (2017-02-16)
------------------
* added a command to list all available generators for the lsb-set module;
* test when the data image is coming via byte stream, for the lsb module.
0.6.4 (2017-02-06)
------------------
* a command line for the 'red' module has been added;
* bugfix: fixed a bug in the lsb-set command line when the generator wasn't
specified by the user.
0.6.3 (2017-01-29)
------------------
* Support for transparent PNG images has been added (lsb and lsbset modules).
0.6.2 (2017-01-19)
------------------
* bugfix: solved a bug when the image data is coming via byte streams (ByteIO),
for the exifHeader hiding method.
0.6.1 (2016-08-25)
------------------
* reorganization of the steganalysis sub-module.
0.6 (2016-08-04)
------------------
* improvements of the command line of Stéganô. The use of Stéganô through the
command line has slightly changed ('hide' and 'reveal' are now sub-parameters
of the command line). No changes if you use Stéganô as a module in your
software. The documentation has been updated accordingly.
0.5.5 (2016-08-03)
------------------
* bugfix: Incorrect padding size in `base642string` in tools.base642binary().
0.5.4 (2016-05-22)
------------------
* the generator provided to the functions lsbset.hide() and lsbset.reveal() is
now a function. This is more convenient for a user who wants to use a custom
generator (not in the module lsbset.generators).
* performance improvements for the lsb and lsbset modules.
0.5.3 (2016-05-19)
------------------
* reorganization of all modules. No impact for the users of Stegano.
0.5.2 (2016-05-18)
------------------
* improvements and bug fixes for the exifHeader module;
* added unit tests for the exifHeader module;
* improvements of the documentation.
0.5.1 (2016-04-16)
------------------
* minor improvements and bug fixes;
* added unit tests for the slsb and slsbset modules.
0.5 (2016-03-18)
----------------
* management of greyscale images.
0.4.6 (2016-03-12)
------------------
* bugfix when the length of the message to hide is not divisible by 3,
for the slsb and slsbset module.
0.4.5 (2015-12-23)
------------------
* bugfix.
0.4.4 (2015-12-23)
------------------
* new project home page;
* minor updated to the documentation.
0.4.3 (2015-10-06)
------------------
* bug fixes for Python 3;
* bug fixes in the scripts in *./bin*.
0.4.2 (2015-10-05)
------------------
* first stable release on PypI.
0.4 (2012-01-02)
----------------
This release introduces a more advanced LSB (Least Significant Bit) method
based on integers sets. The sets generated with Python generators
(Sieve of Eratosthenes, Fermat, Carmichael numbers, etc.) are used to select
the pixels used to hide the information. You can use these new methods in your
Python codes as a Python module or as a program in your scripts.
0.3 (2011-04-15)
----------------
* you can now use Stéganô as a library in your Python program;
(python setup.py install) or as a 'program' thanks to the scripts provided
in the bin directory;
* new documentation (reStructuredText) comes with Stéganô.
0.2 (2011-03-24)
----------------
* this release introduces some bugfixes and a major speed improvement of the
*reveal* function for the LSB method. Moreover it is now possible to hide a
binary file (ogg, executable, etc.);
* a new technique for hiding/revealing a message in a JPEG picture by using the
description field of the image is provided.

104
CONTRIBUTING.rst Normal file
View file

@ -0,0 +1,104 @@
Contribution Guidelines
=======================
Before opening proposing any pull requests (about code or documentation),
please open an issue.
Code Contributions
------------------
When contributing code, you'll want to follow this checklist:
1. Fork the repository on GitHub.
2. Run the tests to confirm they all pass on your system. If they don't, you'll
need to investigate why they fail.
3. Write tests that demonstrate your bug or feature. Ensure that they fail.
4. Make your change.
5. Run the entire test suite again, confirming that all tests pass *including
the ones you just added*.
6. Send a GitHub Pull Request to the main repository's ``master`` branch.
GitHub Pull Requests are the expected method of code collaboration on this
project.
Code Style
~~~~~~~~~~
Please try to respect the `PEP 8`_ code style.
To get the greatest chance of helpful responses, please also observe the
following additional notes.
.. _PEP 8: http://pep8.org
Documentation Contributions
---------------------------
Documentation improvements are always welcome! The documentation files live in
the ``docs/`` directory of the codebase. They're written in
`reStructuredText`_, and use `Sphinx`_ to generate the full suite of
documentation.
When contributing documentation, please do your best to follow the style of the
documentation files. This means a soft-limit of 79 characters wide in your text
files and a semi-formal, yet friendly and approachable, prose style.
When presenting Python code, use single-quoted strings (``'hello'`` instead of
``"hello"``).
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
.. _Sphinx: http://sphinx-doc.org/index.html
Good Bug Reports
----------------
Please be aware of the following things when filing bug reports:
1. Avoid raising duplicate issues. *Please* use the GitHub issue search feature
to check whether your bug report or feature request has been mentioned in
the past. Duplicate bug reports and feature requests are a huge maintenance
burden on the limited resources of the project. If it is clear from your
report that you would have struggled to find the original, that's ok, but
if searching for a selection of words in your issue title would have found
the duplicate then the issue will likely be closed extremely abruptly.
2. When filing bug reports about exceptions or tracebacks, please include the
*complete* traceback. Partial tracebacks, or just the exception text, are
not helpful. Issues that do not contain complete tracebacks may be closed
without warning.
3. Make sure you provide a suitable amount of information to work with. This
means you should provide:
- Guidance on **how to reproduce the issue**. Ideally, this should be a
*small* code sample that can be run immediately by the maintainers.
Failing that, let us know what you're doing, how often it happens, what
environment you're using, etc. Be thorough: it prevents us needing to ask
further questions.
- Tell us **what you expected to happen**. When we run your example code,
what are we expecting to happen? What does "success" look like for your
code?
- Tell us **what actually happens**. It's not helpful for you to say "it
doesn't work" or "it fails". Tell us *how* it fails: do you get an
exception? A hang? A non-200 status code? How was the actual result
different from your expected result?
- Tell us **what version of Stegano you're using**, and
**how you installed it**. Different versions of Stegano behave
differently and have different bugs.
If you do not provide all of these things, it will take us much longer to
fix your problem. If we ask you to clarify these and you never respond, we
will close your issue without fixing it.
Questions
=========
The GitHub issue tracker is for *bug reports* and *feature requests*. Please do
not use it to ask questions about how to use Stegano. These questions should
instead be directed to Stack Overflow. Make sure
that your question is tagged with the `python-stegano` tag when asking it on
Stack Overflow, to ensure that it is answered promptly and accurately.
You can search for questions with
`these tags <http://stackoverflow.com/questions/tagged/python+steganography>`_.

View file

@ -1,21 +0,0 @@
## Owner
- Cédric Bonhomme <cedric@cedricbonhomme.org>
## Contributors
- 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>
- thundersparkf - https://github.com/thundersparkf
- Mickaël Schoentgen <mschoentgen@nuxeo.com>
And thank you to the testers!

14
CONTRIBUTORS.rst Normal file
View file

@ -0,0 +1,14 @@
Owner
=====
- Cédric Bonhomme <cedric@cedricbonhomme.org>
Contributors
============
- Andrew Roberts <andy.roberts.uk@gmail.com>
- Maxwell Gerber
- Nejdet Çağdaş Yücesoy <nejdetyucesoy@gmail.com>
- panni <panni@fragstore.net>
Thank you to the testers!

11
MANIFEST.in Normal file
View file

@ -0,0 +1,11 @@
#documentation
recursive-include docs *
# binary files
recursive-include bin *
#Misc
include COPYING
include README.rst
include CHANGELOG.rst
include requirements.txt

115
README.md
View file

@ -1,115 +0,0 @@
# Stegano
[![Workflow](https://github.com/cedricbonhomme/Stegano/workflows/Python%20application/badge.svg?style=flat-square)](https://github.com/cedricbonhomme/Stegano/actions?query=workflow%3A%22Python+application%22)
[Stegano](https://github.com/cedricbonhomme/Stegano), a pure Python Steganography
module.
Steganography is the art and science of writing hidden messages in such a way
that no one, apart from the sender and intended recipient, suspects the
existence of the message, a form of security through obscurity. Consequently,
functions provided by Stegano only hide messages, without encryption.
Steganography is often used with cryptography.
## Installation
```bash
$ poetry install stegano
```
You will be able to use Stegano in your Python programs.
If you only want to install Stegano as a command line tool:
```bash
$ pipx install stegano
```
pipx installs scripts (system wide available) provided by Python packages into
separate virtualenvs to shield them from your system and each other.
## Usage
A [tutorial](https://stegano.readthedocs.io) is available.
## Use Stegano as a library in your Python program
If you want to use Stegano in your Python program you just have to import the
appropriate steganography technique. For example:
```python
>>> from stegano import lsb
>>> secret = lsb.hide("./tests/sample-files/Lenna.png", "Hello World")
>>> secret.save("./Lenna-secret.png")
>>>
>>> clear_message = lsb.reveal("./Lenna-secret.png")
```
## Use Stegano as a command line tool
### Hide and reveal a message
```bash
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m "Secret Message" -o Lena1.png
$ stegano-lsb reveal -i Lena1.png
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
```
The message will be scattered in the picture, following a set described by the
Sieve of Eratosthenes. Other sets are available. You can also use your own
generators.
This will make a steganalysis more complicated.
## Running the tests
```bash
$ python -m unittest discover -v
```
Running the static type checker:
```bash
$ mypy stegano
```
## Contributions
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:
[![GitHub Sponsors](https://img.shields.io/github/sponsors/cedricbonhomme)](https://github.com/sponsors/cedricbonhomme)
or with Bitcoin to this address:
bc1q56u6sj7cvlwu58v5lemljcvkh7v2gc3tv8mj0e
Thank you !
## License
This software is licensed under
[GNU General Public License version 3](https://www.gnu.org/licenses/gpl-3.0.html)
Copyright (C) 2010-2025 [Cédric Bonhomme](https://www.cedricbonhomme.org)
For more information, [the list of authors and contributors](CONTRIBUTORS.md) is available.

131
README.rst Normal file
View file

@ -0,0 +1,131 @@
Stéganô
=======
.. image:: https://img.shields.io/pypi/pyversions/Stegano.svg?style=flat-square
:target: https://pypi.python.org/pypi/Stegano
.. image:: https://img.shields.io/pypi/v/Stegano.svg?style=flat-square
:target: https://github.com/cedricbonhomme/Stegano/releases/latest
.. image:: https://img.shields.io/pypi/l/Stegano.svg?style=flat-square
:target: https://www.gnu.org/licenses/gpl-3.0.html
.. image:: https://img.shields.io/travis/cedricbonhomme/Stegano/master.svg?style=flat-square
:target: https://travis-ci.org/cedricbonhomme/Stegano
.. image:: https://img.shields.io/coveralls/cedricbonhomme/Stegano/master.svg?style=flat-square
:target: https://coveralls.io/github/cedricbonhomme/Stegano?branch=master
.. image:: https://img.shields.io/github/stars/cedricbonhomme/Stegano.svg?style=flat-square
:target: https://github.com/cedricbonhomme/Stegano/stargazers
.. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg?style=flat-square
:target: https://saythanks.io/to/cedricbonhomme
`Stéganô <https://github.com/cedricbonhomme/Stegano>`_, a pure Python
Steganography module.
Steganography is the art and science of writing hidden messages in such a way
that no one, apart from the sender and intended recipient, suspects the
existence of the message, a form of security through obscurity. Consequently,
functions provided by Stéganô only hide messages, without encryption.
Steganography is often used with cryptography.
Installation
------------
.. code:: bash
$ sudo pip install Stegano
You will be able to use Stéganô in your Python programs or as a command line
tool.
Usage
-----
A `tutorial <https://stegano.readthedocs.io>`_ is available.
Use Stéganô as a library in your Python program
'''''''''''''''''''''''''''''''''''''''''''''''
If you want to use Stéganô in your Python program you just have to import the
appropriate steganography technique. For example:
.. code:: python
>>> from stegano import lsb
>>> secret = lsb.hide("./tests/sample-files/Lenna.png", "Hello World")
>>> secret.save("./Lenna-secret.png")
>>>
>>> clear_message = lsb.reveal("./Lenna-secret.png")
Use Stéganô as a program
''''''''''''''''''''''''
Hide and reveal a message
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: bash
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m "Secret Message" -o Lena1.png
$ stegano-lsb reveal -i Lena1.png
Secret Message
Hide the message with the Sieve of Eratosthenes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: bash
$ 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
Sieve of Eratosthenes. Other sets are available. You can also use your own
generators.
This will make a steganalysis more complicated.
Running the tests
-----------------
.. code:: bash
$ python -m unittest discover -v
Running the static type checker:
.. code:: bash
$ python tools/run_mypy.py
Contributions
-------------
Contributions are welcome. If you want to contribute to Stegano I highly
recommend you to install it in a Python virtual environment. For example:
.. code-block:: bash
$ git clone https://github.com/cedricbonhomme/Stegano.git
$ cd Stegano/
$ pew install 3.6.1 --type CPython
$ pew new --python=$(pew locate_python 3.6.1) -a . -r requirements.txt stegano-dev
stegano-dev$ python
Python 3.6.1 (default, Jun 28 2017, 07:49:05)
[GCC 6.3.0 20170406] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import stegano
>>>
Contact
-------
`Cédric Bonhomme <https://www.cedricbonhomme.org>`_

98
bin/stegano-lsb Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.7 $"
__date__ = "$Date: 2016/08/04 $"
__revision__ = "$Date: 2017/05/16 $"
__license__ = "GPLv3"
try:
from stegano import lsb
except:
print("Install Stegano: sudo pip install Stegano")
from stegano import tools
import argparse
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.")
# 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).")
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)
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)
if arguments.secret_binary != None:
data = tools.base642binary(secret)
with open(arguments.secret_binary, "wb") as f:
f.write(data)
else:
print(secret)

140
bin/stegano-lsb-set Executable file
View file

@ -0,0 +1,140 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.7 $"
__date__ = "$Date: 2016/03/18 $"
__revision__ = "$Date: 2017/05/16 $"
__license__ = "GPLv3"
import inspect
import crayons
try:
from stegano import lsbset
from stegano.lsbset import generators
except:
print("Install stegano: sudo pip install Stegano")
from stegano import tools
import argparse
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')
# Original image
parser_hide.add_argument("-i", "--input", dest="input_image_file",
required=True, help="Input image file.")
parser_hide.add_argument("-e", "--encoding", dest="encoding",
choices=tools.ENCODINGS.keys(), default='UTF-8',
help="Specify the encoding of the message to hide." +
" UTF-8 (default) or UTF-32LE.")
# Generator
parser_hide.add_argument("-g", "--generator", dest="generator_function",
choices=[generator[0] for generator in \
inspect.getmembers(generators, inspect.isfunction)],
required=True, help="Generator")
group_secret = parser_hide.add_mutually_exclusive_group(required=True)
# Non binary secret message to hide
group_secret.add_argument("-m", dest="secret_message",
help="Your secret message to hide (non binary).")
# Binary secret message to hide
group_secret.add_argument("-f", dest="secret_file",
help="Your secret to hide (Text or any binary file).")
# Image containing the secret
parser_hide.add_argument("-o", "--output", dest="output_image_file",
required=True, help="Output image containing the secret.")
# Subparser: Reveal
parser_reveal = subparsers.add_parser('reveal', help='reveal help')
parser_reveal.add_argument("-i", "--input", dest="input_image_file",
required=True, help="Input image file.")
parser_reveal.add_argument("-e", "--encoding", dest="encoding",
choices=tools.ENCODINGS.keys(), default='UTF-8',
help="Specify the encoding of the message to reveal." +
" UTF-8 (default) or UTF-32LE.")
parser_reveal.add_argument("-g", "--generator", dest="generator_function",
choices=[generator[0] for generator in \
inspect.getmembers(generators, inspect.isfunction)],
required=True, help="Generator")
parser_reveal.add_argument("-o", dest="secret_binary",
help="Output for the binary secret (Text or any binary file).")
# Subparser: List generators
parser_list_generators = subparsers.add_parser('list-generators',
help='list-generators help')
arguments = parser.parse_args()
if arguments.command != 'list-generators':
try:
arguments.generator_function
except AttributeError:
print('You must specify the name of a generator.')
parser.print_help()
exit(1)
try:
generator = getattr(generators, arguments.generator_function)()
except AttributeError as e:
print("Unknown generator: {}".format(arguments.generator_function))
exit(1)
if arguments.command == 'hide':
if arguments.secret_message != None:
secret = arguments.secret_message
elif arguments.secret_file != "":
secret = tools.binary2base64(arguments.secret_file)
img_encoded = lsbset.hide(arguments.input_image_file, secret, generator)
try:
img_encoded.save(arguments.output_image_file)
except Exception as e:
# If hide() returns an error (Too long message).
print(e)
elif arguments.command == 'reveal':
try:
secret = lsbset.reveal(arguments.input_image_file, generator)
except IndexError:
print("Impossible to detect message.")
exit(0)
if arguments.secret_binary != None:
data = tools.base642binary(secret)
with open(arguments.secret_binary, "w") as f:
f.write(data)
else:
print(secret)
elif arguments.command == 'list-generators':
all_generators = inspect.getmembers(generators, inspect.isfunction)
for generator in all_generators:
print('Generator id:')
print(' {}'.format(crayons.green(generator[0], bold=True)))
print('Desciption:')
print(' {}'.format(generator[1].__doc__))

56
bin/stegano-red Normal file
View file

@ -0,0 +1,56 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.1 $"
__date__ = "$Date: 2017/02/06 $"
__license__ = "GPLv3"
try:
from stegano import red
except:
print("Install stegano: sudo pip install Stegano")
import argparse
parser = argparse.ArgumentParser(prog='stegano-red')
subparsers = parser.add_subparsers(help='sub-command help', dest='command')
parser_hide = subparsers.add_parser('hide', help='hide help')
parser_hide.add_argument("-i", "--input", dest="input_image_file",
help="Image file")
parser_hide.add_argument("-m", dest="secret_message",
help="Your secret message to hide (non binary).")
parser_hide.add_argument("-o", "--output", dest="output_image_file",
help="Image file")
parser_reveal = subparsers.add_parser('reveal', help='reveal help')
parser_reveal.add_argument("-i", "--input", dest="input_image_file",
help="Image file")
arguments = parser.parse_args()
if arguments.command == 'hide':
secret = red.hide(arguments.input_image_file, arguments.secret_message)
secret.save(arguments.output_image_file)
elif arguments.command == 'reveal':
secret = red.reveal(arguments.input_image_file)
print(secret)

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -22,34 +24,21 @@ __version__ = "$Revision: 0.7 $"
__date__ = "$Date: 2016/08/25 $" __date__ = "$Date: 2016/08/25 $"
__license__ = "GPLv3" __license__ = "GPLv3"
import argparse try:
from stegano.steganalysis import parity
except:
print("Install Stegano: sudo pip install Stegano")
from PIL import Image from PIL import Image
try: import argparse
from stegano.steganalysis import parity parser = argparse.ArgumentParser(prog='stegano-steganalysis-parity')
except Exception: parser.add_argument("-i", "--input", dest="input_image_file",
print("Install Stegano: pipx install Stegano") help="Image file")
parser.add_argument("-o", "--output", dest="output_image_file",
help="Image file")
arguments = parser.parse_args()
input_image_file = Image.open(arguments.input_image_file)
def main(): output_image = parity.steganalyse(input_image_file)
parser = argparse.ArgumentParser(prog="stegano-steganalysis-parity") output_image.save(arguments.output_image_file)
parser.add_argument(
"-i",
"--input",
dest="input_image_file",
required=True,
help="Input image file.",
)
parser.add_argument(
"-o",
"--output",
dest="output_image_file",
required=True,
help="Output image file.",
)
arguments = parser.parse_args()
input_image_file = Image.open(arguments.input_image_file)
output_image = parity.steganalyse(input_image_file)
output_image.save(arguments.output_image_file)

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -23,22 +25,21 @@ __date__ = "$Date: 2016/08/26 $"
__revision__ = "$Date: 2016/08/26 $" __revision__ = "$Date: 2016/08/26 $"
__license__ = "GPLv3" __license__ = "GPLv3"
import argparse try:
from stegano.steganalysis import statistics
except:
print("Install Stegano: sudo pip install Stegano")
from PIL import Image from PIL import Image
try: import argparse
from stegano.steganalysis import statistics parser = argparse.ArgumentParser(prog='stegano-steganalysis-parity')
except Exception: parser.add_argument("-i", "--input", dest="input_image_file",
print("Install Stegano: sudo pip install Stegano") help="Image file")
parser.add_argument("-o", "--output", dest="output_image_file",
help="Image file")
arguments = parser.parse_args()
input_image_file = Image.open(arguments.input_image_file)
def main(): output_image = statistics.steganalyse(input_image_file)
parser = argparse.ArgumentParser(prog="stegano-steganalysis-parity") output_image.save(arguments.output_image_file)
parser.add_argument("-i", "--input", dest="input_image_file", help="Image file")
parser.add_argument("-o", "--output", dest="output_image_file", help="Image file")
arguments = parser.parse_args()
input_image_file = Image.open(arguments.input_image_file)
output_image = statistics.steganalyse(input_image_file)
output_image.save(arguments.output_image_file)

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# #
# Stéganô documentation build configuration file, created by # Stéganô documentation build configuration file, created by
# sphinx-quickstart on Wed Jul 25 13:33:39 2012. # sphinx-quickstart on Wed Jul 25 13:33:39 2012.
@ -9,162 +10,233 @@
# #
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.')) #sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0' #needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [] extensions = []
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ['_templates']
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = ".rst" source_suffix = '.rst'
# The encoding of source files. # The encoding of source files.
# source_encoding = 'utf-8-sig' #source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = "index" master_doc = 'index'
# General information about the project. # General information about the project.
project = "Stegano" project = u'Stéganô'
copyright = "2010-2025, Cédric Bonhomme" copyright = u'2012-2017, Cédric Bonhomme'
author = "Cédric Bonhomme <cedric@cedricbonhomme.org>"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = "0.11" version = '0.8'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = "0.11.0" release = '0.8.1'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
# language = None #language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
# today = '' #today = ''
# Else, today_fmt is used as the format for a strftime call. # Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y' #today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ["_build"] exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None #default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text. # If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True #add_function_parentheses = True
# If true, the current module name will be prepended to all description # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
# add_module_names = True #add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
# show_authors = False #show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] #modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = "sphinx_rtd_theme" html_theme = 'bizstyle'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
# html_theme_options = {} #html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory. # Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = [] #html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".
# html_title = None #html_title = None
# A shorter title for the navigation bar. Default is the same as html_title. # A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None #html_short_title = None
# The name of an image file (relative to this directory) to place at the top # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # of the sidebar.
# html_logo = None #html_logo = None
# The name of an image file (within the static path) to use as favicon of the # The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large. # pixels large.
# html_favicon = None #html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"] html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y' #html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
# html_use_smartypants = True #html_use_smartypants = True
# Custom sidebar templates, maps document names to template names. # Custom sidebar templates, maps document names to template names.
# html_sidebars = {} #html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to # Additional templates that should be rendered to pages, maps page names to
# template names. # template names.
# html_additional_pages = {} #html_additional_pages = {}
# If false, no module index is generated. # If false, no module index is generated.
# html_domain_indices = True #html_domain_indices = True
# If false, no index is generated.
html_use_index = False
# If true, the index is split into individual pages for each letter. # If true, the index is split into individual pages for each letter.
# html_split_index = False #html_split_index = False
# If true, links to the reST sources are added to the pages. # If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True #html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True #html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True #html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will # If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the # contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served. # base URL from which the finished HTML is served.
# html_use_opensearch = '' #html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml"). # This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None #html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Stgandoc'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
latex_engine = "pdflatex" latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
("index", "Stgan.tex", "Stegano Documentation", "Cédric Bonhomme", "howto"), ('index', 'Stgan.tex', u'Stéganô Documentation',
u'Cédric Bonhomme', 'manual'),
] ]
latex_show_urls = True # The name of an image file (relative to this directory) to place at the top of
latex_show_pagerefs = True # the title page.
#latex_logo = None
ADDITIONAL_PREAMBLE = r""" # For "manual" documents, if this is true, then toplevel headings are parts,
\setcounter{tocdepth}{3} # not chapters.
""" #latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'stgan', u'Stéganô Documentation',
[u'Cédric Bonhomme'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Stgan', u'Stéganô Documentation',
u'Cédric Bonhomme', 'Stgan', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'

View file

@ -6,15 +6,33 @@
Presentation Presentation
============ ============
Stegano_ is a pure Python steganography_ module. .. image:: https://img.shields.io/pypi/pyversions/Stegano.svg?style=flat-square
:target: https://pypi.python.org/pypi/Stegano
.. image:: https://img.shields.io/pypi/v/Stegano.svg?style=flat-square
:target: https://github.com/cedricbonhomme/Stegano/releases/latest
.. image:: https://img.shields.io/pypi/l/Stegano.svg?style=flat-square
:target: https://www.gnu.org/licenses/gpl-3.0.html
.. image:: https://img.shields.io/travis/cedricbonhomme/Stegano.svg?style=flat-square
:target: https://travis-ci.org/cedricbonhomme/Stegano
.. image:: https://img.shields.io/github/stars/cedricbonhomme/Stegano.svg?maxAge=2592000&style=flat-square
:target: https://github.com/cedricbonhomme/Stegano/stargazers
.. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg?style=flat-square
:target: https://saythanks.io/to/cedricbonhomme
Stéganô_ is a pure Python steganography_ module.
Steganography is the art and science of writing hidden messages in such a way Steganography is the art and science of writing hidden messages in such a way
that no one, apart from the sender and intended recipient, suspects the that no one, apart from the sender and intended recipient, suspects the
existence of the message, a form of security through obscurity. existence of the message, a form of security through obscurity.
Consequently, functions provided by Stegano only hide messages, Consequently, functions provided by Stéganô only hide messages,
without encryption. Steganography is often used with cryptography. without encryption. Steganography is often used with cryptography.
Stegano implements these methods of hiding: Stéganô implements these methods of hiding:
- using the red portion of a pixel to hide ASCII messages; - using the red portion of a pixel to hide ASCII messages;
- using the `Least Significant Bit <http://en.wikipedia.org/wiki/Least_significant_bit>`_ (LSB) technique; - using the `Least Significant Bit <http://en.wikipedia.org/wiki/Least_significant_bit>`_ (LSB) technique;
@ -26,8 +44,6 @@ Moreover some methods of steganalysis_ are provided:
- steganalysis of LSB encoding in color images; - steganalysis of LSB encoding in color images;
- statistical steganalysis. - statistical steganalysis.
You can also use Stegano through a `Web service <https://github.com/cedricbonhomme/stegano-web>`_.
Not all functionalities of Stegano are covered.
Requirements Requirements
============ ============
@ -37,7 +53,7 @@ Requirements
- `piexif`_. - `piexif`_.
Tutorial Turorial
======== ========
.. toctree:: .. toctree::
@ -48,21 +64,21 @@ Tutorial
software software
steganalysis steganalysis
You can have a look at the You can also have a look at the
`unit tests <https://github.com/cedricbonhomme/Stegano/tree/master/tests>`_. `unit tests <https://github.com/cedricbonhomme/Stegano/tree/master/tests>`_.
License License
======= =======
Stegano_ is under GPL v3 license. Stéganô_ is under GPL v3 license.
Donation Donation
======== ========
If you wish and if you like Stegano, you can If you wish and if you like Stéganô, you can donate via bitcoin.
`donate <https://github.com/sponsors/cedricbonhomme>`_. My bitcoin address: `1GVmhR9fbBeEh7rP1qNq76jWArDdDQ3otZ <http://blockexplorer.com/address/1GVmhR9fbBeEh7rP1qNq76jWArDdDQ3otZ>`_
@ -73,7 +89,7 @@ Contact
.. _Python: https://www.python.org .. _Python: https://www.python.org
.. _Stegano: https://github.com/cedricbonhomme/Stegano .. _Stéganô: https://github.com/cedricbonhomme/Stegano
.. _`Pillow`: https://pypi.python.org/pypi/Pillow .. _`Pillow`: https://pypi.python.org/pypi/Pillow
.. _`piexif`: https://pypi.python.org/pypi/piexif .. _`piexif`: https://pypi.python.org/pypi/piexif
.. _steganography: http://en.wikipedia.org/wiki/Steganography .. _steganography: http://en.wikipedia.org/wiki/Steganography

View file

@ -3,13 +3,16 @@ Installation
.. code-block:: bash .. code-block:: bash
$ poetry install Stegano $ sudo pip install Stegano
You will be able to use Stegano in your Python programs You will be able to use Stéganô in your Python programs
or as a command line tool. or as a command line tool.
If you want to retrieve the source code (with the unit tests): If you want to retrieve the source code (with the unit tests):
.. code-block:: bash .. code-block:: bash
$ git clone https://github.com/cedricbonhomme/Stegano $ git clone https://github.com/cedricbonhomme/Stegano.git
.. image:: https://api.travis-ci.org/cedricbonhomme/Stegano.svg?branch=master
:target: https://travis-ci.org/cedricbonhomme/Stegano

View file

@ -1,4 +1,4 @@
Using Stegano as a Python module Using Stéganô as a Python module
================================ ================================
You can find more examples in the You can find more examples in the
@ -9,7 +9,8 @@ LSB method
.. code-block:: python .. code-block:: python
Python 3.11.0 (main, Oct 31 2022, 15:15:22) [GCC 12.2.0] on linux Python 3.5.1 (default, Dec 7 2015, 11:33:57)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information. Type "help", "copyright", "credits" or "license" for more information.
>>> from stegano import lsb >>> from stegano import lsb
>>> secret = lsb.hide("./tests/sample-files/Lenna.png", "Hello world!") >>> secret = lsb.hide("./tests/sample-files/Lenna.png", "Hello world!")
@ -26,33 +27,29 @@ Sets are used in order to select the pixels where the message will be hidden.
.. code-block:: python .. code-block:: python
Python 3.11.0 (main, Oct 31 2022, 15:15:22) [GCC 12.2.0] on linux Python 3.5.1 (default, Dec 7 2015, 11:33:57)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information. Type "help", "copyright", "credits" or "license" for more information.
>>> from stegano import lsb >>> from stegano import lsbset
>>> from stegano.lsb import generators >>> from stegano.lsbset import generators
# Hide a secret with the Sieve of Eratosthenes # Hide a secret with the Sieve of Eratosthenes
>>> secret_message = "Hello World!" >>> 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") >>> secret_image.save("./image.png")
# Try to decode with another generator # 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): Traceback (most recent call last):
File "/Users/flavien/.local/share/virtualenvs/Stegano-sY_cwr69/bin/stegano-lsb", line 6, in <module> File "<stdin>", line 1, in <module>
sys.exit(main()) File "/home/cedric/projects/Stegano/stegano/lsbset/lsbset.py", line 111, in reveal
File "/Users/flavien/Perso/dev/Stegano/bin/lsb.py", line 190, in main for color in img_list[generated_number]:
img_encoded = lsb.hide( IndexError: list index out of range
File "/Users/flavien/Perso/dev/Stegano/stegano/lsb/lsb.py", line 63, in hide
hider.encode_pixel((col, row))
File "/Users/flavien/Perso/dev/Stegano/stegano/tools.py", line 165, in encode_pixel
r, g, b, *a = self.encoded_image.getpixel(coordinate)
File "/Users/flavien/.local/share/virtualenvs/Stegano-sY_cwr69/lib/python3.10/site-packages/PIL/Image.py", line 1481, in getpixel
return self.im.getpixel(xy)
IndexError: image index out of range
# Decode with Eratosthenes # Decode with Eratosthenes
>>> message = lsb.reveal("./image.png", generators.eratosthenes()) >>> message = lsbset.reveal("./image.png", generators.eratosthenes())
>>> message >>> message
'Hello World!' 'Hello World!'
@ -97,12 +94,6 @@ Sets are used in order to select the pixels where the message will be hidden.
syracuse syracuse
Generate the sequence of Syracuse. Generate the sequence of Syracuse.
shi_tomashi Shi-Tomachi corner generator of the given points
https://docs.opencv.org/4.x/d4/d8c/tutorial_py_shi_tomasi.html
triangular_numbers Triangular numbers: a(n) = C(n+1,2) = n(n+1)/2 = 0+1+2+...+n.
http://oeis.org/A000217
Description field of the image Description field of the image
@ -112,7 +103,8 @@ For JPEG and TIFF images.
.. code-block:: python .. code-block:: python
Python 3.11.0 (main, Oct 31 2022, 15:15:22) [GCC 12.2.0] on linux Python 3.5.1 (default, Dec 7 2015, 11:33:57)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information. Type "help", "copyright", "credits" or "license" for more information.
>>> from stegano import exifHeader >>> from stegano import exifHeader
>>> secret = exifHeader.hide("./tests/sample-files/20160505T130442.jpg", >>> secret = exifHeader.hide("./tests/sample-files/20160505T130442.jpg",

View file

@ -1,2 +0,0 @@
sphinx
sphinx_rtd_theme

View file

@ -1,4 +1,4 @@
Using Stegano in command line Using Stéganô in command line
============================= =============================
The command ``stegano-lsb`` The command ``stegano-lsb``
@ -12,34 +12,31 @@ Display help
.. code-block:: bash .. code-block:: bash
$ stegano-lsb --help $ stegano-lsb --help
usage: stegano-lsb [-h] {hide,reveal,list-generators} ... usage: stegano-lsb [-h] {hide,reveal} ...
positional arguments: positional arguments:
{hide,reveal,list-generators} {hide,reveal} sub-command help
sub-command help
hide hide help hide hide help
reveal reveal help reveal reveal help
list-generators list-generators help
options: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
.. code-block:: bash .. code-block:: bash
$ stegano-lsb hide --help $ 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 -h, --help show this help message and exit
-i INPUT_IMAGE_FILE, --input INPUT_IMAGE_FILE -i INPUT_IMAGE_FILE, --input INPUT_IMAGE_FILE
Input image file. Input image file.
-e {UTF-8,UTF-32LE}, --encoding {UTF-8,UTF-32LE} -e {UTF-8,UTF-32LE}, --encoding {UTF-8,UTF-32LE}
Specify the encoding of the message to hide. UTF-8 (default) or UTF-32LE. Specify the encoding of the message to hide. UTF-8
-g [GENERATOR_FUNCTION ...], --generator [GENERATOR_FUNCTION ...] (default) or UTF-32LE.
Generator (with optional arguments)
-s SHIFT, --shift SHIFT
Shift for the generator
-m SECRET_MESSAGE Your secret message to hide (non binary). -m SECRET_MESSAGE Your secret message to hide (non binary).
-f SECRET_FILE Your secret to hide (Text or any binary file). -f SECRET_FILE Your secret to hide (Text or any binary file).
-o OUTPUT_IMAGE_FILE, --output OUTPUT_IMAGE_FILE -o OUTPUT_IMAGE_FILE, --output OUTPUT_IMAGE_FILE
@ -49,19 +46,18 @@ Display help
.. code-block:: bash .. code-block:: bash
$ stegano-lsb reveal --help $ 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 -h, --help show this help message and exit
-i INPUT_IMAGE_FILE, --input INPUT_IMAGE_FILE -i INPUT_IMAGE_FILE, --input INPUT_IMAGE_FILE
Input image file. Input image file.
-e {UTF-8,UTF-32LE}, --encoding {UTF-8,UTF-32LE} -e {UTF-8,UTF-32LE}, --encoding {UTF-8,UTF-32LE}
Specify the encoding of the message to reveal. UTF-8 (default) or UTF-32LE. Specify the encoding of the message to reveal. UTF-8
-g [GENERATOR_FUNCTION ...], --generator [GENERATOR_FUNCTION ...] (default) or UTF-32LE.
Generator (with optional arguments) -o SECRET_BINARY Output for the binary secret (Text or any binary
-s SHIFT, --shift SHIFT file).
Shift for the generator
-o SECRET_BINARY Output for the binary secret (Text or any binary file).
Hide and reveal a text message Hide and reveal a text message
@ -96,40 +92,52 @@ 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. 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 .. code-block:: bash
# Hide the message with the Sieve of Eratosthenes # 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 # 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 # 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 # 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 # Try to reveal with Sieve of Eratosthenes
$ stegano-lsb reveal --generator eratosthenes -i ./surprise.png $ stegano-lsb-set reveal --generator eratosthenes -i ./surprise.png
Sometimes it can be useful to skip the first values of a set. For example if you want
to hide several messages or because due to the selected generator
(Fibonacci starts with 0, 1, 1, etc.). Or maybe you just want to add more complexity.
In this case, simply use the optional arguments ``--shift`` or ``-s``:
An other example:
.. code-block:: bash .. code-block:: bash
$ stegano-lsb hide -i ./tests/sample-files/Lenna.png -m 'Shifted secret message' -o ~/Lenna1.png --shift 7 # Hide the message - LSB with a set defined by the identity function (f(x) = x).
$ stegano-lsb reveal -i ~/Lenna1.png --shift 7 stegano-lsb-set hide -i ./tests/sample-files/Montenach.png --generator identity -m 'I like steganography.' -o ./enc-identity.png
Shifted secret message
# 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
List all available generators List all available generators
@ -137,7 +145,7 @@ List all available generators
.. code-block:: bash .. code-block:: bash
$ stegano-lsb list-generators $ stegano-lsb-set list-generators
Generator id: Generator id:
ackermann ackermann
Desciption: Desciption:

View file

@ -7,7 +7,7 @@ Parity
.. code-block:: bash .. code-block:: bash
# Hide the message with Sieve of Eratosthenes # 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 # Steganalysis of the original photo
stegano-steganalysis-parity -i ./tests/sample-files/20160505T130442.jpg -o ./surprise_st_original.png 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 stegano-steganalysis-parity -i ./surprise.png -o ./surprise_st_secret.png
# Reveal with Sieve of Eratosthenes # Reveal with Sieve of Eratosthenes
stegano-lsb reveal -i ./surprise.png --generator eratosthenes stegano-lsb-set reveal -i ./surprise.png --generator eratosthenes

1095
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,93 +0,0 @@
[build-system]
requires = ["poetry-core>=2.0"]
build-backend = "poetry.core.masonry.api"
[project]
name = "stegano"
version = "1.0.1"
description = "A pure Python Steganography module."
authors = [
{name = "Cédric Bonhomme", email= "cedric@cedricbonhomme.org"}
]
license = "GPL-3.0-or-later"
readme = "README.md"
keywords = ["Steganography", "Security", "Stegano"]
dynamic = ["classifiers"]
requires-python = ">=3.10,<4.0"
dependencies = [
"pillow (>=9.5,<11.0)",
"piexif (>=1.1.3)",
"crayons (>=0.4.0)",
"opencv-python (>=4.8.1.78)"
]
[project.urls]
Homepage = "https://github.com/cedricbonhomme/Stegano"
Changelog = "https://github.com/cedricbonhomme/Stegano/blob/master/CHANGELOG.md"
Repository = "https://github.com/cedricbonhomme/Stegano"
Documentation = "https://stegano.readthedocs.io"
[project.scripts]
stegano-lsb = "stegano.console.lsb:main"
stegano-red = "stegano.console.red:main"
stegano-steganalysis-parity = "stegano.console.parity:main"
stegano-steganalysis-statistics = "stegano.console.statistics:main"
[tool.poetry]
requires-poetry = ">=2.0"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Topic :: Security",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"
]
include = [
"README.md",
"COPYING",
"CHANGELOG.md",
"docs/**/*",
]
[tool.poetry.group.dev.dependencies]
mypy = "^1.8.0"
flake8 = "^6.0.0"
nose2 = "^0.14.0"
Sphinx = "^6.2.1"
pre-commit = "^3.6.0"
[tool.poetry.group.dev]
optional = true
[tool.mypy]
python_version = "3.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"

4
requirements.dev.txt Normal file
View file

@ -0,0 +1,4 @@
pep8
coverage
coveralls
mypy

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
pillow
piexif
crayons

2
setup.cfg Normal file
View file

@ -0,0 +1,2 @@
[metadata]
description-file = README.rst

57
setup.py Normal file
View file

@ -0,0 +1,57 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from setuptools import setup
packages = [
'stegano',
'stegano.red',
'stegano.exifHeader',
'stegano.lsb',
'stegano.lsbset',
'stegano.steganalysis'
]
scripts = [
'bin/stegano-lsb',
'bin/stegano-lsb-set',
'bin/stegano-red',
'bin/stegano-steganalysis-parity',
'bin/stegano-steganalysis-statistics'
]
requires = ['pillow', 'piexif', 'crayons']
with open('README.rst', 'r') as f:
readme = f.read()
with open('CHANGELOG.rst', 'r') as f:
changelog = f.read()
setup(
name='Stegano',
version='0.8.2',
author='Cédric Bonhomme',
author_email='cedric@cedricbonhomme.org',
packages=packages,
include_package_data=True,
scripts=scripts,
url='https://github.com/cedricbonhomme/Stegano',
description='A pure Python Steganography module.',
long_description=readme + '\n|\n\n' + changelog,
platforms = ['Linux'],
license='GPLv3',
install_requires=requires,
zip_safe=False,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: Science/Research',
'Topic :: Security',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)'
]
)

View file

@ -1,5 +1,9 @@
#!/usr/bin/env python #!/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

View file

@ -1,227 +0,0 @@
#!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module.
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.7 $"
__date__ = "$Date: 2016/03/18 $"
__revision__ = "$Date: 2019/06/04 $"
__license__ = "GPLv3"
import inspect
import crayons
try:
from stegano import lsb
from stegano.lsb import generators
except Exception:
print("Install stegano: pipx install Stegano")
import argparse
from stegano import tools
class ValidateGenerator(argparse.Action):
def __call__(self, parser, args, values, option_string=None):
valid_generators = [
generator[0]
for generator in inspect.getmembers(generators, inspect.isfunction)
]
# Verify that the generator is valid
generator = values[0]
if generator not in valid_generators:
raise ValueError("Unknown generator: %s" % generator)
# Set the generator_function arg of the parser
setattr(args, self.dest, values)
def main():
parser = argparse.ArgumentParser(prog="stegano-lsb")
subparsers = parser.add_subparsers(
help="sub-command help", dest="command", required=True
)
# Subparser: Hide
parser_hide = subparsers.add_parser("hide", help="hide help")
# Original image
parser_hide.add_argument(
"-i",
"--input",
dest="input_image_file",
required=True,
help="Input image file.",
)
parser_hide.add_argument(
"-e",
"--encoding",
dest="encoding",
choices=tools.ENCODINGS.keys(),
default="UTF-8",
help="Specify the encoding of the message to hide."
" UTF-8 (default) or UTF-32LE.",
)
# Generator
parser_hide.add_argument(
"-g",
"--generator",
dest="generator_function",
action=ValidateGenerator,
nargs="*",
required=False,
default=None,
help="Generator (with optional arguments)",
)
# Shift the message to hide
parser_hide.add_argument(
"-s", "--shift", dest="shift", default=0, help="Shift for the generator"
)
group_secret = parser_hide.add_mutually_exclusive_group(required=True)
# Non binary secret message to hide
group_secret.add_argument(
"-m", dest="secret_message", help="Your secret message to hide (non binary)."
)
# Binary secret message to hide
group_secret.add_argument(
"-f", dest="secret_file", help="Your secret to hide (Text or any binary file)."
)
# Image containing the secret
parser_hide.add_argument(
"-o",
"--output",
dest="output_image_file",
required=True,
help="Output image containing the secret.",
)
# Subparser: Reveal
parser_reveal = subparsers.add_parser("reveal", help="reveal help")
parser_reveal.add_argument(
"-i",
"--input",
dest="input_image_file",
required=True,
help="Input image file.",
)
parser_reveal.add_argument(
"-e",
"--encoding",
dest="encoding",
choices=tools.ENCODINGS.keys(),
default="UTF-8",
help="Specify the encoding of the message to reveal."
" UTF-8 (default) or UTF-32LE.",
)
# Generator
parser_reveal.add_argument(
"-g",
"--generator",
dest="generator_function",
action=ValidateGenerator,
nargs="*",
required=False,
help="Generator (with optional arguments)",
)
# Shift the message to reveal
parser_reveal.add_argument(
"-s", "--shift", dest="shift", default=0, help="Shift for the generator"
)
parser_reveal.add_argument(
"-o",
dest="secret_binary",
help="Output for the binary secret (Text or any binary file).",
)
# Subparser: List generators
subparsers.add_parser("list-generators", help="list-generators help")
arguments = parser.parse_args()
if arguments.command != "list-generators":
if not arguments.generator_function:
generator = None
else:
try:
if arguments.generator_function[0] == "LFSR":
# Compute the size of the image for use by the LFSR generator if needed
tmp = tools.open_image(arguments.input_image_file)
size = tmp.width * tmp.height
tmp.close()
arguments.generator_function.append(size)
if len(arguments.generator_function) > 1:
generator = getattr(generators, arguments.generator_function[0])(
*[int(e) for e in arguments.generator_function[1:]]
)
else:
generator = getattr(generators, arguments.generator_function[0])()
except AttributeError:
print(f"Unknown generator: {arguments.generator_function}")
exit(1)
if arguments.command == "hide":
if arguments.secret_message is not None:
secret = arguments.secret_message
elif arguments.secret_file != "":
secret = tools.binary2base64(arguments.secret_file)
img_encoded = lsb.hide(
image=arguments.input_image_file,
message=secret,
generator=generator,
shift=int(arguments.shift),
encoding=arguments.encoding,
)
try:
img_encoded.save(arguments.output_image_file)
except Exception as e:
# If hide() returns an error (Too long message).
print(e)
elif arguments.command == "reveal":
try:
secret = lsb.reveal(
encoded_image=arguments.input_image_file,
generator=generator,
shift=int(arguments.shift),
encoding=arguments.encoding,
)
except IndexError:
print("Impossible to detect message.")
exit(0)
if arguments.secret_binary is not None:
data = tools.base642binary(secret)
with open(arguments.secret_binary, "wb") as f:
f.write(data)
else:
print(secret)
elif arguments.command == "list-generators":
all_generators = inspect.getmembers(generators, inspect.isfunction)
for generator in all_generators:
print("Generator id:")
print(f" {crayons.green(generator[0], bold=True)}")
print("Desciption:")
print(f" {generator[1].__doc__}")

View file

@ -1,61 +0,0 @@
#!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module.
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.1 $"
__date__ = "$Date: 2017/02/06 $"
__license__ = "GPLv3"
import argparse
try:
from stegano import red
except Exception:
print("Install stegano: sudo pip install Stegano")
def main():
parser = argparse.ArgumentParser(prog="stegano-red")
subparsers = parser.add_subparsers(help="sub-command help", dest="command")
parser_hide = subparsers.add_parser("hide", help="hide help")
parser_hide.add_argument(
"-i", "--input", dest="input_image_file", help="Image file"
)
parser_hide.add_argument(
"-m", dest="secret_message", help="Your secret message to hide (non binary)."
)
parser_hide.add_argument(
"-o", "--output", dest="output_image_file", help="Image file"
)
parser_reveal = subparsers.add_parser("reveal", help="reveal help")
parser_reveal.add_argument(
"-i", "--input", dest="input_image_file", help="Image file"
)
arguments = parser.parse_args()
if arguments.command == "hide":
secret = red.hide(arguments.input_image_file, arguments.secret_message)
secret.save(arguments.output_image_file)
elif arguments.command == "reveal":
secret = red.reveal(arguments.input_image_file)
print(secret)

View file

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
#-*- coding: utf-8 -*-
from .exifHeader import hide, reveal from .exifHeader import *
__all__ = ["hide", "reveal"]

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -23,32 +25,25 @@ __date__ = "$Date: 2016/05/26 $"
__revision__ = "$Date: 2017/01/18 $" __revision__ = "$Date: 2017/01/18 $"
__license__ = "GPLv3" __license__ = "GPLv3"
from PIL import Image
import piexif import piexif
from stegano import tools def hide(input_image_file, img_enc, secret_message = None, secret_file = None, img_format = None):
"""Hide a message (string) in an image.
"""
def hide(
input_image_file,
img_enc,
secret_message=None,
secret_file=None,
img_format=None,
):
"""Hide a message (string) in an image."""
from base64 import b64encode
from zlib import compress from zlib import compress
from base64 import b64encode
if secret_file is not None: if secret_file != None:
with open(secret_file) as f: with open(secret_file, "r") as f:
secret_message = f.read() secret_message = f.read()
try: try:
text = compress(b64encode(bytes(secret_message, "utf-8"))) text = compress(b64encode(bytes(secret_message, "utf-8")))
except Exception: except:
text = compress(b64encode(secret_message)) text = compress(b64encode(secret_message))
img = tools.open_image(input_image_file) img = Image.open(input_image_file)
if img_format is None: if img_format is None:
img_format = img.format img_format = img.format
@ -66,20 +61,20 @@ def hide(
def reveal(input_image_file): def reveal(input_image_file):
"""Find a message in an image.""" """Find a message in an image.
"""
from base64 import b64decode from base64 import b64decode
from zlib import decompress from zlib import decompress
img = tools.open_image(input_image_file) img = Image.open(input_image_file)
try: try:
if img.format in ["JPEG", "TIFF"]: if img.format in ['JPEG', 'TIFF']:
if "exif" in img.info: if 'exif' in img.info:
exif_dict = piexif.load(img.info.get("exif", b"")) exif_dict = piexif.load(img.info.get("exif", b''))
description_key = piexif.ImageIFD.ImageDescription description_key = piexif.ImageIFD.ImageDescription
encoded_message = exif_dict["0th"][description_key] encoded_message = exif_dict["0th"][description_key]
else: else:
encoded_message = b"" encoded_message = b''
else: else:
raise ValueError("Given file is neither JPEG nor TIFF.") raise ValueError("Given file is neither JPEG nor TIFF.")
finally: finally:
@ -92,70 +87,41 @@ if __name__ == "__main__":
# Point of entry in execution mode. # Point of entry in execution mode.
# TODO: improve the management of arguments # TODO: improve the management of arguments
from optparse import OptionParser from optparse import OptionParser
parser = OptionParser(version=__version__) parser = OptionParser(version=__version__)
parser.add_option( parser.add_option('--hide', action='store_true', default=False,
"--hide", help="Hides a message in an image.")
action="store_true", parser.add_option('--reveal', action='store_true', default=False,
default=False, help="Reveals the message hided in an image.")
help="Hides a message in an image.",
)
parser.add_option(
"--reveal",
action="store_true",
default=False,
help="Reveals the message hided in an image.",
)
# Original image # Original image
parser.add_option( parser.add_option("-i", "--input", dest="input_image_file",
"-i", "--input", dest="input_image_file", help="Input image file." help="Input image file.")
)
# Image containing the secret # Image containing the secret
parser.add_option( parser.add_option("-o", "--output", dest="output_image_file",
"-o", help="Output image containing the secret.")
"--output",
dest="output_image_file",
help="Output image containing the secret.",
)
# Secret raw message to hide # Secret raw message to hide
parser.add_option( parser.add_option("-m", "--secret-message", dest="secret_message",
"-m", help="Your raw secret message to hide.")
"--secret-message",
dest="secret_message",
help="Your raw secret message to hide.",
)
# Secret text file to hide. # Secret text file to hide.
parser.add_option( parser.add_option("-f", "--secret-file", dest="secret_file",
"-f", help="Your secret text file to hide.")
"--secret-file",
dest="secret_file",
help="Your secret text file to hide.",
)
parser.set_defaults( parser.set_defaults(input_image_file = './pictures/Elisha-Cuthbert.jpg',
input_image_file="./pictures/Elisha-Cuthbert.jpg", output_image_file = './pictures/Elisha-Cuthbert_enc.jpg',
output_image_file="./pictures/Elisha-Cuthbert_enc.jpg", secret_message = '', secret_file = '')
secret_message="",
secret_file="",
)
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
if options.hide: if options.hide:
if options.secret_message != "" and options.secret_file == "": if options.secret_message != "" and options.secret_file == "":
hide( hide(input_image_file=options.input_image_file, \
input_image_file=options.input_image_file, img_enc=options.output_image_file, \
img_enc=options.output_image_file, secret_message=options.secret_message)
secret_message=options.secret_message,
)
elif options.secret_message == "" and options.secret_file != "": elif options.secret_message == "" and options.secret_file != "":
hide( hide(input_image_file=options.input_image_file, \
input_image_file=options.input_image_file, img_enc=options.output_image_file, \
img_enc=options.output_image_file, secret_file=options.secret_file)
secret_file=options.secret_file,
)
elif options.reveal: elif options.reveal:
reveal(input_image_file=options.input_image_file) reveal(input_image_file=options.input_image_file)

View file

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
#-*- coding: utf-8 -*-
from .lsb import hide, reveal from .lsb import *
__all__ = ["hide", "reveal"]

View file

@ -1,254 +0,0 @@
#!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module.
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.3 $"
__date__ = "$Date: 2011/12/28 $"
__revision__ = "$Date: 2021/11/29 $"
__license__ = "GPLv3"
import itertools
import math
from typing import Any, Dict, Iterator, List
import cv2
import numpy as np
def identity() -> Iterator[int]:
"""f(x) = x"""
n = 0
while True:
yield n
n += 1
def triangular_numbers() -> Iterator[int]:
"""Triangular numbers: a(n) = C(n+1,2) = n(n+1)/2 = 0+1+2+...+n.
http://oeis.org/A000217
"""
n = 0
while True:
yield (n * (n + 1)) // 2
n += 1
def fermat() -> Iterator[int]:
"""Generate the n-th Fermat Number.
https://oeis.org/A000215
"""
y = 3
while True:
yield y
y = pow(y - 1, 2) + 1
def mersenne() -> Iterator[int]:
"""Generate 2^p - 1, where p is prime.
https://oeis.org/A001348
"""
prime_numbers = eratosthenes()
while True:
yield 2 ** next(prime_numbers) - 1
def eratosthenes() -> Iterator[int]:
"""Generate the prime numbers with the sieve of Eratosthenes.
https://oeis.org/A000040
"""
d: Dict[int, List[int]] = {}
for i in itertools.count(2):
if i in d:
for j in d[i]:
d[i + j] = d.get(i + j, []) + [j]
del d[i]
else:
d[i * i] = [i]
yield i
def composite() -> Iterator[int]:
"""Generate the composite numbers using the sieve of Eratosthenes.
https://oeis.org/A002808
"""
p1 = 3
for p2 in eratosthenes():
yield from range(p1 + 1, p2)
p1 = p2
def carmichael() -> Iterator[int]:
"""Composite numbers n such that a^(n-1) == 1 (mod n) for every a coprime
to n.
https://oeis.org/A002997
"""
for m in composite():
for a in range(2, m):
if pow(a, m, m) != a:
break
else:
yield m
def ackermann_slow(m: int, n: int) -> int:
"""Ackermann number."""
if m == 0:
return n + 1
elif n == 0:
return ackermann_slow(m - 1, 1)
else:
return ackermann_slow(m - 1, ackermann_slow(m, n - 1))
def ackermann_naive(m: int) -> Iterator[int]:
"""Naive Ackermann encapsulated in a generator."""
n = 0
while True:
yield ackermann_slow(m, n)
n += 1
def ackermann_fast(m: int, n: int) -> int:
"""Ackermann number."""
while m >= 4:
if n == 0:
n = 1
else:
n = ackermann_fast(m, n - 1)
m -= 1
if m == 3:
return (1 << n + 3) - 3
elif m == 2:
return (n << 1) + 3
elif m == 1:
return n + 2
else:
return n + 1
def ackermann(m: int) -> Iterator[int]:
"""Ackermann encapsulated in a generator."""
n = 0
while True:
yield ackermann_fast(m, n)
n += 1
def fibonacci() -> Iterator[int]:
"""Generate the sequence of Fibonacci.
https://oeis.org/A000045
"""
a, b = 1, 2
while True:
yield a
a, b = b, a + b
def log_gen() -> Iterator[int]:
"""Logarithmic generator."""
y = 1
while True:
adder = max(1, math.pow(10, int(math.log10(y))))
yield int(y)
y = y + int(adder)
polys = {
2: [2, 1],
3: [3, 1],
4: [4, 1],
5: [5, 2],
6: [6, 1],
7: [7, 1],
8: [8, 4, 3, 2],
9: [9, 4],
10: [10, 3],
11: [11, 2],
12: [12, 6, 4, 1],
13: [13, 4, 3, 1],
14: [14, 8, 6, 1],
15: [15, 1],
16: [16, 12, 3, 1],
17: [17, 3],
18: [18, 7],
19: [19, 5, 2, 1],
20: [20, 3],
21: [21, 2],
22: [22, 1],
23: [23, 5],
24: [24, 7, 2, 1],
25: [25, 3],
26: [26, 6, 2, 1],
27: [27, 5, 2, 1],
28: [28, 3],
29: [29, 2],
30: [30, 23, 2, 1],
31: [31, 3],
}
def LFSR(m: int) -> Iterator[int]:
"""LFSR generator of the given size
https://en.wikipedia.org/wiki/Linear-feedback_shift_register
"""
n: int = m.bit_length() - 1
# Set initial state to {1 0 0 ... 0}
state: List[int] = [0] * n
state[0] = 1
feedback: int = 0
poly: List[int] = polys[n]
while True:
# Compute the feedback bit
feedback = 0
for i in range(len(poly)):
feedback = feedback ^ state[poly[i] - 1]
# Roll the registers
state.pop()
# Add the feedback bit
state.insert(0, feedback)
# Convert the registers to an int
out = sum(e * (2**i) for i, e in enumerate(state))
yield out
def shi_tomashi(
image_path: str,
max_corners: int = 100,
quality: float = 0.01,
min_distance: float = 10.0,
) -> Iterator[int]:
"""Shi-Tomachi corner generator of the given points
https://docs.opencv.org/4.x/d4/d8c/tutorial_py_shi_tomasi.html
"""
image = cv2.imread(image_path)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
corners: np.ndarray = cv2.goodFeaturesToTrack(
gray, max_corners, quality, min_distance
)
corners_int: np.ndarray[Any, np.dtype[np.signedinteger[Any]]] = np.array(
np.intp(corners)
)
i = 0
while True:
x, y = corners_int[i].ravel()
# Compute the pixel number with top left of image as origin
# using coordinates of the corner.
# (y * number of pixels a row) + pixels left in last row
yield (y * image.shape[1]) + x
i += 1

141
stegano/lsb/lsb.py Normal file → Executable file
View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -18,73 +20,106 @@
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme" __author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.7 $" __version__ = "$Revision: 0.3 $"
__date__ = "$Date: 2016/03/13 $" __date__ = "$Date: 2016/08/04 $"
__revision__ = "$Date: 2019/05/31 $" __revision__ = "$Date: 2017/05/04 $"
__license__ = "GPLv3" __license__ = "GPLv3"
from typing import IO, Iterator, Union import sys
from PIL import Image
from typing import Union, IO
from stegano import tools from stegano import tools
from .generators import identity def hide(input_image: Union[str, IO[bytes]],
def hide(
image: Union[str, IO[bytes]],
message: str, message: str,
generator: Union[None, Iterator[int]] = None, encoding: str = 'UTF-8',
shift: int = 0, auto_convert_rgb: bool = False):
encoding: str = "UTF-8",
auto_convert_rgb: bool = False,
):
"""Hide a message (string) in an image with the """Hide a message (string) in an image with the
LSB (Least Significant Bit) technique. LSB (Least Significant Bit) technique.
""" """
hider = tools.Hider(image, message, encoding, auto_convert_rgb) message_length = len(message)
width = hider.encoded_image.width assert message_length != 0, "message length is zero"
if not generator: img = Image.open(input_image)
generator = identity()
while shift != 0: if img.mode not in ['RGB', 'RGBA']:
next(generator) if not auto_convert_rgb:
shift -= 1 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')
while hider.encode_another_pixel(): encoded = img.copy()
generated_number = next(generator) width, height = img.size
index = 0
col = generated_number % width message = str(message_length) + ":" + str(message)
row = int(generated_number / width) message_bits = "".join(tools.a2bits_list(message, encoding))
message_bits += '0' * ((3 - (len(message_bits) % 3)) % 3)
hider.encode_pixel((col, row)) 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 index + 3 <= len_message_bits :
return hider.encoded_image # Get the colour component.
pixel = img.getpixel((col, row))
r = pixel[0]
g = pixel[1]
b = pixel[2]
# 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':
encoded.putpixel((col, row), (r, g, b, pixel[3]))
else:
encoded.putpixel((col, row), (r, g, b))
index += 3
else:
img.close()
return encoded
def reveal( def reveal(input_image: Union[str, IO[bytes]], encoding='UTF-8'):
encoded_image: Union[str, IO[bytes]], """Find a message in an image (with the LSB technique).
generator: Union[None, Iterator[int]] = None, """
shift: int = 0, img = Image.open(input_image)
encoding: str = "UTF-8", width, height = img.size
close_file: bool = True, buff, count = 0, 0
): bitab = []
"""Find a message in an image (with the LSB technique).""" limit = None
revealer = tools.Revealer(encoded_image, encoding, close_file) for row in range(height):
width = revealer.encoded_image.width for col in range(width):
if not generator: # pixel = [r, g, b] or [r,g,b,a]
generator = identity() 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 == None:
try:
limit = int("".join(bitab[:-1]))
except:
pass
while shift != 0: if len(bitab)-len(str(limit))-1 == limit :
next(generator) img.close()
shift -= 1 return "".join(bitab)[len(str(limit))+1:]
while True:
generated_number = next(generator)
col = generated_number % width
row = int(generated_number / width)
if revealer.decode_pixel((col, row)):
return revealer.secret_message

View file

@ -0,0 +1,4 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
from .lsbset import *

View file

@ -0,0 +1,146 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.3 $"
__date__ = "$Date: 2011/12/28 $"
__revision__ = "$Date: 2017/03/10 $"
__license__ = "GPLv3"
import math
import itertools
from typing import Iterator, List, Dict
def identity() -> Iterator[int]:
"""f(x) = x
"""
n = 0
while True:
yield n
n += 1
def triangular_numbers() -> Iterator[int]:
"""Triangular numbers: a(n) = C(n+1,2) = n(n+1)/2 = 0+1+2+...+n.
http://oeis.org/A000217
"""
n = 0
while True:
yield (n*(n+1))//2
n += 1
def fermat() -> Iterator[int]:
"""Generate the n-th Fermat Number.
https://oeis.org/A000215
"""
y = 3
while True:
yield y
y = pow(y-1,2)+1
def mersenne() -> Iterator[int]:
"""Generate 2^p - 1, where p is prime.
https://oeis.org/A001348
"""
prime_numbers = eratosthenes()
while True:
yield 2**next(prime_numbers) - 1
def eratosthenes() -> Iterator[int]:
"""Generate the prime numbers with the sieve of Eratosthenes.
https://oeis.org/A000040
"""
d = {} # type: Dict[int, List[int]]
for i in itertools.count(2):
if i in d:
for j in d[i]:
d[i + j] = d.get(i + j, []) + [j]
del d[i]
else:
d[i * i] = [i]
yield i
def composite() -> Iterator[int]:
"""Generate the composite numbers using the sieve of Eratosthenes.
https://oeis.org/A002808
"""
p1 = 3
for p2 in eratosthenes():
for n in range(p1 + 1, p2):
yield n
p1 = p2
def carmichael() -> Iterator[int]:
"""Composite numbers n such that a^(n-1) == 1 (mod n) for every a coprime
to n.
https://oeis.org/A002997
"""
for m in composite():
for a in range(2, m):
if pow(a,m,m) != a:
break
else:
yield m
def ackermann_naive(m: int, n: int) -> int:
"""Ackermann number.
"""
if m == 0:
return n + 1
elif n == 0:
return ackermann(m - 1, 1)
else:
return ackermann(m - 1, ackermann(m, n - 1))
def ackermann(m: int, n: int) -> int:
"""Ackermann number.
"""
while m >= 4:
if n == 0:
n = 1
else:
n = ackermann(m, n - 1)
m -= 1
if m == 3:
return (1 << n + 3) - 3
elif m == 2:
return (n << 1) + 3
elif m == 1:
return n + 2
else:
return n + 1
def fibonacci() -> Iterator[int]:
"""Generate the sequence of Fibonacci.
https://oeis.org/A000045
"""
a, b = 1, 2
while True:
yield a
a, b = b, a + b
def log_gen() -> Iterator[int]:
"""Logarithmic generator.
"""
y = 1
while True:
adder = max(1, math.pow(10, int(math.log10(y))))
yield int(y)
y = y + int(adder)

125
stegano/lsbset/lsbset.py Normal file
View file

@ -0,0 +1,125 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.5 $"
__date__ = "$Date: 2016/03/13 $"
__revision__ = "$Date: 2017/05/04 $"
__license__ = "GPLv3"
import sys
from PIL import Image
from typing import Union, Iterator, IO
from stegano import tools
from . import generators
def hide(input_image: Union[str, IO[bytes]],
message: str,
generator: Iterator[int],
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 = Image.open(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 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[0])
else:
img_list[generated_number] = (r, g , b)
index += 3
# create empty new image of appropriate format
encoded = Image.new('RGB', (img.size))
# insert saved data into the image
encoded.putdata(img_list)
return encoded
def reveal(input_image: Union[str, IO[bytes]],
generator: Iterator[int],
encoding: str = 'UTF-8'):
"""Find a message in an image (with the LSB technique).
"""
img = Image.open(input_image)
img_list = list(img.getdata())
width, height = img.size
buff, count = 0, 0
bitab = []
limit = None
while True:
generated_number = next(generator)
# color = [r, g, b]
for color in img_list[generated_number]:
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 == 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:]

View file

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
#-*- coding: utf-8 -*-
from .red import hide, reveal from .red import *
__all__ = ["hide", "reveal"]

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stéganô is a basic Python Steganography module. # -*- coding: utf-8 -*-
# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic 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://github.com/cedricbonhomme/Stegano
# #
@ -23,10 +25,10 @@ __date__ = "$Date: 2010/10/01 $"
__revision__ = "$Date: 2017/02/06 $" __revision__ = "$Date: 2017/02/06 $"
__license__ = "GPLv3" __license__ = "GPLv3"
from typing import IO, Union import sys
from stegano import tools
from PIL import Image
from typing import Union, IO
def hide(input_image: Union[str, IO[bytes]], message: str): def hide(input_image: Union[str, IO[bytes]], message: str):
""" """
@ -39,7 +41,7 @@ def hide(input_image: Union[str, IO[bytes]], message: str):
message_length = len(message) message_length = len(message)
assert message_length != 0, "message message_length is zero" assert message_length != 0, "message message_length is zero"
assert message_length < 255, "message is too long" assert message_length < 255, "message is too long"
img = tools.open_image(input_image) img = Image.open(input_image)
# Use a copy of image to hide the text in # Use a copy of image to hide the text in
encoded = img.copy() encoded = img.copy()
width, height = img.size width, height = img.size
@ -51,16 +53,15 @@ def hide(input_image: Union[str, IO[bytes]], message: str):
if row == 0 and col == 0 and index < message_length: if row == 0 and col == 0 and index < message_length:
asc = message_length asc = message_length
elif index <= message_length: elif index <= message_length:
c = message[index - 1] c = message[index -1]
asc = ord(c) asc = ord(c)
else: else:
asc = r asc = r
encoded.putpixel((col, row), (asc, g, b)) encoded.putpixel((col, row), (asc, g , b))
index += 1 index += 1
img.close() img.close()
return encoded return encoded
def reveal(input_image: Union[str, IO[bytes]]): def reveal(input_image: Union[str, IO[bytes]]):
""" """
Find a message in an image. Find a message in an image.
@ -69,7 +70,7 @@ def reveal(input_image: Union[str, IO[bytes]]):
hidden message characters (ASCII values). hidden message characters (ASCII values).
The red value of the first pixel is used for message_length of string. The red value of the first pixel is used for message_length of string.
""" """
img = tools.open_image(input_image) img = Image.open(input_image)
width, height = img.size width, height = img.size
message = "" message = ""
index = 0 index = 0

View file

@ -1 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
#-*- coding: utf-8 -*-
from .parity import *
from .statistics import *

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -18,26 +20,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme" __author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.9.4 $" __version__ = "$Revision: 0.1 $"
__date__ = "$Date: 2010/10/01 $" __date__ = "$Date: 2010/10/01 $"
__revision__ = "$Date: 2019/06/06 $"
__license__ = "GPLv3" __license__ = "GPLv3"
from PIL import Image from PIL import Image
def steganalyse(img):
def steganalyse(img: Image.Image) -> Image.Image:
""" """
Steganlysis of the LSB technique. Steganlysis of the LSB technique.
""" """
encoded = Image.new(img.mode, (img.size)) encoded = img.copy()
width, height = img.size width, height = img.size
bits = ""
for row in range(height): for row in range(height):
for col in range(width): for col in range(width):
if pixel := img.getpixel((col, row)): r, g, b = img.getpixel((col, row))
r, g, b = pixel[0:3]
else:
raise Exception("Error during steganlysis.")
if r % 2 == 0: if r % 2 == 0:
r = 0 r = 0
else: else:
@ -50,5 +48,5 @@ def steganalyse(img: Image.Image) -> Image.Image:
b = 0 b = 0
else: else:
b = 255 b = 255
encoded.putpixel((col, row), (r, g, b)) encoded.putpixel((col, row), (r, g , b))
return encoded return encoded

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -20,33 +22,35 @@
__author__ = "Cedric Bonhomme" __author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.2 $" __version__ = "$Revision: 0.2 $"
__date__ = "$Date: 2010/10/01 $" __date__ = "$Date: 2010/10/01 $"
__revision__ = "$Date: 2021/11/01 $" __revision__ = "$Date: 2016/08/26 $"
__license__ = "GPLv3" __license__ = "GPLv3"
import typing import operator
from collections import Counter, OrderedDict
from PIL import Image
from collections import Counter
from collections import OrderedDict
def steganalyse(img): def steganalyse(img):
""" """
Steganlysis of the LSB technique. Steganlysis of the LSB technique.
""" """
encoded = img.copy()
width, height = img.size width, height = img.size
colours_counter: typing.Counter[int] = Counter() colours_counter = Counter() # type: Counter[int]
for row in range(height): for row in range(height):
for col in range(width): for col in range(width):
r, g, b = img.getpixel((col, row)) r, g, b = img.getpixel((col, row))
colours_counter[r] += 1 colours_counter[r] += 1
most_common = colours_counter.most_common(10) most_common = colours_counter.most_common(10)
dict_colours = OrderedDict( dict_colours = OrderedDict(sorted(list(colours_counter.items()),
sorted(list(colours_counter.items()), key=lambda t: t[1]) key=lambda t: t[1]))
)
colours: float = 0 colours = 0 # type: float
for colour in list(dict_colours.keys()): for colour in list(dict_colours.keys()):
colours += colour colours += colour
colours = colours / len(dict_colours) colours = colours / len(dict_colours)
# return colours.most_common(10) #return colours.most_common(10)
return list(dict_colours.keys())[:30], most_common return list(dict_colours.keys())[:30], most_common

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. # -*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -25,13 +27,13 @@ __license__ = "GPLv3"
import base64 import base64
import itertools import itertools
from typing import List, Iterator, Tuple, Union
from functools import reduce from functools import reduce
from typing import IO, List, Union
from PIL import Image
ENCODINGS = {"UTF-8": 8, "UTF-32LE": 32}
ENCODINGS = {
'UTF-8': 8,
'UTF-32LE': 32
}
def a2bits(chars: str) -> str: def a2bits(chars: str) -> str:
"""Converts a string to its bits representation as a string of 0's and 1's. """Converts a string to its bits representation as a string of 0's and 1's.
@ -39,10 +41,9 @@ def a2bits(chars: str) -> str:
>>> a2bits("Hello World!") >>> a2bits("Hello World!")
'010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001' '010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001'
""" """
return bin(reduce(lambda x, y: (x << 8) + y, (ord(c) for c in chars), 1))[3:] return bin(reduce(lambda x, y : (x<<8)+y, (ord(c) for c in chars), 1))[3:]
def a2bits_list(chars: str, encoding: str ='UTF-8') -> List[str]:
def a2bits_list(chars: str, encoding: str = "UTF-8") -> List[str]:
"""Convert a string to its bits representation as a list of 0's and 1's. """Convert a string to its bits representation as a list of 0's and 1's.
>>> a2bits_list("Hello World!") >>> a2bits_list("Hello World!")
@ -61,20 +62,20 @@ def a2bits_list(chars: str, encoding: str = "UTF-8") -> List[str]:
>>> "".join(a2bits_list("Hello World!")) >>> "".join(a2bits_list("Hello World!"))
'010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001' '010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001'
""" """
return [bin(ord(x))[2:].rjust(ENCODINGS[encoding], "0") for x in chars] return [bin(ord(x))[2:].rjust(ENCODINGS[encoding],"0") for x in chars]
def bs(s: int) -> str: def bs(s: int) -> str:
"""Converts an int to its bits representation as a string of 0's and 1's.""" """Converts an int to its bits representation as a string of 0's and 1's.
return str(s) if s <= 1 else bs(s >> 1) + str(s & 1) """
return str(s) if s<=1 else bs(s>>1) + str(s&1)
def setlsb(component: int, bit: str) -> int: def setlsb(component: int, bit: str) -> int:
"""Set Least Significant Bit of a colour component.""" """Set Least Significant Bit of a colour component.
"""
return component & ~1 | int(bit) 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. """Returns an iterator which groups n items at a time.
Any final partial tuple will be padded with the fillvalue Any final partial tuple will be padded with the fillvalue
@ -84,7 +85,6 @@ def n_at_a_time(items: List[int], n: int, fillvalue: str):
it = iter(items) it = iter(items)
return itertools.zip_longest(*[it] * n, fillvalue=fillvalue) return itertools.zip_longest(*[it] * n, fillvalue=fillvalue)
def binary2base64(binary_file: str) -> str: def binary2base64(binary_file: str) -> str:
"""Convert a binary file (OGG, executable, etc.) to a """Convert a binary file (OGG, executable, etc.) to a
printable string. printable string.
@ -94,130 +94,8 @@ def binary2base64(binary_file: str) -> str:
encoded_string = base64.b64encode(bin_file.read()) encoded_string = base64.b64encode(bin_file.read())
return encoded_string.decode() return encoded_string.decode()
def base642binary(b64_fname: str) -> bytes: def base642binary(b64_fname: str) -> bytes:
"""Convert a printable string to a binary file.""" """Convert a printable string to a binary file.
b64_fname += "==="
return base64.b64decode(b64_fname)
def open_image(fname_or_instance: Union[str, IO[bytes]]):
"""Opens a Image and returns it.
:param fname_or_instance: Can either be the location of the image as a
string or the Image.Image instance itself.
""" """
if isinstance(fname_or_instance, Image.Image): b64_fname += '==='
return fname_or_instance return base64.b64decode(b64_fname)
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

View file

@ -1,256 +0,0 @@
2
5
11
22
44
88
177
99
199
143
30
61
122
244
232
208
161
67
135
15
31
63
127
255
254
252
249
242
228
200
144
33
66
133
10
20
41
83
167
79
159
62
125
250
245
234
213
170
85
171
87
174
92
184
112
224
193
131
6
12
24
49
98
197
138
21
43
86
172
89
179
102
204
153
50
101
203
151
47
95
191
126
253
251
247
239
222
188
121
243
230
205
155
55
110
221
187
119
238
220
185
114
229
202
149
42
84
169
82
165
74
148
40
81
162
68
137
18
37
75
150
45
90
180
104
209
163
70
140
25
51
103
206
156
57
115
231
207
158
60
120
241
227
198
141
27
54
108
216
176
97
194
132
8
17
34
69
139
23
46
93
186
117
235
215
175
94
189
123
246
237
219
183
111
223
190
124
248
240
225
195
134
13
26
52
105
211
166
77
154
53
107
214
173
91
182
109
218
181
106
212
168
80
160
65
130
4
9
19
39
78
157
59
118
236
217
178
100
201
146
36
73
147
38
76
152
48
96
192
129
3
7
14
29
58
116
233
210
164
72
145
35
71
142
28
56
113
226
196
136
16
32
64
128
1
2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
([58, 56, 54, 57, 60, 59, 62, 61, 63, 65, 64, 66, 67, 254, 68, 69, 255, 70, 253, 71, 72, 74, 252, 73, 251, 250, 75, 249, 76, 77], [(224, 4327), (221, 4138), (223, 4057), (222, 3987), (225, 3713), (220, 3554), (209, 3516), (207, 3476), (208, 3424), (219, 3360)])

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -23,19 +25,20 @@ __date__ = "$Date: 2016/05/17 $"
__revision__ = "$Date: 2017/01/18 $" __revision__ = "$Date: 2017/01/18 $"
__license__ = "GPLv3" __license__ = "GPLv3"
import io
import os import os
import unittest import unittest
import io
from stegano import exifHeader from stegano import exifHeader
class TestEXIFHeader(unittest.TestCase): class TestEXIFHeader(unittest.TestCase):
def test_hide_empty_message(self): def test_hide_empty_message(self):
"""Test hiding the empty string.""" """Test hiding the empty string.
exifHeader.hide( """
"./tests/sample-files/20160505T130442.jpg", "./image.jpg", secret_message="" secret = exifHeader.hide("./tests/sample-files/20160505T130442.jpg",
) "./image.jpg", secret_message="")
#secret.save(""./image.png"")
clear_message = exifHeader.reveal("./image.jpg") clear_message = exifHeader.reveal("./image.jpg")
@ -45,20 +48,17 @@ class TestEXIFHeader(unittest.TestCase):
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"] messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
for message in messages_to_hide: for message in messages_to_hide:
exifHeader.hide( secret = exifHeader.hide("./tests/sample-files/20160505T130442.jpg",
"./tests/sample-files/20160505T130442.jpg", "./image.jpg", secret_message=message)
"./image.jpg",
secret_message=message,
)
clear_message = exifHeader.reveal("./image.jpg") clear_message = exifHeader.reveal("./image.jpg")
self.assertEqual(message, clear_message.decode()) self.assertEqual(message, message)
def test_with_image_without_exif_data(self): def test_with_image_without_exif_data(self):
exifHeader.hide( secret = exifHeader.hide("./tests/sample-files/Lenna.jpg",
"./tests/sample-files/Lenna.jpg", "./image.jpg", secret_message="" "./image.jpg", secret_message="")
) #secret.save(""./image.png"")
clear_message = exifHeader.reveal("./image.jpg") clear_message = exifHeader.reveal("./image.jpg")
@ -68,28 +68,27 @@ class TestEXIFHeader(unittest.TestCase):
text_file_to_hide = "./tests/sample-files/lorem_ipsum.txt" text_file_to_hide = "./tests/sample-files/lorem_ipsum.txt"
with open(text_file_to_hide, "rb") as f: with open(text_file_to_hide, "rb") as f:
message = f.read() message = f.read()
exifHeader.hide( secret = exifHeader.hide("./tests/sample-files/20160505T130442.jpg",
"./tests/sample-files/20160505T130442.jpg",
img_enc="./image.jpg", img_enc="./image.jpg",
secret_file=text_file_to_hide, secret_file=text_file_to_hide)
)
clear_message = exifHeader.reveal("./image.jpg") clear_message = exifHeader.reveal("./image.jpg")
self.assertEqual(message, clear_message) self.assertEqual(message, clear_message)
def test_with_png_image(self): def test_with_png_image(self):
exifHeader.hide( secret = exifHeader.hide("./tests/sample-files/Lenna.png",
"./tests/sample-files/Lenna.png", "./image.png", secret_message="Secret" "./image.png", secret_message="Secret")
) #secret.save(""./image.png"")
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
exifHeader.reveal("./image.png") clear_message = exifHeader.reveal("./image.png")
def test_with_bytes(self): def test_with_bytes(self):
outputBytes = io.BytesIO() outputBytes = io.BytesIO()
message = b"Secret" message = b"Secret"
with open("./tests/sample-files/20160505T130442.jpg", "rb") as f: exifHeader.hide(open("./tests/sample-files/20160505T130442.jpg", 'rb'),
exifHeader.hide(f, outputBytes, secret_message=message) outputBytes,
secret_message=message)
clear_message = exifHeader.reveal(outputBytes) clear_message = exifHeader.reveal(outputBytes)
self.assertEqual(message, clear_message) self.assertEqual(message, clear_message)
@ -97,13 +96,12 @@ class TestEXIFHeader(unittest.TestCase):
def tearDown(self): def tearDown(self):
try: try:
os.unlink("./image.jpg") os.unlink("./image.jpg")
except Exception: except:
pass pass
try: try:
os.unlink("./image.png") os.unlink("./image.png")
except Exception: except:
pass pass
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -23,179 +25,83 @@ __date__ = "$Date: 2017/03/01 $"
__revision__ = "$Date: 2017/03/01 $" __revision__ = "$Date: 2017/03/01 $"
__license__ = "GPLv3" __license__ = "GPLv3"
import itertools
import unittest import unittest
import itertools
import cv2 from stegano.lsbset import generators
import numpy as np
from stegano.lsb import generators
class TestGenerators(unittest.TestCase): class TestGenerators(unittest.TestCase):
def test_identity(self): def test_identity(self):
"""Test the identity generator.""" """Test the identity generator.
self.assertEqual( """
tuple(itertools.islice(generators.identity(), 15)), self.assertEqual(tuple(itertools.islice(generators.identity(), 15)),
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14), (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14))
)
def test_fibonacci(self): def test_fibonacci(self):
"""Test the Fibonacci generator.""" """Test the Fibonacci generator.
self.assertEqual( """
tuple(itertools.islice(generators.fibonacci(), 20)), self.assertEqual(tuple(itertools.islice(generators.fibonacci(), 20)),
( (1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610,
1, 987, 1597, 2584, 4181, 6765, 10946))
2,
3,
5,
8,
13,
21,
34,
55,
89,
144,
233,
377,
610,
987,
1597,
2584,
4181,
6765,
10946,
),
)
def test_eratosthenes(self): def test_eratosthenes(self):
"""Test the Eratosthenes sieve.""" """Test the Eratosthenes sieve.
with open("./tests/expected-results/eratosthenes") as f: """
self.assertEqual( with open('./tests/expected-results/eratosthenes', 'r') as f:
tuple(itertools.islice(generators.eratosthenes(), 168)), self.assertEqual(tuple(itertools.islice(generators.eratosthenes(), 168)),
tuple(int(line) for line in f), tuple(int(line) for line in f))
)
def test_composite(self): def test_composite(self):
"""Test the composite sieve.""" """Test the composite sieve.
with open("./tests/expected-results/composite") as f: """
self.assertEqual( with open('./tests/expected-results/composite', 'r') as f:
tuple(itertools.islice(generators.composite(), 114)), self.assertEqual(tuple(itertools.islice(generators.composite(), 114)),
tuple(int(line) for line in f), tuple(int(line) for line in f))
)
def test_fermat(self): def test_fermat(self):
"""Test the Fermat generator.""" """Test the Fermat generator.
with open("./tests/expected-results/fermat") as f: """
self.assertEqual( with open('./tests/expected-results/fermat', 'r') as f:
tuple(itertools.islice(generators.fermat(), 9)), self.assertEqual(tuple(itertools.islice(generators.fermat(), 9)),
tuple(int(line) for line in f), tuple(int(line) for line in f))
)
def test_triangular_numbers(self): def test_triangular_numbers(self):
"""Test the Triangular numbers generator.""" """Test the Triangular numbers generator.
with open("./tests/expected-results/triangular_numbers") as f: """
self.assertEqual( with open('./tests/expected-results/triangular_numbers', 'r') as f:
tuple(itertools.islice(generators.triangular_numbers(), 54)), self.assertEqual(tuple(itertools.islice(generators.triangular_numbers(), 54)),
tuple(int(line) for line in f), tuple(int(line) for line in f))
)
def test_mersenne(self): def test_mersenne(self):
"""Test the Mersenne generator.""" """Test the Mersenne generator.
with open("./tests/expected-results/mersenne") as f: """
self.assertEqual( with open('./tests/expected-results/mersenne', 'r') as f:
tuple(itertools.islice(generators.mersenne(), 20)), self.assertEqual(tuple(itertools.islice(generators.mersenne(), 20)),
tuple(int(line) for line in f), tuple(int(line) for line in f))
)
def test_carmichael(self): def test_carmichael(self):
"""Test the Carmichael generator.""" """Test the Carmichael generator.
with open("./tests/expected-results/carmichael") as f: """
self.assertEqual( with open('./tests/expected-results/carmichael', 'r') as f:
tuple(itertools.islice(generators.carmichael(), 33)), self.assertEqual(tuple(itertools.islice(generators.carmichael(), 33)),
tuple(int(line) for line in f), tuple(int(line) for line in f))
)
def test_ackermann_slow(self):
"""Test the Ackermann set."""
with open("./tests/expected-results/ackermann") as f:
self.assertEqual(generators.ackermann_slow(3, 1), int(f.readline()))
self.assertEqual(generators.ackermann_slow(3, 2), int(f.readline()))
def test_ackermann_naive(self): def test_ackermann_naive(self):
"""Test the Naive Ackermann generator""" """Test the Ackermann set.
gen = generators.ackermann_naive(3) """
next(gen)
with open("./tests/expected-results/ackermann") as f:
self.assertEqual(next(gen), int(f.readline()))
self.assertEqual(next(gen), int(f.readline()))
def test_ackermann_fast(self): self.assertEqual(generators.ackermann(3, 1), 13)
"""Test the Ackermann set.""" self.assertEqual(generators.ackermann(3, 2), 29)
with open("./tests/expected-results/ackermann") as f:
self.assertEqual(generators.ackermann_fast(3, 1), int(f.readline()))
self.assertEqual(generators.ackermann_fast(3, 2), int(f.readline()))
self.assertEqual(generators.ackermann_fast(4, 1), int(f.readline()))
def test_ackermann(self): def test_ackermann(self):
"""Test the Ackermann generator""" """Test the Ackermann set.
gen = generators.ackermann(3)
next(gen)
with open("./tests/expected-results/ackermann") as f:
self.assertEqual(next(gen), int(f.readline()))
self.assertEqual(next(gen), int(f.readline()))
def test_LFSR(self):
"""Test the LFSR generator"""
with open("./tests/expected-results/LFSR") as f:
self.assertEqual(
tuple(itertools.islice(generators.LFSR(2**8), 256)),
tuple(int(line) for line in f),
)
def test_shi_tomashi(self):
"""Test the Shi Tomashi generator"""
# The expected results are only for tests/sample-files/Montenach.png file and
# the below mentioned shi-tomashi configuration.
# If the values below are changed,
# please ensure the tests/expected-results/shi_tomashi.txt
# is also appropriately modified
# Using the shi_tomashi_reconfigure static method
image = cv2.imread("tests/sample-files/Montenach.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
corners = cv2.goodFeaturesToTrack(gray, 1000, 0.001, 10)
# Commented because min_distance argument of generators.shi_tomashi is now set
# to 10.0:
# corners = np.int0(corners)
corners = corners.reshape(corners.shape[0], -1)
test_file = np.loadtxt("tests/expected-results/shi_tomashi.txt")
test_file_reshaped = test_file.reshape(
int(test_file.shape[0]), int(test_file.shape[1])
)
res = np.testing.assert_allclose(corners, test_file_reshaped, rtol=1e-0, atol=0) # type: ignore
self.assertIsNone(res)
@staticmethod
def shi_tomashi_reconfigure(
file_name: str,
max_corners: int = 1000,
quality: float = 0.001,
min_distance: int = 10,
):
""" """
Method to update/reconfigure Shi-Tomashi for various images and configuration with open('./tests/expected-results/ackermann', 'r') as f:
""" self.assertEqual(generators.ackermann(3, 1), int(f.readline()))
image = cv2.imread(file_name) self.assertEqual(generators.ackermann(3, 2), int(f.readline()))
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) self.assertEqual(generators.ackermann(4, 1), int(f.readline()))
corners = cv2.goodFeaturesToTrack(gray, max_corners, quality, min_distance) self.assertEqual(generators.ackermann(4, 2), int(f.readline()))
# Commented because min_distance argument of generators.shi_tomashi is now set
# to 10.0:
# corners = np.int0(corners)
corners = corners.reshape(corners.shape[0], -1)
np.savetxt("tests/expected-results/shi_tomashi.txt", corners)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic 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://github.com/cedricbonhomme/Stegano
# #
@ -18,231 +20,134 @@
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme" __author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.6 $" __version__ = "$Revision: 0.3 $"
__date__ = "$Date: 2016/04/13 $" __date__ = "$Date: 2016/04/12 $"
__revision__ = "$Date: 2022/01/04 $" __revision__ = "$Date: 2017/05/04 $"
__license__ = "GPLv3" __license__ = "GPLv3"
import io import io
import os import os
import base64
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from stegano import lsb from stegano import lsb
from stegano.lsb import generators
class TestLSB(unittest.TestCase): class TestLSB(unittest.TestCase):
def test_hide_empty_message(self): def test_hide_empty_message(self):
""" """
Test hiding the empty string. Test hiding the empty string.
""" """
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
lsb.hide("./tests/sample-files/Lenna.png", "", generators.eratosthenes()) secret = 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:"] messages_to_hide = ['a', 'foo', 'Hello World!', ':Python:']
for message in messages_to_hide: for message in messages_to_hide:
secret = lsb.hide("./tests/sample-files/Lenna.png", message) secret = lsb.hide("./tests/sample-files/Lenna.png", message)
secret.save("./image.png") secret.save("./image.png")
clear_message = lsb.reveal("./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) self.assertEqual(message, clear_message)
def test_hide_and_reveal_UTF32LE(self): def test_hide_and_reveal_UTF32LE(self):
messages_to_hide = "I love 🍕 and 🍫!" messages_to_hide = 'I love 🍕 and 🍫!'
secret = lsb.hide( secret = lsb.hide("./tests/sample-files/Lenna.png",
"./tests/sample-files/Lenna.png", messages_to_hide, 'UTF-32LE')
messages_to_hide,
generators.eratosthenes(),
encoding="UTF-32LE",
)
secret.save("./image.png") secret.save("./image.png")
clear_message = lsb.reveal( clear_message = lsb.reveal("./image.png", 'UTF-32LE')
"./image.png", generators.eratosthenes(), encoding="UTF-32LE"
)
self.assertEqual(messages_to_hide, clear_message) self.assertEqual(messages_to_hide, clear_message)
def test_with_transparent_png(self): 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: for message in messages_to_hide:
secret = lsb.hide( secret = lsb.hide("./tests/sample-files/transparent.png",
"./tests/sample-files/transparent.png", message, 'UTF-32LE')
message,
generators.eratosthenes(),
)
secret.save("./image.png") secret.save("./image.png")
clear_message = lsb.reveal("./image.png", generators.eratosthenes()) clear_message = lsb.reveal("./image.png", 'UTF-32LE')
self.assertEqual(message, clear_message) self.assertEqual(message, clear_message)
@patch("builtins.input", return_value="y") @patch('builtins.input', return_value='y')
def test_manual_convert_rgb(self, input): def test_manual_convert_rgb(self, input):
message_to_hide = "Hello World!" message_to_hide = 'I love 🍕 and 🍫!'
lsb.hide( secret = lsb.hide("./tests/sample-files/Lenna-grayscale.png",
"./tests/sample-files/Lenna-grayscale.png", message_to_hide, 'UTF-32LE')
message_to_hide,
generators.eratosthenes(),
)
@patch("builtins.input", return_value="n") @patch('builtins.input', return_value='n')
def test_refuse_convert_rgb(self, input): def test_refuse_convert_rgb(self, input):
message_to_hide = "Hello World!" message_to_hide = 'I love 🍕 and 🍫!'
# lsb.hide( with self.assertRaises(Exception):
# "./tests/sample-files/Lenna-grayscale.png", secret = lsb.hide("./tests/sample-files/Lenna-grayscale.png",
# message_to_hide, message_to_hide, 'UTF-32LE')
# generators.eratosthenes(),
# )
with self.assertRaisesRegex(Exception, "Not a RGB image."):
lsb.hide(
"./tests/sample-files/Lenna-grayscale.png",
message_to_hide,
generators.eratosthenes(),
)
def test_with_location_of_image_as_argument(self):
messages_to_hide = ["Hello World!"]
for message in messages_to_hide:
outputBytes = io.BytesIO()
bytes_image = lsb.hide(
"./tests/sample-files/20160505T130442.jpg",
message,
generators.identity(),
)
bytes_image.save(outputBytes, "PNG")
outputBytes.seek(0)
clear_message = lsb.reveal(outputBytes, generators.identity())
self.assertEqual(message, clear_message)
def test_auto_convert_rgb(self): def test_auto_convert_rgb(self):
message_to_hide = "Hello World!" message_to_hide = 'I love 🍕 and 🍫!'
lsb.hide( secret = lsb.hide("./tests/sample-files/Lenna-grayscale.png",
"./tests/sample-files/Lenna-grayscale.png", message_to_hide, 'UTF-32LE', True)
message_to_hide,
generators.eratosthenes(), def test_with_text_file(self):
auto_convert_rgb=True, 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:
pass
def test_with_too_long_message(self): def test_with_too_long_message(self):
with open("./tests/sample-files/lorem_ipsum.txt") as f: with open("./tests/sample-files/lorem_ipsum.txt") as f:
message = f.read() message = f.read()
message += message * 2 message += message*2
with self.assertRaisesRegex( with self.assertRaises(Exception):
Exception, "The message you want to hide is too long:" lsb.hide("./tests/sample-files/Lenna.png", message)
):
lsb.hide("./tests/sample-files/Lenna.png", message, generators.identity())
def test_hide_and_reveal_with_bad_generator(self): def test_with_bytes(self):
message_to_hide = "Hello World!" messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
secret = lsb.hide(
"./tests/sample-files/Lenna.png", message_to_hide, generators.eratosthenes()
)
secret.save("./image.png")
with self.assertRaises(IndexError): for message in messages_to_hide:
lsb.reveal("./image.png", generators.identity()) message = "Hello World"
outputBytes = io.BytesIO()
bytes_image = lsb.hide(open("./tests/sample-files/20160505T130442.jpg", 'rb'), message)
bytes_image.save(outputBytes, "PNG")
outputBytes.seek(0)
def test_with_unknown_generator(self): clear_message = lsb.reveal(outputBytes)
message_to_hide = "Hello World!"
with self.assertRaises(AttributeError): self.assertEqual(message, clear_message)
lsb.hide(
"./tests/sample-files/Lenna.png",
message_to_hide,
generators.unknown_generator(), # type: ignore
)
def tearDown(self): def tearDown(self):
try: try:
os.unlink("./image.png") os.unlink("./image.png")
except Exception: except:
pass pass
if __name__ == "__main__": if __name__ == '__main__':
unittest.main() unittest.main()

131
tests/test_lsbset.py Normal file
View file

@ -0,0 +1,131 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.4 $"
__date__ = "$Date: 2016/04/13 $"
__revision__ = "$Date: 2017/05/04 $"
__license__ = "GPLv3"
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):
secret = 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_UTF32LE(self):
messages_to_hide = 'I love 🍕 and 🍫!'
secret = lsbset.hide("./tests/sample-files/Lenna.png",
messages_to_hide,
generators.eratosthenes(),
'UTF-32LE')
secret.save("./image.png")
clear_message = lsbset.reveal("./image.png", generators.eratosthenes(),
'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!"
secret = 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):
secret = lsbset.hide("./tests/sample-files/Lenna-grayscale.png",
message_to_hide, generators.eratosthenes())
def test_auto_convert_rgb(self):
message_to_hide = "Hello World!"
secret = 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):
clear_message = lsbset.reveal("./image.png", generators.identity())
def test_with_unknown_generator(self):
message_to_hide = "Hello World!"
with self.assertRaises(AttributeError):
secret = lsbset.hide("./tests/sample-files/Lenna.png",
message_to_hide, generators.eratosthene())
def tearDown(self):
try:
os.unlink("./image.png")
except:
pass
if __name__ == '__main__':
unittest.main()

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -27,14 +29,14 @@ import unittest
from stegano import red from stegano import red
class TestRed(unittest.TestCase): class TestRed(unittest.TestCase):
def test_hide_empty_message(self): def test_hide_empty_message(self):
""" """
Test hiding the empty string. Test hiding the empty string.
""" """
with self.assertRaises(AssertionError): 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): def test_hide_and_reveal(self):
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"] messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
@ -45,7 +47,7 @@ class TestRed(unittest.TestCase):
clear_message = red.reveal("./image.png") clear_message = red.reveal("./image.png")
self.assertEqual(message, clear_message) self.assertEqual(message, message)
def test_with_too_long_message(self): def test_with_too_long_message(self):
with open("./tests/sample-files/lorem_ipsum.txt") as f: with open("./tests/sample-files/lorem_ipsum.txt") as f:
@ -56,9 +58,9 @@ class TestRed(unittest.TestCase):
def tearDown(self): def tearDown(self):
try: try:
os.unlink("./image.png") os.unlink("./image.png")
except Exception: except:
pass pass
if __name__ == "__main__": if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -1,66 +0,0 @@
#!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information : https://github.com/cedricbonhomme/Stegano
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.9.4 $"
__date__ = "$Date: 2019/06/06 $"
__revision__ = "$Date: 2019/06/06 $"
__license__ = "GPLv3"
import unittest
from PIL import Image, ImageChops
from stegano import lsb
from stegano.steganalysis import parity, statistics
class TestSteganalysis(unittest.TestCase):
def test_parity(self):
"""Test stegano.steganalysis.parity"""
text_file_to_hide = "./tests/sample-files/lorem_ipsum.txt"
with open(text_file_to_hide) as f:
message = f.read()
secret = lsb.hide("./tests/sample-files/Lenna.png", message)
analysis = parity.steganalyse(secret)
target = Image.open("./tests/expected-results/parity.png")
diff = ImageChops.difference(target, analysis).getbbox()
self.assertTrue(diff is None)
def test_parity_rgba(self):
"""Test that stegano.steganalysis.parity works with RGBA images"""
img = Image.open("./tests/sample-files/transparent.png")
analysis = parity.steganalyse(img)
target = Image.open("./tests/expected-results/parity_rgba.png")
diff = ImageChops.difference(target, analysis).getbbox()
self.assertTrue(diff is None)
def test_statistics(self):
"""Test stegano.steganalysis.statistics"""
image = Image.open("./tests/sample-files/Lenna.png")
stats = str(statistics.steganalyse(image)) + "\n"
file = open("./tests/expected-results/statistics")
target = file.read()
file.close()
self.assertEqual(stats, target)
if __name__ == "__main__":
unittest.main()

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# Stegano - Stegano is a pure Python steganography module. #-*- coding: utf-8 -*-
# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org
# #
# For more information : https://github.com/cedricbonhomme/Stegano # For more information : https://github.com/cedricbonhomme/Stegano
# #
@ -23,65 +25,54 @@ __date__ = "$Date: 2017/02/22 $"
__revision__ = "$Date: 2017/02/22 $" __revision__ = "$Date: 2017/02/22 $"
__license__ = "GPLv3" __license__ = "GPLv3"
import os
import unittest import unittest
import io
from stegano import tools from stegano import tools
class TestTools(unittest.TestCase): class TestTools(unittest.TestCase):
def test_a2bits(self): def test_a2bits(self):
bits = tools.a2bits("Hello World!") bits = tools.a2bits("Hello World!")
self.assertEqual( self.assertEqual(bits, '010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001')
bits,
"010010000110010101101100011011000110111100100000010101110110111101110010011011000110010000100001",
)
def test_a2bits_list_UTF8(self): def test_a2bits_list_UTF8(self):
list_of_bits = tools.a2bits_list("Hello World!") list_of_bits = tools.a2bits_list("Hello World!")
self.assertEqual( self.assertEqual(list_of_bits, ['01001000',
list_of_bits, '01100101',
[ '01101100',
"01001000", '01101100',
"01100101", '01101111',
"01101100", '00100000',
"01101100", '01010111',
"01101111", '01101111',
"00100000", '01110010',
"01010111", '01101100',
"01101111", '01100100',
"01110010", '00100001'])
"01101100",
"01100100",
"00100001",
],
)
def test_a2bits_list_UTF32LE(self): def test_a2bits_list_UTF32LE(self):
list_of_bits = tools.a2bits_list("Hello World!", "UTF-32LE") list_of_bits = tools.a2bits_list("Hello World!", 'UTF-32LE')
self.assertEqual( self.assertEqual(list_of_bits, ['00000000000000000000000001001000',
list_of_bits, '00000000000000000000000001100101',
[ '00000000000000000000000001101100',
"00000000000000000000000001001000", '00000000000000000000000001101100',
"00000000000000000000000001100101", '00000000000000000000000001101111',
"00000000000000000000000001101100", '00000000000000000000000000100000',
"00000000000000000000000001101100", '00000000000000000000000001010111',
"00000000000000000000000001101111", '00000000000000000000000001101111',
"00000000000000000000000000100000", '00000000000000000000000001110010',
"00000000000000000000000001010111", '00000000000000000000000001101100',
"00000000000000000000000001101111", '00000000000000000000000001100100',
"00000000000000000000000001110010", '00000000000000000000000000100001'])
"00000000000000000000000001101100",
"00000000000000000000000001100100",
"00000000000000000000000000100001",
],
)
def test_n_at_a_time(self): def test_n_at_a_time(self):
result = tools.n_at_a_time([1, 2, 3, 4, 5], 2, "X") result = tools.n_at_a_time([1, 2, 3, 4, 5], 2, 'X')
self.assertEqual(list(result), [(1, 2), (3, 4), (5, "X")]) self.assertEqual(list(result), [(1, 2), (3, 4), (5, 'X')])
def test_binary2base64(self): 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() expected_value = f.read()
value = tools.binary2base64("tests/sample-files/free-software-song.ogg") value = tools.binary2base64('tests/sample-files/free-software-song.ogg')
self.assertEqual(expected_value, value) self.assertEqual(expected_value, value)

18
tools/run_mypy.py Normal file
View file

@ -0,0 +1,18 @@
import subprocess
import sys
modules = ["stegano/tools.py",
"stegano/lsb/lsb.py",
"stegano/lsbset/lsbset.py",
"stegano/lsbset/generators.py",
"stegano/red/red.py",
"stegano/exifHeader/exifHeader.py",
"stegano/steganalysis/parity.py",
"stegano/steganalysis/statistics.py"]
exit_codes = []
for module in modules:
rc = subprocess.call(["mypy", "--ignore-missing-imports",
"--follow-imports", "skip", module])
exit_codes.append(rc)
sys.exit(max(exit_codes))