|
#!/usr/bin/python3 |
|
# -*- coding: UTF-8 -*- |
|
from PIL import Image |
|
from tinyscript import * |
|
|
|
|
|
__author__ = "Alexandre D'Hondt" |
|
__version__ = "1.0" |
|
__copyright__ = ("A. D'Hondt", 2026) |
|
__license__ = "gpl-3.0" |
|
__examples__ = ["extract image.png -l 892"] |
|
__doc__ = """ |
|
*StegoEMD* allows to apply steganography based on EMD (Exploiting Modification Direction) in order to retrieve hidden |
|
data from an image. |
|
""" |
|
|
|
|
|
BANNER_FONT = "standard" |
|
BANNER_STYLE = {'fgcolor': "lolcat"} |
|
SCRIPTNAME_FORMAT = "none" |
|
|
|
|
|
class EMD: |
|
""" Original EMD algorithm (Zhang & Wang, 2006) """ |
|
def __init__(self, image, mode="RGB", channel=None, **kw): |
|
self.__image = image |
|
self.__channel = channel |
|
self.__mode = mode |
|
self.__obj = Image.open(image).convert(mode) |
|
logger.debug("Image size: {}x{}".format(*self.__obj.size)) |
|
try: |
|
self.__channel_idx = str(self.__mode).index(self.__channel) |
|
except: |
|
self.__channel_idx = -1 |
|
|
|
def __pixv(self, pixel): |
|
return pixel[self.__channel_idx] if isinstance(pixel, (list, tuple)) else pixel |
|
|
|
def extract(self, length=1024, write=False, **kw): |
|
digits, p, i = [], list(self.__obj.getdata()), -2 |
|
for k in range(3*length): |
|
i += 2 |
|
g1, g2 = self.__pixv(p[i]), self.__pixv(p[i+1]) |
|
digits.append((g1 + 2 * g2) % 5) |
|
out = bytearray() |
|
for i in range(0, len(digits) // 3 * 3, 3): |
|
out.append(digits[i] * 25 + digits[i+1] * 5 + digits[i+2]) |
|
data = out.decode("latin1", errors="replace") |
|
logger.info(f"Hidden data:\n{data}") |
|
if write: |
|
with open(self.secret, 'ab') as f: |
|
f.write(b(data)) |
|
logger.warning(f"Secret saved to '{self.secret}'") |
|
|
|
def hide(self, data, **kw): |
|
if os.path.exists(data): |
|
with open(data, 'rb') as f: |
|
data = f.read() |
|
digits = [] |
|
for byte in b(data): |
|
digits.extend([byte // 25, (byte // 5) % 5, byte % 5]) |
|
p = list(self.__obj.getdata()) |
|
if len(digits) > (capacity := len(p) // 2): |
|
logger.error(f"Message too large ({len(digits)} digits, capacity={capacity})") |
|
return |
|
for pair_index, target_digit in enumerate(digits): |
|
p1 = 2 * pair_index |
|
p2 = p1 + 1 |
|
g1, g2 = self.__pixv(p[p1]), self.__pixv(p[p2]) |
|
current = (g1 + 2 * g2) % 5 |
|
# Already encodes the desired digit |
|
if current == target_digit: |
|
continue |
|
delta = (target_digit - current) % 5 |
|
# Zhang & Wang modification table |
|
if delta == 1: |
|
if g1 < 255: |
|
g1 += 1 |
|
else: |
|
g1 -= 1 |
|
elif delta == 2: |
|
if g2 < 255: |
|
g2 += 1 |
|
else: |
|
g2 -= 1 |
|
elif delta == 3: |
|
if g2 > 0: |
|
g2 -= 1 |
|
else: |
|
g2 += 1 |
|
elif delta == 4: |
|
if g1 > 0: |
|
g1 -= 1 |
|
else: |
|
g1 += 1 |
|
if isinstance(p[p1], (list, tuple)): |
|
pix1, pix2 = list(p[p1]), list(p[p2]) |
|
pix1[self.__channel_idx], pix2[self.__channel_idx] = g1, g2 |
|
p[p1], p[p2] = tuple(pix1), tuple(pix2) |
|
else: |
|
p[p1], p[p2] = g1, g2 |
|
img = Image.new(self.__mode, self.__obj.size) |
|
img.putdata(p) |
|
img.save(self.image) |
|
logger.warning(f"Image saved to '{self.image}'") |
|
return self |
|
|
|
@property |
|
def basename(self): |
|
return os.path.splitext(os.path.basename(self.__image))[0] |
|
|
|
@property |
|
def image(self): |
|
return f"{self.basename}-hidden.png" |
|
|
|
@property |
|
def secret(self): |
|
return f"{self.basename}-secret.txt" |
|
|
|
|
|
if __name__ == "__main__": |
|
subparsers = parser.add_subparsers(help="commands", dest="command") |
|
extract = subparsers.add_parser('extract', help="extract hidden data from the target image") |
|
hide = subparsers.add_parser('hide', help="hide data in the target image") |
|
extract.add_argument("image", type=ts.file_exists, help="image path") |
|
extract.add_argument("-c", "--channel", help="channel to be considered (if relevant)", |
|
note="e.g. use 'A' when using mode 'RGBA' or 'B' if using 'RGB'") |
|
extract.add_argument("-l", "--length", default=1024, type=int, help="number of EMD digits to extract") |
|
extract.add_argument("-m", "--mode", default="RGB", help="image mode") |
|
extract.add_argument("-w", "--write", action="store_true", help="write data to a file") |
|
hide.add_argument("image", type=ts.file_exists, help="image path") |
|
hide.add_argument("data", help="data to be hidden") |
|
initialize(noargs_action="demo") |
|
getattr(EMD(**vars(args)), args.command)(**vars(args)) |